Compare commits
55 commits
20240124.2
...
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 |
199 changed files with 6814 additions and 3413 deletions
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.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
@ -154,6 +154,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.forum__post__action {
|
.forum__post__action {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
|
@ -146,9 +146,14 @@
|
||||||
}
|
}
|
||||||
.header__desktop__user__button__count {
|
.header__desktop__user__button__count {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1px;
|
top: -5px;
|
||||||
right: 1px;
|
right: -3px;
|
||||||
font-size: 10px;
|
z-index: 1;
|
||||||
|
font-size: .5em;
|
||||||
|
line-height: 1.4em;
|
||||||
|
text-align: right;
|
||||||
|
padding: 2px 2px 0;
|
||||||
|
border-radius: 4px;
|
||||||
background-color: var(--header-accent-colour);
|
background-color: var(--header-accent-colour);
|
||||||
opacity: .9;
|
opacity: .9;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
outline-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -57,6 +56,8 @@ body {
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-color: var(--accent-colour) var(--background-colour);
|
scrollbar-color: var(--accent-colour) var(--background-colour);
|
||||||
|
accent-color: var(--accent-colour);
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
@ -163,6 +164,8 @@ html {
|
||||||
|
|
||||||
@include manage/_manage.css;
|
@include manage/_manage.css;
|
||||||
|
|
||||||
|
@include messages/messages.css;
|
||||||
|
|
||||||
@include news/container.css;
|
@include news/container.css;
|
||||||
@include news/feeds.css;
|
@include news/feeds.css;
|
||||||
@include news/list.css;
|
@include news/list.css;
|
||||||
|
|
|
@ -17,4 +17,5 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 5px;
|
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 {
|
.news__feeds {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
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'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
|
@ -56,7 +56,7 @@ const MszAudioEmbedPlayer = function(metadata, options) {
|
||||||
if(haveNativeControls)
|
if(haveNativeControls)
|
||||||
playerAttrs.controls = 'controls';
|
playerAttrs.controls = 'controls';
|
||||||
|
|
||||||
const watchers = new MszWatcherCollection;
|
const watchers = new MszWatchers;
|
||||||
watchers.define(MszAudioEmbedPlayerEvents());
|
watchers.define(MszAudioEmbedPlayerEvents());
|
||||||
|
|
||||||
const player = $e({
|
const player = $e({
|
||||||
|
|
|
@ -229,7 +229,7 @@ const MszVideoEmbedPlayer = function(metadata, options) {
|
||||||
videoAttrs.style.width = initialSize[0].toString() + 'px';
|
videoAttrs.style.width = initialSize[0].toString() + 'px';
|
||||||
videoAttrs.style.height = initialSize[1].toString() + 'px';
|
videoAttrs.style.height = initialSize[1].toString() + 'px';
|
||||||
|
|
||||||
const watchers = new MszWatcherCollection;
|
const watchers = new MszWatchers;
|
||||||
watchers.define(MszVideoEmbedPlayerEvents());
|
watchers.define(MszVideoEmbedPlayerEvents());
|
||||||
|
|
||||||
const player = $e({
|
const player = $e({
|
||||||
|
@ -375,7 +375,7 @@ const MszVideoEmbedYouTube = function(metadata, options) {
|
||||||
currentTime = undefined,
|
currentTime = undefined,
|
||||||
isPlaying = undefined;
|
isPlaying = undefined;
|
||||||
|
|
||||||
const watchers = new MszWatcherCollection;
|
const watchers = new MszWatchers;
|
||||||
watchers.define(MszVideoEmbedPlayerEvents());
|
watchers.define(MszVideoEmbedPlayerEvents());
|
||||||
|
|
||||||
const player = $e({
|
const player = $e({
|
||||||
|
@ -576,7 +576,7 @@ const MszVideoEmbedNicoNico = function(metadata, options) {
|
||||||
currentTime = undefined,
|
currentTime = undefined,
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
|
|
||||||
const watchers = new MszWatcherCollection;
|
const watchers = new MszWatchers;
|
||||||
watchers.define(MszVideoEmbedPlayerEvents());
|
watchers.define(MszVideoEmbedPlayerEvents());
|
||||||
|
|
||||||
const player = $e({
|
const player = $e({
|
||||||
|
|
|
@ -19,7 +19,7 @@ const MszEEPROM = (() => {
|
||||||
const scriptElem = $e({
|
const scriptElem = $e({
|
||||||
tag: 'script',
|
tag: 'script',
|
||||||
attrs: {
|
attrs: {
|
||||||
src: `${peepPath}/eeprom.js`,
|
src: `${peepPath}/scripts/eepromv1a.js`,
|
||||||
charset: 'utf-8',
|
charset: 'utf-8',
|
||||||
type: 'text/javascript',
|
type: 'text/javascript',
|
||||||
onerror: () => reject(),
|
onerror: () => reject(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#include msgbox.jsx
|
#include msgbox.jsx
|
||||||
|
#include parsing.js
|
||||||
#include utility.js
|
#include utility.js
|
||||||
#include ext/eeprom.js
|
#include ext/eeprom.js
|
||||||
|
|
||||||
|
@ -13,10 +14,7 @@ const MszForumEditor = function(form) {
|
||||||
parserElem = form.querySelector('.js-forum-posting-parser'),
|
parserElem = form.querySelector('.js-forum-posting-parser'),
|
||||||
previewElem = form.querySelector('.js-forum-posting-preview'),
|
previewElem = form.querySelector('.js-forum-posting-preview'),
|
||||||
modeElem = form.querySelector('.js-forum-posting-mode'),
|
modeElem = form.querySelector('.js-forum-posting-mode'),
|
||||||
markupBtns = form.querySelectorAll('.js-forum-posting-markup');
|
markupActs = form.querySelector('.js-forum-posting-actions');
|
||||||
|
|
||||||
const bbBtns = $q('.forum__post__actions--bbcode'),
|
|
||||||
mdBtns = $q('.forum__post__actions--markdown');
|
|
||||||
|
|
||||||
let lastPostText = '',
|
let lastPostText = '',
|
||||||
lastPostParser;
|
lastPostParser;
|
||||||
|
@ -24,10 +22,10 @@ const MszForumEditor = function(form) {
|
||||||
MszEEPROM.init()
|
MszEEPROM.init()
|
||||||
.catch(() => console.error('Failed to initialise EEPROM'))
|
.catch(() => console.error('Failed to initialise EEPROM'))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const eepromClient = new EEPROM(peepApp, `${peepPath}/uploads`, '');
|
const eepromClient = new EEPROM(peepApp, peepPath);
|
||||||
const eepromHistory = <div class="eeprom-widget-history-items"/>;
|
const eepromHistory = <div class="eeprom-widget-history-items"/>;
|
||||||
|
|
||||||
const eepromHandleFileUpload = file => {
|
const eepromHandleFileUpload = async file => {
|
||||||
const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>;
|
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 uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>;
|
||||||
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
|
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
|
||||||
|
@ -48,62 +46,21 @@ const MszForumEditor = function(form) {
|
||||||
eepromHistory.appendChild(uploadElem);
|
eepromHistory.appendChild(uploadElem);
|
||||||
|
|
||||||
const explodeUploadElem = () => $r(uploadElem);
|
const explodeUploadElem = () => $r(uploadElem);
|
||||||
const uploadTask = eepromClient.createUpload(file);
|
const uploadTask = eepromClient.create(file);
|
||||||
|
|
||||||
uploadTask.onProgress = function(progressInfo) {
|
uploadTask.onProgress(prog => {
|
||||||
const progressValue = `${progressInfo.progress}%`;
|
uploadElemProgressBarValue.style.width = `${Math.ceil(prog.progress * 100)}%`;
|
||||||
uploadElemProgressBarValue.style.width = progressValue;
|
uploadElemProgressText.textContent = `${prog.progress.toLocaleString(undefined, { style: 'percent' })} (${prog.total - prog.loaded} bytes remaining)`;
|
||||||
uploadElemProgressText.textContent = `${progressValue} (${progressInfo.total - progressInfo.loaded} bytes remaining)`;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
uploadTask.onFailure = function(errorInfo) {
|
try {
|
||||||
if(!errorInfo.userAborted) {
|
const fileInfo = await uploadTask.start();
|
||||||
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 types = ['bytes', 'KB', 'MB', 'GB', 'TB'],
|
|
||||||
typeIndex = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))),
|
|
||||||
number = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2);
|
|
||||||
|
|
||||||
errorText = `Upload may not be larger than ${number} ${types[typeIndex]}.`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadElem.classList.add('eeprom-widget-file-fail');
|
|
||||||
uploadElemProgressText.textContent = errorText;
|
|
||||||
MszShowMessageBox(errorText, 'Upload Error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadTask.onComplete = function(fileInfo) {
|
|
||||||
uploadElem.classList.add('eeprom-widget-file-done');
|
uploadElem.classList.add('eeprom-widget-file-done');
|
||||||
uploadElemName.href = fileInfo.url;
|
uploadElemName.href = fileInfo.url;
|
||||||
uploadElemProgressText.textContent = '';
|
uploadElemProgressText.textContent = '';
|
||||||
|
|
||||||
const insertTheLinkIntoTheBoxEx2 = function() {
|
const insertTheLinkIntoTheBoxEx2 = () => {
|
||||||
const parserMode = parseInt(parserElem.value);
|
const parserMode = parseInt(parserElem.value);
|
||||||
let insertText = location.protocol + fileInfo.url;
|
let insertText = location.protocol + fileInfo.url;
|
||||||
|
|
||||||
|
@ -126,14 +83,27 @@ const MszForumEditor = function(form) {
|
||||||
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
|
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
|
||||||
uploadElemProgressText.appendChild($t(' '));
|
uploadElemProgressText.appendChild($t(' '));
|
||||||
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
|
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
|
||||||
eepromClient.deleteUpload(fileInfo).start();
|
eepromClient.delete(fileInfo)
|
||||||
explodeUploadElem();
|
.then(() => explodeUploadElem())
|
||||||
|
.catch(ex => {
|
||||||
|
console.error(ex);
|
||||||
|
MszShowMessageBox(ex, 'Upload Error');
|
||||||
|
});
|
||||||
}}>Delete</a>);
|
}}>Delete</a>);
|
||||||
|
|
||||||
insertTheLinkIntoTheBoxEx2();
|
insertTheLinkIntoTheBoxEx2();
|
||||||
};
|
} catch(ex) {
|
||||||
|
let errorText = 'Upload aborted.';
|
||||||
|
|
||||||
uploadTask.start();
|
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"
|
const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input"
|
||||||
|
@ -204,13 +174,15 @@ const MszForumEditor = function(form) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for(const button of markupBtns)
|
|
||||||
button.addEventListener('click', () => $insertTags(textElem, button.dataset.tagOpen, button.dataset.tagClose));
|
|
||||||
|
|
||||||
const switchButtons = parser => {
|
const switchButtons = parser => {
|
||||||
parser = parseInt(parser);
|
$rc(markupActs);
|
||||||
bbBtns.hidden = parser !== 1;
|
|
||||||
mdBtns.hidden = parser !== 2;
|
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) => {
|
const renderPreview = async (parser, text) => {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include events/events.js
|
#include events/events.js
|
||||||
#include ext/sakuya.js
|
#include ext/sakuya.js
|
||||||
#include forum/editor.jsx
|
#include forum/editor.jsx
|
||||||
|
#include messages/messages.js
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const initLoginPage = async () => {
|
const initLoginPage = async () => {
|
||||||
|
@ -80,6 +81,8 @@
|
||||||
|
|
||||||
await initLoginPage();
|
await initLoginPage();
|
||||||
|
|
||||||
|
MszMessages();
|
||||||
|
|
||||||
MszEmbed.handle($qa('.js-msz-embed-media'));
|
MszEmbed.handle($qa('.js-msz-embed-media'));
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
console.error(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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,19 +1,36 @@
|
||||||
#include utility.js
|
#include utility.js
|
||||||
|
|
||||||
const MszShowMessageBox = async (text, title, buttons, target) => {
|
const MszShowConfirmBox = async (text, title, target) => {
|
||||||
if(typeof text !== 'string')
|
let result = false;
|
||||||
throw 'text must be a string';
|
|
||||||
|
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))
|
if(!(target instanceof Element))
|
||||||
target = document.body;
|
target = document.body;
|
||||||
|
|
||||||
if(target.querySelector('.messagebox'))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if(typeof title !== 'string')
|
if(typeof title !== 'string')
|
||||||
title = 'Information';
|
title = 'Information';
|
||||||
if(!Array.isArray(buttons))
|
if(!Array.isArray(buttons))
|
||||||
buttons = [];
|
buttons = [];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if(target.querySelector('.messagebox')) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let buttonsElem;
|
let buttonsElem;
|
||||||
const html = <div class="messagebox">
|
const html = <div class="messagebox">
|
||||||
<div class="container messagebox__container">
|
<div class="container messagebox__container">
|
||||||
|
@ -28,13 +45,21 @@ const MszShowMessageBox = async (text, title, buttons, target) => {
|
||||||
|
|
||||||
let firstButton;
|
let firstButton;
|
||||||
if(buttons.length < 1) {
|
if(buttons.length < 1) {
|
||||||
firstButton = <button class="input__button" onclick={() => html.remove()}>OK</button>;
|
firstButton = <button class="input__button" onclick={() => {
|
||||||
|
html.remove();
|
||||||
|
resolve();
|
||||||
|
}}>OK</button>;
|
||||||
buttonsElem.appendChild(firstButton);
|
buttonsElem.appendChild(firstButton);
|
||||||
} else {
|
} else {
|
||||||
for(const button of buttons) {
|
for(const button of buttons) {
|
||||||
const buttonElem = <button class="input__button" onclick={() => { html.remove(); if(typeof button === 'function') button.callback(); }}>
|
const buttonElem = <button class="input__button" onclick={() => {
|
||||||
{button.text}
|
html.remove();
|
||||||
</button>;
|
|
||||||
|
if(typeof button.callback === 'function')
|
||||||
|
button.callback().finally(() => resolve());
|
||||||
|
else
|
||||||
|
resolve();
|
||||||
|
}}>{button.text}</button>;
|
||||||
buttonsElem.appendChild(buttonElem);
|
buttonsElem.appendChild(buttonElem);
|
||||||
|
|
||||||
if(firstButton === undefined)
|
if(firstButton === undefined)
|
||||||
|
@ -44,6 +69,5 @@ const MszShowMessageBox = async (text, title, buttons, target) => {
|
||||||
|
|
||||||
target.appendChild(html);
|
target.appendChild(html);
|
||||||
firstButton.focus();
|
firstButton.focus();
|
||||||
|
});
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
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),
|
||||||
|
};
|
||||||
|
})();
|
|
@ -28,7 +28,7 @@ const MszWatcher = function() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const MszWatcherCollection = function() {
|
const MszWatchers = function() {
|
||||||
const watchers = new Map;
|
const watchers = new Map;
|
||||||
|
|
||||||
const getWatcher = name => {
|
const getWatcher = name => {
|
||||||
|
|
159
build.js
159
build.js
|
@ -1,157 +1,30 @@
|
||||||
// IMPORTS
|
const assproc = require('@railcomm/assproc');
|
||||||
|
const { join: pathJoin } = require('path');
|
||||||
const fs = require('fs');
|
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');
|
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
|
||||||
|
|
||||||
// CONFIG
|
const env = {
|
||||||
const rootDir = __dirname;
|
root: __dirname,
|
||||||
const srcDir = path.join(rootDir, 'assets');
|
source: pathJoin(__dirname, 'assets'),
|
||||||
const srcCurrentInfo = path.join(srcDir, 'current.json');
|
public: pathJoin(__dirname, 'public'),
|
||||||
const pubDir = path.join(rootDir, 'public');
|
debug: isDebug,
|
||||||
const pubAssetsDir = path.join(pubDir, 'assets');
|
swc: {
|
||||||
|
es: 'es2021',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
|
const tasks = {
|
||||||
|
|
||||||
const buildTasks = {
|
|
||||||
js: [
|
js: [
|
||||||
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
|
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
|
||||||
],
|
],
|
||||||
css: [
|
css: [
|
||||||
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
|
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
|
||||||
],
|
],
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// PREP
|
|
||||||
const postcssPlugins = [ require('autoprefixer')({ remove: false }) ];
|
|
||||||
if(!isDebugBuild)
|
|
||||||
postcssPlugins.push(require('cssnano')({
|
|
||||||
preset: [
|
|
||||||
'cssnano-preset-default',
|
|
||||||
{
|
|
||||||
minifyGradients: false,
|
|
||||||
reduceIdents: false,
|
|
||||||
zindex: true,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const swcJscOptions = {
|
|
||||||
target: 'es2021',
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// BUILD
|
|
||||||
(async () => {
|
|
||||||
const files = {};
|
|
||||||
|
|
||||||
console.log('Ensuring assets directory exists...');
|
|
||||||
fs.mkdirSync(pubAssetsDir, { recursive: true });
|
|
||||||
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log('JS assets');
|
|
||||||
for(const info of buildTasks.js) {
|
|
||||||
console.log(`=> Building ${info.source}...`);
|
|
||||||
|
|
||||||
let origTarget = undefined;
|
|
||||||
if('es' in info) {
|
|
||||||
origTarget = swcJscOptions.target;
|
|
||||||
swcJscOptions.target = info.es;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assprocOpts = {
|
|
||||||
prefix: '#',
|
|
||||||
entry: info.entry || 'main.js',
|
|
||||||
};
|
|
||||||
const swcOpts = {
|
|
||||||
filename: info.source,
|
|
||||||
sourceMaps: false,
|
|
||||||
isModule: false,
|
|
||||||
minify: !isDebugBuild,
|
|
||||||
jsc: swcJscOptions,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
|
const files = await assproc.process(env, tasks);
|
||||||
.then(output => swc.transform(output, swcOpts))
|
|
||||||
.then(output => {
|
|
||||||
const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) });
|
|
||||||
const pubName = path.join(info.target || '', name);
|
|
||||||
|
|
||||||
console.log(` Saving to ${pubName}...`);
|
fs.writeFileSync(pathJoin(__dirname, 'assets/current.json'), JSON.stringify(files));
|
||||||
fs.writeFileSync(path.join(pubDir, pubName), output.code);
|
|
||||||
|
|
||||||
return pubName;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(origTarget !== undefined)
|
|
||||||
swcJscOptions.target = origTarget;
|
|
||||||
|
|
||||||
files[info.source] = pubName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log('CSS assets');
|
|
||||||
for(const info of buildTasks.css) {
|
|
||||||
console.log(`=> Building ${info.source}...`);
|
|
||||||
|
|
||||||
const sourcePath = path.join(srcDir, info.source);
|
|
||||||
const assprocOpts = {
|
|
||||||
prefix: '@',
|
|
||||||
entry: info.entry || 'main.css',
|
|
||||||
};
|
|
||||||
const postcssOpts = { from: sourcePath };
|
|
||||||
|
|
||||||
files[info.source] = await assproc.process(sourcePath, assprocOpts)
|
|
||||||
.then(output => postcss(postcssPlugins).process(output, postcssOpts)
|
|
||||||
.then(output => {
|
|
||||||
const name = utils.strtr(info.name, { hash: utils.shortHash(output.css) });
|
|
||||||
const pubName = path.join(info.target || '', name);
|
|
||||||
|
|
||||||
console.log(` Saving to ${pubName}...`);
|
|
||||||
fs.writeFileSync(path.join(pubDir, pubName), output.css);
|
|
||||||
|
|
||||||
return pubName;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log('Writing assets info...');
|
|
||||||
fs.writeFileSync(srcCurrentInfo, JSON.stringify(files));
|
|
||||||
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log('Cleaning up old builds...');
|
|
||||||
assproc.housekeep(pubAssetsDir);
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
{
|
{
|
||||||
"minimum-stability": "dev",
|
|
||||||
"prefer-stable": true,
|
|
||||||
"require": {
|
"require": {
|
||||||
"flashwave/index": "dev-master",
|
"flashwave/index": "^0.2410",
|
||||||
"flashwave/sasae": "dev-master",
|
"flashii/rpcii": "^2.0",
|
||||||
"erusev/parsedown": "~1.6",
|
"erusev/parsedown": "~1.6",
|
||||||
"chillerlan/php-qrcode": "^4.3",
|
"chillerlan/php-qrcode": "^4.3",
|
||||||
"symfony/mailer": "^6.0",
|
"symfony/mailer": "^6.0",
|
||||||
"matomo/device-detector": "^6.1",
|
"matomo/device-detector": "^6.1",
|
||||||
"sentry/sdk": "^4.0",
|
"sentry/sdk": "^4.0",
|
||||||
"flashwave/syokuhou": "dev-master"
|
"nesbot/carbon": "^3.7"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
@ -34,6 +32,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^1.10"
|
"phpstan/phpstan": "^1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1213
composer.lock
generated
1213
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
// Switching to the Index migration system!!!!!!
|
// Switching to the Index migration system!!!!!!
|
||||||
|
|
||||||
final class InitialStructureNdx_20230107_023235 implements IDbMigration {
|
final class InitialStructureNdx_20230107_023235 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$hasMszTrack = false;
|
$hasMszTrack = false;
|
||||||
|
|
||||||
// check if the old migrations table exists
|
// check if the old migrations table exists
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class CreateTopicRedirsTable_20230430_001226 implements IDbMigration {
|
final class CreateTopicRedirsTable_20230430_001226 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
CREATE TABLE msz_forum_topics_redirects (
|
CREATE TABLE msz_forum_topics_redirects (
|
||||||
topic_id INT(10) UNSIGNED NOT NULL,
|
topic_id INT(10) UNSIGNED NOT NULL,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
|
|
||||||
final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
|
final class UpdateUserAgentStorage_20230721_121854 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
// convert user agent fields to BLOB and add field for client info storage
|
// convert user agent fields to BLOB and add field for client info storage
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
ALTER TABLE msz_login_attempts
|
ALTER TABLE msz_login_attempts
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class AddModeratorNotesTable_20230724_201010 implements IDbMigration {
|
final class AddModeratorNotesTable_20230724_201010 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
CREATE TABLE msz_users_modnotes (
|
CREATE TABLE msz_users_modnotes (
|
||||||
note_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
note_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class AddNewBansTable_20230726_175936 implements IDbMigration {
|
final class AddNewBansTable_20230726_175936 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
CREATE TABLE msz_users_bans (
|
CREATE TABLE msz_users_bans (
|
||||||
ban_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
ban_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class RedoWarningsTable_20230726_210150 implements IDbMigration {
|
final class RedoWarningsTable_20230726_210150 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
CREATE TABLE msz_users_warnings (
|
CREATE TABLE msz_users_warnings (
|
||||||
warn_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
warn_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class PluraliseUsersForRoleRelations_20230727_130516 implements IDbMigration {
|
final class PluraliseUsersForRoleRelations_20230727_130516 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('RENAME TABLE msz_user_roles TO msz_users_roles');
|
$conn->execute('RENAME TABLE msz_user_roles TO msz_users_roles');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class CreateCountersTable_20230728_212101 implements IDbMigration {
|
final class CreateCountersTable_20230728_212101 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
CREATE TABLE msz_counters (
|
CREATE TABLE msz_counters (
|
||||||
counter_name VARBINARY(64) NOT NULL,
|
counter_name VARBINARY(64) NOT NULL,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class UpdateCollationsInVariousTables_20230803_114403 implements IDbMigration {
|
final class UpdateCollationsInVariousTables_20230803_114403 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
$conn->execute('
|
$conn->execute('
|
||||||
ALTER TABLE msz_audit_log
|
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_action log_action VARCHAR(50) NOT NULL COLLATE "ascii_general_ci" AFTER user_id,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\DbConnection;
|
||||||
use Index\Data\Migration\IDbMigration;
|
use Index\Db\Migration\DbMigration;
|
||||||
|
|
||||||
final class NewPermissionsSystem_20230830_213930 implements IDbMigration {
|
final class NewPermissionsSystem_20230830_213930 implements DbMigration {
|
||||||
public function migrate(IDbConnection $conn): void {
|
public function migrate(DbConnection $conn): void {
|
||||||
// make sure cron doesn't fuck us over
|
// make sure cron doesn't fuck us over
|
||||||
$conn->execute('DELETE FROM msz_config WHERE config_name = "perms.needsRecalc"');
|
$conn->execute('DELETE FROM msz_config WHERE config_name = "perms.needsRecalc"');
|
||||||
|
|
||||||
|
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ Below are a number of links to source code repositories related to Flashii.net a
|
||||||
- [Seria](https://patchii.net/flashii/seria): Software used by the downloads tracker.
|
- [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.
|
- [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.
|
- [Awaki](https://patchii.net/flashii/awaki): Redirect service hosted on fii.moe.
|
||||||
|
- [Aleister](https://patchii.net/flashii/aleister): Public API gateway.
|
||||||
|
|
||||||
## Tools & Software
|
## Tools & Software
|
||||||
- [SoFii](https://patchii.net/flashii/sofii): Launcher for Soldier of Fortune 2
|
- [SoFii](https://patchii.net/flashii/sofii): Launcher for Soldier of Fortune 2
|
||||||
|
@ -21,8 +22,7 @@ Below are a number of links to source code repositories related to Flashii.net a
|
||||||
|
|
||||||
## First-Party Libraries
|
## First-Party Libraries
|
||||||
- [Index](https://patchii.net/flash/index): Base library used in almost any component of the website that uses PHP.
|
- [Index](https://patchii.net/flash/index): Base library used in almost any component of the website that uses PHP.
|
||||||
- [Sasae](https://patchii.net/flash/sasae): Extension to the Twig templating library.
|
- [RPCii](https://patchii.net/flashii/rpcii): Internal RPC extension, mainly used to supply data to the API gateway.
|
||||||
- [Syokuhou](https://patchii.net/flash/syokuhou): Configuration library.
|
|
||||||
|
|
||||||
## Historical
|
## Historical
|
||||||
- [AJAX Chat (fork)](https://patchii.net/flashii/ajax-chat): Old chat software (2013-2015). Still kept on life support for the nostalgia.
|
- [AJAX Chat (fork)](https://patchii.net/flashii/ajax-chat): Old chat software (2013-2015). Still kept on life support for the nostalgia.
|
||||||
|
|
15
misuzu.php
15
misuzu.php
|
@ -1,10 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use Index\Environment;
|
use Index\Db\DbBackends;
|
||||||
use Index\Data\DbTools;
|
use Index\Config\Db\DbConfig;
|
||||||
use Syokuhou\DbConfig;
|
use Index\Config\Fs\FsConfig;
|
||||||
use Syokuhou\SharpConfig;
|
|
||||||
|
|
||||||
define('MSZ_STARTUP', microtime(true));
|
define('MSZ_STARTUP', microtime(true));
|
||||||
define('MSZ_ROOT', __DIR__);
|
define('MSZ_ROOT', __DIR__);
|
||||||
|
@ -19,11 +18,11 @@ define('MSZ_ASSETS', MSZ_ROOT . '/assets');
|
||||||
|
|
||||||
require_once MSZ_ROOT . '/vendor/autoload.php';
|
require_once MSZ_ROOT . '/vendor/autoload.php';
|
||||||
|
|
||||||
Environment::setDebug(MSZ_DEBUG);
|
error_reporting(MSZ_DEBUG ? -1 : 0);
|
||||||
mb_internal_encoding('UTF-8');
|
mb_internal_encoding('UTF-8');
|
||||||
date_default_timezone_set('UTC');
|
date_default_timezone_set('GMT');
|
||||||
|
|
||||||
$cfg = SharpConfig::fromFile(MSZ_CONFIG . '/config.cfg');
|
$cfg = FsConfig::fromFile(MSZ_CONFIG . '/config.cfg');
|
||||||
|
|
||||||
if($cfg->hasValues('sentry:dsn'))
|
if($cfg->hasValues('sentry:dsn'))
|
||||||
(function($cfg) {
|
(function($cfg) {
|
||||||
|
@ -38,7 +37,7 @@ if($cfg->hasValues('sentry:dsn'))
|
||||||
});
|
});
|
||||||
})($cfg->scopeTo('sentry'));
|
})($cfg->scopeTo('sentry'));
|
||||||
|
|
||||||
$db = DbTools::create($cfg->getString('database:dsn', 'null:'));
|
$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->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\';');
|
||||||
|
|
||||||
$cfg = new DbConfig($db, 'msz_config');
|
$cfg = new DbConfig($db, 'msz_config');
|
||||||
|
|
942
package-lock.json
generated
942
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/core": "^1.3.69",
|
"@railcomm/assproc": "^1.0.0"
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"cssnano": "^6.0.1",
|
|
||||||
"postcss": "^8.4.26"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,6 @@ parameters:
|
||||||
- src
|
- src
|
||||||
bootstrapFiles:
|
bootstrapFiles:
|
||||||
- misuzu.php
|
- misuzu.php
|
||||||
|
dynamicConstantNames:
|
||||||
|
- MSZ_CLI
|
||||||
|
- MSZ_DEBUG
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Misuzu;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Misuzu\Auth\AuthTokenCookie;
|
use Misuzu\Auth\AuthTokenCookie;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if($authInfo->isLoggedIn()) {
|
if($authInfo->isLoggedIn()) {
|
||||||
Tools::redirect($urls->format('index'));
|
Tools::redirect($urls->format('index'));
|
||||||
|
|
|
@ -24,4 +24,4 @@ if($authInfo->isLoggedIn()) {
|
||||||
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
|
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
Tools::redirect($msz->getURLs()->format('index'));;
|
Tools::redirect($msz->getUrls()->format('index'));;
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Misuzu;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if($authInfo->isLoggedIn()) {
|
if($authInfo->isLoggedIn()) {
|
||||||
Tools::redirect($urls->format('settings-account'));
|
Tools::redirect($urls->format('settings-account'));
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Misuzu;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if($authInfo->isLoggedIn()) {
|
if($authInfo->isLoggedIn()) {
|
||||||
Tools::redirect($urls->format('index'));
|
Tools::redirect($urls->format('index'));
|
||||||
|
|
|
@ -3,7 +3,7 @@ namespace Misuzu;
|
||||||
|
|
||||||
use Misuzu\Auth\AuthTokenCookie;
|
use Misuzu\Auth\AuthTokenCookie;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
if(CSRF::validateRequest()) {
|
if(CSRF::validateRequest()) {
|
||||||
$tokenInfo = $msz->getAuthInfo()->getTokenInfo();
|
$tokenInfo = $msz->getAuthInfo()->getTokenInfo();
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use RuntimeException;
|
||||||
use Misuzu\TOTPGenerator;
|
use Misuzu\TOTPGenerator;
|
||||||
use Misuzu\Auth\AuthTokenCookie;
|
use Misuzu\Auth\AuthTokenCookie;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if($authInfo->isLoggedIn()) {
|
if($authInfo->isLoggedIn()) {
|
||||||
Tools::redirect($urls->format('index'));
|
Tools::redirect($urls->format('index'));
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Misuzu;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getURLs()->format('index');
|
$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getUrls()->format('index');
|
||||||
|
|
||||||
if(!Tools::isLocalURL($redirect))
|
if(!Tools::isLocalURL($redirect))
|
||||||
Template::displayInfo('Possible request forgery detected.', 403);
|
Template::displayInfo('Possible request forgery detected.', 403);
|
||||||
|
|
|
@ -168,7 +168,7 @@ $perms = $perms->checkMany([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Template::render('forum.forum', [
|
Template::render('forum.forum', [
|
||||||
'forum_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo),
|
'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
|
||||||
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
||||||
'forum_info' => $categoryInfo,
|
'forum_info' => $categoryInfo,
|
||||||
'forum_children' => $children,
|
'forum_children' => $children,
|
||||||
|
|
|
@ -32,14 +32,14 @@ if($mode === 'mark') {
|
||||||
$forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
|
$forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tools::redirect($msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
|
Tools::redirect($msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Template::render('confirm', [
|
Template::render('confirm', [
|
||||||
'title' => 'Mark forum as read',
|
'title' => 'Mark forum as read',
|
||||||
'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' 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]),
|
'return' => $msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
|
||||||
'params' => [
|
'params' => [
|
||||||
'forum' => $categoryId,
|
'forum' => $categoryId,
|
||||||
]
|
]
|
||||||
|
|
|
@ -94,7 +94,7 @@ MD;
|
||||||
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
|
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
|
||||||
$ranking->user?->getName() ?? 'Deleted User',
|
$ranking->user?->getName() ?? 'Deleted User',
|
||||||
$msz->getSiteInfo()->getURL(),
|
$msz->getSiteInfo()->getURL(),
|
||||||
$msz->getURLs()->format('user-profile', ['user' => $ranking->userId]),
|
$msz->getUrls()->format('user-profile', ['user' => $ranking->userId]),
|
||||||
number_format($ranking->postsCount));
|
number_format($ranking->postsCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ namespace Misuzu;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$forumCtx = $msz->getForumContext();
|
$forumCtx = $msz->getForumContext();
|
||||||
$forumPosts = $forumCtx->getPosts();
|
$forumPosts = $forumCtx->getPosts();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
|
|
|
@ -3,9 +3,10 @@ namespace Misuzu;
|
||||||
|
|
||||||
use stdClass;
|
use stdClass;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\DateTime;
|
|
||||||
use Misuzu\Forum\ForumTopicInfo;
|
use Misuzu\Forum\ForumTopicInfo;
|
||||||
use Misuzu\Parsers\Parser;
|
use Misuzu\Parsers\Parser;
|
||||||
|
use Index\XDateTime;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->isLoggedIn())
|
if(!$authInfo->isLoggedIn())
|
||||||
|
@ -152,11 +153,11 @@ if(!empty($_POST)) {
|
||||||
if($mode === 'create') {
|
if($mode === 'create') {
|
||||||
$postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
|
$postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
|
||||||
if($postTimeout > 0) {
|
if($postTimeout > 0) {
|
||||||
$postTimeoutThreshold = DateTime::now()->modify(sprintf('-%d seconds', $postTimeout));
|
$postTimeoutThreshold = new CarbonImmutable(sprintf('-%d seconds', $postTimeout));
|
||||||
$lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
|
$lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
|
||||||
|
|
||||||
if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) {
|
if(XDateTime::compare($lastPostCreatedAt, $postTimeoutThreshold) > 0) {
|
||||||
$waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time());
|
$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[] = 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.";
|
$notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";
|
||||||
|
@ -257,7 +258,7 @@ if(!empty($_POST)) {
|
||||||
|
|
||||||
if(empty($notices)) {
|
if(empty($notices)) {
|
||||||
// does this ternary ever return forum-topic?
|
// does this ternary ever return forum-topic?
|
||||||
$redirect = $msz->getURLs()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
|
$redirect = $msz->getUrls()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
|
||||||
'topic' => $topicId ?? 0,
|
'topic' => $topicId ?? 0,
|
||||||
'post' => $postId ?? 0,
|
'post' => $postId ?? 0,
|
||||||
]);
|
]);
|
||||||
|
@ -296,7 +297,7 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
Template::render('forum.posting', [
|
Template::render('forum.posting', [
|
||||||
'posting_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo),
|
'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
|
||||||
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
||||||
'posting_user' => $currentUser,
|
'posting_user' => $currentUser,
|
||||||
'posting_user_colour' => $usersCtx->getUserColour($currentUser),
|
'posting_user_colour' => $usersCtx->getUserColour($currentUser),
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Misuzu;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$forumCtx = $msz->getForumContext();
|
$forumCtx = $msz->getForumContext();
|
||||||
$forumCategories = $forumCtx->getCategories();
|
$forumCategories = $forumCtx->getCategories();
|
||||||
$forumTopics = $forumCtx->getTopics();
|
$forumTopics = $forumCtx->getTopics();
|
||||||
|
@ -291,7 +291,11 @@ $postInfos = $forumPosts->getPosts(
|
||||||
if(empty($postInfos))
|
if(empty($postInfos))
|
||||||
Template::throwError(404);
|
Template::throwError(404);
|
||||||
|
|
||||||
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
|
try {
|
||||||
|
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
Template::throwError(404);
|
||||||
|
}
|
||||||
|
|
||||||
$posts = [];
|
$posts = [];
|
||||||
|
|
||||||
|
@ -326,7 +330,7 @@ $perms = $perms->checkMany([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Template::render('forum.topic', [
|
Template::render('forum.topic', [
|
||||||
'topic_breadcrumbs' => $forumCategories->getCategoryAncestry($topicInfo),
|
'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
|
||||||
'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
|
'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
|
||||||
'topic_info' => $topicInfo,
|
'topic_info' => $topicInfo,
|
||||||
'category_info' => $categoryInfo,
|
'category_info' => $categoryInfo,
|
||||||
|
|
|
@ -3,9 +3,9 @@ namespace Misuzu;
|
||||||
|
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\DateTime;
|
|
||||||
use Index\XArray;
|
|
||||||
use Misuzu\Changelog\Changelog;
|
use Misuzu\Changelog\Changelog;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Index\{XArray,XDateTime};
|
||||||
|
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
|
if(!$authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
|
||||||
|
@ -15,7 +15,7 @@ $changeActions = [];
|
||||||
foreach(Changelog::ACTIONS as $action)
|
foreach(Changelog::ACTIONS as $action)
|
||||||
$changeActions[$action] = Changelog::actionText($action);
|
$changeActions[$action] = Changelog::actionText($action);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$changelog = $msz->getChangelog();
|
$changelog = $msz->getChangelog();
|
||||||
$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$changeInfo = null;
|
$changeInfo = null;
|
||||||
|
@ -58,8 +58,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
if(empty($createdAt))
|
if(empty($createdAt))
|
||||||
$createdAt = null;
|
$createdAt = null;
|
||||||
else {
|
else {
|
||||||
$createdAt = DateTime::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z');
|
$createdAt = CarbonImmutable::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z');
|
||||||
if($createdAt->getUnixTimeSeconds() < 0)
|
if((int)$createdAt->format('U') < 0)
|
||||||
$createdAt = null;
|
$createdAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
$summary = null;
|
$summary = null;
|
||||||
if($body === $changeInfo->getBody())
|
if($body === $changeInfo->getBody())
|
||||||
$body = null;
|
$body = null;
|
||||||
if($createdAt !== null && $createdAt->equals($changeInfo->getCreatedAt()))
|
if($createdAt !== null && XDateTime::compare($createdAt, $changeInfo->getCreatedAt()) === 0)
|
||||||
$createdAt = null;
|
$createdAt = null;
|
||||||
$updateUserInfo = $userId !== $changeInfo->getUserId();
|
$updateUserInfo = $userId !== $changeInfo->getUserId();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use RuntimeException;
|
||||||
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
|
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$changelog = $msz->getChangelog();
|
$changelog = $msz->getChangelog();
|
||||||
$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
|
$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$loadTagInfo = fn() => $changelog->getTag($tagId);
|
$loadTagInfo = fn() => $changelog->getTag($tagId);
|
||||||
|
|
|
@ -5,7 +5,7 @@ $authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
|
if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$forumCtx = $msz->getForumContext();
|
$forumCtx = $msz->getForumContext();
|
||||||
$forumTopicRedirects = $forumCtx->getTopicRedirects();
|
$forumTopicRedirects = $forumCtx->getTopicRedirects();
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ else
|
||||||
try {
|
try {
|
||||||
$isNew = false;
|
$isNew = false;
|
||||||
$emoteInfo = $emotes->getEmote($emoteId);
|
$emoteInfo = $emotes->getEmote($emoteId);
|
||||||
$emoteStrings = $emotes->getEmoteStrings($emoteInfo);
|
$emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
Template::throwError(404);
|
Template::throwError(404);
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
[$emoteInfo->getId()]
|
[$emoteInfo->getId()]
|
||||||
);
|
);
|
||||||
|
|
||||||
Tools::redirect($msz->getURLs()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
|
Tools::redirect($msz->getUrls()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tools::redirect($msz->getURLs()->format('manage-general-emoticons'));
|
Tools::redirect($msz->getUrls()->format('manage-general-emoticons'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ $pagination = new Pagination($auditLog->countLogs(), 50);
|
||||||
if(!$pagination->hasValidOffset())
|
if(!$pagination->hasValidOffset())
|
||||||
Template::throwError(404);
|
Template::throwError(404);
|
||||||
|
|
||||||
$logs = $auditLog->getLogs(pagination: $pagination);
|
$logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
|
||||||
$userInfos = [];
|
$userInfos = [];
|
||||||
$userColours = [];
|
$userColours = [];
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
$valueName = $valueInfo->getName();
|
$valueName = $valueInfo->getName();
|
||||||
$msz->createAuditLog('CONFIG_DELETE', [$valueName]);
|
$msz->createAuditLog('CONFIG_DELETE', [$valueName]);
|
||||||
$cfg->removeValues($valueName);
|
$cfg->removeValues($valueName);
|
||||||
Tools::redirect($msz->getURLs()->format('manage-general-settings'));
|
Tools::redirect($msz->getUrls()->format('manage-general-settings'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use Syokuhou\DbConfig;
|
use Index\Config\Db\DbConfig;
|
||||||
|
|
||||||
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
|
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
@ -73,7 +73,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
|
|
||||||
$msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
|
$msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
|
||||||
$applyFunc($sName, $sValue);
|
$applyFunc($sName, $sValue);
|
||||||
Tools::redirect($msz->getURLs()->format('manage-general-settings'));
|
Tools::redirect($msz->getUrls()->format('manage-general-settings'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use RuntimeException;
|
||||||
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
|
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$news = $msz->getNews();
|
$news = $msz->getNews();
|
||||||
$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);
|
$loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);
|
||||||
|
|
|
@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
|
if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$news = $msz->getNews();
|
$news = $msz->getNews();
|
||||||
$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
|
$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$loadPostInfo = fn() => $news->getPost($postId);
|
$loadPostInfo = fn() => $news->getPost($postId);
|
||||||
|
|
|
@ -3,13 +3,13 @@ namespace Misuzu;
|
||||||
|
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\DateTime;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
|
if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$bans = $usersCtx->getBans();
|
$bans = $usersCtx->getBans();
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
if($expires === -1) {
|
if($expires === -1) {
|
||||||
$expires = null;
|
$expires = null;
|
||||||
} elseif($expires === -2) {
|
} elseif($expires === -2) {
|
||||||
$expires = DateTime::createFromFormat(DateTimeInterface::ATOM, $expiresCustom . ':00Z');
|
$expires = CarbonImmutable::createFromFormat(DateTimeInterface::ATOM, $expiresCustom . ':00Z');
|
||||||
} else {
|
} else {
|
||||||
echo 'Invalid duration specified.';
|
echo 'Invalid duration specified.';
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -13,7 +13,7 @@ $hasUserId = filter_has_var(INPUT_GET, 'u');
|
||||||
if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
|
if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
|
||||||
Template::throwError(400);
|
Template::throwError(400);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$modNotes = $usersCtx->getModNotes();
|
$modNotes = $usersCtx->getModNotes();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ namespace Misuzu;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Colour\Colour;
|
use Index\Colour\Colour;
|
||||||
use Index\Colour\ColourRGB;
|
use Index\Colour\ColourRgb;
|
||||||
use Misuzu\Perm;
|
use Misuzu\Perm;
|
||||||
|
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
|
@ -42,6 +42,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$roleString = (string)filter_input(INPUT_POST, 'ur_string');
|
||||||
$roleName = (string)filter_input(INPUT_POST, 'ur_name');
|
$roleName = (string)filter_input(INPUT_POST, 'ur_name');
|
||||||
$roleHide = !empty($_POST['ur_hidden']);
|
$roleHide = !empty($_POST['ur_hidden']);
|
||||||
$roleLeavable = !empty($_POST['ur_leavable']);
|
$roleLeavable = !empty($_POST['ur_leavable']);
|
||||||
|
@ -54,6 +55,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
$colourBlue = (int)filter_input(INPUT_POST, 'ur_col_blue', FILTER_SANITIZE_NUMBER_INT);
|
$colourBlue = (int)filter_input(INPUT_POST, 'ur_col_blue', FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
|
||||||
Template::set([
|
Template::set([
|
||||||
|
'role_ur_string' => $roleString,
|
||||||
'role_ur_name' => $roleName,
|
'role_ur_name' => $roleName,
|
||||||
'role_ur_hidden' => $roleHide,
|
'role_ur_hidden' => $roleHide,
|
||||||
'role_ur_leavable' => $roleLeavable,
|
'role_ur_leavable' => $roleLeavable,
|
||||||
|
@ -84,7 +86,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
|
|
||||||
$roleColour = $colourInherit
|
$roleColour = $colourInherit
|
||||||
? Colour::none()
|
? Colour::none()
|
||||||
: new ColourRGB($colourRed, $colourGreen, $colourBlue);
|
: new ColourRgb($colourRed, $colourGreen, $colourBlue);
|
||||||
|
|
||||||
if(mb_strlen($roleDesc) > 1000) {
|
if(mb_strlen($roleDesc) > 1000) {
|
||||||
echo 'Description may not be longer than 1000 characters.';
|
echo 'Description may not be longer than 1000 characters.';
|
||||||
|
@ -96,11 +98,31 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(strlen($roleString) > 20) {
|
||||||
|
echo 'Role string may not be longer than 20 characters.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(strlen($roleString) > 1 && !ctype_alpha($roleString[0])) {
|
||||||
|
echo 'Role string most start with an alphabetical character.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if($isNew) {
|
if($isNew) {
|
||||||
$roleInfo = $roles->createRole($roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
|
$roleInfo = $roles->createRole(
|
||||||
|
$roleName,
|
||||||
|
$roleRank,
|
||||||
|
$roleColour,
|
||||||
|
string: $roleString,
|
||||||
|
title: $roleTitle,
|
||||||
|
description: $roleDesc,
|
||||||
|
hidden: $roleHide,
|
||||||
|
leavable: $roleLeavable
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if($roleName === $roleInfo->getName())
|
if($roleName === $roleInfo->getName())
|
||||||
$roleName = null;
|
$roleName = null;
|
||||||
|
if($roleString === $roleInfo->getString())
|
||||||
|
$roleString = null;
|
||||||
if($roleHide === $roleInfo->isHidden())
|
if($roleHide === $roleInfo->isHidden())
|
||||||
$roleHide = null;
|
$roleHide = null;
|
||||||
if($roleLeavable === $roleInfo->isLeavable())
|
if($roleLeavable === $roleInfo->isLeavable())
|
||||||
|
@ -115,7 +137,17 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
if((string)$roleColour === (string)$roleInfo->getColour())
|
if((string)$roleColour === (string)$roleInfo->getColour())
|
||||||
$roleColour = null;
|
$roleColour = null;
|
||||||
|
|
||||||
$roles->updateRole($roleInfo, $roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
|
$roles->updateRole(
|
||||||
|
$roleInfo,
|
||||||
|
string: $roleString,
|
||||||
|
name: $roleName,
|
||||||
|
rank: $roleRank,
|
||||||
|
colour: $roleColour,
|
||||||
|
title: $roleTitle,
|
||||||
|
description: $roleDesc,
|
||||||
|
hidden: $roleHide,
|
||||||
|
leavable: $roleLeavable
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$msz->createAuditLog(
|
$msz->createAuditLog(
|
||||||
|
@ -136,7 +168,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
$msz->getConfig()->setBoolean('perms.needsRecalc', true);
|
$msz->getConfig()->setBoolean('perms.needsRecalc', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tools::redirect($msz->getURLs()->format('manage-role', ['role' => $roleInfo->getId()]));
|
Tools::redirect($msz->getUrls()->format('manage-role', ['role' => $roleInfo->getId()]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ $viewerPerms = $authInfo->getPerms('user');
|
||||||
if(!$authInfo->isLoggedIn())
|
if(!$authInfo->isLoggedIn())
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$users = $usersCtx->getUsers();
|
$users = $usersCtx->getUsers();
|
||||||
$roles = $usersCtx->getRoles();
|
$roles = $usersCtx->getRoles();
|
||||||
|
@ -106,7 +106,7 @@ if(CSRF::validateRequest() && $canEdit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$existingRoles = [];
|
$existingRoles = [];
|
||||||
foreach($roles->getRoles(userInfo: $userInfo) as $roleInfo)
|
foreach(iterator_to_array($roles->getRoles(userInfo: $userInfo)) as $roleInfo)
|
||||||
$existingRoles[$roleInfo->getId()] = $roleInfo;
|
$existingRoles[$roleInfo->getId()] = $roleInfo;
|
||||||
|
|
||||||
$removeRoles = [];
|
$removeRoles = [];
|
||||||
|
@ -226,7 +226,7 @@ if(CSRF::validateRequest() && $canEdit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$rolesAll = $roles->getRoles();
|
$rolesAll = iterator_to_array($roles->getRoles());
|
||||||
$userRoleIds = $users->hasRoles($userInfo, $rolesAll);
|
$userRoleIds = $users->hasRoles($userInfo, $rolesAll);
|
||||||
|
|
||||||
Template::render('manage.users.user', [
|
Template::render('manage.users.user', [
|
||||||
|
|
|
@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
|
||||||
if(!$authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
|
if(!$authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$users = $usersCtx->getUsers();
|
$users = $usersCtx->getUsers();
|
||||||
$warns = $usersCtx->getWarnings();
|
$warns = $usersCtx->getWarnings();
|
||||||
|
|
|
@ -5,7 +5,6 @@ use stdClass;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\ByteFormat;
|
use Index\ByteFormat;
|
||||||
use Index\DateTime;
|
|
||||||
use Misuzu\Parsers\Parser;
|
use Misuzu\Parsers\Parser;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
use Misuzu\Users\Assets\UserAvatarAsset;
|
use Misuzu\Users\Assets\UserAvatarAsset;
|
||||||
|
@ -15,7 +14,7 @@ $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
|
||||||
$profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
$profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||||
$isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
|
$isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
|
||||||
|
|
||||||
$urls = $msz->getURLs();
|
$urls = $msz->getUrls();
|
||||||
$usersCtx = $msz->getUsersContext();
|
$usersCtx = $msz->getUsersContext();
|
||||||
$users = $usersCtx->getUsers();
|
$users = $usersCtx->getUsers();
|
||||||
$forumCtx = $msz->getForumContext();
|
$forumCtx = $msz->getForumContext();
|
||||||
|
@ -71,15 +70,16 @@ $notices = [];
|
||||||
$userRank = $usersCtx->getUserRank($userInfo);
|
$userRank = $usersCtx->getUserRank($userInfo);
|
||||||
$viewerRank = $usersCtx->getUserRank($viewerInfo);
|
$viewerRank = $usersCtx->getUserRank($viewerInfo);
|
||||||
|
|
||||||
$viewerPerms = $authInfo->getPerms('user');
|
$viewerPermsGlobal = $authInfo->getPerms('global');
|
||||||
|
$viewerPermsUser = $authInfo->getPerms('user');
|
||||||
|
|
||||||
$activeBanInfo = $usersCtx->tryGetActiveBan($userInfo);
|
$activeBanInfo = $usersCtx->tryGetActiveBan($userInfo);
|
||||||
$isBanned = $activeBanInfo !== null;
|
$isBanned = $activeBanInfo !== null;
|
||||||
$profileFields = $msz->getProfileFields();
|
$profileFields = $msz->getProfileFields();
|
||||||
$viewingOwnProfile = (string)$viewerId === $userInfo->getId();
|
$viewingOwnProfile = (string)$viewerId === $userInfo->getId();
|
||||||
$canManageWarnings = $viewerPerms->check(Perm::U_WARNINGS_MANAGE);
|
$canManageWarnings = $viewerPermsUser->check(Perm::U_WARNINGS_MANAGE);
|
||||||
$canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->isSuperUser() || (
|
$canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->isSuperUser() || (
|
||||||
$viewerPerms->check(Perm::U_USERS_MANAGE) && ($viewingOwnProfile || $viewerRank > $userRank)
|
$viewerPermsUser->check(Perm::U_USERS_MANAGE) && ($viewingOwnProfile || $viewerRank > $userRank)
|
||||||
));
|
));
|
||||||
$avatarInfo = new UserAvatarAsset($userInfo);
|
$avatarInfo = new UserAvatarAsset($userInfo);
|
||||||
$backgroundInfo = new UserBackgroundAsset($userInfo);
|
$backgroundInfo = new UserBackgroundAsset($userInfo);
|
||||||
|
@ -88,7 +88,7 @@ if($isEditing) {
|
||||||
if(!$canEdit)
|
if(!$canEdit)
|
||||||
Template::throwError(403);
|
Template::throwError(403);
|
||||||
|
|
||||||
$perms = $viewerPerms->checkMany([
|
$perms = $viewerPermsUser->checkMany([
|
||||||
'edit_profile' => Perm::U_PROFILE_EDIT,
|
'edit_profile' => Perm::U_PROFILE_EDIT,
|
||||||
'edit_avatar' => Perm::U_AVATAR_CHANGE,
|
'edit_avatar' => Perm::U_AVATAR_CHANGE,
|
||||||
'edit_background' => PERM::U_PROFILE_BACKGROUND_CHANGE,
|
'edit_background' => PERM::U_PROFILE_BACKGROUND_CHANGE,
|
||||||
|
@ -112,7 +112,7 @@ if($isEditing) {
|
||||||
if(!$perms->edit_profile) {
|
if(!$perms->edit_profile) {
|
||||||
$notices[] = 'You\'re not allowed to edit your profile';
|
$notices[] = 'You\'re not allowed to edit your profile';
|
||||||
} else {
|
} else {
|
||||||
$profileFieldInfos = $profileFields->getFields();
|
$profileFieldInfos = iterator_to_array($profileFields->getFields());
|
||||||
$profileFieldsSetInfos = [];
|
$profileFieldsSetInfos = [];
|
||||||
$profileFieldsSetValues = [];
|
$profileFieldsSetValues = [];
|
||||||
$profileFieldsRemove = [];
|
$profileFieldsRemove = [];
|
||||||
|
@ -298,7 +298,7 @@ $profileStats->forum_post_count = $forumCtx->countTotalUserPosts($userInfo);
|
||||||
$profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
|
$profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
|
||||||
|
|
||||||
if(!$viewingAsGuest) {
|
if(!$viewingAsGuest) {
|
||||||
Template::set('profile_warnings', $usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo));
|
Template::set('profile_warnings', iterator_to_array($usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)));
|
||||||
|
|
||||||
if((!$isBanned || $canEdit)) {
|
if((!$isBanned || $canEdit)) {
|
||||||
$unranked = $cfg->getValues([
|
$unranked = $cfg->getValues([
|
||||||
|
@ -322,9 +322,9 @@ if(!$viewingAsGuest) {
|
||||||
);
|
);
|
||||||
$activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null;
|
$activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null;
|
||||||
|
|
||||||
$profileFieldValues = $profileFields->getFieldValues($userInfo);
|
$profileFieldValues = iterator_to_array($profileFields->getFieldValues($userInfo));
|
||||||
$profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues);
|
$profileFieldInfos = $profileFieldInfos ?? iterator_to_array($profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
|
||||||
$profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues);
|
$profileFieldFormats = iterator_to_array($profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
|
||||||
|
|
||||||
$profileFieldRawValues = [];
|
$profileFieldRawValues = [];
|
||||||
$profileFieldLinkValues = [];
|
$profileFieldLinkValues = [];
|
||||||
|
@ -384,4 +384,5 @@ Template::render('profile.index', [
|
||||||
'profile_ban_info' => $activeBanInfo,
|
'profile_ban_info' => $activeBanInfo,
|
||||||
'profile_avatar_info' => $avatarInfo,
|
'profile_avatar_info' => $avatarInfo,
|
||||||
'profile_background_info' => $backgroundInfo,
|
'profile_background_info' => $backgroundInfo,
|
||||||
|
'profile_can_send_messages' => $viewerPermsGlobal->check(Perm::G_MESSAGES_SEND),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -116,7 +116,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
|
||||||
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
|
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
|
||||||
$userInfo = $users->getUser($userInfo->getId(), 'id');
|
$userInfo = $users->getUser($userInfo->getId(), 'id');
|
||||||
|
|
||||||
$userRoles = $roles->getRoles(userInfo: $userInfo);
|
$userRoles = iterator_to_array($roles->getRoles(userInfo: $userInfo));
|
||||||
|
|
||||||
Template::render('settings.account', [
|
Template::render('settings.account', [
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
|
|
|
@ -3,7 +3,6 @@ namespace Misuzu;
|
||||||
|
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
use Index\XString;
|
use Index\XString;
|
||||||
use Index\IO\FileStream;
|
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
|
||||||
$authInfo = $msz->getAuthInfo();
|
$authInfo = $msz->getAuthInfo();
|
||||||
|
@ -42,7 +41,7 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%s-%s-%s.tmp', $userId, $baseName, XString::random(8));
|
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%s-%s-%s.tmp', $userId, $baseName, XString::random(8));
|
||||||
$tmpStream = FileStream::newWrite($tmpName);
|
$tmpHandle = fopen($tmpName, 'wb');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $dbConn->prepare(sprintf('SELECT %s FROM msz_%s WHERE %s = ?', implode(', ', $fields), $baseName, $userIdField));
|
$stmt = $dbConn->prepare(sprintf('SELECT %s FROM msz_%s WHERE %s = ?', implode(', ', $fields), $baseName, $userIdField));
|
||||||
|
@ -85,12 +84,12 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
|
||||||
$row[$fieldInfo['name']] = $fieldValue;
|
$row[$fieldInfo['name']] = $fieldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpStream->write(json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
|
fwrite($tmpHandle, json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
|
||||||
$tmpStream->write("\n");
|
fwrite($tmpHandle, "\n");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$tmpStream->flush();
|
fflush($tmpHandle);
|
||||||
$tmpStream->close();
|
fclose($tmpHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
$archive->addFile($tmpName, $baseName . '.jsonl');
|
$archive->addFile($tmpName, $baseName . '.jsonl');
|
||||||
|
@ -129,6 +128,7 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']);
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']);
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']);
|
||||||
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'messages', ['msg_id:s', 'msg_owner_id:s', 'msg_author_id:s:n', 'msg_recipient_id:s:n', 'msg_reply_to:s:n', 'msg_title:s', 'msg_body:s', 'msg_parser:i', 'msg_created:t', 'msg_sent:t:n', 'msg_read:t:n', 'msg_deleted:t:n'], 'msg_owner_id');
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']);
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms', ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms', ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']);
|
||||||
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated', ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']);
|
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated', ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']);
|
||||||
|
|
|
@ -15,8 +15,8 @@ $auditLog = $msz->getAuditLog();
|
||||||
$loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
|
$loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
|
||||||
$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
|
$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
|
||||||
|
|
||||||
$loginHistory = $loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination);
|
$loginHistory = iterator_to_array($loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
|
||||||
$auditLogs = $auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination);
|
$auditLogs = iterator_to_array($auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
|
||||||
|
|
||||||
Template::render('settings.logs', [
|
Template::render('settings.logs', [
|
||||||
'login_history_list' => $loginHistory,
|
'login_history_list' => $loginHistory,
|
||||||
|
|
|
@ -37,7 +37,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if($activeSessionKilled) {
|
if($activeSessionKilled) {
|
||||||
Tools::redirect($msz->getURLs()->format('index'));
|
Tools::redirect($msz->getUrls()->format('index'));
|
||||||
return;
|
return;
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Misuzu\Auth\AuthTokenBuilder;
|
use Misuzu\Auth\{AuthTokenBuilder,AuthTokenCookie,AuthTokenInfo};
|
||||||
use Misuzu\Auth\AuthTokenCookie;
|
|
||||||
use Misuzu\Auth\AuthTokenInfo;
|
|
||||||
use Sasae\SasaeEnvironment;
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../misuzu.php';
|
require_once __DIR__ . '/../misuzu.php';
|
||||||
|
|
||||||
|
|
|
@ -2,28 +2,23 @@
|
||||||
namespace Misuzu\AuditLog;
|
namespace Misuzu\AuditLog;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\Db\{DbConnection,DbStatementCache};
|
||||||
use Index\Data\IDbConnection;
|
|
||||||
use Index\Data\IDbResult;
|
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
|
||||||
class AuditLog {
|
class AuditLog {
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(DbConnection $dbConn) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function countLogs(
|
public function countLogs(
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
IPAddress|string|null $remoteAddr = null
|
?string $remoteAddr = null
|
||||||
): int {
|
): int {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
$hasUserInfo = $userInfo !== null;
|
$hasUserInfo = $userInfo !== null;
|
||||||
$hasRemoteAddr = $remoteAddr !== null;
|
$hasRemoteAddr = $remoteAddr !== null;
|
||||||
|
@ -59,13 +54,11 @@ class AuditLog {
|
||||||
|
|
||||||
public function getLogs(
|
public function getLogs(
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
IPAddress|string|null $remoteAddr = null,
|
?string $remoteAddr = null,
|
||||||
?Pagination $pagination = null
|
?Pagination $pagination = null
|
||||||
): array {
|
): iterable {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
$hasUserInfo = $userInfo !== null;
|
$hasUserInfo = $userInfo !== null;
|
||||||
$hasRemoteAddr = $remoteAddr !== null;
|
$hasRemoteAddr = $remoteAddr !== null;
|
||||||
|
@ -98,26 +91,19 @@ class AuditLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$result = $stmt->getResult();
|
|
||||||
$logs = [];
|
|
||||||
|
|
||||||
while($result->next())
|
return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...));
|
||||||
$logs[] = new AuditLogInfo($result);
|
|
||||||
|
|
||||||
return $logs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createLog(
|
public function createLog(
|
||||||
UserInfo|string|null $userInfo,
|
UserInfo|string|null $userInfo,
|
||||||
string $action,
|
string $action,
|
||||||
array $params = [],
|
array $params = [],
|
||||||
IPAddress|string $remoteAddr = '::1',
|
string $remoteAddr = '::1',
|
||||||
string $countryCode = 'XX'
|
string $countryCode = 'XX'
|
||||||
): void {
|
): void {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
// action names should have stricter validation,
|
// action names should have stricter validation,
|
||||||
// i do want to switch to a lowercase colon separated format later but i'll save that for the unified log in Hanyuu
|
// i do want to switch to a lowercase colon separated format later but i'll save that for the unified log in Hanyuu
|
||||||
|
|
|
@ -2,25 +2,28 @@
|
||||||
namespace Misuzu\AuditLog;
|
namespace Misuzu\AuditLog;
|
||||||
|
|
||||||
use ValueError;
|
use ValueError;
|
||||||
use Index\DateTime;
|
use Carbon\CarbonImmutable;
|
||||||
use Index\Data\IDbResult;
|
use Index\Db\DbResult;
|
||||||
use Index\Net\IPAddress;
|
|
||||||
|
|
||||||
class AuditLogInfo {
|
class AuditLogInfo {
|
||||||
private ?string $userId;
|
public function __construct(
|
||||||
private string $action;
|
private ?string $userId,
|
||||||
private array $params;
|
private string $action,
|
||||||
private int $created;
|
private array $params,
|
||||||
private string $address;
|
private int $created,
|
||||||
private string $country;
|
private string $address,
|
||||||
|
private string $country,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __construct(IDbResult $result) {
|
public static function fromResult(DbResult $result): AuditLogInfo {
|
||||||
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
|
return new AuditLogInfo(
|
||||||
$this->action = $result->getString(1);
|
userId: $result->getStringOrNull(0),
|
||||||
$this->params = json_decode($result->getString(2));
|
action: $result->getString(1),
|
||||||
$this->created = $result->getInteger(3);
|
params: json_decode($result->getString(2)),
|
||||||
$this->address = $result->isNull(4) ? '::1' : $result->getString(4); // apparently this being NULL is possible?
|
created: $result->getInteger(3),
|
||||||
$this->country = $result->getString(5);
|
address: $result->isNull(4) ? '::1' : $result->getString(4), // apparently this being NULL is possible?
|
||||||
|
country: $result->getString(5),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasUserId(): bool {
|
public function hasUserId(): bool {
|
||||||
|
@ -43,18 +46,14 @@ class AuditLogInfo {
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): DateTime {
|
public function getCreatedAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->created);
|
return CarbonImmutable::createFromTimestampUTC($this->created);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddressRaw(): string {
|
public function getRemoteAddress(): string {
|
||||||
return $this->address;
|
return $this->address;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddress(): IPAddress {
|
|
||||||
return IPAddress::parse($this->address);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCountryCode(): string {
|
public function getCountryCode(): string {
|
||||||
return $this->country;
|
return $this->country;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\Data\IDbConnection;
|
use Index\Config\Config;
|
||||||
use Syokuhou\IConfig;
|
use Index\Db\DbConnection;
|
||||||
|
|
||||||
class AuthContext {
|
class AuthContext {
|
||||||
private Sessions $sessions;
|
private Sessions $sessions;
|
||||||
|
@ -10,9 +10,9 @@ class AuthContext {
|
||||||
private RecoveryTokens $recoveryTokens;
|
private RecoveryTokens $recoveryTokens;
|
||||||
private TwoFactorAuthSessions $tfaSessions;
|
private TwoFactorAuthSessions $tfaSessions;
|
||||||
|
|
||||||
private IConfig $config;
|
private Config $config;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn, IConfig $config) {
|
public function __construct(DbConnection $dbConn, Config $config) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->sessions = new Sessions($dbConn);
|
$this->sessions = new Sessions($dbConn);
|
||||||
$this->loginAttempts = new LoginAttempts($dbConn);
|
$this->loginAttempts = new LoginAttempts($dbConn);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\XArray;
|
|
||||||
use Misuzu\Auth\SessionInfo;
|
use Misuzu\Auth\SessionInfo;
|
||||||
use Misuzu\Forum\ForumCategoryInfo;
|
use Misuzu\Forum\ForumCategoryInfo;
|
||||||
use Misuzu\Perms\IPermissionResult;
|
use Misuzu\Perms\IPermissionResult;
|
||||||
|
|
65
src/Auth/AuthRpcHandler.php
Normal file
65
src/Auth/AuthRpcHandler.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Misuzu\Users\{UsersContext,UserInfo};
|
||||||
|
use RPCii\Server\{RpcHandler,RpcHandlerCommon,RpcAction};
|
||||||
|
use Index\Config\Config;
|
||||||
|
|
||||||
|
final class AuthRpcHandler implements RpcHandler {
|
||||||
|
use RpcHandlerCommon;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Config $impersonateConfig,
|
||||||
|
private UsersContext $usersCtx,
|
||||||
|
private AuthContext $authCtx
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
|
||||||
|
if($impersonator->isSuperUser())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
|
||||||
|
return in_array($targetId, $whitelist, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RpcAction('misuzu:auth:attemptMisuzuAuth')]
|
||||||
|
public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array {
|
||||||
|
$tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);
|
||||||
|
if(!$tokenInfo->isEmpty())
|
||||||
|
$token = $tokenInfo->getSessionToken();
|
||||||
|
|
||||||
|
$sessions = $this->authCtx->getSessions();
|
||||||
|
try {
|
||||||
|
$sessionInfo = $sessions->getSession(sessionToken: $token);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return ['method' => 'misuzu', 'error' => 'token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if($sessionInfo->hasExpired()) {
|
||||||
|
$sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||||
|
return ['method' => 'misuzu', 'error' => 'expired'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
|
||||||
|
|
||||||
|
$users = $this->usersCtx->getUsers();
|
||||||
|
$userInfo = $users->getUser($sessionInfo->getUserId(), 'id');
|
||||||
|
if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
|
||||||
|
$userInfoReal = $userInfo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
$userInfo = $userInfoReal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'method' => 'misuzu',
|
||||||
|
'type' => 'user',
|
||||||
|
'user' => $userInfo->getId(),
|
||||||
|
'expires' => $sessionInfo->getExpiresTime(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,7 @@
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\IO\MemoryStream;
|
use Index\UriBase64;
|
||||||
use Index\Serialisation\UriBase64;
|
|
||||||
|
|
||||||
class AuthTokenPacker {
|
class AuthTokenPacker {
|
||||||
private const EPOCH_V2 = 1682985600;
|
private const EPOCH_V2 = 1682985600;
|
||||||
|
@ -64,33 +63,41 @@ class AuthTokenPacker {
|
||||||
|
|
||||||
$unpackTime = unpack('Nts', $timestamp);
|
$unpackTime = unpack('Nts', $timestamp);
|
||||||
if($unpackTime === false)
|
if($unpackTime === false)
|
||||||
throw new RuntimeException('$token does not contain a valid timestamp.');
|
return AuthTokenInfo::empty();
|
||||||
|
|
||||||
$timestamp = $unpackTime['ts'] + self::EPOCH_V2;
|
$timestamp = $unpackTime['ts'] + self::EPOCH_V2;
|
||||||
|
|
||||||
$stream = MemoryStream::fromString($data);
|
$handle = fopen('php://memory', 'rb+');
|
||||||
$stream->seek(0);
|
if($handle === false)
|
||||||
|
return AuthTokenInfo::empty();
|
||||||
|
|
||||||
|
try {
|
||||||
|
fwrite($handle, $data);
|
||||||
|
fseek($handle, 0);
|
||||||
|
|
||||||
for(;;) {
|
for(;;) {
|
||||||
$length = $stream->readChar();
|
$length = fgetc($handle);
|
||||||
if($length === null)
|
if($length === false)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
$length = ord($length);
|
$length = ord($length);
|
||||||
if($length < 1)
|
if($length < 1)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
$name = $stream->read($length);
|
$name = fread($handle, $length);
|
||||||
$value = null;
|
$value = null;
|
||||||
$length = $stream->readChar();
|
$length = fgetc($handle);
|
||||||
if($length !== null) {
|
if($length !== false) {
|
||||||
$length = ord($length);
|
$length = ord($length);
|
||||||
if($length > 0)
|
if($length > 0)
|
||||||
$value = $stream->read($length);
|
$value = fread($handle, $length);
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder->setProperty($name, $value);
|
$builder->setProperty($name, $value);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
} else
|
} else
|
||||||
return AuthTokenInfo::empty();
|
return AuthTokenInfo::empty();
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,31 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\DateTime;
|
|
||||||
use Index\Data\IDbResult;
|
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Index\Db\DbResult;
|
||||||
|
|
||||||
class LoginAttemptInfo {
|
class LoginAttemptInfo {
|
||||||
private ?string $userId;
|
public function __construct(
|
||||||
private bool $success;
|
private ?string $userId,
|
||||||
private string $remoteAddr;
|
private bool $success,
|
||||||
private string $countryCode;
|
private string $remoteAddr,
|
||||||
private int $created;
|
private string $countryCode,
|
||||||
private string $userAgent;
|
private int $created,
|
||||||
private string $clientInfo;
|
private string $userAgent,
|
||||||
|
private string $clientInfo,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __construct(IDbResult $result) {
|
public static function fromResult(DbResult $result): LoginAttemptInfo {
|
||||||
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
|
return new LoginAttemptInfo(
|
||||||
$this->success = $result->getInteger(1) !== 0;
|
userId: $result->getStringOrNull(0),
|
||||||
$this->remoteAddr = $result->getString(2);
|
success: $result->getBoolean(1),
|
||||||
$this->countryCode = $result->getString(3);
|
remoteAddr: $result->getString(2),
|
||||||
$this->created = $result->getInteger(4);
|
countryCode: $result->getString(3),
|
||||||
$this->userAgent = $result->getString(5);
|
created: $result->getInteger(4),
|
||||||
$this->clientInfo = $result->getString(6);
|
userAgent: $result->getString(5),
|
||||||
|
clientInfo: $result->getString(6),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasUserId(): bool {
|
public function hasUserId(): bool {
|
||||||
|
@ -37,14 +40,10 @@ class LoginAttemptInfo {
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddressRaw(): string {
|
public function getRemoteAddress(): string {
|
||||||
return $this->remoteAddr;
|
return $this->remoteAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddress(): IPAddress {
|
|
||||||
return IPAddress::parse($this->remoteAddr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCountryCode(): string {
|
public function getCountryCode(): string {
|
||||||
return $this->countryCode;
|
return $this->countryCode;
|
||||||
}
|
}
|
||||||
|
@ -53,8 +52,8 @@ class LoginAttemptInfo {
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): DateTime {
|
public function getCreatedAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->created);
|
return CarbonImmutable::createFromTimestampUTC($this->created);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUserAgentString(): string {
|
public function getUserAgentString(): string {
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\TimeSpan;
|
use Index\Db\{DbConnection,DbStatementCache};
|
||||||
use Index\Data\DbStatementCache;
|
|
||||||
use Index\Data\IDbConnection;
|
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
@ -15,22 +12,18 @@ class LoginAttempts {
|
||||||
|
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(DbConnection $dbConn) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function countAttempts(
|
public function countAttempts(
|
||||||
?bool $success = null,
|
?bool $success = null,
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
IPAddress|string|null $remoteAddr = null,
|
?string $remoteAddr = null,
|
||||||
TimeSpan|int|null $timeRange = null
|
int|null $timeRange = null
|
||||||
): int {
|
): int {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
if($timeRange instanceof TimeSpan)
|
|
||||||
$timeRange = (int)$timeRange->totalSeconds();
|
|
||||||
|
|
||||||
$hasSuccess = $success !== null;
|
$hasSuccess = $success !== null;
|
||||||
$hasUserInfo = $userInfo !== null;
|
$hasUserInfo = $userInfo !== null;
|
||||||
|
@ -69,7 +62,7 @@ class LoginAttempts {
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function countRemainingAttempts(IPAddress|string $remoteAddr): int {
|
public function countRemainingAttempts(string $remoteAddr): int {
|
||||||
return self::REMAINING_MAX - $this->countAttempts(
|
return self::REMAINING_MAX - $this->countAttempts(
|
||||||
success: false,
|
success: false,
|
||||||
timeRange: self::REMAINING_WINDOW,
|
timeRange: self::REMAINING_WINDOW,
|
||||||
|
@ -80,16 +73,12 @@ class LoginAttempts {
|
||||||
public function getAttempts(
|
public function getAttempts(
|
||||||
?bool $success = null,
|
?bool $success = null,
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
IPAddress|string|null $remoteAddr = null,
|
?string $remoteAddr = null,
|
||||||
TimeSpan|int|null $timeRange = null,
|
?int $timeRange = null,
|
||||||
?Pagination $pagination = null
|
?Pagination $pagination = null
|
||||||
): array {
|
): iterable {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
if($timeRange instanceof TimeSpan)
|
|
||||||
$timeRange = (int)$timeRange->totalSeconds();
|
|
||||||
|
|
||||||
$hasSuccess = $success !== null;
|
$hasSuccess = $success !== null;
|
||||||
$hasUserInfo = $userInfo !== null;
|
$hasUserInfo = $userInfo !== null;
|
||||||
|
@ -127,25 +116,17 @@ class LoginAttempts {
|
||||||
}
|
}
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
$result = $stmt->getResult();
|
return $stmt->getResult()->getIterator(LoginAttemptInfo::fromResult(...));
|
||||||
$attempts = [];
|
|
||||||
|
|
||||||
while($result->next())
|
|
||||||
$attempts[] = new LoginAttemptInfo($result);
|
|
||||||
|
|
||||||
return $attempts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function recordAttempt(
|
public function recordAttempt(
|
||||||
bool $success,
|
bool $success,
|
||||||
IPAddress|string $remoteAddr,
|
string $remoteAddr,
|
||||||
string $countryCode,
|
string $countryCode,
|
||||||
string $userAgentString,
|
string $userAgentString,
|
||||||
?ClientInfo $clientInfo = null,
|
?ClientInfo $clientInfo = null,
|
||||||
UserInfo|string|null $userInfo = null
|
UserInfo|string|null $userInfo = null
|
||||||
): void {
|
): void {
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\DateTime;
|
use Carbon\CarbonImmutable;
|
||||||
use Index\Data\IDbResult;
|
use Index\Db\DbResult;
|
||||||
use Index\Net\IPAddress;
|
|
||||||
|
|
||||||
class RecoveryTokenInfo {
|
class RecoveryTokenInfo {
|
||||||
public const LIFETIME = 60 * 60;
|
public const LIFETIME = 60 * 60;
|
||||||
|
@ -13,7 +12,7 @@ class RecoveryTokenInfo {
|
||||||
private int $created;
|
private int $created;
|
||||||
private ?string $code;
|
private ?string $code;
|
||||||
|
|
||||||
public function __construct(IDbResult $result) {
|
public function __construct(DbResult $result) {
|
||||||
$this->userId = (string)$result->getInteger(0);
|
$this->userId = (string)$result->getInteger(0);
|
||||||
$this->remoteAddr = $result->getString(1);
|
$this->remoteAddr = $result->getString(1);
|
||||||
$this->created = $result->getInteger(2);
|
$this->created = $result->getInteger(2);
|
||||||
|
@ -24,28 +23,24 @@ class RecoveryTokenInfo {
|
||||||
return $this->userId;
|
return $this->userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddressRaw(): string {
|
public function getRemoteAddress(): string {
|
||||||
return $this->remoteAddr;
|
return $this->remoteAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRemoteAddress(): IPAddress {
|
|
||||||
return IPAddress::parse($this->remoteAddr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCreatedTime(): int {
|
public function getCreatedTime(): int {
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): DateTime {
|
public function getCreatedAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->created);
|
return CarbonImmutable::createFromTimestampUTC($this->created);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExpiresTime(): int {
|
public function getExpiresTime(): int {
|
||||||
return $this->created + self::LIFETIME;
|
return $this->created + self::LIFETIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExpiresAt(): DateTime {
|
public function getExpiresAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->created + self::LIFETIME);
|
return CarbonImmutable::createFromTimestampUTC($this->getExpiresTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasExpired(): bool {
|
public function hasExpired(): bool {
|
||||||
|
|
|
@ -3,10 +3,8 @@ namespace Misuzu\Auth;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\Base32;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Db\{DbConnection,DbStatementCache};
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Index\Serialisation\Base32;
|
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
@ -14,7 +12,7 @@ use Misuzu\Users\UserInfo;
|
||||||
class RecoveryTokens {
|
class RecoveryTokens {
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(DbConnection $dbConn) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,14 +23,12 @@ class RecoveryTokens {
|
||||||
|
|
||||||
public function getToken(
|
public function getToken(
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
IPAddress|string|null $remoteAddr = null,
|
?string $remoteAddr = null,
|
||||||
?string $verifyCode = null,
|
?string $verifyCode = null,
|
||||||
?bool $isUnused = null
|
?bool $isUnused = null
|
||||||
): RecoveryTokenInfo {
|
): RecoveryTokenInfo {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
$hasUserInfo = $userInfo !== null;
|
$hasUserInfo = $userInfo !== null;
|
||||||
$hasRemoteAddr = $remoteAddr !== null;
|
$hasRemoteAddr = $remoteAddr !== null;
|
||||||
|
@ -76,12 +72,11 @@ class RecoveryTokens {
|
||||||
|
|
||||||
public function createToken(
|
public function createToken(
|
||||||
UserInfo|string $userInfo,
|
UserInfo|string $userInfo,
|
||||||
IPAddress|string $remoteAddr
|
string $remoteAddr
|
||||||
): RecoveryTokenInfo {
|
): RecoveryTokenInfo {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
$verifyCode = self::generateCode();
|
$verifyCode = self::generateCode();
|
||||||
|
|
||||||
$stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)');
|
$stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)');
|
||||||
|
|
|
@ -1,38 +1,41 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\DateTime;
|
|
||||||
use Index\Data\IDbResult;
|
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Index\Db\DbResult;
|
||||||
|
|
||||||
class SessionInfo {
|
class SessionInfo {
|
||||||
private string $id;
|
public function __construct(
|
||||||
private string $userId;
|
private string $id,
|
||||||
private string $token;
|
private string $userId,
|
||||||
private string $firstRemoteAddr;
|
private string $token,
|
||||||
private ?string $lastRemoteAddr;
|
private string $firstRemoteAddr,
|
||||||
private string $userAgent;
|
private ?string $lastRemoteAddr,
|
||||||
private string $clientInfo;
|
private string $userAgent,
|
||||||
private string $countryCode;
|
private string $clientInfo,
|
||||||
private int $expires;
|
private string $countryCode,
|
||||||
private bool $bumpExpires;
|
private int $expires,
|
||||||
private int $created;
|
private bool $bumpExpires,
|
||||||
private ?int $lastActive;
|
private int $created,
|
||||||
|
private ?int $lastActive,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __construct(IDbResult $result) {
|
public static function fromResult(DbResult $result): SessionInfo {
|
||||||
$this->id = (string)$result->getInteger(0);
|
return new SessionInfo(
|
||||||
$this->userId = (string)$result->getInteger(1);
|
id: $result->getString(0),
|
||||||
$this->token = $result->getString(2);
|
userId: $result->getString(1),
|
||||||
$this->firstRemoteAddr = $result->getString(3);
|
token: $result->getString(2),
|
||||||
$this->lastRemoteAddr = $result->isNull(4) ? null : $result->getString(4);
|
firstRemoteAddr: $result->getString(3),
|
||||||
$this->userAgent = $result->getString(5);
|
lastRemoteAddr: $result->getStringOrNull(4),
|
||||||
$this->clientInfo = $result->getString(6);
|
userAgent: $result->getString(5),
|
||||||
$this->countryCode = $result->getString(7);
|
clientInfo: $result->getString(6),
|
||||||
$this->expires = $result->getInteger(8);
|
countryCode: $result->getString(7),
|
||||||
$this->bumpExpires = $result->getInteger(9) !== 0;
|
expires: $result->getInteger(8),
|
||||||
$this->created = $result->getInteger(10);
|
bumpExpires: $result->getBoolean(9),
|
||||||
$this->lastActive = $result->isNull(11) ? null : $result->getInteger(11);
|
created: $result->getInteger(10),
|
||||||
|
lastActive: $result->getIntegerOrNull(11),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): string {
|
public function getId(): string {
|
||||||
|
@ -47,26 +50,18 @@ class SessionInfo {
|
||||||
return $this->token;
|
return $this->token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFirstRemoteAddressRaw(): string {
|
public function getFirstRemoteAddress(): string {
|
||||||
return $this->firstRemoteAddr;
|
return $this->firstRemoteAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFirstRemoteAddress(): IPAddress {
|
|
||||||
return IPAddress::parse($this->firstRemoteAddr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasLastRemoteAddress(): bool {
|
public function hasLastRemoteAddress(): bool {
|
||||||
return $this->lastRemoteAddr !== null;
|
return $this->lastRemoteAddr !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastRemoteAddressRaw(): string {
|
public function getLastRemoteAddress(): ?string {
|
||||||
return $this->lastRemoteAddr;
|
return $this->lastRemoteAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastRemoteAddress(): ?IPAddress {
|
|
||||||
return $this->lastRemoteAddr === null ? null : IPAddress::parse($this->lastRemoteAddr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUserAgentString(): string {
|
public function getUserAgentString(): string {
|
||||||
return $this->userAgent;
|
return $this->userAgent;
|
||||||
}
|
}
|
||||||
|
@ -87,8 +82,8 @@ class SessionInfo {
|
||||||
return $this->expires;
|
return $this->expires;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExpiresAt(): DateTime {
|
public function getExpiresAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->expires);
|
return CarbonImmutable::createFromTimestampUTC($this->expires);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function shouldBumpExpires(): bool {
|
public function shouldBumpExpires(): bool {
|
||||||
|
@ -103,8 +98,8 @@ class SessionInfo {
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): DateTime {
|
public function getCreatedAt(): CarbonImmutable {
|
||||||
return DateTime::fromUnixTimeSeconds($this->created);
|
return CarbonImmutable::createFromTimestampUTC($this->created);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasLastActive(): bool {
|
public function hasLastActive(): bool {
|
||||||
|
@ -115,7 +110,7 @@ class SessionInfo {
|
||||||
return $this->lastActive;
|
return $this->lastActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastActiveAt(): ?DateTime {
|
public function getLastActiveAt(): ?CarbonImmutable {
|
||||||
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive);
|
return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,19 +4,16 @@ namespace Misuzu\Auth;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\XString;
|
use Index\XString;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\Db\{DbConnection,DbStatementCache,DbTools};
|
||||||
use Index\Data\DbTools;
|
|
||||||
use Index\Data\IDbConnection;
|
|
||||||
use Index\Net\IPAddress;
|
|
||||||
use Misuzu\ClientInfo;
|
use Misuzu\ClientInfo;
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
|
||||||
class Sessions {
|
class Sessions {
|
||||||
private IDbConnection $dbConn;
|
private DbConnection $dbConn;
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(DbConnection $dbConn) {
|
||||||
$this->dbConn = $dbConn;
|
$this->dbConn = $dbConn;
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +55,7 @@ class Sessions {
|
||||||
public function getSessions(
|
public function getSessions(
|
||||||
UserInfo|string|null $userInfo = null,
|
UserInfo|string|null $userInfo = null,
|
||||||
?Pagination $pagination = null
|
?Pagination $pagination = null
|
||||||
): array {
|
): iterable {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
|
|
||||||
|
@ -85,13 +82,7 @@ class Sessions {
|
||||||
}
|
}
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
$result = $stmt->getResult();
|
return $stmt->getResult()->getIterator(SessionInfo::fromResult(...));
|
||||||
$sessions = [];
|
|
||||||
|
|
||||||
while($result->next())
|
|
||||||
$sessions[] = new SessionInfo($result);
|
|
||||||
|
|
||||||
return $sessions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSession(
|
public function getSession(
|
||||||
|
@ -124,20 +115,18 @@ class Sessions {
|
||||||
if(!$result->next())
|
if(!$result->next())
|
||||||
throw new RuntimeException('Session not found.');
|
throw new RuntimeException('Session not found.');
|
||||||
|
|
||||||
return new SessionInfo($result);
|
return SessionInfo::fromResult($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createSession(
|
public function createSession(
|
||||||
UserInfo|string $userInfo,
|
UserInfo|string $userInfo,
|
||||||
IPAddress|string $remoteAddr,
|
string $remoteAddr,
|
||||||
string $countryCode,
|
string $countryCode,
|
||||||
string $userAgentString,
|
string $userAgentString,
|
||||||
?ClientInfo $clientInfo = null
|
?ClientInfo $clientInfo = null
|
||||||
): SessionInfo {
|
): SessionInfo {
|
||||||
if($userInfo instanceof UserInfo)
|
if($userInfo instanceof UserInfo)
|
||||||
$userInfo = $userInfo->getId();
|
$userInfo = $userInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
$sessionToken = self::generateToken();
|
$sessionToken = self::generateToken();
|
||||||
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
|
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
|
||||||
|
@ -249,7 +238,7 @@ class Sessions {
|
||||||
public function recordSessionActivity(
|
public function recordSessionActivity(
|
||||||
SessionInfo|string|null $sessionInfo = null,
|
SessionInfo|string|null $sessionInfo = null,
|
||||||
?string $sessionToken = null,
|
?string $sessionToken = null,
|
||||||
IPAddress|string|null $remoteAddr = null
|
?string $remoteAddr = null
|
||||||
): void {
|
): void {
|
||||||
if($sessionInfo === null && $sessionToken === null)
|
if($sessionInfo === null && $sessionToken === null)
|
||||||
throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
|
throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
|
||||||
|
@ -257,8 +246,6 @@ class Sessions {
|
||||||
throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
|
throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
|
||||||
if($sessionInfo instanceof SessionInfo)
|
if($sessionInfo instanceof SessionInfo)
|
||||||
$sessionInfo = $sessionInfo->getId();
|
$sessionInfo = $sessionInfo->getId();
|
||||||
if($remoteAddr instanceof IPAddress)
|
|
||||||
$remoteAddr = (string)$remoteAddr;
|
|
||||||
|
|
||||||
$hasSessionInfo = $sessionInfo !== null;
|
$hasSessionInfo = $sessionInfo !== null;
|
||||||
$hasSessionToken = $sessionToken !== null;
|
$hasSessionToken = $sessionToken !== null;
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
namespace Misuzu\Auth;
|
namespace Misuzu\Auth;
|
||||||
|
|
||||||
use Index\XString;
|
use Index\XString;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\Db\{DbConnection,DbStatementCache};
|
||||||
use Index\Data\IDbConnection;
|
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
|
|
||||||
class TwoFactorAuthSessions {
|
class TwoFactorAuthSessions {
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(DbConnection $dbConn) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/CSRF.php
16
src/CSRF.php
|
@ -1,13 +1,23 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use Index\Security\CSRFP;
|
use Index\CsrfToken;
|
||||||
|
|
||||||
final class CSRF {
|
final class CSRF {
|
||||||
private static CSRFP $instance;
|
private static CsrfToken $instance;
|
||||||
|
private static string $secretKey = '';
|
||||||
|
|
||||||
|
public static function create(string $identity, ?string $secretKey = null): CsrfToken {
|
||||||
|
if($secretKey === null)
|
||||||
|
$secretKey = self::$secretKey;
|
||||||
|
else
|
||||||
|
self::$secretKey = $secretKey;
|
||||||
|
|
||||||
|
return new CsrfToken($secretKey, $identity);
|
||||||
|
}
|
||||||
|
|
||||||
public static function init(string $secretKey, string $identity): void {
|
public static function init(string $secretKey, string $identity): void {
|
||||||
self::$instance = new CSRFP($secretKey, $identity);
|
self::$instance = self::create($identity, $secretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function validate(string $token, int $tolerance = -1): bool {
|
public static function validate(string $token, int $tolerance = -1): bool {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue