Compare commits

...

114 commits

Author SHA1 Message Date
flash 44a4bb6e6f Prevent access to private messages when impersonating a user. 2024-06-02 19:57:58 +00:00
flash ec00cfa176 Base64 encode PM titles and bodies in the database.
To prevent personal discomfort with having to do database messages and seeing people's personal conversations.
I haven't run into it yet, but I'd rather avoid it altogether.
2024-06-02 19:54:33 +00:00
flash 1d295df8da Added broom closet PM stats. 2024-06-02 19:43:57 +00:00
flash 6a88ed8b11 Updated libraries. 2024-05-30 22:02:09 +00:00
flash 36bcf1ab1d Built Playpen icon updating into Misuzu.
Was previously handled by a stinky script.
2024-05-30 22:00:41 +00:00
flash 5d3e1d4960 Fixed wrong HTTP verb. 2024-03-30 15:22:11 +00:00
flash 9bb943bacf Fixed various oversights. 2024-03-30 03:19:08 +00:00
flash 107d16cf46 Updated Misuzu to new HTTP router. 2024-03-30 03:14:03 +00:00
flash 0afc5186a7 Fixed error when trying to access a topic with no posts associated. 2024-02-24 22:03:32 +00:00
flash 0300bae994 hurr 2024-02-21 00:31:25 +00:00
flash cb0c64f8ed Stinky fix for impersonation in chat auth. 2024-02-20 23:56:43 +00:00
flash 89ef9d9ad1 Fixed bans no longer working. 2024-02-15 22:55:24 +00:00
flash c02d922dc6 Fixed Forum Activity section always showing up. 2024-02-13 21:22:56 +00:00
flash 80cd6222c4 Fixed profile fields not showing up anymore. 2024-02-11 02:22:22 +00:00
flash 344a3c9160 Missed one! 2024-02-09 16:07:43 +00:00
flash df5dbdf3ad Fixed forum/topic breadcrumbs. 2024-02-08 15:20:44 +00:00
flash c0caceed7b Fixed use of wrong BanInfo constructor. 2024-02-08 15:18:57 +00:00
flash be54ce2c22 Fixed oversights on landing page. 2024-02-08 00:06:23 +00:00
flash 070dc5e782 Added lazy database object creation. 2024-02-07 00:04:45 +00:00
flash b89621cb1a Added PMs to data export. 2024-02-05 22:56:51 +00:00
flash 760cca0e5d whoops 2024-02-02 21:53:36 +00:00
flash fe77f1616c Updated to new EEPROM script. 2024-02-02 21:42:40 +00:00
flash eb81ed7a82 Added notice when recipient is banned. 2024-02-02 02:16:37 +00:00
flash 8ef11afe02 Check if recipient is actually able to receive messages. 2024-02-02 02:07:29 +00:00
flash cca016ba10 Prevent banned users from sending messages. 2024-02-02 01:59:21 +00:00
flash b80151583e Added private messages. 2024-01-30 23:47:02 +00:00
flash d8cc208a85 Use accent-color and color-scheme CSS directives. 2024-01-25 18:17:54 +00:00
flash 4b2f9a2fec Fixed Ctrl+Enter submission not working anymore either. 2024-01-25 00:18:56 +00:00
flash ddb255bf32 Fixed forum post form throwing up the navigation confirmation when it isn't supposed to. 2024-01-25 00:12:53 +00:00
flash 5a70e3f3f1 Include SameSite attribute on cookies. 2024-01-24 22:14:48 +00:00
flash bd3e055323 Rewrote Javascript code. 2024-01-24 21:53:26 +00:00
flash dba5754ccc Fixed error when trying to add a new change. 2024-01-24 18:28:13 +00:00
flash ec6ba3f781 Imported new asset build script. 2024-01-24 18:24:40 +00:00
flash 70ec285f99 Added links to Amimami repositories. 2024-01-18 20:31:08 +00:00
flash 77eadd5bde Adjusted CORS handling for emoticon endpoint. 2024-01-17 19:57:46 +00:00
flash f0fc735975 Updated browserlists. 2024-01-08 13:43:34 +00:00
flash adb80bad9e Added server side image map support. 2024-01-08 13:42:22 +00:00
flash f30cf41f86 Ported boolean attribute support. 2024-01-08 13:36:47 +00:00
flash b4f5dd0660 Removed broken CONSTRAINT from perms table creation. 2023-12-16 18:51:17 +00:00
flash 133e2f420c Fixed markdown styling issues. 2023-12-15 12:56:08 +00:00
flash bf65c95490 Updated highlight.js and created new code theme. 2023-12-15 12:47:01 +00:00
flash 7ef5994da4 Updated Sentry library to 4.0 in Misuzu. 2023-12-15 01:03:57 +00:00
flash 2b34bde413 Fixed error when trying to create a new role. 2023-12-02 02:57:46 +00:00
flash 432615508d Fixed undefined variable. 2023-11-26 22:23:47 +00:00
flash a4cc14e4c1 Libraries have been updated once more. 2023-11-20 19:10:47 +00:00
flash 65e695e9d9 git.flash.moe -> patchii.net 2023-11-20 19:04:59 +00:00
flash 2e6a84b46d Updated source.md. 2023-11-09 20:58:56 +00:00
flash 8f56174637 Supply super user status in auth data. 2023-11-07 14:38:53 +00:00
flash 19fbe59ddd Return to purple. 2023-11-01 09:36:49 +00:00
flash f7a571e551 moguu? 2023-10-21 23:54:41 +00:00
flash 5f57e3fdf4 Use SharpConfig format for the pre-database config. 2023-10-21 23:45:40 +00:00
flash c2836719c7 Updated to use Syokuhou config library. 2023-10-20 22:29:28 +00:00
flash 14c9a1d9f6 Fixed oversight on members list. 2023-10-18 10:16:32 +00:00
flash 4f1e35b566 Fixed overly eager url encoding on the search page. 2023-10-18 10:11:21 +00:00
flash 9aa2a1431e Enable Spookii 2023-10-01 18:44:59 +00:00
flash 4322f2561c Fixed chat routes being broken. 2023-09-11 20:36:20 +00:00
flash 67d9620037 Fixed legacy paths being too / tolerant. 2023-09-11 20:15:48 +00:00
flash 904d220582 Fixed router related explosions. 2023-09-11 20:10:37 +00:00
flash d9b152fb78 Fixed oversight on memberlist. 2023-09-11 19:19:19 +00:00
flash a945cc518a Fixed syntax error in post.php. 2023-09-11 19:18:10 +00:00
flash edc64b45ff Fixed error when trying to view a non-existent topic when logged out. 2023-09-10 21:04:10 +00:00
flash 17e0d1f591 Added Sentry error logging on the server side. 2023-09-10 20:46:58 +00:00
flash 5554c5c28d Removed unused pagination helper function. 2023-09-10 20:12:27 +00:00
flash 55e23c7b5d Fixed CSRF tokens not being added to URLs that need them. 2023-09-10 20:02:11 +00:00
flash e376671136 Attempt at fixing forum issues. 2023-09-10 19:13:36 +00:00
flash 3e49f6e503 Added URL registry attributes. 2023-09-10 00:04:53 +00:00
flash 7db43a2acd Revert "チルノの日"
This reverts commit 099bd899ed.
2023-09-09 11:54:33 +00:00
flash 099bd899ed チルノの日 2023-09-08 23:07:37 +00:00
flash 1248c0d2f6 Moved various .php file redirects into the LegacyRouter. 2023-09-08 20:47:54 +00:00
flash c3bed1c0e3 Rewrote URL registry. 2023-09-08 20:40:48 +00:00
flash 163da8b213 Added separate context class for forum stuff and split up handling of each object type. 2023-09-08 13:22:46 +00:00
flash c68279add9 Cleaned up some things I missed. 2023-09-08 01:05:17 +00:00
flash 737c99280e Make PHPStan happy. 2023-09-08 00:54:19 +00:00
flash 8b0f960c86 Split auth stuff off into own context. 2023-09-08 00:43:00 +00:00
flash c5a284f360 Route registration with attributes! 2023-09-08 00:13:30 +00:00
flash 506d32d210 Fixed incorrect type on latest forum post fetching result. 2023-09-07 20:53:19 +00:00
flash 498ec0cf9a Merge SharpChat permission set into the Misuzu permission system directly. 2023-09-06 20:44:28 +00:00
flash 15e96684c2 Moved authentication related macros out of MisuzuContext. 2023-09-06 20:06:07 +00:00
flash 73e4597e16 Rewrote Satori recent forum post fetch. 2023-09-06 19:35:50 +00:00
flash 9b2c409a24 Moved user related stuff into its own context object. 2023-09-06 13:50:19 +00:00
flash 7190a5f4df Syntactic sugar for mass route registration. 2023-09-06 11:59:44 +00:00
flash 5c67d49225 Fixed edit display threshold. 2023-09-06 11:32:13 +00:00
flash 69e4d05be6 Pluralise Views. 2023-09-06 11:19:54 +00:00
flash 2d0f083e1a Fixed topic read status check. 2023-09-06 11:19:04 +00:00
flash 1da6470928 Switch to Sasae. 2023-08-31 21:33:34 +00:00
flash 9682fa595a Fixed static analysis detections. 2023-08-31 17:14:41 +00:00
flash c14195c4c3 Moved render_info and render_error into Template class. 2023-08-31 15:59:53 +00:00
flash 45500ce698 Removed html_colour function, moved renamed DateCheck to Tools and moved the country names function into it and use new callable syntax. 2023-08-31 14:55:39 +00:00
flash 0c9bac473b No longer rely on Referer header for the comments return URL. 2023-08-31 14:39:50 +00:00
flash 061d4c8a8f Fixed leaderboard name not retaining the leading 0. 2023-08-31 00:54:17 +00:00
flash 6fc10984e1 Append total posts count at the end of the leaderboard. 2023-08-31 00:52:14 +00:00
flash e222009dd0 Fixed oversight. 2023-08-31 00:40:07 +00:00
flash 85b629bc08 Fixed missing use statement. 2023-08-31 00:38:20 +00:00
flash 16ea495c7a Added permission for displaying load timings in the footer. 2023-08-31 00:37:09 +00:00
flash ad3fe74275 Removed old database backend. 2023-08-31 00:31:11 +00:00
flash 29426fafc1 Count profile stats using Index database backend. 2023-08-31 00:24:59 +00:00
flash 4d6fb64f3a Added shitty search hack to users class. 2023-08-31 00:19:20 +00:00
flash 40558ceb39 Added targeted permission recalculation.
Reduces reliance on full recalculation and actually makes it viable to do from within the browser.
2023-08-30 23:56:33 +00:00
flash f03c8ebfa5 Moved validation methods into the new Users class. 2023-08-30 23:41:44 +00:00
flash 07a2868159 Rewrote permissions system. 2023-08-30 22:37:21 +00:00
flash ca23822e40 Fixed errors on profiles. 2023-08-28 14:45:32 +00:00
flash 34bd71600a Removed manage.php. 2023-08-28 13:45:36 +00:00
flash 5bab957a7c Fixed user colours in comments sections. 2023-08-28 13:33:39 +00:00
flash 57b9e82c10 Fixed topic type string usage. 2023-08-28 01:41:13 +00:00
flash 460a0ca57d Fixed user colours not showing on forum posts. 2023-08-28 01:32:05 +00:00
flash 39c6269cf3 Rewrote forum backend. 2023-08-28 01:17:34 +00:00
flash fb41c71ee9 Fixed emoticon ordering in chat. 2023-08-07 12:59:08 +00:00
flash 2214dffc5b Fixed profile editing failing due to old argument. 2023-08-06 19:09:59 +00:00
flash bab8b29c5b Fixed error 500 when trying to log in to a non-existing user. 2023-08-06 18:22:39 +00:00
flash 0a11c5525a Fixed oversight regarding RNG ordering of user list. 2023-08-05 13:55:34 +00:00
flash d4f6990e8a Made data source argument lists for News, Changelog, Comments and Emotes consistent with the rest. 2023-08-05 13:50:15 +00:00
flash 87915b6a25 Fixed forum post deletion and editing. 2023-08-04 22:49:09 +00:00
flash cf71129153 Converted all Misuzu style route handlers to Index style ones. 2023-08-04 20:51:02 +00:00
flash 6bfa3d7238 Fixed error 500 when viewing profiles as guest. 2023-08-04 17:44:37 +00:00
287 changed files with 16976 additions and 10617 deletions

6
.gitignore vendored
View file

@ -11,6 +11,8 @@
/composer.local.json /composer.local.json
# Configuration # Configuration
/config/config.cfg
/config/github.cfg
/config/config.ini /config/config.ini
/config/github.ini /config/github.ini
/.debug /.debug
@ -43,3 +45,7 @@
# Google # Google
/public/robots.txt /public/robots.txt
# moguu?
/public/moguu.swf
/public/moguu.html

View file

