Compare commits
190 commits
20230721.3
...
master
Author | SHA1 | Date | |
---|---|---|---|
39be84fcc0 | |||
242e70eabf | |||
174ceaa4e7 | |||
058b409adf | |||
8e006c7003 | |||
23d47fa6d2 | |||
bdad34e065 | |||
fc6a899f16 | |||
1f16de2239 | |||
1550a5da57 | |||
7ef1974c88 | |||
0f45a5f60f | |||
324fe21d73 | |||
153abde3a2 | |||
f8aaa71260 | |||
37a3bc1ee6 | |||
f547812d5a | |||
8a06836985 | |||
34528ae413 | |||
0bf7ca0d52 | |||
cc9fccdf18 | |||
ca77b501e7 | |||
2439f87df9 | |||
400253e04b | |||
01c60e3027 | |||
37d8413118 | |||
8cfa07bc8c | |||
a65579bf9d | |||
44a4bb6e6f | |||
ec00cfa176 | |||
1d295df8da | |||
6a88ed8b11 | |||
36bcf1ab1d | |||
5d3e1d4960 | |||
9bb943bacf | |||
107d16cf46 | |||
0afc5186a7 | |||
0300bae994 | |||
cb0c64f8ed | |||
89ef9d9ad1 | |||
c02d922dc6 | |||
80cd6222c4 | |||
344a3c9160 | |||
df5dbdf3ad | |||
c0caceed7b | |||
be54ce2c22 | |||
070dc5e782 | |||
b89621cb1a | |||
760cca0e5d | |||
fe77f1616c | |||
eb81ed7a82 | |||
8ef11afe02 | |||
cca016ba10 | |||
b80151583e | |||
d8cc208a85 | |||
4b2f9a2fec | |||
ddb255bf32 | |||
5a70e3f3f1 | |||
bd3e055323 | |||
dba5754ccc | |||
ec6ba3f781 | |||
70ec285f99 | |||
77eadd5bde | |||
f0fc735975 | |||
adb80bad9e | |||
f30cf41f86 | |||
b4f5dd0660 | |||
133e2f420c | |||
bf65c95490 | |||
7ef5994da4 | |||
2b34bde413 | |||
432615508d | |||
a4cc14e4c1 | |||
65e695e9d9 | |||
2e6a84b46d | |||
8f56174637 | |||
19fbe59ddd | |||
f7a571e551 | |||
5f57e3fdf4 | |||
c2836719c7 | |||
14c9a1d9f6 | |||
4f1e35b566 | |||
9aa2a1431e | |||
4322f2561c | |||
67d9620037 | |||
904d220582 | |||
d9b152fb78 | |||
a945cc518a | |||
edc64b45ff | |||
17e0d1f591 | |||
5554c5c28d | |||
55e23c7b5d | |||
e376671136 | |||
3e49f6e503 | |||
7db43a2acd | |||
099bd899ed | |||
1248c0d2f6 | |||
c3bed1c0e3 | |||
163da8b213 | |||
c68279add9 | |||
737c99280e | |||
8b0f960c86 | |||
c5a284f360 | |||
506d32d210 | |||
498ec0cf9a | |||
15e96684c2 | |||
73e4597e16 | |||
9b2c409a24 | |||
7190a5f4df | |||
5c67d49225 | |||
69e4d05be6 | |||
2d0f083e1a | |||
1da6470928 | |||
9682fa595a | |||
c14195c4c3 | |||
45500ce698 | |||
0c9bac473b | |||
061d4c8a8f | |||
6fc10984e1 | |||
e222009dd0 | |||
85b629bc08 | |||
16ea495c7a | |||
ad3fe74275 | |||
29426fafc1 | |||
4d6fb64f3a | |||
40558ceb39 | |||
f03c8ebfa5 | |||
07a2868159 | |||
ca23822e40 | |||
34bd71600a | |||
5bab957a7c | |||
57b9e82c10 | |||
460a0ca57d | |||
39c6269cf3 | |||
fb41c71ee9 | |||
2214dffc5b | |||
bab8b29c5b | |||
0a11c5525a | |||
d4f6990e8a | |||
87915b6a25 | |||
cf71129153 | |||
6bfa3d7238 | |||
b7de5acfd8 | |||
9dd7156c79 | |||
00d1d2922d | |||
383e2ed0e0 | |||
57081d858d | |||
e813f2a90e | |||
0158333c90 | |||
a89d8d26f4 | |||
e3c0ae662e | |||
61daa21d3a | |||
934b016541 | |||
8ef113f3a9 | |||
a22433f7dd | |||
c5ec94289d | |||
8c52fc81e2 | |||
d2f0eebfb2 | |||
3148da4403 | |||
5c8ffa09fc | |||
20b309563e | |||
461ffbf73b | |||
26a0e11253 | |||
70623d3a7c | |||
b4d4e8578c | |||
8480d5f043 | |||
a30df1b17c | |||
351043e283 | |||
2231cd8124 | |||
86432616c6 | |||
1d552e907b | |||
057551edb3 | |||
710049794f | |||
ca1edb4270 | |||
f4f465d8d8 | |||
81f4dfce19 | |||
bd683d8404 | |||
3299d73df2 | |||
ee304af133 | |||
3d67b59238 | |||
dd21fce6e3 | |||
f6058823f1 | |||
392881c0d8 | |||
6e3023a772 | |||
d0e3f6ce65 | |||
42d893fc18 | |||
baefea88df | |||
e369038609 | |||
9962bbc5df | |||
761bc94b8e |
362 changed files with 24381 additions and 14878 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -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
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -154,6 +154,9 @@
|
|||
}
|
||||
|
||||
.forum__post__action {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
margin: 1px;
|
||||
color: inherit;
|
||||
|
|
|
@ -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;
|
||||
|
|
92
assets/misuzu.css/hljs.css
Normal file
92
assets/misuzu.css/hljs.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
@ -160,21 +163,8 @@ html {
|
|||
@include home/landingv2.css;
|
||||
|
||||
@include manage/_manage.css;
|
||||
@include manage/blacklist.css;
|
||||
@include manage/changelog-actions-tags.css;
|
||||
@include manage/emote.css;
|
||||
@include manage/emotes.css;
|
||||
@include manage/navigation.css;
|
||||
@include manage/role-item.css;
|
||||
@include manage/roles.css;
|
||||
@include manage/settings.css;
|
||||
@include manage/statistic.css;
|
||||
@include manage/statistics.css;
|
||||
@include manage/tag.css;
|
||||
@include manage/tags.css;
|
||||
@include manage/user-item.css;
|
||||
@include manage/user.css;
|
||||
@include manage/users.css;
|
||||
|
||||
@include messages/messages.css;
|
||||
|
||||
@include news/container.css;
|
||||
@include news/feeds.css;
|
||||
|
@ -191,7 +181,7 @@ html {
|
|||
@include profile/header.css;
|
||||
@include profile/profile.css;
|
||||
@include profile/signature.css;
|
||||
@include profile/warning.css;
|
||||
@include profile/warnings.css;
|
||||
|
||||
@include search/anchor.css;
|
||||
@include search/categories.css;
|
||||
|
|
|
@ -23,3 +23,25 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include manage/ban.css;
|
||||
@include manage/bans.css;
|
||||
@include manage/blacklist.css;
|
||||
@include manage/changelog-actions-tags.css;
|
||||
@include manage/emote.css;
|
||||
@include manage/emotes.css;
|
||||
@include manage/navigation.css;
|
||||
@include manage/note.css;
|
||||
@include manage/notes.css;
|
||||
@include manage/role-item.css;
|
||||
@include manage/roles.css;
|
||||
@include manage/settings.css;
|
||||
@include manage/statistic.css;
|
||||
@include manage/statistics.css;
|
||||
@include manage/tag.css;
|
||||
@include manage/tags.css;
|
||||
@include manage/user-item.css;
|
||||
@include manage/user.css;
|
||||
@include manage/users.css;
|
||||
@include manage/warning.css;
|
||||
@include manage/warnings.css;
|
||||
|
|
73
assets/misuzu.css/manage/ban.css
Normal file
73
assets/misuzu.css/manage/ban.css
Normal file
|
@ -0,0 +1,73 @@
|
|||
.manage__ban__field {
|
||||
margin: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.manage__ban__title {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.manage__ban__desc {
|
||||
font-size: .9em;
|
||||
line-height: 1.5em;
|
||||
font-style: italic;
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
padding: 2px 4px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.manage__ban__duration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
.manage__ban__duration__value__custom--hidden {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.manage__ban__severity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
.manage__ban__severity__slider {
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
.manage__ban__severity__slider input {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.manage__ban__severity__display {
|
||||
max-width: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
.manage__ban__severity__display input {
|
||||
width: 100%;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.manage__ban__reason {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
.manage__ban__reason textarea {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.manage__ban__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
}
|
122
assets/misuzu.css/manage/bans.css
Normal file
122
assets/misuzu.css/manage/bans.css
Normal file
|
@ -0,0 +1,122 @@
|
|||
.manage__bans__pagination {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__bans__actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__bans__item {
|
||||
padding: 0 2px;
|
||||
margin: 2px;
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
}
|
||||
.manage__bans__item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
}
|
||||
|
||||
.manage__bans__item__header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manage__bans__item__attributes {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 0 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manage__bans__item__attribute {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.manage__bans__item__created__icon,
|
||||
.manage__bans__item__expires__icon,
|
||||
.manage__bans__item__permanent__icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.manage__bans__item__expires__status span {
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.manage__bans__item__expires__status--active span {
|
||||
background: rgba(255, 100, 100, 0.2);
|
||||
font-weight: 700;
|
||||
}
|
||||
.manage__bans__item__expires__status--expired span {
|
||||
background: rgba(100, 255, 100, 0.2);
|
||||
}
|
||||
.manage__bans__item__permanent {
|
||||
background: rgba(255, 200, 100, 0.2);
|
||||
border-radius: 2px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.manage__bans__item__permanent__time {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.manage__bans__item__actions {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
.manage__bans__item__action {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.manage__bans__item__author a,
|
||||
.manage__bans__item__user a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.manage__bans__item__author__name a,
|
||||
.manage__bans__item__user__name a {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
border-bottom: 2px solid var(--user-colour, #fff);
|
||||
}
|
||||
.manage__bans__item__user__filter a {
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: background .2s;
|
||||
}
|
||||
.manage__bans__item__user__filter a:hover,
|
||||
.manage__bans__item__user__filter a:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.manage__bans__item__user__filter a:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.manage__bans__item__reason {
|
||||
margin: 1px 4px;
|
||||
padding: 2px 4px;
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
}
|
||||
.manage__bans__item__reason__title {
|
||||
font-size: .9em;
|
||||
line-height: 1.5em;
|
||||
font-style: italic;
|
||||
}
|
||||
.manage__bans__item__reason__body {
|
||||
padding-left: 4px;
|
||||
border-left: 2px solid var(--accent-colour);
|
||||
}
|
||||
|
||||
.manage__bans__item__noreason {
|
||||
font-size: .9em;
|
||||
font-style: italic;
|
||||
}
|
88
assets/misuzu.css/manage/note.css
Normal file
88
assets/misuzu.css/manage/note.css
Normal file
|
@ -0,0 +1,88 @@
|
|||
.manage__note {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__note--view .manage__note--editing,
|
||||
.manage__note--edit .manage__note--viewing {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.manage__note__header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manage__note__title {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.manage__note__title__text {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.manage__note__title input {
|
||||
width: 100%;
|
||||
}
|
||||
.manage__note__actions {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
.manage__note__action {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.manage__note__attributes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.manage__note__attribute {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.manage__note__created__icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.manage__note__author a,
|
||||
.manage__note__user a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.manage__note__author__name a,
|
||||
.manage__note__user__name a {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
border-bottom: 2px solid var(--user-colour, #fff);
|
||||
}
|
||||
|
||||
.manage__note__body {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__note__nobody {
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.manage__note__editor {
|
||||
width: 100%;
|
||||
}
|
||||
.manage__note__editor textarea {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 300px;
|
||||
}
|
122
assets/misuzu.css/manage/notes.css
Normal file
122
assets/misuzu.css/manage/notes.css
Normal file
|
@ -0,0 +1,122 @@
|
|||
.manage__notes__pagination {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__notes__actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__notes__item {
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
.manage__notes__item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
}
|
||||
|
||||
.manage__notes__item__header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
.manage__notes__item__title {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.3em;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.manage__notes__item__title a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.manage__notes__item__title a:hover,
|
||||
.manage__notes__item__title a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.manage__notes__item__actions {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
.manage__notes__item__action {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.manage__notes__item__attributes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.manage__notes__item__attribute {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.manage__notes__item__created__icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.manage__notes__item__author a,
|
||||
.manage__notes__item__user a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.manage__notes__item__author__name a,
|
||||
.manage__notes__item__user__name a {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
border-bottom: 2px solid var(--user-colour, #fff);
|
||||
}
|
||||
.manage__notes__item__user__filter a {
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: background .2s;
|
||||
}
|
||||
.manage__notes__item__user__filter a:hover,
|
||||
.manage__notes__item__user__filter a:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.manage__notes__item__user__filter a:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.manage__notes__item__body {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__notes__item__nobody {
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.manage__notes__item__continue {
|
||||
text-align: center;
|
||||
}
|
||||
.manage__notes__item__continue a {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: background .2s;
|
||||
}
|
||||
.manage__notes__item__continue a:hover,
|
||||
.manage__notes__item__continue a:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.manage__notes__item__continue a:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
|
@ -9,6 +9,12 @@
|
|||
}
|
||||
.manage__statistic__value {
|
||||
text-align: right;
|
||||
font-size: 1.5em;
|
||||
line-height: 2em;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.manage__statistic__updated {
|
||||
text-align: right;
|
||||
font-size: .9em;
|
||||
font-style: italic;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
.manage__statistics {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
padding: 5px;
|
||||
grid-gap: 5px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.manage__statistics {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.manage__statistics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
37
assets/misuzu.css/manage/warning.css
Normal file
37
assets/misuzu.css/manage/warning.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
.manage__warning__field {
|
||||
margin: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.manage__warning__title {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.manage__warning__desc {
|
||||
font-size: .9em;
|
||||
line-height: 1.5em;
|
||||
font-style: italic;
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
padding: 2px 4px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.manage__warning__body {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
.manage__warning__body textarea {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.manage__warning__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
}
|
91
assets/misuzu.css/manage/warnings.css
Normal file
91
assets/misuzu.css/manage/warnings.css
Normal file
|
@ -0,0 +1,91 @@
|
|||
.manage__warnings__pagination {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__warnings__actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.manage__warnings__item {
|
||||
padding: 0 2px;
|
||||
margin: 2px;
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
}
|
||||
.manage__warnings__item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
}
|
||||
|
||||
.manage__warnings__item__header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manage__warnings__item__attributes {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 0 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manage__warnings__item__attribute {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.manage__warnings__item__created__icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.manage__warnings__item__actions {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
.manage__warnings__item__action {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.manage__warnings__item__author a,
|
||||
.manage__warnings__item__user a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.manage__warnings__item__author__name a,
|
||||
.manage__warnings__item__user__name a {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
border-bottom: 2px solid var(--user-colour, #fff);
|
||||
}
|
||||
.manage__warnings__item__user__filter a {
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: background .2s;
|
||||
}
|
||||
.manage__warnings__item__user__filter a:hover,
|
||||
.manage__warnings__item__user__filter a:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.manage__warnings__item__user__filter a:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.manage__warnings__item__reason {
|
||||
margin: 1px 4px;
|
||||
padding: 2px 4px;
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
}
|
||||
.manage__warnings__item__reason p {
|
||||
padding-left: 4px;
|
||||
border-left: 2px solid var(--accent-colour);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,4 +17,5 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
|
37
assets/misuzu.css/messages/actions.css
Normal file
37
assets/misuzu.css/messages/actions.css
Normal 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;
|
||||
}
|
26
assets/misuzu.css/messages/columns.css
Normal file
26
assets/misuzu.css/messages/columns.css
Normal 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%;
|
||||
}
|
||||
}
|
80
assets/misuzu.css/messages/entry.css
Normal file
80
assets/misuzu.css/messages/entry.css
Normal 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;
|
||||
}
|
33
assets/misuzu.css/messages/folder.css
Normal file
33
assets/misuzu.css/messages/folder.css
Normal 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;
|
||||
}
|
135
assets/misuzu.css/messages/message.css
Normal file
135
assets/misuzu.css/messages/message.css
Normal 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;
|
||||
}
|
9
assets/misuzu.css/messages/messages.css
Normal file
9
assets/misuzu.css/messages/messages.css
Normal 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;
|
17
assets/misuzu.css/messages/recipient.css
Normal file
17
assets/misuzu.css/messages/recipient.css
Normal 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%;
|
||||
}
|
52
assets/misuzu.css/messages/reply.css
Normal file
52
assets/misuzu.css/messages/reply.css
Normal 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;
|
||||
}
|
11
assets/misuzu.css/messages/sidebar.css
Normal file
11
assets/misuzu.css/messages/sidebar.css
Normal 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;
|
||||
}
|
5
assets/misuzu.css/messages/thread.css
Normal file
5
assets/misuzu.css/messages/thread.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.messages-thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
.news__feeds {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,6 @@
|
|||
.news__preview {
|
||||
display: flex;
|
||||
margin: 2px 0;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
--user-colour: var(--accent-colour);
|
||||
}
|
||||
.news__preview__info__content {
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.news__preview__info__background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-image: linear-gradient(90deg, transparent 10%, var(--background-colour) 100%);
|
||||
-webkit-mask-image: linear-gradient(90deg, transparent 10%, var(--background-colour) 100%);
|
||||
background: var(--background-pattern);
|
||||
background-color: var(--user-colour);
|
||||
background-blend-mode: multiply;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.news__preview__listing {
|
||||
|
@ -33,68 +8,63 @@
|
|||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.news__preview__container {
|
||||
.news__preview__header {
|
||||
border-bottom: 1px solid var(--accent-colour);
|
||||
display: flex;
|
||||
margin: 1px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news__preview__user {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.news__preview__user__details {
|
||||
|
||||
.news__preview__title {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.news__preview__title h1 {
|
||||
font-size: 2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.news__preview__attrs {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.news__preview__avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 10px;
|
||||
.news__preview__attr {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.news__preview__username {
|
||||
color: inherit;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.news__preview__username[href]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.news__preview__date {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.news__preview__category {
|
||||
.news__preview__author a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5em;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.news__preview__category:hover {
|
||||
.news__preview__author__name a {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
border-bottom: 2px solid var(--user-colour, #fff);
|
||||
}
|
||||
|
||||
.news__preview__category a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.news__preview__category a:hover,
|
||||
.news__preview__category a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.news__preview__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.2em;
|
||||
flex: 1 1 auto;
|
||||
line-height: 1.4em;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
margin: 2px;
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.news__preview__text {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.news__preview__links {
|
||||
|
@ -105,25 +75,3 @@
|
|||
.news__preview__link {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.news__preview { flex-direction: column-reverse; }
|
||||
.news__preview__info { display: none; }
|
||||
.news__preview__info__content {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
text-align: left;
|
||||
}
|
||||
.news__preview__info__background {
|
||||
mask-image: linear-gradient(180deg, transparent 10%, var(--background-colour) 100%);
|
||||
-webkit-mask-image: linear-gradient(180deg, transparent 10%, var(--background-colour) 100%);
|
||||
}
|
||||
.news__preview__user {
|
||||
margin-bottom: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.news__preview__avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
.profile__warning {
|
||||
margin: 2px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--accent-colour);
|
||||
}
|
||||
.profile__warning__container {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.profile__warning--warning {
|
||||
--accent-colour: #666;
|
||||
}
|
||||
|
||||
.profile__warning--silence {
|
||||
--accent-colour: #f70;
|
||||
}
|
||||
|
||||
.profile__warning--ban {
|
||||
--accent-colour: #c33;
|
||||
}
|
||||
|
||||
.profile__warning--extendo {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.profile__warning__background {
|
||||
background-color: var(--accent-colour);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.profile__warning__content {
|
||||
background-color: var(--background-colour-translucent-9);
|
||||
display: flex;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.profile__warning__type,
|
||||
.profile__warning__created,
|
||||
.profile__warning__duration {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile__warning__type {
|
||||
min-width: 80px;
|
||||
background-color: var(--accent-colour);
|
||||
border-radius: 1px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.profile__warning__created,
|
||||
.profile__warning__duration {
|
||||
min-width: 100px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.profile__warning__note {
|
||||
padding: 1px 4px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.profile__warning__private {
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
margin-top: 1px;
|
||||
width: 100%;
|
||||
opacity: .5;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.profile__warning__private:hover,
|
||||
.profile__warning__private:active,
|
||||
.profile__warning__private:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile__warning__tools {
|
||||
display: flex;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.profile__warning__options {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile__warning__option {
|
||||
padding: 2px 5px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.profile__warning__user {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.profile__warning__user__avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.profile__warning__user__username {
|
||||
padding: 0 5px;
|
||||
min-width: 60px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.profile__warning__user__username:hover,
|
||||
.profile__warning__user__username:focus,
|
||||
.profile__warning__user__username:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile__warning__user__ip {
|
||||
display: inline-flex;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.profile__warning__user__ip:before { content: "("; }
|
||||
.profile__warning__user__ip:after { content: ")"; }
|
||||
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.profile__warning__content {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.profile__warning__tools {
|
||||
flex-direction: column;
|
||||
}
|
||||
.profile__warning__options {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
26
assets/misuzu.css/profile/warnings.css
Normal file
26
assets/misuzu.css/profile/warnings.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
.profile__warnings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.profile__warnings__item {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.profile__warnings__item:not(:last-child) {
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
.profile__warnings__datetime {
|
||||
font-size: .9em;
|
||||
line-height: 1.5em;
|
||||
font-style: italic;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.profile__warnings__body {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.profile__warnings__body p {
|
||||
line-height: 1.4em;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
.settings__account-logs__pagination {
|
||||
margin: 4px;
|
||||
}
|
||||
.settings__account-logs__none {
|
||||
padding: 2px 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings__account-log {
|
||||
border: 1px solid var(--accent-colour);
|
||||
|
|
|
@ -9,10 +9,21 @@
|
|||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.warning--red {
|
||||
--start-colour: #ff3d3d;
|
||||
--end-colour: #f00;
|
||||
}
|
||||
.warning--bigger {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.warning__content {
|
||||
background-color: rgba(17, 17, 17, .9);
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.warning--bigger .warning__content {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
.warning__link {
|
||||
color: inherit;
|
||||
text-decoration: underline dotted;
|
||||
|
|
40
assets/misuzu.js/csrfp.js
Normal file
40
assets/misuzu.js/csrfp.js
Normal 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'));
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -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); });
|
||||
|
135
assets/misuzu.js/embed/embed.js
Normal file
135
assets/misuzu.js/embed/embed.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
#include utils.js
|
||||
#include utility.js
|
||||
|
||||
const MszImageEmbed = function(metadata, options, target) {
|
||||
options = options || {};
|
|
@ -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 = {
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
37
assets/misuzu.js/ext/eeprom.js
Normal file
37
assets/misuzu.js/ext/eeprom.js
Normal 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);
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
const Sakuya = (function() {
|
||||
const MszSakuya = (function() {
|
||||
const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
||||
const divisions = [
|
||||
{ amount: 60, name: 'seconds' },
|
15
assets/misuzu.js/ext/uiharu.js
Normal file
15
assets/misuzu.js/ext/uiharu.js
Normal 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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
291
assets/misuzu.js/forum/editor.jsx
Normal file
291
assets/misuzu.js/forum/editor.jsx
Normal 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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
Misuzu.Forum = {};
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
89
assets/misuzu.js/messages/actbtn.js
Normal file
89
assets/misuzu.js/messages/actbtn.js
Normal 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;
|
||||
};
|
167
assets/misuzu.js/messages/list.js
Normal file
167
assets/misuzu.js/messages/list.js
Normal 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());
|
||||
},
|
||||
};
|
||||
};
|
386
assets/misuzu.js/messages/messages.js
Normal file
386
assets/misuzu.js/messages/messages.js
Normal file
|
@ -0,0 +1,386 @@
|
|||
#include csrfp.js
|
||||
#include msgbox.jsx
|
||||
#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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
56
assets/misuzu.js/messages/recipient.js
Normal file
56
assets/misuzu.js/messages/recipient.js
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
171
assets/misuzu.js/messages/reply.jsx
Normal file
171
assets/misuzu.js/messages/reply.jsx
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
78
assets/misuzu.js/messages/thread.js
Normal file
78
assets/misuzu.js/messages/thread.js
Normal 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,
|
||||
};
|
||||
};
|
73
assets/misuzu.js/msgbox.jsx
Normal file
73
assets/misuzu.js/msgbox.jsx
Normal 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();
|
||||
});
|
||||
};
|
56
assets/misuzu.js/parsing.js
Normal file
56
assets/misuzu.js/parsing.js
Normal 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),
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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();
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
105
build.js
105
build.js
|
@ -1,89 +1,30 @@
|
|||
const assproc = require('@railcomm/assproc');
|
||||
const { join: pathJoin } = require('path');
|
||||
const fs = require('fs');
|
||||
const swc = require('@swc/core');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const postcss = require('postcss');
|
||||
const utils = require('./assets/utils.js');
|
||||
const assproc = require('./assets/assproc.js');
|
||||
|
||||
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 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 isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
|
||||
|
||||
const swcJscOptions = {
|
||||
target: 'es2016',
|
||||
loose: false,
|
||||
externalHelpers: false,
|
||||
keepClassNames: true,
|
||||
preserveAllComments: false,
|
||||
transform: {},
|
||||
parser: {
|
||||
syntax: 'ecmascript',
|
||||
jsx: true,
|
||||
dynamicImport: false,
|
||||
privateMethod: false,
|
||||
functionBind: false,
|
||||
exportDefaultFrom: false,
|
||||
exportNamespaceFrom: false,
|
||||
decorators: false,
|
||||
decoratorsBeforeExport: false,
|
||||
topLevelAwait: true,
|
||||
importMeta: false,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'classic',
|
||||
pragma: '$er',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const postcssPlugins = [];
|
||||
if(!isDebugBuild) postcssPlugins.push(require('cssnano'));
|
||||
postcssPlugins.push(require('autoprefixer')({
|
||||
remove: false,
|
||||
}));
|
||||
|
||||
fs.mkdirSync(pubAssetsFull, { recursive: true });
|
||||
|
||||
(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 isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
|
||||
|
||||
const mszJsName = await assproc.process(assetsJS, { 'prefix': '#', 'entry': 'main.js' })
|
||||
.then(output => swc.transform(output, {
|
||||
filename: 'misuzu.js',
|
||||
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;
|
||||
}));
|
||||
const env = {
|
||||
root: __dirname,
|
||||
source: pathJoin(__dirname, 'assets'),
|
||||
public: pathJoin(__dirname, 'public'),
|
||||
debug: isDebug,
|
||||
swc: {
|
||||
es: 'es2021',
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(assetsInfo, JSON.stringify({
|
||||
mszjs: mszJsName,
|
||||
mszcss: mszCssName,
|
||||
}));
|
||||
const tasks = {
|
||||
js: [
|
||||
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
|
||||
],
|
||||
css: [
|
||||
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
|
||||
],
|
||||
};
|
||||
|
||||
assproc.housekeep(pubAssetsFull);
|
||||
const files = await assproc.process(env, tasks);
|
||||
|
||||
fs.writeFileSync(pathJoin(__dirname, 'assets/current.json'), JSON.stringify(files));
|
||||
})();
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
{
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"flashwave/index": "dev-master",
|
||||
"twig/twig": "^3.0",
|
||||
"flashwave/index": "^0.2410",
|
||||
"flashii/rpcii": "^2.0",
|
||||
"erusev/parsedown": "~1.6",
|
||||
"chillerlan/php-qrcode": "^4.3",
|
||||
"symfony/mailer": "^6.0",
|
||||
"matomo/device-detector": "^6.1",
|
||||
"wikimedia/composer-merge-plugin": "^2.1"
|
||||
"sentry/sdk": "^4.0",
|
||||
"nesbot/carbon": "^3.7"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
@ -28,16 +27,11 @@
|
|||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"composer/installers": true,
|
||||
"wikimedia/composer-merge-plugin": true
|
||||
"wikimedia/composer-merge-plugin": false,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"merge-plugin": {
|
||||
"include": [
|
||||
"composer.local.json"
|
||||
],
|
||||
"recurse": true,
|
||||
"replace": true
|
||||
}
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.11"
|
||||
}
|
||||
}
|
||||
|
|
1767
composer.lock
generated
1767
composer.lock
generated
File diff suppressed because it is too large
Load diff
8
config/config.example.cfg
Normal file
8
config/config.example.cfg
Normal 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
|
|
@ -1,10 +0,0 @@
|
|||
; Example configuration for Misuzu
|
||||
|
||||
[Database]
|
||||
driver = mysql
|
||||
host = localhost
|
||||
port = 3306
|
||||
username = username
|
||||
password = password
|
||||
dbname = database
|
||||
charset = utf8mb4
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\Migration\IDbMigration;
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
// Switching to the Index migration system!!!!!!
|
||||
|
||||
final class InitialStructureNdx_20230107_023235 implements IDbMigration {
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
final class InitialStructureNdx_20230107_023235 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$hasMszTrack = false;
|
||||
|
||||
// check if the old migrations table exists
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\Migration\IDbMigration;
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class CreateTopicRedirsTable_20230430_001226 implements IDbMigration {
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
final class CreateTopicRedirsTable_20230430_001226 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('
|
||||
CREATE TABLE msz_forum_topics_redirects (
|
||||
topic_id INT(10) UNSIGNED NOT NULL,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\Migration\IDbMigration;
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
use Misuzu\ClientInfo;
|
||||
|
||||
final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
final class UpdateUserAgentStorage_20230721_121854 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
// convert user agent fields to BLOB and add field for client info storage
|
||||
$conn->execute('
|
||||
ALTER TABLE msz_login_attempts
|
||||
|
@ -23,7 +23,7 @@ final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
|
|||
while($selectLoginAttempts->next()) {
|
||||
$updateLoginAttempts->reset();
|
||||
$userAgent = $selectLoginAttempts->getString(0);
|
||||
$updateLoginAttempts->addParameter(1, ClientInfo::parse($userAgent)->encode());
|
||||
$updateLoginAttempts->addParameter(1, json_encode(ClientInfo::parse($userAgent)));
|
||||
$updateLoginAttempts->addParameter(2, $userAgent);
|
||||
$updateLoginAttempts->execute();
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
|
|||
while($selectSessions->next()) {
|
||||
$updateSessions->reset();
|
||||
$userAgent = $selectSessions->getString(0);
|
||||
$updateSessions->addParameter(1, ClientInfo::parse($userAgent)->encode());
|
||||
$updateSessions->addParameter(1, json_encode(ClientInfo::parse($userAgent)));
|
||||
$updateSessions->addParameter(2, $userAgent);
|
||||
$updateSessions->execute();
|
||||
}
|
||||
|
|
46
database/2023_07_24_201010_add_moderator_notes_table.php
Normal file
46
database/2023_07_24_201010_add_moderator_notes_table.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class AddModeratorNotesTable_20230724_201010 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('
|
||||
CREATE TABLE msz_users_modnotes (
|
||||
note_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id INT(10) UNSIGNED NOT NULL,
|
||||
author_id INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
note_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
note_title VARCHAR(255) NOT NULL,
|
||||
note_body TEXT NOT NULL,
|
||||
PRIMARY KEY (note_id),
|
||||
KEY users_modnotes_user_foreign (user_id),
|
||||
KEY users_modnotes_author_foreign (author_id),
|
||||
KEY users_modnotes_created_index (note_created),
|
||||
CONSTRAINT users_modnotes_user_foreign
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT users_modnotes_author_foreign
|
||||
FOREIGN KEY (author_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB COLLATE=utf8mb4_bin
|
||||
');
|
||||
|
||||
// migrate existing notes
|
||||
$conn->execute('
|
||||
INSERT INTO msz_users_modnotes (user_id, author_id, note_created, note_title, note_body)
|
||||
SELECT user_id, issuer_id, warning_created, warning_note, COALESCE(warning_note_private, "")
|
||||
FROM msz_user_warnings
|
||||
WHERE warning_type = 0
|
||||
');
|
||||
|
||||
// delete notes from the warnings table
|
||||
$conn->execute('DELETE FROM msz_user_warnings WHERE warning_type = 0');
|
||||
|
||||
// for good measure update silences to bans since i forgot to do that as a migration
|
||||
$conn->execute('UPDATE msz_user_warnings SET warning_type = 3 WHERE warning_type = 2');
|
||||
}
|
||||
}
|
45
database/2023_07_26_175936_add_new_bans_table.php
Normal file
45
database/2023_07_26_175936_add_new_bans_table.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class AddNewBansTable_20230726_175936 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('
|
||||
CREATE TABLE msz_users_bans (
|
||||
ban_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id INT(10) UNSIGNED NOT NULL,
|
||||
mod_id INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
ban_severity TINYINT(4) NOT NULL,
|
||||
ban_reason_public TEXT NOT NULL,
|
||||
ban_reason_private TEXT NOT NULL,
|
||||
ban_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
ban_expires TIMESTAMP NULL DEFAULT NULL,
|
||||
PRIMARY KEY (ban_id),
|
||||
KEY users_bans_user_foreign (user_id),
|
||||
KEY users_bans_mod_foreign (mod_id),
|
||||
KEY users_bans_created_index (ban_created),
|
||||
KEY users_bans_expires_index (ban_expires),
|
||||
KEY users_bans_severity_index (ban_severity),
|
||||
CONSTRAINT users_bans_users_foreign
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT users_bans_mod_foreign
|
||||
FOREIGN KEY (mod_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB COLLATE=utf8mb4_bin
|
||||
');
|
||||
|
||||
$conn->execute('
|
||||
INSERT INTO msz_users_bans (user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, ban_created, ban_expires)
|
||||
SELECT user_id, issuer_id, 0, warning_note, COALESCE(warning_note_private, ""), warning_created, warning_duration
|
||||
FROM msz_user_warnings
|
||||
WHERE warning_type = 3
|
||||
');
|
||||
|
||||
$conn->execute('DELETE FROM msz_user_warnings WHERE warning_type = 3');
|
||||
}
|
||||
}
|
43
database/2023_07_26_210150_redo_warnings_table.php
Normal file
43
database/2023_07_26_210150_redo_warnings_table.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class RedoWarningsTable_20230726_210150 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('
|
||||
CREATE TABLE msz_users_warnings (
|
||||
warn_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id INT(10) UNSIGNED NOT NULL,
|
||||
mod_id INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
warn_body TEXT NOT NULL,
|
||||
warn_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (warn_id),
|
||||
KEY users_warnings_user_foreign (user_id),
|
||||
KEY users_warnings_mod_foreign (mod_id),
|
||||
KEY users_warnings_created_index (warn_created),
|
||||
CONSTRAINT users_warnings_user_foreign
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT users_warnings_mod_foreign
|
||||
FOREIGN KEY (mod_id)
|
||||
REFERENCES msz_users (user_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB COLLATE=utf8mb4_bin
|
||||
');
|
||||
|
||||
// migrate existing warnings, public and private note have been merged but that's fine in prod
|
||||
// still specifying type = 1 as well even though that should be the only type remaining
|
||||
$conn->execute('
|
||||
INSERT INTO msz_users_warnings (user_id, mod_id, warn_body, warn_created)
|
||||
SELECT user_id, issuer_id, TRIM(CONCAT(COALESCE(warning_note, ""), "\n", COALESCE(warning_note_private, ""))), warning_created
|
||||
FROM msz_user_warnings
|
||||
WHERE warning_type = 1
|
||||
');
|
||||
|
||||
// drop the old table with non-plural "user"
|
||||
$conn->execute('DROP TABLE msz_user_warnings');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class PluraliseUsersForRoleRelations_20230727_130516 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('RENAME TABLE msz_user_roles TO msz_users_roles');
|
||||
}
|
||||
}
|
16
database/2023_07_28_212101_create_counters_table.php
Normal file
16
database/2023_07_28_212101_create_counters_table.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class CreateCountersTable_20230728_212101 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('
|
||||
CREATE TABLE msz_counters (
|
||||
counter_name VARBINARY(64) NOT NULL,
|
||||
counter_value BIGINT(20) NOT NULL DEFAULT "0",
|
||||
counter_updated TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (counter_name)
|
||||
) ENGINE=InnoDB COLLATE=utf8mb4_bin
|
||||
');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class UpdateCollationsInVariousTables_20230803_114403 implements DbMigration {
|
||||
public function migrate(DbConnection $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;
|
||||
');
|
||||
}
|
||||
}
|
123
database/2023_08_30_213930_new_permissions_system.php
Normal file
123
database/2023_08_30_213930_new_permissions_system.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class NewPermissionsSystem_20230830_213930 implements DbMigration {
|
||||
public function migrate(DbConnection $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;")');
|
||||
}
|
||||
}
|
48
database/2024_01_30_233734_create_messages_table.php
Normal file
48
database/2024_01_30_233734_create_messages_table.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class CreateMessagesTable_20240130_233734 implements DbMigration {
|
||||
public function migrate(DbConnection $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;
|
||||
');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute('UPDATE msz_messages SET msg_title = TO_BASE64(msg_title), msg_body = TO_BASE64(msg_body)');
|
||||
$conn->execute('
|
||||
ALTER TABLE `msz_messages`
|
||||
CHANGE COLUMN `msg_title` `msg_title` TINYBLOB NOT NULL AFTER `msg_reply_to`,
|
||||
CHANGE COLUMN `msg_body` `msg_body` BLOB NOT NULL AFTER `msg_title`;
|
||||
');
|
||||
}
|
||||
}
|
13
database/2024_09_16_205613_add_role_id_string.php
Normal file
13
database/2024_09_16_205613_add_role_id_string.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class AddRoleIdString_20240916_205613 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute(<<<SQL
|
||||
ALTER TABLE msz_roles
|
||||
ADD COLUMN role_string VARCHAR(20) NULL DEFAULT NULL COLLATE 'ascii_general_ci' AFTER role_id,
|
||||
ADD UNIQUE INDEX roles_string_unique (role_string);
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
- [Aleister](https://patchii.net/flashii/aleister): Public API gateway.
|
||||
|
||||
## 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.
|
||||
- [RPCii](https://patchii.net/flashii/rpcii): Internal RPC extension, mainly used to supply data to the API gateway.
|
||||
|
||||
## 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).
|
||||
|
|
51
misuzu.php
51
misuzu.php
|
@ -1,10 +1,9 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Index\Autoloader;
|
||||
use Index\Environment;
|
||||
use Index\Data\DbTools;
|
||||
use Misuzu\Config\DbConfig;
|
||||
use Index\Db\DbBackends;
|
||||
use Index\Config\Db\DbConfig;
|
||||
use Index\Config\Fs\FsConfig;
|
||||
|
||||
define('MSZ_STARTUP', microtime(true));
|
||||
define('MSZ_ROOT', __DIR__);
|
||||
|
@ -19,37 +18,29 @@ 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');
|
||||
error_reporting(MSZ_DEBUG ? -1 : 0);
|
||||
mb_internal_encoding('UTF-8');
|
||||
date_default_timezone_set('GMT');
|
||||
|
||||
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 = FsConfig::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 = DbBackends::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'));
|
||||
|
||||
|
|
1029
package-lock.json
generated
1029
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.3.69",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cssnano": "^6.0.1",
|
||||
"postcss": "^8.4.26"
|
||||
"@railcomm/assproc": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,3 +4,6 @@ parameters:
|
|||
- src
|
||||
bootstrapFiles:
|
||||
- misuzu.php
|
||||
dynamicConstantNames:
|
||||
- MSZ_CLI
|
||||
- MSZ_DEBUG
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
url_redirect('auth-login');
|
|
@ -1,38 +1,40 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\AuthToken;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
use Misuzu\Users\UserAuthSession;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Users\UserSessionCreationFailedException;
|
||||
use Exception;
|
||||
use Misuzu\Auth\AuthTokenCookie;
|
||||
|
||||
if(UserSession::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(UserNotFoundException $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;
|
||||
}
|
||||
|
@ -40,7 +42,9 @@ if(!empty($_GET['resolve'])) {
|
|||
$notices = [];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
|
||||
$remainingAttempts = UserLoginAttempt::remaining($ipAddress);
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
$siteIsPrivate = $cfg->getBoolean('private.enable');
|
||||
if($siteIsPrivate) {
|
||||
|
@ -81,6 +85,8 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
break;
|
||||
}
|
||||
|
||||
$clientInfo = ClientInfo::fromRequest();
|
||||
|
||||
$attemptsRemainingError = sprintf(
|
||||
"%d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
|
@ -89,57 +95,60 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
|
||||
|
||||
try {
|
||||
$userInfo = User::byUsernameOrEMailAddress($_POST['login']['username']);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, false);
|
||||
$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'])) {
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, false, $userInfo);
|
||||
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.";
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, true, $userInfo);
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
if($userInfo->hasTOTP()) {
|
||||
url_redirect('auth-two-factor', [
|
||||
'token' => UserAuthSession::create($userInfo)->getToken(),
|
||||
]);
|
||||
if($userInfo->hasTOTPKey()) {
|
||||
$tfaToken = $authCtx->getTwoFactorAuthSessions()->createToken($userInfo);
|
||||
Tools::redirect($urls->format('auth-two-factor', ['token' => $tfaToken]));
|
||||
return;
|
||||
}
|
||||
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, true, $userInfo);
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::create($userInfo, $ipAddress, $countryCode);
|
||||
$sessionInfo->setCurrent();
|
||||
} catch(UserSessionCreationFailedException $ex) {
|
||||
$sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
@ -147,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', [
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Auth\AuthTokenCookie;
|
||||
|
||||
if(!UserSession::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()) {
|
||||
AuthToken::nukeCookie();
|
||||
UserSession::getCurrent()->delete();
|
||||
UserSession::unsetCurrent();
|
||||
User::unsetCurrent();
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
||||
Template::render('auth.logout');
|
||||
Tools::redirect($msz->getUrls()->format('index'));;
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
use Misuzu\Users\UserRecoveryToken;
|
||||
use Misuzu\Users\UserRecoveryTokenNotFoundException;
|
||||
use Misuzu\Users\UserRecoveryTokenCreationFailedException;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(UserSession::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'] : (
|
||||
|
@ -22,9 +24,9 @@ $userId = !empty($reset['user']) ? (int)$reset['user'] : (
|
|||
|
||||
if($userId > 0)
|
||||
try {
|
||||
$userInfo = User::byId($userId);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
url_redirect('auth-forgot');
|
||||
$userInfo = $users->getUser((string)$userId, 'id');
|
||||
} catch(RuntimeException $ex) {
|
||||
Tools::redirect($urls->format('auth-forgot'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -32,7 +34,8 @@ $notices = [];
|
|||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$siteIsPrivate = $cfg->getBoolean('private.enable');
|
||||
$canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true;
|
||||
$remainingAttempts = UserLoginAttempt::remaining($ipAddress);
|
||||
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
while($canResetPassword) {
|
||||
if(!empty($reset) && $userId > 0) {
|
||||
|
@ -41,15 +44,15 @@ while($canResetPassword) {
|
|||
break;
|
||||
}
|
||||
|
||||
$verificationCode = !empty($reset['verification']) && is_string($reset['verification']) ? $reset['verification'] : '';
|
||||
$verifyCode = !empty($reset['verification']) && is_string($reset['verification']) ? $reset['verification'] : '';
|
||||
|
||||
try {
|
||||
$tokenInfo = UserRecoveryToken::byToken($verificationCode);
|
||||
} catch(UserRecoveryTokenNotFoundException $ex) {
|
||||
$tokenInfo = $recoveryTokens->getToken(verifyCode: $verifyCode);
|
||||
} catch(RuntimeException $ex) {
|
||||
unset($tokenInfo);
|
||||
}
|
||||
|
||||
if(empty($tokenInfo) || !$tokenInfo->isValid() || $tokenInfo->getUserId() !== $userInfo->getId()) {
|
||||
if(empty($tokenInfo) || !$tokenInfo->isValid() || $tokenInfo->getUserId() !== (string)$userInfo->getId()) {
|
||||
$notices[] = 'Invalid verification code!';
|
||||
break;
|
||||
}
|
||||
|
@ -64,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);
|
||||
|
||||
$tokenInfo->invalidate();
|
||||
$recoveryTokens->invalidateToken($tokenInfo);
|
||||
|
||||
url_redirect('auth-login', ['redirect' => '/']);
|
||||
Tools::redirect($urls->format('auth-login', ['redirect' => '/']));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -100,8 +102,8 @@ while($canResetPassword) {
|
|||
}
|
||||
|
||||
try {
|
||||
$forgotUser = User::byEMailAddress($forgot['email']);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
$forgotUser = $users->getUser($forgot['email'], 'email');
|
||||
} catch(RuntimeException $ex) {
|
||||
unset($forgotUser);
|
||||
}
|
||||
|
||||
|
@ -111,28 +113,28 @@ while($canResetPassword) {
|
|||
}
|
||||
|
||||
try {
|
||||
$tokenInfo = UserRecoveryToken::byUserAndRemoteAddress($forgotUser, $ipAddress);
|
||||
} catch(UserRecoveryTokenNotFoundException $ex) {
|
||||
$tokenInfo = UserRecoveryToken::create($forgotUser, $ipAddress);
|
||||
$tokenInfo = $recoveryTokens->getToken(userInfo: $forgotUser, remoteAddr: $ipAddress);
|
||||
} catch(RuntimeException $ex) {
|
||||
$tokenInfo = $recoveryTokens->createToken($forgotUser, $ipAddress);
|
||||
|
||||
$recoveryMessage = Mailer::template('password-recovery', [
|
||||
'username' => $forgotUser->getUsername(),
|
||||
'token' => $tokenInfo->getToken(),
|
||||
'username' => $forgotUser->getName(),
|
||||
'token' => $tokenInfo->getCode(),
|
||||
]);
|
||||
|
||||
$recoveryMail = Mailer::sendMessage(
|
||||
[$forgotUser->getEMailAddress() => $forgotUser->getUsername()],
|
||||
[$forgotUser->getEMailAddress() => $forgotUser->getName()],
|
||||
$recoveryMessage['subject'], $recoveryMessage['message']
|
||||
);
|
||||
|
||||
if(!$recoveryMail) {
|
||||
$notices[] = "Failed to send reset email, please contact the administrator.";
|
||||
$tokenInfo->invalidate();
|
||||
$recoveryTokens->invalidateToken($tokenInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
url_redirect('auth-reset', ['user' => $forgotUser->getId()]);
|
||||
Tools::redirect($urls->format('auth-reset', ['user' => $forgotUser->getId()]));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -144,5 +146,5 @@ Template::render(isset($userInfo) ? 'auth.password_reset' : 'auth.password_forgo
|
|||
'password_email' => !empty($forget['email']) && is_string($forget['email']) ? $forget['email'] : '',
|
||||
'password_attempts_remaining' => $remainingAttempts,
|
||||
'password_user' => $userInfo ?? null,
|
||||
'password_verification' => $verificationCode ?? '',
|
||||
'password_verification' => $verifyCode ?? '',
|
||||
]);
|
||||
|
|
|
@ -1,24 +1,40 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserCreationFailedException;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
use Misuzu\Users\UserRole;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Users\UserWarning;
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
url_redirect('index');
|
||||
$urls = $msz->getUrls();
|
||||
$authInfo = $msz->getAuthInfo();
|
||||
if($authInfo->isLoggedIn()) {
|
||||
Tools::redirect($urls->format('index'));
|
||||
return;
|
||||
}
|
||||
|
||||
$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 = [];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
|
||||
$remainingAttempts = UserLoginAttempt::remaining($_SERVER['REMOTE_ADDR']);
|
||||
$restricted = UserWarning::countByRemoteAddress($ipAddress) > 0 ? 'ban' : '';
|
||||
|
||||
// there is currently no ip banning system.
|
||||
// because people can have a wide variety of ip address
|
||||
// it doesn't make sense to include a single row for it
|
||||
// in the user bans table
|
||||
// add better ip tracking and reintroduce the blacklist
|
||||
// was thinking of having both a storage table and an expanded table
|
||||
// with the storage table contains range syntaxes and whatnot
|
||||
// and the expanded table just having seas of raw ips in it with a primary key
|
||||
// for fast matching
|
||||
$restricted = '';
|
||||
|
||||
$loginAttempts = $authCtx->getLoginAttempts();
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
while(!$restricted && !empty($register)) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
|
@ -53,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(UserCreationFailedException $ex) {
|
||||
} catch(RuntimeException $ex) {
|
||||
$notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!';
|
||||
break;
|
||||
}
|
||||
|
||||
$createUser->addRole(UserRole::byDefault());
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -1,43 +1,47 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Users\UserSessionCreationFailedException;
|
||||
use Misuzu\Users\UserAuthSession;
|
||||
use Misuzu\Users\UserAuthSessionNotFoundException;
|
||||
use RuntimeException;
|
||||
use Misuzu\TOTPGenerator;
|
||||
use Misuzu\Auth\AuthTokenCookie;
|
||||
|
||||
if(UserSession::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 = [];
|
||||
$remainingAttempts = UserLoginAttempt::remaining($ipAddress);
|
||||
|
||||
try {
|
||||
$tokenInfo = UserAuthSession::byToken(
|
||||
!empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
|
||||
!empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
|
||||
)
|
||||
);
|
||||
} catch(UserAuthSessionNotFoundException $ex) {}
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
if(empty($tokenInfo) || $tokenInfo->hasExpired()) {
|
||||
url_redirect('auth-login');
|
||||
$tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
|
||||
!empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
|
||||
);
|
||||
|
||||
$tokenUserId = $tfaSessions->getTokenUserId($tokenString);
|
||||
if(empty($tokenUserId)) {
|
||||
Tools::redirect($urls->format('auth-login'));
|
||||
return;
|
||||
}
|
||||
|
||||
$userInfo = $tokenInfo->getUser();
|
||||
$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->hasTOTP()) {
|
||||
url_redirect('auth-login');
|
||||
if(!$userInfo->hasTOTPKey()) {
|
||||
Tools::redirect($urls->format('auth-login'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -60,41 +64,47 @@ while(!empty($twofactor)) {
|
|||
break;
|
||||
}
|
||||
|
||||
if(!in_array($twofactor['code'], $userInfo->getValidTOTPTokens())) {
|
||||
$clientInfo = ClientInfo::fromRequest();
|
||||
$totp = new TOTPGenerator($userInfo->getTOTPKey());
|
||||
|
||||
if(!in_array($twofactor['code'], $totp->generateRange())) {
|
||||
$notices[] = sprintf(
|
||||
"Invalid two factor code, %d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
$remainingAttempts === 2 ? '' : 's'
|
||||
);
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, false, $userInfo);
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
UserLoginAttempt::create($ipAddress, $countryCode, true, $userInfo);
|
||||
$tokenInfo->delete();
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
$tfaSessions->deleteToken($tokenString);
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::create($userInfo, $ipAddress, $countryCode);
|
||||
$sessionInfo->setCurrent();
|
||||
} catch(UserSessionCreationFailedException $ex) {
|
||||
$sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
|
||||
} catch(RuntimeException $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($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' => $tokenInfo->getToken(),
|
||||
'twofactor_token' => $tokenString,
|
||||
]);
|
||||
|
|
|
@ -2,40 +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($currentUserInfo->isBanned()) {
|
||||
echo render_info('You have been banned, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
if($currentUserInfo->isSilenced()) {
|
||||
echo render_info('You have been silenced, 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 = $currentUserInfo->commentPerms();
|
||||
$perms = $authInfo->getPerms('global');
|
||||
|
||||
$commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
||||
$commentMode = (string)filter_input(INPUT_GET, 'm');
|
||||
|
@ -43,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);
|
||||
|
@ -114,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);
|
||||
|
||||
|
@ -151,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);
|
||||
|
||||
|
@ -173,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())
|
||||
|
@ -217,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(
|
||||
|
@ -249,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);
|
||||
}
|
||||
|
|
|
@ -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) && $forumUser->hasActiveWarning())
|
||||
$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,
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
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'] : '';
|
||||
|
@ -10,206 +14,129 @@ $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) &
|
|||
|
||||
$postRequestVerified = CSRF::validateRequest();
|
||||
|
||||
if(!empty($postMode) && !UserSession::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(isset($currentUser) && $currentUser->isBanned()) {
|
||||
echo render_info('You have been banned, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
if(isset($currentUser) && $currentUser->isSilenced()) {
|
||||
echo render_info('You have been silenced, 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
|
||||
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
|
||||
|
||||
if(!empty($postInfo['post_deleted']) && !$canDeleteAny) {
|
||||
echo render_error(404);
|
||||
break;
|
||||
}
|
||||
|
||||
$postFind = forum_post_find($postInfo['post_id'], $currentUserId);
|
||||
|
||||
if(empty($postFind)) {
|
||||
echo render_error(404);
|
||||
break;
|
||||
}
|
||||
|
||||
if($canDeleteAny) {
|
||||
$postInfo['preceeding_post_count'] += $postInfo['preceeding_post_deleted_count'];
|
||||
}
|
||||
|
||||
unset($postInfo['preceeding_post_deleted_count']);
|
||||
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $postFind['topic_id'],
|
||||
'page' => floor($postFind['preceeding_post_count'] / MSZ_FORUM_POSTS_PER_PAGE) + 1,
|
||||
]);
|
||||
Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
use Misuzu\Forum\ForumTopicInfo;
|
||||
use Misuzu\Parsers\Parser;
|
||||
use Misuzu\Users\User;
|
||||
use Index\XDateTime;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
$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($currentUser->hasActiveWarning()) {
|
||||
echo render_error(403);
|
||||
return;
|
||||
}
|
||||
if($usersCtx->hasActiveBan($currentUser))
|
||||
Template::throwError(403);
|
||||
|
||||
$forumPostingModes = [
|
||||
'create', 'edit', 'quote', 'preview',
|
||||
|
@ -34,10 +39,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');
|
||||
|
@ -55,79 +58,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 = [];
|
||||
|
||||
|
@ -135,38 +142,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 = new CarbonImmutable(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(XDateTime::compare($lastPostCreatedAt, $postTimeoutThreshold) > 0) {
|
||||
$waitSeconds = $postTimeout + ((int)$lastPostCreatedAt->format('U') - 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);
|
||||
|
@ -175,92 +189,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,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue