XCL Web Application Platform 2.5.0
The XoopsCube Legacy Project
📘  
Loading...
Searching...
No Matches
recovery.php
1<?php
11
12// XOOPSCube XCL Language File Audit Tool
13// Scans all modules in XOOPS_ROOT_PATH and XOOPS_TRUST_PATH for language files
14// Compares all constants in /english/*.php to those in each other language folder
15// Outputs an HTML report with emoji for success/fail
16// Add also reports extra constants
17// (defined in translation but not in English, or not used anywhere on the code)
18
19// Polyfill for PHP < 8 str_contains
20if (!function_exists('str_contains')) {
21 function str_contains($haystack, $needle) {
22 return $needle !== '' && mb_strpos($haystack, $needle) !== false;
23 }
24}
25
26// Emergency language reset handler (must be at top, before any output)
27if (isset($_GET['resetlang']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
28 header('Content-Type: application/json');
29 require_once dirname(__FILE__) . '/mainfile.php';
31 $table = XOOPS_DB_PREFIX . '_config';
32 $sql = "UPDATE `$table` SET conf_value='english' WHERE conf_name='language' AND conf_modid=0";
33 $ok = $db->queryF($sql);
34 if ($ok) {
35 echo json_encode(['success' => true]);
36 } else {
37 echo json_encode(['success' => false, 'error' => 'DB update failed']);
38 }
39 exit;
40}
41
42require_once __DIR__ . '/mainfile.php';
43
44// Only enforce admin session for normal web requests
45global $xoopsUser;
46if (!is_object($xoopsUser) || !in_array(1, $xoopsUser->getGroups())) {
47 header('HTTP/1.1 403 Forbidden');
48 echo '<h1>403 Forbidden</h1><p>Admin access only.</p>';
49 exit();
50}
51
52// Extras for D3 modules prefix
53function extract_defines($file, $mydirname = null) {
54 $defines = [];
55 if (!file_exists($file)) return $defines;
56 $content = file_get_contents($file);
57 // D3 module support: detect $constpref assignment
58 $constpref = null;
59 if (preg_match('/\$constpref\s*=\s*([\'\"])?(_MB_|_MI_|_MD_)?\.'
60 . '\s*strtoupper\s*\‍(\s*\$mydirname\s*\‍)\s*([\'\"])?/i', $content)) {
61 // If $mydirname is not set, try to infer from path
62 if (!$mydirname) {
63 // e.g. .../modules/d3forum/language/...
64 if (preg_match('#modules/([^/]+)/language/#', str_replace('\\','/',$file), $m)) {
65 $mydirname = $m[1];
66 }
67 }
68 if ($mydirname) {
69 // Try to infer prefix (e.g. _MB_D3FORUM_)
70 if (preg_match('/\$constpref\s*=\s*([\'\"])?(_MB_|_MI_|_MD_)?\.'
71 . '\s*strtoupper\s*\‍(\s*\$mydirname\s*\‍)\s*([\'\"])?/i', $content, $pm)) {
72 $constpref = ($pm[2] ?? '') . strtoupper($mydirname) . '_';
73 }
74 }
75 }
76 // Match define($constpref . '_XYZ', ...)
77 if ($constpref && preg_match_all('/define\s*\‍(\s*\$constpref\s*\.\s*[\'\"](_[A-Z0-9]+)[\'\"]\s*,/', $content, $matches)) {
78 foreach ($matches[1] as $suffix) {
79 $defines[] = $constpref . ltrim($suffix, '_');
80 }
81 }
82 // Also match normal define('CONSTANT', ...)
83 if (preg_match_all("/define\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,/", $content, $matches2)) {
84 foreach ($matches2[1] as $c) {
85 // Avoid duplicates
86 if (!in_array($c, $defines)) $defines[] = $c;
87 }
88 }
89 return $defines;
90}
91
92// Get all used constants in codebase (for extra constant reporting)
93function get_all_used_constants($root_dirs) {
94 $used = [];
95 foreach ($root_dirs as $dir) {
96 $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
97 foreach ($rii as $file) {
98 if ($file->isFile() && strtolower($file->getExtension()) === 'php') {
99 $content = file_get_contents($file->getPathname());
100 // Match both constant('_CONST') and _CONST direct usage
101 if (preg_match_all("/(['\"])(_[_A-Z0-9]+)\\1|\b(_[_A-Z0-9]+)\b/", $content, $matches)) {
102 foreach (array_merge($matches[2], $matches[3]) as $const) {
103 if ($const) $used[$const] = true;
104 }
105 }
106 }
107 }
108 }
109 return $used;
110}
111
112$roots = [
113 __DIR__ . '/modules',
114 dirname(__DIR__) . '/xoops_trust_path/modules'
115];
116$code_roots = [
117 __DIR__,
118 dirname(__DIR__) . '/xoops_trust_path'
119];
120$used_constants = get_all_used_constants($code_roots);
121
122$all_reports = [];
123
124foreach ($roots as $base) {
125 foreach (glob($base . '/*', GLOB_ONLYDIR) as $moddir) {
126 $module = basename($moddir);
127 $langbase = $moddir . '/language';
128 if (!is_dir($langbase)) continue;
129 $languages = [];
130 foreach (glob($langbase . '/*', GLOB_ONLYDIR) as $langdir) {
131 $langcode = basename($langdir);
132 $languages[$langcode] = true;
133 }
134 $englishdir = $langbase . '/english';
135 if (!is_dir($englishdir)) continue;
136 foreach (glob($englishdir . '/*.php') as $efile) {
137 $fname = basename($efile);
138 $edefs = extract_defines($efile);
139 foreach ($languages as $langcode => $_) {
140 if ($langcode == 'english') continue;
141 $tfile = $langbase . '/' . $langcode . '/' . $fname;
142
143// --- Add Debugging Here ---
144// if ($langcode == 'pt_utf8' && $fname == 'admin.php') { // Optional: Only print for the specific file
145// error_log("Checking for file: " . $tfile); // Log to PHP error log
146 // Or use echo for direct output (might clutter the page):
147// echo "Checking for file: " . $tfile . "<br>";
148// echo "File exists check: " . (file_exists($tfile) ? 'TRUE' : 'FALSE') . "<br>";
149// echo "File is readable check: " . (is_readable($tfile) ? 'TRUE' : 'FALSE') . "<br>";
150// }
151 // --- End Debugging ---
152
153 $tdefs = extract_defines($tfile);
154 $missing = array_diff($edefs, $tdefs);
155 $extra = array_diff($tdefs, $edefs);
156 $extra_unused = array_filter($extra, function($const) use ($used_constants) {
157 return !isset($used_constants[$const]);
158 });
159 $all_reports[] = [
160 'module' => $module,
161 'lang' => $langcode,
162 'file' => $fname,
163 'missing' => $missing,
164 'extra' => $extra,
165 'extra_unused' => $extra_unused,
166 'ok' => empty($missing)
167 ];
168 }
169 }
170 }
171}
172
173// Collect unique modules and languages
174$modules = [];
175$languages = [];
176foreach ($all_reports as $r) {
177 $modules[$r['module']] = true;
178 $languages[$r['lang']] = true;
179}
180ksort($modules); ksort($languages);
181
182// --- END PHP PROCESSING SECTION ---
183
184header('Content-Type: text/html; charset=utf-8');
185?>
186<!DOCTYPE html>
187<html
188<head>
189 <title>XOOPSCube XCL - Recovery Language Audit</title>
190 <meta charset="UTF-8">
191 <meta name="viewport" content="width=device-width, initial-scale=1">
192 <style>
193 *::before,*::after {box-sizing: border-box;}
194 body{font:14px sans-serif;}
195 .ok{color:green;}.fail{color:red;}.flex{display:flex;}
196 .flex-1{padding:1rem;width: 50%;}ul{margin:0 0 1em 2em;}
197 li{margin:0.2em 0;}section {max-width: 1024px; margin:0 auto 2rem;}
198 .audit-header{background:#f5f5f5;padding:1em 2em 1em 2em;border-bottom:1px solid #ccc;}
199 .audit-btn{background:#0074d9;color:#fff;border:none;padding:0.5em 1.25em;margin: 1em 0 0;margin-top:1em;font-size:1.1em;border-radius:3px;cursor:pointer;}
200 .audit-btn:active{background:#005fa3;} #newauditbtn{background:#ff851b;}
201 .audit-result-wrap{margin:1em auto 0;max-width:1100px;background:#fff;position:relative}
202 .audit-controls{position:sticky;top:0;z-index:10;background-color:#f8f8f8;padding:1em 1.5em;border-bottom:1px solid #ddd;margin:0 auto;max-width:1100px;box-sizing:border-box}
203 #audit-results-inner{height:480px;overflow-y:auto;padding:1.5em 2em;border:1px solid #eee;margin-top:1em}
204 .audit-export-btn{background:#1b932a;border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:1em;margin:1em 0 0;padding:.65em}
205 .audit-export-btn:active{background:#229c2b}
206 input,select{max-width:180px;position:relative;appearance:none;background-color:#fff;border:1px solid #caced1;border-radius:4px;color:#111;cursor:pointer;padding:.5em;width:100%}
207 select{background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m13 16.172l5.364-5.364l1.414 1.414L12 20l-7.778-7.778l1.414-1.414L11 16.172V4h2z'/%3E%3C/svg%3E") right 5% bottom 50%/.8em auto no-repeat #fff}
208 .lang-reset-dialog{background:#b22222;border:1px solid #ffa9a9;border-radius:4px;color:#facfcf;margin:1em 0 1.5em;padding:1em 2em;text-align:center}
209 #lang-reset-english-btn{background:#d9534f;border:none;border-radius:4px;color:#fff;cursor:pointer;font-weight:700;padding:.5em 1.25em}
210 </style>
211</head>
212<body>
213<section>
214 <div class="audit-header">
215 <h1>XOOPSCube XCL Language Audit Tool</h1>
216 <p>This tool scans all modules for language files and compares constants between English and other languages.</p>
217 <div class="flex">
218 <div class="flex-1">
219 <button id="runbtn" class="audit-btn" onclick="runAudit()">Start Audit</button>
220 <button id="newauditbtn" class="audit-btn" onclick="newAudit()" style="display:none;">New Audit</button>
221 </div>
222 <?php
223 if (is_object($xoopsUser) && $xoopsUser->isAdmin(1)) {
224 echo '<div class="flex-1 lang-reset-dialog">
225 <strong>Stuck with a broken language?</strong><br>
226 <button id="lang-reset-english-btn">Reset Site Language to English</button><br>
227 <small>(Emergency recovery)</small>
228 </div>';
229 }
230 ?>
231 </div>
232 <div id="audit-loading" style="display:none;margin-top:1em;">
233 <p>Analyzing language files... please wait.</p>
234 </div>
235 </div>
236
237<!-- Controls Section -->
238<div id="audit-controls-section" class="audit-controls" style="display:none;">
239 <div style="display:flex;flex-wrap:wrap;gap:1em;align-items:center;">
240 <div style="flex:1;">
241 <label for="audit-module"><b>Filter by Module:</b></label>
242 <select id="audit-module" onchange="filterReports()" style="width:100%;">
243 <option value="">All Modules</option>
244 <?php foreach (array_keys($modules) as $m): ?>
245 <option value="<?php echo htmlspecialchars($m); ?>"><?php echo htmlspecialchars($m); ?></option>
246 <?php endforeach; ?>
247 </select>
248 </div>
249 <div style="flex:1;">
250 <label for="audit-lang"><b>Filter by Language:</b></label>
251 <select id="audit-lang" onchange="filterReports()" style="width:100%;">
252 <option value="">All Languages</option>
253 <?php foreach (array_keys($languages) as $l): ?>
254 <option value="<?php echo htmlspecialchars($l); ?>"><?php echo htmlspecialchars($l); ?></option>
255 <?php endforeach; ?>
256 </select>
257 </div>
258 <div style="flex:1;">
259 <label for="audit-file"><b>Filter by Filename:</b></label>
260 <input type="text" id="audit-file" onkeyup="filterReports()" placeholder="e.g. admin.php" style="width:100%;">
261 </div>
262 <div style="flex:1;">
263 <label for="audit-export-select"><b>Export:</b></label>
264 <select id="audit-export-select" style="min-width:150px;">
265 <option value="">Export Options</option>
266 <option value="view-html">View as HTML</option>
267 <option value="download-html">Download HTML</option>
268 <option value="download-csv">Download CSV</option>
269 <option value="download-json">Download JSON</option>
270 </select>
271 </div>
272 <div><button id="audit-send-email-btn" class="audit-export-btn">Send Report via Email</button></div>
273 </div>
274<!-- // --- Email Modal (can stay here, it's position:fixed) -->
275
276<div id="audit-email-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
277 <div style="background:firebrick;color:#fff;padding:2em 2.5em;border-radius:8px;max-width:350px;margin:auto;box-shadow:0 4px 18px #0002;">
278 <h>Send Report via Email</h3>
279 <form id="audit-email-form" onsubmit="return false;">
280 <label>Email: <input type="email" id="audit-email-to" required style="width:100%;margin-bottom:1em;"></label>
281 <div style="text-align:right;">
282 <button type="button" onclick="closeAuditEmailModal()" style="margin-right:0.7em;">Cancel</button>
283 <button type="submit">Send</button>
284 </div>
285 <div id="audit-email-status" style="color:#0074d9;margin-top:0.6em;"></div>
286 </form>
287 </div>
288</div>
289</div><!-- // End of audit-controls -->
290<!-- Summary container -->
291<div id="audit-summary-container"></div>
292</div>
293<!-- Results Section -->
294<div id="audit-results" class="audit-result-wrap" style="display:none;">
295 <div id="audit-results-inner"></div>
296</div>
297<script>
298// All JavaScript functions grouped together
299 let allReports = <?php echo json_encode($all_reports, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_UNESCAPED_UNICODE); ?>;
300 // Filter reports based on selected criteria
301 function filterReports() {
302 let mod = document.getElementById("audit-module").value;
303 let lang = document.getElementById("audit-lang").value;
304 let fileFilter = document.getElementById("audit-file").value.toLowerCase();
305
306 let results = allReports.filter(r =>
307 (mod === "" || r.module === mod) &&
308 (lang === "" || r.lang === lang) &&
309 (fileFilter === "" || r.file.toLowerCase().includes(fileFilter))
310 );
311 renderReports(results);
312 }
313 // Render the filtered reports
314 function renderReports(reports) {
315 // Summary statistics
316 let totalModules = new Set(reports.map(r => r.module)).size;
317 let totalLangs = new Set(reports.map(r => r.lang)).size;
318 let totalFiles = reports.length;
319 let totalMissing = reports.reduce((sum, r) => sum + (r.missing ? r.missing.length : 0), 0);
320 let totalExtra = reports.reduce((sum, r) => sum + (r.extra ? r.extra.length : 0), 0);
321 let totalUnused = reports.reduce((sum, r) => sum + (r.extra_unused ? r.extra_unused.length : 0), 0);
322 let totalOk = reports.filter(r => r.ok).length;
323
324 // Generate summary HTML
325 let summaryHtml = `<div id="action-control" style="border:1px solid #eee; background:#f8f8f8; padding:1em 1.5em; margin-bottom:1.2em; border-radius:4px;">
326 <b>Summary:</b>
327 <span title='Modules'><b>🧩 ${totalModules}</b> modules</span> &nbsp;
328 <span title='Languages'><b>🌐 ${totalLangs}</b> languages</span> &nbsp;
329 <span title='Files'><b>📄 ${totalFiles}</b> files</span> &nbsp;
330 <span title='OK'><b>✅ ${totalOk}</b> all present</span> &nbsp;
331 <span title='Missing'><b>❌ ${totalMissing}</b> missing</span> &nbsp;
332 <span title='Extra'><b>⚠️ ${totalExtra}</b> extra</span> &nbsp;
333 <span title='Unused'><b>🚫 ${totalUnused}</b> unused</span>
334 </div>`;
335
336 // Update summary container
337 document.getElementById("audit-summary-container").innerHTML = summaryHtml;
338
339 // Generate detailed report HTML
340 let reportHtml = "";
341 for (let report of reports) {
342 let icon = report.ok ? "✅" : "❌";
343 reportHtml += `<h3>${icon} Module: <b>${report.module}</b> &mdash; Language: <b>${report.lang}</b> &mdash; File: <b>${report.file}</b></h3>`;
344 if (report.ok) {
345 reportHtml += `<div class="ok">All constants present.</div>`;
346 } else {
347 // Check for missing constants
348 if (report.missing && report.missing.length) {
349 reportHtml += `<div class="fail">Missing constants:<ul>`;
350 for (let c of report.missing) reportHtml += `<li>❌ <code>${c}</code></li>`;
351 reportHtml += `</ul></div>`;
352 }
353
354 // Check for extra constants
355 if (report.extra && report.extra.length) {
356 reportHtml += `<div>Extra constants:<ul>`;
357 for (let c of report.extra) {
358 let warn = (report.extra_unused && report.extra_unused.includes(c)) ? " <span style='color:red'>🚫 Not used in code</span>" : "";
359 reportHtml += `<li>⚠️ <code>${c}</code>${warn}</li>`;
360 }
361 reportHtml += `</ul></div>`;
362 }
363 }
364 }
365
366 // Add legend
367 reportHtml += `<hr><div>✅ = All constants present<br>❌ = Missing constant<br>⚠️ = Extra constant in translation<br>🚫 = Extra constant not used anywhere in codebase</div>`;
368
369 // Update results container
370 document.getElementById("audit-results-inner").innerHTML = reportHtml;
371 }
372 // Start the audit process
373 function runAudit() {
374 const runBtn = document.getElementById("runbtn");
375 if (runBtn) runBtn.disabled = true;
376 document.getElementById("audit-loading").style.display = "block";
377 if (runBtn) runBtn.style.display = "none";
378
379 setTimeout(() => {
380 filterReports();
381 document.getElementById("audit-controls-section").style.display = "block";
382 document.getElementById("audit-results").style.display = "block";
383 document.getElementById("audit-loading").style.display = "none";
384 document.getElementById("newauditbtn").style.display = "inline-block";
385 }, 250);
386 }
387 // Reset for a new audit
388 function newAudit() {
389 const runBtn = document.getElementById("runbtn");
390 if (runBtn) runBtn.disabled = false;
391 document.getElementById("audit-controls-section").style.display = "none";
392 document.getElementById("audit-results").style.display = "none";
393 document.getElementById("audit-loading").style.display = "none";
394 document.getElementById("audit-results-inner").innerHTML = "";
395
396 // Clear filters and localStorage
397 localStorage.removeItem('auditModule');
398 localStorage.removeItem('auditLang');
399 localStorage.removeItem('auditFile');
400
401 // Reset filter fields
402 document.getElementById("audit-module").value = "";
403 document.getElementById("audit-lang").value = "";
404 document.getElementById("audit-file").value = "";
405
406 document.getElementById("newauditbtn").style.display = "none";
407 if (runBtn) runBtn.style.display = "inline-block";
408 }
409
410 // Email modal logic.
411 // Note that email depends on the context settings
412 function openAuditEmailModal() {
413 document.getElementById('audit-email-modal').style.display = 'flex';
414 document.getElementById('audit-email-to').focus();
415 document.getElementById('audit-email-status').textContent = '';
416 }
417 function closeAuditEmailModal() {
418 document.getElementById('audit-email-modal').style.display = 'none';
419 }
420 window.addEventListener('DOMContentLoaded', function() {
421 var sendBtn = document.getElementById('audit-send-email-btn');
422 if (sendBtn) sendBtn.addEventListener('click', openAuditEmailModal);
423
424 var emailForm = document.getElementById('audit-email-form');
425 if (emailForm) emailForm.addEventListener('submit', function(e) {
426 e.preventDefault();
427 var to = document.getElementById('audit-email-to').value.trim();
428 if (!to) return;
429 var mod = document.getElementById('audit-module').value;
430 var lang = document.getElementById('audit-lang').value;
431 var reports = allReports.filter(r =>
432 (mod === "" || r.module === mod) && (lang === "" || r.lang === lang)
433 );
434 var data = JSON.stringify(reports, null, 2);
435 var xhr = new XMLHttpRequest();
436 xhr.open('POST', window.location.pathname + '?sendmail=1');
437 xhr.setRequestHeader('Content-Type', 'application/json');
438 xhr.onload = function() {
439 var msg = (xhr.status === 200) ? '✅ Email sent!' : '❌ Failed to send: ' + xhr.responseText;
440 document.getElementById('audit-email-status').textContent = msg;
441 if (xhr.status === 200) setTimeout(closeAuditEmailModal, 1800);
442 };
443 xhr.send(JSON.stringify({ to: to, mod: mod, lang: lang, data: data }));
444 document.getElementById('audit-email-status').textContent = 'Sending...';
445 });
446 });
447
448 // Export functionality
449 function exportAudit(format, action) {
450 let mod = document.getElementById("audit-module").value;
451 let lang = document.getElementById("audit-lang").value;
452 let fileFilter = document.getElementById("audit-file").value.toLowerCase();
453
454 // Filter reports based on current selections
455 let reports = allReports.filter(r =>
456 (mod === "" || r.module === mod) &&
457 (lang === "" || r.lang === lang) &&
458 (fileFilter === "" || r.file.toLowerCase().includes(fileFilter))
459 );
460
461 let content = '';
462 let filename = 'lang_audit_report';
463 let mimeType = 'text/plain';
464
465 // Add module/language info to filename if filtered
466 if (mod) filename += '_' + mod;
467 if (lang) filename += '_' + lang;
468
469 // Generate content based on format
470 if (format === 'json') {
471 content = JSON.stringify(reports, null, 2);
472 mimeType = 'application/json';
473 filename += '.json';
474 } else if (format === 'csv') {
475 // CSV header
476 content = 'Module,Language,File,Status,Missing Constants,Extra Constants,Unused Constants\n';
477
478 // CSV rows
479 for (let r of reports) {
480 let status = r.ok ? 'OK' : 'Missing';
481 let missing = r.missing ? r.missing.join('|') : '';
482 let extra = r.extra ? r.extra.join('|') : '';
483 let unused = r.extra_unused ? r.extra_unused.join('|') : '';
484
485 // Escape quotes in CSV fields
486 missing = missing.replace(/"/g, '""');
487 extra = extra.replace(/"/g, '""');
488 unused = unused.replace(/"/g, '""');
489
490 content += `"${r.module}","${r.lang}","${r.file}","${status}","${missing}","${extra}","${unused}"\n`;
491 }
492 mimeType = 'text/csv';
493 filename += '.csv';
494 } else if (format === 'html') {
495 // Create HTML report
496 content = `<!DOCTYPE html>
497<html>
498<head>
499 <meta charset="UTF-8">
500 <title>Language Audit Report</title>
501 <style>
502 body { font-family: Arial, sans-serif; margin: 20px; }
503 h1 { color: #333; }
504 table { border-collapse: collapse; width: 100%; margin-top: 20px; }
505 th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
506 th { background-color: #f2f2f2; }
507 tr:nth-child(even) { background-color: #f9f9f9; }
508 .ok { color: green; }
509 .missing { color: red; }
510 .extra { color: orange; }
511 </style>
512</head>
513<body>
514 <h1>Language Audit Report</h1>
515 <p>Generated: ${new Date().toLocaleString()}</p>`;
516
517 if (mod || lang) {
518 content += '<p><strong>Filters:</strong> ';
519 if (mod) content += `Module: ${mod} `;
520 if (lang) content += `Language: ${lang}`;
521 content += '</p>';
522 }
523
524 content += `
525 <table>
526 <tr>
527 <th>Module</th>
528 <th>Language</th>
529 <th>File</th>
530 <th>Status</th>
531 <th>Missing Constants</th>
532 <th>Extra Constants</th>
533 </tr>`;
534
535 for (let r of reports) {
536 let statusClass = r.ok ? 'ok' : 'missing';
537 let statusText = r.ok ? '✅ OK' : '❌ Missing';
538
539 content += `
540 <tr>
541 <td>${r.module}</td>
542 <td>${r.lang}</td>
543 <td>${r.file}</td>
544 <td class="${statusClass}">${statusText}</td>
545 <td>`;
546
547 if (r.missing && r.missing.length) {
548 content += '<ul>';
549 for (let c of r.missing) {
550 content += `<li class="missing">${c}</li>`;
551 }
552 content += '</ul>';
553 }
554
555 content += `</td>
556 <td>`;
557
558 if (r.extra && r.extra.length) {
559 content += '<ul>';
560 for (let c of r.extra) {
561 let unusedMark = (r.extra_unused && r.extra_unused.includes(c)) ? ' 🚫' : '';
562 content += `<li class="extra">${c}${unusedMark}</li>`;
563 }
564 content += '</ul>';
565 }
566
567 content += `</td>
568 </tr>`;
569 }
570
571 content += `
572 </table>
573 <p>✅ = All constants present<br>❌ = Missing constant<br>⚠️ = Extra constant in translation<br>🚫 = Extra constant not used anywhere in codebase</p>
574</body>
575</html>`;
576 mimeType = 'text/html';
577 filename += '.html';
578 }
579 // Handle view or download based on action
580 if (action === 'view') {
581 // Open in new tab
582 let blob = new Blob([content], {type: mimeType});
583 let url = URL.createObjectURL(blob);
584 window.open(url, '_blank');
585 } else if (action === 'download') {
586 // Download file
587 let blob = new Blob([content], {type: mimeType});
588 let url = URL.createObjectURL(blob);
589 let a = document.createElement('a');
590 a.href = url;
591 a.download = filename;
592 document.body.appendChild(a);
593 a.click();
594 setTimeout(function() {
595 document.body.removeChild(a);
596 window.URL.revokeObjectURL(url);
597 }, 0);
598 }
599 }
600 // Event listeners
601 window.addEventListener('DOMContentLoaded', function() {
602 // Export select handler
603 var exportSel = document.getElementById('audit-export-select');
604 if (exportSel) {
605 exportSel.addEventListener('change', function(e) {
606 const val = e.target.value;
607 if (!val) return;
608 let [action, format] = val.split('-');
609 exportAudit(format, action);
610 e.target.value = '';
611 });
612 }
613
614 // Restore filters from localStorage
615 const savedMod = localStorage.getItem('auditModule');
616 const savedLang = localStorage.getItem('auditLang');
617 const savedFile = localStorage.getItem('auditFile');
618
619 if (savedMod) {
620 const modSel = document.getElementById('audit-module');
621 if (modSel) modSel.value = savedMod;
622 }
623 if (savedLang) {
624 const langSel = document.getElementById('audit-lang');
625 if (langSel) langSel.value = savedLang;
626 }
627 if (savedFile) {
628 const fileInput = document.getElementById('audit-file');
629 if (fileInput) fileInput.value = savedFile;
630 }
631
632 // If results are visible, apply filters
633 if (document.getElementById('audit-results').style.display === 'block') {
634 filterReports();
635 }
636 });
637 // Save filter selections to localStorage
638 document.addEventListener('change', function(e) {
639 if (e.target && e.target.id === 'audit-module') {
640 localStorage.setItem('auditModule', e.target.value);
641 }
642 if (e.target && e.target.id === 'audit-lang') {
643 localStorage.setItem('auditLang', e.target.value);
644 }
645 if (e.target && e.target.id === 'audit-file') {
646 localStorage.setItem('auditFile', e.target.value);
647 }
648 });
649 // Reset emergency
650 window.addEventListener('DOMContentLoaded', function() {
651 var resetBtn = document.getElementById('lang-reset-english-btn');
652 if (resetBtn) {
653 resetBtn.addEventListener('click', function() {
654 if (!confirm('Are you sure you want to reset the site language to English? This is for emergency recovery.')) return;
655 resetBtn.disabled = true;
656 resetBtn.textContent = 'Resetting...';
657 fetch(window.location.pathname + '?resetlang=1', {method:'POST'})
658 .then(r => r.json())
659 .then(resp => {
660 if (resp.success) {
661 alert('Site language has been reset to English. Please reload the page.');
662 location.reload();
663 } else {
664 alert('Failed to reset language: ' + (resp.error || 'Unknown error'));
665 resetBtn.disabled = false;
666 resetBtn.textContent = 'Reset Site Language to English';
667 }
668 })
669 .catch(e => {
670 alert('Error: ' + e);
671 resetBtn.disabled = false;
672 resetBtn.textContent = 'Reset Site Language to English';
673 });
674 });
675 }
676});
677const savedFile = localStorage.getItem('auditFile');
678if (savedFile) {
679 const fileInput = document.getElementById('audit-file');
680 if (fileInput) fileInput.value = savedFile;
681}
682// Re-apply filters if needed (already there)
683// if (document.getElementById('audit-results').style.display === 'block') {
684// filterReports();
685// }
686</script>
687</section>
688</body>
689</html>
690<?php
691// TODO check if any setup available
692// PHP: handle email send request using Mailer
693if (isset($_GET['sendmail']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
694 header('Content-Type: application/json');
695 $input = json_decode(file_get_contents('php://input'), true);
696 $to = filter_var($input['to'] ?? '', FILTER_VALIDATE_EMAIL);
697 $mod = $input['mod'] ?? '';
698 $lang = $input['lang'] ?? '';
699 $data = $input['data'] ?? '';
700 if (!$to || !$data) {
701 http_response_code(400);
702 echo json_encode(['error' => 'Invalid email or data.']);
703 exit;
704 }
705 require_once dirname(__FILE__) . '/mainfile.php';
706 require_once XOOPS_ROOT_PATH . '/class/xoopsmailer.php';
707 $subject = 'XOOPSCube XCL - Recovery Language Audit Report';
708 if ($mod || $lang) $subject .= " [" . ($mod ? "Module: $mod " : "") . ($lang ? "Lang: $lang" : "") . "]";
709 $body = "Attached is the audit report as a JSON file.\n\n";
710 $mailer = new xoopsmailer(); // Use the global $mailer instance
711 $mailer->useMail();
712 $mailer->setToEmails([$to]);
713 $mailer->setFromEmail($GLOBALS['xoopsConfig']['adminmail'] ?? 'webmaster@' . $_SERVER['SERVER_NAME']);
714 $mailer->setFromName($GLOBALS['xoopsConfig']['sitename'] ?? 'XOOPS Site');
715 $mailer->setSubject($subject);
716 $mailer->setBody($body);
717 // Attach the JSON report
718 $filename = 'lang_audit_report.json';
719 if (method_exists($mailer->multimailer, 'addStringAttachment')) {
720 $mailer->multimailer->addStringAttachment($data, $filename, 'base64', 'application/json');
721 }
722 $ok = $mailer->send();
723 if ($ok) {
724 echo json_encode(['success' => true]);
725 } else {
726 http_response_code(500);
727 $errors = method_exists($mailer, 'getErrors') ? $mailer->getErrors(false) : ['Mail send failed.'];
728 echo json_encode(['error' => 'Mail send failed.', 'details' => $errors]);
729 }
730 exit;
731}
static & getDatabaseConnection()
This file was entirely rewritten by the XOOPSCube Legacy project for compatibility with XOOPS2.