@ -1,4 +1,4 @@
Copyright (c) 2017-2023, flashwave <me@flash.moe> Copyright (c) 2017-2024, flashwave <me@flash.moe>
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -2,6 +2,6 @@
> Misuzu can and will steal your lunch money. > Misuzu can and will steal your lunch money.
## Requirements ## Requirements
- PHP 8.2 - PHP 8.2 (64-bit)
- MariaDB 10.6 - MariaDB 10.6
- [Composer](https://getcomposer.org/) - [Composer](https://getcomposer.org/)

View file

@ -17,15 +17,10 @@ exports.process = async function(root, options) {
return ''; return '';
included.push(fullPath); included.push(fullPath);
if(!fullPath.startsWith(root)) { if(!fullPath.startsWith(root))
console.error('INVALID PATH: ' + fullPath);
return '/* *** INVALID PATH: ' + fullPath + ' */'; return '/* *** INVALID PATH: ' + fullPath + ' */';
} if(!fs.existsSync(fullPath))
if(!fs.existsSync(fullPath)) {
console.error('FILE NOT FOUND: ' + fullPath);
return '/* *** FILE NOT FOUND: ' + fullPath + ' */'; return '/* *** FILE NOT FOUND: ' + fullPath + ' */';
}
const lines = readline.createInterface({ const lines = readline.createInterface({
input: fs.createReadStream(fullPath), input: fs.createReadStream(fullPath),
@ -58,6 +53,19 @@ exports.process = async function(root, options) {
break; break;
} }
case 'buildvars':
if(typeof options.buildVars === 'object') {
const bvTarget = options.buildVarsTarget || 'window';
const bvProps = [];
for(const bvName in options.buildVars)
bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])}, writable: false }`);
if(Object.keys(bvProps).length > 0)
output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`;
}
break;
default: default:
output += line; output += line;
output += "\n"; output += "\n";
@ -84,7 +92,7 @@ exports.housekeep = function(assetsPath) {
}; };
}).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name); }).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name);
const regex = /^(.+)-([a-f0-9]+)\.(.+)$/i; const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i;
const counts = {}; const counts = {};
for(const fileName of files) { for(const fileName of files) {

View file

@ -154,6 +154,9 @@
} }
.forum__post__action { .forum__post__action {
background-color: transparent;
border: 0;
display: block;
padding: 5px 10px; padding: 5px 10px;
margin: 1px; margin: 1px;
color: inherit; color: inherit;

View file

@ -146,9 +146,14 @@
} }
.header__desktop__user__button__count { .header__desktop__user__button__count {
position: absolute; position: absolute;
bottom: 1px; top: -5px;
right: 1px; right: -3px;
font-size: 10px; z-index: 1;
font-size: .5em;
line-height: 1.4em;
text-align: right;
padding: 2px 2px 0;
border-radius: 4px;
background-color: var(--header-accent-colour); background-color: var(--header-accent-colour);
opacity: .9; opacity: .9;
border-radius: 4px; border-radius: 4px;

View file

@ -0,0 +1,92 @@
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
font-size: 1.2em;
font-family: var(--font-monospace);
}
code.hljs {
padding: 2px 5px;
}
.hljs {
color: #eee;
background: #121212;
}
.hljs-strong,
.hljs-emphasis,
.hljs-section {
font-weight: 700;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal {
color: #b2b376;
}
.hljs-code {
background-color: #242424;
}
.hljs-comment,
.hljs-meta,
.hljs-emphasis,
.hljs-stronge,
.hljs-type,
.hljs-attribute,
.hljs-params {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-section,
.hljs-symbol,
.hljs-name {
color: #9475b2;
}
.hljs-built_in,
.hljs-subst,
.hljs-tag,
.hljs-title,
.hljs-selector-attr {
color: #c8b9d7;
}
.hljs-variable,
.hljs-class .hljs-title,
.hljs-selector-class,
.hljs-selector-id,
.hljs-selector-pseudo {
color: #b37fae;
}
.hljs-string {
color: #76b38a;
}
.hljs-type,
.hljs-template-tag,
.hljs-template-variable,
.hljs-link {
color: #b39a76;
}
.hljs-comment,
.hljs-meta {
color: #70647b;
}
.hljs-addition {
background: #0e4d0e;
}
.hljs-deletion {
background: #4d0e0e;
}

View file

@ -3,7 +3,6 @@
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
outline-style: none;
} }
html, html,
@ -57,6 +56,8 @@ body {
html { html {
scrollbar-color: var(--accent-colour) var(--background-colour); scrollbar-color: var(--accent-colour) var(--background-colour);
accent-color: var(--accent-colour);
color-scheme: dark;
} }
.main { .main {
@ -117,6 +118,8 @@ html {
@include permissions.css; @include permissions.css;
@include warning.css; @include warning.css;
@include hljs.css;
@include _input/button.css; @include _input/button.css;
@include _input/checkbox.css; @include _input/checkbox.css;
@include _input/colour.css; @include _input/colour.css;
@ -161,6 +164,8 @@ html {
@include manage/_manage.css; @include manage/_manage.css;
@include messages/messages.css;
@include news/container.css; @include news/container.css;
@include news/feeds.css; @include news/feeds.css;
@include news/list.css; @include news/list.css;

View file

@ -53,7 +53,6 @@
.markdown code { .markdown code {
padding: .2em .4em; padding: .2em .4em;
margin: 0; margin: 0;
background-color: rgba(0, 0, 0, .7);
border-radius: 2px; border-radius: 2px;
} }
.markdown del code { text-decoration: inherit; } .markdown del code { text-decoration: inherit; }
@ -65,7 +64,6 @@
overflow: hidden; overflow: hidden;
line-height: inherit; line-height: inherit;
word-wrap: break-word; word-wrap: break-word;
background: transparent;
border: 0; border: 0;
} }

View file

@ -17,4 +17,5 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 5px; padding: 5px;
gap: 5px;
} }

View file

@ -0,0 +1,37 @@
.messages-actions-item {
display: flex;
align-items: center;
height: 30px;
margin: 1px;
font-size: 1.3em;
line-height: 1.4em;
color: #fff;
text-decoration: none;
transition: background-color .1s;
width: 100%;
border: 0;
background-color: inherit;
text-align: left;
}
.messages-actions-item:hover,
.messages-actions-item:focus {
background-color: #444f;
}
.messages-actions-item:active,
.messages-actions-item-current {
background-color: var(--accent-colour) !important;
}
.messages-actions-item[disabled] {
background-color: inherit !important;
opacity: .4;
}
.messages-actions-item-icon {
text-align: center;
width: 30px;
flex-grow: 0;
flex-shrink: 0;
}
.messages-actions-item-label {
flex-grow: 1;
flex-shrink: 1;
}

View file

@ -0,0 +1,26 @@
.messages-columns {
display: flex;
gap: 2px;
}
.messages-columns-sidebar {
width: 200px;
flex-shrink: 0;
flex-grow: 0;
}
.messages-columns-content {
flex-shrink: 1;
flex-grow: 1;
overflow: hidden;
}
@media (max-width: 800px) {
.messages-columns {
flex-direction: column;
}
.messages-columns-sidebar {
width: 100%;
}
}

View file

@ -0,0 +1,80 @@
.messages-entry {
color: inherit;
text-decoration: none;
display: flex;
flex-direction: column;
padding: 2px 4px;
gap: 4px;
overflow: hidden;
cursor: pointer;
}
.messages-entry-header {
display: flex;
font-size: 1.1em;
line-height: 1.6em;
border-bottom: 2px solid #9999;
gap: 2px;
}
.messages-entry-check {
flex-grow: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
}
.messages-entry-check input {
display: block;
}
.messages-entry-unread {
flex-grow: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
}
.messages-entry-unread-orb {
width: 8px;
height: 8px;
background-color: var(--accent-colour);
border-radius: 100%;
}
.messages-entry-author {
font-weight: bold;
border-bottom: 2px solid var(--user-colour, currentColor);
margin: 0 0 -2px;
flex-grow: 0;
flex-shrink: 1;
overflow: hidden;
white-space: nowrap;
}
.messages-entry-spacing {
flex-grow: 1;
flex-shrink: 1;
}
.messages-entry-datetime {
flex-grow: 0;
flex-shrink: 0;
color: #aaa;
align-self: flex-end;
}
.messages-entry-subject {
line-height: 1.4em;
color: #fff;
overflow: hidden;
}
.messages-entry-preview {
line-height: 1.4em;
color: #888;
overflow: hidden;
}
.messages-entry-preview .messages-entry-overflow {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.messages-entry-overflow {
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,33 @@
.messages-folder {
margin: 1px;
display: flex;
flex-direction: column;
gap: 1px;
padding: 1px;
}
.messages-folder-item {
background-color: #161616;
transition: background-color .1s;
}
.messages-folder-item:nth-child(2n) {
background-color: #1f1f1f;
}
.messages-folder-item:hover,
.messages-folder-item:focus {
background-color: #262626;
}
.messages-folder-item:active,
.messages-folder-item-current {
background-color: var(--accent-colour) !important;
}
.messages-folder-notice {
text-align: center;
margin: 10px;
}
.messages-folder-notice-text {
font-size: 1.4em;
line-height: 1.5em;
}
.messages-folder .pagination {
margin-top: 2px;
}

View file

@ -0,0 +1,135 @@
.messages-message {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
}
.messages-message-snippet {
cursor: pointer;
font-size: .9em;
line-height: 1.5em;
color: #888;
gap: 5px;
opacity: .8;
transition: opacity .1s;
}
.messages-message-snippet:hover,
.messages-message-snippet:focus,
.messages-message-snippet:focus-within {
opacity: 1;
}
.messages-message-draft {
border-top: 2px solid var(--accent-colour) !important;
border-left: 2px solid var(--accent-colour) !important;
border-right: 2px solid var(--accent-colour);
border-bottom: 2px solid var(--accent-colour);
}
.messages-message-deleted {
border-top: 2px solid red;
border-left: 2px solid red;
border-right: 2px solid red !important;
border-bottom: 2px solid red !important;
}
.messages-message-overflow {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.messages-message-header {
display: flex;
gap: 10px;
border-bottom: 1px #444 solid;
padding-bottom: 10px;
align-items: center;
}
.messages-message-sender-avatar {
flex-shrink: 0;
flex-grow: 0;
width: 40px;
height: 40px;
}
.messages-message-sender-avatar img {
object-fit: cover;
}
.messages-message-details {
display: flex;
flex-direction: column;
flex-shrink: 1;
flex-grow: 1;
overflow: hidden;
gap: 2px;
}
.messages-message-details-spacing {
flex-grow: 1;
flex-shrink: 1;
}
.messages-message-header-columns {
display: flex;
gap: 2px;
}
.messages-message-sender-name {
flex-grow: 0;
flex-shrink: 1;
overflow: hidden;
white-space: nowrap;
}
.messages-message-sender-name a {
color: inherit;
text-decoration: none;
font-weight: 700;
border-bottom: 2px solid var(--user-colour, currentColor);
}
.messages-message-datetime {
flex-shrink: 0;
flex-grow: 0;
align-self: flex-end;
padding-bottom: 2px;
}
.messages-message-addressee {
display: flex;
gap: 4px;
}
.messages-message-addressee-to {
flex-shrink: 0;
flex-grow: 0;
}
.messages-message-addressee-user {
flex-shrink: 1;
flex-grow: 0;
overflow: hidden;
white-space: nowrap;
}
.messages-message-addressee-user a {
color: inherit;
text-decoration: none;
font-weight: 700;
border-bottom: 2px solid var(--user-colour, currentColor);
}
.messages-message-subject {
line-height: 2em;
}
.messages-message-body {
line-height: 1.4em;
}
.messages-message-body p:first-child {
margin-top: 0 !important;
}
.messages-message-body p:last-child {
margin-bottom: 0 !important;
}
.messages-message-snippet-body {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4em;
}

View file

@ -0,0 +1,9 @@
@include messages/actions.css;
@include messages/columns.css;
@include messages/entry.css;
@include messages/folder.css;
@include messages/message.css;
@include messages/recipient.css;
@include messages/reply.css;
@include messages/sidebar.css;
@include messages/thread.css;

View file

@ -0,0 +1,17 @@
.messages-recipient {
display: flex;
flex-direction: column;
}
.messages-recipient-avatar {
display: flex;
justify-content: center;
padding: 10px;
}
.messages-recipient-name {
padding: 5px;
}
.messages-recipient-name-input {
width: 100%;
}

View file

@ -0,0 +1,52 @@
.messages-reply-form {
display: flex;
flex-direction: column;
width: 100%;
gap: 5px;
padding: 5px;
}
.messages-reply-subject-input {
width: 100%;
}
.messages-reply-body-input {
min-width: 100%;
max-width: 100%;
min-height: 100px;
}
.messages-reply-compose .messages-reply-body-input {
min-height: 300px;
}
.messages-reply-actions {
display: flex;
padding: 1px;
gap: 1px;
}
.messages-reply-action {
background-color: transparent;
border: 0;
display: block;
padding: 5px 10px;
color: inherit;
text-decoration: none;
transition: background-color .2s;
border-radius: 3px;
cursor: pointer;
}
.messages-reply-action:hover,
.messages-reply-action:focus {
background-color: rgba(0, 0, 0, .2);
}
.messages-reply-options {
display: flex;
align-items: center;
justify-content: space-between;
}
.messages-reply-settings {
display: flex;
align-items: center;
gap: 5px;
}

View file

@ -0,0 +1,11 @@
.messages-sidebar {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.messages-sidebar-button {
text-align: center;
padding: 10px;
}

View file

@ -0,0 +1,5 @@
.messages-thread {
display: flex;
flex-direction: column;
gap: 1px;
}

40
assets/misuzu.js/csrfp.js Normal file
View file

@ -0,0 +1,40 @@
#include utility.js
const MszCSRFP = (() => {
let elem;
const getElement = () => {
if(elem === undefined)
elem = $q('meta[name="csrfp-token"]');
return elem;
};
const getToken = () => {
const elem = getElement();
return typeof elem.content === 'string' ? elem.content : '';
};
const setToken = token => {
if(typeof token !== 'string')
throw 'token must be a string';
const elem = getElement();
if(typeof elem.content === 'string')
elem.content = token;
};
return {
getToken: getToken,
setToken: setToken,
setFromHeaders: result => {
if(typeof result.headers !== 'function')
throw 'result.headers is not a function';
const headers = result.headers();
if(!(headers instanceof Map))
throw 'result of result.headers does not return a map';
if(headers.has('x-csrfp-token'))
setToken(headers.get('x-csrfp-token'));
},
};
})();

View file

@ -1,128 +0,0 @@
#include utils.js
#include uiharu.js
#include aembed.js
#include iembed.js
#include vembed.js
var MszEmbed = (function() {
let uiharu = undefined;
return {
init: function(endPoint) {
uiharu = new Uiharu(endPoint);
},
handle: function(targets) {
if(!Array.isArray(targets))
targets = Array.from(targets);
const filtered = new Map;
for(const target of targets) {
if(!(target instanceof HTMLElement)
|| !('dataset' in target)
|| !('mszEmbedUrl' in target.dataset))
continue;
const cleanUrl = target.dataset.mszEmbedUrl.replace(/ /, '%20');
if(cleanUrl.indexOf('https://') !== 0
&& cleanUrl.indexOf('http://') !== 0
&& cleanUrl.indexOf('//') !== 0) {
target.textContent = target.dataset.mszEmbedUrl;
continue;
}
$rc(target);
target.appendChild($e({
tag: 'i',
attrs: {
className: 'fas fa-2x fa-spinner fa-pulse',
style: {
width: '32px',
height: '32px',
lineHeight: '32px',
textAlign: 'center',
},
},
}));
if(filtered.has(cleanUrl))
filtered.get(cleanUrl).push(target);
else
filtered.set(cleanUrl, [target]);
}
const replaceWithUrl = function(targets, url) {
for(const target of targets) {
let body = $e({
tag: 'a',
attrs: {
className: 'link',
href: url,
target: '_blank',
rel: 'noopener noreferrer',
},
child: url
});
$ib(target, body);
$r(target);
}
};
filtered.forEach(function(targets, url) {
uiharu.lookupOne(url, function(metadata) {
if(metadata.error) {
replaceWithUrl(targets, url);
console.error(metadata.error);
return;
}
if(metadata.title === undefined) {
replaceWithUrl(targets, url);
console.warn('Media is no longer available.');
return;
}
let phc = undefined,
options = {
onembed: console.log,
};
if(metadata.type === 'youtube:video') {
phc = MszVideoEmbedPlaceholder;
options.type = 'youtube';
options.player = MszVideoEmbedYouTube;
} else if(metadata.type === 'niconico:video') {
phc = MszVideoEmbedPlaceholder;
options.type = 'nicovideo';
options.player = MszVideoEmbedNicoNico;
} else if(metadata.is_video) {
phc = MszVideoEmbedPlaceholder;
options.type = 'external';
options.player = MszVideoEmbedPlayer;
//options.frame = MszVideoEmbedFrame;
options.nativeControls = true;
options.autosize = false;
options.maxWidth = 640;
options.maxHeight = 360;
} else if(metadata.is_audio) {
phc = MszAudioEmbedPlaceholder;
options.type = 'external';
options.player = MszAudioEmbedPlayer;
options.nativeControls = true;
} else if(metadata.is_image) {
phc = MszImageEmbed;
options.type = 'external';
}
if(phc === undefined)
return;
for(const target of targets) {
const placeholder = new phc(metadata, options, target);
if(placeholder !== undefined)
placeholder.replaceElement(target);
}
});
});
},
};
})();

View file

@ -1,4 +1,4 @@
#include utils.js #include utility.js
#include watcher.js #include watcher.js
const MszAudioEmbedPlayerEvents = function() { const MszAudioEmbedPlayerEvents = function() {
@ -56,7 +56,7 @@ const MszAudioEmbedPlayer = function(metadata, options) {
if(haveNativeControls) if(haveNativeControls)
playerAttrs.controls = 'controls'; playerAttrs.controls = 'controls';
const watchers = new MszWatcherCollection; const watchers = new MszWatchers;
watchers.define(MszAudioEmbedPlayerEvents()); watchers.define(MszAudioEmbedPlayerEvents());
const player = $e({ const player = $e({
@ -84,7 +84,8 @@ const MszAudioEmbedPlayer = function(metadata, options) {
getType: function() { return 'external'; }, getType: function() { return 'external'; },
}; };
watchers.proxy(pub); pub.watch = (name, handler) => watchers.watch(name, handler);
pub.unwatch = (name, handler) => watchers.unwatch(name, handler);
player.addEventListener('play', function() { watchers.call('play', pub); }); player.addEventListener('play', function() { watchers.call('play', pub); });

View file

@ -0,0 +1,135 @@
#include utility.js
#include embed/audio.js
#include embed/image.js
#include embed/video.js
#include ext/uiharu.js
const MszEmbed = (function() {
let uiharu = undefined;
return {
init: function(endPoint) {
uiharu = new MszUiharu(endPoint);
},
handle: function(targets) {
if(!Array.isArray(targets))
targets = Array.from(targets);
const filtered = new Map;
for(const target of targets) {
if(!(target instanceof HTMLElement)
|| !('dataset' in target)
|| !('mszEmbedUrl' in target.dataset))
continue;
const cleanUrl = target.dataset.mszEmbedUrl.replace(/ /, '%20');
if(cleanUrl.indexOf('https://') !== 0
&& cleanUrl.indexOf('http://') !== 0
&& cleanUrl.indexOf('//') !== 0) {
target.textContent = target.dataset.mszEmbedUrl;
continue;
}
$rc(target);
target.appendChild($e({
tag: 'i',
attrs: {
className: 'fas fa-2x fa-spinner fa-pulse',
style: {
width: '32px',
height: '32px',
lineHeight: '32px',
textAlign: 'center',
},
},
}));
if(filtered.has(cleanUrl))
filtered.get(cleanUrl).push(target);
else
filtered.set(cleanUrl, [target]);
}
const replaceWithUrl = function(targets, url) {
for(const target of targets) {
let body = $e({
tag: 'a',
attrs: {
className: 'link',
href: url,
target: '_blank',
rel: 'noopener noreferrer',
},
child: url
});
$ib(target, body);
$r(target);
}
};
filtered.forEach(function(targets, url) {
uiharu.lookupOne(url)
.catch(ex => {
replaceWithUrl(targets, url);
console.error(ex);
})
.then(result => {
const metadata = result.body();
if(metadata.error) {
replaceWithUrl(targets, url);
console.error(metadata.error);
return;
}
if(metadata.title === undefined) {
replaceWithUrl(targets, url);
console.warn('Media is no longer available.');
return;
}
let phc = undefined,
options = {
onembed: console.log,
};
if(metadata.type === 'youtube:video') {
phc = MszVideoEmbedPlaceholder;
options.type = 'youtube';
options.player = MszVideoEmbedYouTube;
} else if(metadata.type === 'niconico:video') {
phc = MszVideoEmbedPlaceholder;
options.type = 'nicovideo';
options.player = MszVideoEmbedNicoNico;
} else if(metadata.is_video) {
phc = MszVideoEmbedPlaceholder;
options.type = 'external';
options.player = MszVideoEmbedPlayer;
//options.frame = MszVideoEmbedFrame;
options.nativeControls = true;
options.autosize = false;
options.maxWidth = 640;
options.maxHeight = 360;
} else if(metadata.is_audio) {
phc = MszAudioEmbedPlaceholder;
options.type = 'external';
options.player = MszAudioEmbedPlayer;
options.nativeControls = true;
} else if(metadata.is_image) {
phc = MszImageEmbed;
options.type = 'external';
}
if(phc === undefined)
return;
for(const target of targets) {
const placeholder = new phc(metadata, options, target);
if(placeholder !== undefined)
placeholder.replaceElement(target);
}
});
});
},
};
})();

View file

@ -1,4 +1,4 @@
#include utils.js #include utility.js
const MszImageEmbed = function(metadata, options, target) { const MszImageEmbed = function(metadata, options, target) {
options = options || {}; options = options || {};

View file

@ -1,5 +1,5 @@
#include utils.js #include utility.js
#include rng.js #include uniqstr.js
#include watcher.js #include watcher.js
const MszVideoEmbedPlayerEvents = function() { const MszVideoEmbedPlayerEvents = function() {
@ -229,7 +229,7 @@ const MszVideoEmbedPlayer = function(metadata, options) {
videoAttrs.style.width = initialSize[0].toString() + 'px'; videoAttrs.style.width = initialSize[0].toString() + 'px';
videoAttrs.style.height = initialSize[1].toString() + 'px'; videoAttrs.style.height = initialSize[1].toString() + 'px';
const watchers = new MszWatcherCollection; const watchers = new MszWatchers;
watchers.define(MszVideoEmbedPlayerEvents()); watchers.define(MszVideoEmbedPlayerEvents());
const player = $e({ const player = $e({
@ -265,12 +265,13 @@ const MszVideoEmbedPlayer = function(metadata, options) {
getHeight: function() { return height; }, getHeight: function() { return height; },
}; };
watchers.proxy(pub); pub.watch = (name, handler) => watchers.watch(name, handler);
pub.unwatch = (name, handler) => watchers.unwatch(name, handler);
if(shouldObserveResize) if(shouldObserveResize)
player.addEventListener('resize', function() { setSize(player.videoWidth, player.videoHeight); }); player.addEventListener('resize', function() { setSize(player.videoWidth, player.videoHeight); });
player.addEventListener('play', function() { watchers.call('play', pub); }); player.addEventListener('play', function() { watchers.call('play'); });
const pPlay = function() { player.play(); }; const pPlay = function() { player.play(); };
pub.play = pPlay; pub.play = pPlay;
@ -280,7 +281,7 @@ const MszVideoEmbedPlayer = function(metadata, options) {
let stopCalled = false; let stopCalled = false;
player.addEventListener('pause', function() { player.addEventListener('pause', function() {
watchers.call(stopCalled ? 'stop' : 'pause', pub); watchers.call(stopCalled ? 'stop' : 'pause');
stopCalled = false; stopCalled = false;
}); });
@ -301,9 +302,9 @@ const MszVideoEmbedPlayer = function(metadata, options) {
player.addEventListener('volumechange', function() { player.addEventListener('volumechange', function() {
if(lastMuteState !== player.muted) { if(lastMuteState !== player.muted) {
lastMuteState = player.muted; lastMuteState = player.muted;
watchers.call('mute', pub, [lastMuteState]); watchers.call('mute', lastMuteState);
} else } else
watchers.call('volume', pub, [player.volume]); watchers.call('volume', player.volume);
}); });
const pSetMuted = function(state) { player.muted = state; }; const pSetMuted = function(state) { player.muted = state; };
@ -319,21 +320,21 @@ const MszVideoEmbedPlayer = function(metadata, options) {
pub.getPlaybackRate = pGetPlaybackRate; pub.getPlaybackRate = pGetPlaybackRate;
player.addEventListener('ratechange', function() { player.addEventListener('ratechange', function() {
watchers.call('rate', pub, [player.playbackRate]); watchers.call('rate', player.playbackRate);
}); });
const pSetPlaybackRate = function(rate) { player.playbackRate = rate; }; const pSetPlaybackRate = function(rate) { player.playbackRate = rate; };
pub.setPlaybackRate = pSetPlaybackRate; pub.setPlaybackRate = pSetPlaybackRate;
window.addEventListener('durationchange', function() { window.addEventListener('durationchange', function() {
watchers.call('duration', pub, [player.duration]); watchers.call('duration', player.duration);
}); });
const pGetDuration = function() { return player.duration; }; const pGetDuration = function() { return player.duration; };
pub.getDuration = pGetDuration; pub.getDuration = pGetDuration;
window.addEventListener('timeupdate', function() { window.addEventListener('timeupdate', function() {
watchers.call('time', pub, [player.currentTime]); watchers.call('time', player.currentTime);
}); });
const pGetTime = function() { return player.currentTime; }; const pGetTime = function() { return player.currentTime; };
@ -374,7 +375,7 @@ const MszVideoEmbedYouTube = function(metadata, options) {
currentTime = undefined, currentTime = undefined,
isPlaying = undefined; isPlaying = undefined;
const watchers = new MszWatcherCollection; const watchers = new MszWatchers;
watchers.define(MszVideoEmbedPlayerEvents()); watchers.define(MszVideoEmbedPlayerEvents());
const player = $e({ const player = $e({
@ -410,7 +411,8 @@ const MszVideoEmbedYouTube = function(metadata, options) {
getPlayerId: function() { return playerId; }, getPlayerId: function() { return playerId; },
}; };
watchers.proxy(pub); pub.watch = (name, handler) => watchers.watch(name, handler);
pub.unwatch = (name, handler) => watchers.unwatch(name, handler);
const postMessage = function(data) { const postMessage = function(data) {
player.contentWindow.postMessage(JSON.stringify(data), ytOrigin); player.contentWindow.postMessage(JSON.stringify(data), ytOrigin);
@ -463,33 +465,33 @@ const MszVideoEmbedYouTube = function(metadata, options) {
lastPlayerState = state; lastPlayerState = state;
if(eventName !== undefined && eventName !== lastPlayerStateEvent) { if(eventName !== undefined && eventName !== lastPlayerStateEvent) {
lastPlayerStateEvent = eventName; lastPlayerStateEvent = eventName;
watchers.call(eventName, pub); watchers.call(eventName);
} }
}; };
const handleMuted = function(muted) { const handleMuted = function(muted) {
isMuted = muted; isMuted = muted;
watchers.call('mute', pub, [isMuted]); watchers.call('mute', isMuted);
}; };
const handleVolume = function(value) { const handleVolume = function(value) {
volume = value / 100; volume = value / 100;
watchers.call('volume', pub, [volume]); watchers.call('volume', volume);
}; };
const handleRate = function(rate) { const handleRate = function(rate) {
playbackRate = rate; playbackRate = rate;
watchers.call('rate', pub, [playbackRate]); watchers.call('rate', playbackRate);
}; };
const handleDuration = function(time) { const handleDuration = function(time) {
duration = time; duration = time;
watchers.call('duration', pub, [duration]); watchers.call('duration', duration);
}; };
const handleTime = function(time) { const handleTime = function(time) {
currentTime = time; currentTime = time;
watchers.call('time', pub, [currentTime]); watchers.call('time', currentTime);
}; };
const handlePresetRates = function(rates) { const handlePresetRates = function(rates) {
@ -574,7 +576,7 @@ const MszVideoEmbedNicoNico = function(metadata, options) {
currentTime = undefined, currentTime = undefined,
isPlaying = false; isPlaying = false;
const watchers = new MszWatcherCollection; const watchers = new MszWatchers;
watchers.define(MszVideoEmbedPlayerEvents()); watchers.define(MszVideoEmbedPlayerEvents());
const player = $e({ const player = $e({
@ -610,7 +612,8 @@ const MszVideoEmbedNicoNico = function(metadata, options) {
getPlayerId: function() { return playerId; }, getPlayerId: function() { return playerId; },
}; };
watchers.proxy(pub); pub.watch = (name, handler) => watchers.watch(name, handler);
pub.unwatch = (name, handler) => watchers.unwatch(name, handler);
const postMessage = function(name, data) { const postMessage = function(name, data) {
if(name === undefined) if(name === undefined)
@ -660,28 +663,28 @@ const MszVideoEmbedNicoNico = function(metadata, options) {
if(eventName !== undefined && eventName !== lastPlayerStateEvent) { if(eventName !== undefined && eventName !== lastPlayerStateEvent) {
lastPlayerStateEvent = eventName; lastPlayerStateEvent = eventName;
watchers.call(eventName, pub); watchers.call(eventName);
} }
}; };
const handleMuted = function(muted) { const handleMuted = function(muted) {
isMuted = muted; isMuted = muted;
watchers.call('mute', pub, [isMuted]); watchers.call('mute', isMuted);
}; };
const handleVolume = function(value) { const handleVolume = function(value) {
volume = value; volume = value;
watchers.call('volume', pub, [volume]); watchers.call('volume', volume);
}; };
const handleDuration = function(time) { const handleDuration = function(time) {
duration = time / 1000; duration = time / 1000;
watchers.call('duration', pub, [duration]); watchers.call('duration', duration);
}; };
const handleTime = function(time) { const handleTime = function(time) {
currentTime = time / 1000; currentTime = time / 1000;
watchers.call('time', pub, [currentTime]); watchers.call('time', currentTime);
}; };
const metadataHanders = { const metadataHanders = {

View file

@ -1,35 +1,49 @@
#include utils.js #include utility.js
Misuzu.Events.Christmas2019 = function() { const MszChristmas2019EventInfo = function() {
this.propName = propName = 'msz-christmas-' + (new Date).getFullYear().toString(); return {
isActive: () => {
const d = new Date;
return d.getMonth() === 11 && d.getDate() > 5 && d.getDate() < 27;
},
dispatch: () => {
const impl = new MszChristmas2019Event;
impl.dispatch();
return impl;
},
};
}; };
Misuzu.Events.Christmas2019.prototype.changeColour = function() {
var count = parseInt(localStorage.getItem(this.propName)); const MszChristmas2019Event = function() {
document.body.style.setProperty('--header-accent-colour', (count++ % 2) ? 'green' : 'red'); const propName = 'msz-christmas-' + (new Date).getFullYear().toString();
localStorage.setItem(this.propName, count.toString()); const headerBg = $q('.header__background');
}; const menuBgs = Array.from($qa('.header__desktop__submenu__background'));
Misuzu.Events.Christmas2019.prototype.isActive = function() {
var d = new Date; if(!localStorage.getItem(propName))
return d.getMonth() === 11 && d.getDate() > 5 && d.getDate() < 27; localStorage.setItem(propName, '0');
};
Misuzu.Events.Christmas2019.prototype.dispatch = function() { const changeColour = () => {
var headerBg = $q('.header__background'), let count = parseInt(localStorage.getItem(propName));
menuBgs = $qa('.header__desktop__submenu__background'); document.body.style.setProperty('--header-accent-colour', (count++ % 2) ? 'green' : 'red');
localStorage.setItem(propName, count.toString());
if(!localStorage.getItem(this.propName)) };
localStorage.setItem(this.propName, '0');
return {
if(headerBg) changeColour: changeColour,
headerBg.style.transition = 'background-color .4s'; dispatch: () => {
if(headerBg)
setTimeout(function() { headerBg.style.transition = 'background-color .4s';
if(headerBg)
headerBg.style.transition = 'background-color 1s'; setTimeout(() => {
if(headerBg)
for(var i = 0; i < menuBgs.length; i++) headerBg.style.transition = 'background-color 1s';
menuBgs[i].style.transition = 'background-color 1s';
}, 1000); for(const menuBg of menuBgs)
menuBg.style.transition = 'background-color 1s';
this.changeColour(); }, 1000);
setInterval(this.changeColour, 10000);
changeColour();
setInterval(changeColour, 10000);
},
};
}; };

View file

@ -1,15 +1,15 @@
Misuzu.Events = {}; const MszSeasonalEvents = function() {
const events = [];
#include events/christmas2019.js return {
add: eventInfo => {
Misuzu.Events.getList = function() { if(!events.includes(eventInfo))
return [ events.push(eventInfo);
new Misuzu.Events.Christmas2019, },
]; dispatch: () => {
}; for(const info of events)
Misuzu.Events.dispatch = function() { if(info.isActive())
var list = Misuzu.Events.getList(); info.dispatch();
for(var i = 0; i < list.length; ++i) },
if(list[i].isActive()) };
list[i].dispatch();
}; };

View file

@ -0,0 +1,37 @@
#include utility.js
const MszEEPROM = (() => {
let eepromScript;
return {
init: () => {
return new Promise((resolve, reject) => {
if(eepromScript !== undefined) {
resolve(false);
return;
}
if(typeof peepPath !== 'string') {
reject();
return;
}
const scriptElem = $e({
tag: 'script',
attrs: {
src: `${peepPath}/scripts/eepromv1a.js`,
charset: 'utf-8',
type: 'text/javascript',
onerror: () => reject(),
onload: () => {
eepromScript = scriptElem;
resolve(true);
},
},
});
document.body.appendChild(scriptElem);
});
},
};
})();

View file

@ -1,4 +1,4 @@
const Sakuya = (function() { const MszSakuya = (function() {
const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }); const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
const divisions = [ const divisions = [
{ amount: 60, name: 'seconds' }, { amount: 60, name: 'seconds' },

View file

@ -0,0 +1,15 @@
#include utility.js
const MszUiharu = function(apiUrl) {
const maxBatchSize = 4;
const lookupOneUrl = apiUrl + '/metadata';
return {
lookupOne: async targetUrl => {
if(typeof targetUrl !== 'string')
throw 'targetUrl must be a string';
return $x.post(lookupOneUrl, { type: 'json' }, targetUrl);
},
};
};

View file

@ -1,437 +0,0 @@
#include forum/forum.js
Misuzu.Forum.Editor = {};
Misuzu.Forum.Editor.allowWindowClose = false;
Misuzu.Forum.Editor.init = function() {
const postingForm = $q('.js-forum-posting');
if(!postingForm)
return;
const postingButtons = postingForm.querySelector('.js-forum-posting-buttons'),
postingText = postingForm.querySelector('.js-forum-posting-text'),
postingParser = postingForm.querySelector('.js-forum-posting-parser'),
postingPreview = postingForm.querySelector('.js-forum-posting-preview'),
postingMode = postingForm.querySelector('.js-forum-posting-mode'),
previewButton = document.createElement('button'),
bbcodeButtons = $q('.forum__post__actions--bbcode'),
markdownButtons = $q('.forum__post__actions--markdown'),
markupButtons = $qa('.forum__post__action--tag');
// Initialise EEPROM, code sucks ass but it's getting nuked soon again anyway
if(typeof peepPath === 'string')
document.body.appendChild($e({
tag: 'script',
attrs: {
src: peepPath + '/eeprom.js',
charset: 'utf-8',
type: 'text/javascript',
onload: function() {
const eepromClient = new EEPROM(peepApp, peepPath + '/uploads', '');
const eepromHistory = $e({
attrs: {
className: 'eeprom-widget-history-items',
},
});
const eepromHandleFileUpload = function(file) {
const uploadElemNameValue = $e({
attrs: {
className: 'eeprom-widget-file-name-value',
title: file.name,
},
child: file.name,
});
const uploadElemName = $e({
tag: 'a',
attrs: {
className: 'eeprom-widget-file-name',
target: '_blank',
},
child: uploadElemNameValue,
});
const uploadElemProgressText = $e({
attrs: {
className: 'eeprom-widget-file-progress',
},
child: 'Please wait...',
});
const uploadElemProgressBarValue = $e({
attrs: {
className: 'eeprom-widget-file-bar-fill',
style: {
width: '0%',
},
},
});
const uploadElem = $e({
attrs: {
className: 'eeprom-widget-file',
},
child: [
{
attrs: {
className: 'eeprom-widget-file-info',
},
child: [
uploadElemName,
uploadElemProgressText,
],
},
{
attrs: {
className: 'eeprom-widget-file-bar',
},
child: uploadElemProgressBarValue,
},
],
});
if(eepromHistory.children.length > 0)
$ib(eepromHistory.firstChild, uploadElem);
else
eepromHistory.appendChild(uploadElem);
const explodeUploadElem = function() {
$r(uploadElem);
};
const uploadTask = eepromClient.createUpload(file);
uploadTask.onProgress = function(progressInfo) {
const progressValue = progressInfo.progress.toString() + '%';
uploadElemProgressBarValue.style.width = progressValue;
uploadElemProgressText.textContent = progressValue + ' (' + (progressInfo.total - progressInfo.loaded).toString() + ' bytes remaining)';
};
uploadTask.onFailure = function(errorInfo) {
if(!errorInfo.userAborted) {
let errorText = 'Was unable to upload file.';
switch(errorInfo.error) {
case EEPROM.ERR_INVALID:
errorText = 'Upload request was invalid.';
break;
case EEPROM.ERR_AUTH:
errorText = 'Upload authentication failed, refresh and try again.';
break;
case EEPROM.ERR_ACCESS:
errorText = 'You\'re not allowed to upload files.';
break;
case EEPROM.ERR_GONE:
errorText = 'Upload client has a configuration error or the server is gone.';
break;
case EEPROM.ERR_DMCA:
errorText = 'This file has been uploaded before and was removed for copyright reasons, you cannot upload this file.';
break;
case EEPROM.ERR_SERVER:
errorText = 'Upload server returned a critical error, try again later.';
break;
case EEPROM.ERR_SIZE:
if(errorInfo.maxSize < 1)
errorText = 'Selected file is too large.';
else {
const _t = ['bytes', 'KB', 'MB', 'GB', 'TB'],
_i = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))),
_s = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2);
errorText = 'Upload may not be larger than %1 %2.'.replace('%1', _s).replace('%2', _t[_i]);
}
break;
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
Misuzu.showMessageBox(errorText, 'Upload Error');
}
};
uploadTask.onComplete = function(fileInfo) {
uploadElem.classList.add('eeprom-widget-file-done');
uploadElemName.href = fileInfo.url;
uploadElemProgressText.textContent = '';
const insertTheLinkIntoTheBoxEx2 = function() {
const parserMode = parseInt(postingParser.value);
let insertText = location.protocol + fileInfo.url;
if(parserMode == 1) { // bbcode
if(fileInfo.isImage())
insertText = '[img]' + fileInfo.url + '[/img]';
else if(fileInfo.isAudio())
insertText = '[audio]' + fileInfo.url + '[/audio]';
else if(fileInfo.isVideo())
insertText = '[video]' + fileInfo.url + '[/video]';
} else if(parserMode == 2) { // markdown
if(fileInfo.isMedia())
insertText = '![](' + fileInfo.url + ')';
}
$insertTags(postingText, insertText, '');
postingText.value = postingText.value.trim();
};
uploadElemProgressText.appendChild($e({
tag: 'a',
attrs: {
href: 'javascript:void(0);',
onclick: function() { insertTheLinkIntoTheBoxEx2(); },
},
child: 'Insert',
}));
uploadElemProgressText.appendChild($t(' '));
uploadElemProgressText.appendChild($e({
tag: 'a',
attrs: {
href: 'javascript:void(0);',
onclick: function() {
eepromClient.deleteUpload(fileInfo).start();
explodeUploadElem();
},
},
child: 'Delete',
}));
insertTheLinkIntoTheBoxEx2();
};
uploadTask.start();
};
const eepromFormInput = $e({
tag: 'input',
attrs: {
type: 'file',
multiple: 'multiple',
className: 'eeprom-widget-form-input',
onchange: function(ev) {
const files = this.files;
for(const file of files)
eepromHandleFileUpload(file);
this.value = '';
},
},
});
const eepromForm = $e({
tag: 'label',
attrs: {
className: 'eeprom-widget-form',
},
child: [
eepromFormInput,
{
attrs: {
className: 'eeprom-widget-form-text',
},
child: 'Select Files...',
}
],
});
const eepromWidget = $e({
attrs: {
className: 'eeprom-widget',
},
child: [
eepromForm,
{
attrs: {
className: 'eeprom-widget-history',
},
child: eepromHistory,
},
],
});
postingForm.appendChild(eepromWidget);
postingText.addEventListener('paste', function(ev) {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
ev.preventDefault();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
document.body.addEventListener('dragenter', function(ev) {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', function(ev) {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', function(ev) {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', function(ev) {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
},
onerror: function(ev) {
console.error('Failed to initialise EEPROM: ', ev);
},
},
}));
// hack: don't prompt user when hitting submit, really need to make this not stupid.
postingButtons.firstElementChild.addEventListener('click', function() {
Misuzu.Forum.Editor.allowWindowClose = true;
});
window.addEventListener('beforeunload', function(ev) {
if(!Misuzu.Forum.Editor.allowWindowClose && postingText.value.length > 0) {
ev.preventDefault();
ev.returnValue = '';
}
});
for(var i = 0; i < markupButtons.length; ++i)
(function(currentBtn) {
currentBtn.addEventListener('click', function(ev) {
$insertTags(postingText, currentBtn.dataset.tagOpen, currentBtn.dataset.tagClose);
});
})(markupButtons[i]);
Misuzu.Forum.Editor.switchButtons(parseInt(postingParser.value));
var lastPostText = '',
lastPostParser = null;
postingParser.addEventListener('change', function() {
var postParser = parseInt(postingParser.value);
Misuzu.Forum.Editor.switchButtons(postParser);
if(postingPreview.hasAttribute('hidden'))
return;
// dunno if this would even be possible, but ech
if(postParser === lastPostParser)
return;
postingParser.setAttribute('disabled', 'disabled');
previewButton.setAttribute('disabled', 'disabled');
previewButton.classList.add('input__button--busy');
Misuzu.Forum.Editor.renderPreview(postParser, lastPostText, function(success, text) {
if(!success) {
Misuzu.showMessageBox(text);
return;
}
postingPreview.classList[postParser == 2 ? 'add' : 'remove']('markdown');
lastPostParser = postParser;
postingPreview.innerHTML = text;
MszEmbed.handle($qa('.js-msz-embed-media'));
previewButton.removeAttribute('disabled');
postingParser.removeAttribute('disabled');
previewButton.classList.remove('input__button--busy');
});
});
previewButton.className = 'input__button';
previewButton.textContent = 'Preview';
previewButton.type = 'button';
previewButton.value = 'preview';
previewButton.addEventListener('click', function() {
if(previewButton.value === 'back') {
postingPreview.setAttribute('hidden', 'hidden');
postingText.removeAttribute('hidden');
previewButton.value = 'preview';
previewButton.textContent = 'Preview';
postingMode.textContent = postingMode.dataset.original;
postingMode.dataset.original = null;
} else {
var postText = postingText.value,
postParser = parseInt(postingParser.value);
if(lastPostText === postText && lastPostParser === postParser) {
postingPreview.removeAttribute('hidden');
postingText.setAttribute('hidden', 'hidden');
previewButton.value = 'back';
previewButton.textContent = 'Edit';
postingMode.dataset.original = postingMode.textContent;
postingMode.textContent = 'Previewing';
return;
}
postingParser.setAttribute('disabled', 'disabled');
previewButton.setAttribute('disabled', 'disabled');
previewButton.classList.add('input__button--busy');
Misuzu.Forum.Editor.renderPreview(postParser, postText, function(success, text) {
if(!success) {
Misuzu.showMessageBox(text);
return;
}
postingPreview.classList[postParser == 2 ? 'add' : 'remove']('markdown');
lastPostText = postText;
lastPostParser = postParser;
postingPreview.innerHTML = text;
MszEmbed.handle($qa('.js-msz-embed-media'));
postingPreview.removeAttribute('hidden');
postingText.setAttribute('hidden', 'hidden');
previewButton.value = 'back';
previewButton.textContent = 'Back';
previewButton.removeAttribute('disabled');
postingParser.removeAttribute('disabled');
previewButton.classList.remove('input__button--busy');
postingMode.dataset.original = postingMode.textContent;
postingMode.textContent = 'Previewing';
});
}
});
postingButtons.insertBefore(previewButton, postingButtons.firstChild);
};
Misuzu.Forum.Editor.switchButtons = function(parser) {
var bbcodeButtons = $q('.forum__post__actions--bbcode'),
markdownButtons = $q('.forum__post__actions--markdown');
bbcodeButtons.hidden = parser != 1;
markdownButtons.hidden = parser != 2;
};
Misuzu.Forum.Editor.renderPreview = function(parser, text, callback) {
if(!callback)
return;
parser = parseInt(parser);
text = text || '';
var xhr = new XMLHttpRequest,
formData = new FormData;
formData.append('post[mode]', 'preview');
formData.append('post[text]', text);
formData.append('post[parser]', parser.toString());
xhr.addEventListener('readystatechange', function() {
if(xhr.readyState !== XMLHttpRequest.DONE)
return;
if(xhr.status === 200)
callback(true, xhr.response);
else
callback(false, 'Failed to render preview.');
});
// need to figure out a url registry system again, current one is too much overhead so lets just do this for now
xhr.open('POST', '/forum/posting.php');
xhr.withCredentials = true;
xhr.send(formData);
};

View file

@ -0,0 +1,291 @@
#include msgbox.jsx
#include parsing.js
#include utility.js
#include ext/eeprom.js
let MszForumEditorAllowClose = false;
const MszForumEditor = function(form) {
if(!(form instanceof Element))
throw 'form must be an instance of element';
const buttonsElem = form.querySelector('.js-forum-posting-buttons'),
textElem = form.querySelector('.js-forum-posting-text'),
parserElem = form.querySelector('.js-forum-posting-parser'),
previewElem = form.querySelector('.js-forum-posting-preview'),
modeElem = form.querySelector('.js-forum-posting-mode'),
markupActs = form.querySelector('.js-forum-posting-actions');
let lastPostText = '',
lastPostParser;
MszEEPROM.init()
.catch(() => console.error('Failed to initialise EEPROM'))
.then(() => {
const eepromClient = new EEPROM(peepApp, peepPath);
const eepromHistory = <div class="eeprom-widget-history-items"/>;
const eepromHandleFileUpload = async file => {
const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>;
const uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>;
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
const uploadElemProgressBarValue = <div class="eeprom-widget-file-bar-fill" style={{ width: '0%' }}/>;
const uploadElem = <div class="eeprom-widget-file">
<div class="eeprom-widget-file-info">
{uploadElemName}
{uploadElemProgressText}
</div>
<div class="eeprom-widget-file-bar">
{uploadElemProgressBarValue}
</div>
</div>;
if(eepromHistory.children.length > 0)
$ib(eepromHistory.firstChild, uploadElem);
else
eepromHistory.appendChild(uploadElem);
const explodeUploadElem = () => $r(uploadElem);
const uploadTask = eepromClient.create(file);
uploadTask.onProgress(prog => {
uploadElemProgressBarValue.style.width = `${Math.ceil(prog.progress * 100)}%`;
uploadElemProgressText.textContent = `${prog.progress.toLocaleString(undefined, { style: 'percent' })} (${prog.total - prog.loaded} bytes remaining)`;
});
try {
const fileInfo = await uploadTask.start();
uploadElem.classList.add('eeprom-widget-file-done');
uploadElemName.href = fileInfo.url;
uploadElemProgressText.textContent = '';
const insertTheLinkIntoTheBoxEx2 = () => {
const parserMode = parseInt(parserElem.value);
let insertText = location.protocol + fileInfo.url;
if(parserMode == 1) { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserMode == 2) { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
}
$insertTags(textElem, insertText, '');
textElem.value = textElem.value.trim();
};
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
uploadElemProgressText.appendChild($t(' '));
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
eepromClient.delete(fileInfo)
.then(() => explodeUploadElem())
.catch(ex => {
console.error(ex);
MszShowMessageBox(ex, 'Upload Error');
});
}}>Delete</a>);
insertTheLinkIntoTheBoxEx2();
} catch(ex) {
let errorText = 'Upload aborted.';
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
await MszShowMessageBox(errorText, 'Upload Error');
}
};
const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input"
onchange={() => {
const files = eepromFormInput.files;
for(const file of files)
eepromHandleFileUpload(file);
eepromFormInput.value = '';
}}/>;
const eepromForm = <label class="eeprom-widget-form">
{eepromFormInput}
<div class="eeprom-widget-form-text">
Select Files...
</div>
</label>;
const eepromWidget = <div class="eeprom-widget">
{eepromForm}
<div class="eeprom-widget-history">
{eepromHistory}
</div>
</div>;
form.appendChild(eepromWidget);
textElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
ev.preventDefault();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
});
// hack: don't prompt user when hitting submit, really need to make this not stupid.
buttonsElem.firstChild.addEventListener('click', () => MszForumEditorAllowClose = true);
window.addEventListener('beforeunload', function(ev) {
if(!MszForumEditorAllowClose && textElem.value.length > 0) {
ev.preventDefault();
ev.returnValue = '';
}
});
const switchButtons = parser => {
$rc(markupActs);
const tags = MszParsing.getTagsFor(parser);
for(const tag of tags)
markupActs.appendChild(<button class={['forum__post__action', 'forum__post__action--tag', `forum__post__action--${tag.name}`]}
type="button" title={tag.summary} onclick={() => $insertTags(textElem, tag.open, tag.close)}>
<i class={tag.icon}/>
</button>);
};
const renderPreview = async (parser, text) => {
if(typeof text !== 'string')
return '';
const formData = new FormData;
formData.append('post[mode]', 'preview');
formData.append('post[text]', text);
formData.append('post[parser]', parseInt(parser));
const result = await $x.post('/forum/posting.php', { authed: true }, formData);
return result.body();
};
const previewBtn = <button class="input__button" type="button" value="preview">Preview</button>;
previewBtn.addEventListener('click', function() {
if(previewBtn.value === 'back') {
previewElem.setAttribute('hidden', 'hidden');
textElem.removeAttribute('hidden');
previewBtn.value = 'preview';
previewBtn.textContent = 'Preview';
modeElem.textContent = modeElem.dataset.original;
modeElem.dataset.original = null;
} else {
const postText = textElem.value,
postParser = parseInt(parserElem.value);
if(lastPostText === postText && lastPostParser === postParser) {
previewElem.removeAttribute('hidden');
textElem.setAttribute('hidden', 'hidden');
previewBtn.value = 'back';
previewBtn.textContent = 'Edit';
modeElem.dataset.original = modeElem.textContent;
modeElem.textContent = 'Previewing';
return;
}
parserElem.setAttribute('disabled', 'disabled');
previewBtn.setAttribute('disabled', 'disabled');
previewBtn.classList.add('input__button--busy');
renderPreview(postParser, postText)
.catch(() => {
previewElem.innerHTML = '';
MszShowMessageBox('Failed to render preview.');
})
.then(body => {
previewElem.classList.toggle('markdown', postParser === 2);
lastPostText = postText;
lastPostParser = postParser;
previewElem.innerHTML = body;
MszEmbed.handle($qa('.js-msz-embed-media'));
previewElem.removeAttribute('hidden');
textElem.setAttribute('hidden', 'hidden');
previewBtn.value = 'back';
previewBtn.textContent = 'Back';
previewBtn.removeAttribute('disabled');
parserElem.removeAttribute('disabled');
previewBtn.classList.remove('input__button--busy');
modeElem.dataset.original = modeElem.textContent;
modeElem.textContent = 'Previewing';
});
}
});
buttonsElem.insertBefore(previewBtn, buttonsElem.firstChild);
switchButtons(parserElem.value);
parserElem.addEventListener('change', () => {
const postParser = parseInt(parserElem.value);
switchButtons(postParser);
if(previewElem.hasAttribute('hidden'))
return;
// dunno if this would even be possible, but ech
if(postParser === lastPostParser)
return;
parserElem.setAttribute('disabled', 'disabled');
previewBtn.setAttribute('disabled', 'disabled');
previewBtn.classList.add('input__button--busy');
renderPreview(postParser, lastPostText)
.catch(() => {
previewElem.innerHTML = '';
MszShowMessageBox('Failed to render preview.');
})
.then(body => {
previewElem.classList.add('markdown', postParser === 2);
lastPostParser = postParser;
previewElem.innerHTML = body;
MszEmbed.handle($qa('.js-msz-embed-media'));
previewBtn.removeAttribute('disabled');
parserElem.removeAttribute('disabled');
previewBtn.classList.remove('input__button--busy');
});
});
};

View file

@ -1 +0,0 @@
Misuzu.Forum = {};

View file

@ -1,135 +1,90 @@
#include sakuya.js #include utility.js
#include embed/embed.js
var Misuzu = function() { #include events/christmas2019.js
Sakuya.trackElements($qa('time'));
hljs.initHighlighting();
MszEmbed.init(location.protocol + '//uiharu.' + location.host);
Misuzu.initQuickSubmit(); // only used by the forum posting form
Misuzu.Forum.Editor.init();
Misuzu.Events.dispatch();
Misuzu.initLoginPage();
MszEmbed.handle($qa('.js-msz-embed-media'));
};
#include utils.js
#include embed.js
#include forum/editor.js
#include events/events.js #include events/events.js
#include ext/sakuya.js
#include forum/editor.jsx
#include messages/messages.js
Misuzu.showMessageBox = function(text, title, buttons) { (async () => {
if($q('.messagebox')) const initLoginPage = async () => {
return false; const forms = Array.from($qa('.js-login-form'));
if(forms.length < 1)
return;
text = text || ''; const updateForm = async (avatar, userName) => {
title = title || ''; if(!(avatar instanceof Element) || !(userName instanceof Element))
buttons = buttons || [];
var element = document.createElement('div');
element.className = 'messagebox';
var container = element.appendChild(document.createElement('div'));
container.className = 'container messagebox__container';
var titleElement = container.appendChild(document.createElement('div')),
titleBackground = titleElement.appendChild(document.createElement('div')),
titleText = titleElement.appendChild(document.createElement('div'));
titleElement.className = 'container__title';
titleBackground.className = 'container__title__background';
titleText.className = 'container__title__text';
titleText.textContent = title || 'Information';
var textElement = container.appendChild(document.createElement('div'));
textElement.className = 'container__content';
textElement.textContent = text;
var buttonsContainer = container.appendChild(document.createElement('div'));
buttonsContainer.className = 'messagebox__buttons';
var firstButton = null;
if(buttons.length < 1) {
firstButton = buttonsContainer.appendChild(document.createElement('button'));
firstButton.className = 'input__button';
firstButton.textContent = 'OK';
firstButton.addEventListener('click', function() { element.remove(); });
} else {
for(var i = 0; i < buttons.length; i++) {
var button = buttonsContainer.appendChild(document.createElement('button'));
button.className = 'input__button';
button.textContent = buttons[i].text;
button.addEventListener('click', function() {
element.remove();
buttons[i].callback();
});
if(firstButton === null)
firstButton = button;
}
}
document.body.appendChild(element);
firstButton.focus();
return true;
};
Misuzu.initLoginPage = function() {
var updateForm = function(avatarElem, usernameElem) {
var xhr = new XMLHttpRequest;
xhr.addEventListener('readystatechange', function() {
if(xhr.readyState !== 4)
return; return;
var json = JSON.parse(xhr.responseText); const result = (await $x.get(`/auth/login.php?resolve=1&name=${encodeURIComponent(userName.value)}`, { type: 'json' })).body();
if(!json)
return;
if(json.name) avatar.src = result.avatar;
usernameElem.value = json.name; if(result.name.length > 0)
avatarElem.src = json.avatar; userName.value = result.name;
}); };
// need to figure out a url registry system again, current one is too much overhead so lets just do this for now
xhr.open('GET', '/auth/login.php?resolve=1&name=' + encodeURIComponent(usernameElem.value));
xhr.send();
};
var loginForms = $c('js-login-form'); for(const form of forms) {
const avatar = form.querySelector('.js-login-avatar');
const userName = form.querySelector('.js-login-username');
let timeOut;
for(var i = 0; i < loginForms.length; ++i) await updateForm(avatar, userName);
(function(form) {
var loginTimeOut = 0,
loginAvatar = form.querySelector('.js-login-avatar'),
loginUsername = form.querySelector('.js-login-username');
updateForm(loginAvatar, loginUsername); userName.addEventListener('input', function() {
loginUsername.addEventListener('keyup', function() { if(timeOut !== undefined)
if(loginTimeOut)
return; return;
loginTimeOut = setTimeout(function() {
updateForm(loginAvatar, loginUsername); timeOut = setTimeout(() => {
clearTimeout(loginTimeOut); updateForm(avatar, userName)
loginTimeOut = 0; .finally(() => {
clearTimeout(timeOut);
timeOut = undefined;
});
}, 750); }, 750);
}); });
})(loginForms[i]); }
}; };
Misuzu.initQuickSubmit = function() {
var ctrlSubmit = Array.from($qa('.js-quick-submit, .js-ctrl-enter-submit'));
if(!ctrlSubmit)
return;
for(var i = 0; i < ctrlSubmit.length; ++i) const initQuickSubmit = () => {
ctrlSubmit[i].addEventListener('keydown', function(ev) { const elems = Array.from($qa('.js-quick-submit, .js-ctrl-enter-submit'));
if((ev.code === 'Enter' || ev.code === 'NumpadEnter') // i hate this fucking language so much if(elems.length < 1)
&& ev.ctrlKey && !ev.altKey && !ev.shiftKey && !ev.metaKey) { return;
// hack: prevent forum editor from screaming when using this keycombo
// can probably be done in a less stupid manner
Misuzu.Forum.Editor.allowWindowClose = true;
this.form.submit(); for(const elem of elems)
ev.preventDefault(); elem.addEventListener('keydown', ev => {
} if((ev.code === 'Enter' || ev.code === 'NumpadEnter') && ev.ctrlKey && !ev.altKey && !ev.shiftKey && !ev.metaKey) {
}); // hack: prevent forum editor from screaming when using this keycombo
}; // can probably be done in a less stupid manner
MszForumEditorAllowClose = true;
elem.form.submit();
ev.preventDefault();
}
});
};
try {
MszSakuya.trackElements($qa('time'));
hljs.highlightAll();
MszEmbed.init(`${location.protocol}//uiharu.${location.host}`);
// only used by the forum posting form
initQuickSubmit();
const forumPostingForm = $q('.js-forum-posting');
if(forumPostingForm !== null)
MszForumEditor(forumPostingForm);
const events = new MszSeasonalEvents;
events.add(new MszChristmas2019EventInfo);
events.dispatch();
await initLoginPage();
MszMessages();
MszEmbed.handle($qa('.js-msz-embed-media'));
} catch(ex) {
console.error(ex);
}
})();

View file

@ -0,0 +1,89 @@
#include watcher.js
const MszMessagesActionButton = function(button, stateless) {
if(!(button instanceof Element))
throw 'button must be an element';
const stateful = !stateless;
const pub = {};
const icon = button.querySelector('.js-messages-button-icon i');
const label = button.querySelector('.js-messages-button-label');
const update = () => {
if(stateful) {
icon.className = button.dataset[`${button.dataset.state}Ico`];
label.textContent = button.dataset[`${button.dataset.state}Str`];
}
};
pub.update = update;
const stateWatcher = new MszWatcher;
const getState = () => button.dataset.state !== 'inactive';
const setState = state => {
button.dataset.state = state ? 'active' : 'inactive';
update();
stateWatcher.call(getState());
};
if(stateful) {
pub.getState = getState;
pub.setState = setState;
pub.watchState = handler => { stateWatcher.watch(handler, getState()); };
pub.unwatchState = handler => { stateWatcher.unwatch(handler); };
}
let clickAction;
const click = async () => {
if(clickAction !== undefined) {
if(stateful) {
const result = await clickAction(getState());
if(typeof result === 'boolean')
setState(result);
} else
await clickAction();
}
};
pub.click = click;
button.addEventListener('click', () => click());
update();
pub.setAction = action => {
if(typeof action !== 'function')
throw 'action must be a function';
clickAction = action;
};
let preventEnable = false;
pub.getEnabled = () => !button.disabled;
pub.setEnabled = state => {
if(!preventEnable)
button.disabled = !state;
};
pub.disableWith = async callback => {
if(typeof callback !== 'function')
throw 'callback must be a function';
if(preventEnable)
throw 'preventEnable is true';
preventEnable = true;
const wasDisabled = button.disabled;
button.disabled = true;
try {
return await callback();
} finally {
button.disabled = wasDisabled;
preventEnable = false;
}
};
pub.setHidden = state => {
button.hidden = state;
};
return pub;
};

View file

@ -0,0 +1,167 @@
#include utility.js
#include watcher.js
const MsgMessagesList = function(list) {
if(!(list instanceof Element))
throw 'list must be an element';
const watchers = new MszWatchers;
watchers.define(['select']);
let selectedCount = 0;
const items = Array.from(list.querySelectorAll('.js-messages-entry')).map(elem => {
const item = new MsgMessagesEntry(elem);
item.onSelectedChange((state, initial) => {
if(state)
++selectedCount;
else if(!initial)
--selectedCount;
if(!initial)
watchers.call('select', selectedCount, items.length);
});
return item;
});
const recountSelected = () => {
selectedCount = 0;
for(const item of items)
if(item.getSelected())
++selectedCount;
};
const onSelectedChange = handler => {
watchers.watch('select', handler, selectedCount, items.length);
};
onSelectedChange(selectedCount => {
const state = selectedCount > 0;
for(const item of items)
item.setClickIsSelect(state);
});
return {
getItems: () => items,
getItemsCount: () => items.length,
getSelectedItems: () => {
const selected = [];
for(const item of items)
if(item.getSelected())
selected.push(item);
return selected;
},
removeItem: item => {
$ari(items, item);
$r(item.getElement());
recountSelected();
watchers.call('select', selectedCount, items.length);
},
getAllSelected: () => {
if(items.length < 1)
return false;
for(const item of items)
if(!item.getSelected())
return false;
return true;
},
setAllSelected: state => {
for(const item of items)
item.setSelected(state);
selectedCount = state ? items.length : 0;
watchers.call('select', selectedCount, items.length);
},
onSelectedChange: onSelectedChange,
};
};
const MsgMessagesEntry = function(entry) {
if(!(entry instanceof Element))
throw 'entry must be an element';
const msgId = entry.dataset.msgId;
const unreadElem = entry.querySelector('.js-messages-entry-unread');
const isRead = () => entry.dataset.msgRead === 'read';
const setRead = state => {
if(state) {
entry.dataset.msgRead = 'read';
unreadElem.hidden = true;
} else {
entry.dataset.msgRead = 'unread';
unreadElem.hidden = false;
}
};
const isSent = () => entry.dataset.msgSent === 'sent';
const setSent = state => {
entry.dataset.msgRead = state ? 'sent' : 'draft';
};
const checkbox = entry.querySelector('.js-entry-checkbox');
const getSelected = () => checkbox.checked;
const setSelected = state => checkbox.checked = state;
const toggleSelected = () => checkbox.checked = !checkbox.checked;
let clickIsSelect = false;
const watchers = new MszWatchers;
watchers.define(['select']);
checkbox.addEventListener('click', ev => ev.stopPropagation());
checkbox.addEventListener('keydown', ev => ev.stopPropagation());
checkbox.addEventListener('change', () => {
watchers.call('select', getSelected());
});
const navigateToMessage = () => {
const url = entry.dataset.msgUrl;
if(url !== undefined && url.startsWith('/') && !url.startsWith('//'))
location.assign(url);
};
entry.addEventListener('keydown', ev => {
if(ev.key === 'Enter' || ev.key === 'NumpadEnter') {
ev.preventDefault();
entry.click();
}
});
entry.addEventListener('click', ev => {
ev.preventDefault();
if(clickIsSelect)
checkbox.click();
else
navigateToMessage();
});
entry.addEventListener('dblclick', ev => {
ev.preventDefault();
if(clickIsSelect)
navigateToMessage();
});
return {
getId: () => msgId,
getElement: () => entry,
isRead: isRead,
setRead: setRead,
isSent: isSent,
setSent: setSent,
getSelected: getSelected,
setSelected: setSelected,
toggleSelected: toggleSelected,
setClickIsSelect: state => clickIsSelect = state,
onSelectedChange: handler => {
watchers.watch('select', handler, getSelected());
},
};
};

View file

@ -0,0 +1,386 @@
#include csrfp.js
#include msgbox.js
#include utility.js
#include messages/actbtn.js
#include messages/list.js
#include messages/recipient.js
#include messages/reply.jsx
#include messages/thread.js
const MszMessages = () => {
const extractMsgIds = msg => {
if(typeof msg.getId === 'function')
return msg.getId();
if(typeof msg.toString === 'function')
return msg.toString();
throw 'unsupported message type';
};
const displayErrorMessage = async error => {
let text;
if(typeof error === 'string')
text = error;
else if(typeof error.text === 'string')
text = error.text;
else if(typeof error.toString === 'function')
text = error.toString();
else
text = 'Something indescribable happened.';
await MszShowMessageBox(text, 'Error');
return false;
};
const msgsCreate = async (title, text, parser, draft, recipient, replyTo) => {
const formData = new FormData;
formData.append('_csrfp', MszCSRFP.getToken());
formData.append('title', title);
formData.append('body', text);
formData.append('parser', parser);
formData.append('draft', draft);
formData.append('recipient', recipient);
formData.append('reply', replyTo);
const result = await $x.post('/messages/create', { type: 'json' }, formData);
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return body;
};
const msgsUpdate = async (messageId, title, text, parser, draft) => {
const formData = new FormData;
formData.append('_csrfp', MszCSRFP.getToken());
formData.append('title', title);
formData.append('body', text);
formData.append('parser', parser);
formData.append('draft', draft);
const result = await $x.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json' }, formData);
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return body;
};
const msgsMark = async (msgs, state) => {
const result = await $x.post('/messages/mark', { type: 'json' }, {
_csrfp: MszCSRFP.getToken(),
type: state,
messages: msgs.map(extractMsgIds).join(','),
});
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return true;
};
const msgsDelete = async msgs => {
const result = await $x.post('/messages/delete', { type: 'json' }, {
_csrfp: MszCSRFP.getToken(),
messages: msgs.map(extractMsgIds).join(','),
});
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return true;
};
const msgsRestore = async msgs => {
const result = await $x.post('/messages/restore', { type: 'json' }, {
_csrfp: MszCSRFP.getToken(),
messages: msgs.map(extractMsgIds).join(','),
});
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return true;
};
const msgsNuke = async msgs => {
const result = await $x.post('/messages/nuke', { type: 'json' }, {
_csrfp: MszCSRFP.getToken(),
messages: msgs.map(extractMsgIds).join(','),
});
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(body.error !== undefined)
throw body.error;
return true;
};
const msgsUserBtns = Array.from($qa('.js-header-pms-button'));
if(msgsUserBtns.length > 0)
$x.get('/messages/stats', { type: 'json' }).then(result => {
const body = result.body();
if(typeof body === 'object' && typeof body.unread === 'number')
if(body.unread > 0)
for(const msgsUserBtn of msgsUserBtns)
msgsUserBtn.append($e({ child: body.unread.toLocaleString(), attrs: { className: 'header__desktop__user__button__count' } }));
});
const msgsListElem = $q('.js-messages-list');
const msgsList = msgsListElem instanceof Element ? new MsgMessagesList(msgsListElem) : undefined;
const msgsListEmptyNotice = $q('.js-messages-folder-empty');
const msgsThreadElem = $q('.js-messages-thread');
const msgsThread = msgsThreadElem instanceof Element ? new MszMessagesThread(msgsThreadElem) : undefined;
const msgsRecipientElem = $q('.js-messages-recipient');
const msgsRecipient = msgsRecipientElem instanceof Element ? new MszMessagesRecipient(msgsRecipientElem) : undefined;
const msgsReplyElem = $q('.js-messages-reply');
const msgsReply = msgsReplyElem instanceof Element ? new MszMessagesReply(msgsReplyElem) : undefined;
if(msgsReply !== undefined) {
if(msgsRecipient !== undefined)
msgsRecipient.onUpdate(async info => {
msgsReply.setRecipient(typeof info.id === 'string' ? info.id : '');
msgsReply.setWarning(info.ban ? `${(typeof info.name === 'string' ? info.name : 'This user')} has been banned and will be unable to respond to your messages.` : undefined);
});
msgsReply.onSubmit(async form => {
try {
let result;
if(typeof form.message === 'string') {
result = await msgsUpdate(
form.message,
form.title,
form.body,
form.parser,
form.draft
);
} else {
result = await msgsCreate(
form.title,
form.body,
form.parser,
form.draft,
form.recipient,
form.reply || ''
);
}
if(typeof result.url === 'string')
location.assign(result.url);
} catch(ex) {
return await displayErrorMessage(ex);
}
});
}
let actSelectAll, actMarkRead, actMoveTrash, actNuke;
const actSelectAllBtn = $q('.js-messages-actions-select-all');
if(actSelectAllBtn instanceof Element) {
actSelectAll = new MszMessagesActionButton(actSelectAllBtn);
if(msgsList !== undefined) {
actSelectAll.setAction(async state => {
msgsList.setAllSelected(!state);
return !state;
});
msgsList.onSelectedChange((selectedNo, itemNo) => {
actSelectAll.setState(selectedNo >= itemNo);
});
actSelectAll.setState(msgsList.getAllSelected());
}
}
const actMarkReadBtn = $q('.js-messages-actions-mark-read');
if(actMarkReadBtn instanceof Element) {
actMarkRead = new MszMessagesActionButton(actMarkReadBtn);
if(msgsList !== undefined) {
msgsList.onSelectedChange(selectedNo => {
const enabled = selectedNo > 0;
actMarkRead.setEnabled(enabled);
if(enabled) {
const items = msgsList.getSelectedItems();
let readNo = 0, unreadNo = 0;
for(const item of items) {
if(item.isRead())
++readNo;
else
++unreadNo;
}
actMarkRead.setState(readNo > unreadNo);
}
});
actMarkRead.setAction(async state => {
const items = msgsList.getSelectedItems();
const result = await actMarkRead.disableWith(async () => {
try {
return await msgsMark(items, state ? 'unread' : 'read');
} catch(ex) {
return await displayErrorMessage(ex);
}
});
if(result) {
state = !state;
for(const item of items)
item.setRead(state);
return state;
}
});
} else if(msgsThread !== undefined) {
actMarkRead.setAction(async state => {
const items = [msgsThread.getMessage()];
const result = await actMarkRead.disableWith(async () => {
try {
return await msgsMark(items, state ? 'unread' : 'read');
} catch(ex) {
return await displayErrorMessage(ex);
}
});
return result ? !state : state;
});
}
}
const actMoveTrashBtn = $q('.js-messages-actions-move-trash');
if(actMoveTrashBtn instanceof Element) {
actMoveTrash = new MszMessagesActionButton(actMoveTrashBtn);
if(msgsList !== undefined) {
msgsList.onSelectedChange(selectedNo => actMoveTrash.setEnabled(selectedNo > 0));
actMoveTrash.setAction(async state => {
const items = msgsList.getSelectedItems();
if(!state && !await MszShowConfirmBox(`Are you sure you wish to delete ${items.length} item${items.length === 1 ? '' : 's'}?`, 'Confirmation'))
return;
const result = await actMoveTrash.disableWith(async () => {
try {
if(state)
return await msgsRestore(items);
return await msgsDelete(items);
} catch(ex) {
return await displayErrorMessage(ex);
}
});
if(result)
for(const message of items)
msgsList.removeItem(message);
if(msgsListEmptyNotice instanceof Element)
msgsListEmptyNotice.hidden = msgsList.getItemsCount() > 0;
});
} else if(msgsThread !== undefined) {
actMoveTrash.setAction(async state => {
if(!state && !await MszShowConfirmBox('Are you sure you wish to delete this message?', 'Confirmation'))
return;
const items = [msgsThread.getMessage()];
const result = await actMoveTrash.disableWith(async () => {
try {
if(state)
return await msgsRestore(items);
return await msgsDelete(items);
} catch(ex) {
return await displayErrorMessage(ex);
}
});
if(result) {
state = !state;
if(msgsReply !== undefined)
msgsReply.setHidden(state);
const msg = msgsThread.getMessage();
if(msg !== undefined)
msg.setDeleted(state);
return state;
}
});
}
}
const actNukeBtn = $q('.js-messages-actions-nuke');
if(actNukeBtn instanceof Element) {
actNuke = new MszMessagesActionButton(actNukeBtn, true);
if(msgsList !== undefined) {
msgsList.onSelectedChange(selectedNo => actNuke.setEnabled(selectedNo > 0));
actNuke.setAction(async () => {
const items = msgsList.getSelectedItems();
if(!await MszShowConfirmBox(`Are you sure you wish to PERMANENTLY delete ${items.length} item${items.length === 1 ? '' : 's'}?`, 'Confirmation'))
return;
const result = await actNuke.disableWith(async () => {
try {
return await msgsNuke(items);
} catch(ex) {
return await displayErrorMessage(ex);
}
});
if(result)
for(const message of items)
msgsList.removeItem(message);
if(msgsListEmptyNotice instanceof Element)
msgsListEmptyNotice.hidden = msgsList.getItemsCount() > 0;
});
} else if(msgsThread !== undefined) {
actMoveTrash.watchState(state => {
actNuke.setHidden(!state);
});
actNuke.setAction(async () => {
if(!await MszShowConfirmBox('Are you sure you wish to PERMANENTLY delete this message?', 'Confirmation'))
return;
const items = [msgsThread.getMessage()];
const result = await actNuke.disableWith(async () => {
try {
return await msgsNuke(items);
} catch(ex) {
return await displayErrorMessage(ex);
}
});
if(result)
location.assign('/messages');
});
}
}
};

View file

@ -0,0 +1,56 @@
#include csrfp.js
#include utility.js
const MszMessagesRecipient = function(element) {
if(!(element instanceof Element))
throw 'element must be an instance of Element';
const avatarElem = element.querySelector('.js-messages-recipient-avatar img');
const nameInput = element.querySelector('.js-messages-recipient-name');
let updateHandler = undefined;
const update = async () => {
const result = await $x.post(element.dataset.msgLookup, { type: 'json' }, {
_csrfp: MszCSRFP.getToken(),
name: nameInput.value,
});
MszCSRFP.setFromHeaders(result);
const body = result.body();
if(updateHandler !== undefined)
await updateHandler(body);
if(typeof body.avatar === 'string')
avatarElem.src = body.avatar;
if(typeof body.name === 'string')
nameInput.value = body.name;
};
let nameTimeout = null;
nameInput.addEventListener('input', () => {
if(nameTimeout !== undefined)
return;
nameTimeout = setTimeout(() => {
update().finally(() => {
clearTimeout(nameTimeout);
nameTimeout = undefined;
});
}, 750);
});
update().finally(() => nameTimeout = undefined);
return {
getElement: () => element,
onUpdate: handler => {
if(typeof handler !== 'function')
throw 'handler must be a function';
updateHandler = handler;
},
};
};

View file

@ -0,0 +1,171 @@
#include parsing.js
#include ext/eeprom.js
const MszMessagesReply = function(element) {
if(!(element instanceof Element))
throw 'element must be an Element';
const form = element.querySelector('.js-messages-reply-form');
const bodyElem = form.querySelector('.js-messages-reply-body');
const actsElem = form.querySelector('.js-messages-reply-actions');
const parserSelect = form.querySelector('.js-messages-reply-parser');
const saveBtn = form.querySelector('.js-messages-reply-save');
const sendBtn = form.querySelector('.js-messages-reply-send');
const warnElem = form.querySelector('.js-reply-form-warning');
const warnText = warnElem instanceof Element ? warnElem.querySelector('.js-reply-form-warning-text') : undefined;
let submitHandler;
form.addEventListener('submit', ev => {
ev.preventDefault();
if(typeof submitHandler === 'function') {
const fields = Array.from(form.elements);
const result = {};
for(const field of fields) {
if((field instanceof HTMLButtonElement || (field instanceof HTMLInputElement && field.type === 'submit')) && ev.submitter !== field)
continue;
if(typeof field.name === 'string' && field.name.length > 0)
result[field.name] = field.value;
}
submitHandler(result);
}
});
bodyElem.addEventListener('keydown', ev => {
if((ev.code === 'Enter' || ev.code === 'NumpadEnter') && ev.ctrlKey && !ev.altKey && !ev.metaKey) {
ev.preventDefault();
if(ev.shiftKey)
saveBtn.click();
else
sendBtn.click();
}
});
const switchButtons = parser => {
$rc(actsElem);
const tags = MszParsing.getTagsFor(parser);
actsElem.hidden = tags.length < 1;
for(const tag of tags)
actsElem.appendChild(<button class="messages-reply-action" type="button" title={tag.summary} onclick={() => $insertTags(bodyElem, tag.open, tag.close)}>
<i class={tag.icon}/>
</button>);
};
switchButtons(parserSelect.value);
parserSelect.addEventListener('change', () => {
switchButtons(parserSelect.value);
});
// this implementation is godawful but it'll do for now lol
// need to make it easier to share the forum's implementation
MszEEPROM.init()
.catch(() => console.error('Failed to initialise EEPROM'))
.then(() => {
const eepromClient = new EEPROM(peepApp, peepPath);
const eepromHandleFileUpload = async file => {
const uploadTask = eepromClient.create(file);
try {
const fileInfo = await uploadTask.start();
const parserMode = parseInt(parserSelect.value);
let insertText = location.protocol + fileInfo.url;
if(parserMode == 1) { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserMode == 2) { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
}
$insertTags(bodyElem, insertText, '');
bodyElem.value = bodyElem.value.trim();
} catch(ex) {
let errorText = 'Upload aborted.';
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
await MszShowMessageBox(errorText, 'Upload Error');
}
};
bodyElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
ev.preventDefault();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
});
return {
getElement: () => element,
setWarning: text => {
if(warnElem === undefined || warnText === undefined)
return;
if(text === undefined) {
warnElem.hidden = true;
warnText.textContent = '';
} else {
warnElem.hidden = false;
warnText.textContent = text;
}
},
setRecipient: userId => {
for(const field of form.elements)
if(field.name === 'recipient') {
field.value = userId;
break;
}
},
getHidden: () => element.hidden,
setHidden: state => {
element.hidden = state;
},
onSubmit: handler => {
if(typeof handler !== 'function')
throw 'handler must be a function';
submitHandler = handler;
},
};
};

View file

@ -0,0 +1,78 @@
const MszMessagesThread = function(thread) {
if(!(thread instanceof Element))
throw 'thread must be an element';
const messages = Array.from(thread.querySelectorAll('.js-messages-message')).map(elem => new MszMessagesThreadMessage(elem));
const message = messages.find(msg => msg.isFull());
return {
getMessage: () => message,
getMessages: () => messages,
};
};
const MszMessagesThreadMessage = function(message) {
if(!(message instanceof Element))
throw 'message must be an element';
const msgId = message.dataset.msgId;
const type = message.dataset.msgType;
const url = message.dataset.msgUrl;
if(type === 'snip') {
message.addEventListener('click', ev => {
if(typeof url !== 'string')
return;
let target = ev.target;
while(target !== message) {
if(target instanceof HTMLAnchorElement)
return;
target = target.parentNode;
}
ev.preventDefault();
location.assign(url);
});
} else if(type === 'full') {
message.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
const isRead = () => message.dataset.msgRead === 'read';
const setRead = state => {
message.dataset.msgRead = state ? 'read' : 'unread';
};
const isSent = () => message.dataset.msgSent === 'sent';
const setSent = state => {
message.dataset.msgRead = state ? 'sent' : 'draft';
};
const isDeleted = () => message.dataset.msgDeleted === 'yes';
const setDeleted = state => {
if(state) {
message.dataset.msgDeleted = 'yes';
message.classList.add('messages-message-deleted');
} else {
message.dataset.msgDeleted = 'no';
message.classList.remove('messages-message-deleted');
}
};
return {
getId: () => msgId,
getType: () => type,
isFull: () => type === 'full',
isSnippet: () => type === 'snip',
isRead: isRead,
setRead: setRead,
isSent: isSent,
setSent: setSent,
isDeleted: isDeleted,
setDeleted: setDeleted,
};
};

View file

@ -0,0 +1,73 @@
#include utility.js
const MszShowConfirmBox = async (text, title, target) => {
let result = false;
await MszShowMessageBox(text, title, [
{ text: 'Yes', callback: async () => result = true },
{ text: 'No' },
], target);
return result;
};
const MszShowMessageBox = (text, title, buttons, target) => {
if(typeof text !== 'string') {
if(text !== undefined && text !== null && typeof text.toString === 'function')
text = text.toString();
else throw 'text must be a string';
}
if(!(target instanceof Element))
target = document.body;
if(typeof title !== 'string')
title = 'Information';
if(!Array.isArray(buttons))
buttons = [];
return new Promise((resolve, reject) => {
if(target.querySelector('.messagebox')) {
reject();
return;
}
let buttonsElem;
const html = <div class="messagebox">
<div class="container messagebox__container">
<div class="container__title">
<div class="container__title__background"/>
<div class="container__title__text">{title}</div>
</div>
<div class="container__content">{text}</div>
{buttonsElem = <div class="messagebox__buttons"/>}
</div>
</div>;
let firstButton;
if(buttons.length < 1) {
firstButton = <button class="input__button" onclick={() => {
html.remove();
resolve();
}}>OK</button>;
buttonsElem.appendChild(firstButton);
} else {
for(const button of buttons) {
const buttonElem = <button class="input__button" onclick={() => {
html.remove();
if(typeof button.callback === 'function')
button.callback().finally(() => resolve());
else
resolve();
}}>{button.text}</button>;
buttonsElem.appendChild(buttonElem);
if(firstButton === undefined)
firstButton = buttonElem;
}
}
target.appendChild(html);
firstButton.focus();
});
};

View file

@ -0,0 +1,56 @@
// welcome to the shitty temporary file for managing the bbcode/markdown/whatever button
const MszParsing = (() => {
const defineTag = (name, open, close, summary, icon) => {
return {
name: name,
open: open,
close: close,
summary: summary,
icon: icon,
};
};
const bbTags = [
defineTag('bb-bold', '[b]', '[/b]', 'Bold [b]<text>[/b]', 'fas fa-bold fa-fw'),
defineTag('bb-italic', '[i]', '[/i]', 'Italic [i]<text>[/i]', 'fas fa-italic fa-fw'),
defineTag('bb-underline', '[u]', '[/u]', 'Underline [u]<text>[/u]', 'fas fa-underline fa-fw'),
defineTag('bb-strike', '[s]', '[/s]', 'Strikethrough [s]<text>[/s]', 'fas fa-strikethrough fa-fw'),
defineTag('bb-link', '[url=]', '[/url]', 'Link [url]<url>[/url] or [url=<url>]<text>[/url]', 'fas fa-link fa-fw'),
defineTag('bb-image', '[img]', '[/img]', 'Image [img]<url>[/img]', 'fas fa-image fa-fw'),
defineTag('bb-audio', '[audio]', '[/audio]', 'Audio [audio]<url>[/audio]', 'fas fa-music fa-fw'),
defineTag('bb-video', '[video]', '[/video]', 'Video [video]<url>[/video]', 'fas fa-video fa-fw'),
defineTag('bb-code', '[code]', '[/code]', 'Code [code]<code>[/code]', 'fas fa-code fa-fw'),
defineTag('bb-zalgo', '[zalgo]', '[/zalgo]', 'Zalgo [zalgo]<text>[/zalgo]', 'fas fa-frog fa-fw'),
];
const mdTags = [
defineTag('md-bold', '**', '**', 'Bold **<text>**', 'fas fa-bold fa-fw'),
defineTag('md-italic', '*', '*', 'Italic *<text>* or _<text>_', 'fas fa-italic fa-fw'),
defineTag('md-underline', '__', '__', 'Underline __<text>__', 'fas fa-underline fa-fw'),
defineTag('md-strike', '~~', '~~', 'Strikethrough ~~<text>~~', 'fas fa-strikethrough fa-fw'),
defineTag('md-link', '[](', ')', 'Link [<text>](<url>)', 'fas fa-link fa-fw'),
defineTag('md-image', '![](', ')', 'Image ![<alt text>](<url>)', 'fas fa-image fa-fw'),
defineTag('md-audio', '![](', ')', 'Audio ![<alt text>](<url>)', 'fas fa-music fa-fw'),
defineTag('md-video', '![](', ')', 'Video ![<alt text>](<url>)', 'fas fa-video fa-fw'),
defineTag('md-code', '```', '```', 'Code `<code>` or ```<code>```', 'fas fa-code fa-fw'),
];
const getTagsFor = parser => {
if(typeof parser !== 'number')
parser = parseInt(parser);
if(parser === 1)
return bbTags;
if(parser === 2)
return mdTags;
return [];
};
return {
getTagsFor: getTagsFor,
getTagsForPlainText: () => getTagsFor(0),
getTagsForBBcode: () => getTagsFor(1),
getTagsForMarkdown: () => getTagsFor(2),
};
})();

View file

@ -1,58 +0,0 @@
const Uiharu = function(apiUrl) {
const maxBatchSize = 4;
const lookupOneUrl = apiUrl + '/metadata',
lookupManyUrl = apiUrl + '/metadata/batch';
const lookupManyInternal = function(targetUrls, callback) {
const formData = new FormData;
for(const url of targetUrls)
formData.append('url[]', url);
const xhr = new XMLHttpRequest;
xhr.addEventListener('load', function() {
callback(JSON.parse(xhr.responseText));
});
xhr.addEventListener('error', function(ev) {
callback({ status: xhr.status, error: 'xhr', details: ev });
});
xhr.open('POST', lookupManyUrl);
xhr.send(formData);
};
return {
lookupOne: function(targetUrl, callback) {
if(typeof callback !== 'function')
throw 'callback is missing';
targetUrl = (targetUrl || '').toString();
if(targetUrl.length < 1)
return;
const xhr = new XMLHttpRequest;
xhr.addEventListener('load', function() {
callback(JSON.parse(xhr.responseText));
});
xhr.addEventListener('error', function() {
callback({ status: xhr.status, error: 'xhr', details: ex });
});
xhr.open('POST', lookupOneUrl);
xhr.send(targetUrl);
},
lookupMany: function(targetUrls, callback) {
if(!Array.isArray(targetUrls))
throw 'targetUrls must be an array of urls';
if(typeof callback !== 'function')
throw 'callback is missing';
if(targetUrls < 1)
return;
if(targetUrls.length <= maxBatchSize) {
lookupManyInternal(targetUrls, callback);
return;
}
for(let i = 0; i < targetUrls.length; i += maxBatchSize)
lookupManyInternal(targetUrls.slice(i, i + maxBatchSize), callback);
},
};
};

View file

@ -13,6 +13,10 @@ const $ri = function(name) {
$r($i(name)); $r($i(name));
}; };
const $rq = function(query) {
$r($q(query));
};
const $ib = function(ref, elem) { const $ib = function(ref, elem) {
ref.parentNode.insertBefore(elem, ref); ref.parentNode.insertBefore(elem, ref);
}; };
@ -79,6 +83,11 @@ const $e = function(info, attrs, child, created) {
} }
break; break;
case 'boolean':
if(attr)
elem.setAttribute(key, '');
break;
default: default:
if(key === 'className') if(key === 'className')
key = 'class'; key = 'class';
@ -153,17 +162,125 @@ const $as = function(array) {
} }
}; };
var $insertTags = function(target, tagOpen, tagClose) { const $x = (function() {
const send = function(method, url, options, body) {
if(options === undefined)
options = {};
else if(typeof options !== 'object')
throw 'options must be undefined or an object';
const xhr = new XMLHttpRequest;
const requestHeaders = new Map;
if('headers' in options && typeof options.headers === 'object')
for(const name in options.headers)
if(options.headers.hasOwnProperty(name))
requestHeaders.set(name.toLowerCase(), options.headers[name]);
if(typeof options.download === 'function') {
xhr.onloadstart = ev => options.download(ev);
xhr.onprogress = ev => options.download(ev);
xhr.onloadend = ev => options.download(ev);
}
if(typeof options.upload === 'function') {
xhr.upload.onloadstart = ev => options.upload(ev);
xhr.upload.onprogress = ev => options.upload(ev);
xhr.upload.onloadend = ev => options.upload(ev);
}
if(options.authed)
xhr.withCredentials = true;
if(typeof options.timeout === 'number')
xhr.timeout = options.timeout;
if(typeof options.type === 'string')
xhr.responseType = options.type;
if(typeof options.abort === 'function')
options.abort(() => xhr.abort());
if(typeof options.xhr === 'function')
options.xhr(() => xhr);
if(typeof body === 'object') {
if(body instanceof URLSearchParams) {
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
} else if(body instanceof FormData) {
// content-type is implicitly set
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
if(!requestHeaders.has('content-type'))
requestHeaders.set('content-type', 'application/octet-stream');
} else if(!requestHeaders.has('content-type')) {
const bodyParts = [];
for(const name in body)
if(body.hasOwnProperty(name))
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
body = bodyParts.join('&');
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
}
}
return new Promise((resolve, reject) => {
let responseHeaders = undefined;
xhr.onload = ev => resolve({
status: xhr.status,
body: () => xhr.response,
text: () => xhr.responseText,
headers: () => {
if(responseHeaders !== undefined)
return responseHeaders;
responseHeaders = new Map;
const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
for(const name in raw)
if(raw.hasOwnProperty(name)) {
const parts = raw[name].split(': ');
responseHeaders.set(parts.shift(), parts.join(': '));
}
return responseHeaders;
},
xhr: xhr,
ev: ev,
});
xhr.onerror = ev => reject({
xhr: xhr,
ev: ev,
});
xhr.open(method, url);
for(const [name, value] of requestHeaders)
xhr.setRequestHeader(name, value);
xhr.send(body);
});
};
return {
send: send,
get: (url, options, body) => send('GET', url, options, body),
post: (url, options, body) => send('POST', url, options, body),
delete: (url, options, body) => send('DELETE', url, options, body),
patch: (url, options, body) => send('PATCH', url, options, body),
put: (url, options, body) => send('PUT', url, options, body),
};
})();
const $insertTags = function(target, tagOpen, tagClose) {
tagOpen = tagOpen || ''; tagOpen = tagOpen || '';
tagClose = tagClose || ''; tagClose = tagClose || '';
if(document.selection) { if(document.selection) {
target.focus(); target.focus();
var selected = document.selection.createRange(); const selected = document.selection.createRange();
selected.text = tagOpen + selected.text + tagClose; selected.text = tagOpen + selected.text + tagClose;
target.focus(); target.focus();
} else if(target.selectionStart || target.selectionStart === 0) { } else if(target.selectionStart || target.selectionStart === 0) {
var startPos = target.selectionStart, const startPos = target.selectionStart,
endPos = target.selectionEnd, endPos = target.selectionEnd,
scrollTop = target.scrollTop; scrollTop = target.scrollTop;
@ -176,7 +293,7 @@ var $insertTags = function(target, tagOpen, tagClose) {
target.focus(); target.focus();
target.selectionStart = startPos + tagOpen.length; target.selectionStart = startPos + tagOpen.length;
target.selectionEnd = endPos + tagOpen.length; target.selectionEnd = endPos + tagOpen.length;
target.scrollTop + scrollTop; target.scrollTop = scrollTop;
} else { } else {
target.value += tagOpen + tagClose; target.value += tagOpen + tagClose;
target.focus(); target.focus();

View file

@ -1,83 +1,65 @@
const MszWatcher = function() { const MszWatcher = function() {
let watchers = []; const handlers = [];
return { const watch = (handler, ...args) => {
watch: function(watcher, thisArg, args) { if(typeof handler !== 'function')
if(typeof watcher !== 'function') throw 'handler must be a function';
throw 'watcher must be a function'; if(handlers.includes(handler))
if(watchers.indexOf(watcher) >= 0) throw 'handler already registered';
return;
watchers.push(watcher); handlers.push(handler);
args.push(true);
if(thisArg !== undefined) { handler(...args);
if(!Array.isArray(args)) {
if(args !== undefined)
args = [args];
else args = [];
}
// initial call
args.push(true);
watcher.apply(thisArg, args);
}
},
unwatch: function(watcher) {
$ari(watchers, watcher);
},
call: function(thisArg, args) {
if(!Array.isArray(args)) {
if(args !== undefined)
args = [args];
else args = [];
}
args.push(false);
for(const watcher of watchers)
watcher.apply(thisArg, args);
},
};
};
const MszWatcherCollection = function() {
const collection = new Map;
const watch = function(name, watcher, thisArg, args) {
const watchers = collection.get(name);
if(watchers === undefined)
throw 'undefined watcher name';
watchers.watch(watcher, thisArg, args);
}; };
const unwatch = function(name, watcher) { const unwatch = handler => {
const watchers = collection.get(name); $ari(handlers, handler);
if(watchers === undefined)
throw 'undefined watcher name';
watchers.unwatch(watcher);
}; };
return { return {
define: function(names) {
if(!Array.isArray(names))
names = [names];
for(const name of names)
collection.set(name, new MszWatcher);
},
call: function(name, thisArg, args) {
const watchers = collection.get(name);
if(watchers === undefined)
throw 'undefined watcher name';
watchers.call(thisArg, args);
},
watch: watch, watch: watch,
unwatch: unwatch, unwatch: unwatch,
proxy: function(obj) { call: (...args) => {
obj.watch = function(name, watcher) { args.push(false);
watch(name, watcher);
}; for(const handler of handlers)
obj.unwatch = unwatch; handler(...args);
},
};
};
const MszWatchers = function() {
const watchers = new Map;
const getWatcher = name => {
const watcher = watchers.get(name);
if(watcher === undefined)
throw 'undefined watcher name';
return watcher;
};
const watch = (name, handler, ...args) => {
getWatcher(name).watch(handler, ...args);
};
const unwatch = (name, handler) => {
getWatcher(name).unwatch(handler);
};
return {
watch: watch,
unwatch: unwatch,
define: names => {
if(typeof names === 'string')
watchers.set(names, new MszWatcher);
else if(Array.isArray(names))
for(const name of names)
watchers.set(name, new MszWatcher);
else
throw 'names must be an array of names or a single name';
},
call: (name, ...args) => {
getWatcher(name).call(...args);
}, },
}; };
}; };

View file

@ -1,5 +1,9 @@
const crypto = require('crypto'); const crypto = require('crypto');
exports.strtr = (str, replacements) => str.toString().replace(
/{([^}]+)}/g, (match, key) => replacements[key] || match
);
const trim = function(str, chars, flags) { const trim = function(str, chars, flags) {
if(chars === undefined) if(chars === undefined)
chars = " \n\r\t\v\0"; chars = " \n\r\t\v\0";

146
build.js
View file

@ -1,3 +1,4 @@
// IMPORTS
const fs = require('fs'); const fs = require('fs');
const swc = require('@swc/core'); const swc = require('@swc/core');
const path = require('path'); const path = require('path');
@ -6,25 +7,42 @@ const postcss = require('postcss');
const utils = require('./assets/utils.js'); const utils = require('./assets/utils.js');
const assproc = require('./assets/assproc.js'); const assproc = require('./assets/assproc.js');
// CONFIG
const rootDir = __dirname; const rootDir = __dirname;
const modulesDir = path.join(rootDir, 'node_modules'); const srcDir = path.join(rootDir, 'assets');
const srcCurrentInfo = path.join(srcDir, 'current.json');
const assetsDir = path.join(rootDir, 'assets');
const assetsCSS = path.join(assetsDir, 'misuzu.css');
const assetsJS = path.join(assetsDir, 'misuzu.js');
const assetsInfo = path.join(assetsDir, 'current.json');
const pubDir = path.join(rootDir, 'public'); const pubDir = path.join(rootDir, 'public');
const pubIndex = path.join(pubDir, 'index.html'); const pubAssetsDir = path.join(pubDir, 'assets');
const pubAssets = '/assets';
const pubAssetsFull = path.join(pubDir, pubAssets);
const pubAssetCSSFormat = '%s-%s.css';
const pubAssetJSFormat = '%s-%s.js';
const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug')); const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
const buildTasks = {
js: [
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
],
css: [
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
],
};
// PREP
const postcssPlugins = [ require('autoprefixer')({ remove: false }) ];
if(!isDebugBuild)
postcssPlugins.push(require('cssnano')({
preset: [
'cssnano-preset-default',
{
minifyGradients: false,
reduceIdents: false,
zindex: true,
}
],
}));
const swcJscOptions = { const swcJscOptions = {
target: 'es2016', target: 'es2021',
loose: false, loose: false,
externalHelpers: false, externalHelpers: false,
keepClassNames: true, keepClassNames: true,
@ -51,39 +69,89 @@ const swcJscOptions = {
}, },
}; };
const postcssPlugins = [];
if(!isDebugBuild) postcssPlugins.push(require('cssnano'));
postcssPlugins.push(require('autoprefixer')({
remove: false,
}));
fs.mkdirSync(pubAssetsFull, { recursive: true });
// BUILD
(async () => { (async () => {
const mszCssName = await assproc.process(assetsCSS, { 'prefix': '@', 'entry': 'main.css' }) const files = {};
.then(output => postcss(postcssPlugins).process(output, { from: assetsCSS }).then(output => {
const mszCssName = path.join(pubAssets, util.format(pubAssetCSSFormat, 'misuzu', utils.shortHash(output.css)));
fs.writeFileSync(path.join(pubDir, mszCssName), output.css);
return mszCssName;
}));
const mszJsName = await assproc.process(assetsJS, { 'prefix': '#', 'entry': 'main.js' }) console.log('Ensuring assets directory exists...');
.then(output => swc.transform(output, { fs.mkdirSync(pubAssetsDir, { recursive: true });
filename: 'misuzu.js',
console.log();
console.log('JS assets');
for(const info of buildTasks.js) {
console.log(`=> Building ${info.source}...`);
let origTarget = undefined;
if('es' in info) {
origTarget = swcJscOptions.target;
swcJscOptions.target = info.es;
}
const assprocOpts = {
prefix: '#',
entry: info.entry || 'main.js',
};
const swcOpts = {
filename: info.source,
sourceMaps: false, sourceMaps: false,
isModule: false, isModule: false,
minify: !isDebugBuild, minify: !isDebugBuild,
jsc: swcJscOptions, jsc: swcJscOptions,
}).then(async output => { };
const mszJsName = path.join(pubAssets, util.format(pubAssetJSFormat, 'misuzu', utils.shortHash(output.code)));
fs.writeFileSync(path.join(pubDir, mszJsName), output.code);
return mszJsName;
}));
fs.writeFileSync(assetsInfo, JSON.stringify({ const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
mszjs: mszJsName, .then(output => swc.transform(output, swcOpts))
mszcss: mszCssName, .then(output => {
})); const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) });
const pubName = path.join(info.target || '', name);
assproc.housekeep(pubAssetsFull); console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.code);
return pubName;
});
if(origTarget !== undefined)
swcJscOptions.target = origTarget;
files[info.source] = pubName;
}
console.log();
console.log('CSS assets');
for(const info of buildTasks.css) {
console.log(`=> Building ${info.source}...`);
const sourcePath = path.join(srcDir, info.source);
const assprocOpts = {
prefix: '@',
entry: info.entry || 'main.css',
};
const postcssOpts = { from: sourcePath };
files[info.source] = await assproc.process(sourcePath, assprocOpts)
.then(output => postcss(postcssPlugins).process(output, postcssOpts)
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.css) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.css);
return pubName;
}));
}
console.log();
console.log('Writing assets info...');
fs.writeFileSync(srcCurrentInfo, JSON.stringify(files));
console.log();
console.log('Cleaning up old builds...');
assproc.housekeep(pubAssetsDir);
})(); })();

View file

@ -3,11 +3,13 @@
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"flashwave/index": "dev-master", "flashwave/index": "dev-master",
"twig/twig": "^3.0", "flashwave/sasae": "dev-master",
"erusev/parsedown": "~1.6", "erusev/parsedown": "~1.6",
"chillerlan/php-qrcode": "^4.3", "chillerlan/php-qrcode": "^4.3",
"symfony/mailer": "^6.0", "symfony/mailer": "^6.0",
"matomo/device-detector": "^6.1" "matomo/device-detector": "^6.1",
"sentry/sdk": "^4.0",
"flashwave/syokuhou": "dev-master"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [
@ -27,7 +29,8 @@
"preferred-install": "dist", "preferred-install": "dist",
"allow-plugins": { "allow-plugins": {
"composer/installers": true, "composer/installers": true,
"wikimedia/composer-merge-plugin": false "wikimedia/composer-merge-plugin": false,
"php-http/discovery": true
} }
}, },
"require-dev": { "require-dev": {

1146
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
# Example configuration for Misuzu
# and ; can be used at the start of a line for comments.
database:dsn mariadb://<user>:<pass>@<host>/<name>?charset=utf8mb4
;sentry:dsn https://sentry dsn here
;sentry:tracesRate 1.0
;sentry:profilesRate 1.0

View file

@ -1,10 +0,0 @@
; Example configuration for Misuzu
[Database]
driver = mysql
host = localhost
port = 3306
username = username
password = password
dbname = database
charset = utf8mb4

View file

@ -0,0 +1,123 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class NewPermissionsSystem_20230830_213930 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
// make sure cron doesn't fuck us over
$conn->execute('DELETE FROM msz_config WHERE config_name = "perms.needsRecalc"');
$conn->execute('
CREATE TABLE msz_perms (
user_id INT(10) UNSIGNED NULL DEFAULT NULL,
role_id INT(10) UNSIGNED NULL DEFAULT NULL,
forum_id INT(10) UNSIGNED NULL DEFAULT NULL,
perms_category VARBINARY(64) NOT NULL,
perms_allow BIGINT(20) UNSIGNED NOT NULL,
perms_deny BIGINT(20) UNSIGNED NOT NULL,
UNIQUE KEY perms_unique (user_id, role_id, forum_id, perms_category),
KEY perms_user_foreign (user_id),
KEY perms_role_foreign (role_id),
KEY perms_forum_foreign (forum_id),
KEY perms_category_index (perms_category),
CONSTRAINT perms_user_foreign
FOREIGN KEY (user_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT perms_role_foreign
FOREIGN KEY (role_id)
REFERENCES msz_roles (role_id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT perms_forum_foreign
FOREIGN KEY (forum_id)
REFERENCES msz_forum_categories (forum_id)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE=InnoDB COLLATE=utf8mb4_bin
');
$conn->execute('
ALTER TABLE msz_perms
ADD CONSTRAINT perms_53bit
CHECK (perms_allow >= 0 AND perms_deny >= 0 AND perms_allow <= 9007199254740991 AND perms_deny <= 9007199254740991)
');
$conn->execute('
CREATE TABLE msz_perms_calculated (
user_id INT(10) UNSIGNED NULL DEFAULT NULL,
forum_id INT(10) UNSIGNED NULL DEFAULT NULL,
perms_category VARBINARY(64) NOT NULL,
perms_calculated BIGINT(20) UNSIGNED NOT NULL,
UNIQUE KEY perms_calculated_unique (user_id, forum_id, perms_category),
KEY perms_calculated_user_foreign (user_id),
KEY perms_calculated_forum_foreign (forum_id),
KEY perms_calculated_category_index (perms_category),
CONSTRAINT perms_calculated_user_foreign
FOREIGN KEY (user_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT perms_calculated_forum_foreign
FOREIGN KEY (forum_id)
REFERENCES msz_forum_categories (forum_id)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE=InnoDB COLLATE=utf8mb4_bin
');
$conn->execute('
ALTER TABLE msz_perms_calculated
ADD CONSTRAINT perms_calculated_53bit
CHECK (perms_calculated >= 0 AND perms_calculated <= 9007199254740991)
');
$insert = $conn->prepare('INSERT INTO msz_perms (user_id, role_id, forum_id, perms_category, perms_allow, perms_deny) VALUES (?, ?, ?, ?, ?, ?)');
$result = $conn->query('SELECT user_id, role_id, general_perms_allow, general_perms_deny, user_perms_allow, user_perms_deny, changelog_perms_allow, changelog_perms_deny, news_perms_allow, news_perms_deny, forum_perms_allow, forum_perms_deny, comments_perms_allow, comments_perms_deny FROM msz_permissions');
while($result->next()) {
$insert->addParameter(1, $result->isNull(0) ? null : $result->getString(0));
$insert->addParameter(2, $result->isNull(1) ? null : $result->getString(1));
$insert->addParameter(3, null);
$insert->addParameter(4, 'user');
$insert->addParameter(5, $result->getInteger(4));
$insert->addParameter(6, $result->getInteger(5));
$insert->execute();
$allow = $result->getInteger(2);
$allow |= $result->getInteger(6) << 8;
$allow |= $result->getInteger(8) << 16;
$allow |= $result->getInteger(10) << 24;
$allow |= $result->getInteger(12) << 32;
$deny = $result->getInteger(3);
$deny |= $result->getInteger(7) << 8;
$deny |= $result->getInteger(9) << 16;
$deny |= $result->getInteger(11) << 24;
$deny |= $result->getInteger(13) << 32;
$insert->addParameter(4, 'global');
$insert->addParameter(5, $allow);
$insert->addParameter(6, $deny);
$insert->execute();
}
$result = $conn->query('SELECT user_id, role_id, forum_id, forum_perms_allow, forum_perms_deny FROM msz_forum_permissions');
while($result->next()) {
$insert->addParameter(1, $result->isNull(0) ? null : $result->getString(0));
$insert->addParameter(2, $result->isNull(1) ? null : $result->getString(1));
$insert->addParameter(3, $result->getString(2));
$insert->addParameter(4, 'forum');
$insert->addParameter(5, $result->getInteger(3));
$insert->addParameter(6, $result->getInteger(4));
$insert->execute();
}
$conn->execute('DROP TABLE msz_forum_permissions');
$conn->execute('DROP TABLE msz_permissions');
// schedule recalc
$conn->execute('INSERT INTO msz_config (config_name, config_value) VALUES ("perms.needsRecalc", "b:1;")');
}
}

View file

@ -0,0 +1,48 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class CreateMessagesTable_20240130_233734 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_messages (
msg_id BINARY(8) NOT NULL,
msg_owner_id INT(10) UNSIGNED NOT NULL,
msg_author_id INT(10) UNSIGNED NULL DEFAULT NULL,
msg_recipient_id INT(10) UNSIGNED NULL DEFAULT NULL,
msg_reply_to BINARY(8) NULL DEFAULT NULL,
msg_title TINYTEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci",
msg_body TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci",
msg_parser TINYINT(3) UNSIGNED NOT NULL,
msg_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
msg_sent TIMESTAMP NULL DEFAULT NULL,
msg_read TIMESTAMP NULL DEFAULT NULL,
msg_deleted TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (msg_id, msg_owner_id),
KEY messages_owner_foreign (msg_owner_id),
KEY messages_author_foreign (msg_author_id),
KEY messages_recipient_foreign (msg_recipient_id),
KEY messages_reply_to_index (msg_reply_to),
KEY messages_created_index (msg_created),
KEY messages_sent_index (msg_sent),
KEY messages_read_index (msg_read),
KEY messages_deleted_index (msg_deleted),
CONSTRAINT messages_owner_foreign
FOREIGN KEY (msg_owner_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT messages_author_foreign
FOREIGN KEY (msg_author_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE SET NULL,
CONSTRAINT messages_recipient_foreign
FOREIGN KEY (msg_recipient_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE SET NULL
) ENGINE=InnoDB COLLATE=utf8mb4_bin;
');
}
}

View file

@ -0,0 +1,14 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('UPDATE msz_messages SET msg_title = TO_BASE64(msg_title), msg_body = TO_BASE64(msg_body)');
$conn->execute('
ALTER TABLE `msz_messages`
CHANGE COLUMN `msg_title` `msg_title` TINYBLOB NOT NULL AFTER `msg_reply_to`,
CHANGE COLUMN `msg_body` `msg_body` BLOB NOT NULL AFTER `msg_title`;
');
}
}

View file

@ -1,12 +1,29 @@
# Source Code Repositories # Source Code Repositories
- [Misuzu](https://git.flash.moe/flashii/misuzu): Backend of the main website. Below are a number of links to source code repositories related to Flashii.net and its services.
- [Sharp Chat](https://git.flash.moe/flashii/sharp-chat): Chat Server software.
- [Backup Manager](https://git.flash.moe/flashii/backup-manager): Program that runs every day at 12:00am UTC to back up any user generated content. ## Websites & Services
- [Index](https://git.flash.moe/flash/index): Base library used in almost any component of the website that uses PHP. - [Misuzu](https://patchii.net/flashii/misuzu): Backend of the main website.
- [Uiharu](https://git.flash.moe/flashii/uiharu): Service for looking up URL metadata. - [Sharp Chat](https://patchii.net/flashii/sharp-chat): Chat Server software.
- [Futami](https://git.flash.moe/flashii/futami): Common data shared between the chat clients. - [Futami](https://patchii.net/flashii/futami): Common data shared between the chat clients.
- [AJAX Chat (fork)](https://git.flash.moe/flashii/ajax-chat): Old chat software (2013-2015). - [Mami](https://patchii.net/flashii/mami): Web client for chat.
- [Seria](https://git.flash.moe/flashii/seria): Software used by the torrent tracker. - [Ami](https://patchii.net/flashii/ami): Web client for chat for older browsers.
- [Mince](https://git.flash.moe/flashii/mince): Source code for the Minecraft servers subwebsite. - [EEPROM](https://patchii.net/flashii/eeprom): Service for file uploading.
- [Awaki](https://git.flash.moe/flashii/awaki): Redirect service hosted on fii.moe. - [Uiharu](https://patchii.net/flashii/uiharu): Service for looking up URL metadata.
- [Seria](https://patchii.net/flashii/seria): Software used by the downloads tracker.
- [Mince](https://patchii.net/flashii/mince): Source code for the Minecraft servers subwebsite.
- [Awaki](https://patchii.net/flashii/awaki): Redirect service hosted on fii.moe.
## Tools & Software
- [SoFii](https://patchii.net/flashii/sofii): Launcher for Soldier of Fortune 2
- [MCExts](https://patchii.net/flashii/mcexts): Minecraft Client and Server extensions.
- [Backup Tools](https://patchii.net/flashii/backup-tools): Scripts that run every day at 12:00am UTC to back up any user generated content.
## First-Party Libraries
- [Index](https://patchii.net/flash/index): Base library used in almost any component of the website that uses PHP.
- [Sasae](https://patchii.net/flash/sasae): Extension to the Twig templating library.
- [Syokuhou](https://patchii.net/flash/syokuhou): Configuration library.
## Historical
- [AJAX Chat (fork)](https://patchii.net/flashii/ajax-chat): Old chat software (2013-2015). Still kept on life support for the nostalgia.
- [Hajime](https://patchii.net/flash/hajime): Cleaned up source of an older version of the website (2014-2015).

View file

@ -3,7 +3,8 @@ namespace Misuzu;
use Index\Environment; use Index\Environment;
use Index\Data\DbTools; use Index\Data\DbTools;
use Misuzu\Config\DbConfig; use Syokuhou\DbConfig;
use Syokuhou\SharpConfig;
define('MSZ_STARTUP', microtime(true)); define('MSZ_STARTUP', microtime(true));
define('MSZ_ROOT', __DIR__); define('MSZ_ROOT', __DIR__);
@ -19,36 +20,28 @@ define('MSZ_ASSETS', MSZ_ROOT . '/assets');
require_once MSZ_ROOT . '/vendor/autoload.php'; require_once MSZ_ROOT . '/vendor/autoload.php';
Environment::setDebug(MSZ_DEBUG); Environment::setDebug(MSZ_DEBUG);
mb_internal_encoding('utf-8'); mb_internal_encoding('UTF-8');
date_default_timezone_set('utc'); date_default_timezone_set('UTC');
require_once MSZ_ROOT . '/utility.php'; $cfg = SharpConfig::fromFile(MSZ_CONFIG . '/config.cfg');
require_once MSZ_SOURCE . '/perms.php';
require_once MSZ_SOURCE . '/manage.php';
require_once MSZ_SOURCE . '/url.php';
require_once MSZ_SOURCE . '/Forum/perms.php';
require_once MSZ_SOURCE . '/Forum/forum.php';
require_once MSZ_SOURCE . '/Forum/leaderboard.php';
require_once MSZ_SOURCE . '/Forum/post.php';
require_once MSZ_SOURCE . '/Forum/topic.php';
require_once MSZ_SOURCE . '/Forum/validate.php';
$dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED); if($cfg->hasValues('sentry:dsn'))
(function($cfg) {
\Sentry\init([
'dsn' => $cfg->getString('dsn'),
'traces_sample_rate' => $cfg->getFloat('tracesRate', 0.2),
'profiles_sample_rate' => $cfg->getFloat('profilesRate', 0.2),
]);
if(empty($dbConfig)) { set_exception_handler(function(\Throwable $ex) {
echo 'Database config is missing.'; \Sentry\captureException($ex);
exit; });
} })($cfg->scopeTo('sentry'));
define('MSZ_DB_INIT', 'SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';'); $db = DbTools::create($cfg->getString('database:dsn', 'null:'));
$db->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';');
$db = DbTools::create($dbConfig['dsn']); $cfg = new DbConfig($db, 'msz_config');
$db->execute(MSZ_DB_INIT);
DB::init(DbTools::parse($dbConfig['dsn']));
DB::exec(MSZ_DB_INIT);
$cfg = new DbConfig($db);
Mailer::init($cfg->scopeTo('mail')); Mailer::init($cfg->scopeTo('mail'));

692
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{ {
"dependencies": { "dependencies": {
"@swc/core": "^1.3.69", "@swc/core": "^1.5.24",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.19",
"cssnano": "^6.0.1", "cssnano": "^6.1.2",
"postcss": "^8.4.26" "postcss": "^8.4.38"
} }
} }

View file

@ -43,11 +43,11 @@ header('Content-Type: text/plain; charset=utf-8');
if($_SERVER['REQUEST_METHOD'] !== 'POST') if($_SERVER['REQUEST_METHOD'] !== 'POST')
die('no'); die('no');
$config = MSZ_ROOT . '/config/github.ini'; $config = MSZ_CONFIG . '/github.ini';
if(!is_file($config)) if(!is_file($config))
die('config missing'); die('config missing');
$config = parse_ini_file(MSZ_ROOT . '/config/github.ini', true); $config = parse_ini_file($config, true);
if(empty($config['tokens']['token'])) if(empty($config['tokens']['token']))
die('config invalid'); die('config invalid');

View file

@ -1,4 +0,0 @@
<?php
namespace Misuzu;
url_redirect('auth-login');

View file

@ -4,14 +4,17 @@ namespace Misuzu;
use Exception; use Exception;
use Misuzu\Auth\AuthTokenCookie; use Misuzu\Auth\AuthTokenCookie;
if($msz->isLoggedIn()) { $urls = $msz->getURLs();
url_redirect('index'); $authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return; return;
} }
$users = $msz->getUsers(); $authCtx = $msz->getAuthContext();
$sessions = $msz->getSessions(); $users = $msz->getUsersContext()->getUsers();
$loginAttempts = $msz->getLoginAttempts(); $sessions = $authCtx->getSessions();
$loginAttempts = $authCtx->getLoginAttempts();
if(!empty($_GET['resolve'])) { if(!empty($_GET['resolve'])) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
@ -23,7 +26,7 @@ if(!empty($_GET['resolve'])) {
echo json_encode([ echo json_encode([
'id' => 0, 'id' => 0,
'name' => '', 'name' => '',
'avatar' => url('user-avatar', ['res' => 200, 'user' => 0]), 'avatar' => $urls->format('user-avatar', ['res' => 200, 'user' => 0]),
]); ]);
return; return;
} }
@ -31,7 +34,7 @@ if(!empty($_GET['resolve'])) {
echo json_encode([ echo json_encode([
'id' => (int)$userInfo->getId(), 'id' => (int)$userInfo->getId(),
'name' => $userInfo->getName(), 'name' => $userInfo->getName(),
'avatar' => url('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]), 'avatar' => $urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
]); ]);
return; return;
} }
@ -93,7 +96,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
try { try {
$userInfo = $users->getUser($_POST['login']['username'], 'login'); $userInfo = $users->getUser($_POST['login']['username'], 'login');
} catch(RuntimeException $ex) { } catch(Exception $ex) {
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo); $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
$notices[] = $loginFailedError; $notices[] = $loginFailedError;
break; break;
@ -113,17 +116,15 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
if($userInfo->passwordNeedsRehash()) if($userInfo->passwordNeedsRehash())
$users->updateUser($userInfo, password: $_POST['login']['password']); $users->updateUser($userInfo, password: $_POST['login']['password']);
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userInfo->getId(), $loginPermVal)) { if(!empty($loginPermCat) && $loginPermVal > 0 && !$msz->getPerms()->checkPermissions($loginPermCat, $loginPermVal, $userInfo)) {
$notices[] = "Login succeeded, but you're not allowed to browse the site right now."; $notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo); $loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
break; break;
} }
if($userInfo->hasTOTPKey()) { if($userInfo->hasTOTPKey()) {
$tfaToken = $msz->getTFASessions()->createToken($userInfo); $tfaToken = $authCtx->getTwoFactorAuthSessions()->createToken($userInfo);
url_redirect('auth-two-factor', [ Tools::redirect($urls->format('auth-two-factor', ['token' => $tfaToken]));
'token' => $tfaToken,
]);
return; return;
} }
@ -131,12 +132,12 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
try { try {
$sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo); $sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
} catch(RuntimeException $ex) { } catch(Exception $ex) {
$notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!"; $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
break; break;
} }
$tokenBuilder = $msz->getAuthInfo()->getTokenInfo()->toBuilder(); $tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
$tokenBuilder->setUserId($userInfo); $tokenBuilder->setUserId($userInfo);
$tokenBuilder->setSessionToken($sessionInfo); $tokenBuilder->setSessionToken($sessionInfo);
$tokenBuilder->removeImpersonatedUserId(); $tokenBuilder->removeImpersonatedUserId();
@ -144,10 +145,10 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo)); AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
if(!is_local_url($loginRedirect)) if(!Tools::isLocalURL($loginRedirect))
$loginRedirect = url('index'); $loginRedirect = $urls->format('index');
redirect($loginRedirect); Tools::redirect($loginRedirect);
return; return;
} }
@ -155,7 +156,7 @@ $welcomeMode = !empty($_GET['welcome']);
$loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login']['username']) ? $_POST['login']['username'] : ( $loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login']['username']) ? $_POST['login']['username'] : (
!empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : '' !empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : ''
); );
$loginRedirect = $welcomeMode ? url('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? url('index'); $loginRedirect = $welcomeMode ? $urls->format('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? $urls->format('index');
$canRegisterAccount = !$siteIsPrivate; $canRegisterAccount = !$siteIsPrivate;
Template::render('auth.login', [ Template::render('auth.login', [

View file

@ -3,15 +3,17 @@ namespace Misuzu;
use Misuzu\Auth\AuthTokenCookie; use Misuzu\Auth\AuthTokenCookie;
if($msz->isLoggedIn()) { $authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest()) {
Template::render('auth.logout'); Template::render('auth.logout');
return; return;
} }
$tokenInfo = $msz->getAuthInfo()->getTokenInfo(); $tokenInfo = $authInfo->getTokenInfo();
$msz->getSessions()->deleteSessions(sessionTokens: $tokenInfo->getSessionToken()); $authCtx = $msz->getAuthContext();
$authCtx->getSessions()->deleteSessions(sessionTokens: $tokenInfo->getSessionToken());
$tokenBuilder = $tokenInfo->toBuilder(); $tokenBuilder = $tokenInfo->toBuilder();
$tokenBuilder->removeUserId(); $tokenBuilder->removeUserId();
@ -22,4 +24,4 @@ if($msz->isLoggedIn()) {
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo)); AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
} }
url_redirect('index'); Tools::redirect($msz->getURLs()->format('index'));;

View file

@ -4,14 +4,17 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
if($msz->isLoggedIn()) { $urls = $msz->getURLs();
url_redirect('settings-account'); $authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('settings-account'));
return; return;
} }
$users = $msz->getUsers(); $authCtx = $msz->getAuthContext();
$recoveryTokens = $msz->getRecoveryTokens(); $users = $msz->getUsersContext()->getUsers();
$loginAttempts = $msz->getLoginAttempts(); $recoveryTokens = $authCtx->getRecoveryTokens();
$loginAttempts = $authCtx->getLoginAttempts();
$reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : []; $reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : [];
$forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : []; $forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : [];
@ -23,7 +26,7 @@ if($userId > 0)
try { try {
$userInfo = $users->getUser((string)$userId, 'id'); $userInfo = $users->getUser((string)$userId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
url_redirect('auth-forgot'); Tools::redirect($urls->format('auth-forgot'));
return; return;
} }
@ -64,8 +67,9 @@ while($canResetPassword) {
break; break;
} }
if(User::validatePassword($passwordNew) !== '') { $passwordValidation = $users->validatePassword($passwordNew);
$notices[] = 'Your password is too weak!'; if($passwordValidation !== '') {
$notices[] = $users->validatePasswordText($passwordValidation);
break; break;
} }
@ -77,7 +81,7 @@ while($canResetPassword) {
$recoveryTokens->invalidateToken($tokenInfo); $recoveryTokens->invalidateToken($tokenInfo);
url_redirect('auth-login', ['redirect' => '/']); Tools::redirect($urls->format('auth-login', ['redirect' => '/']));
return; return;
} }
@ -130,7 +134,7 @@ while($canResetPassword) {
} }
} }
url_redirect('auth-reset', ['user' => $forgotUser->getId()]); Tools::redirect($urls->format('auth-reset', ['user' => $forgotUser->getId()]));
return; return;
} }

View file

@ -4,13 +4,17 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
if($msz->isLoggedIn()) { $urls = $msz->getURLs();
url_redirect('index'); $authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return; return;
} }
$users = $msz->getUsers(); $authCtx = $msz->getAuthContext();
$roles = $msz->getRoles(); $usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$config = $msz->getConfig(); $config = $msz->getConfig();
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : []; $register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
@ -29,7 +33,7 @@ $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
// for fast matching // for fast matching
$restricted = ''; $restricted = '';
$loginAttempts = $msz->getLoginAttempts(); $loginAttempts = $authCtx->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress); $remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
while(!$restricted && !empty($register)) { while(!$restricted && !empty($register)) {
@ -65,21 +69,20 @@ while(!$restricted && !empty($register)) {
break; break;
} }
$usernameValidation = User::validateUsername($register['username']); $usernameValidation = $users->validateName($register['username']);
if($usernameValidation !== '') if($usernameValidation !== '')
$notices[] = User::usernameValidationErrorString($usernameValidation); $notices[] = $users->validateNameText($usernameValidation);
$emailValidation = User::validateEMailAddress($register['email']); $emailValidation = $users->validateEMailAddress($register['email']);
if($emailValidation !== '') if($emailValidation !== '')
$notices[] = $emailValidation === 'in-use' $notices[] = $users->validateEMailAddressText($emailValidation);
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
if($register['password_confirm'] !== $register['password']) if($register['password_confirm'] !== $register['password'])
$notices[] = 'The given passwords don\'t match.'; $notices[] = 'The given passwords don\'t match.';
if(User::validatePassword($register['password']) !== '') $passwordValidation = $users->validatePassword($register['password']);
$notices[] = 'Your password is too weak!'; if($passwordValidation !== '')
$notices[] = $users->validatePasswordText($passwordValidation);
if(!empty($notices)) if(!empty($notices))
break; break;
@ -102,8 +105,12 @@ while(!$restricted && !empty($register)) {
$users->addRoles($userInfo, $defaultRoleInfo); $users->addRoles($userInfo, $defaultRoleInfo);
$config->setString('users.newest', $userInfo->getId()); $config->setString('users.newest', $userInfo->getId());
$msz->getPerms()->precalculatePermissions(
$msz->getForumContext()->getCategories(),
[$userInfo->getId()]
);
url_redirect('auth-login-welcome', ['username' => $userInfo->getName()]); Tools::redirect($urls->format('auth-login-welcome', ['username' => $userInfo->getName()]));
return; return;
} }

View file

@ -3,6 +3,7 @@ namespace Misuzu;
use Misuzu\Auth\AuthTokenCookie; use Misuzu\Auth\AuthTokenCookie;
$urls = $msz->getURLs();
if(CSRF::validateRequest()) { if(CSRF::validateRequest()) {
$tokenInfo = $msz->getAuthInfo()->getTokenInfo(); $tokenInfo = $msz->getAuthInfo()->getTokenInfo();
@ -14,9 +15,9 @@ if(CSRF::validateRequest()) {
$tokenInfo = $tokenBuilder->toInfo(); $tokenInfo = $tokenBuilder->toInfo();
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo)); AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
url_redirect('manage-user', ['user' => $impUserId]); Tools::redirect($urls->format('manage-user', ['user' => $impUserId]));
return; return;
} }
} }
url_redirect('index'); Tools::redirect($urls->format('index'));

View file

@ -5,15 +5,18 @@ use RuntimeException;
use Misuzu\TOTPGenerator; use Misuzu\TOTPGenerator;
use Misuzu\Auth\AuthTokenCookie; use Misuzu\Auth\AuthTokenCookie;
if($msz->isLoggedIn()) { $urls = $msz->getURLs();
url_redirect('index'); $authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return; return;
} }
$users = $msz->getUsers(); $authCtx = $msz->getAuthContext();
$sessions = $msz->getSessions(); $users = $msz->getUsersContext()->getUsers();
$tfaSessions = $msz->getTFASessions(); $sessions = $authCtx->getSessions();
$loginAttempts = $msz->getLoginAttempts(); $tfaSessions = $authCtx->getTwoFactorAuthSessions();
$loginAttempts = $authCtx->getLoginAttempts();
$ipAddress = $_SERVER['REMOTE_ADDR']; $ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
@ -29,7 +32,7 @@ $tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['toke
$tokenUserId = $tfaSessions->getTokenUserId($tokenString); $tokenUserId = $tfaSessions->getTokenUserId($tokenString);
if(empty($tokenUserId)) { if(empty($tokenUserId)) {
url_redirect('auth-login'); Tools::redirect($urls->format('auth-login'));
return; return;
} }
@ -38,7 +41,7 @@ $userInfo = $users->getUser($tokenUserId, 'id');
// checking user_totp_key specifically because there's a fringe chance that // checking user_totp_key specifically because there's a fringe chance that
// there's a token present, but totp is actually disabled // there's a token present, but totp is actually disabled
if(!$userInfo->hasTOTPKey()) { if(!$userInfo->hasTOTPKey()) {
url_redirect('auth-login'); Tools::redirect($urls->format('auth-login'));
return; return;
} }
@ -84,7 +87,7 @@ while(!empty($twofactor)) {
break; break;
} }
$tokenBuilder = $msz->getAuthInfo()->getTokenInfo()->toBuilder(); $tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
$tokenBuilder->setUserId($userInfo); $tokenBuilder->setUserId($userInfo);
$tokenBuilder->setSessionToken($sessionInfo); $tokenBuilder->setSessionToken($sessionInfo);
$tokenBuilder->removeImpersonatedUserId(); $tokenBuilder->removeImpersonatedUserId();
@ -92,16 +95,16 @@ while(!empty($twofactor)) {
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo)); AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
if(!is_local_url($redirect)) if(!Tools::isLocalURL($redirect))
$redirect = url('index'); $redirect = $urls->format('index');
redirect($redirect); Tools::redirect($redirect);
return; return;
} }
Template::render('auth.twofactor', [ Template::render('auth.twofactor', [
'twofactor_notices' => $notices, 'twofactor_notices' => $notices,
'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : url('index'), 'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : $urls->format('index'),
'twofactor_attempts_remaining' => $remainingAttempts, 'twofactor_attempts_remaining' => $remainingAttempts,
'twofactor_token' => $tokenString, 'twofactor_token' => $tokenString,
]); ]);

View file

@ -3,34 +3,26 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
// basing whether or not this is an xhr request on whether a referrer header is present $usersCtx = $msz->getUsersContext();
// this page is never directy accessed, under normal circumstances $redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getURLs()->format('index');
$redirect = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : url('index');
if(!is_local_url($redirect)) { if(!Tools::isLocalURL($redirect))
echo render_info('Possible request forgery detected.', 403); Template::displayInfo('Possible request forgery detected.', 403);
return;
}
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest())
echo render_info("Couldn't verify this request, please refresh the page and try again.", 403); Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
if(!$msz->isLoggedIn()) { $authInfo = $msz->getAuthInfo();
echo render_info('You must be logged in to manage comments.', 403); if(!$authInfo->isLoggedIn())
return; Template::displayInfo('You must be logged in to manage comments.', 403);
}
if($msz->hasActiveBan()) { $currentUserInfo = $authInfo->getUserInfo();
echo render_info('You have been banned, check your profile for more information.', 403);
return;
}
$currentUserInfo = $msz->getActiveUser(); if($usersCtx->hasActiveBan($currentUserInfo))
Template::displayInfo('You have been banned, check your profile for more information.', 403);
$comments = $msz->getComments(); $comments = $msz->getComments();
$commentPerms = perms_for_comments($currentUserInfo->getId()); $perms = $authInfo->getPerms('global');
$commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); $commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$commentMode = (string)filter_input(INPUT_GET, 'm'); $commentMode = (string)filter_input(INPUT_GET, 'm');
@ -38,69 +30,52 @@ $commentVote = (int)filter_input(INPUT_GET, 'v', FILTER_SANITIZE_NUMBER_INT);
if(!empty($commentId)) { if(!empty($commentId)) {
try { try {
$commentInfo = $comments->getPostById($commentId); $commentInfo = $comments->getPost($commentId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_info('Post not found.', 404); Template::displayInfo('Post not found.', 404);
return;
} }
$categoryInfo = $comments->getCategoryByPost($commentInfo); $categoryInfo = $comments->getCategory(postInfo: $commentInfo);
} }
if($commentMode !== 'create' && empty($commentInfo)) { if($commentMode !== 'create' && empty($commentInfo))
echo render_error(400); Template::throwError(400);
return;
}
switch($commentMode) { switch($commentMode) {
case 'pin': case 'pin':
case 'unpin': case 'unpin':
if(!$commentPerms['can_pin'] && !$categoryInfo->isOwner($currentUserInfo)) { if(!$perms->check(Perm::G_COMMENTS_PIN) && !$categoryInfo->isOwner($currentUserInfo))
echo render_info("You're not allowed to pin comments.", 403); Template::displayInfo("You're not allowed to pin comments.", 403);
break;
}
if($commentInfo->isDeleted()) { if($commentInfo->isDeleted())
echo render_info("This comment doesn't exist!", 400); Template::displayInfo("This comment doesn't exist!", 400);
break;
}
if($commentInfo->isReply()) { if($commentInfo->isReply())
echo render_info("You can't pin replies!", 400); Template::displayInfo("You can't pin replies!", 400);
break;
}
$isPinning = $commentMode === 'pin'; $isPinning = $commentMode === 'pin';
if($isPinning) { if($isPinning) {
if($commentInfo->isPinned()) { if($commentInfo->isPinned())
echo render_info('This comment is already pinned.', 400); Template::displayInfo('This comment is already pinned.', 400);
break;
}
$comments->pinPost($commentInfo); $comments->pinPost($commentInfo);
} else { } else {
if(!$commentInfo->isPinned()) { if(!$commentInfo->isPinned())
echo render_info("This comment isn't pinned yet.", 400); Template::displayInfo("This comment isn't pinned yet.", 400);
break;
}
$comments->unpinPost($commentInfo); $comments->unpinPost($commentInfo);
} }
redirect($redirect . '#comment-' . $commentInfo->getId()); Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
break; break;
case 'vote': case 'vote':
if(!$commentPerms['can_vote'] && !$categoryInfo->isOwner($currentUserInfo)) { if(!$perms->check(Perm::G_COMMENTS_VOTE) && !$categoryInfo->isOwner($currentUserInfo))
echo render_info("You're not allowed to vote on comments.", 403); Template::displayInfo("You're not allowed to vote on comments.", 403);
break;
}
if($commentInfo->isDeleted()) { if($commentInfo->isDeleted())
echo render_info("This comment doesn't exist!", 400); Template::displayInfo("This comment doesn't exist!", 400);
break;
}
if($commentVote > 0) if($commentVote > 0)
$comments->addPostPositiveVote($commentInfo, $currentUserInfo); $comments->addPostPositiveVote($commentInfo, $currentUserInfo);
@ -109,30 +84,26 @@ switch($commentMode) {
else else
$comments->removePostVote($commentInfo, $currentUserInfo); $comments->removePostVote($commentInfo, $currentUserInfo);
redirect($redirect . '#comment-' . $commentInfo->getId()); Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
break; break;
case 'delete': case 'delete':
if(!$commentPerms['can_delete'] && !$categoryInfo->isOwner($currentUserInfo)) { $canDelete = $perms->check(Perm::G_COMMENTS_DELETE_OWN | Perm::G_COMMENTS_DELETE_ANY);
echo render_info("You're not allowed to delete comments.", 403); if(!$canDelete && !$categoryInfo->isOwner($currentUserInfo))
break; Template::displayInfo("You're not allowed to delete comments.", 403);
}
if($commentInfo->isDeleted()) { $canDeleteAny = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
echo render_info( if($commentInfo->isDeleted())
$commentPerms['can_delete_any'] ? 'This comment is already marked for deletion.' : "This comment doesn't exist.", Template::displayInfo(
$canDeleteAny ? 'This comment is already marked for deletion.' : "This comment doesn't exist.",
400 400
); );
break;
}
$isOwnComment = $commentInfo->getUserId() === $currentUserInfo->getId(); $isOwnComment = $commentInfo->getUserId() === $currentUserInfo->getId();
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment; $isModAction = $canDeleteAny && !$isOwnComment;
if(!$isModAction && !$isOwnComment) { if(!$isModAction && !$isOwnComment)
echo render_info("You're not allowed to delete comments made by others.", 403); Template::displayInfo("You're not allowed to delete comments made by others.", 403);
break;
}
$comments->deletePost($commentInfo); $comments->deletePost($commentInfo);
@ -146,19 +117,15 @@ switch($commentMode) {
$msz->createAuditLog('COMMENT_ENTRY_DELETE', [$commentInfo->getId()]); $msz->createAuditLog('COMMENT_ENTRY_DELETE', [$commentInfo->getId()]);
} }
redirect($redirect); Tools::redirect($redirect);
break; break;
case 'restore': case 'restore':
if(!$commentPerms['can_delete_any']) { if(!$perms->check(Perm::G_COMMENTS_DELETE_ANY))
echo render_info("You're not allowed to restore deleted comments.", 403); Template::displayInfo("You're not allowed to restore deleted comments.", 403);
break;
}
if(!$commentInfo->isDeleted()) { if(!$commentInfo->isDeleted())
echo render_info("This comment isn't in a deleted state.", 400); Template::displayInfo("This comment isn't in a deleted state.", 400);
break;
}
$comments->restorePost($commentInfo); $comments->restorePost($commentInfo);
@ -168,39 +135,33 @@ switch($commentMode) {
'<username>', '<username>',
]); ]);
redirect($redirect . '#comment-' . $commentInfo->getId()); Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
break; break;
case 'create': case 'create':
if(!$commentPerms['can_comment'] && !$categoryInfo->isOwner($currentUserInfo)) { if(!$perms->check(Perm::G_COMMENTS_CREATE) && !$categoryInfo->isOwner($currentUserInfo))
echo render_info("You're not allowed to post comments.", 403); Template::displayInfo("You're not allowed to post comments.", 403);
break;
}
if(empty($_POST['comment']) || !is_array($_POST['comment'])) { if(empty($_POST['comment']) || !is_array($_POST['comment']))
echo render_info('Missing data.', 400); Template::displayInfo('Missing data.', 400);
break;
}
try { try {
$categoryId = isset($_POST['comment']['category']) && is_string($_POST['comment']['category']) $categoryId = isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
? (int)$_POST['comment']['category'] ? (int)$_POST['comment']['category']
: 0; : 0;
$categoryInfo = $comments->getCategoryById($categoryId); $categoryInfo = $comments->getCategory(categoryId: $categoryId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_info('This comment category doesn\'t exist.', 404); Template::displayInfo('This comment category doesn\'t exist.', 404);
break;
} }
if($categoryInfo->isLocked() && !$commentPerms['can_lock']) { $canLock = $perms->check(Perm::G_COMMENTS_LOCK);
echo render_info('This comment category has been locked.', 403); if($categoryInfo->isLocked() && !$canLock)
break; Template::displayInfo('This comment category has been locked.', 403);
}
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : ''; $commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
$commentReply = (string)(!empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0); $commentReply = (string)(!empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0);
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock']; $commentLock = !empty($_POST['comment']['lock']) && $canLock;
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin']; $commentPin = !empty($_POST['comment']['pin']) && $perms->check(Perm::G_COMMENTS_PIN);
if($commentLock) { if($commentLock) {
if($categoryInfo->isLocked()) if($categoryInfo->isLocked())
@ -212,28 +173,24 @@ switch($commentMode) {
if(strlen($commentText) > 0) { if(strlen($commentText) > 0) {
$commentText = preg_replace("/[\r\n]{2,}/", "\n", $commentText); $commentText = preg_replace("/[\r\n]{2,}/", "\n", $commentText);
} else { } else {
if($commentPerms['can_lock']) { if($canLock) {
echo render_info('The action has been processed.', 400); Template::displayInfo('The action has been processed.', 400);
} else { } else {
echo render_info('Your comment is too short.', 400); Template::displayInfo('Your comment is too short.', 400);
} }
break; break;
} }
if(mb_strlen($commentText) > 5000) { if(mb_strlen($commentText) > 5000)
echo render_info('Your comment is too long.', 400); Template::displayInfo('Your comment is too long.', 400);
break;
}
if($commentReply > 0) { if($commentReply > 0) {
try { try {
$parentInfo = $comments->getPostById($commentReply); $parentInfo = $comments->getPost($commentReply);
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
if(!isset($parentInfo) || $parentInfo->isDeleted()) { if(!isset($parentInfo) || $parentInfo->isDeleted())
echo render_info('The comment you tried to reply to does not exist.', 404); Template::displayInfo('The comment you tried to reply to does not exist.', 404);
break;
}
} }
$commentInfo = $comments->createPost( $commentInfo = $comments->createPost(
@ -244,9 +201,9 @@ switch($commentMode) {
$commentPin $commentPin
); );
redirect($redirect . '#comment-' . $commentInfo->getId()); Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
break; break;
default: default:
echo render_info('Not found.', 404); Template::displayInfo('Not found.', 404);
} }

View file

@ -1,77 +1,179 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0; use stdClass;
$forumId = max($forumId, 0); use RuntimeException;
if($forumId === 0) { $forumCtx = $msz->getForumContext();
url_redirect('forum-index'); $forumCategories = $forumCtx->getCategories();
exit; $forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$categoryId = (int)filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
try {
$categoryInfo = $forumCategories->getCategory(categoryId: $categoryId);
} catch(RuntimeException $ex) {
Template::throwError(404);
} }
$forum = forum_get($forumId); $authInfo = $msz->getAuthInfo();
$forumUser = $msz->getActiveUser(); $perms = $authInfo->getPerms('forum', $categoryInfo);
$forumUserId = $forumUser === null ? '0' : $forumUser->getId();
if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) { $currentUser = $authInfo->getUserInfo();
echo render_error(404); $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
return;
if(!$perms->check(Perm::F_CATEGORY_VIEW))
Template::throwError(403);
if($usersCtx->hasActiveBan($currentUser))
$perms = $perms->apply(fn($calc) => $calc & (Perm::F_CATEGORY_LIST | Perm::F_CATEGORY_VIEW));
if($categoryInfo->isLink()) {
if($categoryInfo->hasLinkTarget()) {
$forumCategories->incrementCategoryClicks($categoryInfo);
Tools::redirect($categoryInfo->getLinkTarget());
return;
}
Template::throwError(404);
} }
$perms = forum_perms_get_user($forum['forum_id'], $forumUserId)[MSZ_FORUM_PERMS_GENERAL]; $forumPagination = new Pagination($forumTopics->countTopics(
categoryInfo: $categoryInfo,
global: true,
deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false
), 20);
if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { if(!$forumPagination->hasValidOffset())
echo render_error(403); Template::throwError(404);
return;
}
if(isset($forumUser) && $msz->hasActiveBan($forumUser)) $children = [];
$perms &= ~MSZ_FORUM_PERM_SET_WRITE; $topics = [];
Template::set('forum_perms', $perms); if($categoryInfo->mayHaveChildren()) {
$children = $forumCategories->getCategoryChildren($categoryInfo, hidden: false, asTree: true);
if($forum['forum_type'] == MSZ_FORUM_TYPE_LINK) { foreach($children as $childId => $child) {
forum_increment_clicks($forum['forum_id']); $childPerms = $authInfo->getPerms('forum', $child->info);
redirect($forum['forum_link']); if(!$childPerms->check(Perm::F_CATEGORY_LIST)) {
return; unset($category->children[$childId]);
} continue;
}
$forumPagination = new Pagination($forum['forum_topic_count'], 20); $childUnread = false;
if(!$forumPagination->hasValidOffset() && $forum['forum_topic_count'] > 0) { if($child->info->mayHaveChildren()) {
echo render_error(404); foreach($child->children as $grandChildId => $grandChild) {
return; $grandChildPerms = $authInfo->getPerms('forum', $grandChild->info);
} if(!$grandChildPerms->check(Perm::F_CATEGORY_LIST)) {
unset($child->children[$grandChildId]);
continue;
}
$forumMayHaveTopics = forum_may_have_topics($forum['forum_type']); $grandChildUnread = false;
$topics = $forumMayHaveTopics
? forum_topic_listing(
$forum['forum_id'],
$forumUserId,
$forumPagination->getOffset(),
$forumPagination->getRange(),
perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)
)
: [];
$forumMayHaveChildren = forum_may_have_children($forum['forum_type']); if($grandChild->info->mayHaveTopics()) {
$catIds = [$grandChild->info->getId()];
foreach($grandChild->childIds as $greatGrandChildId) {
$greatGrandChildPerms = $authInfo->getPerms('forum', $greatGrandChildId);
if(!$greatGrandChildPerms->check(Perm::F_CATEGORY_LIST))
$catIds[] = $greatGrandChildId;
}
if($forumMayHaveChildren) { $grandChildUnread = $forumCategories->checkCategoryUnread($catIds, $currentUser);
$forum['forum_subforums'] = forum_get_children($forum['forum_id'], $forumUserId); if($grandChildUnread)
$childUnread = true;
}
foreach($forum['forum_subforums'] as $skey => $subforum) { $grandChild->perms = $grandChildPerms;
$forum['forum_subforums'][$skey]['forum_subforums'] $grandChild->unread = $grandChildUnread;
= forum_get_children($subforum['forum_id'], $forumUserId); }
}
if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) {
$catIds = [$child->info->getId()];
foreach($child->childIds as $grandChildId) {
$grandChildPerms = $authInfo->getPerms('forum', $grandChildId);
if($grandChildPerms->check(Perm::F_CATEGORY_LIST))
$catIds[] = $grandChildId;
}
try {
$lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
} catch(RuntimeException $ex) {
$lastPostInfo = null;
}
if($lastPostInfo !== null) {
$child->lastPost = new stdClass;
$child->lastPost->info = $lastPostInfo;
$child->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
if($lastPostInfo->hasUserId()) {
$child->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
$child->lastPost->colour = $usersCtx->getUserColour($child->lastPost->user);
}
}
}
if($child->info->mayHaveTopics() && !$childUnread)
$childUnread = $forumCategories->checkCategoryUnread($child->info, $currentUser);
$child->perms = $childPerms;
$child->unread = $childUnread;
} }
} }
if($categoryInfo->mayHaveTopics()) {
$topicInfos = $forumTopics->getTopics(
categoryInfo: $categoryInfo,
global: true,
deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false,
pagination: $forumPagination,
);
foreach($topicInfos as $topicInfo) {
$topics[] = $topic = new stdClass;
$topic->info = $topicInfo;
$topic->unread = $forumTopics->checkTopicUnread($topicInfo, $currentUser);
$topic->participated = $forumTopics->checkTopicParticipated($topicInfo, $currentUser);
if($topicInfo->hasUserId()) {
$topic->user = $usersCtx->getUserInfo($topicInfo->getUserId());
$topic->colour = $usersCtx->getUserColour($topic->user);
}
try {
$topic->lastPost = new stdClass;
$topic->lastPost->info = $lastPostInfo = $forumPosts->getPost(
topicInfo: $topicInfo,
getLast: true,
deleted: $topicInfo->isDeleted() ? null : false,
);
if($lastPostInfo->hasUserId()) {
$topic->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
$topic->lastPost->colour = $usersCtx->getUserColour($topic->lastPost->user);
}
} catch(RuntimeException $ex) {
$topic->lastPost = null;
}
}
}
$perms = $perms->checkMany([
'can_create_topic' => Perm::F_TOPIC_CREATE,
]);
Template::render('forum.forum', [ Template::render('forum.forum', [
'forum_breadcrumbs' => forum_get_breadcrumbs($forum['forum_id']), 'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => forum_get_colour($forum['forum_id']), 'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'forum_may_have_topics' => $forumMayHaveTopics, 'forum_info' => $categoryInfo,
'forum_may_have_children' => $forumMayHaveChildren, 'forum_children' => $children,
'forum_info' => $forum,
'forum_topics' => $topics, 'forum_topics' => $topics,
'forum_pagination' => $forumPagination, 'forum_pagination' => $forumPagination,
'forum_show_mark_as_read' => $forumUser !== null, 'forum_show_mark_as_read' => $currentUser !== null,
'forum_perms' => $perms,
]); ]);

View file

@ -1,38 +1,191 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
$indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; use stdClass;
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0; use RuntimeException;
$currentUser = $msz->getActiveUser(); $forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$mode = (string)filter_input(INPUT_GET, 'm');
$authInfo = $msz->getAuthInfo();
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId(); $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
switch($indexMode) { if($mode === 'mark') {
case 'mark': if(!$authInfo->isLoggedIn())
url_redirect($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]); Template::throwError(403);
break;
default: $categoryId = filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
$categories = forum_get_root_categories($currentUserId);
$blankForum = count($categories) < 1;
foreach($categories as $key => $category) { if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$categories[$key]['forum_subforums'] = forum_get_children($category['forum_id'], $currentUserId); $categoryInfos = $categoryId === null
? $forumCategories->getCategories()
: $forumCategories->getCategoryChildren(parentInfo: $categoryId, includeSelf: true);
foreach($categories[$key]['forum_subforums'] as $skey => $sub) { foreach($categoryInfos as $categoryInfo) {
if(!forum_may_have_children($sub['forum_type'])) { $perms = $authInfo->getPerms('forum', $categoryInfo);
continue; if($perms->check(Perm::F_CATEGORY_LIST))
} $forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
$categories[$key]['forum_subforums'][$skey]['forum_subforums']
= forum_get_children($sub['forum_id'], $currentUserId);
}
} }
Template::render('forum.index', [ Tools::redirect($msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
'forum_categories' => $categories, return;
'forum_empty' => $blankForum, }
'forum_show_mark_as_read' => $currentUser !== null,
]); Template::render('confirm', [
break; 'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?',
'return' => $msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
'params' => [
'forum' => $categoryId,
]
]);
return;
} }
if($mode !== '')
Template::throwError(404);
$categories = $forumCategories->getCategories(hidden: false, asTree: true);
foreach($categories as $categoryId => $category) {
$perms = $authInfo->getPerms('forum', $category->info);
if(!$perms->check(Perm::F_CATEGORY_LIST)) {
unset($categories[$categoryId]);
continue;
}
$unread = false;
if($category->info->mayHaveChildren())
foreach($category->children as $childId => $child) {
$childPerms = $authInfo->getPerms('forum', $child->info);
if(!$childPerms->check(Perm::F_CATEGORY_LIST)) {
unset($category->children[$childId]);
continue;
}
$childUnread = false;
if($category->info->isListing()) {
if($child->info->mayHaveChildren()) {
foreach($child->children as $grandChildId => $grandChild) {
$grandChildPerms = $authInfo->getPerms('forum', $grandChild->info);
if(!$grandChildPerms->check(Perm::F_CATEGORY_LIST)) {
unset($child->children[$grandChildId]);
continue;
}
$grandChildUnread = false;
if($grandChild->info->mayHaveTopics()) {
$catIds = [$grandChild->info->getId()];
foreach($grandChild->childIds as $greatGrandChildId) {
$greatGrandChildPerms = $authInfo->getPerms('forum', $greatGrandChildId);
if($greatGrandChildPerms->check(Perm::F_CATEGORY_LIST))
$catIds[] = $greatGrandChildId;
}
$grandChildUnread = $forumCategories->checkCategoryUnread($catIds, $currentUser);
if($grandChildUnread)
$childUnread = true;
}
$grandChild->perms = $grandChildPerms;
$grandChild->unread = $grandChildUnread;
}
}
if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) {
$catIds = [$child->info->getId()];
foreach($child->childIds as $grandChildId) {
$grandChildPerms = $authInfo->getPerms('forum', $grandChildId);
if($grandChildPerms->check(Perm::F_CATEGORY_LIST))
$catIds[] = $grandChildId;
}
try {
$lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
} catch(RuntimeException $ex) {
$lastPostInfo = null;
}
if($lastPostInfo !== null) {
$child->lastPost = new stdClass;
$child->lastPost->info = $lastPostInfo;
$child->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
if($lastPostInfo->hasUserId()) {
$child->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
$child->lastPost->colour = $usersCtx->getUserColour($child->lastPost->user);
}
}
}
}
if($child->info->mayHaveTopics() && !$childUnread) {
$childUnread = $forumCategories->checkCategoryUnread($child->info, $currentUser);
if($childUnread)
$unread = true;
}
$child->perms = $childPerms;
$child->unread = $childUnread;
}
if($category->info->mayHaveTopics() && !$unread)
$unread = $forumCategories->checkCategoryUnread($category->info, $currentUser);
if(!$category->info->isListing()) {
if(!array_key_exists('0', $categories)) {
$categories['0'] = $root = new stdClass;
$root->info = null;
$root->perms = 0;
$root->unread = false;
$root->colour = null;
$root->children = [];
}
$categories['0']->children[$categoryId] = $category;
unset($categories[$categoryId]);
if($category->info->mayHaveChildren() || $category->info->mayHaveTopics()) {
$catIds = [$category->info->getId()];
foreach($category->childIds as $childId) {
$childPerms = $authInfo->getPerms('forum', $childId);
if($childPerms->check(Perm::F_CATEGORY_LIST))
$catIds[] = $childId;
}
try {
$lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
} catch(RuntimeException $ex) {
$lastPostInfo = null;
}
if($lastPostInfo !== null) {
$category->lastPost = new stdClass;
$category->lastPost->info = $lastPostInfo;
$category->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
if($lastPostInfo->hasUserId()) {
$category->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
$category->lastPost->colour = $usersCtx->getUserColour($category->lastPost->user);
}
}
}
}
$category->perms = $perms;
$category->unread = $unread;
}
Template::render('forum.index', [
'forum_categories' => $categories,
'forum_empty' => empty($categories),
'forum_show_mark_as_read' => $currentUser !== null,
]);

View file

@ -1,63 +1,113 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_FORUM, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) { use RuntimeException;
echo render_error(403);
return; if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_FORUM_LEADERBOARD_VIEW))
Template::throwError(403);
$forumCtx = $msz->getForumContext();
$usersCtx = $msz->getUsersContext();
$config = $cfg->getValues([
['forum_leader.first_year:i', 2018],
['forum_leader.first_month:i', 12],
'forum_leader.unranked.forum:a',
'forum_leader.unranked.topic:a',
]);
$mode = (string)filter_input(INPUT_GET, 'mode');
$yearMonth = (string)filter_input(INPUT_GET, 'id');
$year = $month = 0;
$currentYear = (int)date('Y');
$currentMonth = (int)date('m');
if(!empty($yearMonth)) {
$yearMonthLength = strlen($yearMonth);
if(($yearMonthLength !== 4 && $yearMonthLength !== 6) || !ctype_digit($yearMonth))
Template::throwError(404);
$year = (int)substr($yearMonth, 0, 4);
if($year < $config['forum_leader.first_year'] || $year > $currentYear)
Template::throwError(404);
if($yearMonthLength === 6) {
$month = (int)substr($yearMonth, 4, 2);
if($month < 1 || $month > 12 || ($year === $config['forum_leader.first_year'] && $month < $config['forum_leader.first_month']))
Template::throwError(404);
}
} }
$leaderboardMode = !empty($_GET['mode']) && is_string($_GET['mode']) && ctype_lower($_GET['mode']) ? $_GET['mode'] : ''; if(filter_has_var(INPUT_GET, 'allow_unranked')) {
$leaderboardId = !empty($_GET['id']) && is_string($_GET['id']) $unrankedForums = $unrankedTopics = [];
&& ctype_digit($_GET['id']) } else {
? $_GET['id'] $unrankedForums = $config['forum_leader.unranked.forum'];
: MSZ_FORUM_LEADERBOARD_CATEGORY_ALL; $unrankedTopics = $config['forum_leader.unranked.topic'];
$leaderboardIdLength = strlen($leaderboardId);
$leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null;
$leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) : null;
if(empty($_GET['allow_unranked'])) {
[
'forum_leader.unranked.forum' => $unrankedForums,
'forum_leader.unranked.topic' => $unrankedTopics,
] = $cfg->getValues([
'forum_leader.unranked.forum:a',
'forum_leader.unranked.topic:a',
]);
} else $unrankedForums = $unrankedTopics = [];
$leaderboards = forum_leaderboard_categories();
$leaderboard = forum_leaderboard_listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics);
$leaderboardName = 'All Time';
if($leaderboardYear) {
$leaderboardName = "Leaderboard {$leaderboardYear}";
if($leaderboardMonth)
$leaderboardName .= "-{$leaderboardMonth}";
} }
if($leaderboardMode === 'markdown') { $years = $months = [];
for($i = $currentYear; $i >= $config['forum_leader.first_year']; $i--)
$years[(string)$i] = sprintf('Leaderboard %d', $i);
for($i = $currentYear, $j = $currentMonth;;) {
$months[sprintf('%d%02d', $i, $j)] = sprintf('Leaderboard %d-%02d', $i, $j);
if($j <= 1) {
$i--; $j = 12;
} else $j--;
if($i <= $config['forum_leader.first_year'] && $j < $config['forum_leader.first_month'])
break;
}
$rankings = $forumCtx->getPosts()->generatePostRankings($year, $month, $unrankedForums, $unrankedTopics);
foreach($rankings as $ranking) {
$ranking->user = $ranking->colour = null;
if($ranking->userId !== '')
$ranking->user = $usersCtx->getUserInfo($ranking->userId);
}
$name = 'All Time';
if($year > 0) {
$name = sprintf("Leaderboard %04d", $year);
if($month > 0)
$name .= sprintf("-%02d", $month);
}
if($mode === 'markdown') {
$markdown = <<<MD $markdown = <<<MD
# {$leaderboardName} # {$name}
| Rank | Usename | Post count | | Rank | Usename | Post count |
| ----:|:------- | ----------:| | ----:|:------- | ----------:|
MD; MD;
foreach($leaderboard as $user) { $totalPostsCount = 0;
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $user['rank'], $user['username'], url_prefix(false), url('user-profile', ['user' => $user['user_id']]), $user['posts']);
foreach($rankings as $ranking) {
$totalPostsCount += $ranking->postsCount;
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
$ranking->user?->getName() ?? 'Deleted User',
$msz->getSiteInfo()->getURL(),
$msz->getURLs()->format('user-profile', ['user' => $ranking->userId]),
number_format($ranking->postsCount));
} }
$markdown .= sprintf("\r\nIn total %s posts were made!\r\n", number_format($totalPostsCount));
Template::set('leaderboard_markdown', $markdown); Template::set('leaderboard_markdown', $markdown);
} }
Template::render('forum.leaderboard', [ Template::render('forum.leaderboard', [
'leaderboard_id' => $leaderboardId, 'leaderboard_id' => $yearMonth,
'leaderboard_name' => $leaderboardName, 'leaderboard_name' => $name,
'leaderboard_categories' => $leaderboards, 'leaderboard_years' => $years,
'leaderboard_data' => $leaderboard, 'leaderboard_months' => $months,
'leaderboard_mode' => $leaderboardMode, 'leaderboard_data' => $rankings,
'leaderboard_mode' => $mode,
]); ]);

View file

@ -1,188 +1,142 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use RuntimeException;
$urls = $msz->getURLs();
$forumCtx = $msz->getForumContext();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$postRequestVerified = CSRF::validateRequest(); $postRequestVerified = CSRF::validateRequest();
if(!empty($postMode) && !$msz->isLoggedIn()) { $authInfo = $msz->getAuthInfo();
echo render_info('You must be logged in to manage posts.', 401); if(!empty($postMode) && !$authInfo->isLoggedIn())
return; Template::displayInfo('You must be logged in to manage posts.', 401);
}
$currentUser = $msz->getActiveUser(); $currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId(); $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
if($postMode !== '' && $msz->hasActiveBan()) { if($postMode !== '' && $usersCtx->hasActiveBan($currentUser))
echo render_info('You have been banned, check your profile for more information.', 403); Template::displayInfo('You have been banned, check your profile for more information.', 403);
return;
try {
$postInfo = $forumPosts->getPost(postId: $postId);
} catch(RuntimeException $ex) {
Template::throwError(404);
} }
$postInfo = forum_post_get($postId, true); $perms = $authInfo->getPerms('forum', $postInfo->getCategoryId());
$perms = empty($postInfo)
? 0 if(!$perms->check(Perm::F_CATEGORY_VIEW))
: forum_perms_get_user($postInfo['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; Template::throwError(403);
$canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
switch($postMode) { switch($postMode) {
case 'delete': case 'delete':
$canDelete = forum_post_can_delete($postInfo, $currentUserId); if($canDeleteAny) {
$canDeleteMsg = ''; if($postInfo->isDeleted())
$responseCode = 200; Template::displayInfo('This post has already been marked as deleted.', 404);
} else {
if($postInfo->isDeleted())
Template::throwError(404);
switch($canDelete) { if(!$perms->check(Perm::F_POST_DELETE_OWN))
case MSZ_E_FORUM_POST_DELETE_USER: // i don't think this is ever reached but we may as well have it Template::displayInfo('You are not allowed to delete posts.', 403);
$responseCode = 401;
$canDeleteMsg = 'You must be logged in to delete posts.'; if($postInfo->getUserId() !== $currentUser->getId())
break; Template::displayInfo('You can only delete your own posts.', 403);
case MSZ_E_FORUM_POST_DELETE_POST:
$responseCode = 404; // posts may only be deleted within a week of creation, this should be a config value
$canDeleteMsg = "This post doesn't exist."; $deleteTimeFrame = 60 * 60 * 24 * 7;
break; if($postInfo->getCreatedTime() < time() - $deleteTimeFrame)
case MSZ_E_FORUM_POST_DELETE_DELETED: Template::displayInfo('This post has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
$responseCode = 404;
$canDeleteMsg = 'This post has already been marked as deleted.';
break;
case MSZ_E_FORUM_POST_DELETE_OWNER:
$responseCode = 403;
$canDeleteMsg = 'You can only delete your own posts.';
break;
case MSZ_E_FORUM_POST_DELETE_OLD:
$responseCode = 401;
$canDeleteMsg = 'This post has existed for too long. Ask a moderator to remove if it absolutely necessary.';
break;
case MSZ_E_FORUM_POST_DELETE_PERM:
$responseCode = 401;
$canDeleteMsg = 'You are not allowed to delete posts.';
break;
case MSZ_E_FORUM_POST_DELETE_OP:
$responseCode = 403;
$canDeleteMsg = 'This is the opening post of a topic, it may not be deleted without deleting the entire topic as well.';
break;
case MSZ_E_FORUM_POST_DELETE_OK:
break;
default:
$responseCode = 500;
$canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete);
} }
if($canDelete !== MSZ_E_FORUM_POST_DELETE_OK) { $originalPostInfo = $forumPosts->getPost(topicInfo: $postInfo->getTopicId());
echo render_info($canDeleteMsg, $responseCode); if($originalPostInfo->getId() === $postInfo->getId())
break; Template::displayInfo('This is the opening post of the topic it belongs to, it may not be deleted without deleting the entire topic as well.', 403);
}
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
'post' => $postInfo['post_id'],
'post_fragment' => 'p' . $postInfo['post_id'],
]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post deletion', 'title' => 'Confirm post deletion',
'class' => 'far fa-trash-alt', 'class' => 'far fa-trash-alt',
'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo['post_id']), 'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo->getId()),
'params' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'delete', 'm' => 'delete',
], ],
]); ]);
break; break;
} }
$deletePost = forum_post_delete($postInfo['post_id']); $forumPosts->deletePost($postInfo);
$msz->createAuditLog('FORUM_POST_DELETE', [$postInfo->getId()]);
if($deletePost) { Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
$msz->createAuditLog('FORUM_POST_DELETE', [$postInfo['post_id']]);
}
if(!$deletePost) {
echo render_error(500);
break;
}
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]);
break; break;
case 'nuke': case 'nuke':
if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { if(!$canDeleteAny)
echo render_error(403); Template::throwError(403);
break;
}
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
'post' => $postInfo['post_id'],
'post_fragment' => 'p' . $postInfo['post_id'],
]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post nuke', 'title' => 'Confirm post nuke',
'class' => 'fas fa-radiation', 'class' => 'fas fa-radiation',
'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo['post_id']), 'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo->getId()),
'params' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'nuke', 'm' => 'nuke',
], ],
]); ]);
break; break;
} }
$nukePost = forum_post_nuke($postInfo['post_id']); $forumPosts->nukePost($postInfo->getId());
$msz->createAuditLog('FORUM_POST_NUKE', [$postInfo->getId()]);
if(!$nukePost) { Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
echo render_error(500);
break;
}
$msz->createAuditLog('FORUM_POST_NUKE', [$postInfo['post_id']]);
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]);
break; break;
case 'restore': case 'restore':
if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { if(!$canDeleteAny)
echo render_error(403); Template::throwError(403);
break;
}
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
'post' => $postInfo['post_id'],
'post_fragment' => 'p' . $postInfo['post_id'],
]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post restore', 'title' => 'Confirm post restore',
'class' => 'fas fa-magic', 'class' => 'fas fa-magic',
'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo['post_id']), 'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo->getId()),
'params' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'restore', 'm' => 'restore',
], ],
]); ]);
break; break;
} }
$restorePost = forum_post_restore($postInfo['post_id']); $forumPosts->restorePost($postInfo->getId());
$msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo->getId()]);
if(!$restorePost) { Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
echo render_error(500);
break;
}
$msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo['post_id']]);
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]);
break; break;
default: // function as an alt for topic.php?p= by default default: // function as an alt for topic.php?p= by default
url_redirect('forum-post', [ Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
'post' => $postInfo['post_id'],
'post_fragment' => 'p' . $postInfo['post_id'],
]);
break; break;
} }

View file

@ -1,19 +1,26 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use stdClass;
use RuntimeException;
use Index\DateTime;
use Misuzu\Forum\ForumTopicInfo;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
if(!$msz->isLoggedIn()) { $authInfo = $msz->getAuthInfo();
echo render_error(401); if(!$authInfo->isLoggedIn())
return; Template::throwError(401);
}
$currentUser = $msz->getActiveUser(); $forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser->getId(); $currentUserId = $currentUser->getId();
if($msz->hasActiveBan()) { if($usersCtx->hasActiveBan($currentUser))
echo render_error(403); Template::throwError(403);
return;
}
$forumPostingModes = [ $forumPostingModes = [
'create', 'edit', 'quote', 'preview', 'create', 'edit', 'quote', 'preview',
@ -31,10 +38,8 @@ if(!empty($_POST)) {
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0; $forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
} }
if(!in_array($mode, $forumPostingModes, true)) { if(!in_array($mode, $forumPostingModes, true))
echo render_error(400); Template::throwError(400);
return;
}
if($mode === 'preview') { if($mode === 'preview') {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
@ -52,79 +57,83 @@ if($mode === 'preview') {
return; return;
} }
if(empty($postId) && empty($topicId) && empty($forumId)) { if(empty($postId) && empty($topicId) && empty($forumId))
echo render_error(404); Template::throwError(404);
return;
}
if(!empty($postId)) { if(empty($postId)) {
$post = forum_post_get($postId); $hasPostInfo = false;
} else {
if(isset($post['topic_id'])) { // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first try {
$topicId = (int)$post['topic_id']; $postInfo = $forumPosts->getPost(postId: $postId);
} catch(RuntimeException $ex) {
Template::throwError(404);
} }
if($postInfo->isDeleted())
Template::throwError(404);
// should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first <-- what did i mean by this?
$topicId = $postInfo->getTopicId();
$hasPostInfo = true;
} }
if(!empty($topicId)) { if(empty($topicId)) {
$topic = forum_topic_get($topicId); $hasTopicInfo = false;
} else {
if(isset($topic['forum_id'])) { try {
$forumId = (int)$topic['forum_id']; $topicInfo = $forumTopics->getTopic(topicId: $topicId);
} catch(RuntimeException $ex) {
Template::throwError(404);
} }
if($topicInfo->isDeleted())
Template::throwError(404);
$forumId = $topicInfo->getCategoryId();
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
$hasTopicInfo = true;
} }
if(!empty($forumId)) { if(empty($forumId)) {
$forum = forum_get($forumId); $hasCategoryInfo = false;
} else {
try {
$categoryInfo = $forumCategories->getCategory(categoryId: $forumId);
} catch(RuntimeException $ex) {
Template::throwError(404);
}
$hasCategoryInfo = true;
} }
if(empty($forum)) { $perms = $authInfo->getPerms('forum', $categoryInfo);
echo render_error(404);
return;
}
$perms = forum_perms_get_user($forum['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; if($categoryInfo->isArchived()
|| (isset($topicInfo) && $topicInfo->isLocked() && !$perms->check(Perm::F_TOPIC_LOCK))
|| !$perms->check(Perm::F_CATEGORY_VIEW)
|| !$perms->check(Perm::F_POST_CREATE)
|| (!isset($topicInfo) && !$perms->check(Perm::F_TOPIC_CREATE)))
Template::throwError(403);
if($forum['forum_archived'] if(!$categoryInfo->mayHaveTopics())
|| (!empty($topic['topic_locked']) && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC)) Template::throwError(400);
|| !perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM | MSZ_FORUM_PERM_CREATE_POST)
|| (empty($topic) && !perms_check($perms, MSZ_FORUM_PERM_CREATE_TOPIC))) {
echo render_error(403);
return;
}
if(!forum_may_have_topics($forum['forum_type'])) {
echo render_error(400);
return;
}
$topicTypes = []; $topicTypes = [];
if($mode === 'create' || $mode === 'edit') { if($mode === 'create' || $mode === 'edit') {
$topicTypes[MSZ_TOPIC_TYPE_DISCUSSION] = 'Normal discussion'; $topicTypes['discussion'] = 'Normal discussion';
if(perms_check($perms, MSZ_FORUM_PERM_STICKY_TOPIC)) { if($perms->check(Perm::F_TOPIC_STICKY))
$topicTypes[MSZ_TOPIC_TYPE_STICKY] = 'Sticky topic'; $topicTypes['sticky'] = 'Sticky topic';
} if($perms->check(Perm::F_TOPIC_ANNOUNCE_LOCAL))
if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC)) { $topicTypes['announce'] = 'Announcement';
$topicTypes[MSZ_TOPIC_TYPE_ANNOUNCEMENT] = 'Announcement'; if($perms->check(Perm::F_TOPIC_ANNOUNCE_GLOBAL))
} $topicTypes['global'] = 'Global Announcement';
if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC)) {
$topicTypes[MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT] = 'Global Announcement';
}
} }
// edit mode stuff // edit mode stuff
if($mode === 'edit') { if($mode === 'edit' && !$perms->check($postInfo->getUserId() === $currentUserId ? Perm::F_POST_EDIT_OWN : Perm::F_POST_EDIT_ANY))
if(empty($post)) { Template::throwError(403);
echo render_error(404);
return;
}
if(!perms_check($perms, $post['poster_id'] === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) {
echo render_error(403);
return;
}
}
$notices = []; $notices = [];
@ -132,38 +141,45 @@ if(!empty($_POST)) {
$topicTitle = $_POST['post']['title'] ?? ''; $topicTitle = $_POST['post']['title'] ?? '';
$postText = $_POST['post']['text'] ?? ''; $postText = $_POST['post']['text'] ?? '';
$postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE); $postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE);
$topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : null; $topicType = isset($_POST['post']['type']) ? $_POST['post']['type'] : null;
$postSignature = isset($_POST['post']['signature']); $postSignature = isset($_POST['post']['signature']);
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest()) {
$notices[] = 'Could not verify request.'; $notices[] = 'Could not verify request.';
} else { } else {
$isEditingTopic = empty($topic) || ($mode === 'edit' && $post['is_opening_post']); $isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->getId() == $postInfo->getId());
if($mode === 'create') { if($mode === 'create') {
$timeoutCheck = max(1, forum_timeout($forumId, $currentUserId)); $postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
if($postTimeout > 0) {
$postTimeoutThreshold = DateTime::now()->modify(sprintf('-%d seconds', $postTimeout));
$lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
if($timeoutCheck < 5) { if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) {
$notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck)); $waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time());
$notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";
$notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($waitSeconds));
$notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";
}
} }
} }
if($isEditingTopic) { if($isEditingTopic) {
$originalTopicTitle = $topic['topic_title'] ?? null; $originalTopicTitle = $topicInfo?->getTitle() ?? null;
$topicTitleChanged = $topicTitle !== $originalTopicTitle; $topicTitleChanged = $topicTitle !== $originalTopicTitle;
$originalTopicType = (int)($topic['topic_type'] ?? MSZ_TOPIC_TYPE_DISCUSSION); $originalTopicType = $topicInfo?->getTypeString() ?? 'discussion';
$topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType; $topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType;
switch(forum_validate_title($topicTitle)) { $topicTitleLengths = $cfg->getValues([
case 'too-short': ['forum.topic.minLength:i', 3],
$notices[] = 'Topic title was too short.'; ['forum.topic.maxLength:i', 100],
break; ]);
case 'too-long': $topicTitleLength = mb_strlen(trim($topicTitle));
$notices[] = 'Topic title was too long.'; if($topicTitleLength < $topicTitleLengths['forum.topic.minLength'])
break; $notices[] = 'Topic title was too short.';
} elseif($topicTitleLength > $topicTitleLengths['forum.topic.maxLength'])
$notices[] = 'Topic title was too long.';
if($mode === 'create' && $topicType === null) { if($mode === 'create' && $topicType === null) {
$topicType = array_key_first($topicTypes); $topicType = array_key_first($topicTypes);
@ -172,92 +188,121 @@ if(!empty($_POST)) {
} }
} }
if(!Parser::isValid($postParser)) { if(!Parser::isValid($postParser))
$notices[] = 'Invalid parser selected.'; $notices[] = 'Invalid parser selected.';
}
switch(forum_validate_post($postText)) { $postTextLengths = $cfg->getValues([
case 'too-short': ['forum.post.minLength:i', 1],
$notices[] = 'Post content was too short.'; ['forum.post.maxLength:i', 60000],
break; ]);
case 'too-long': $postTextLength = mb_strlen(trim($postText));
$notices[] = 'Post content was too long.'; if($postTextLength < $postTextLengths['forum.post.minLength'])
break; $notices[] = 'Post content was too short.';
} elseif($postTextLength > $postTextLengths['forum.post.maxLength'])
$notices[] = 'Post content was too long.';
if(empty($notices)) { if(empty($notices)) {
switch($mode) { switch($mode) {
case 'create': case 'create':
if(!empty($topic)) { if(empty($topicInfo)) {
forum_topic_bump($topic['topic_id']); $topicInfo = $forumTopics->createTopic(
} else { $categoryInfo,
$topicId = forum_topic_create( $currentUser,
$forum['forum_id'],
$currentUserId,
$topicTitle, $topicTitle,
$topicType $topicType
); );
}
$postId = forum_post_create( $topicId = $topicInfo->getId();
$forumCategories->incrementCategoryTopics($categoryInfo);
} else
$forumTopics->bumpTopic($topicInfo);
$postInfo = $forumPosts->createPost(
$topicId, $topicId,
$forum['forum_id'], $currentUser,
$currentUserId,
$_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_ADDR'],
$postText, $postText,
$postParser, $postParser,
$postSignature $postSignature,
$categoryInfo
); );
forum_topic_mark_read($currentUserId, $topicId, $forum['forum_id']);
forum_count_increase($forum['forum_id'], empty($topic)); $postId = $postInfo->getId();
$forumCategories->incrementCategoryPosts($categoryInfo);
break; break;
case 'edit': case 'edit':
$markUpdated = $post['poster_id'] === $currentUserId $markUpdated = $postInfo->getUserId() === $currentUserId
&& $post['post_created_unix'] < strtotime('-1 minutes') && $postInfo->shouldMarkAsEdited()
&& $postText !== $post['post_text']; && $postText !== $postInfo->getBody();
if(!forum_post_update($postId, $_SERVER['REMOTE_ADDR'], $postText, $postParser, $postSignature, $markUpdated)) { $forumPosts->updatePost(
$notices[] = 'Post edit failed.'; $postId,
} remoteAddr: $_SERVER['REMOTE_ADDR'],
body: $postText,
bodyParser: $postParser,
displaySignature: $postSignature,
bumpEdited: $markUpdated
);
if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged)) { if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged))
if(!forum_topic_update($topicId, $topicTitle, $topicType)) { $forumTopics->updateTopic(
$notices[] = 'Topic update failed.'; $topicId,
} title: $topicTitle,
} type: $topicType
);
break; break;
} }
if(empty($notices)) { if(empty($notices)) {
$redirect = url(empty($topic) ? 'forum-topic' : 'forum-post', [ // does this ternary ever return forum-topic?
$redirect = $msz->getURLs()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
'topic' => $topicId ?? 0, 'topic' => $topicId ?? 0,
'post' => $postId ?? 0, 'post' => $postId ?? 0,
'post_fragment' => 'p' . ($postId ?? 0),
]); ]);
redirect($redirect); Tools::redirect($redirect);
return; return;
} }
} }
} }
} }
if(!empty($topic)) { if(!empty($topicInfo))
Template::set('posting_topic', $topic); Template::set('posting_topic', $topicInfo);
}
if($mode === 'edit') { // $post is pretty much sure to be populated at this point if($mode === 'edit') { // $post is pretty much sure to be populated at this point
$post = new stdClass;
$post->info = $postInfo;
if($postInfo->hasUserId()) {
$post->user = $usersCtx->getUserInfo($postInfo->getUserId());
$post->colour = $usersCtx->getUserColour($post->user);
$post->postsCount = $forumCtx->countTotalUserPosts($post->user);
}
$post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId();
$post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId()
&& $originalPostInfo->getUserId() === $postInfo->getUserId();
Template::set('posting_post', $post); Template::set('posting_post', $post);
} }
$displayInfo = forum_posting_info($currentUserId); try {
$lastPostInfo = $forumPosts->getPost(userInfo: $currentUser, getLast: true, deleted: false);
$selectedParser = $lastPostInfo->getParser();
} catch(RuntimeException $ex) {
$selectedParser = Parser::BBCODE;
}
Template::render('forum.posting', [ Template::render('forum.posting', [
'posting_breadcrumbs' => forum_get_breadcrumbs($forumId), 'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => forum_get_colour($forumId), 'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'posting_forum' => $forum, 'posting_user' => $currentUser,
'posting_info' => $displayInfo, 'posting_user_colour' => $usersCtx->getUserColour($currentUser),
'posting_user_posts_count' => $forumCtx->countTotalUserPosts($currentUser),
'posting_user_preferred_parser' => $selectedParser,
'posting_forum' => $categoryInfo,
'posting_notices' => $notices, 'posting_notices' => $notices,
'posting_mode' => $mode, 'posting_mode' => $mode,
'posting_types' => $topicTypes, 'posting_types' => $topicTypes,

View file

@ -1,66 +1,107 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use stdClass;
use RuntimeException;
$urls = $msz->getURLs();
$forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumTopicRedirects = $forumCtx->getTopicRedirects();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0; $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
$categoryId = null;
$moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$topicUser = $msz->getActiveUser(); $authInfo = $msz->getAuthInfo();
$topicUserId = $topicUser === null ? '0' : $topicUser->getId(); $currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId();
if($topicId < 1 && $postId > 0) { if($topicId < 1 && $postId > 0) {
$postInfo = forum_post_find($postId, $topicUserId); try {
$postInfo = $forumPosts->getPost(postId: $postId);
if(!empty($postInfo['topic_id'])) { } catch(RuntimeException $ex) {
$topicId = (int)$postInfo['topic_id']; Template::throwError(404);
} }
$categoryId = $postInfo->getCategoryId();
$perms = $authInfo->getPerms('forum', $postInfo->getCategoryId());
$canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
if($postInfo->isDeleted() && !$canDeleteAny)
Template::throwError(404);
$topicId = $postInfo->getTopicId();
$preceedingPostCount = $forumPosts->countPosts(
topicInfo: $topicId,
upToPostInfo: $postInfo,
deleted: $canDeleteAny ? null : false
);
} }
$topic = forum_topic_get($topicId, true); try {
$perms = $topic $topicIsNuked = $topicIsDeleted = $canDeleteAny = false;
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL] $topicInfo = $forumTopics->getTopic(topicId: $topicId);
: 0; } catch(RuntimeException $ex) {
$topicIsNuked = true;
}
if(isset($topicUser) && $msz->hasActiveBan($topicUser)) if(!$topicIsNuked) {
$perms &= ~MSZ_FORUM_PERM_SET_WRITE; $topicIsDeleted = $topicInfo->isDeleted();
$topicIsNuked = empty($topic['topic_id']); if($categoryId !== (int)$topicInfo->getCategoryId()) {
$topicIsDeleted = !empty($topic['topic_deleted']); $categoryId = (int)$topicInfo->getCategoryId();
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); $perms = $authInfo->getPerms('forum', $topicInfo->getCategoryId());
}
if($usersCtx->hasActiveBan($currentUser))
$perms = $perms->apply(fn($calc) => $calc & (Perm::F_CATEGORY_LIST | Perm::F_CATEGORY_VIEW));
$canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
}
if($topicIsNuked || $topicIsDeleted) { if($topicIsNuked || $topicIsDeleted) {
$topicRedirectInfo = forum_topic_redir_info($topicId); if($forumTopicRedirects->hasTopicRedirect($topicId)) {
Template::set('topic_redir_info', $topicRedirectInfo); $topicRedirectInfo = $forumTopicRedirects->getTopicRedirect($topicId);
Template::set('topic_redir_info', $topicRedirectInfo);
if($topicIsNuked || !$canDeleteAny) { if($topicIsNuked || !$canDeleteAny) {
if(empty($topicRedirectInfo)) header('Location: ' . $topicRedirectInfo->getLinkTarget());
echo render_error(404); return;
else }
header('Location: ' . $topicRedirectInfo->topic_redir_url);
return;
} }
if(empty($topicRedirectInfo))
Template::throwError(404);
} }
if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { if(!$perms->check(Perm::F_CATEGORY_VIEW))
echo render_error(403); Template::throwError(403);
return;
}
$topicIsLocked = !empty($topic['topic_locked']); // Maximum amount of posts a topic may contain to still be deletable by the author
$topicIsArchived = !empty($topic['topic_archived']); // this should be in the config
$topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']); $deletePostThreshold = 1;
$categoryInfo = $forumCategories->getCategory(topicInfo: $topicInfo);
$topicIsLocked = $topicInfo->isLocked();
$topicIsArchived = $categoryInfo->isArchived();
$topicPostsTotal = $topicInfo->getTotalPostsCount();
$topicIsFrozen = $topicIsArchived || $topicIsDeleted; $topicIsFrozen = $topicIsArchived || $topicIsDeleted;
$canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && perms_check($perms, MSZ_FORUM_PERM_DELETE_POST); $canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && $perms->check(Perm::F_POST_DELETE_OWN);
$canBumpTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_BUMP_TOPIC); $canBumpTopic = !$topicIsFrozen && $perms->check(Perm::F_TOPIC_BUMP);
$canLockTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC); $canLockTopic = !$topicIsFrozen && $perms->check(Perm::F_TOPIC_LOCK);
$canNukeOrRestore = $canDeleteAny && $topicIsDeleted; $canNukeOrRestore = $canDeleteAny && $topicIsDeleted;
$canDelete = !$topicIsDeleted && ( $canDelete = !$topicIsDeleted && (
$canDeleteAny || ( $canDeleteAny || (
$topicPostsTotal > 0 $topicPostsTotal > 0
&& $topicPostsTotal <= MSZ_FORUM_TOPIC_DELETE_POST_LIMIT && $topicPostsTotal <= $deletePostThreshold
&& $canDeleteOwn && $canDeleteOwn
&& $topic['author_user_id'] === $topicUserId && $topicInfo->getUserId() === (string)$currentUserId
) )
); );
@ -70,256 +111,229 @@ $validModerationModes = [
]; ];
if(in_array($moderationMode, $validModerationModes, true)) { if(in_array($moderationMode, $validModerationModes, true)) {
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest())
echo render_info("Couldn't verify this request, please refresh the page and try again.", 403); Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
if(!$msz->isLoggedIn()) { $authInfo = $authInfo;
echo render_info('You must be logged in to manage posts.', 401); if(!$authInfo->isLoggedIn())
return; Template::displayInfo('You must be logged in to manage posts.', 401);
}
if($msz->hasActiveBan()) { if($usersCtx->hasActiveBan($currentUser))
echo render_info('You have been banned, check your profile for more information.', 403); Template::displayInfo('You have been banned, check your profile for more information.', 403);
return;
}
switch($moderationMode) { switch($moderationMode) {
case 'delete': case 'delete':
$canDeleteCode = forum_topic_can_delete($topic, $topicUserId); if($canDeleteAny) {
$canDeleteMsg = ''; if($topicInfo->isDeleted())
$responseCode = 200; Template::displayInfo('This topic has already been marked as deleted.', 404);
} else {
if($topicInfo->isDeleted())
Template::throwError(404);
switch($canDeleteCode) { if(!$canDeleteOwn)
case MSZ_E_FORUM_TOPIC_DELETE_USER: Template::displayInfo("You aren't allowed to delete topics.", 403);
$responseCode = 401;
$canDeleteMsg = 'You must be logged in to delete topics.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_TOPIC:
$responseCode = 404;
$canDeleteMsg = "This topic doesn't exist.";
break;
case MSZ_E_FORUM_TOPIC_DELETE_DELETED:
$responseCode = 404;
$canDeleteMsg = 'This topic has already been marked as deleted.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_OWNER:
$responseCode = 403;
$canDeleteMsg = 'You can only delete your own topics.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_OLD:
$responseCode = 401;
$canDeleteMsg = 'This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_PERM:
$responseCode = 401;
$canDeleteMsg = 'You are not allowed to delete topics.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_POSTS:
$responseCode = 403;
$canDeleteMsg = 'This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.';
break;
case MSZ_E_FORUM_TOPIC_DELETE_OK:
break;
default:
$responseCode = 500;
$canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete);
}
if($canDeleteCode !== MSZ_E_FORUM_TOPIC_DELETE_OK) { if($topicInfo->getUserId() !== $currentUser->getId())
echo render_info($canDeleteMsg, $responseCode); Template::displayInfo('You can only delete your own topics.', 403);
break;
// topics may only be deleted within a day of creation, this should be a config value
$deleteTimeFrame = 60 * 60 * 24;
if($topicInfo->getCreatedTime() < time() - $deleteTimeFrame)
Template::displayInfo('This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
// deleted posts are intentionally included
$topicPostCount = $forumPosts->countPosts(topicInfo: $topicInfo);
if($topicPostCount > $deletePostThreshold)
Template::displayInfo('This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.', 403);
} }
if(!isset($_GET['confirm'])) { if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm topic deletion', 'title' => 'Confirm topic deletion',
'class' => 'far fa-trash-alt', 'class' => 'far fa-trash-alt',
'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topic['topic_id']), 'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->getId()),
'params' => [ 'params' => [
't' => $topic['topic_id'], 't' => $topicInfo->getId(),
'm' => 'delete', 'm' => 'delete',
], ],
]); ]);
break; break;
} elseif(!$submissionConfirmed) { } elseif(!$submissionConfirmed) {
url_redirect( Tools::redirect($urls->format(
'forum-topic', 'forum-topic',
['topic' => $topic['topic_id']] ['topic' => $topicInfo->getId()]
); ));
break; break;
} }
$deleteTopic = forum_topic_delete($topic['topic_id']); $forumTopics->deleteTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->getId()]);
if($deleteTopic) Tools::redirect($urls->format('forum-category', [
$msz->createAuditLog('FORUM_TOPIC_DELETE', [$topic['topic_id']]); 'forum' => $categoryInfo->getId(),
]));
if(!$deleteTopic) {
echo render_error(500);
break;
}
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
break; break;
case 'restore': case 'restore':
if(!$canNukeOrRestore) { if(!$canNukeOrRestore)
echo render_error(403); Template::throwError(403);
break;
}
if(!isset($_GET['confirm'])) { if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm topic restore', 'title' => 'Confirm topic restore',
'class' => 'fas fa-magic', 'class' => 'fas fa-magic',
'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topic['topic_id']), 'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->getId()),
'params' => [ 'params' => [
't' => $topic['topic_id'], 't' => $topicInfo->getId(),
'm' => 'restore', 'm' => 'restore',
], ],
]); ]);
break; break;
} elseif(!$submissionConfirmed) { } elseif(!$submissionConfirmed) {
url_redirect('forum-topic', [ Tools::redirect($urls->format('forum-topic', [
'topic' => $topic['topic_id'], 'topic' => $topicInfo->getId(),
]); ]));
break; break;
} }
$restoreTopic = forum_topic_restore($topic['topic_id']); $forumTopics->restoreTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->getId()]);
if(!$restoreTopic) { Tools::redirect($urls->format('forum-category', [
echo render_error(500); 'forum' => $categoryInfo->getId(),
break; ]));
}
$msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topic['topic_id']]);
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
break; break;
case 'nuke': case 'nuke':
if(!$canNukeOrRestore) { if(!$canNukeOrRestore)
echo render_error(403); Template::throwError(403);
break;
}
if(!isset($_GET['confirm'])) { if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm topic nuke', 'title' => 'Confirm topic nuke',
'class' => 'fas fa-radiation', 'class' => 'fas fa-radiation',
'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topic['topic_id']), 'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->getId()),
'params' => [ 'params' => [
't' => $topic['topic_id'], 't' => $topicInfo->getId(),
'm' => 'nuke', 'm' => 'nuke',
], ],
]); ]);
break; break;
} elseif(!$submissionConfirmed) { } elseif(!$submissionConfirmed) {
url_redirect('forum-topic', [ Tools::redirect($urls->format('forum-topic', [
'topic' => $topic['topic_id'], 'topic' => $topicInfo->getId(),
]); ]));
break; break;
} }
$nukeTopic = forum_topic_nuke($topic['topic_id']); $forumTopics->nukeTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->getId()]);
if(!$nukeTopic) { Tools::redirect($urls->format('forum-category', [
echo render_error(500); 'forum' => $categoryInfo->getId(),
break; ]));
}
$msz->createAuditLog('FORUM_TOPIC_NUKE', [$topic['topic_id']]);
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
break; break;
case 'bump': case 'bump':
if($canBumpTopic && forum_topic_bump($topic['topic_id'])) { if($canBumpTopic) {
$msz->createAuditLog('FORUM_TOPIC_BUMP', [$topic['topic_id']]); $forumTopics->bumpTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->getId()]);
} }
url_redirect('forum-topic', [ Tools::redirect($urls->format('forum-topic', [
'topic' => $topic['topic_id'], 'topic' => $topicInfo->getId(),
]); ]));
break; break;
case 'lock': case 'lock':
if($canLockTopic && !$topicIsLocked && forum_topic_lock($topic['topic_id'])) { if($canLockTopic && !$topicIsLocked) {
$msz->createAuditLog('FORUM_TOPIC_LOCK', [$topic['topic_id']]); $forumTopics->lockTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->getId()]);
} }
url_redirect('forum-topic', [ Tools::redirect($urls->format('forum-topic', [
'topic' => $topic['topic_id'], 'topic' => $topicInfo->getId(),
]); ]));
break; break;
case 'unlock': case 'unlock':
if($canLockTopic && $topicIsLocked && forum_topic_unlock($topic['topic_id'])) { if($canLockTopic && $topicIsLocked) {
$msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topic['topic_id']]); $forumTopics->unlockTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->getId()]);
} }
url_redirect('forum-topic', [ Tools::redirect($urls->format('forum-topic', [
'topic' => $topic['topic_id'], 'topic' => $topicInfo->getId(),
]); ]));
break; break;
} }
return; return;
} }
$topicPosts = $topic['topic_count_posts']; $topicPosts = $topicInfo->getPostsCount();
if($canDeleteAny)
$topicPosts += $topicInfo->getDeletedPostsCount();
if($canDeleteAny) { $topicPagination = new Pagination($topicPosts, 10, 'page');
$topicPosts += $topic['topic_count_posts_deleted'];
}
$topicPagination = new Pagination($topicPosts, MSZ_FORUM_POSTS_PER_PAGE, 'page'); if(isset($preceedingPostCount))
$topicPagination->setPage(floor($preceedingPostCount / $topicPagination->getRange()), true);
if(isset($postInfo['preceeding_post_count'])) { if(!$topicPagination->hasValidOffset())
$preceedingPosts = $postInfo['preceeding_post_count']; Template::throwError(404);
if($canDeleteAny) { $postInfos = $forumPosts->getPosts(
$preceedingPosts += $postInfo['preceeding_post_deleted_count']; topicInfo: $topicInfo,
} deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false,
pagination: $topicPagination,
$topicPagination->setPage(floor($preceedingPosts / $topicPagination->getRange()), true);
}
if(!$topicPagination->hasValidOffset()) {
echo render_error(404);
return;
}
Template::set('topic_perms', $perms);
$posts = forum_post_listing(
$topic['topic_id'],
$topicPagination->getOffset(),
$topicPagination->getRange(),
perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)
); );
if(!$posts) { if(empty($postInfos))
echo render_error(404); Template::throwError(404);
return;
try {
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
} catch(RuntimeException $ex) {
Template::throwError(404);
} }
$canReply = !$topicIsArchived && !$topicIsLocked && !$topicIsDeleted && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST); $posts = [];
forum_topic_mark_read($topicUserId, $topic['topic_id'], $topic['forum_id']); foreach($postInfos as $postInfo) {
$posts[] = $post = new stdClass;
$post->info = $postInfo;
if($postInfo->hasUserId()) {
$post->user = $usersCtx->getUserInfo($postInfo->getUserId());
$post->colour = $usersCtx->getUserColour($post->user);
$post->postsCount = $forumCtx->countTotalUserPosts($post->user);
}
$post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId();
$post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId()
&& $originalPostInfo->getUserId() === $postInfo->getUserId();
}
$canReply = !$topicIsArchived && !$topicIsLocked && !$topicIsDeleted && $perms->check(Perm::F_POST_CREATE);
if(!$forumTopics->checkUserHasReadTopic($currentUser, $topicInfo))
$forumTopics->incrementTopicViews($topicInfo);
$forumTopics->updateUserReadTopic($currentUser, $topicInfo);
$perms = $perms->checkMany([
'can_create_post' => Perm::F_POST_CREATE,
'can_edit_post' => Perm::F_POST_EDIT_OWN,
'can_edit_any_post' => Perm::F_POST_EDIT_ANY,
'can_delete_post' => Perm::F_POST_DELETE_OWN,
'can_delete_any_post' => Perm::F_POST_DELETE_ANY,
]);
Template::render('forum.topic', [ Template::render('forum.topic', [
'topic_breadcrumbs' => forum_get_breadcrumbs($topic['forum_id']), 'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
'global_accent_colour' => forum_get_colour($topic['forum_id']), 'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
'topic_info' => $topic, 'topic_info' => $topicInfo,
'category_info' => $categoryInfo,
'topic_posts' => $posts, 'topic_posts' => $posts,
'can_reply' => $canReply, 'can_reply' => $canReply,
'topic_pagination' => $topicPagination, 'topic_pagination' => $topicPagination,
@ -327,5 +341,6 @@ Template::render('forum.topic', [
'topic_can_nuke_or_restore' => $canNukeOrRestore, 'topic_can_nuke_or_restore' => $canNukeOrRestore,
'topic_can_bump' => $canBumpTopic, 'topic_can_bump' => $canBumpTopic,
'topic_can_lock' => $canLockTopic, 'topic_can_lock' => $canLockTopic,
'topic_user_id' => $topicUserId, 'topic_user_id' => $currentUserId,
'topic_perms' => $perms,
]); ]);

View file

@ -4,39 +4,42 @@ namespace Misuzu;
use DateTimeInterface; use DateTimeInterface;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
use Index\XArray;
use Misuzu\Changelog\Changelog; use Misuzu\Changelog\Changelog;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); if(!$authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
return; Template::throwError(403);
}
$changeActions = []; $changeActions = [];
foreach(Changelog::ACTIONS as $action) foreach(Changelog::ACTIONS as $action)
$changeActions[$action] = Changelog::actionText($action); $changeActions[$action] = Changelog::actionText($action);
$urls = $msz->getURLs();
$changelog = $msz->getChangelog(); $changelog = $msz->getChangelog();
$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); $changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$loadChangeInfo = fn() => $changelog->getChangeById($changeId, withTags: true); $changeInfo = null;
$changeTags = $changelog->getAllTags(); $changeTagIds = [];
$tagInfos = $changelog->getTags();
if(empty($changeId)) if(empty($changeId))
$isNew = true; $isNew = true;
else else
try { try {
$isNew = false; $isNew = false;
$changeInfo = $loadChangeInfo(); $changeInfo = $changelog->getChange($changeId);
$changeTagIds = XArray::select($changelog->getTags(changeInfo: $changeInfo), fn($tagInfo) => $tagInfo->getId());
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
$changelog->deleteChange($changeInfo); Template::throwError(403);
$msz->createAuditLog('CHANGELOG_ENTRY_DELETE', [$changeInfo->getId()]);
url_redirect('manage-changelog-changes'); $changelog->deleteChange($changeInfo);
} else render_error(403); $msz->createAuditLog('CHANGELOG_ENTRY_DELETE', [$changeInfo->getId()]);
Tools::redirect($urls->format('manage-changelog-changes'));
return; return;
} }
@ -78,7 +81,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
} }
if(!empty($tags)) { if(!empty($tags)) {
$tCurrent = $changeInfo->getTagIds(); $tCurrent = $changeTagIds;
$tApply = $tags; $tApply = $tags;
$tRemove = []; $tRemove = [];
@ -102,17 +105,15 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
[$changeInfo->getId()] [$changeInfo->getId()]
); );
if($isNew) { Tools::redirect($urls->format('manage-changelog-change', ['change' => $changeInfo->getId()]));
url_redirect('manage-changelog-change', ['change' => $changeInfo->getId()]); return;
return;
} else $changeInfo = $loadChangeInfo();
break;
} }
Template::render('manage.changelog.change', [ Template::render('manage.changelog.change', [
'change_new' => $isNew, 'change_new' => $isNew,
'change_info' => $changeInfo ?? null, 'change_info' => $changeInfo,
'change_tags' => $changeTags, 'change_info_tags' => $changeTagIds,
'change_tags' => $tagInfos,
'change_actions' => $changeActions, 'change_actions' => $changeActions,
'change_author_id' => $msz->getActiveUser()->getId(), 'change_author_id' => $authInfo->getUserInfo()->getId(),
]); ]);

