Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_maintenance.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_maintenance.cgi

added on local at 2026-07-01 14:13:41

Added
+247
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 3ff474f70e1b
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2
3##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26use CGI; my $query = new CGI; my $form = $query->Vars;
27use MODS::Login; my $login = new MODS::Login;
28use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls();
29use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
30use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config;
31use MODS::DBConnect; my $db = new MODS::DBConnect;
32
33
34
35#############################################################
36## Get form data
37#############################################################
38#The action endpoint accepts an HTML message body - preserve it before quotemeta
39my $message_raw = $form->{message};
40foreach my $line(keys %$form){
41 $form->{$line} =~ s/[^!-~\s]//g;
42 $form->{$line} = quotemeta("$form->{$line}");
43}
44$form->{message} = $message_raw if defined $message_raw;
45
46
47
48############################################################
49# Program Controls
50############################################################
51$|=1;
52my $userinfo = $login->login_verify();
53my $is_action = (($ENV{SCRIPT_NAME} || '') =~ m{/admin_maintenance_action\.cgi$}) ? 1 : 0;
54
55if(!$userinfo){
56 if($is_action){print qq~Status: 403 Forbidden\nContent-Type: text/plain\n\nsuper-admin only\n~; exit;}
57 print qq~Status: 302 Found\nLocation: $url->{login}\n\n~;
58 exit;
59}
60if(!$userinfo->{is_super_admin}){
61 if($is_action){print qq~Status: 403 Forbidden\nContent-Type: text/plain\n\nsuper-admin only\n~; exit;}
62 print qq~Status: 403 Forbidden\nContent-Type: text/html\n\n<h1>Forbidden</h1><p>Only super-admins can manage maintenance mode.</p>\n~;
63 exit;
64}
65
66if($is_action){
67 my $op = $form->{op} || '';
68 if ($op eq 'lock' || $op eq 'unlock'){&toggle_lock($op);}
69 elsif($op eq 'save_message') {&save_message;}
70 else {print qq~Status: 302 Found\nLocation: $url->{admin_maintenance}&err=unknown_op\n\n~;}
71}else{
72 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
73 &maintenance_panel;
74}
75
76
77############################################################
78# Subroutines
79############################################################
80sub esc{
81 my $s = shift // '';
82 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g;
83 return $s;
84}
85
86sub toggle_lock{
87 my($op) = @_;
88 my $dbh = $db->db_connect();
89 if(!$dbh){
90 print qq~Status: 302 Found\nLocation: $url->{admin_maintenance}&err=db_unavailable\n\n~;
91 return;
92 }
93
94 my $v = ($op eq 'lock') ? '1' : '0';
95 my $sql = qq~INSERT INTO platform_settings (setting_key, setting_value) VALUES ('maintenance_locked', '$v') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)~;
96 $db->db_readwrite($dbh, $sql, __FILE__, __LINE__);
97 $db->db_disconnect($dbh);
98
99 my $flag = ($op eq 'lock') ? 'locked=1' : 'unlocked=1';
100 print qq~Status: 302 Found\nLocation: $url->{admin_maintenance}&$flag\n\n~;
101}
102
103sub save_message{
104 my $dbh = $db->db_connect();
105 if(!$dbh){
106 print qq~Status: 302 Found\nLocation: $url->{admin_maintenance}&err=db_unavailable\n\n~;
107 return;
108 }
109
110 #Cap to keep platform_settings reasonable, then SQL-escape
111 my $msg = defined($form->{message}) ? $form->{message} : '';
112 $msg = substr($msg, 0, 20000) if length($msg) > 20000;
113 my $msg_q = $msg;
114 $msg_q =~ s/\\/\\\\/g;
115 $msg_q =~ s/'/''/g;
116
117 my $sql = qq~INSERT INTO platform_settings (setting_key, setting_value) VALUES ('maintenance_message', '$msg_q') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)~;
118 $db->db_readwrite($dbh, $sql, __FILE__, __LINE__);
119 $db->db_disconnect($dbh);
120
121 print qq~Status: 302 Found\nLocation: $url->{admin_maintenance}&saved=1\n\n~;
122}
123
124sub maintenance_panel{
125 my $locked = int($config->settings('maintenance_locked') || 0);
126 my $msg = $config->settings('maintenance_message') || '';
127
128 #Flash banners driven by the ?saved=1 / ?locked=1 / ?unlocked=1 query params
129 #set by the action endpoint after each save
130 my $flash = '';
131 if($form->{saved}) {$flash = '<div class="banner success">Maintenance message saved.</div>';}
132 if($form->{locked}) {$flash = '<div class="banner warn"><strong>System is now LOCKED.</strong> Only super-admins can sign in or stay signed in.</div>';}
133 if($form->{unlocked}) {$flash = '<div class="banner success">System is now open to all users.</div>';}
134 if($form->{err}) {my $e = &esc($form->{err}); $flash = qq~<div class="banner danger">$e</div>~;}
135
136 my $msg_h = &esc($msg);
137 my $status_lbl = $locked ? 'LOCKED' : 'OPEN';
138 my $status_pill_class = $locked ? 'pill-bad' : 'pill-ok';
139 my $toggle_op = $locked ? 'unlock' : 'lock';
140 my $toggle_lbl = $locked ? 'Unlock System' : 'Lock System';
141 my $toggle_btn_class = $locked ? 'btn btn-primary' : 'btn btn-danger';
142 my $toggle_confirm = $locked
143 ? "return confirm('Unlock the site? Regular users will be able to sign in again.');"
144 : "return confirm('Lock the site? Only super-admins will be able to sign in until you unlock. Existing non-super-admin sessions are killed immediately.');";
145
146 my $body = qq~
147<div class="page-head"><div class="left">
148 <span class="page-eyebrow"><span class="dot"></span> Operations</span>
149 <h1 class="page-title">Maintenance mode</h1>
150 <p class="page-subtitle">Block all non-super-admin users from signing in (or staying signed in), with a custom message shown on the login page.</p>
151</div></div>
152$flash
153
154<div class="module" style="margin-bottom:16px">
155 <div class="module-head"><div class="left"><div class="module-title">Status</div></div></div>
156 <div class="module-body" style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">
157 <span class="pill $status_pill_class" style="font-size:14px;padding:4px 12px">$status_lbl</span>
158 <form method="POST" action="$url->{admin_maintenance_action}" style="margin:0" onsubmit="$toggle_confirm">
159 <input type="hidden" name="op" value="$toggle_op">
160 <button type="submit" class="$toggle_btn_class">$toggle_lbl</button>
161 </form>
162 <div style="flex:1"></div>
163 <a href="$url->{login}&_preview_maintenance=1" target="_blank" class="btn">View as user (opens login)</a>
164 </div>
165</div>
166
167<div class="module">
168 <div class="module-head"><div class="left"><div class="module-title">Message users see while locked</div></div></div>
169 <div class="module-body">
170 <form method="POST" action="$url->{admin_maintenance_action}">
171 <input type="hidden" name="op" value="save_message">
172 <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:12px">
173 <div>
174 <label class="lbl"><span class="t">HTML source</span>
175 <style>
176 #maint_src{width:100%;height:360px;padding:12px 14px;background-color:#0a0a0a;color:#cbd5e1;color-scheme:dark;border:1px solid rgba(255,255,255,.08);border-radius:8px;font-family:'JetBrains Mono','Consolas',monospace;font-size:12px;line-height:1.5;resize:vertical;box-sizing:border-box}
177 </style>
178 <textarea id="maint_src" name="message" spellcheck="false">$msg_h</textarea>
179 </label>
180 <div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px;font-size:11px">
181 <button type="button" class="btn btn-sm" onclick="wrap('<strong>','</strong>')">B</button>
182 <button type="button" class="btn btn-sm" onclick="wrap('<em>','</em>')"><em>I</em></button>
183 <button type="button" class="btn btn-sm" onclick="wrap('<h2>','</h2>')">H2</button>
184 <button type="button" class="btn btn-sm" onclick="wrap('<p>','</p>')">P</button>
185 <button type="button" class="btn btn-sm" onclick="ins('<br>')">BR</button>
186 <button type="button" class="btn btn-sm" onclick="linkPrompt()">Link</button>
187 <button type="button" class="btn btn-sm" onclick="resetMsg()">Reset to default</button>
188 </div>
189 </div>
190 <div>
191 <label class="lbl"><span class="t">Live preview</span>
192 <iframe id="maint_preview"
193 style="width:100%;height:360px;background:#0a0a0a;border:1px solid var(--col-border);border-radius:5px"></iframe>
194 </label>
195 <div class="dim" style="font-size:11px;margin-top:4px">Preview updates as you type. Bg is the same dark theme users see on /login.cgi.</div>
196 </div>
197 </div>
198 <button type="submit" class="btn btn-primary">Save message</button>
199 </form>
200 </div>
201</div>
202
203<script>
204(function(){
205 var ta = document.getElementById('maint_src');
206 var pv = document.getElementById('maint_preview');
207 function refresh() {
208 var html = '<style>body{font-family:system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:#0a0a0a;color:#e5e5e5;margin:0;padding:24px;min-height:100vh;display:flex;align-items:center;justify-content:center}</style>' + ta.value;
209 pv.srcdoc = html;
210 }
211 ta.addEventListener('input', refresh);
212 refresh();
213 window.wrap = function(o,c){
214 var s=ta.selectionStart, e=ta.selectionEnd, v=ta.value;
215 ta.value = v.slice(0,s) + o + v.slice(s,e) + c + v.slice(e);
216 ta.focus(); ta.selectionStart=s+o.length; ta.selectionEnd=e+o.length; refresh();
217 };
218 window.ins = function(t){
219 var s=ta.selectionStart, v=ta.value;
220 ta.value = v.slice(0,s) + t + v.slice(s);
221 ta.focus(); ta.selectionStart=ta.selectionEnd=s+t.length; refresh();
222 };
223 window.linkPrompt = function(){
224 var url = prompt('Link URL', 'https://');
225 if (!url) return;
226 wrap('<a href="' + url + '">', '</a>');
227 };
228 window.resetMsg = function(){
229 if (!confirm('Reset to default message? Unsaved edits will be lost.')) return;
230 ta.value = '<div style="text-align:center;padding:40px 20px;max-width:560px;margin:0 auto">' +
231 '<h2 style="margin:0 0 14px;font-size:28px">We will be right back</h2>' +
232 '<p style="margin:0 0 14px;color:#aaa;font-size:16px;line-height:1.6">We are rolling out a quick improvement to the system. Most updates wrap up in under an hour.</p>' +
233 '<p style="margin:24px 0 0;color:#888;font-size:13px">Thanks for your patience. -- The team</p>' +
234 '</div>';
235 refresh();
236 };
237})();
238</script>
239~;
240
241 $load->render({
242 userinfo => $userinfo,
243 page_key => 'admin_maintenance',
244 title => 'Maintenance',
245 body => $body,
246 });
247}