Compare commits

...

113 commits

Author SHA1 Message Date
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
flash b7de5acfd8 Fixed search and updated collations of various fields to more appropriate ones. 2023-08-03 12:40:37 +00:00
flash 9dd7156c79 Fixed issue caused by used of dangling variable on sessions page. 2023-08-03 01:43:43 +00:00
flash 00d1d2922d Changed the way msz_auth is handled.
Going forward msz_auth is always assumed to be present, even while the user is not logged in.
If the cookie is not present a default, empty value will be used.
The msz_uid and msz_sid cookies are also still upconverted for some reason but are no longer removed even though there's no active sessions that can possibly have those anymore.
As with the previous change, shit may be broken so report any Anomalies you come across, through flashii-issues@flash.moe if necessary.
2023-08-03 01:35:08 +00:00
flash 383e2ed0e0 Rewrote the user information class.
This one took multiple days and it pretty invasive into the core of Misuzu so issue might (will) arise, there's also some features that have gone temporarily missing in the mean time and some inefficiencies introduced that will be fixed again at a later time.
The old class isn't gone entirely because I still have to figure out what I'm gonna do about validation, but for the most part this knocks out one of the "layers of backwards compatibility", as I've been referring to it, and is moving us closer to a future where Flashii actually gets real updates.
If you run into anything that's broken and you're inhibited from reporting it through the forum, do it through chat or mail me at flashii-issues@flash.moe.
2023-08-02 22:12:47 +00:00
304 changed files with 18667 additions and 11909 deletions

6
.gitignore vendored
View file

@ -11,6 +11,8 @@
/composer.local.json
# Configuration
/config/config.cfg
/config/github.cfg
/config/config.ini
/config/github.ini
/.debug
@ -43,3 +45,7 @@
# Google
/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.
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.
## Requirements
- PHP 8.2
- PHP 8.2 (64-bit)
- MariaDB 10.6
- [Composer](https://getcomposer.org/)

View file

@ -17,15 +17,10 @@ exports.process = async function(root, options) {
return '';
included.push(fullPath);
if(!fullPath.startsWith(root)) {
console.error('INVALID PATH: ' + fullPath);
if(!fullPath.startsWith(root))
return '/* *** INVALID PATH: ' + fullPath + ' */';
}
if(!fs.existsSync(fullPath)) {
console.error('FILE NOT FOUND: ' + fullPath);
if(!fs.existsSync(fullPath))
return '/* *** FILE NOT FOUND: ' + fullPath + ' */';
}
const lines = readline.createInterface({
input: fs.createReadStream(fullPath),
@ -58,6 +53,19 @@ exports.process = async function(root, options) {
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:
output += line;
output += "\n";
@ -84,7 +92,7 @@ exports.housekeep = function(assetsPath) {
};
}).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 = {};
for(const fileName of files) {

View file

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

View file

@ -146,9 +146,14 @@
}
.header__desktop__user__button__count {
position: absolute;
bottom: 1px;
right: 1px;
font-size: 10px;
top: -5px;
right: -3px;
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);
opacity: .9;
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;
box-sizing: border-box;
position: relative;
outline-style: none;
}
html,
@ -57,6 +56,8 @@ body {
html {
scrollbar-color: var(--accent-colour) var(--background-colour);
accent-color: var(--accent-colour);
color-scheme: dark;
}
.main {
@ -117,6 +118,8 @@ html {
@include permissions.css;
@include warning.css;
@include hljs.css;
@include _input/button.css;
@include _input/checkbox.css;
@include _input/colour.css;
@ -161,6 +164,8 @@ html {
@include manage/_manage.css;
@include messages/messages.css;
@include news/container.css;
@include news/feeds.css;
@include news/list.css;

View file

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

View file

@ -17,4 +17,5 @@
display: flex;
justify-content: center;
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
const MszAudioEmbedPlayerEvents = function() {
@ -56,7 +56,7 @@ const MszAudioEmbedPlayer = function(metadata, options) {
if(haveNativeControls)
playerAttrs.controls = 'controls';
const watchers = new MszWatcherCollection;
const watchers = new MszWatchers;
watchers.define(MszAudioEmbedPlayerEvents());
const player = $e({
@ -84,7 +84,8 @@ const MszAudioEmbedPlayer = function(metadata, options) {
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); });

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) {
options = options || {};

View file

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

View file

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

View file

@ -1,15 +1,15 @@
Misuzu.Events = {};
const MszSeasonalEvents = function() {
const events = [];
#include events/christmas2019.js
Misuzu.Events.getList = function() {
return [
new Misuzu.Events.Christmas2019,
];
};
Misuzu.Events.dispatch = function() {
var list = Misuzu.Events.getList();
for(var i = 0; i < list.length; ++i)
if(list[i].isActive())
list[i].dispatch();
return {
add: eventInfo => {
if(!events.includes(eventInfo))
events.push(eventInfo);
},
dispatch: () => {
for(const info of events)
if(info.isActive())
info.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 divisions = [
{ 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
var Misuzu = function() {
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 utility.js
#include embed/embed.js
#include events/christmas2019.js
#include events/events.js
#include ext/sakuya.js
#include forum/editor.jsx
#include messages/messages.js
Misuzu.showMessageBox = function(text, title, buttons) {
if($q('.messagebox'))
return false;
(async () => {
const initLoginPage = async () => {
const forms = Array.from($qa('.js-login-form'));
if(forms.length < 1)
return;
text = text || '';
title = title || '';
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)
const updateForm = async (avatar, userName) => {
if(!(avatar instanceof Element) || !(userName instanceof Element))
return;
var json = JSON.parse(xhr.responseText);
if(!json)
return;
const result = (await $x.get(`/auth/login.php?resolve=1&name=${encodeURIComponent(userName.value)}`, { type: 'json' })).body();
if(json.name)
usernameElem.value = json.name;
avatarElem.src = json.avatar;
});
// 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();
};
avatar.src = result.avatar;
if(result.name.length > 0)
userName.value = result.name;
};
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)
(function(form) {
var loginTimeOut = 0,
loginAvatar = form.querySelector('.js-login-avatar'),
loginUsername = form.querySelector('.js-login-username');
await updateForm(avatar, userName);
updateForm(loginAvatar, loginUsername);
loginUsername.addEventListener('keyup', function() {
if(loginTimeOut)
userName.addEventListener('input', function() {
if(timeOut !== undefined)
return;
loginTimeOut = setTimeout(function() {
updateForm(loginAvatar, loginUsername);
clearTimeout(loginTimeOut);
loginTimeOut = 0;
timeOut = setTimeout(() => {
updateForm(avatar, userName)
.finally(() => {
clearTimeout(timeOut);
timeOut = undefined;
});
}, 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)
ctrlSubmit[i].addEventListener('keydown', function(ev) {
if((ev.code === 'Enter' || ev.code === 'NumpadEnter') // i hate this fucking language so much
&& 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
Misuzu.Forum.Editor.allowWindowClose = true;
const initQuickSubmit = () => {
const elems = Array.from($qa('.js-quick-submit, .js-ctrl-enter-submit'));
if(elems.length < 1)
return;
this.form.submit();
ev.preventDefault();
}
});
};
for(const elem of elems)
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));
};
const $rq = function(query) {
$r($q(query));
};
const $ib = function(ref, elem) {
ref.parentNode.insertBefore(elem, ref);
};
@ -79,6 +83,11 @@ const $e = function(info, attrs, child, created) {
}
break;
case 'boolean':
if(attr)
elem.setAttribute(key, '');
break;
default:
if(key === 'className')
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 || '';
tagClose = tagClose || '';
if(document.selection) {
target.focus();
var selected = document.selection.createRange();
const selected = document.selection.createRange();
selected.text = tagOpen + selected.text + tagClose;
target.focus();
} else if(target.selectionStart || target.selectionStart === 0) {
var startPos = target.selectionStart,
const startPos = target.selectionStart,
endPos = target.selectionEnd,
scrollTop = target.scrollTop;
@ -176,7 +293,7 @@ var $insertTags = function(target, tagOpen, tagClose) {
target.focus();
target.selectionStart = startPos + tagOpen.length;
target.selectionEnd = endPos + tagOpen.length;
target.scrollTop + scrollTop;
target.scrollTop = scrollTop;
} else {
target.value += tagOpen + tagClose;
target.focus();

View file

@ -1,83 +1,65 @@
const MszWatcher = function() {
let watchers = [];
const handlers = [];
return {
watch: function(watcher, thisArg, args) {
if(typeof watcher !== 'function')
throw 'watcher must be a function';
if(watchers.indexOf(watcher) >= 0)
return;
const watch = (handler, ...args) => {
if(typeof handler !== 'function')
throw 'handler must be a function';
if(handlers.includes(handler))
throw 'handler already registered';
watchers.push(watcher);
if(thisArg !== undefined) {
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);
handlers.push(handler);
args.push(true);
handler(...args);
};
const unwatch = function(name, watcher) {
const watchers = collection.get(name);
if(watchers === undefined)
throw 'undefined watcher name';
watchers.unwatch(watcher);
const unwatch = handler => {
$ari(handlers, handler);
};
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,
unwatch: unwatch,
proxy: function(obj) {
obj.watch = function(name, watcher) {
watch(name, watcher);
};
obj.unwatch = unwatch;
call: (...args) => {
args.push(false);
for(const handler of handlers)
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');
exports.strtr = (str, replacements) => str.toString().replace(
/{([^}]+)}/g, (match, key) => replacements[key] || match
);
const trim = function(str, chars, flags) {
if(chars === undefined)
chars = " \n\r\t\v\0";

146
build.js
View file

@ -1,3 +1,4 @@
// IMPORTS
const fs = require('fs');
const swc = require('@swc/core');
const path = require('path');
@ -6,25 +7,42 @@ const postcss = require('postcss');
const utils = require('./assets/utils.js');
const assproc = require('./assets/assproc.js');
// CONFIG
const rootDir = __dirname;
const modulesDir = path.join(rootDir, 'node_modules');
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 srcDir = path.join(rootDir, 'assets');
const srcCurrentInfo = path.join(srcDir, 'current.json');
const pubDir = path.join(rootDir, 'public');
const pubIndex = path.join(pubDir, 'index.html');
const pubAssets = '/assets';
const pubAssetsFull = path.join(pubDir, pubAssets);
const pubAssetCSSFormat = '%s-%s.css';
const pubAssetJSFormat = '%s-%s.js';
const pubAssetsDir = path.join(pubDir, 'assets');
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 = {
target: 'es2016',
target: 'es2021',
loose: false,
externalHelpers: false,
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 () => {
const mszCssName = await assproc.process(assetsCSS, { 'prefix': '@', 'entry': 'main.css' })
.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 files = {};
const mszJsName = await assproc.process(assetsJS, { 'prefix': '#', 'entry': 'main.js' })
.then(output => swc.transform(output, {
filename: 'misuzu.js',
console.log('Ensuring assets directory exists...');
fs.mkdirSync(pubAssetsDir, { recursive: true });
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,
isModule: false,
minify: !isDebugBuild,
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({
mszjs: mszJsName,
mszcss: mszCssName,
}));
const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
.then(output => swc.transform(output, swcOpts))
.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,
"require": {
"flashwave/index": "dev-master",
"twig/twig": "^3.0",
"flashwave/sasae": "dev-master",
"erusev/parsedown": "~1.6",
"chillerlan/php-qrcode": "^4.3",
"symfony/mailer": "^6.0",
"matomo/device-detector": "^6.1"
"matomo/device-detector": "^6.1",
"sentry/sdk": "^4.0",
"flashwave/syokuhou": "dev-master"
},
"autoload": {
"classmap": [
@ -27,7 +29,8 @@
"preferred-install": "dist",
"allow-plugins": {
"composer/installers": true,
"wikimedia/composer-merge-plugin": false
"wikimedia/composer-merge-plugin": false,
"php-http/discovery": true
}
},
"require-dev": {

1165
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,162 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class UpdateCollationsInVariousTables_20230803_114403 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('
ALTER TABLE msz_audit_log
CHANGE COLUMN log_action log_action VARCHAR(50) NOT NULL COLLATE "ascii_general_ci" AFTER user_id,
CHANGE COLUMN log_country log_country CHAR(2) NOT NULL DEFAULT "XX" COLLATE "ascii_general_ci" AFTER log_ip;
');
$conn->execute('
ALTER TABLE msz_auth_tfa
CHANGE COLUMN tfa_token tfa_token CHAR(32) NOT NULL COLLATE "ascii_bin" AFTER user_id;
');
$conn->execute('
ALTER TABLE msz_changelog_changes
CHANGE COLUMN change_log change_log VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER change_created,
CHANGE COLUMN change_text change_text TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER change_log;
');
$conn->execute('
ALTER TABLE msz_changelog_tags
CHANGE COLUMN tag_name tag_name VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER tag_id,
CHANGE COLUMN tag_description tag_description TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER tag_name;
');
$conn->execute('
ALTER TABLE msz_comments_categories
CHANGE COLUMN category_name category_name VARCHAR(255) NOT NULL COLLATE "ascii_bin" AFTER category_id;
');
$conn->execute('
ALTER TABLE msz_comments_posts
CHANGE COLUMN comment_text comment_text TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER comment_reply_to;
');
$conn->execute('
ALTER TABLE msz_config
CHANGE COLUMN config_name config_name VARCHAR(100) NOT NULL COLLATE "ascii_general_ci" FIRST;
');
$conn->execute('
ALTER TABLE msz_emoticons
CHANGE COLUMN emote_url emote_url VARCHAR(255) NOT NULL COLLATE "ascii_bin" AFTER emote_hierarchy;
');
$conn->execute('
ALTER TABLE msz_emoticons_strings
CHANGE COLUMN emote_string emote_string VARCHAR(50) NOT NULL COLLATE "ascii_general_ci" AFTER emote_string_order;
');
$conn->execute('
ALTER TABLE msz_forum_categories
CHANGE COLUMN forum_name forum_name VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER forum_parent,
CHANGE COLUMN forum_description forum_description TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER forum_type,
CHANGE COLUMN forum_icon forum_icon VARCHAR(50) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER forum_description,
CHANGE COLUMN forum_link forum_link VARCHAR(255) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER forum_colour;
');
$conn->execute('
ALTER TABLE msz_forum_posts
CHANGE COLUMN post_text post_text MEDIUMTEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER post_ip;
');
$conn->execute('
ALTER TABLE msz_forum_topics
CHANGE COLUMN topic_title topic_title VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER topic_type;
');
$conn->execute('
ALTER TABLE msz_forum_topics_redirects
CHANGE COLUMN topic_redir_url topic_redir_url VARCHAR(255) NOT NULL COLLATE "ascii_bin" AFTER user_id;
');
$conn->execute('
ALTER TABLE msz_login_attempts
CHANGE COLUMN attempt_country attempt_country CHAR(2) NOT NULL DEFAULT "XX" COLLATE "ascii_general_ci" AFTER attempt_ip,
CHANGE COLUMN attempt_user_agent attempt_user_agent TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER attempt_created;
');
$conn->execute('
ALTER TABLE msz_news_categories
CHANGE COLUMN category_name category_name VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER category_id,
CHANGE COLUMN category_description category_description TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER category_name;
');
$conn->execute('
ALTER TABLE msz_news_posts
CHANGE COLUMN post_title post_title VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER post_is_featured,
CHANGE COLUMN post_text post_text TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER post_title;
');
$conn->execute('
ALTER TABLE msz_profile_fields
CHANGE COLUMN field_key field_key VARCHAR(50) NOT NULL COLLATE "ascii_general_ci" AFTER field_order,
CHANGE COLUMN field_title field_title VARCHAR(50) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER field_key,
CHANGE COLUMN field_regex field_regex VARCHAR(255) NOT NULL COLLATE "ascii_bin" AFTER field_title;
');
$conn->execute('
ALTER TABLE msz_profile_fields_formats
CHANGE COLUMN format_regex format_regex VARCHAR(255) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER field_id,
CHANGE COLUMN format_link format_link VARCHAR(255) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER format_regex,
CHANGE COLUMN format_display format_display VARCHAR(255) NOT NULL DEFAULT "%s" COLLATE "utf8mb4_unicode_520_ci" AFTER format_link;
');
$conn->execute('
ALTER TABLE msz_profile_fields_values
CHANGE COLUMN field_value field_value VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER format_id;
');
$conn->execute('
ALTER TABLE msz_roles
CHANGE COLUMN role_name role_name VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER role_hierarchy,
CHANGE COLUMN role_title role_title VARCHAR(64) NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER role_name,
CHANGE COLUMN role_description role_description TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER role_title;
');
$conn->execute('
ALTER TABLE msz_sessions
CHANGE COLUMN session_user_agent session_user_agent TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER session_ip_last,
CHANGE COLUMN session_country session_country CHAR(2) NOT NULL DEFAULT "XX" COLLATE "ascii_general_ci" AFTER session_client_info;
');
$conn->execute('
ALTER TABLE msz_users
CHANGE COLUMN username username VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER user_id,
CHANGE COLUMN password password VARCHAR(255) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER username,
CHANGE COLUMN email email VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER password,
CHANGE COLUMN user_country user_country CHAR(2) NOT NULL DEFAULT "XX" COLLATE "ascii_general_ci" AFTER user_super,
CHANGE COLUMN user_totp_key user_totp_key CHAR(26) NULL DEFAULT NULL COLLATE "ascii_bin" AFTER display_role,
CHANGE COLUMN user_about_content user_about_content TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER user_totp_key,
CHANGE COLUMN user_signature_content user_signature_content TEXT NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER user_about_parser,
CHANGE COLUMN user_title user_title VARCHAR(64) NULL DEFAULT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER user_background_settings;
');
$conn->execute('
ALTER TABLE msz_users_bans
CHANGE COLUMN ban_reason_public ban_reason_public TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER ban_severity,
CHANGE COLUMN ban_reason_private ban_reason_private TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER ban_reason_public;
');
$conn->execute('
ALTER TABLE msz_users_modnotes
CHANGE COLUMN note_title note_title VARCHAR(255) NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER note_created,
CHANGE COLUMN note_body note_body TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER note_title;
');
$conn->execute('
ALTER TABLE msz_users_password_resets
CHANGE COLUMN verification_code verification_code CHAR(12) NULL DEFAULT NULL COLLATE "ascii_general_ci" AFTER reset_requested;
');
$conn->execute('
ALTER TABLE msz_users_warnings
CHANGE COLUMN warn_body warn_body TEXT NOT NULL COLLATE "utf8mb4_unicode_520_ci" AFTER mod_id;
');
}
}

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

@ -1,12 +1,29 @@
# Source Code Repositories
- [Misuzu](https://git.flash.moe/flashii/misuzu): Backend of the main website.
- [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.
- [Index](https://git.flash.moe/flash/index): Base library used in almost any component of the website that uses PHP.
- [Uiharu](https://git.flash.moe/flashii/uiharu): Service for looking up URL metadata.
- [Futami](https://git.flash.moe/flashii/futami): Common data shared between the chat clients.
- [AJAX Chat (fork)](https://git.flash.moe/flashii/ajax-chat): Old chat software (2013-2015).
- [Seria](https://git.flash.moe/flashii/seria): Software used by the torrent tracker.
- [Mince](https://git.flash.moe/flashii/mince): Source code for the Minecraft servers subwebsite.
- [Awaki](https://git.flash.moe/flashii/awaki): Redirect service hosted on fii.moe.
Below are a number of links to source code repositories related to Flashii.net and its services.
## Websites & Services
- [Misuzu](https://patchii.net/flashii/misuzu): Backend of the main website.
- [Sharp Chat](https://patchii.net/flashii/sharp-chat): Chat Server software.
- [Futami](https://patchii.net/flashii/futami): Common data shared between the chat clients.
- [Mami](https://patchii.net/flashii/mami): Web client for chat.
- [Ami](https://patchii.net/flashii/ami): Web client for chat for older browsers.
- [EEPROM](https://patchii.net/flashii/eeprom): Service for file uploading.
- [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\Data\DbTools;
use Misuzu\Config\DbConfig;
use Syokuhou\DbConfig;
use Syokuhou\SharpConfig;
define('MSZ_STARTUP', microtime(true));
define('MSZ_ROOT', __DIR__);
@ -19,36 +20,28 @@ define('MSZ_ASSETS', MSZ_ROOT . '/assets');
require_once MSZ_ROOT . '/vendor/autoload.php';
Environment::setDebug(MSZ_DEBUG);
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
mb_internal_encoding('UTF-8');
date_default_timezone_set('UTC');
require_once MSZ_ROOT . '/utility.php';
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';
$cfg = SharpConfig::fromFile(MSZ_CONFIG . '/config.cfg');
$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)) {
echo 'Database config is missing.';
exit;
}
set_exception_handler(function(\Throwable $ex) {
\Sentry\captureException($ex);
});
})($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']);
$db->execute(MSZ_DB_INIT);
DB::init(DbTools::parse($dbConfig['dsn']));
DB::exec(MSZ_DB_INIT);
$cfg = new DbConfig($db);
$cfg = new DbConfig($db, 'msz_config');
Mailer::init($cfg->scopeTo('mail'));

576
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,33 +1,40 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
use Exception;
use Misuzu\Auth\AuthTokenCookie;
if(User::hasCurrent()) {
url_redirect('index');
$urls = $msz->getURLs();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return;
}
$authCtx = $msz->getAuthContext();
$users = $msz->getUsersContext()->getUsers();
$sessions = $authCtx->getSessions();
$loginAttempts = $authCtx->getLoginAttempts();
if(!empty($_GET['resolve'])) {
header('Content-Type: application/json; charset=utf-8');
try {
// Only works for usernames, this is by design
$userInfo = User::byUsername((string)filter_input(INPUT_GET, 'name'));
} catch(RuntimeException $ex) {
$userInfo = $users->getUser((string)filter_input(INPUT_GET, 'name'), 'name');
} catch(Exception $ex) {
echo json_encode([
'id' => 0,
'name' => '',
'avatar' => url('user-avatar', ['res' => 200, 'user' => 0]),
'avatar' => $urls->format('user-avatar', ['res' => 200, 'user' => 0]),
]);
return;
}
echo json_encode([
'id' => $userInfo->getId(),
'name' => $userInfo->getUsername(),
'avatar' => url('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
'id' => (int)$userInfo->getId(),
'name' => $userInfo->getName(),
'avatar' => $urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
]);
return;
}
@ -37,9 +44,6 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$sessions = $msz->getSessions();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
$siteIsPrivate = $cfg->getBoolean('private.enable');
@ -91,38 +95,36 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
try {
$userInfo = User::byUsernameOrEMailAddress($_POST['login']['username']);
} catch(RuntimeException $ex) {
$userInfo = $users->getUser($_POST['login']['username'], 'login');
} catch(Exception $ex) {
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
$notices[] = $loginFailedError;
break;
}
if(!$userInfo->hasPassword()) {
if(!$userInfo->hasPasswordHash()) {
$notices[] = 'Your password has been invalidated, please reset it.';
break;
}
if($userInfo->isDeleted() || !$userInfo->checkPassword($_POST['login']['password'])) {
if($userInfo->isDeleted() || !$userInfo->verifyPassword($_POST['login']['password'])) {
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
$notices[] = $loginFailedError;
break;
}
if($userInfo->passwordNeedsRehash())
$userInfo->setPassword($_POST['login']['password'])->save();
$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.";
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
break;
}
if($userInfo->hasTOTPKey()) {
$tfaToken = $msz->getTFASessions()->createToken($userInfo);
url_redirect('auth-two-factor', [
'token' => $tfaToken,
]);
$tfaToken = $authCtx->getTwoFactorAuthSessions()->createToken($userInfo);
Tools::redirect($urls->format('auth-two-factor', ['token' => $tfaToken]));
return;
}
@ -130,18 +132,23 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
try {
$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!";
break;
}
$authToken = AuthToken::create($userInfo, $sessionInfo);
$authToken->applyCookie($sessionInfo->getExpiresTime());
$tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
$tokenBuilder->setUserId($userInfo);
$tokenBuilder->setSessionToken($sessionInfo);
$tokenBuilder->removeImpersonatedUserId();
$tokenInfo = $tokenBuilder->toInfo();
if(!is_local_url($loginRedirect))
$loginRedirect = url('index');
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
redirect($loginRedirect);
if(!Tools::isLocalURL($loginRedirect))
$loginRedirect = $urls->format('index');
Tools::redirect($loginRedirect);
return;
}
@ -149,7 +156,7 @@ $welcomeMode = !empty($_GET['welcome']);
$loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login']['username']) ? $_POST['login']['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;
Template::render('auth.login', [

View file

@ -1,18 +1,27 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Auth\AuthTokenCookie;
if(!User::hasCurrent()) {
url_redirect('index');
return;
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
if(!CSRF::validateRequest()) {
Template::render('auth.logout');
return;
}
$tokenInfo = $authInfo->getTokenInfo();
$authCtx = $msz->getAuthContext();
$authCtx->getSessions()->deleteSessions(sessionTokens: $tokenInfo->getSessionToken());
$tokenBuilder = $tokenInfo->toBuilder();
$tokenBuilder->removeUserId();
$tokenBuilder->removeSessionToken();
$tokenBuilder->removeImpersonatedUserId();
$tokenInfo = $tokenBuilder->toInfo();
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
}
if(CSRF::validateRequest()) {
$msz->getSessions()->deleteSessions(sessionTokens: $authToken->getSessionToken());
AuthToken::nukeCookie();
url_redirect('index');
return;
}
Template::render('auth.logout');
Tools::redirect($msz->getURLs()->format('index'));;

View file

@ -4,11 +4,18 @@ namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(User::hasCurrent()) {
url_redirect('settings-account');
$urls = $msz->getURLs();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('settings-account'));
return;
}
$authCtx = $msz->getAuthContext();
$users = $msz->getUsersContext()->getUsers();
$recoveryTokens = $authCtx->getRecoveryTokens();
$loginAttempts = $authCtx->getLoginAttempts();
$reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : [];
$forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : [];
$userId = !empty($reset['user']) ? (int)$reset['user'] : (
@ -17,9 +24,9 @@ $userId = !empty($reset['user']) ? (int)$reset['user'] : (
if($userId > 0)
try {
$userInfo = User::byId($userId);
$userInfo = $users->getUser((string)$userId, 'id');
} catch(RuntimeException $ex) {
url_redirect('auth-forgot');
Tools::redirect($urls->format('auth-forgot'));
return;
}
@ -28,8 +35,6 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
$siteIsPrivate = $cfg->getBoolean('private.enable');
$canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true;
$recoveryTokens = $msz->getRecoveryTokens();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
while($canResetPassword) {
@ -62,22 +67,21 @@ while($canResetPassword) {
break;
}
if(User::validatePassword($passwordNew) !== '') {
$notices[] = 'Your password is too weak!';
$passwordValidation = $users->validatePassword($passwordNew);
if($passwordValidation !== '') {
$notices[] = $users->validatePasswordText($passwordValidation);
break;
}
// also disables two factor auth to prevent getting locked out of account entirely
// this behaviour should really be replaced with recovery keys...
$userInfo->setPassword($passwordNew)
->removeTOTPKey()
->save();
$users->updateUser($userInfo, password: $passwordNew, totpKey: '');
$msz->createAuditLog('PASSWORD_RESET', [], $userInfo);
$recoveryTokens->invalidateToken($tokenInfo);
url_redirect('auth-login', ['redirect' => '/']);
Tools::redirect($urls->format('auth-login', ['redirect' => '/']));
return;
}
@ -98,7 +102,7 @@ while($canResetPassword) {
}
try {
$forgotUser = User::byEMailAddress($forgot['email']);
$forgotUser = $users->getUser($forgot['email'], 'email');
} catch(RuntimeException $ex) {
unset($forgotUser);
}
@ -114,12 +118,12 @@ while($canResetPassword) {
$tokenInfo = $recoveryTokens->createToken($forgotUser, $ipAddress);
$recoveryMessage = Mailer::template('password-recovery', [
'username' => $forgotUser->getUsername(),
'username' => $forgotUser->getName(),
'token' => $tokenInfo->getCode(),
]);
$recoveryMail = Mailer::sendMessage(
[$forgotUser->getEMailAddress() => $forgotUser->getUsername()],
[$forgotUser->getEMailAddress() => $forgotUser->getName()],
$recoveryMessage['subject'], $recoveryMessage['message']
);
@ -130,7 +134,7 @@ while($canResetPassword) {
}
}
url_redirect('auth-reset', ['user' => $forgotUser->getId()]);
Tools::redirect($urls->format('auth-reset', ['user' => $forgotUser->getId()]));
return;
}

View file

@ -4,13 +4,18 @@ namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(User::hasCurrent()) {
url_redirect('index');
$urls = $msz->getURLs();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return;
}
$users = $msz->getUsers();
$roles = $msz->getRoles();
$authCtx = $msz->getAuthContext();
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$config = $msz->getConfig();
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
$notices = [];
@ -28,7 +33,7 @@ $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
// for fast matching
$restricted = '';
$loginAttempts = $msz->getLoginAttempts();
$loginAttempts = $authCtx->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
while(!$restricted && !empty($register)) {
@ -64,41 +69,48 @@ while(!$restricted && !empty($register)) {
break;
}
$usernameValidation = User::validateUsername($register['username']);
$usernameValidation = $users->validateName($register['username']);
if($usernameValidation !== '')
$notices[] = User::usernameValidationErrorString($usernameValidation);
$notices[] = $users->validateNameText($usernameValidation);
$emailValidation = User::validateEMailAddress($register['email']);
$emailValidation = $users->validateEMailAddress($register['email']);
if($emailValidation !== '')
$notices[] = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
$notices[] = $users->validateEMailAddressText($emailValidation);
if($register['password_confirm'] !== $register['password'])
$notices[] = 'The given passwords don\'t match.';
if(User::validatePassword($register['password']) !== '')
$notices[] = 'Your password is too weak!';
$passwordValidation = $users->validatePassword($register['password']);
if($passwordValidation !== '')
$notices[] = $users->validatePasswordText($passwordValidation);
if(!empty($notices))
break;
$defaultRoleInfo = $roles->getDefaultRole();
try {
$createUser = User::create(
$userInfo = $users->createUser(
$register['username'],
$register['password'],
$register['email'],
$ipAddress,
$countryCode
$countryCode,
$defaultRoleInfo
);
} catch(RuntimeException $ex) {
$notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!';
break;
}
$users->addRoles($createUser, $roles->getDefaultRole());
$users->addRoles($userInfo, $defaultRoleInfo);
$config->setString('users.newest', $userInfo->getId());
$msz->getPerms()->precalculatePermissions(
$msz->getForumContext()->getCategories(),
[$userInfo->getId()]
);
url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]);
Tools::redirect($urls->format('auth-login-welcome', ['username' => $userInfo->getName()]));
return;
}

View file

@ -1,19 +1,23 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Auth\AuthTokenCookie;
if(!isset($userInfoReal) || !$authToken->hasImpersonatedUserId() || !CSRF::validateRequest()) {
url_redirect('index');
return;
$urls = $msz->getURLs();
if(CSRF::validateRequest()) {
$tokenInfo = $msz->getAuthInfo()->getTokenInfo();
if($tokenInfo->hasImpersonatedUserId()) {
$impUserId = $tokenInfo->getImpersonatedUserId();
$tokenBuilder = $tokenInfo->toBuilder();
$tokenBuilder->removeImpersonatedUserId();
$tokenInfo = $tokenBuilder->toInfo();
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
Tools::redirect($urls->format('manage-user', ['user' => $impUserId]));
return;
}
}
$authToken->removeImpersonatedUserId();
$authToken->applyCookie();
$impUserId = User::hasCurrent() ? User::getCurrent()->getId() : 0;
url_redirect(
$impUserId > 0 ? 'manage-user' : 'index',
['user' => $impUserId]
);
Tools::redirect($urls->format('index'));

View file

@ -2,23 +2,28 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
use Misuzu\TOTPGenerator;
use Misuzu\Auth\AuthTokenCookie;
if(User::hasCurrent()) {
url_redirect('index');
$urls = $msz->getURLs();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));
return;
}
$authCtx = $msz->getAuthContext();
$users = $msz->getUsersContext()->getUsers();
$sessions = $authCtx->getSessions();
$tfaSessions = $authCtx->getTwoFactorAuthSessions();
$loginAttempts = $authCtx->getLoginAttempts();
$ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
$notices = [];
$sessions = $msz->getSessions();
$tfaSessions = $msz->getTFASessions();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
$tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
@ -27,16 +32,16 @@ $tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['toke
$tokenUserId = $tfaSessions->getTokenUserId($tokenString);
if(empty($tokenUserId)) {
url_redirect('auth-login');
Tools::redirect($urls->format('auth-login'));
return;
}
$userInfo = User::byId((int)$tokenUserId);
$userInfo = $users->getUser($tokenUserId, 'id');
// checking user_totp_key specifically because there's a fringe chance that
// there's a token present, but totp is actually disabled
if(!$userInfo->hasTOTPKey()) {
url_redirect('auth-login');
Tools::redirect($urls->format('auth-login'));
return;
}
@ -60,7 +65,7 @@ while(!empty($twofactor)) {
}
$clientInfo = ClientInfo::fromRequest();
$totp = $userInfo->createTOTPGenerator();
$totp = new TOTPGenerator($userInfo->getTOTPKey());
if(!in_array($twofactor['code'], $totp->generateRange())) {
$notices[] = sprintf(
@ -82,19 +87,24 @@ while(!empty($twofactor)) {
break;
}
$authToken = AuthToken::create($userInfo, $sessionInfo);
$authToken->applyCookie($sessionInfo->getExpiresTime());
$tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
$tokenBuilder->setUserId($userInfo);
$tokenBuilder->setSessionToken($sessionInfo);
$tokenBuilder->removeImpersonatedUserId();
$tokenInfo = $tokenBuilder->toInfo();
if(!is_local_url($redirect))
$redirect = url('index');
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
redirect($redirect);
if(!Tools::isLocalURL($redirect))
$redirect = $urls->format('index');
Tools::redirect($redirect);
return;
}
Template::render('auth.twofactor', [
'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_token' => $tokenString,
]);

View file

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

View file

@ -1,78 +1,179 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
use stdClass;
use RuntimeException;
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
$forumId = max($forumId, 0);
$forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
if($forumId === 0) {
url_redirect('forum-index');
exit;
$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);
$forumUser = User::getCurrent();
$forumUserId = $forumUser === null ? 0 : $forumUser->getId();
$authInfo = $msz->getAuthInfo();
$perms = $authInfo->getPerms('forum', $categoryInfo);
if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) {
echo render_error(404);
return;
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId();
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)) {
echo render_error(403);
return;
}
if(!$forumPagination->hasValidOffset())
Template::throwError(404);
if(isset($forumUser) && $msz->hasActiveBan($forumUser))
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
$children = [];
$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) {
forum_increment_clicks($forum['forum_id']);
redirect($forum['forum_link']);
return;
}
foreach($children as $childId => $child) {
$childPerms = $authInfo->getPerms('forum', $child->info);
if(!$childPerms->check(Perm::F_CATEGORY_LIST)) {
unset($category->children[$childId]);
continue;
}
$forumPagination = new Pagination($forum['forum_topic_count'], 20);
$childUnread = false;
if(!$forumPagination->hasValidOffset() && $forum['forum_topic_count'] > 0) {
echo render_error(404);
return;
}
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;
}
$forumMayHaveTopics = forum_may_have_topics($forum['forum_type']);
$topics = $forumMayHaveTopics
? forum_topic_listing(
$forum['forum_id'],
$forumUserId,
$forumPagination->getOffset(),
$forumPagination->getRange(),
perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)
)
: [];
$grandChildUnread = false;
$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) {
$forum['forum_subforums'] = forum_get_children($forum['forum_id'], $forumUserId);
$grandChildUnread = $forumCategories->checkCategoryUnread($catIds, $currentUser);
if($grandChildUnread)
$childUnread = true;
}
foreach($forum['forum_subforums'] as $skey => $subforum) {
$forum['forum_subforums'][$skey]['forum_subforums']
= forum_get_children($subforum['forum_id'], $forumUserId);
$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);
$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', [
'forum_breadcrumbs' => forum_get_breadcrumbs($forum['forum_id']),
'global_accent_colour' => forum_get_colour($forum['forum_id']),
'forum_may_have_topics' => $forumMayHaveTopics,
'forum_may_have_children' => $forumMayHaveChildren,
'forum_info' => $forum,
'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'forum_info' => $categoryInfo,
'forum_children' => $children,
'forum_topics' => $topics,
'forum_pagination' => $forumPagination,
'forum_show_mark_as_read' => $currentUser !== null,
'forum_perms' => $perms,
]);

View file

@ -1,39 +1,191 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
use stdClass;
use RuntimeException;
$indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
$forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$mode = (string)filter_input(INPUT_GET, 'm');
$currentUser = User::getCurrent();
$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
$authInfo = $msz->getAuthInfo();
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId();
switch($indexMode) {
case 'mark':
url_redirect($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]);
break;
if($mode === 'mark') {
if(!$authInfo->isLoggedIn())
Template::throwError(403);
default:
$categories = forum_get_root_categories($currentUserId);
$blankForum = count($categories) < 1;
$categoryId = filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
foreach($categories as $key => $category) {
$categories[$key]['forum_subforums'] = forum_get_children($category['forum_id'], $currentUserId);
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$categoryInfos = $categoryId === null
? $forumCategories->getCategories()
: $forumCategories->getCategoryChildren(parentInfo: $categoryId, includeSelf: true);
foreach($categories[$key]['forum_subforums'] as $skey => $sub) {
if(!forum_may_have_children($sub['forum_type'])) {
continue;
}
$categories[$key]['forum_subforums'][$skey]['forum_subforums']
= forum_get_children($sub['forum_id'], $currentUserId);
}
foreach($categoryInfos as $categoryInfo) {
$perms = $authInfo->getPerms('forum', $categoryInfo);
if($perms->check(Perm::F_CATEGORY_LIST))
$forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
}
Template::render('forum.index', [
'forum_categories' => $categories,
'forum_empty' => $blankForum,
]);
break;
Tools::redirect($msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
return;
}
Template::render('confirm', [
'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,65 +1,113 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
use RuntimeException;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_FORUM, User::getCurrent()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) {
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'] : '';
$leaderboardId = !empty($_GET['id']) && is_string($_GET['id'])
&& ctype_digit($_GET['id'])
? $_GET['id']
: MSZ_FORUM_LEADERBOARD_CATEGORY_ALL;
$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(filter_has_var(INPUT_GET, 'allow_unranked')) {
$unrankedForums = $unrankedTopics = [];
} else {
$unrankedForums = $config['forum_leader.unranked.forum'];
$unrankedTopics = $config['forum_leader.unranked.topic'];
}
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
# {$leaderboardName}
# {$name}
| Rank | Usename | Post count |
| ----:|:------- | ----------:|
MD;
foreach($leaderboard as $user) {
$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']);
$totalPostsCount = 0;
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::render('forum.leaderboard', [
'leaderboard_id' => $leaderboardId,
'leaderboard_name' => $leaderboardName,
'leaderboard_categories' => $leaderboards,
'leaderboard_data' => $leaderboard,
'leaderboard_mode' => $leaderboardMode,
'leaderboard_id' => $yearMonth,
'leaderboard_name' => $name,
'leaderboard_years' => $years,
'leaderboard_months' => $months,
'leaderboard_data' => $rankings,
'leaderboard_mode' => $mode,
]);

View file

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

View file

@ -1,21 +1,26 @@
<?php
namespace Misuzu;
use stdClass;
use RuntimeException;
use Index\DateTime;
use Misuzu\Forum\ForumTopicInfo;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
$currentUser = User::getCurrent();
$authInfo = $msz->getAuthInfo();
if(!$authInfo->isLoggedIn())
Template::throwError(401);
if($currentUser === null) {
echo render_error(401);
return;
}
$forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser->getId();
if($msz->hasActiveBan()) {
echo render_error(403);
return;
}
if($usersCtx->hasActiveBan($currentUser))
Template::throwError(403);
$forumPostingModes = [
'create', 'edit', 'quote', 'preview',
@ -33,10 +38,8 @@ if(!empty($_POST)) {
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
}
if(!in_array($mode, $forumPostingModes, true)) {
echo render_error(400);
return;
}
if(!in_array($mode, $forumPostingModes, true))
Template::throwError(400);
if($mode === 'preview') {
header('Content-Type: text/plain; charset=utf-8');
@ -54,79 +57,83 @@ if($mode === 'preview') {
return;
}
if(empty($postId) && empty($topicId) && empty($forumId)) {
echo render_error(404);
return;
}
if(empty($postId) && empty($topicId) && empty($forumId))
Template::throwError(404);
if(!empty($postId)) {
$post = forum_post_get($postId);
if(isset($post['topic_id'])) { // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first
$topicId = (int)$post['topic_id'];
if(empty($postId)) {
$hasPostInfo = false;
} else {
try {
$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)) {
$topic = forum_topic_get($topicId);
if(isset($topic['forum_id'])) {
$forumId = (int)$topic['forum_id'];
if(empty($topicId)) {
$hasTopicInfo = false;
} else {
try {
$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)) {
$forum = forum_get($forumId);
if(empty($forumId)) {
$hasCategoryInfo = false;
} else {
try {
$categoryInfo = $forumCategories->getCategory(categoryId: $forumId);
} catch(RuntimeException $ex) {
Template::throwError(404);
}
$hasCategoryInfo = true;
}
if(empty($forum)) {
echo render_error(404);
return;
}
$perms = $authInfo->getPerms('forum', $categoryInfo);
$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']
|| (!empty($topic['topic_locked']) && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC))
|| !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;
}
if(!$categoryInfo->mayHaveTopics())
Template::throwError(400);
$topicTypes = [];
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)) {
$topicTypes[MSZ_TOPIC_TYPE_STICKY] = 'Sticky topic';
}
if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC)) {
$topicTypes[MSZ_TOPIC_TYPE_ANNOUNCEMENT] = 'Announcement';
}
if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC)) {
$topicTypes[MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT] = 'Global Announcement';
}
if($perms->check(Perm::F_TOPIC_STICKY))
$topicTypes['sticky'] = 'Sticky topic';
if($perms->check(Perm::F_TOPIC_ANNOUNCE_LOCAL))
$topicTypes['announce'] = 'Announcement';
if($perms->check(Perm::F_TOPIC_ANNOUNCE_GLOBAL))
$topicTypes['global'] = 'Global Announcement';
}
// edit mode stuff
if($mode === 'edit') {
if(empty($post)) {
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;
}
}
if($mode === 'edit' && !$perms->check($postInfo->getUserId() === $currentUserId ? Perm::F_POST_EDIT_OWN : Perm::F_POST_EDIT_ANY))
Template::throwError(403);
$notices = [];
@ -134,38 +141,45 @@ if(!empty($_POST)) {
$topicTitle = $_POST['post']['title'] ?? '';
$postText = $_POST['post']['text'] ?? '';
$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']);
if(!CSRF::validateRequest()) {
$notices[] = 'Could not verify request.';
} else {
$isEditingTopic = empty($topic) || ($mode === 'edit' && $post['is_opening_post']);
$isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->getId() == $postInfo->getId());
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) {
$notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck));
$notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";
if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) {
$waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time());
$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) {
$originalTopicTitle = $topic['topic_title'] ?? null;
$originalTopicTitle = $topicInfo?->getTitle() ?? null;
$topicTitleChanged = $topicTitle !== $originalTopicTitle;
$originalTopicType = (int)($topic['topic_type'] ?? MSZ_TOPIC_TYPE_DISCUSSION);
$originalTopicType = $topicInfo?->getTypeString() ?? 'discussion';
$topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType;
switch(forum_validate_title($topicTitle)) {
case 'too-short':
$notices[] = 'Topic title was too short.';
break;
$topicTitleLengths = $cfg->getValues([
['forum.topic.minLength:i', 3],
['forum.topic.maxLength:i', 100],
]);
case 'too-long':
$notices[] = 'Topic title was too long.';
break;
}
$topicTitleLength = mb_strlen(trim($topicTitle));
if($topicTitleLength < $topicTitleLengths['forum.topic.minLength'])
$notices[] = 'Topic title was too short.';
elseif($topicTitleLength > $topicTitleLengths['forum.topic.maxLength'])
$notices[] = 'Topic title was too long.';
if($mode === 'create' && $topicType === null) {
$topicType = array_key_first($topicTypes);
@ -174,92 +188,121 @@ if(!empty($_POST)) {
}
}
if(!Parser::isValid($postParser)) {
if(!Parser::isValid($postParser))
$notices[] = 'Invalid parser selected.';
}
switch(forum_validate_post($postText)) {
case 'too-short':
$notices[] = 'Post content was too short.';
break;
$postTextLengths = $cfg->getValues([
['forum.post.minLength:i', 1],
['forum.post.maxLength:i', 60000],
]);
case 'too-long':
$notices[] = 'Post content was too long.';
break;
}
$postTextLength = mb_strlen(trim($postText));
if($postTextLength < $postTextLengths['forum.post.minLength'])
$notices[] = 'Post content was too short.';
elseif($postTextLength > $postTextLengths['forum.post.maxLength'])
$notices[] = 'Post content was too long.';
if(empty($notices)) {
switch($mode) {
case 'create':
if(!empty($topic)) {
forum_topic_bump($topic['topic_id']);
} else {
$topicId = forum_topic_create(
$forum['forum_id'],
$currentUserId,
if(empty($topicInfo)) {
$topicInfo = $forumTopics->createTopic(
$categoryInfo,
$currentUser,
$topicTitle,
$topicType
);
}
$postId = forum_post_create(
$topicId = $topicInfo->getId();
$forumCategories->incrementCategoryTopics($categoryInfo);
} else
$forumTopics->bumpTopic($topicInfo);
$postInfo = $forumPosts->createPost(
$topicId,
$forum['forum_id'],
$currentUserId,
$currentUser,
$_SERVER['REMOTE_ADDR'],
$postText,
$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;
case 'edit':
$markUpdated = $post['poster_id'] === $currentUserId
&& $post['post_created_unix'] < strtotime('-1 minutes')
&& $postText !== $post['post_text'];
$markUpdated = $postInfo->getUserId() === $currentUserId
&& $postInfo->shouldMarkAsEdited()
&& $postText !== $postInfo->getBody();
if(!forum_post_update($postId, $_SERVER['REMOTE_ADDR'], $postText, $postParser, $postSignature, $markUpdated)) {
$notices[] = 'Post edit failed.';
}
$forumPosts->updatePost(
$postId,
remoteAddr: $_SERVER['REMOTE_ADDR'],
body: $postText,
bodyParser: $postParser,
displaySignature: $postSignature,
bumpEdited: $markUpdated
);
if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged)) {
if(!forum_topic_update($topicId, $topicTitle, $topicType)) {
$notices[] = 'Topic update failed.';
}
}
if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged))
$forumTopics->updateTopic(
$topicId,
title: $topicTitle,
type: $topicType
);
break;
}
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,
'post' => $postId ?? 0,
'post_fragment' => 'p' . ($postId ?? 0),
]);
redirect($redirect);
Tools::redirect($redirect);
return;
}
}
}
}
if(!empty($topic)) {
Template::set('posting_topic', $topic);
}
if(!empty($topicInfo))
Template::set('posting_topic', $topicInfo);
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);
}
$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', [
'posting_breadcrumbs' => forum_get_breadcrumbs($forumId),
'global_accent_colour' => forum_get_colour($forumId),
'posting_forum' => $forum,
'posting_info' => $displayInfo,
'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'posting_user' => $currentUser,
'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_mode' => $mode,
'posting_types' => $topicTypes,

View file

@ -1,68 +1,107 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
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;
$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'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$topicUser = User::getCurrent();
$topicUserId = $topicUser === null ? 0 : $topicUser->getId();
$authInfo = $msz->getAuthInfo();
$currentUser = $authInfo->getUserInfo();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId();
if($topicId < 1 && $postId > 0) {
$postInfo = forum_post_find($postId, $topicUserId);
if(!empty($postInfo['topic_id'])) {
$topicId = (int)$postInfo['topic_id'];
try {
$postInfo = $forumPosts->getPost(postId: $postId);
} catch(RuntimeException $ex) {
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);
$perms = $topic
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL]
: 0;
try {
$topicIsNuked = $topicIsDeleted = $canDeleteAny = false;
$topicInfo = $forumTopics->getTopic(topicId: $topicId);
} catch(RuntimeException $ex) {
$topicIsNuked = true;
}
if(isset($topicUser) && $msz->hasActiveBan($topicUser))
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
if(!$topicIsNuked) {
$topicIsDeleted = $topicInfo->isDeleted();
$topicIsNuked = empty($topic['topic_id']);
$topicIsDeleted = !empty($topic['topic_deleted']);
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
if($categoryId !== (int)$topicInfo->getCategoryId()) {
$categoryId = (int)$topicInfo->getCategoryId();
$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) {
$topicRedirectInfo = forum_topic_redir_info($topicId);
Template::set('topic_redir_info', $topicRedirectInfo);
if($forumTopicRedirects->hasTopicRedirect($topicId)) {
$topicRedirectInfo = $forumTopicRedirects->getTopicRedirect($topicId);
Template::set('topic_redir_info', $topicRedirectInfo);
if($topicIsNuked || !$canDeleteAny) {
if(empty($topicRedirectInfo))
echo render_error(404);
else
header('Location: ' . $topicRedirectInfo->topic_redir_url);
return;
if($topicIsNuked || !$canDeleteAny) {
header('Location: ' . $topicRedirectInfo->getLinkTarget());
return;
}
}
if(empty($topicRedirectInfo))
Template::throwError(404);
}
if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
echo render_error(403);
return;
}
if(!$perms->check(Perm::F_CATEGORY_VIEW))
Template::throwError(403);
$topicIsLocked = !empty($topic['topic_locked']);
$topicIsArchived = !empty($topic['topic_archived']);
$topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']);
// Maximum amount of posts a topic may contain to still be deletable by the author
// this should be in the config
$deletePostThreshold = 1;
$categoryInfo = $forumCategories->getCategory(topicInfo: $topicInfo);
$topicIsLocked = $topicInfo->isLocked();
$topicIsArchived = $categoryInfo->isArchived();
$topicPostsTotal = $topicInfo->getTotalPostsCount();
$topicIsFrozen = $topicIsArchived || $topicIsDeleted;
$canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && perms_check($perms, MSZ_FORUM_PERM_DELETE_POST);
$canBumpTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_BUMP_TOPIC);
$canLockTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC);
$canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && $perms->check(Perm::F_POST_DELETE_OWN);
$canBumpTopic = !$topicIsFrozen && $perms->check(Perm::F_TOPIC_BUMP);
$canLockTopic = !$topicIsFrozen && $perms->check(Perm::F_TOPIC_LOCK);
$canNukeOrRestore = $canDeleteAny && $topicIsDeleted;
$canDelete = !$topicIsDeleted && (
$canDeleteAny || (
$topicPostsTotal > 0
&& $topicPostsTotal <= MSZ_FORUM_TOPIC_DELETE_POST_LIMIT
&& $topicPostsTotal <= $deletePostThreshold
&& $canDeleteOwn
&& $topic['author_user_id'] === $topicUserId
&& $topicInfo->getUserId() === (string)$currentUserId
)
);
@ -72,256 +111,229 @@ $validModerationModes = [
];
if(in_array($moderationMode, $validModerationModes, true)) {
if(!CSRF::validateRequest()) {
echo render_info("Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
if(!CSRF::validateRequest())
Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
if(!User::hasCurrent()) {
echo render_info('You must be logged in to manage posts.', 401);
return;
}
$authInfo = $authInfo;
if(!$authInfo->isLoggedIn())
Template::displayInfo('You must be logged in to manage posts.', 401);
if($msz->hasActiveBan()) {
echo render_info('You have been banned, check your profile for more information.', 403);
return;
}
if($usersCtx->hasActiveBan($currentUser))
Template::displayInfo('You have been banned, check your profile for more information.', 403);
switch($moderationMode) {
case 'delete':
$canDeleteCode = forum_topic_can_delete($topic, $topicUserId);
$canDeleteMsg = '';
$responseCode = 200;
if($canDeleteAny) {
if($topicInfo->isDeleted())
Template::displayInfo('This topic has already been marked as deleted.', 404);
} else {
if($topicInfo->isDeleted())
Template::throwError(404);
switch($canDeleteCode) {
case MSZ_E_FORUM_TOPIC_DELETE_USER:
$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(!$canDeleteOwn)
Template::displayInfo("You aren't allowed to delete topics.", 403);
if($canDeleteCode !== MSZ_E_FORUM_TOPIC_DELETE_OK) {
echo render_info($canDeleteMsg, $responseCode);
break;
if($topicInfo->getUserId() !== $currentUser->getId())
Template::displayInfo('You can only delete your own topics.', 403);
// 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'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic deletion',
'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' => [
't' => $topic['topic_id'],
't' => $topicInfo->getId(),
'm' => 'delete',
],
]);
break;
} elseif(!$submissionConfirmed) {
url_redirect(
Tools::redirect($urls->format(
'forum-topic',
['topic' => $topic['topic_id']]
);
['topic' => $topicInfo->getId()]
));
break;
}
$deleteTopic = forum_topic_delete($topic['topic_id']);
$forumTopics->deleteTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->getId()]);
if($deleteTopic)
$msz->createAuditLog('FORUM_TOPIC_DELETE', [$topic['topic_id']]);
if(!$deleteTopic) {
echo render_error(500);
break;
}
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
Tools::redirect($urls->format('forum-category', [
'forum' => $categoryInfo->getId(),
]));
break;
case 'restore':
if(!$canNukeOrRestore) {
echo render_error(403);
break;
}
if(!$canNukeOrRestore)
Template::throwError(403);
if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic restore',
'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' => [
't' => $topic['topic_id'],
't' => $topicInfo->getId(),
'm' => 'restore',
],
]);
break;
} elseif(!$submissionConfirmed) {
url_redirect('forum-topic', [
'topic' => $topic['topic_id'],
]);
Tools::redirect($urls->format('forum-topic', [
'topic' => $topicInfo->getId(),
]));
break;
}
$restoreTopic = forum_topic_restore($topic['topic_id']);
$forumTopics->restoreTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->getId()]);
if(!$restoreTopic) {
echo render_error(500);
break;
}
$msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topic['topic_id']]);
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
Tools::redirect($urls->format('forum-category', [
'forum' => $categoryInfo->getId(),
]));
break;
case 'nuke':
if(!$canNukeOrRestore) {
echo render_error(403);
break;
}
if(!$canNukeOrRestore)
Template::throwError(403);
if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic nuke',
'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' => [
't' => $topic['topic_id'],
't' => $topicInfo->getId(),
'm' => 'nuke',
],
]);
break;
} elseif(!$submissionConfirmed) {
url_redirect('forum-topic', [
'topic' => $topic['topic_id'],
]);
Tools::redirect($urls->format('forum-topic', [
'topic' => $topicInfo->getId(),
]));
break;
}
$nukeTopic = forum_topic_nuke($topic['topic_id']);
$forumTopics->nukeTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->getId()]);
if(!$nukeTopic) {
echo render_error(500);
break;
}
$msz->createAuditLog('FORUM_TOPIC_NUKE', [$topic['topic_id']]);
url_redirect('forum-category', [
'forum' => $topic['forum_id'],
]);
Tools::redirect($urls->format('forum-category', [
'forum' => $categoryInfo->getId(),
]));
break;
case 'bump':
if($canBumpTopic && forum_topic_bump($topic['topic_id'])) {
$msz->createAuditLog('FORUM_TOPIC_BUMP', [$topic['topic_id']]);
if($canBumpTopic) {
$forumTopics->bumpTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->getId()]);
}
url_redirect('forum-topic', [
'topic' => $topic['topic_id'],
]);
Tools::redirect($urls->format('forum-topic', [
'topic' => $topicInfo->getId(),
]));
break;
case 'lock':
if($canLockTopic && !$topicIsLocked && forum_topic_lock($topic['topic_id'])) {
$msz->createAuditLog('FORUM_TOPIC_LOCK', [$topic['topic_id']]);
if($canLockTopic && !$topicIsLocked) {
$forumTopics->lockTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->getId()]);
}
url_redirect('forum-topic', [
'topic' => $topic['topic_id'],
]);
Tools::redirect($urls->format('forum-topic', [
'topic' => $topicInfo->getId(),
]));
break;
case 'unlock':
if($canLockTopic && $topicIsLocked && forum_topic_unlock($topic['topic_id'])) {
$msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topic['topic_id']]);
if($canLockTopic && $topicIsLocked) {
$forumTopics->unlockTopic($topicInfo->getId());
$msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->getId()]);
}
url_redirect('forum-topic', [
'topic' => $topic['topic_id'],
]);
Tools::redirect($urls->format('forum-topic', [
'topic' => $topicInfo->getId(),
]));
break;
}
return;
}
$topicPosts = $topic['topic_count_posts'];
$topicPosts = $topicInfo->getPostsCount();
if($canDeleteAny)
$topicPosts += $topicInfo->getDeletedPostsCount();
if($canDeleteAny) {
$topicPosts += $topic['topic_count_posts_deleted'];
}
$topicPagination = new Pagination($topicPosts, 10, 'page');
$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'])) {
$preceedingPosts = $postInfo['preceeding_post_count'];
if(!$topicPagination->hasValidOffset())
Template::throwError(404);
if($canDeleteAny) {
$preceedingPosts += $postInfo['preceeding_post_deleted_count'];
}
$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)
$postInfos = $forumPosts->getPosts(
topicInfo: $topicInfo,
deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false,
pagination: $topicPagination,
);
if(!$posts) {
echo render_error(404);
return;
if(empty($postInfos))
Template::throwError(404);
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', [
'topic_breadcrumbs' => forum_get_breadcrumbs($topic['forum_id']),
'global_accent_colour' => forum_get_colour($topic['forum_id']),
'topic_info' => $topic,
'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
'topic_info' => $topicInfo,
'category_info' => $categoryInfo,
'topic_posts' => $posts,
'can_reply' => $canReply,
'topic_pagination' => $topicPagination,
@ -329,4 +341,6 @@ Template::render('forum.topic', [
'topic_can_nuke_or_restore' => $canNukeOrRestore,
'topic_can_bump' => $canBumpTopic,
'topic_can_lock' => $canLockTopic,
'topic_user_id' => $currentUserId,
'topic_perms' => $perms,
]);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,12 +2,9 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
echo render_error(403);
return;
}
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
Template::throwError(403);
$emotes = $msz->getEmotes();
@ -15,10 +12,9 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
$emoteId = (string)filter_input(INPUT_GET, 'emote', FILTER_SANITIZE_NUMBER_INT);
try {
$emoteInfo = $emotes->getEmoteById($emoteId);
$emoteInfo = $emotes->getEmote($emoteId);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
if(!empty($_GET['delete'])) {
@ -27,7 +23,7 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
} else {
if(isset($_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);
$msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->getId()]);
}
@ -41,10 +37,10 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
}
}
url_redirect('manage-general-emoticons');
Tools::redirect($msz->getURLs()->format('manage-general-emoticons'));
return;
}
Template::render('manage.general.emoticons', [
'emotes' => $emotes->getAllEmotes(),
'emotes' => $emotes->getEmotes(),
]);

View file

@ -2,32 +2,33 @@
namespace Misuzu;
use Misuzu\Pagination;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_VIEW_LOGS)) {
echo render_error(403);
return;
}
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_LOGS_VIEW))
Template::throwError(403);
$usersCtx = $msz->getUsersContext();
$auditLog = $msz->getAuditLog();
$pagination = new Pagination($auditLog->countLogs(), 50);
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
if(!$pagination->hasValidOffset())
Template::throwError(404);
$logs = $auditLog->getLogs(pagination: $pagination);
$logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
$userInfos = [];
$userColours = [];
foreach($logs as $log)
if($log->hasUserId()) {
$userId = $log->getUserId();
if(!array_key_exists($userId, $userInfos))
$userInfos[$userId] = User::byId($userId);
if(!array_key_exists($userId, $userInfos)) {
$userInfos[$userId] = $usersCtx->getUserInfo($userId);
$userColours[$userId] = $usersCtx->getUserColour($userId);
}
}
Template::render('manage.general.logs', [
'global_logs' => $logs,
'global_logs_pagination' => $pagination,
'global_logs_users' => $userInfos,
'global_logs_users_colours' => $userColours,
]);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,14 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
echo render_error(403);
return;
}
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$news = $msz->getNews();
$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))
$isNew = true;
@ -20,16 +18,16 @@ else
$isNew = false;
$categoryInfo = $loadCategoryInfo();
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) {
$news->deleteCategory($categoryInfo);
$msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]);
url_redirect('manage-news-categories');
} else render_error(403);
if(!CSRF::validateRequest())
Template::throwError(403);
$news->deleteCategory($categoryInfo);
$msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]);
Tools::redirect($urls->format('manage-news-categories'));
return;
}
@ -58,7 +56,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
);
if($isNew) {
url_redirect('manage-news-category', ['category' => $categoryInfo->getId()]);
Tools::redirect($urls->format('manage-news-category', ['category' => $categoryInfo->getId()]));
return;
} else $categoryInfo = $loadCategoryInfo();
break;

View file

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

View file

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

View file

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

View file

@ -4,39 +4,38 @@ namespace Misuzu;
use DateTimeInterface;
use RuntimeException;
use Index\DateTime;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_BANS)) {
echo render_error(403);
return;
}
$authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
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(CSRF::validateRequest()) {
try {
$banInfo = $bans->getBan((string)filter_input(INPUT_GET, 'b'));
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
if(!CSRF::validateRequest())
Template::throwError(403);
$bans->deleteBans($banInfo);
$msz->createAuditLog('BAN_DELETE', [$banInfo->getId(), $banInfo->getUserId()]);
url_redirect('manage-users-bans', ['user' => $banInfo->getUserId()]);
} else render_error(403);
try {
$banInfo = $bans->getBan((string)filter_input(INPUT_GET, 'b'));
} catch(RuntimeException $ex) {
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;
}
try {
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
$userInfo = $usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
$modInfo = User::getCurrent();
$modInfo = $authInfo->getUserInfo();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);
@ -71,7 +70,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
);
$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;
}

View file

@ -2,57 +2,49 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_BANS)) {
echo render_error(403);
return;
}
if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_BANS_MANAGE))
Template::throwError(403);
$userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(),
];
$usersCtx = $msz->getUsersContext();
$bans = $usersCtx->getBans();
$filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
$filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try {
$filterUser = User::byId($filterUserId);
$userInfos[(string)$filterUser->getId()] = $filterUser;
$filterUser = $usersCtx->getUserInfo($filterUserId);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
}
$bans = $msz->getBans();
$pagination = new Pagination($bans->countBans(userInfo: $filterUser), 10);
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
if(!$pagination->hasValidOffset())
Template::throwError(404);
$banList = [];
$banInfos = $bans->getBans(userInfo: $filterUser, activeFirst: true, pagination: $pagination);
foreach($banInfos as $banInfo) {
if(array_key_exists($banInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$banInfo->getUserId()];
else
$userInfos[$banInfo->getUserId()] = $userInfo = User::byId((int)$banInfo->getUserId());
$userInfo = $usersCtx->getUserInfo($banInfo->getUserId());
$userColour = $usersCtx->getUserColour($userInfo);
if(!$banInfo->hasModId())
if(!$banInfo->hasModId()) {
$modInfo = null;
elseif(array_key_exists($banInfo->getModId(), $userInfos))
$modInfo = $userInfos[$banInfo->getModId()];
else
$userInfos[$banInfo->getModId()] = $modInfo = User::byId((int)$banInfo->getModId());
$modColour = null;
} else {
$modInfo = $usersCtx->getUserInfo($banInfo->getModId());
$modColour = $usersCtx->getUserColour($modInfo);
}
$banList[] = [
'info' => $banInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'mod' => $modInfo,
'mod_colour' => $modColour,
];
}

View file

@ -1,21 +1,38 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_USERS_MANAGE))
Template::throwError(403);
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
echo render_error(403);
return;
}
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$pagination = new Pagination($users->countUsers(), 30);
$pagination = new Pagination(User::countAll(true), 30);
if(!$pagination->hasValidOffset())
Template::throwError(404);
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
$userList = [];
$userInfos = $users->getUsers(pagination: $pagination, orderBy: 'id');
$roleInfos = [];
foreach($userInfos as $userInfo) {
$displayRoleId = $userInfo->getDisplayRoleId() ?? '1';
if(array_key_exists($displayRoleId, $roleInfos))
$roleInfo = $roleInfos[$displayRoleId];
else
$roleInfos[$displayRoleId] = $roleInfo = $roles->getRole($displayRoleId);
$colour = $userInfo->hasColour() ? $userInfo->getColour() : $roleInfo->getColour();
$userList[] = [
'info' => $userInfo,
'role' => $roleInfo,
'colour' => $colour,
];
}
Template::render('manage.users.users', [
'manage_users' => User::all(true, $pagination),
'manage_users' => $userList,
'manage_users_pagination' => $pagination,
]);

View file

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

View file

@ -2,57 +2,49 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) {
echo render_error(403);
return;
}
if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_NOTES_MANAGE))
Template::throwError(403);
$userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(),
];
$usersCtx = $msz->getUsersContext();
$modNotes = $usersCtx->getModNotes();
$filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
$filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try {
$filterUser = User::byId($filterUserId);
$userInfos[(string)$filterUser->getId()] = $filterUser;
$filterUser = $usersCtx->getUserInfo($filterUserId);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
}
$modNotes = $msz->getModNotes();
$pagination = new Pagination($modNotes->countNotes(userInfo: $filterUser), 10);
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
if(!$pagination->hasValidOffset())
Template::throwError(404);
$notes = [];
$noteInfos = $modNotes->getNotes(userInfo: $filterUser, pagination: $pagination);
foreach($noteInfos as $noteInfo) {
if(array_key_exists($noteInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$noteInfo->getUserId()];
else
$userInfos[$noteInfo->getUserId()] = $userInfo = User::byId((int)$noteInfo->getUserId());
$userInfo = $usersCtx->getUserInfo($noteInfo->getUserId());
$userColour = $usersCtx->getUserColour($userInfo);
if(!$noteInfo->hasAuthorId())
if(!$noteInfo->hasAuthorId()) {
$authorInfo = null;
elseif(array_key_exists($noteInfo->getAuthorId(), $userInfos))
$authorInfo = $userInfos[$noteInfo->getAuthorId()];
else
$userInfos[$noteInfo->getAuthorId()] = $authorInfo = User::byId((int)$noteInfo->getAuthorId());
$authorColour = null;
} else {
$authorInfo = $usersCtx->getUserInfo($noteInfo->getAuthorId());
$authorColour = $usersCtx->getUserColour($authorInfo);
}
$notes[] = [
'info' => $noteInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'author' => $authorInfo,
'author_colour' => $authorColour,
];
}

View file

@ -4,14 +4,18 @@ namespace Misuzu;
use RuntimeException;
use Index\Colour\Colour;
use Index\Colour\ColourRGB;
use Misuzu\Users\User;
use Misuzu\Perm;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
echo render_error(403);
return;
}
$authInfo = $msz->getAuthInfo();
$viewerPerms = $authInfo->getPerms('user');
if(!$viewerPerms->check(Perm::U_ROLES_MANAGE))
Template::throwError(403);
$roles = $msz->getRoles();
$roleInfo = null;
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();
$perms = $msz->getPerms();
if(filter_has_var(INPUT_GET, 'r')) {
$roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
@ -20,20 +24,20 @@ if(filter_has_var(INPUT_GET, 'r')) {
$isNew = false;
$roleInfo = $roles->getRole($roleId);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
Template::throwError(404);
}
} else $isNew = true;
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$currentUser = $authInfo->getUserInfo();
$canEditPerms = $viewerPerms->check(Perm::U_PERMS_MANAGE);
if($canEditPerms)
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
$permsInfos = $roleInfo === null ? null : $perms->getPermissionInfo(roleInfo: $roleInfo, categoryNames: Perm::INFO_FOR_ROLE);
$permsLists = Perm::createList(Perm::LISTS_FOR_ROLE);
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!$isNew && !$currentUser->isSuper() && $roleInfo->getRank() >= $currentUser->getRank()) {
$userRank = $users->getUserRank($currentUser);
if(!$isNew && !$currentUser->isSuperUser() && $roleInfo->getRank() >= $userRank) {
echo 'You aren\'t allowed to edit this role.';
break;
}
@ -62,7 +66,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
'role_ur_col_blue' => $colourBlue,
]);
if(!$currentUser->isSuper() && $roleRank >= $currentUser->getRank()) {
if(!$currentUser->isSuperUser() && $roleRank >= $userRank) {
echo 'You aren\'t allowed to make a role with equal rank to your own.';
break;
}
@ -119,36 +123,27 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
[$roleInfo->getId()]
);
if(!empty($permissions) && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']);
if($canEditPerms && filter_has_var(INPUT_POST, 'perms')) {
$permsApply = Perm::convertSubmission(
filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY),
Perm::INFO_FOR_ROLE
);
if($perms !== null) {
$permKeys = array_keys($perms);
$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($permsApply as $categoryName => $values)
$perms->setPermissions($categoryName, $values['allow'], $values['deny'], roleInfo: $roleInfo);
foreach($perms as $key => $value) {
$setPermissions->bind($key, $value);
}
$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();
}
// could target all users with the role but ech
$msz->getConfig()->setBoolean('perms.needsRecalc', true);
}
url_redirect('manage-role', ['role' => $roleInfo->getId()]);
Tools::redirect($msz->getURLs()->format('manage-role', ['role' => $roleInfo->getId()]));
return;
}
Template::render('manage.users.role', [
'role_new' => $isNew,
'role_info' => $roleInfo ?? null,
'can_manage_perms' => $canEditPerms,
'permissions' => $permissions ?? [],
'can_edit_perms' => $canEditPerms,
'perms_lists' => $permsLists,
'perms_infos' => $permsInfos,
]);

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