View file

@ -3,44 +3,28 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$changelog = $msz->getChangelog(); $changelog = $msz->getChangelog();
$changelogPagination = new Pagination($changelog->countAllChanges(), 30); $usersCtx = $msz->getUsersContext();
if(!$changelogPagination->hasValidOffset()) { $changelogPagination = new Pagination($changelog->countChanges(), 30);
echo render_error(404);
return;
}
$changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination); if(!$changelogPagination->hasValidOffset())
Template::throwError(404);
$changeInfos = $changelog->getChanges(pagination: $changelogPagination);
$changes = []; $changes = [];
$userInfos = [];
$userColours = [];
foreach($changeInfos as $changeInfo) { foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId(); $userInfo = $changeInfo->hasUserId() ? $usersCtx->getUserInfo($changeInfo->getUserId()) : null;
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = $users->getUser($userId, 'id');
$userColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
$changes[] = [ $changes[] = [
'change' => $changeInfo, 'change' => $changeInfo,
'tags' => $changelog->getTags(changeInfo: $changeInfo),
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColours[$userId] ?? \Index\Colour\Colour::none(), 'user_colour' => $usersCtx->getUserColour($userInfo),
]; ];
} }

View file

@ -3,14 +3,13 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$urls = $msz->getURLs();
$changelog = $msz->getChangelog(); $changelog = $msz->getChangelog();
$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT); $tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
$loadTagInfo = fn() => $changelog->getTagById($tagId); $loadTagInfo = fn() => $changelog->getTag($tagId);
if(empty($tagId)) if(empty($tagId))
$isNew = true; $isNew = true;
@ -19,16 +18,16 @@ else
$isNew = false; $isNew = false;
$tagInfo = $loadTagInfo(); $tagInfo = $loadTagInfo();
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
$changelog->deleteTag($tagInfo); Template::throwError(403);
$msz->createAuditLog('CHANGELOG_TAG_DELETE', [$tagInfo->getId()]);
url_redirect('manage-changelog-tags'); $changelog->deleteTag($tagInfo);
} else render_error(403); $msz->createAuditLog('CHANGELOG_TAG_DELETE', [$tagInfo->getId()]);
Tools::redirect($urls->format('manage-changelog-tags'));
return; return;
} }
@ -57,7 +56,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
); );
if($isNew) { if($isNew) {
url_redirect('manage-changelog-tag', ['tag' => $tagInfo->getId()]); Tools::redirect($urls->format('manage-changelog-tag', ['tag' => $tagInfo->getId()]));
return; return;
} else $tagInfo = $loadTagInfo(); } else $tagInfo = $loadTagInfo();
break; break;

View file

@ -1,11 +1,9 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
Template::render('manage.changelog.tags', [ Template::render('manage.changelog.tags', [
'changelog_tags' => $msz->getChangelog()->getAllTags(), 'changelog_tags' => $msz->getChangelog()->getTags(),
]); ]);

View file

@ -1,22 +0,0 @@
<?php
namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
echo render_error(403);
return;
}
$getForum = DB::prepare('
SELECT *
FROM `msz_forum_categories`
WHERE `forum_id` = :forum_id
');
$getForum->bind('forum_id', (int)($_GET['f'] ?? 0));
$forum = $getForum->fetch();
if(!$forum) {
echo render_error(404);
return;
}
Template::render('manage.forum.forum', compact('forum'));

View file

@ -1,19 +1,22 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) { use Misuzu\Perm;
echo render_error(403);
return;
}
$forums = DB::query('SELECT * FROM `msz_forum_categories`')->fetchAll(); if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_FORUM_CATEGORIES_MANAGE))
$rawPerms = perms_create(MSZ_FORUM_PERM_MODES); Template::throwError(403);
$perms = manage_forum_perms_list($rawPerms);
if(!empty($_POST['perms']) && is_array($_POST['perms'])) { $perms = $msz->getPerms();
$finalPerms = manage_perms_apply($perms, $_POST['perms'], $rawPerms); $permsInfos = $perms->getPermissionInfo(categoryNames: Perm::INFO_FOR_FORUM_CATEGORY);
$perms = manage_forum_perms_list($finalPerms); $permsLists = Perm::createList(Perm::LISTS_FOR_FORUM_CATEGORY);
Template::set('calculated_perms', $finalPerms);
}
Template::render('manage.forum.listing', compact('forums', 'perms')); if(filter_has_var(INPUT_POST, 'perms'))
Template::set('calculated_perms', Perm::convertSubmission(
filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY),
Perm::INFO_FOR_FORUM_CATEGORY
));
Template::render('manage.forum.listing', [
'perms_lists' => $permsLists,
'perms_infos' => $permsInfos,
]);

