ghost rider
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.reports ADD COLUMN IF NOT EXISTS reporter_ip text;
|
||||||
@@ -1321,6 +1321,7 @@ ALTER SEQUENCE public.private_messages_id_seq OWNED BY public.private_messages.i
|
|||||||
CREATE TABLE public.reports (
|
CREATE TABLE public.reports (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
reporter_id integer,
|
reporter_id integer,
|
||||||
|
reporter_ip text,
|
||||||
item_id integer,
|
item_id integer,
|
||||||
comment_id integer,
|
comment_id integer,
|
||||||
user_id integer,
|
user_id integer,
|
||||||
|
|||||||
+112
-19
@@ -794,7 +794,9 @@ html[theme='paper'] a#prev {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[theme='light'] .item_title,
|
html[theme='light'] .item_title,
|
||||||
html[theme='paper'] .item_title {
|
html[theme='light'] .item-title,
|
||||||
|
html[theme='paper'] .item_title,
|
||||||
|
html[theme='paper'] .item-title {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2866,7 +2868,8 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layout-legacy .item-layout-container .item-main-content .item_title {
|
body.layout-legacy .item-layout-container .item-main-content .item_title,
|
||||||
|
body.layout-legacy .item-layout-container .item-main-content .item-title {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -3217,6 +3220,7 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
|
|
||||||
.item-layout-container .item-main-content>._204863,
|
.item-layout-container .item-main-content>._204863,
|
||||||
.item-layout-container .item-main-content>.item_title,
|
.item-layout-container .item-main-content>.item_title,
|
||||||
|
.item-layout-container .item-main-content>.item-title,
|
||||||
.item-layout-container .item-main-content>.content,
|
.item-layout-container .item-main-content>.content,
|
||||||
.item-layout-container .item-main-content>.metadata,
|
.item-layout-container .item-main-content>.metadata,
|
||||||
.item-layout-container .item-main-content #comments-container {
|
.item-layout-container .item-main-content #comments-container {
|
||||||
@@ -3231,11 +3235,13 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
/* Reset z-index in fullscreen to prevent sibling stacking contexts from showing metadata/comments over the player */
|
/* Reset z-index in fullscreen to prevent sibling stacking contexts from showing metadata/comments over the player */
|
||||||
#main:fullscreen .item-layout-container .item-main-content>._204863,
|
#main:fullscreen .item-layout-container .item-main-content>._204863,
|
||||||
#main:fullscreen .item-layout-container .item-main-content>.item_title,
|
#main:fullscreen .item-layout-container .item-main-content>.item_title,
|
||||||
|
#main:fullscreen .item-layout-container .item-main-content>.item-title,
|
||||||
#main:fullscreen .item-layout-container .item-main-content>.content,
|
#main:fullscreen .item-layout-container .item-main-content>.content,
|
||||||
#main:fullscreen .item-layout-container .item-main-content>.metadata,
|
#main:fullscreen .item-layout-container .item-main-content>.metadata,
|
||||||
#main:fullscreen .item-layout-container .item-main-content #comments-container,
|
#main:fullscreen .item-layout-container .item-main-content #comments-container,
|
||||||
#onara-modal:fullscreen .item-layout-container .item-main-content>._204863,
|
#onara-modal:fullscreen .item-layout-container .item-main-content>._204863,
|
||||||
#onara-modal:fullscreen .item-layout-container .item-main-content>.item_title,
|
#onara-modal:fullscreen .item-layout-container .item-main-content>.item_title,
|
||||||
|
#onara-modal:fullscreen .item-layout-container .item-main-content>.item-title,
|
||||||
#onara-modal:fullscreen .item-layout-container .item-main-content>.content,
|
#onara-modal:fullscreen .item-layout-container .item-main-content>.content,
|
||||||
#onara-modal:fullscreen .item-layout-container .item-main-content>.metadata,
|
#onara-modal:fullscreen .item-layout-container .item-main-content>.metadata,
|
||||||
#onara-modal:fullscreen .item-layout-container .item-main-content #comments-container {
|
#onara-modal:fullscreen .item-layout-container .item-main-content #comments-container {
|
||||||
@@ -4850,7 +4856,8 @@ html[theme='f0ck95d'] {
|
|||||||
--comment-bg: #0b0a0a;
|
--comment-bg: #0b0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .item_title {
|
html[theme="f0ck95d"] .item_title,
|
||||||
|
html[theme="f0ck95d"] .item-title {
|
||||||
background: black;
|
background: black;
|
||||||
border-left: 2px solid gray;
|
border-left: 2px solid gray;
|
||||||
border-right: 2px solid gray;
|
border-right: 2px solid gray;
|
||||||
@@ -5753,7 +5760,8 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
|
|
||||||
.content,
|
.content,
|
||||||
._204863,
|
._204863,
|
||||||
.item_title {
|
.item_title,
|
||||||
|
.item-title {
|
||||||
animation: contentSlideIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: contentSlideIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
@@ -5769,9 +5777,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.slide-next,
|
.content.slide-next,
|
||||||
._204863.slide-next,
|
._204863.slide-next,
|
||||||
.item_title.slide-next,
|
.item_title.slide-next,
|
||||||
|
.item-title.slide-next,
|
||||||
#onara-modal .content.slide-next,
|
#onara-modal .content.slide-next,
|
||||||
#onara-modal ._204863.slide-next,
|
#onara-modal ._204863.slide-next,
|
||||||
#onara-modal .item_title.slide-next {
|
#onara-modal .item_title.slide-next,
|
||||||
|
#onara-modal .item-title.slide-next {
|
||||||
animation: contentSlideInRight 0.45s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: contentSlideInRight 0.45s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
@@ -5779,9 +5789,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.slide-prev,
|
.content.slide-prev,
|
||||||
._204863.slide-prev,
|
._204863.slide-prev,
|
||||||
.item_title.slide-prev,
|
.item_title.slide-prev,
|
||||||
|
.item-title.slide-prev,
|
||||||
#onara-modal .content.slide-prev,
|
#onara-modal .content.slide-prev,
|
||||||
#onara-modal ._204863.slide-prev,
|
#onara-modal ._204863.slide-prev,
|
||||||
#onara-modal .item_title.slide-prev {
|
#onara-modal .item_title.slide-prev,
|
||||||
|
#onara-modal .item-title.slide-prev {
|
||||||
animation: contentSlideInLeft 0.45s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: contentSlideInLeft 0.45s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
@@ -5789,9 +5801,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.slide-out-left,
|
.content.slide-out-left,
|
||||||
._204863.slide-out-left,
|
._204863.slide-out-left,
|
||||||
.item_title.slide-out-left,
|
.item_title.slide-out-left,
|
||||||
|
.item-title.slide-out-left,
|
||||||
#onara-modal .content.slide-out-left,
|
#onara-modal .content.slide-out-left,
|
||||||
#onara-modal ._204863.slide-out-left,
|
#onara-modal ._204863.slide-out-left,
|
||||||
#onara-modal .item_title.slide-out-left {
|
#onara-modal .item_title.slide-out-left,
|
||||||
|
#onara-modal .item-title.slide-out-left {
|
||||||
animation: contentSlideOutLeft 0.22s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
animation: contentSlideOutLeft 0.22s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
@@ -5800,9 +5814,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.slide-out-right,
|
.content.slide-out-right,
|
||||||
._204863.slide-out-right,
|
._204863.slide-out-right,
|
||||||
.item_title.slide-out-right,
|
.item_title.slide-out-right,
|
||||||
|
.item-title.slide-out-right,
|
||||||
#onara-modal .content.slide-out-right,
|
#onara-modal .content.slide-out-right,
|
||||||
#onara-modal ._204863.slide-out-right,
|
#onara-modal ._204863.slide-out-right,
|
||||||
#onara-modal .item_title.slide-out-right {
|
#onara-modal .item_title.slide-out-right,
|
||||||
|
#onara-modal .item-title.slide-out-right {
|
||||||
animation: contentSlideOutRight 0.22s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
animation: contentSlideOutRight 0.22s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
@@ -5811,9 +5827,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.fade-in-zoom,
|
.content.fade-in-zoom,
|
||||||
._204863.fade-in-zoom,
|
._204863.fade-in-zoom,
|
||||||
.item_title.fade-in-zoom,
|
.item_title.fade-in-zoom,
|
||||||
|
.item-title.fade-in-zoom,
|
||||||
#onara-modal .content.fade-in-zoom,
|
#onara-modal .content.fade-in-zoom,
|
||||||
#onara-modal ._204863.fade-in-zoom,
|
#onara-modal ._204863.fade-in-zoom,
|
||||||
#onara-modal .item_title.fade-in-zoom {
|
#onara-modal .item_title.fade-in-zoom,
|
||||||
|
#onara-modal .item-title.fade-in-zoom {
|
||||||
animation: contentFadeInZoom 0.35s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: contentFadeInZoom 0.35s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
@@ -5821,9 +5839,11 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
.content.fade-out-zoom,
|
.content.fade-out-zoom,
|
||||||
._204863.fade-out-zoom,
|
._204863.fade-out-zoom,
|
||||||
.item_title.fade-out-zoom,
|
.item_title.fade-out-zoom,
|
||||||
|
.item-title.fade-out-zoom,
|
||||||
#onara-modal .content.fade-out-zoom,
|
#onara-modal .content.fade-out-zoom,
|
||||||
#onara-modal ._204863.fade-out-zoom,
|
#onara-modal ._204863.fade-out-zoom,
|
||||||
#onara-modal .item_title.fade-out-zoom {
|
#onara-modal .item_title.fade-out-zoom,
|
||||||
|
#onara-modal .item-title.fade-out-zoom {
|
||||||
animation: contentFadeOutZoom 0.18s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
animation: contentFadeOutZoom 0.18s cubic-bezier(0.3, 0, 0.8, 1) forwards !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
@@ -5990,11 +6010,13 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_title {
|
.item_title,
|
||||||
|
.item-title,
|
||||||
|
.item-itle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
padding: 4px 10px;
|
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
padding: 0px 0px 5px 0px;
|
||||||
color: rgba(255, 255, 255, 0.72);
|
color: rgba(255, 255, 255, 0.72);
|
||||||
font-size: 0.82em;
|
font-size: 0.82em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -6004,12 +6026,26 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
min-height: calc(1.6em + 5px) !important;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_title:empty::before {
|
.item_title_text:empty::before,
|
||||||
|
.item_title:empty::before,
|
||||||
|
.item-title:empty::before,
|
||||||
|
.item-itle:empty::before {
|
||||||
content: '\00a0';
|
content: '\00a0';
|
||||||
|
visibility: hidden;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_title:has(.info-title-edit-wrap),
|
||||||
|
.item-title:has(.info-title-edit-wrap),
|
||||||
|
.item-itle:has(.info-title-edit-wrap) {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .location {
|
/* .location {
|
||||||
@@ -12034,7 +12070,15 @@ textarea.mod-reason {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
padding: 5px;
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.mod-reason:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-msg {
|
.error-msg {
|
||||||
@@ -12044,6 +12088,47 @@ textarea.mod-reason {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-msg:not(:empty) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#report-error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 0.88em;
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255, 107, 107, 0.12);
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.35);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#report-error:empty {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-error {
|
||||||
|
border-color: #ff6b6b !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.4) !important;
|
||||||
|
animation: fieldShake 0.4s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-report-recaptcha.field-error {
|
||||||
|
border: 1px solid #ff6b6b !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.4) !important;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fieldShake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20%, 60% { transform: translateX(-6px); }
|
||||||
|
40%, 80% { transform: translateX(6px); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- notifications.html styles ---------- */
|
/* ---------- notifications.html styles ---------- */
|
||||||
.notif-history-container {
|
.notif-history-container {
|
||||||
max-width: 760px !important;
|
max-width: 760px !important;
|
||||||
@@ -17748,7 +17833,6 @@ body.scroller-active #gchat-reopen-bubble {
|
|||||||
.info-title-edit-wrap {
|
.info-title-edit-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17756,9 +17840,8 @@ body.scroller-active #gchat-reopen-bubble {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 6px 10px;
|
padding: 7px 10px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.18s ease;
|
transition: all 0.18s ease;
|
||||||
@@ -17774,7 +17857,6 @@ body.scroller-active #gchat-reopen-bubble {
|
|||||||
background: var(--accent, #9f0);
|
background: var(--accent, #9f0);
|
||||||
color: #111;
|
color: #111;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -19019,6 +19101,16 @@ body.onara-modal-open #sidebar-drag-zone {
|
|||||||
z-index: 10061 !important;
|
z-index: 10061 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.onara-modal-open .admin-bar {
|
||||||
|
z-index: 100080 !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
body.onara-modal-open .admin-bar.open {
|
||||||
|
opacity: 1 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal wrapper covers main content area with subtle dark glassmorphism, respecting sidebar */
|
/* Modal wrapper covers main content area with subtle dark glassmorphism, respecting sidebar */
|
||||||
.onara-modal-wrapper {
|
.onara-modal-wrapper {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
@@ -19204,7 +19296,8 @@ body.layout-modern #onara-modal .item-layout-container .item-main-content {
|
|||||||
|
|
||||||
#onara-modal .content:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom),
|
#onara-modal .content:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom),
|
||||||
#onara-modal ._204863:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom),
|
#onara-modal ._204863:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom),
|
||||||
#onara-modal .item_title:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom) {
|
#onara-modal .item_title:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom),
|
||||||
|
#onara-modal .item-title:not(.slide-next):not(.slide-prev):not(.slide-out-left):not(.slide-out-right):not(.fade-in-zoom):not(.fade-out-zoom) {
|
||||||
animation: contentSlideIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: contentSlideIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,11 +391,70 @@
|
|||||||
deleteEvent(e);
|
deleteEvent(e);
|
||||||
} else if (target.closest("#a_pin")) {
|
} else if (target.closest("#a_pin")) {
|
||||||
pinButtonEvent(e);
|
pinButtonEvent(e);
|
||||||
|
} else if (target.closest("#a_unavailable")) {
|
||||||
|
unavailableButtonEvent(e);
|
||||||
} else if (target.closest("#a_favo")) {
|
} else if (target.closest("#a_favo")) {
|
||||||
toggleFavEvent(e);
|
toggleFavEvent(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unavailableButtonEvent = async e => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
const ctx = getContext();
|
||||||
|
if (!ctx) return;
|
||||||
|
const { postid } = ctx;
|
||||||
|
|
||||||
|
const unavBtn = document.querySelector('#a_unavailable');
|
||||||
|
if (!unavBtn) return;
|
||||||
|
|
||||||
|
const currentVis = parseInt(unavBtn.getAttribute('data-visibility') || '0', 10);
|
||||||
|
const willBeUnavailable = currentVis !== 3;
|
||||||
|
const targetVis = willBeUnavailable ? 3 : 0;
|
||||||
|
const actionText = willBeUnavailable ? 'Make Unavailable (serves HTTP 451 to non-logged in visitors)' : 'Make Available (Public)';
|
||||||
|
|
||||||
|
const proceed = async () => {
|
||||||
|
try {
|
||||||
|
const res = await (await fetch('/api/v2/item/visibility', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ postid, id: postid, visibility: targetVis })
|
||||||
|
})).json();
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
const isNowUnav = res.visibility === 3;
|
||||||
|
unavBtn.setAttribute('data-visibility', res.visibility);
|
||||||
|
unavBtn.classList.toggle('active', isNowUnav);
|
||||||
|
unavBtn.style.color = isNowUnav ? 'var(--danger, #ff4444)' : '';
|
||||||
|
unavBtn.setAttribute('title', isNowUnav ? 'Make Available (Public)' : 'Make Unavailable (451)');
|
||||||
|
|
||||||
|
const infoVisLabel = document.getElementById('info-visibility-label');
|
||||||
|
if (infoVisLabel) {
|
||||||
|
infoVisLabel.innerHTML = isNowUnav
|
||||||
|
? '<i class="fa-solid fa-ban" style="color: var(--color-danger, #ff4444);"></i> Unavailable (451)'
|
||||||
|
: '<i class="fa-solid fa-globe" style="color: var(--color-success, #00C851);"></i> Public';
|
||||||
|
}
|
||||||
|
const infoVisBtn = document.getElementById('info-visibility-edit-btn');
|
||||||
|
if (infoVisBtn) infoVisBtn.dataset.visibility = res.visibility;
|
||||||
|
|
||||||
|
window.flashMessage(isNowUnav ? 'ITEM MARKED UNAVAILABLE (451)' : 'ITEM RESTORED TO PUBLIC');
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (res.msg || 'Failed to update visibility'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unavailable error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof ModAction !== 'undefined' && ModAction.confirm) {
|
||||||
|
ModAction.confirm('Item Visibility', `${actionText} for post <strong>#${postid}</strong>?`, proceed);
|
||||||
|
} else if (confirm(`${actionText} for post #${postid}?`)) {
|
||||||
|
proceed();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pinButtonEvent = async e => {
|
const pinButtonEvent = async e => {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
|
|||||||
@@ -2245,7 +2245,7 @@ class CommentSystem {
|
|||||||
|
|
||||||
const anonDataAttrs = isAnonGuest ? '' : `data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}"`;
|
const anonDataAttrs = isAnonGuest ? '' : `data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}"`;
|
||||||
|
|
||||||
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}>${avatarHtml}<div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${authorHtml}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" ${anonDataAttrs}>${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, isAnonGuest ? null : comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button><button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" ${anonDataAttrs}>#${comment.id}</a></div>${repliesHtml}`;
|
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}>${avatarHtml}<div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${authorHtml}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" ${anonDataAttrs}>${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, isAnonGuest ? null : comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted ? `${currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button>` : ''}<button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" ${anonDataAttrs}>#${comment.id}</a></div>${repliesHtml}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeAgo(date) {
|
timeAgo(date) {
|
||||||
|
|||||||
+220
-57
@@ -972,12 +972,53 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent left-click navigation on timestamp links
|
const fallbackCopy = (text, msg) => {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.left = "-9999px";
|
||||||
|
textarea.style.top = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand("copy");
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage(msg);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback copy failed:", err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCurrentUrl = () => {
|
||||||
|
const url = window.location.href;
|
||||||
|
const msg = (window.f0ckI18n && window.f0ckI18n.copied) || 'URL copied to clipboard';
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage(msg);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
fallbackCopy(url, msg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy(url, msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.copyCurrentUrl = copyCurrentUrl;
|
||||||
|
|
||||||
|
// Left-click on timestamp copies the current URL to clipboard and shows toast (same as pressing "y")
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.timestamp-link')) {
|
const tsTarget = e.target.closest('.timestamp-link, time.timeago');
|
||||||
|
if (tsTarget) {
|
||||||
|
if (e.button === 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
copyCurrentUrl();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -5492,26 +5533,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
"i": clickOnElementBinding("a#a_addtag"),
|
"i": clickOnElementBinding("a#a_addtag"),
|
||||||
"l": () => window.toggleBackground(),
|
"l": () => window.toggleBackground(),
|
||||||
"y": () => {
|
"y": () => {
|
||||||
const url = window.location.href;
|
copyCurrentUrl();
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(url).then(() => window.flashMessage('URL copied to clipboard'));
|
|
||||||
} else {
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.value = url;
|
|
||||||
textarea.style.position = "fixed";
|
|
||||||
textarea.style.left = "-9999px";
|
|
||||||
textarea.style.top = "0";
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.focus();
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand("copy");
|
|
||||||
window.flashMessage((window.f0ckI18n && window.f0ckI18n.copied) || 'URL copied to clipboard');
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Fallback copy failed:", err);
|
|
||||||
}
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
" ": () => {
|
" ": () => {
|
||||||
if (video && typeof video.play === 'function') { // Check if video wrapper exists/is valid
|
if (video && typeof video.play === 'function') { // Check if video wrapper exists/is valid
|
||||||
@@ -7683,8 +7705,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
document.querySelector('.messages-convo-page')
|
document.querySelector('.messages-convo-page')
|
||||||
);
|
);
|
||||||
const sel = hasDedicatedWrapper
|
const sel = hasDedicatedWrapper
|
||||||
? '.index-layout-wrapper, .meme-layout-wrapper, .messages-convo-page, .pagination-container-fluid'
|
? '.index-layout-wrapper, .meme-layout-wrapper, .messages-convo-page, .pagination-container-fluid, .admin-bar'
|
||||||
: '.pagination-container-fluid, .pagewrapper';
|
: '.pagination-container-fluid, .pagewrapper, .admin-bar';
|
||||||
return document.querySelectorAll(sel);
|
return document.querySelectorAll(sel);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7705,10 +7727,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const wrappers = getSidebarWrappers();
|
const wrappers = getSidebarWrappers();
|
||||||
if (animateWrappers) {
|
if (animateWrappers) {
|
||||||
wrappers.forEach(w => {
|
wrappers.forEach(w => {
|
||||||
const isPagination = w.classList.contains('pagination-container-fluid');
|
const isRightEdge = w.classList.contains('pagination-container-fluid') || w.classList.contains('admin-bar');
|
||||||
const isMessages = w.classList.contains('messages-convo-page');
|
const isMessages = w.classList.contains('messages-convo-page');
|
||||||
|
|
||||||
const prop = (isPagination || isMessages) ? 'right' : 'padding-right';
|
const prop = (isRightEdge || isMessages) ? 'right' : 'padding-right';
|
||||||
const targetVal = isHidden ? sidebarWidth + 'px' : '0px';
|
const targetVal = isHidden ? sidebarWidth + 'px' : '0px';
|
||||||
|
|
||||||
w.style.setProperty('transition', `${prop} 0.3s ease-in-out`, 'important');
|
w.style.setProperty('transition', `${prop} 0.3s ease-in-out`, 'important');
|
||||||
@@ -11166,6 +11188,82 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const reportUserInput = document.getElementById('report-user-id');
|
const reportUserInput = document.getElementById('report-user-id');
|
||||||
const reportReason = document.getElementById('report-reason');
|
const reportReason = document.getElementById('report-reason');
|
||||||
const reportError = document.getElementById('report-error');
|
const reportError = document.getElementById('report-error');
|
||||||
|
let _reportRcWidgetId = null;
|
||||||
|
|
||||||
|
const showReportError = (msg, targetEl) => {
|
||||||
|
if (!reportError) return;
|
||||||
|
reportError.innerHTML = '';
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.className = 'fa fa-circle-exclamation';
|
||||||
|
icon.style.marginRight = '6px';
|
||||||
|
reportError.appendChild(icon);
|
||||||
|
reportError.appendChild(document.createTextNode(' ' + msg));
|
||||||
|
reportError.style.display = 'block';
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
targetEl.classList.remove('field-error');
|
||||||
|
void targetEl.offsetWidth; // Force reflow to re-trigger CSS shake animation
|
||||||
|
targetEl.classList.add('field-error');
|
||||||
|
if (typeof targetEl.focus === 'function') {
|
||||||
|
try { targetEl.focus(); } catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearReportError = () => {
|
||||||
|
if (reportError) {
|
||||||
|
reportError.innerHTML = '';
|
||||||
|
reportError.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (reportReason) reportReason.classList.remove('field-error');
|
||||||
|
const rcEl = document.getElementById('modal-report-recaptcha');
|
||||||
|
if (rcEl) rcEl.classList.remove('field-error');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reportReason) {
|
||||||
|
reportReason.addEventListener('input', () => {
|
||||||
|
if (reportReason.value.trim()) {
|
||||||
|
reportReason.classList.remove('field-error');
|
||||||
|
const rcEl = document.getElementById('modal-report-recaptcha');
|
||||||
|
if (!rcEl || !rcEl.classList.contains('field-error')) {
|
||||||
|
clearReportError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reportReason.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = document.getElementById('report-submit');
|
||||||
|
if (submitBtn && !submitBtn.disabled) submitBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.renderReportRecaptcha = function() {
|
||||||
|
const rcEl = document.getElementById('modal-report-recaptcha');
|
||||||
|
if (!rcEl || !window.grecaptcha || typeof grecaptcha.render !== 'function') return;
|
||||||
|
if (_reportRcWidgetId !== null) {
|
||||||
|
try { grecaptcha.reset(_reportRcWidgetId); } catch(e) {}
|
||||||
|
} else {
|
||||||
|
const sitekey = rcEl.dataset.sitekey || (window.f0ckConfig && window.f0ckConfig.recaptcha_site_key);
|
||||||
|
if (sitekey) {
|
||||||
|
try {
|
||||||
|
_reportRcWidgetId = grecaptcha.render(rcEl, {
|
||||||
|
sitekey: sitekey,
|
||||||
|
theme: 'dark',
|
||||||
|
callback: function() {
|
||||||
|
rcEl.classList.remove('field-error');
|
||||||
|
if (!reportReason || !reportReason.classList.contains('field-error')) {
|
||||||
|
clearReportError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('[REPORT] Failed to render recaptcha:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Open item report
|
// Open item report
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
@@ -11176,9 +11274,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
reportCommentInput.value = '';
|
reportCommentInput.value = '';
|
||||||
reportUserInput.value = '';
|
reportUserInput.value = '';
|
||||||
reportReason.value = '';
|
reportReason.value = '';
|
||||||
reportError.textContent = '';
|
clearReportError();
|
||||||
reportModal.style.display = 'flex';
|
reportModal.style.display = 'flex';
|
||||||
document.body.classList.add('modal-open');
|
document.body.classList.add('modal-open');
|
||||||
|
setTimeout(() => { try { reportReason.focus(); } catch(err){} }, 50);
|
||||||
|
if (window.renderReportRecaptcha) window.renderReportRecaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentBtn = e.target.closest('.report-comment-btn');
|
const commentBtn = e.target.closest('.report-comment-btn');
|
||||||
@@ -11188,9 +11288,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
reportCommentInput.value = commentBtn.dataset.id;
|
reportCommentInput.value = commentBtn.dataset.id;
|
||||||
reportUserInput.value = '';
|
reportUserInput.value = '';
|
||||||
reportReason.value = '';
|
reportReason.value = '';
|
||||||
reportError.textContent = '';
|
clearReportError();
|
||||||
reportModal.style.display = 'flex';
|
reportModal.style.display = 'flex';
|
||||||
document.body.classList.add('modal-open');
|
document.body.classList.add('modal-open');
|
||||||
|
setTimeout(() => { try { reportReason.focus(); } catch(err){} }, 50);
|
||||||
|
if (window.renderReportRecaptcha) window.renderReportRecaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userBtn = e.target.closest('.report-user-btn'); // for future
|
const userBtn = e.target.closest('.report-user-btn'); // for future
|
||||||
@@ -11200,32 +11302,58 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
reportCommentInput.value = '';
|
reportCommentInput.value = '';
|
||||||
reportUserInput.value = userBtn.dataset.userId;
|
reportUserInput.value = userBtn.dataset.userId;
|
||||||
reportReason.value = '';
|
reportReason.value = '';
|
||||||
reportError.textContent = '';
|
clearReportError();
|
||||||
reportModal.style.display = 'flex';
|
reportModal.style.display = 'flex';
|
||||||
document.body.classList.add('modal-open');
|
document.body.classList.add('modal-open');
|
||||||
|
setTimeout(() => { try { reportReason.focus(); } catch(err){} }, 50);
|
||||||
|
if (window.renderReportRecaptcha) window.renderReportRecaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close logic
|
// Close logic
|
||||||
if (e.target.matches('#report-cancel') || e.target.id === 'report-modal') {
|
if (e.target.matches('#report-cancel') || e.target.id === 'report-modal') {
|
||||||
reportModal.style.display = 'none';
|
reportModal.style.display = 'none';
|
||||||
document.body.classList.remove('modal-open');
|
document.body.classList.remove('modal-open');
|
||||||
|
clearReportError();
|
||||||
|
if (_reportRcWidgetId !== null && window.grecaptcha) {
|
||||||
|
try { grecaptcha.reset(_reportRcWidgetId); } catch(e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit logic
|
// Submit logic
|
||||||
if (e.target.matches('#report-submit')) {
|
if (e.target.matches('#report-submit')) {
|
||||||
|
clearReportError();
|
||||||
const reason = reportReason.value.trim();
|
const reason = reportReason.value.trim();
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
reportError.textContent = (window.f0ckI18n && window.f0ckI18n.reason_required) || 'Please provide a reason.';
|
showReportError((window.f0ckI18n && window.f0ckI18n.reason_required) || 'Please provide a reason.', reportReason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rcEl = document.getElementById('modal-report-recaptcha');
|
||||||
|
let rcToken = '';
|
||||||
|
if (rcEl) {
|
||||||
|
if (_reportRcWidgetId === null || !window.grecaptcha) {
|
||||||
|
showReportError((window.f0ckI18n && window.f0ckI18n.captcha_loading) || 'CAPTCHA is still loading. Please wait a moment.', rcEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rcToken = grecaptcha.getResponse(_reportRcWidgetId);
|
||||||
|
if (!rcToken) {
|
||||||
|
showReportError((window.f0ckI18n && window.f0ckI18n.captcha_required) || 'Please complete the CAPTCHA.', rcEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = new URLSearchParams();
|
const payload = new URLSearchParams();
|
||||||
if (reportItemInput.value) payload.append('item_id', reportItemInput.value);
|
if (reportItemInput.value) payload.append('item_id', reportItemInput.value);
|
||||||
if (reportCommentInput.value) payload.append('comment_id', reportCommentInput.value);
|
if (reportCommentInput.value) payload.append('comment_id', reportCommentInput.value);
|
||||||
if (reportUserInput.value) payload.append('reported_user_id', reportUserInput.value);
|
if (reportUserInput.value) payload.append('reported_user_id', reportUserInput.value);
|
||||||
payload.append('reason', reason);
|
payload.append('reason', reason);
|
||||||
|
if (rcToken) payload.append('g-recaptcha-response', rcToken);
|
||||||
|
|
||||||
|
const submitBtn = e.target;
|
||||||
|
const originalBtnText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin" style="margin-right: 5px;"></i>' + ((window.f0ckI18n && window.f0ckI18n.processing) || 'Processing...');
|
||||||
|
|
||||||
e.target.disabled = true;
|
|
||||||
fetch('/api/v2/report', {
|
fetch('/api/v2/report', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
@@ -11235,20 +11363,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
reportModal.style.display = 'none';
|
reportModal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
clearReportError();
|
||||||
if (window.showFlash) window.showFlash((window.f0ckI18n && window.f0ckI18n.report_success) || 'Report submitted successfully.', 'success');
|
if (window.showFlash) window.showFlash((window.f0ckI18n && window.f0ckI18n.report_success) || 'Report submitted successfully.', 'success');
|
||||||
// Reset fields for future reports
|
// Reset fields for future reports
|
||||||
reportReason.value = '';
|
reportReason.value = '';
|
||||||
} else {
|
} else {
|
||||||
reportError.textContent = data.msg || (window.f0ckI18n && window.f0ckI18n.report_error) || 'An error occurred.';
|
const errMsg = data.msg || (window.f0ckI18n && window.f0ckI18n.report_error) || 'An error occurred.';
|
||||||
if (window.showFlash) window.showFlash(data.msg || (window.f0ckI18n && window.f0ckI18n.report_error) || 'An error occurred.', 'error');
|
showReportError(errMsg);
|
||||||
|
if (window.showFlash) window.showFlash(errMsg, 'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
reportError.textContent = (window.f0ckI18n && window.f0ckI18n.network_error) || 'Network error.';
|
const errMsg = (window.f0ckI18n && window.f0ckI18n.network_error) || 'Network error.';
|
||||||
if (window.showFlash) window.showFlash((window.f0ckI18n && window.f0ckI18n.network_error) || 'Network error.', 'error');
|
showReportError(errMsg);
|
||||||
|
if (window.showFlash) window.showFlash(errMsg, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
e.target.disabled = false;
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText;
|
||||||
|
if (_reportRcWidgetId !== null && window.grecaptcha) {
|
||||||
|
try { grecaptcha.reset(_reportRcWidgetId); } catch(e) {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -12585,9 +12721,9 @@ document.addEventListener('click', (e) => {
|
|||||||
visSaveBtn.textContent = origText;
|
visSaveBtn.textContent = origText;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const visVal = parseInt(data.visibility, 10);
|
const visVal = parseInt(data.visibility, 10);
|
||||||
const titles = ['Public', 'Unlisted', 'Private'];
|
const titles = ['Public', 'Unlisted', 'Private', 'Unavailable'];
|
||||||
const icons = ['fa-globe', 'fa-link', 'fa-lock'];
|
const icons = ['fa-globe', 'fa-link', 'fa-lock', 'fa-ban'];
|
||||||
const colors = ['var(--color-success, #00C851)', 'var(--color-warning, #ffbb33)', 'var(--color-danger, #ff4444)'];
|
const colors = ['var(--color-success, #00C851)', 'var(--color-warning, #ffbb33)', 'var(--color-danger, #ff4444)', 'var(--color-danger, #ff4444)'];
|
||||||
|
|
||||||
const visBtn = document.getElementById('a_visibility');
|
const visBtn = document.getElementById('a_visibility');
|
||||||
if (visBtn) {
|
if (visBtn) {
|
||||||
@@ -12595,6 +12731,14 @@ document.addEventListener('click', (e) => {
|
|||||||
visBtn.setAttribute('title', `Visibility: ${titles[visVal]} (Click to change)`);
|
visBtn.setAttribute('title', `Visibility: ${titles[visVal]} (Click to change)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unavBtn = document.getElementById('a_unavailable');
|
||||||
|
if (unavBtn) {
|
||||||
|
unavBtn.dataset.visibility = visVal;
|
||||||
|
unavBtn.classList.toggle('active', visVal === 3);
|
||||||
|
unavBtn.style.color = visVal === 3 ? 'var(--danger, #ff4444)' : '';
|
||||||
|
unavBtn.setAttribute('title', visVal === 3 ? 'Make Available (Public)' : 'Make Unavailable (451)');
|
||||||
|
}
|
||||||
|
|
||||||
const infoVisBtn = document.getElementById('info-visibility-edit-btn');
|
const infoVisBtn = document.getElementById('info-visibility-edit-btn');
|
||||||
if (infoVisBtn) {
|
if (infoVisBtn) {
|
||||||
infoVisBtn.dataset.visibility = visVal;
|
infoVisBtn.dataset.visibility = visVal;
|
||||||
@@ -12627,8 +12771,9 @@ document.addEventListener('click', (e) => {
|
|||||||
const saveBtn = e.target.closest('#info-title-save');
|
const saveBtn = e.target.closest('#info-title-save');
|
||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const input = document.getElementById('info-title-input');
|
const container = saveBtn.closest('.item_title') || document;
|
||||||
const status = document.getElementById('info-title-status');
|
const input = container.querySelector('#info-title-input') || document.getElementById('info-title-input');
|
||||||
|
const status = container.querySelector('#info-title-status') || document.getElementById('info-title-status');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const itemId = input.dataset.itemId;
|
const itemId = input.dataset.itemId;
|
||||||
@@ -12652,25 +12797,27 @@ document.addEventListener('click', (e) => {
|
|||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.innerHTML = origIcon;
|
saveBtn.innerHTML = origIcon;
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Update the live .item_title bar below the ID bar
|
// Keep input form intact and update its value
|
||||||
const titleBar = document.querySelector('.item_title');
|
input.value = data.title || '';
|
||||||
if (titleBar) {
|
|
||||||
titleBar.textContent = data.title || '';
|
// Update any plain-text title display element
|
||||||
} else if (data.title) {
|
const textEl = container.querySelector('.item_title_text') || document.querySelector('.item_title_text');
|
||||||
// Fallback: create it if somehow missing (e.g. enable_item_title was toggled)
|
if (textEl) {
|
||||||
const idBar = document.querySelector('.item-main-content > ._204863');
|
textEl.textContent = data.title || '';
|
||||||
if (idBar) {
|
|
||||||
const newBar = document.createElement('div');
|
|
||||||
newBar.className = 'item_title';
|
|
||||||
newBar.textContent = data.title;
|
|
||||||
idBar.insertAdjacentElement('afterend', newBar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate cache so navigating next/prev fetches fresh HTML from server
|
||||||
|
if (window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
status.textContent = '✓ Saved';
|
status.style.display = 'none';
|
||||||
status.style.color = 'var(--accent, #5cb85c)';
|
status.textContent = '';
|
||||||
status.style.display = 'inline';
|
}
|
||||||
setTimeout(() => { status.style.display = 'none'; }, 2000);
|
|
||||||
|
if (window.flashMessage) {
|
||||||
|
window.flashMessage('Title saved', 2000, 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (status) {
|
if (status) {
|
||||||
@@ -12678,6 +12825,9 @@ document.addEventListener('click', (e) => {
|
|||||||
status.style.color = '#e84040';
|
status.style.color = '#e84040';
|
||||||
status.style.display = 'inline';
|
status.style.display = 'inline';
|
||||||
}
|
}
|
||||||
|
if (window.flashMessage) {
|
||||||
|
window.flashMessage('Error while saving Title', 3000, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -12688,6 +12838,9 @@ document.addEventListener('click', (e) => {
|
|||||||
status.style.color = '#e84040';
|
status.style.color = '#e84040';
|
||||||
status.style.display = 'inline';
|
status.style.display = 'inline';
|
||||||
}
|
}
|
||||||
|
if (window.flashMessage) {
|
||||||
|
window.flashMessage('Error while saving Title', 3000, 'error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12823,6 +12976,16 @@ document.addEventListener('click', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Enter key on title input
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && e.target && e.target.id === 'info-title-input') {
|
||||||
|
e.preventDefault();
|
||||||
|
const container = e.target.closest('.item_title') || document;
|
||||||
|
const saveBtn = container.querySelector('#info-title-save') || document.getElementById('info-title-save');
|
||||||
|
if (saveBtn) saveBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Ensure any navigation event restores the scroll state
|
// Ensure any navigation event restores the scroll state
|
||||||
window.addEventListener('pjax:start', () => {
|
window.addEventListener('pjax:start', () => {
|
||||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ if (!window.UserCommentSystem) {
|
|||||||
? `style="--author-banner: url('/a/${c.banner_file}'); --author-banner-position: ${c.banner_position === 'center' ? 'center top' : (c.banner_position || 'center top')}; --author-banner-size: ${(c.banner_size && c.banner_size !== 'cover') ? c.banner_size : '100% auto'}; --author-banner-repeat: no-repeat;"`
|
? `style="--author-banner: url('/a/${c.banner_file}'); --author-banner-position: ${c.banner_position === 'center' ? 'center top' : (c.banner_position || 'center top')}; --author-banner-size: ${(c.banner_size && c.banner_size !== 'cover') ? c.banner_size : '100% auto'}; --author-banner-repeat: no-repeat;"`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `<div class="comment" id="c${c.id}" ${bannerStyle}><div class="comment-avatar"><a href="/${itemKey}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${window.f0ckSession && window.f0ckSession.logged_in ? `<button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button>` : ''}</div></div></div></div><a href="/${itemKey}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
|
return `<div class="comment" id="c${c.id}" ${bannerStyle}><div class="comment-avatar"><a href="/${itemKey}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions"><button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button></div></div></div></div><a href="/${itemKey}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
startLiveTimestamps() {
|
startLiveTimestamps() {
|
||||||
|
|||||||
@@ -518,5 +518,15 @@ export default new class {
|
|||||||
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
|
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
|
||||||
return hostNoPort.endsWith('.onion');
|
return hostNoPort.endsWith('.onion');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLocalhostRequest(req) {
|
||||||
|
if (!req) return false;
|
||||||
|
const rawHost = req.headers?.['x-forwarded-host'] || req.headers?.['host'] || req.headers?.['x-forwarded-server'] || '';
|
||||||
|
if (!rawHost) return false;
|
||||||
|
const hostStr = Array.isArray(rawHost) ? rawHost[0] : String(rawHost);
|
||||||
|
const firstHost = hostStr.split(',')[0].trim();
|
||||||
|
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
|
||||||
|
return hostNoPort === 'localhost' || hostNoPort === '127.0.0.1' || hostNoPort === '::1';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -643,6 +643,8 @@
|
|||||||
"reason_required": "Grund ist erforderlich.",
|
"reason_required": "Grund ist erforderlich.",
|
||||||
"reason_optional": "Grund (optional)",
|
"reason_optional": "Grund (optional)",
|
||||||
"reason_required_label": "Grund (erforderlich)",
|
"reason_required_label": "Grund (erforderlich)",
|
||||||
|
"captcha_required": "Bitte füllen Sie das CAPTCHA aus.",
|
||||||
|
"captcha_loading": "CAPTCHA wird noch geladen. Bitte kurz warten.",
|
||||||
"processing": "Wird verarbeitet...",
|
"processing": "Wird verarbeitet...",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
|
|||||||
@@ -647,6 +647,8 @@
|
|||||||
"reason_required": "Reason is required.",
|
"reason_required": "Reason is required.",
|
||||||
"reason_optional": "Reason (optional)",
|
"reason_optional": "Reason (optional)",
|
||||||
"reason_required_label": "Reason (required)",
|
"reason_required_label": "Reason (required)",
|
||||||
|
"captcha_required": "Please complete the CAPTCHA.",
|
||||||
|
"captcha_loading": "CAPTCHA is still loading. Please wait a moment.",
|
||||||
"processing": "Processing...",
|
"processing": "Processing...",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
|||||||
@@ -641,6 +641,8 @@
|
|||||||
"reason_required": "Reden is vereist.",
|
"reason_required": "Reden is vereist.",
|
||||||
"reason_optional": "Reden (optioneel)",
|
"reason_optional": "Reden (optioneel)",
|
||||||
"reason_required_label": "Reden (vereist)",
|
"reason_required_label": "Reden (vereist)",
|
||||||
|
"captcha_required": "Vul alstublieft de CAPTCHA in.",
|
||||||
|
"captcha_loading": "CAPTCHA is nog aan het laden. Even geduld a.u.b.",
|
||||||
"processing": "Verwerken...",
|
"processing": "Verwerken...",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nee",
|
"no": "Nee",
|
||||||
|
|||||||
@@ -642,6 +642,8 @@
|
|||||||
"reason_required": "Grund ist erforderlich.",
|
"reason_required": "Grund ist erforderlich.",
|
||||||
"reason_optional": "Grund (optional)",
|
"reason_optional": "Grund (optional)",
|
||||||
"reason_required_label": "Grund (erforderlich)",
|
"reason_required_label": "Grund (erforderlich)",
|
||||||
|
"captcha_required": "Bitte füllen Sie das CAPTCHA aus.",
|
||||||
|
"captcha_loading": "CAPTCHA wird noch geladen. Bitte kurz warten.",
|
||||||
"processing": "Verarbeitung wird durchgeführt...",
|
"processing": "Verarbeitung wird durchgeführt...",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
|
|||||||
+84
-19
@@ -3,32 +3,44 @@ import db from "./sql.mjs";
|
|||||||
// Maps for fast O(1) in-memory lookups:
|
// Maps for fast O(1) in-memory lookups:
|
||||||
// dest (string) -> owner username (lowercase string)
|
// dest (string) -> owner username (lowercase string)
|
||||||
const _privateDests = new Map();
|
const _privateDests = new Map();
|
||||||
// id (number) -> owner username (lowercase string)
|
|
||||||
const _privateIds = new Map();
|
const _privateIds = new Map();
|
||||||
|
|
||||||
|
// Unavailable items (visibility === 3, serves HTTP 451 for non-logged in users):
|
||||||
|
const _unavailableDests = new Map();
|
||||||
|
const _unavailableIds = new Map();
|
||||||
|
|
||||||
let _initialized = false;
|
let _initialized = false;
|
||||||
let _initPromise = null;
|
let _initPromise = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all active private items into memory cache.
|
* Load all active private and unavailable items into memory cache.
|
||||||
*/
|
*/
|
||||||
export async function initPrivateItems() {
|
export async function initPrivateItems() {
|
||||||
try {
|
try {
|
||||||
const rows = await db`
|
const rows = await db`
|
||||||
SELECT id, dest, LOWER(username) as username
|
SELECT id, dest, LOWER(username) as username, visibility
|
||||||
FROM items
|
FROM items
|
||||||
WHERE visibility = 2 AND is_deleted = false
|
WHERE visibility IN (2, 3) AND is_deleted = false
|
||||||
`;
|
`;
|
||||||
_privateDests.clear();
|
_privateDests.clear();
|
||||||
_privateIds.clear();
|
_privateIds.clear();
|
||||||
|
_unavailableDests.clear();
|
||||||
|
_unavailableIds.clear();
|
||||||
|
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
if (r.dest) _privateDests.set(r.dest, r.username || '');
|
const u = r.username || '';
|
||||||
if (r.id) _privateIds.set(Number(r.id), r.username || '');
|
if (r.visibility === 2) {
|
||||||
|
if (r.dest) _privateDests.set(r.dest, u);
|
||||||
|
if (r.id) _privateIds.set(Number(r.id), u);
|
||||||
|
} else if (r.visibility === 3) {
|
||||||
|
if (r.dest) _unavailableDests.set(r.dest, u);
|
||||||
|
if (r.id) _unavailableIds.set(Number(r.id), u);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
console.log(`[BOOT] Loaded ${_privateDests.size} private item(s) into memory cache`);
|
console.log(`[BOOT] Loaded ${_privateDests.size} private item(s) and ${_unavailableDests.size} unavailable item(s) into memory cache`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[BOOT] Failed to load private items into cache:', err.message);
|
console.error('[BOOT] Failed to load private/unavailable items into cache:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +60,8 @@ export function addPrivateItem(id, dest, username) {
|
|||||||
const u = (username || '').toLowerCase();
|
const u = (username || '').toLowerCase();
|
||||||
if (dest) _privateDests.set(dest, u);
|
if (dest) _privateDests.set(dest, u);
|
||||||
if (id) _privateIds.set(Number(id), u);
|
if (id) _privateIds.set(Number(id), u);
|
||||||
|
// Ensure not in unavailable
|
||||||
|
removeUnavailableItem(id, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removePrivateItem(id, dest) {
|
export function removePrivateItem(id, dest) {
|
||||||
@@ -55,9 +69,22 @@ export function removePrivateItem(id, dest) {
|
|||||||
if (id) _privateIds.delete(Number(id));
|
if (id) _privateIds.delete(Number(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addUnavailableItem(id, dest, username) {
|
||||||
|
const u = (username || '').toLowerCase();
|
||||||
|
if (dest) _unavailableDests.set(dest, u);
|
||||||
|
if (id) _unavailableIds.set(Number(id), u);
|
||||||
|
// Ensure not in private
|
||||||
|
removePrivateItem(id, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeUnavailableItem(id, dest) {
|
||||||
|
if (dest) _unavailableDests.delete(dest);
|
||||||
|
if (id) _unavailableIds.delete(Number(id));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given pathname (/b/<dest>, /t/<id>..., /ca/<id>...) is a private item.
|
* Checks if a given pathname (/b/<dest>, /t/<id>..., /ca/<id>...) is a private or unavailable item.
|
||||||
* Returns { isPrivate: boolean, owner: string } or null if not private.
|
* Returns { isPrivate: boolean, isUnavailable: boolean, owner: string } or null if normal public.
|
||||||
*/
|
*/
|
||||||
export function getPrivateItemFromPath(pathname) {
|
export function getPrivateItemFromPath(pathname) {
|
||||||
if (!pathname || typeof pathname !== 'string') return null;
|
if (!pathname || typeof pathname !== 'string') return null;
|
||||||
@@ -70,9 +97,13 @@ export function getPrivateItemFromPath(pathname) {
|
|||||||
dest = pathname.slice(3);
|
dest = pathname.slice(3);
|
||||||
}
|
}
|
||||||
dest = dest.split('?')[0].split('#')[0];
|
dest = dest.split('?')[0].split('#')[0];
|
||||||
const owner = _privateDests.get(dest);
|
const privOwner = _privateDests.get(dest);
|
||||||
if (owner !== undefined) {
|
if (privOwner !== undefined) {
|
||||||
return { isPrivate: true, owner };
|
return { isPrivate: true, isUnavailable: false, owner: privOwner };
|
||||||
|
}
|
||||||
|
const unavOwner = _unavailableDests.get(dest);
|
||||||
|
if (unavOwner !== undefined) {
|
||||||
|
return { isPrivate: false, isUnavailable: true, owner: unavOwner };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -88,9 +119,13 @@ export function getPrivateItemFromPath(pathname) {
|
|||||||
const match = filename.match(/^(\d+)/);
|
const match = filename.match(/^(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const id = parseInt(match[1], 10);
|
const id = parseInt(match[1], 10);
|
||||||
const owner = _privateIds.get(id);
|
const privOwner = _privateIds.get(id);
|
||||||
if (owner !== undefined) {
|
if (privOwner !== undefined) {
|
||||||
return { isPrivate: true, owner };
|
return { isPrivate: true, isUnavailable: false, owner: privOwner };
|
||||||
|
}
|
||||||
|
const unavOwner = _unavailableIds.get(id);
|
||||||
|
if (unavOwner !== undefined) {
|
||||||
|
return { isPrivate: false, isUnavailable: true, owner: unavOwner };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -107,9 +142,13 @@ export function getPrivateItemFromPath(pathname) {
|
|||||||
const match = filename.match(/^(\d+)/);
|
const match = filename.match(/^(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const id = parseInt(match[1], 10);
|
const id = parseInt(match[1], 10);
|
||||||
const owner = _privateIds.get(id);
|
const privOwner = _privateIds.get(id);
|
||||||
if (owner !== undefined) {
|
if (privOwner !== undefined) {
|
||||||
return { isPrivate: true, owner };
|
return { isPrivate: true, isUnavailable: false, owner: privOwner };
|
||||||
|
}
|
||||||
|
const unavOwner = _unavailableIds.get(id);
|
||||||
|
if (unavOwner !== undefined) {
|
||||||
|
return { isPrivate: false, isUnavailable: true, owner: unavOwner };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -141,3 +180,29 @@ export function render502(req, res) {
|
|||||||
}).end(body);
|
}).end(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTTP 451 Unavailable For Legal Reasons response.
|
||||||
|
*/
|
||||||
|
export function render451(req, res) {
|
||||||
|
if (req.headers && (req.headers['x-requested-with'] === 'XMLHttpRequest' || req.headers.accept?.includes('application/json'))) {
|
||||||
|
res.writeHead(451, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
||||||
|
}).end(JSON.stringify({ success: false, is_unavailable: true, msg: '451 - Unavailable For Legal Reasons' }));
|
||||||
|
} else {
|
||||||
|
const body = `<html>\r
|
||||||
|
<head><title>451 Unavailable For Legal Reasons</title></head>\r
|
||||||
|
<body>\r
|
||||||
|
<center><h1>451 Unavailable For Legal Reasons</h1></center>\r
|
||||||
|
<hr><center>nginx</center>\r
|
||||||
|
</body>\r
|
||||||
|
</html>\r
|
||||||
|
`;
|
||||||
|
res.writeHead(451, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
||||||
|
}).end(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -371,11 +371,12 @@ const buildFeedFilters = async ({
|
|||||||
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
|
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAdmin = !!session?.admin;
|
||||||
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
|
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
|
||||||
const visibilityFilter = isOwnerOrAdmin
|
const visibilityFilter = isAdmin
|
||||||
? db``
|
? db``
|
||||||
: (session && session.user
|
: (session && session.user
|
||||||
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
|
? db`and (coalesce(items.visibility, 0) = 0 or (lower(items.username) = ${session.user.toLowerCase()} and items.visibility != 3))`
|
||||||
: db`and coalesce(items.visibility, 0) = 0`);
|
: db`and coalesce(items.visibility, 0) = 0`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -881,10 +882,11 @@ export default {
|
|||||||
|
|
||||||
// Helper to construct shared filter conditions
|
// Helper to construct shared filter conditions
|
||||||
const buildConditions = () => {
|
const buildConditions = () => {
|
||||||
const visibilityFilter = isOwnerOrAdmin
|
const isAdmin = !!session?.admin;
|
||||||
|
const visibilityFilter = isAdmin
|
||||||
? db``
|
? db``
|
||||||
: (session && session.user
|
: (session && session.user
|
||||||
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
|
? db`and (coalesce(items.visibility, 0) = 0 or (lower(items.username) = ${session.user.toLowerCase()} and items.visibility != 3))`
|
||||||
: db`and coalesce(items.visibility, 0) = 0`);
|
: db`and coalesce(items.visibility, 0) = 0`);
|
||||||
|
|
||||||
return db`
|
return db`
|
||||||
@@ -964,9 +966,18 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If request was by sequential numeric ID (/123) and item visibility > 0 (unlisted):
|
// If item is Unavailable (visibility === 3):
|
||||||
|
// Only viewable by admins, not regular users or mods (renders normal post not found)
|
||||||
|
if (actitem.visibility === 3 && !session?.admin) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "404 - upload not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If request was by sequential numeric ID (/123) and item visibility > 0 (unlisted/private):
|
||||||
// Block numeric enumeration unless viewer is owner/admin
|
// Block numeric enumeration unless viewer is owner/admin
|
||||||
if (isNumeric && actitem.visibility > 0 && !isOwnerOrAdmin) {
|
if (isNumeric && (actitem.visibility === 1 || actitem.visibility === 2) && !isOwnerOrAdmin) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "404 - upload not found"
|
message: "404 - upload not found"
|
||||||
|
|||||||
@@ -137,6 +137,25 @@ export default (router, tpl) => {
|
|||||||
if (data.item) {
|
if (data.item) {
|
||||||
const session = data.session;
|
const session = data.session;
|
||||||
const item = data.item;
|
const item = data.item;
|
||||||
|
// When guest anonymization is active, suppress uploader identity, banner, avatar, and source URL
|
||||||
|
if (cfg.main.guest_anonymize && !req.session) {
|
||||||
|
if (item.src) item.src = null;
|
||||||
|
item.username = 'anonymous';
|
||||||
|
item.author_banner_file = null;
|
||||||
|
item.author_banner_position = null;
|
||||||
|
item.author_banner_size = null;
|
||||||
|
item.author_avatar = null;
|
||||||
|
item.author_avatar_file = null;
|
||||||
|
item.author_color = null;
|
||||||
|
item.author_description = null;
|
||||||
|
item.author_display_name = null;
|
||||||
|
item.author_id = null;
|
||||||
|
if (data.uploader) {
|
||||||
|
data.uploader.name = 'anonymous';
|
||||||
|
data.uploader.id = null;
|
||||||
|
data.uploader.color = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
|
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
|
||||||
data.can_manage_item = !!(session && (session.admin || session.is_moderator || (session.user && item.username && session.user.toLowerCase() === item.username.toLowerCase())));
|
data.can_manage_item = !!(session && (session.admin || session.is_moderator || (session.user && item.username && session.user.toLowerCase() === item.username.toLowerCase())));
|
||||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import audit from '../../audit.mjs';
|
|||||||
import { parseMultipart, collectBody } from '../../multipart.mjs';
|
import { parseMultipart, collectBody } from '../../multipart.mjs';
|
||||||
import { purgeExpiredUploads } from '../../lib_delete.mjs';
|
import { purgeExpiredUploads } from '../../lib_delete.mjs';
|
||||||
import { calculateExpiresAt } from './upload.mjs';
|
import { calculateExpiresAt } from './upload.mjs';
|
||||||
import { addPrivateItem, removePrivateItem } from '../../private_items.mjs';
|
import { addPrivateItem, removePrivateItem, addUnavailableItem, removeUnavailableItem } from '../../private_items.mjs';
|
||||||
|
|
||||||
const allowedMimes = ["audio", "image", "video", "%"];
|
const allowedMimes = ["audio", "image", "video", "%"];
|
||||||
const getGlobalfilter = () => {
|
const getGlobalfilter = () => {
|
||||||
@@ -1067,7 +1067,11 @@ export default router => {
|
|||||||
if (!isOwner && !isMod) return res.json({ success: false, msg: 'Forbidden' }, 403);
|
if (!isOwner && !isMod) return res.json({ success: false, msg: 'Forbidden' }, 403);
|
||||||
|
|
||||||
// Accept title from JSON or URL-encoded body
|
// Accept title from JSON or URL-encoded body
|
||||||
let rawTitle = req.post?.title ?? req.body?.title ?? null;
|
let body = req.post || req.body || {};
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
try { body = JSON.parse(body); } catch (_) {}
|
||||||
|
}
|
||||||
|
let rawTitle = body?.title ?? null;
|
||||||
if (rawTitle !== null) rawTitle = String(rawTitle).trim();
|
if (rawTitle !== null) rawTitle = String(rawTitle).trim();
|
||||||
// Empty string → null (clears the title)
|
// Empty string → null (clears the title)
|
||||||
const title = (rawTitle === '' || rawTitle === null) ? null : rawTitle.substring(0, 500);
|
const title = (rawTitle === '' || rawTitle === null) ? null : rawTitle.substring(0, 500);
|
||||||
@@ -1317,7 +1321,7 @@ export default router => {
|
|||||||
}
|
}
|
||||||
const postid = req.post?.postid || req.post?.id || req.body?.postid || req.body?.id;
|
const postid = req.post?.postid || req.post?.id || req.body?.postid || req.body?.id;
|
||||||
const visibility = parseInt(req.post?.visibility ?? req.body?.visibility, 10);
|
const visibility = parseInt(req.post?.visibility ?? req.body?.visibility, 10);
|
||||||
if (!postid || isNaN(visibility) || ![0, 1, 2].includes(visibility)) {
|
if (!postid || isNaN(visibility) || ![0, 1, 2, 3].includes(visibility)) {
|
||||||
return res.json({ success: false, msg: 'Invalid parameters' }, 400);
|
return res.json({ success: false, msg: 'Invalid parameters' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1340,12 +1344,21 @@ export default router => {
|
|||||||
return res.json({ success: false, msg: 'Unauthorized' }, 403);
|
return res.json({ success: false, msg: 'Unauthorized' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (visibility === 3 && !isAdmin) {
|
||||||
|
return res.json({ success: false, msg: 'Only moderators or administrators can make an item unavailable' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
await db`UPDATE items SET visibility = ${visibility} WHERE id = ${item[0].id}`;
|
await db`UPDATE items SET visibility = ${visibility} WHERE id = ${item[0].id}`;
|
||||||
|
|
||||||
if (visibility === 2) {
|
if (visibility === 2) {
|
||||||
addPrivateItem(item[0].id, item[0].dest, item[0].username);
|
addPrivateItem(item[0].id, item[0].dest, item[0].username);
|
||||||
|
removeUnavailableItem(item[0].id, item[0].dest);
|
||||||
|
} else if (visibility === 3) {
|
||||||
|
addUnavailableItem(item[0].id, item[0].dest, item[0].username);
|
||||||
|
removePrivateItem(item[0].id, item[0].dest);
|
||||||
} else {
|
} else {
|
||||||
removePrivateItem(item[0].id, item[0].dest);
|
removePrivateItem(item[0].id, item[0].dest);
|
||||||
|
removeUnavailableItem(item[0].id, item[0].dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
f0cklib.clearCountCache();
|
f0cklib.clearCountCache();
|
||||||
|
|||||||
@@ -398,9 +398,24 @@ export default (router, tpl) => {
|
|||||||
// Hall columns for display
|
// Hall columns for display
|
||||||
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
|
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
|
||||||
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
|
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
|
||||||
// When guest anonymization is active, suppress source URL from the info modal
|
// When guest anonymization is active, suppress uploader identity, banner, avatar, and source URL
|
||||||
if (cfg.main.guest_anonymize && !req.session && item.src) {
|
if (cfg.main.guest_anonymize && !req.session) {
|
||||||
item.src = null;
|
if (item.src) item.src = null;
|
||||||
|
item.username = 'anonymous';
|
||||||
|
item.author_banner_file = null;
|
||||||
|
item.author_banner_position = null;
|
||||||
|
item.author_banner_size = null;
|
||||||
|
item.author_avatar = null;
|
||||||
|
item.author_avatar_file = null;
|
||||||
|
item.author_color = null;
|
||||||
|
item.author_description = null;
|
||||||
|
item.author_display_name = null;
|
||||||
|
item.author_id = null;
|
||||||
|
if (data.uploader) {
|
||||||
|
data.uploader.name = 'anonymous';
|
||||||
|
data.uploader.id = null;
|
||||||
|
data.uploader.color = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Precomputed for template engine compatibility (avoids nested { } inside {{ }})
|
// Precomputed for template engine compatibility (avoids nested { } inside {{ }})
|
||||||
data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged'));
|
data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged'));
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import db from "../sql.mjs";
|
import db from "../sql.mjs";
|
||||||
import lib from "../lib.mjs";
|
import lib from "../lib.mjs";
|
||||||
import audit from "../audit.mjs";
|
import audit from "../audit.mjs";
|
||||||
|
import cfg from "../config.mjs";
|
||||||
|
import security from "../security.mjs";
|
||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
|
|
||||||
// User: Submit a new report
|
// Submit a new report (Users & Guests)
|
||||||
router.post(/^\/api\/v2\/report\/?$/, lib.loggedin, async (req, res) => {
|
router.post(/^\/api\/v2\/report\/?$/, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { item_id, comment_id, reported_user_id, reason } = req.post;
|
const { item_id, comment_id, reported_user_id, reason } = req.post;
|
||||||
|
|
||||||
@@ -18,10 +20,40 @@ export default (router, tpl) => {
|
|||||||
return res.json({ success: false, msg: "Must specify an item, comment, or user to report." }, 400);
|
return res.json({ success: false, msg: "Must specify an item, comment, or user to report." }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ip = security.getRealIP(req);
|
||||||
|
const isGuest = !req.session;
|
||||||
|
|
||||||
|
if (isGuest) {
|
||||||
|
// CAPTCHA verification for guest reports (bypassed for .onion and localhost)
|
||||||
|
const isOnion = lib.isOnionRequest(req);
|
||||||
|
const isLocalhost = lib.isLocalhostRequest(req);
|
||||||
|
if (!isOnion && !isLocalhost && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
|
||||||
|
const rcToken = req.post['g-recaptcha-response'];
|
||||||
|
if (!rcToken) {
|
||||||
|
return res.json({ success: false, msg: "Please complete the CAPTCHA." }, 400);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const verifyRes = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ secret: cfg.recaptcha.secret_key, response: rcToken, remoteip: ip })
|
||||||
|
});
|
||||||
|
const { success } = await verifyRes.json();
|
||||||
|
if (!success) {
|
||||||
|
return res.json({ success: false, msg: "CAPTCHA verification failed. Please try again." }, 400);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[REPORT] reCAPTCHA error:', e.message);
|
||||||
|
return res.json({ success: false, msg: "CAPTCHA verification error. Please try again." }, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reportRes = await db`
|
const reportRes = await db`
|
||||||
INSERT INTO reports (reporter_id, item_id, comment_id, user_id, reason)
|
INSERT INTO reports (reporter_id, reporter_ip, item_id, comment_id, user_id, reason)
|
||||||
VALUES (
|
VALUES (
|
||||||
${req.session.id},
|
${req.session ? req.session.id : null},
|
||||||
|
${ip},
|
||||||
${item_id ? +item_id : null},
|
${item_id ? +item_id : null},
|
||||||
${comment_id ? +comment_id : null},
|
${comment_id ? +comment_id : null},
|
||||||
${reported_user_id ? +reported_user_id : null},
|
${reported_user_id ? +reported_user_id : null},
|
||||||
@@ -86,7 +118,8 @@ export default (router, tpl) => {
|
|||||||
c.content AS comment_body,
|
c.content AS comment_body,
|
||||||
COALESCE(r.item_id, c.item_id) AS resolved_item_id,
|
COALESCE(r.item_id, c.item_id) AS resolved_item_id,
|
||||||
COALESCE(i.dest, ci.dest) AS resolved_item_dest,
|
COALESCE(i.dest, ci.dest) AS resolved_item_dest,
|
||||||
COALESCE(i.mime, ci.mime) AS resolved_item_mime
|
COALESCE(i.mime, ci.mime) AS resolved_item_mime,
|
||||||
|
COALESCE(i.visibility, ci.visibility, 0) AS resolved_item_visibility
|
||||||
FROM reports r
|
FROM reports r
|
||||||
LEFT JOIN "user" rep ON r.reporter_id = rep.id
|
LEFT JOIN "user" rep ON r.reporter_id = rep.id
|
||||||
LEFT JOIN "user" tgt_u ON r.user_id = tgt_u.id
|
LEFT JOIN "user" tgt_u ON r.user_id = tgt_u.id
|
||||||
|
|||||||
@@ -185,9 +185,24 @@ export default (router, tpl) => {
|
|||||||
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
||||||
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
||||||
data.item_has_dimensions = !!(item.width && item.height);
|
data.item_has_dimensions = !!(item.width && item.height);
|
||||||
// When guest anonymization is active, suppress source URL from the info modal
|
// When guest anonymization is active, suppress uploader identity, banner, avatar, and source URL
|
||||||
if (cfg.main.guest_anonymize && !req.session && item.src) {
|
if (cfg.main.guest_anonymize && !req.session) {
|
||||||
item.src = null;
|
if (item.src) item.src = null;
|
||||||
|
item.username = 'anonymous';
|
||||||
|
item.author_banner_file = null;
|
||||||
|
item.author_banner_position = null;
|
||||||
|
item.author_banner_size = null;
|
||||||
|
item.author_avatar = null;
|
||||||
|
item.author_avatar_file = null;
|
||||||
|
item.author_color = null;
|
||||||
|
item.author_description = null;
|
||||||
|
item.author_display_name = null;
|
||||||
|
item.author_id = null;
|
||||||
|
if (data.uploader) {
|
||||||
|
data.uploader.name = 'anonymous';
|
||||||
|
data.uploader.id = null;
|
||||||
|
data.uploader.color = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-3
@@ -26,7 +26,7 @@ import { createI18n } from "./inc/i18n.mjs";
|
|||||||
import { safeDeleteMediaFile, purgeExpiredUploads } from "./inc/lib_delete.mjs";
|
import { safeDeleteMediaFile, purgeExpiredUploads } from "./inc/lib_delete.mjs";
|
||||||
|
|
||||||
import security from "./inc/security.mjs";
|
import security from "./inc/security.mjs";
|
||||||
import { initPrivateItems, getPrivateItemFromPath, isPrivateItemPath, render502 } from "./inc/private_items.mjs";
|
import { initPrivateItems, getPrivateItemFromPath, isPrivateItemPath, render502, render451 } from "./inc/private_items.mjs";
|
||||||
|
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
const _require = createRequire(import.meta.url);
|
const _require = createRequire(import.meta.url);
|
||||||
@@ -87,7 +87,7 @@ const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.si
|
|||||||
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
|
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
|
||||||
|
|
||||||
function getGateLoginInjection(req) {
|
function getGateLoginInjection(req) {
|
||||||
const rcEnabled = _rcEnabled && !lib.isOnionRequest(req);
|
const rcEnabled = _rcEnabled && !lib.isOnionRequest(req) && !lib.isLocalhostRequest(req);
|
||||||
return `
|
return `
|
||||||
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
|
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
|
||||||
|
|
||||||
@@ -726,6 +726,41 @@ process.on('uncaughtException', err => {
|
|||||||
if (req.url.pathname.match(/^\/(b|c|t|ca|a|memes)\//) || req.url.pathname.startsWith('/s/emojis/')) {
|
if (req.url.pathname.match(/^\/(b|c|t|ca|a|memes)\//) || req.url.pathname.startsWith('/s/emojis/')) {
|
||||||
const privItem = getPrivateItemFromPath(req.url.pathname);
|
const privItem = getPrivateItemFromPath(req.url.pathname);
|
||||||
if (privItem) {
|
if (privItem) {
|
||||||
|
if (privItem.isUnavailable) {
|
||||||
|
// Unavailable item (visibility === 3):
|
||||||
|
// Direct URLs MUST serve 451 when requested by non-admins (or without a session)
|
||||||
|
let isAdmin = false;
|
||||||
|
if (req.cookies?.session) {
|
||||||
|
const _sessionHash = lib.sha256(req.cookies.session);
|
||||||
|
let user = _scGet(_sessionHash);
|
||||||
|
if (!user) {
|
||||||
|
const urows = await db`
|
||||||
|
select "user".id, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_expires
|
||||||
|
from "user_sessions"
|
||||||
|
left join "user" on "user".id = "user_sessions".user_id
|
||||||
|
where "user_sessions".session = ${_sessionHash}
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
if (urows.length > 0) {
|
||||||
|
user = urows[0];
|
||||||
|
_scSet(_sessionHash, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user && !user.banned && user.admin) {
|
||||||
|
isAdmin = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
render451(req, res);
|
||||||
|
req.url.pathname = '/unavailable_item_bypass';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Private item (visibility === 2):
|
// Private item (visibility === 2):
|
||||||
// Direct URLs MUST serve 502 when requested without a session (or by unauthorized users),
|
// Direct URLs MUST serve 502 when requested without a session (or by unauthorized users),
|
||||||
// regardless of the protect_files setting.
|
// regardless of the protect_files setting.
|
||||||
@@ -1693,7 +1728,7 @@ process.on('uncaughtException', err => {
|
|||||||
const defaultRecaptcha = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
const defaultRecaptcha = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
||||||
let perRequestRecaptcha = defaultRecaptcha;
|
let perRequestRecaptcha = defaultRecaptcha;
|
||||||
|
|
||||||
if (effectiveReq && lib.isOnionRequest(effectiveReq)) {
|
if (effectiveReq && (lib.isOnionRequest(effectiveReq) || lib.isLocalhostRequest(effectiveReq))) {
|
||||||
perRequestRecaptcha = false;
|
perRequestRecaptcha = false;
|
||||||
} else if (data && typeof data.recaptcha_enabled === 'boolean') {
|
} else if (data && typeof data.recaptcha_enabled === 'boolean') {
|
||||||
perRequestRecaptcha = data.recaptcha_enabled;
|
perRequestRecaptcha = data.recaptcha_enabled;
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
<div class="item_title">
|
<div class="item_title item-title">
|
||||||
{!! item.title || '' !!}
|
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span>
|
||||||
@if(can_manage_item)
|
@if(can_manage_item)
|
||||||
<div class="info-title-edit-wrap">
|
<div class="info-title-edit-wrap">
|
||||||
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
||||||
@@ -93,9 +93,10 @@
|
|||||||
|
|
||||||
<div class="blahlol">
|
<div class="blahlol">
|
||||||
@if(user_alternative_infobox)
|
@if(user_alternative_infobox)
|
||||||
<div class="user-infobox-block" data-banner-user="{{ item.username || '' }}" style="--author-accent: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif; --author-border: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif;@if(item.author_banner_file && user_banner_enabled) --author-banner: url('/a/{{ item.author_banner_file }}'); --author-banner-position: {{ item.author_banner_position === 'center' ? 'center top' : (item.author_banner_position || 'center top') }}; --author-banner-size: {{ item.author_banner_size || 'cover' }};@endif">
|
<div class="user-infobox-block" data-banner-user="@if(session || !guest_anonymize){{ item.username || '' }}@endif" style="--author-accent: @if(session || !guest_anonymize)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif; --author-border: @if(session || !guest_anonymize)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif;@if(session || !guest_anonymize)@if(item.author_banner_file && user_banner_enabled) --author-banner: url('/a/{{ item.author_banner_file }}'); --author-banner-position: {{ item.author_banner_position === 'center' ? 'center top' : (item.author_banner_position || 'center top') }}; --author-banner-size: {{ item.author_banner_size || 'cover' }};@endif@endif">
|
||||||
|
|
||||||
<div class="user-infobox-avatar">
|
<div class="user-infobox-avatar">
|
||||||
|
@if(session || !guest_anonymize)
|
||||||
<a href="/user/{{ (item.username || '').toLowerCase() }}">
|
<a href="/user/{{ (item.username || '').toLowerCase() }}">
|
||||||
@if(item.author_avatar_file)
|
@if(item.author_avatar_file)
|
||||||
<img src="/a/{{ item.author_avatar_file }}" />
|
<img src="/a/{{ item.author_avatar_file }}" />
|
||||||
@@ -105,6 +106,11 @@
|
|||||||
<img src="/a/default.png" style="border-color: #444;" />
|
<img src="/a/default.png" style="border-color: #444;" />
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</a>
|
||||||
|
@else
|
||||||
|
<span>
|
||||||
|
<img src="/a/default.png" style="border-color: #444;" />
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="user-infobox-info">
|
<div class="user-infobox-info">
|
||||||
<div class="user-infobox-header">
|
<div class="user-infobox-header">
|
||||||
@@ -119,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="user-infobox-body">
|
<div class="user-infobox-body">
|
||||||
<div class="user-infobox-description">
|
<div class="user-infobox-description">
|
||||||
{!! item.author_description || '' !!}
|
@if(session || !guest_anonymize){!! item.author_description || '' !!}@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="user-infobox-actions">
|
<div class="user-infobox-actions">
|
||||||
@if(session)
|
@if(session)
|
||||||
@@ -177,10 +183,12 @@
|
|||||||
@endif
|
@endif
|
||||||
@if(is_mod_or_admin)
|
@if(is_mod_or_admin)
|
||||||
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
|
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
|
||||||
|
<i class="iconset fa-solid fa-ban{{ item.visibility === 3 ? ' active' : '' }}" id="a_unavailable" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="{{ item.visibility === 3 ? 'Make Available (Public)' : 'Make Unavailable (451)' }}" @if(item.visibility === 3) style="color: var(--danger, #ff4444);" @endif></i>
|
||||||
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
|
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||||
|
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-dark" id="tags" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}">
|
<span class="badge badge-dark" id="tags" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}">
|
||||||
@@ -295,6 +303,15 @@
|
|||||||
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@if(is_mod_or_admin)
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="3" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-ban" style="color: var(--danger, #ff4444);"></i> Unavailable (451)</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to administrators. Serves HTTP 451 on direct media for all other users.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,8 +74,8 @@
|
|||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
<div class="item_title">
|
<div class="item_title item-title">
|
||||||
{!! item.title || '' !!}
|
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span>
|
||||||
@if(can_manage_item)
|
@if(can_manage_item)
|
||||||
<div class="info-title-edit-wrap">
|
<div class="info-title-edit-wrap">
|
||||||
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
||||||
@@ -173,10 +173,12 @@
|
|||||||
@endif
|
@endif
|
||||||
@if(is_mod_or_admin)
|
@if(is_mod_or_admin)
|
||||||
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
|
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
|
||||||
|
<i class="iconset fa-solid fa-ban{{ item.visibility === 3 ? ' active' : '' }}" id="a_unavailable" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="{{ item.visibility === 3 ? 'Make Available (Public)' : 'Make Unavailable (451)' }}" @if(item.visibility === 3) style="color: var(--danger, #ff4444);" @endif></i>
|
||||||
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
|
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||||
|
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
|
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
|
||||||
@@ -235,6 +237,15 @@
|
|||||||
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@if(is_mod_or_admin)
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="3" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-ban" style="color: var(--danger, #ff4444);"></i> Unavailable (451)</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to administrators. Serves HTTP 451 on direct media for all other users.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+42
-3
@@ -136,10 +136,17 @@ window.loadReports = async function(page = 1) {
|
|||||||
'<button class="btn btn-sm btn-danger" onclick="window.resolveReport(' + r.id + ', "rejected")">Reject</button>';
|
'<button class="btn btn-sm btn-danger" onclick="window.resolveReport(' + r.id + ', "rejected")">Reject</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reporterHtml = '';
|
||||||
|
if (r.reporter_name) {
|
||||||
|
reporterHtml = '<a href="/user/' + r.reporter_name + '" style="color: var(--accent); font-weight: bold;">' + r.reporter_name + '</a>' + (r.reporter_ip ? ' <span style="font-size: 0.75rem; color: #888;">(' + r.reporter_ip + ')</span>' : '');
|
||||||
|
} else {
|
||||||
|
reporterHtml = '<span style="color: #aaa; font-style: italic;">Guest' + (r.reporter_ip ? ' (' + r.reporter_ip + ')' : '') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
'<td data-label="ID">' + r.id + '</td>' +
|
'<td data-label="ID">' + r.id + '</td>' +
|
||||||
'<td data-label="Reporter"><a href="/user/' + r.reporter_name + '" style="color: var(--accent); font-weight: bold;">' + r.reporter_name + '</a></td>' +
|
'<td data-label="Reporter">' + reporterHtml + '</td>' +
|
||||||
'<td data-label="Target">' + targetHtml + '</td>' +
|
'<td data-label="Target">' + targetHtml + '</td>' +
|
||||||
'<td data-label="Reason"><span style="opacity: 0.8;">' + r.reason + '</span></td>' +
|
'<td data-label="Reason"><span style="opacity: 0.8;">' + r.reason + '</span></td>' +
|
||||||
'<td data-label="Date"><span style="font-size: 0.85rem; color: #888;">' + new Date(r.created_at).toLocaleString() + '</span></td>' +
|
'<td data-label="Date"><span style="font-size: 0.85rem; color: #888;">' + new Date(r.created_at).toLocaleString() + '</span></td>' +
|
||||||
@@ -255,9 +262,11 @@ window.expandItem = function(e, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let buttonsHtml = '';
|
let buttonsHtml = '';
|
||||||
// Delete Video button only for direct Video reports
|
// Delete Item and Make Unavailable buttons for direct Item reports
|
||||||
if (isItem && !isComment) {
|
if (isItem && !isComment) {
|
||||||
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteItem(' + r.resolved_item_id + ')">Delete Item</button>';
|
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteItem(' + r.resolved_item_id + ')">Delete Item</button>';
|
||||||
|
const isUnav = r.resolved_item_visibility === 3;
|
||||||
|
buttonsHtml += ' <button class="btn ' + (isUnav ? 'btn-success' : 'btn-warning') + '" onclick="window.modToggleUnavailable(' + r.resolved_item_id + ', ' + (r.resolved_item_visibility || 0) + ')">' + (isUnav ? 'Make Available' : 'Make Unavailable (451)') + '</button>';
|
||||||
}
|
}
|
||||||
if (isComment) {
|
if (isComment) {
|
||||||
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteComment(' + r.comment_id + ')">Delete Comment</button>';
|
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteComment(' + r.comment_id + ')">Delete Comment</button>';
|
||||||
@@ -289,7 +298,7 @@ window.expandItem = function(e, id) {
|
|||||||
'<div style="font-weight: bold; opacity: 1; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 1.5px; margin-bottom: 5px;">Moderation Action:</div>' +
|
'<div style="font-weight: bold; opacity: 1; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 1.5px; margin-bottom: 5px;">Moderation Action:</div>' +
|
||||||
'<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;">' +
|
'<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;">' +
|
||||||
buttonsHtml +
|
buttonsHtml +
|
||||||
'<button class="btn btn-secondary" onclick="window.modWarnUser(' + r.reporter_id + ')">Warn Reporter (' + r.reporter_name + ')</button>' +
|
(r.reporter_id && r.reporter_name ? '<button class="btn btn-secondary" onclick="window.modWarnUser(' + r.reporter_id + ')">Warn Reporter (' + r.reporter_name + ')</button>' : (r.reporter_ip ? '<span style="color: #888; font-size: 0.85rem;">Reporter: Guest (' + r.reporter_ip + ')</span>' : '')) +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -335,6 +344,36 @@ window.adminDeleteItem = function(id) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.modToggleUnavailable = function(id, currentVis) {
|
||||||
|
const willBeUnavailable = currentVis !== 3;
|
||||||
|
const targetVis = willBeUnavailable ? 3 : 0;
|
||||||
|
const actionText = willBeUnavailable ? 'Make Unavailable (serves HTTP 451 to non-logged in visitors)' : 'Make Available (Public)';
|
||||||
|
|
||||||
|
window.ModAction.confirm('Item Visibility', actionText + ' for item #' + id + '?', async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('postid', id);
|
||||||
|
params.append('id', id);
|
||||||
|
params.append('visibility', targetVis);
|
||||||
|
const res = await fetch('/api/v2/item/visibility', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||||
|
},
|
||||||
|
body: params
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
if (window.showFlash) window.showFlash(willBeUnavailable ? 'Item marked unavailable (451)' : 'Item restored to public', 'success');
|
||||||
|
const item = window.currentReports.find(x => x.resolved_item_id === id);
|
||||||
|
if (item) item.resolved_item_visibility = targetVis;
|
||||||
|
window.loadReports(window.currentPage);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.msg || 'Failed to update visibility');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
window.modWarnUser = function(userId) {
|
window.modWarnUser = function(userId) {
|
||||||
window.ModAction.confirm('Warn User ID ' + userId, '', async (reason) => {
|
window.ModAction.confirm('Warn User ID ' + userId, '', async (reason) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|||||||
@@ -74,6 +74,9 @@
|
|||||||
<input type="hidden" id="report-comment-id">
|
<input type="hidden" id="report-comment-id">
|
||||||
<input type="hidden" id="report-user-id">
|
<input type="hidden" id="report-user-id">
|
||||||
<textarea id="report-reason" class="mod-reason" placeholder="{{ t('report.placeholder') }}"></textarea>
|
<textarea id="report-reason" class="mod-reason" placeholder="{{ t('report.placeholder') }}"></textarea>
|
||||||
|
@if(recaptcha_enabled && !session)
|
||||||
|
<div id="modal-report-recaptcha" data-sitekey="{{ recaptcha_site_key }}" style="margin: 10px 0; display: flex; justify-content: center;"></div>
|
||||||
|
@endif
|
||||||
<div id="report-error" class="error-msg"></div>
|
<div id="report-error" class="error-msg"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="report-submit" class="btn-danger">{{ t('report.submit') }}</button>
|
<button id="report-submit" class="btn-danger">{{ t('report.submit') }}</button>
|
||||||
@@ -496,6 +499,8 @@
|
|||||||
reason_required: "{{ t('toast.reason_required') }}",
|
reason_required: "{{ t('toast.reason_required') }}",
|
||||||
reason_optional: "{{ t('toast.reason_optional') }}",
|
reason_optional: "{{ t('toast.reason_optional') }}",
|
||||||
reason_required_label: "{{ t('toast.reason_required_label') }}",
|
reason_required_label: "{{ t('toast.reason_required_label') }}",
|
||||||
|
captcha_required: "{{ t('toast.captcha_required') }}",
|
||||||
|
captcha_loading: "{{ t('toast.captcha_loading') }}",
|
||||||
confirm_yes: "{{ t('toast.yes') }}",
|
confirm_yes: "{{ t('toast.yes') }}",
|
||||||
confirm_no: "{{ t('toast.no') }}",
|
confirm_no: "{{ t('toast.no') }}",
|
||||||
confirm_btn: "{{ t('toast.confirm') }}",
|
confirm_btn: "{{ t('toast.confirm') }}",
|
||||||
|
|||||||
+224
-29
@@ -58,20 +58,6 @@
|
|||||||
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
|
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
|
||||||
@endif
|
@endif
|
||||||
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
|
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
|
||||||
@if(session.admin)
|
|
||||||
<a href="/admin">{{ t('nav.admin') }}
|
|
||||||
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
|
|
||||||
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/admin/approve';"></span>
|
|
||||||
@endif
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
@if(session.admin || session.is_moderator)
|
|
||||||
<a href="/mod">{{ t('nav.mod') }}
|
|
||||||
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
|
|
||||||
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/mod/approve';"></span>
|
|
||||||
@endif
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
|
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
|
||||||
<div class="nav-user-divider"></div>
|
<div class="nav-user-divider"></div>
|
||||||
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
|
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
|
||||||
@@ -156,9 +142,11 @@
|
|||||||
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
|
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(session.admin || session._is_impersonating)
|
</nav>
|
||||||
<!-- Admin Bar -->
|
|
||||||
<div class="admin-bar" id="admin-bar">
|
@if(session.admin || session._is_impersonating)
|
||||||
|
<!-- Admin Bar (Bottom, hotkey-activated) -->
|
||||||
|
<div class="admin-bar" id="admin-bar">
|
||||||
@if(session._is_impersonating)
|
@if(session._is_impersonating)
|
||||||
<!-- Impersonation Banner -->
|
<!-- Impersonation Banner -->
|
||||||
<div class="admin-bar-impersonate-notice">
|
<div class="admin-bar-impersonate-notice">
|
||||||
@@ -168,6 +156,9 @@
|
|||||||
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
|
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
|
||||||
<i class="fa-solid fa-door-open"></i> Exit
|
<i class="fa-solid fa-door-open"></i> Exit
|
||||||
</button>
|
</button>
|
||||||
|
<button class="admin-bar-close-btn" id="admin-bar-close-imp" title="Close admin bar (^ or ~)" aria-label="Close admin bar" style="margin-left: 8px;">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<!-- Normal Admin Bar -->
|
<!-- Normal Admin Bar -->
|
||||||
@@ -207,12 +198,14 @@
|
|||||||
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
|
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</nav>
|
<button class="admin-bar-close-btn" id="admin-bar-close" title="Close admin bar (^ or ~)" aria-label="Close admin bar">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Floating exit-preview pill (visible only during role preview) -->
|
<!-- Floating exit-preview pill (visible only during role preview) -->
|
||||||
@if(session.admin)
|
@if(session.admin)
|
||||||
@@ -380,6 +373,7 @@
|
|||||||
@if(session && session.admin)
|
@if(session && session.admin)
|
||||||
<div class="shortcut-item"><span><kbd>x</kbd></span><span>{{ t('shortcuts.delete') }}</span></div>
|
<div class="shortcut-item"><span><kbd>x</kbd></span><span>{{ t('shortcuts.delete') }}</span></div>
|
||||||
<div class="shortcut-item"><span><kbd>p</kbd></span><span>{{ t('shortcuts.toggle_rating') }}</span></div>
|
<div class="shortcut-item"><span><kbd>p</kbd></span><span>{{ t('shortcuts.toggle_rating') }}</span></div>
|
||||||
|
<div class="shortcut-item"><span><kbd>^</kbd> / <kbd>~</kbd></span><span>Toggle Admin Bar</span></div>
|
||||||
@endif
|
@endif
|
||||||
<div class="shortcut-item"><span><kbd>Space</kbd></span><span>{{ t('shortcuts.play_pause') }}</span></div>
|
<div class="shortcut-item"><span><kbd>Space</kbd></span><span>{{ t('shortcuts.play_pause') }}</span></div>
|
||||||
<div class="shortcut-item"><span><kbd>←</kbd> <kbd>→</kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
|
<div class="shortcut-item"><span><kbd>←</kbd> <kbd>→</kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
|
||||||
@@ -483,6 +477,7 @@
|
|||||||
window.onRecaptchaModalReady = function() {
|
window.onRecaptchaModalReady = function() {
|
||||||
_rcLoaded = true;
|
_rcLoaded = true;
|
||||||
renderModalRecaptcha();
|
renderModalRecaptcha();
|
||||||
|
if (typeof window.renderReportRecaptcha === 'function') window.renderReportRecaptcha();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -506,23 +501,111 @@
|
|||||||
<style>
|
<style>
|
||||||
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
|
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
|
||||||
.admin-bar {
|
.admin-bar {
|
||||||
width: 100%;
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: var(--admin-bar-left, 0px);
|
||||||
|
right: var(--admin-bar-right, 300px);
|
||||||
|
width: auto;
|
||||||
font-family: var(--font, system-ui, sans-serif);
|
font-family: var(--font, system-ui, sans-serif);
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
z-index: 900;
|
z-index: 100080 !important;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
|
box-shadow: 0 -3px 14px rgba(0, 0, 0, 0.5);
|
||||||
border-bottom: 1px solid rgba(255, 180, 0, 0.18);
|
border-top: 1px solid rgba(255, 190, 0, 0.28);
|
||||||
|
border-bottom: none;
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
opacity 0.22s ease,
|
||||||
|
left 0.3s ease-in-out,
|
||||||
|
right 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-bar.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-open .admin-bar {
|
||||||
|
z-index: 100080 !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.onara-modal-open .admin-bar.open {
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar boundaries via CSS */
|
||||||
|
body.sidebar-right-hidden .admin-bar {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 999px) {
|
||||||
|
.admin-bar {
|
||||||
|
right: min(280px, 80vw);
|
||||||
|
}
|
||||||
|
body.sidebar-right-hidden .admin-bar {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(:has(.global-sidebar-right, .item-sidebar-right, .index-sidebar-right)) .admin-bar {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
body.layout-modern:not(.sidebar-left-hidden):not(.comments-hidden):has(.item-sidebar-left) .admin-bar {
|
||||||
|
left: min(350px, 30vw);
|
||||||
|
}
|
||||||
|
body.layout-modern.sidebar-left-hidden .admin-bar,
|
||||||
|
body.layout-modern.comments-hidden .admin-bar,
|
||||||
|
body:has(.item-layout-container.sidebar-hidden) .admin-bar {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lift bottom pagination smoothly if admin bar is open */
|
||||||
|
body.admin-bar-open .pagination-container-fluid {
|
||||||
|
bottom: 32px;
|
||||||
|
}
|
||||||
|
.pagination-container-fluid {
|
||||||
|
transition: right 0.3s ease-in-out, bottom 0.3s ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-bar-close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 210, 80, 0.45);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 6px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.admin-bar-close-btn:hover {
|
||||||
|
color: #ffd844;
|
||||||
|
background: rgba(255, 200, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Normal admin bar */
|
/* Normal admin bar */
|
||||||
.admin-bar-inner {
|
.admin-bar-inner {
|
||||||
background: linear-gradient(90deg, #1a1200 0%, #1e1600 40%, #141000 100%);
|
background: linear-gradient(90deg, #1a1200 0%, #1e1600 40%, #141000 100%);
|
||||||
border-top: 1px solid rgba(255, 190, 0, 0.22);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
height: 30px;
|
height: 32px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +730,7 @@
|
|||||||
border: 1px solid rgba(255, 200, 0, 0.3);
|
border: 1px solid rgba(255, 200, 0, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
|
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
|
||||||
z-index: 9999;
|
z-index: 100090 !important;
|
||||||
display: none;
|
display: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
@@ -846,6 +929,7 @@ body.preview-as-user .admin-pin-btn,
|
|||||||
body.preview-as-user .delete-btn,
|
body.preview-as-user .delete-btn,
|
||||||
body.preview-as-user .poll-delete-btn,
|
body.preview-as-user .poll-delete-btn,
|
||||||
body.preview-as-user [data-is-admin] .admin-only { display: none !important; }
|
body.preview-as-user [data-is-admin] .admin-only { display: none !important; }
|
||||||
|
body.preview-as-user .item_title_text { display: inline !important; }
|
||||||
|
|
||||||
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */
|
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */
|
||||||
body.preview-as-mod #a_rethumb,
|
body.preview-as-mod #a_rethumb,
|
||||||
@@ -866,6 +950,117 @@ body.preview-as-mod .btn-infobox-action { display: none !important; }
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/* ── Admin bar: Sidebar Width Awareness & Hotkey Toggle ────────────────── */
|
||||||
|
const adminBar = document.getElementById('admin-bar');
|
||||||
|
const closeBtn = document.getElementById('admin-bar-close');
|
||||||
|
const closeImpBtn = document.getElementById('admin-bar-close-imp');
|
||||||
|
|
||||||
|
function syncAdminBarSidebars() {
|
||||||
|
if (!adminBar) return;
|
||||||
|
|
||||||
|
// Right sidebar detection
|
||||||
|
const rightSb = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
|
const isRightHidden = !rightSb ||
|
||||||
|
document.body.classList.contains('sidebar-right-hidden') ||
|
||||||
|
window.getComputedStyle(rightSb).display === 'none' ||
|
||||||
|
window.getComputedStyle(rightSb).visibility === 'hidden';
|
||||||
|
|
||||||
|
let rw = 0;
|
||||||
|
if (!isRightHidden && rightSb) {
|
||||||
|
rw = rightSb.offsetWidth || (window.innerWidth <= 999 ? Math.min(280, Math.round(window.innerWidth * 0.8)) : 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left sidebar detection (modern layout item page)
|
||||||
|
const leftSb = document.querySelector('.item-sidebar-left');
|
||||||
|
const isLeftHidden = !leftSb ||
|
||||||
|
document.body.classList.contains('sidebar-left-hidden') ||
|
||||||
|
document.body.classList.contains('comments-hidden') ||
|
||||||
|
window.innerWidth < 1000 ||
|
||||||
|
window.getComputedStyle(leftSb).display === 'none' ||
|
||||||
|
window.getComputedStyle(leftSb).visibility === 'hidden' ||
|
||||||
|
!!document.querySelector('.item-layout-container.sidebar-hidden');
|
||||||
|
|
||||||
|
let lw = 0;
|
||||||
|
if (!isLeftHidden && leftSb) {
|
||||||
|
lw = leftSb.offsetWidth || Math.min(350, Math.round(window.innerWidth * 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
adminBar.style.setProperty('--admin-bar-right', rw + 'px');
|
||||||
|
adminBar.style.setProperty('--admin-bar-left', lw + 'px');
|
||||||
|
adminBar.style.right = rw + 'px';
|
||||||
|
adminBar.style.left = lw + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdminBar() {
|
||||||
|
if (!adminBar) return;
|
||||||
|
syncAdminBarSidebars();
|
||||||
|
adminBar.classList.add('open');
|
||||||
|
document.body.classList.add('admin-bar-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAdminBar() {
|
||||||
|
if (!adminBar) return;
|
||||||
|
adminBar.classList.remove('open');
|
||||||
|
document.body.classList.remove('admin-bar-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAdminBar() {
|
||||||
|
if (!adminBar) return;
|
||||||
|
if (adminBar.classList.contains('open')) {
|
||||||
|
closeAdminBar();
|
||||||
|
} else {
|
||||||
|
openAdminBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeBtn) closeBtn.addEventListener('click', closeAdminBar);
|
||||||
|
if (closeImpBtn) closeImpBtn.addEventListener('click', closeAdminBar);
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
// Never trigger if focus is inside an input, textarea, select or contenteditable
|
||||||
|
const target = e.target;
|
||||||
|
const tag = (target && target.tagName) ? target.tagName.toUpperCase() : '';
|
||||||
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (target && target.isContentEditable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hotkey: '^' or '~' (also match Backquote key without modifier keys)
|
||||||
|
const isHotkey = (
|
||||||
|
e.key === '^' ||
|
||||||
|
e.key === '~' ||
|
||||||
|
(e.code === 'Backquote' && !e.ctrlKey && !e.metaKey && !e.altKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHotkey) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAdminBar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape closes admin bar if open
|
||||||
|
if (e.key === 'Escape' && adminBar && adminBar.classList.contains('open')) {
|
||||||
|
closeAdminBar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch body class changes for sidebar toggles
|
||||||
|
if (window.MutationObserver) {
|
||||||
|
const bodyObserver = new MutationObserver(() => {
|
||||||
|
syncAdminBarSidebars();
|
||||||
|
});
|
||||||
|
bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', syncAdminBarSidebars, { passive: true });
|
||||||
|
document.addEventListener('f0ck:contentLoaded', syncAdminBarSidebars);
|
||||||
|
document.addEventListener('transitionend', (e) => {
|
||||||
|
if (e.target && (e.target.classList.contains('global-sidebar-right') ||
|
||||||
|
e.target.classList.contains('item-sidebar-right') ||
|
||||||
|
e.target.classList.contains('item-sidebar-left'))) {
|
||||||
|
syncAdminBarSidebars();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ── Admin bar: View-As (impersonation) ────────────────────────────────── */
|
/* ── Admin bar: View-As (impersonation) ────────────────────────────────── */
|
||||||
const input = document.getElementById('admin-viewas-input');
|
const input = document.getElementById('admin-viewas-input');
|
||||||
const suggestBox = document.getElementById('admin-viewas-suggestions');
|
const suggestBox = document.getElementById('admin-viewas-suggestions');
|
||||||
|
|||||||
Reference in New Issue
Block a user