View file

@ -1,24 +1,24 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_TOPIC_REDIRS)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
return; Template::throwError(403);
}
$urls = $msz->getURLs();
$forumCtx = $msz->getForumContext();
$forumTopicRedirects = $forumCtx->getTopicRedirects();
if($_SERVER['REQUEST_METHOD'] === 'POST') { if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(!CSRF::validateRequest()) if(!CSRF::validateRequest())
throw new \Exception("Request verification failed."); throw new \Exception("Request verification failed.");
$rTopicId = (int)filter_input(INPUT_POST, 'topic_redir_id'); $rTopicId = (string)filter_input(INPUT_POST, 'topic_redir_id');
$rTopicURL = trim((string)filter_input(INPUT_POST, 'topic_redir_url')); $rTopicURL = trim((string)filter_input(INPUT_POST, 'topic_redir_url'));
if($rTopicId < 1)
throw new \Exception("Invalid topic id.");
$msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]); $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]);
forum_topic_redir_create($rTopicId, $msz->getActiveUser()->getId(), $rTopicURL); $forumTopicRedirects->createTopicRedirect($rTopicId, $authInfo->getUserInfo(), $rTopicURL);
url_redirect('manage-forum-topic-redirs'); Tools::redirect($urls->format('manage-forum-topic-redirs'));
return; return;
} }
@ -26,20 +26,18 @@ if(filter_input(INPUT_GET, 'm') === 'explode') {
if(!CSRF::validateRequest()) if(!CSRF::validateRequest())
throw new \Exception("Request verification failed."); throw new \Exception("Request verification failed.");
$rTopicId = (int)filter_input(INPUT_GET, 't'); $rTopicId = (string)filter_input(INPUT_GET, 't');
$msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]); $msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]);
forum_topic_redir_remove($rTopicId); $forumTopicRedirects->deleteTopicRedirect($rTopicId);
url_redirect('manage-forum-topic-redirs'); Tools::redirect($urls->format('manage-forum-topic-redirs'));
return; return;
} }
$pagination = new Pagination(forum_topic_redir_count(), 20); $pagination = new Pagination($forumTopicRedirects->countTopicRedirects(), 20);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$redirs = forum_topic_redir_all($pagination->getOffset(), $pagination->getRange()); $redirs = $forumTopicRedirects->getTopicRedirects(pagination: $pagination);
Template::render('manage.forum.redirs', [ Template::render('manage.forum.redirs', [
'manage_redirs' => $redirs, 'manage_redirs' => $redirs,

View file

@ -2,25 +2,25 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Index\XArray;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$emotes = $msz->getEmotes(); $emotes = $msz->getEmotes();
$emoteId = (string)filter_input(INPUT_GET, 'e', FILTER_SANITIZE_NUMBER_INT); $emoteId = (string)filter_input(INPUT_GET, 'e', FILTER_SANITIZE_NUMBER_INT);
$loadEmoteInfo = fn() => $emotes->getEmoteById($emoteId, true); $emoteInfo = [];
$emoteStrings = [];
if(empty($emoteId)) if(empty($emoteId))
$isNew = true; $isNew = true;
else else
try { try {
$isNew = false; $isNew = false;
$emoteInfo = $loadEmoteInfo(); $emoteInfo = $emotes->getEmote($emoteId);
$emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
// make errors not echos lol // make errors not echos lol
@ -60,7 +60,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$emotes->updateEmote($emoteInfo, $order, $minRank, $url); $emotes->updateEmote($emoteInfo, $order, $minRank, $url);
} }
$sCurrent = $emoteInfo->getStringsRaw(); $sCurrent = XArray::select($emoteStrings, fn($stringInfo) => $stringInfo->getString());
$sApply = $strings; $sApply = $strings;
$sRemove = []; $sRemove = [];
@ -97,14 +97,12 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
[$emoteInfo->getId()] [$emoteInfo->getId()]
); );
if($isNew) { Tools::redirect($msz->getURLs()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
url_redirect('manage-general-emoticon', ['emote' => $emoteInfo->getId()]); return;
return;
} else $emoteInfo = $loadEmoteInfo();
break;
} }
Template::render('manage.general.emoticon', [ Template::render('manage.general.emoticon', [
'emote_new' => $isNew, 'emote_new' => $isNew,
'emote_info' => $emoteInfo ?? null, 'emote_info' => $emoteInfo,
'emote_strings' => $emoteStrings,
]); ]);

View file

@ -3,10 +3,8 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$emotes = $msz->getEmotes(); $emotes = $msz->getEmotes();
@ -14,10 +12,9 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
$emoteId = (string)filter_input(INPUT_GET, 'emote', FILTER_SANITIZE_NUMBER_INT); $emoteId = (string)filter_input(INPUT_GET, 'emote', FILTER_SANITIZE_NUMBER_INT);
try { try {
$emoteInfo = $emotes->getEmoteById($emoteId); $emoteInfo = $emotes->getEmote($emoteId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if(!empty($_GET['delete'])) { if(!empty($_GET['delete'])) {
@ -26,7 +23,7 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
} else { } else {
if(isset($_GET['order'])) { if(isset($_GET['order'])) {
$order = filter_input(INPUT_GET, 'order'); $order = filter_input(INPUT_GET, 'order');
$offset = $order === 'i' ? 1 : ($order === 'd' ? -1 : 0); $offset = $order === 'i' ? 10 : ($order === 'd' ? -10 : 0);
$emotes->updateEmoteOrderOffset($emoteInfo, $offset); $emotes->updateEmoteOrderOffset($emoteInfo, $offset);
$msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->getId()]); $msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->getId()]);
} }
@ -40,10 +37,10 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
} }
} }
url_redirect('manage-general-emoticons'); Tools::redirect($msz->getURLs()->format('manage-general-emoticons'));
return; return;
} }
Template::render('manage.general.emoticons', [ Template::render('manage.general.emoticons', [
'emotes' => $emotes->getAllEmotes(), 'emotes' => $emotes->getEmotes(),
]); ]);

View file

@ -3,21 +3,17 @@ namespace Misuzu;
use Misuzu\Pagination; use Misuzu\Pagination;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_VIEW_LOGS)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_LOGS_VIEW))
echo render_error(403); Template::throwError(403);
return;
}
$users = $msz->getUsers(); $usersCtx = $msz->getUsersContext();
$auditLog = $msz->getAuditLog(); $auditLog = $msz->getAuditLog();
$pagination = new Pagination($auditLog->countLogs(), 50); $pagination = new Pagination($auditLog->countLogs(), 50);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$logs = $auditLog->getLogs(pagination: $pagination); $logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
$userInfos = []; $userInfos = [];
$userColours = []; $userColours = [];
@ -25,8 +21,8 @@ foreach($logs as $log)
if($log->hasUserId()) { if($log->hasUserId()) {
$userId = $log->getUserId(); $userId = $log->getUserId();
if(!array_key_exists($userId, $userInfos)) { if(!array_key_exists($userId, $userInfos)) {
$userInfos[$userId] = $users->getUser($userId, 'id'); $userInfos[$userId] = $usersCtx->getUserInfo($userId);
$userColours[$userId] = $users->getUserColour($userInfos[$userId]); $userColours[$userId] = $usersCtx->getUserColour($userId);
} }
} }

View file

@ -1,25 +1,19 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\CfgTools; if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
Template::throwError(403);
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
echo render_error(403);
return;
}
$valueName = (string)filter_input(INPUT_GET, 'name'); $valueName = (string)filter_input(INPUT_GET, 'name');
$valueInfo = $cfg->getValueInfo($valueName); $valueInfo = $cfg->getValueInfo($valueName);
if($valueInfo === null) { if($valueInfo === null)
echo render_error(404); Template::throwError(404);
return;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$valueName = $valueInfo->getName(); $valueName = $valueInfo->getName();
$msz->createAuditLog('CONFIG_DELETE', [$valueName]); $msz->createAuditLog('CONFIG_DELETE', [$valueName]);
$cfg->removeValues($valueName); $cfg->removeValues($valueName);
url_redirect('manage-general-settings'); Tools::redirect($msz->getURLs()->format('manage-general-settings'));
return; return;
} }

View file

@ -1,12 +1,10 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\DbConfig; use Syokuhou\DbConfig;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$isNew = true; $isNew = true;
$sName = (string)filter_input(INPUT_GET, 'name'); $sName = (string)filter_input(INPUT_GET, 'name');
@ -75,7 +73,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]); $msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
$applyFunc($sName, $sValue); $applyFunc($sName, $sValue);
url_redirect('manage-general-settings'); Tools::redirect($msz->getURLs()->format('manage-general-settings'));
return; return;
} }

View file

@ -1,10 +1,8 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$hidden = $cfg->getArray('settings.hidden'); $hidden = $cfg->getArray('settings.hidden');
$vars = $cfg->getAllValueInfos(); $vars = $cfg->getAllValueInfos();

View file

@ -1,4 +0,0 @@
<?php
namespace Misuzu;
url_redirect('manage-general-overview');

View file

@ -1,20 +1,16 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$news = $msz->getNews(); $news = $msz->getNews();
$pagination = new Pagination($news->countAllCategories(true), 15); $pagination = new Pagination($news->countCategories(), 15);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$categories = $news->getAllCategories(true, $pagination); $categories = $news->getCategories(pagination: $pagination);
Template::render('manage.news.categories', [ Template::render('manage.news.categories', [
'news_categories' => $categories, 'news_categories' => $categories,

View file

@ -3,14 +3,13 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$urls = $msz->getURLs();
$news = $msz->getNews(); $news = $msz->getNews();
$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); $categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$loadCategoryInfo = fn() => $news->getCategoryById($categoryId); $loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);
if(empty($categoryId)) if(empty($categoryId))
$isNew = true; $isNew = true;
@ -19,16 +18,16 @@ else
$isNew = false; $isNew = false;
$categoryInfo = $loadCategoryInfo(); $categoryInfo = $loadCategoryInfo();
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
$news->deleteCategory($categoryInfo); Template::throwError(403);
$msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]);
url_redirect('manage-news-categories'); $news->deleteCategory($categoryInfo);
} else render_error(403); $msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]);
Tools::redirect($urls->format('manage-news-categories'));
return; return;
} }
@ -57,7 +56,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
); );
if($isNew) { if($isNew) {
url_redirect('manage-news-category', ['category' => $categoryInfo->getId()]); Tools::redirect($urls->format('manage-news-category', ['category' => $categoryInfo->getId()]));
return; return;
} else $categoryInfo = $loadCategoryInfo(); } else $categoryInfo = $loadCategoryInfo();
break; break;

View file

@ -1,4 +0,0 @@
<?php
namespace Misuzu;
url_redirect('manage-news-categories');

View file

@ -3,14 +3,14 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
return; Template::throwError(403);
}
$urls = $msz->getURLs();
$news = $msz->getNews(); $news = $msz->getNews();
$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT); $postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
$loadPostInfo = fn() => $news->getPostById($postId); $loadPostInfo = fn() => $news->getPost($postId);
if(empty($postId)) if(empty($postId))
$isNew = true; $isNew = true;
@ -19,16 +19,16 @@ else
$isNew = false; $isNew = false;
$postInfo = $loadPostInfo(); $postInfo = $loadPostInfo();
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
$news->deletePost($postInfo); Template::throwError(403);
$msz->createAuditLog('NEWS_POST_DELETE', [$postInfo->getId()]);
url_redirect('manage-news-posts'); $news->deletePost($postInfo);
} else render_error(403); $msz->createAuditLog('NEWS_POST_DELETE', [$postInfo->getId()]);
Tools::redirect($urls->format('manage-news-posts'));
return; return;
} }
@ -39,7 +39,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'np_body')); $body = trim((string)filter_input(INPUT_POST, 'np_body'));
if($isNew) { if($isNew) {
$postInfo = $news->createPost($category, $title, $body, $featured, $msz->getActiveUser()); $postInfo = $news->createPost($category, $title, $body, $featured, $authInfo->getUserInfo());
} else { } else {
if($category === $postInfo->getCategoryId()) if($category === $postInfo->getCategoryId())
$category = null; $category = null;
@ -64,14 +64,14 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
// Twitter integration used to be here, replace with Railgun Pulse integration // Twitter integration used to be here, replace with Railgun Pulse integration
} }
url_redirect('manage-news-post', ['post' => $postInfo->getId()]); Tools::redirect($urls->format('manage-news-post', ['post' => $postInfo->getId()]));
return; return;
} else $postInfo = $loadPostInfo(); } else $postInfo = $loadPostInfo();
break; break;
} }
$categories = []; $categories = [];
foreach($news->getAllCategories(true) as $categoryInfo) foreach($news->getCategories() as $categoryInfo)
$categories[$categoryInfo->getId()] = $categoryInfo->getName(); $categories[$categoryInfo->getId()] = $categoryInfo->getName();
Template::render('manage.news.post', [ Template::render('manage.news.post', [

View file

@ -1,23 +1,19 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) { if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$news = $msz->getNews(); $news = $msz->getNews();
$pagination = new Pagination($news->countAllPosts( $pagination = new Pagination($news->countPosts(
includeScheduled: true, includeScheduled: true,
includeDeleted: true includeDeleted: true
), 15); ), 15);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$posts = $news->getAllPosts( $posts = $news->getPosts(
includeScheduled: true, includeScheduled: true,
includeDeleted: true, includeDeleted: true,
pagination: $pagination pagination: $pagination

View file

@ -5,39 +5,37 @@ use DateTimeInterface;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_BANS)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
return; Template::throwError(403);
}
$bans = $msz->getBans(); $urls = $msz->getURLs();
$usersCtx = $msz->getUsersContext();
$bans = $usersCtx->getBans();
if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
try { Template::throwError(403);
$banInfo = $bans->getBan((string)filter_input(INPUT_GET, 'b'));
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
$bans->deleteBans($banInfo); try {
$msz->createAuditLog('BAN_DELETE', [$banInfo->getId(), $banInfo->getUserId()]); $banInfo = $bans->getBan((string)filter_input(INPUT_GET, 'b'));
url_redirect('manage-users-bans', ['user' => $banInfo->getUserId()]); } catch(RuntimeException $ex) {
} else render_error(403); Template::throwError(404);
}
$bans->deleteBans($banInfo);
$msz->createAuditLog('BAN_DELETE', [$banInfo->getId(), $banInfo->getUserId()]);
Tools::redirect($urls->format('manage-users-bans', ['user' => $banInfo->getUserId()]));
return; return;
} }
$users = $msz->getUsers();
try { try {
$userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id'); $userInfo = $usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
$modInfo = $msz->getActiveUser(); $modInfo = $authInfo->getUserInfo();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT); $expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);
@ -72,7 +70,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
); );
$msz->createAuditLog('BAN_CREATE', [$banInfo->getId(), $userInfo->getId()]); $msz->createAuditLog('BAN_CREATE', [$banInfo->getId(), $userInfo->getId()]);
url_redirect('manage-users-bans', ['user' => $userInfo->getId()]); Tools::redirect($urls->format('manage-users-bans', ['user' => $userInfo->getId()]));
return; return;
} }

View file

@ -3,68 +3,40 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_BANS)) { if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_BANS_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$users = $msz->getUsers(); $usersCtx = $msz->getUsersContext();
$bans = $usersCtx->getBans();
$userInfos = [
$msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
];
$filterUser = null; $filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) { if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$filterUser = $users->getUser($filterUserId, 'id'); $filterUser = $usersCtx->getUserInfo($filterUserId);
$userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
} }
$bans = $msz->getBans();
$pagination = new Pagination($bans->countBans(userInfo: $filterUser), 10); $pagination = new Pagination($bans->countBans(userInfo: $filterUser), 10);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$banList = []; $banList = [];
$banInfos = $bans->getBans(userInfo: $filterUser, activeFirst: true, pagination: $pagination); $banInfos = $bans->getBans(userInfo: $filterUser, activeFirst: true, pagination: $pagination);
foreach($banInfos as $banInfo) { foreach($banInfos as $banInfo) {
if(array_key_exists($banInfo->getUserId(), $userInfos)) $userInfo = $usersCtx->getUserInfo($banInfo->getUserId());
$userInfo = $userInfos[$banInfo->getUserId()]; $userColour = $usersCtx->getUserColour($userInfo);
else
$userInfos[$banInfo->getUserId()] = $userInfo = $users->getUser($banInfo->getUserId(), 'id');
if(array_key_exists($userInfo->getId(), $userColours))
$userColour = $userColours[$userInfo->getId()];
else
$userColours[$userInfo->getId()] = $userColour = $users->getUserColour($userInfo);
if(!$banInfo->hasModId()) { if(!$banInfo->hasModId()) {
$modInfo = null; $modInfo = null;
$modColour = null; $modColour = null;
} else { } else {
if(array_key_exists($banInfo->getModId(), $userInfos)) $modInfo = $usersCtx->getUserInfo($banInfo->getModId());
$modInfo = $userInfos[$banInfo->getModId()]; $modColour = $usersCtx->getUserColour($modInfo);
else
$userInfos[$banInfo->getModId()] = $modInfo = $users->getUser($banInfo->getModId(), 'id');
if(array_key_exists($modInfo->getId(), $userColours))
$modColour = $userColours[$modInfo->getId()];
else
$userColours[$modInfo->getId()] = $modColour = $users->getUserColour($modInfo);
} }
$banList[] = [ $banList[] = [

View file

@ -1,19 +1,16 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS)) { if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_USERS_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$users = $msz->getUsers(); $usersCtx = $msz->getUsersContext();
$roles = $msz->getRoles(); $users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$pagination = new Pagination($users->countUsers(), 30); $pagination = new Pagination($users->countUsers(), 30);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$userList = []; $userList = [];
$userInfos = $users->getUsers(pagination: $pagination, orderBy: 'id'); $userInfos = $users->getUsers(pagination: $pagination, orderBy: 'id');

View file

@ -3,54 +3,51 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); if(!$authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE))
return; Template::throwError(403);
}
$hasNoteId = filter_has_var(INPUT_GET, 'n'); $hasNoteId = filter_has_var(INPUT_GET, 'n');
$hasUserId = filter_has_var(INPUT_GET, 'u'); $hasUserId = filter_has_var(INPUT_GET, 'u');
if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId)) { if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
echo render_error(400); Template::throwError(400);
return;
}
$users = $msz->getUsers(); $urls = $msz->getURLs();
$modNotes = $msz->getModNotes(); $usersCtx = $msz->getUsersContext();
$modNotes = $usersCtx->getModNotes();
if($hasUserId) { if($hasUserId) {
$isNew = true; $isNew = true;
try { try {
$userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id'); $userInfo = $usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
$authorInfo = $msz->getActiveUser(); $authorInfo = $authInfo->getUserInfo();
} elseif($hasNoteId) { } elseif($hasNoteId) {
$isNew = false; $isNew = false;
try { try {
$noteInfo = $modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT)); $noteInfo = $modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT));
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) {
if(CSRF::validateRequest()) { if(!CSRF::validateRequest())
$modNotes->deleteNotes($noteInfo); Template::throwError(403);
$msz->createAuditLog('MOD_NOTE_DELETE', [$noteInfo->getId(), $noteInfo->getUserId()]);
url_redirect('manage-users-notes', ['user' => $noteInfo->getUserId()]); $modNotes->deleteNotes($noteInfo);
} else render_error(403); $msz->createAuditLog('MOD_NOTE_DELETE', [$noteInfo->getId(), $noteInfo->getUserId()]);
Tools::redirect($urls->format('manage-users-notes', ['user' => $noteInfo->getUserId()]));
return; return;
} }
$userInfo = $users->getUser($noteInfo->getUserId(), 'id'); $userInfo = $usersCtx->getUserInfo($noteInfo->getUserId());
$authorInfo = $noteInfo->hasAuthorId() ? $users->getUser($noteInfo->getAuthorId(), 'id') : null; $authorInfo = $noteInfo->hasAuthorId() ? $usersCtx->getUserInfo($noteInfo->getAuthorId()) : null;
} }
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
@ -75,7 +72,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
); );
// this is easier // this is easier
url_redirect('manage-users-note', ['note' => $noteInfo->getId()]); Tools::redirect($urls->format('manage-users-note', ['note' => $noteInfo->getId()]));
return; return;
} }
@ -83,7 +80,7 @@ Template::render('manage.users.note', [
'note_new' => $isNew, 'note_new' => $isNew,
'note_info' => $noteInfo ?? null, 'note_info' => $noteInfo ?? null,
'note_user' => $userInfo, 'note_user' => $userInfo,
'note_user_colour' => $users->getUserColour($userInfo), 'note_user_colour' => $usersCtx->getUserColour($userInfo),
'note_author' => $authorInfo, 'note_author' => $authorInfo,
'note_author_colour' => $users->getUserColour($authorInfo), 'note_author_colour' => $usersCtx->getUserColour($authorInfo),
]); ]);

View file

@ -3,68 +3,40 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) { if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_NOTES_MANAGE))
echo render_error(403); Template::throwError(403);
return;
}
$users = $msz->getUsers(); $usersCtx = $msz->getUsersContext();
$modNotes = $usersCtx->getModNotes();
$userInfos = [
$msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
];
$filterUser = null; $filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) { if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$filterUser = $users->getUser($filterUserId, 'id'); $filterUser = $usersCtx->getUserInfo($filterUserId);
$userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
} }
$modNotes = $msz->getModNotes();
$pagination = new Pagination($modNotes->countNotes(userInfo: $filterUser), 10); $pagination = new Pagination($modNotes->countNotes(userInfo: $filterUser), 10);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset())
echo render_error(404); Template::throwError(404);
return;
}
$notes = []; $notes = [];
$noteInfos = $modNotes->getNotes(userInfo: $filterUser, pagination: $pagination); $noteInfos = $modNotes->getNotes(userInfo: $filterUser, pagination: $pagination);
foreach($noteInfos as $noteInfo) { foreach($noteInfos as $noteInfo) {
if(array_key_exists($noteInfo->getUserId(), $userInfos)) $userInfo = $usersCtx->getUserInfo($noteInfo->getUserId());
$userInfo = $userInfos[$noteInfo->getUserId()]; $userColour = $usersCtx->getUserColour($userInfo);
else
$userInfos[$noteInfo->getUserId()] = $userInfo = $users->getUser($noteInfo->getUserId(), 'id');
if(array_key_exists($userInfo->getId(), $userColours))
$userColour = $userColours[$userInfo->getId()];
else
$userColours[$userInfo->getId()] = $userColour = $users->getUserColour($userInfo);
if(!$noteInfo->hasAuthorId()) { if(!$noteInfo->hasAuthorId()) {
$authorInfo = null; $authorInfo = null;
$authorColour = null; $authorColour = null;
} else { } else {
if(array_key_exists($noteInfo->getAuthorId(), $userInfos)) $authorInfo = $usersCtx->getUserInfo($noteInfo->getAuthorId());
$authorInfo = $userInfos[$noteInfo->getAuthorId()]; $authorColour = $usersCtx->getUserColour($authorInfo);
else
$userInfos[$noteInfo->getAuthorId()] = $modInfo = $users->getUser($noteInfo->getAuthorId(), 'id');
if(array_key_exists($authorInfo->getId(), $userColours))
$authorColour = $userColours[$authorInfo->getId()];
else
$userColours[$authorInfo->getId()] = $authorColour = $users->getUserColour($authorInfo);
} }
$notes[] = [ $notes[] = [

View file

@ -4,14 +4,18 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Colour\ColourRGB; use Index\Colour\ColourRGB;
use Misuzu\Perm;
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) { $authInfo = $msz->getAuthInfo();
echo render_error(403); $viewerPerms = $authInfo->getPerms('user');
return; if(!$viewerPerms->check(Perm::U_ROLES_MANAGE))
} Template::throwError(403);
$users = $msz->getUsers(); $roleInfo = null;
$roles = $msz->getRoles(); $usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$perms = $msz->getPerms();
if(filter_has_var(INPUT_GET, 'r')) { if(filter_has_var(INPUT_GET, 'r')) {
$roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT); $roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
@ -20,16 +24,15 @@ if(filter_has_var(INPUT_GET, 'r')) {
$isNew = false; $isNew = false;
$roleInfo = $roles->getRole($roleId); $roleInfo = $roles->getRole($roleId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); Template::throwError(404);
return;
} }
} else $isNew = true; } else $isNew = true;
$currentUser = $msz->getActiveUser(); $currentUser = $authInfo->getUserInfo();
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_PERMS); $canEditPerms = $viewerPerms->check(Perm::U_PERMS_MANAGE);
if($canEditPerms) $permsInfos = $roleInfo === null ? null : $perms->getPermissionInfo(roleInfo: $roleInfo, categoryNames: Perm::INFO_FOR_ROLE);
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0)); $permsLists = Perm::createList(Perm::LISTS_FOR_ROLE);
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$userRank = $users->getUserRank($currentUser); $userRank = $users->getUserRank($currentUser);
@ -120,36 +123,27 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
[$roleInfo->getId()] [$roleInfo->getId()]
); );
if(!empty($permissions) && !empty($_POST['perms']) && is_array($_POST['perms'])) { if($canEditPerms && filter_has_var(INPUT_POST, 'perms')) {
$perms = manage_perms_apply($permissions, $_POST['perms']); $permsApply = Perm::convertSubmission(
filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY),
Perm::INFO_FOR_ROLE
);
if($perms !== null) { foreach($permsApply as $categoryName => $values)
$permKeys = array_keys($perms); $perms->setPermissions($categoryName, $values['allow'], $values['deny'], roleInfo: $roleInfo);
$setPermissions = DB::prepare('
REPLACE INTO `msz_permissions` (`role_id`, `user_id`, `' . implode('`, `', $permKeys) . '`)
VALUES (:role_id, NULL, :' . implode(', :', $permKeys) . ')
');
$setPermissions->bind('role_id', $roleInfo->getId());
foreach($perms as $key => $value) { // could target all users with the role but ech
$setPermissions->bind($key, $value); $msz->getConfig()->setBoolean('perms.needsRecalc', true);
}
$setPermissions->execute();
} else {
$deletePermissions = DB::prepare('DELETE FROM `msz_permissions` WHERE `role_id` = :role_id AND `user_id` IS NULL');
$deletePermissions->bind('role_id', $roleInfo->getId());
$deletePermissions->execute();
}
} }
url_redirect('manage-role', ['role' => $roleInfo->getId()]); Tools::redirect($msz->getURLs()->format('manage-role', ['role' => $roleInfo->getId()]));
return; return;
} }
Template::render('manage.users.role', [ Template::render('manage.users.role', [
'role_new' => $isNew, 'role_new' => $isNew,
'role_info' => $roleInfo ?? null, 'role_info' => $roleInfo ?? null,
'can_manage_perms' => $canEditPerms, 'can_edit_perms' => $canEditPerms,
'permissions' => $permissions ?? [], 'perms_lists' => $permsLists,
'perms_infos' => $permsInfos,
]); ]);

Some files were not shown because too many files have changed in this diff Show more