| @@ -1245,8 +1245,9 @@ | ||
| 1245 | 1245 | results.extra = results.extra || []; |
| 1246 | 1246 | results.missing = results.missing || []; |
| 1247 | 1247 | |
| 1248 | 1248 | var strings = vigilanteAdmin.strings; |
| 1249 | + var esc = Vigilante_Admin.escapeHtml; | |
| 1249 | 1250 | |
| 1250 | 1251 | var html = '<h2>' + strings.scanResults + '</h2>'; |
| 1251 | 1252 | html += '<div class="vigilante-scan-summary">'; |
| 1252 | 1253 | html += '<div class="vigilante-scan-stat vigilante-stat-ok">'; |
| @@ -1295,13 +1296,13 @@ | ||
| 1295 | 1296 | html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 250px;">' + strings.reason + '</th><th style="width: 120px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>'; |
| 1296 | 1297 | html += '<tbody>'; |
| 1297 | 1298 | results.suspicious.forEach(function(file) { |
| 1298 | 1299 | html += '<tr>'; |
| 1299 | - html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + file.file + '"></th>'; | |
| 1300 | - html += '<td><code style="color: #d63638;">' + file.file + '</code></td>'; | |
| 1301 | - html += '<td>' + (file.reason || strings.unknown) + '</td>'; | |
| 1302 | - html += '<td>' + (file.type || strings.unknown) + '</td>'; | |
| 1303 | - html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + file.file + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1300 | + html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>'; | |
| 1301 | + html += '<td><code style="color: #d63638;">' + esc(file.file) + '</code></td>'; | |
| 1302 | + html += '<td>' + esc(file.reason || strings.unknown) + '</td>'; | |
| 1303 | + html += '<td>' + esc(file.type === 'vigilante_self' ? (strings.selfType || 'Vigilant (self)') : (file.type || strings.unknown)) + '</td>'; | |
| 1304 | + html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1304 | 1305 | html += '</tr>'; |
| 1305 | 1306 | }); |
| 1306 | 1307 | html += '</tbody></table>'; |
| 1307 | 1308 | html += '</div>'; |
| @@ -1318,13 +1319,13 @@ | ||
| 1318 | 1319 | html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 250px;">' + strings.reason + '</th><th style="width: 120px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>'; |
| 1319 | 1320 | html += '<tbody>'; |
| 1320 | 1321 | results.extra.forEach(function(file) { |
| 1321 | 1322 | html += '<tr>'; |
| 1322 | - html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + file.file + '"></th>'; | |
| 1323 | - html += '<td><code style="color: #b32d2e;">' + file.file + '</code></td>'; | |
| 1324 | - html += '<td>' + (file.reason || strings.unknown) + '</td>'; | |
| 1325 | - html += '<td>' + (file.type || strings.unknown) + '</td>'; | |
| 1326 | - html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + file.file + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1323 | + html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>'; | |
| 1324 | + html += '<td><code style="color: #b32d2e;">' + esc(file.file) + '</code></td>'; | |
| 1325 | + html += '<td>' + esc(file.reason || strings.unknown) + '</td>'; | |
| 1326 | + html += '<td>' + esc(file.type === 'vigilante_self' ? (strings.selfType || 'Vigilant (self)') : (file.type || strings.unknown)) + '</td>'; | |
| 1327 | + html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1327 | 1328 | html += '</tr>'; |
| 1328 | 1329 | }); |
| 1329 | 1330 | html += '</tbody></table>'; |
| 1330 | 1331 | html += '</div>'; |
| @@ -1339,8 +1340,16 @@ | ||
| 1339 | 1340 | } else { |
| 1340 | 1341 | regularModified.push(file); |
| 1341 | 1342 | } |
| 1342 | 1343 | }); |
| 1344 | + // A missing file of Vigilant is critical and the generic missing | |
| 1345 | + // files have no table: list it with the modified files, or the | |
| 1346 | + // screen said "All clear" with a module deleted. | |
| 1347 | + results.missing.forEach(function(file) { | |
| 1348 | + if (file && file.type === 'vigilante_self') { | |
| 1349 | + regularModified.push(file); | |
| 1350 | + } | |
| 1351 | + }); | |
| 1343 | 1352 | |
| 1344 | 1353 | // Critical config files section (same visual treatment as suspicious files) |
| 1345 | 1354 | if (criticalModified.length > 0) { |
| 1346 | 1355 | html += '<div class="vigilante-file-list vigilante-critical-config-files">'; |
| @@ -1349,9 +1358,13 @@ | ||
| 1349 | 1358 | html += '<table class="wp-list-table widefat fixed striped">'; |
| 1350 | 1359 | html += '<thead><tr><th>' + strings.file + '</th><th style="width: 200px;">' + (strings.changes || 'Changes') + '</th><th style="width: 220px;">' + (strings.actions || 'Actions') + '</th></tr></thead>'; |
| 1351 | 1360 | html += '<tbody>'; |
| 1352 | 1361 | |
| 1353 | - var escHtml = function(s) { return $('<span>').text(s).html(); }; | |
| 1362 | + // Delega en el escapador del objeto, que codifica comillas. La | |
| 1363 | + // version anterior era $('<span>').text(s).html(), que no las toca: | |
| 1364 | + // aqui solo se usa en posicion de texto, pero es el patron del | |
| 1365 | + // CVE-2026-81754 esperando a que alguien lo mueva a un atributo. | |
| 1366 | + var escHtml = Vigilante_Admin.escapeHtml; | |
| 1354 | 1367 | |
| 1355 | 1368 | criticalModified.forEach(function(file) { |
| 1356 | 1369 | var fileId = file.file.replace(/[^a-z0-9]/gi, '-'); |
| 1357 | 1370 | var baselineSize = file.baseline_size ? Number(file.baseline_size).toLocaleString() : '?'; |
| @@ -1362,9 +1375,9 @@ | ||
| 1362 | 1375 | var diffUnavailable = !!diff.unavailable; |
| 1363 | 1376 | |
| 1364 | 1377 | // Main row |
| 1365 | 1378 | html += '<tr>'; |
| 1366 | - html += '<td><code style="color: #e36210;">' + file.file + '</code></td>'; | |
| 1379 | + html += '<td><code style="color: #e36210;">' + esc(file.file) + '</code></td>'; | |
| 1367 | 1380 | html += '<td>'; |
| 1368 | 1381 | if (!diffUnavailable) { |
| 1369 | 1382 | html += '<span style="color: #007017;">+' + added.length + '</span> <span style="color: #b32d2e;">-' + removed.length + '</span> ' + (strings.diffLines || 'lines') + '<br>'; |
| 1370 | 1383 | } |
| @@ -1371,9 +1384,24 @@ | ||
| 1371 | 1384 | html += '<small style="color: #50575e;">' + baselineSize + ' → ' + currentSize + ' bytes</small>'; |
| 1372 | 1385 | html += '</td>'; |
| 1373 | 1386 | html += '<td>'; |
| 1374 | 1387 | html += '<button type="button" class="button button-small vigilante-toggle-critical-content" data-target="vigilante-critical-content-' + fileId + '" data-label-show="' + (strings.reviewChanges || 'Review changes') + '" data-label-hide="' + (strings.hideChanges || 'Hide changes') + '">' + (strings.reviewChanges || 'Review changes') + '</button> '; |
| 1375 | - html += '<button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="' + file.file + '">' + (strings.approve || 'Approve') + '</button>'; | |
| 1388 | + | |
| 1389 | + // Same gate as the AJAX handler behind the button, and as the | |
| 1390 | + // first render in PHP. Since 2.11.3 approving these two files | |
| 1391 | + // takes a network administrator, so painting the button to | |
| 1392 | + // anybody else buys them a Permission denied and no reason why. | |
| 1393 | + // | |
| 1394 | + // Truthy test on purpose: wp_localize_script() casts every | |
| 1395 | + // scalar to a string, so the PHP boolean arrives here as "1" | |
| 1396 | + // or "" and never as true. Measured on the network, admin of | |
| 1397 | + // a subsite against network admin. A === true would gate | |
| 1398 | + // nobody and would look right. | |
| 1399 | + if (vigilanteAdmin.approvalLocked) { | |
| 1400 | + html += '<span class="description" style="display:block;margin-top:4px;">' + escHtml(strings.approvalLockedNotice || '') + '</span>'; | |
| 1401 | + } else { | |
| 1402 | + html += '<button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="' + esc(file.file) + '">' + (strings.approve || 'Approve') + '</button>'; | |
| 1403 | + } | |
| 1376 | 1404 | html += '</td>'; |
| 1377 | 1405 | html += '</tr>'; |
| 1378 | 1406 | |
| 1379 | 1407 | // Expandable diff row |
| @@ -1379,9 +1407,15 @@ | ||
| 1379 | 1407 | // Expandable diff row |
| 1380 | 1408 | html += '<tr id="vigilante-critical-content-' + fileId + '" class="vigilante-critical-content-row" style="display:none;">'; |
| 1381 | 1409 | html += '<td colspan="3" style="padding: 0;">'; |
| 1382 | 1410 | html += '<div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;">'; |
| 1383 | - if (diffUnavailable) { | |
| 1411 | + if (diff.network) { | |
| 1412 | + html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffNetwork || '') + '</p>'; | |
| 1413 | + } else if (diff.rescan) { | |
| 1414 | + html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffRescan || '') + '</p>'; | |
| 1415 | + } else if (diff.redaction) { | |
| 1416 | + html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffRedaction || '') + '</p>'; | |
| 1417 | + } else if (diffUnavailable) { | |
| 1384 | 1418 | html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + (strings.diffUnavailable || 'Diff not available.') + '</p>'; |
| 1385 | 1419 | } else if (added.length === 0 && removed.length === 0) { |
| 1386 | 1420 | html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + (strings.diffEmpty || 'No line changes detected.') + '</p>'; |
| 1387 | 1421 | } else { |
| @@ -1412,12 +1446,12 @@ | ||
| 1412 | 1446 | html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 100px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>'; |
| 1413 | 1447 | html += '<tbody>'; |
| 1414 | 1448 | regularModified.forEach(function(file) { |
| 1415 | 1449 | html += '<tr>'; |
| 1416 | - html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + file.file + '"></th>'; | |
| 1417 | - html += '<td><code>' + file.file + '</code></td>'; | |
| 1418 | - html += '<td>' + (file.type || strings.unknown) + '</td>'; | |
| 1419 | - html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + file.file + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1450 | + html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>'; | |
| 1451 | + html += '<td><code>' + esc(file.file) + '</code></td>'; | |
| 1452 | + html += '<td>' + esc(file.type === 'vigilante_self' ? (strings.selfType || 'Vigilant (self)') : (file.type || strings.unknown)) + '</td>'; | |
| 1453 | + html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>'; | |
| 1420 | 1454 | html += '</tr>'; |
| 1421 | 1455 | }); |
| 1422 | 1456 | html += '</tbody></table>'; |
| 1423 | 1457 | html += '</div>'; |
| @@ -1422,9 +1456,9 @@ | ||
| 1422 | 1456 | html += '</tbody></table>'; |
| 1423 | 1457 | html += '</div>'; |
| 1424 | 1458 | } |
| 1425 | 1459 | |
| 1426 | - if (results.modified.length === 0 && results.suspicious.length === 0 && results.extra.length === 0) { | |
| 1460 | + if (criticalModified.length === 0 && regularModified.length === 0 && results.suspicious.length === 0 && results.extra.length === 0) { | |
| 1427 | 1461 | html += '<div class="vigilante-all-clear"><span class="dashicons dashicons-yes-alt"></span> ' + strings.allClear + '</div>'; |
| 1428 | 1462 | } |
| 1429 | 1463 | |
| 1430 | 1464 | $container.html(html).show(); |
| @@ -1967,14 +2001,16 @@ | ||
| 1967 | 2001 | user: log.user_login || '', |
| 1968 | 2002 | ip: log.ip_address || '', |
| 1969 | 2003 | user_agent: log.user_agent || '', |
| 1970 | 2004 | request_method: log.request_method || '', |
| 2005 | + request_uri: log.request_uri || '', | |
| 1971 | 2006 | date: log.created_at || '', |
| 1972 | 2007 | severity: log.severity || 'info', |
| 1973 | 2008 | is_ip_whitelisted: !!log.is_ip_whitelisted, |
| 1974 | 2009 | is_ip_blacklisted: !!log.is_ip_blacklisted, |
| 1975 | 2010 | is_ua_whitelisted: !!log.is_ua_whitelisted, |
| 1976 | - is_ua_blacklisted: !!log.is_ua_blacklisted | |
| 2011 | + is_ua_blacklisted: !!log.is_ua_blacklisted, | |
| 2012 | + self: log.self_guidance || null | |
| 1977 | 2013 | }; |
| 1978 | 2014 | |
| 1979 | 2015 | var displayType = typeLabels[log.event_type] || log.event_type; |
| 1980 | 2016 | var displaySeverity = severityLabels[log.severity] || log.severity; |
| @@ -2054,20 +2090,39 @@ | ||
| 2054 | 2090 | }, |
| 2055 | 2091 | |
| 2056 | 2092 | /** |
| 2057 | 2093 | * Download logs as CSV |
| 2094 | + * | |
| 2095 | + * Every cell goes through csvCell(): quotes are doubled in all columns, | |
| 2096 | + * not only the message, and a value starting with = + - @ or a control | |
| 2097 | + * character gets an apostrophe in front so a spreadsheet shows it | |
| 2098 | + * instead of running it as a formula (S12). Action, address and | |
| 2099 | + * User-Agent were added for B38: the columns that help diagnose an | |
| 2100 | + * entry were exactly the ones the export left out. | |
| 2058 | 2101 | */ |
| 2059 | 2102 | downloadCSV: function(logs) { |
| 2060 | - var csv = 'Date,Type,Method,Severity,Message,User,IP\n'; | |
| 2103 | + var csvCell = function(value) { | |
| 2104 | + var s = (value === null || value === undefined) ? '' : String(value); | |
| 2105 | + if (/^[=+\-@\t\r]/.test(s)) { | |
| 2106 | + s = "'" + s; | |
| 2107 | + } | |
| 2108 | + return '"' + s.replace(/"/g, '""') + '"'; | |
| 2109 | + }; | |
| 2110 | + var csv = 'Date,Type,Action,Method,Severity,Message,User,IP,Address,User Agent\n'; | |
| 2061 | 2111 | |
| 2062 | 2112 | logs.forEach(function(log) { |
| 2063 | - csv += '"' + log.created_at + '",'; | |
| 2064 | - csv += '"' + log.event_type + '",'; | |
| 2065 | - csv += '"' + (log.request_method || '') + '",'; | |
| 2066 | - csv += '"' + log.severity + '",'; | |
| 2067 | - csv += '"' + (log.event_message || '').replace(/"/g, '""') + '",'; | |
| 2068 | - csv += '"' + (log.user_login || '') + '",'; | |
| 2069 | - csv += '"' + log.ip_address + '"\n'; | |
| 2113 | + csv += [ | |
| 2114 | + csvCell(log.created_at), | |
| 2115 | + csvCell(log.event_type), | |
| 2116 | + csvCell(log.event_action), | |
| 2117 | + csvCell(log.request_method), | |
| 2118 | + csvCell(log.severity), | |
| 2119 | + csvCell(log.event_message), | |
| 2120 | + csvCell(log.user_login), | |
| 2121 | + csvCell(log.ip_address), | |
| 2122 | + csvCell(log.request_uri), | |
| 2123 | + csvCell(log.user_agent) | |
| 2124 | + ].join(',') + '\n'; | |
| 2070 | 2125 | }); |
| 2071 | 2126 | |
| 2072 | 2127 | var blob = new Blob([csv], { type: 'text/csv' }); |
| 2073 | 2128 | var url = URL.createObjectURL(blob); |
| @@ -2592,9 +2647,43 @@ | ||
| 2592 | 2647 | |
| 2593 | 2648 | var html = ''; |
| 2594 | 2649 | |
| 2595 | 2650 | var s = vigilanteAdmin.strings; |
| 2651 | + var esc = Vigilante_Admin.escapeHtml; | |
| 2596 | 2652 | |
| 2653 | + // -- Section: what happened (self-protection entries) -- | |
| 2654 | + // Text only: every value below is printed as text, never as an | |
| 2655 | + // attribute, and the link is a fixed URL built with esc_url(). | |
| 2656 | + if (details.self && details.self.title) { | |
| 2657 | + html += '<div class="vigilante-popup-section">'; | |
| 2658 | + html += '<h4 class="vigilante-popup-section-title">' + esc(s.logWhatHappened || 'What happened') + '</h4>'; | |
| 2659 | + html += '<p><strong>' + esc(details.self.title) + '</strong></p>'; | |
| 2660 | + if (details.self.files && details.self.files.length) { | |
| 2661 | + html += '<ul class="vigilante-popup-files">'; | |
| 2662 | + details.self.files.slice(0, 20).forEach(function(file) { | |
| 2663 | + html += '<li><code>' + esc(file) + '</code></li>'; | |
| 2664 | + }); | |
| 2665 | + if (details.self.files.length > 20) { | |
| 2666 | + html += '<li>' + esc('+' + (details.self.files.length - 20)) + '</li>'; | |
| 2667 | + } | |
| 2668 | + html += '</ul>'; | |
| 2669 | + } | |
| 2670 | + if (details.self.meaning) { | |
| 2671 | + html += '<p><strong>' + esc(s.logWhatItMeans || 'What it means') + ':</strong> ' + esc(details.self.meaning) + '</p>'; | |
| 2672 | + } | |
| 2673 | + if (details.self.steps && details.self.steps.length) { | |
| 2674 | + html += '<p><strong>' + esc(s.logWhatToDo || 'What to do') + ':</strong></p><ol>'; | |
| 2675 | + details.self.steps.forEach(function(step) { | |
| 2676 | + html += '<li>' + esc(step) + '</li>'; | |
| 2677 | + }); | |
| 2678 | + html += '</ol>'; | |
| 2679 | + } | |
| 2680 | + if (vigilanteAdmin.selfBoxUrl) { | |
| 2681 | + html += '<p><a href="' + esc(vigilanteAdmin.selfBoxUrl) + '">' + esc(s.logSelfSeeDetails || 'Open File Integrity for the full detail') + '</a></p>'; | |
| 2682 | + } | |
| 2683 | + html += '</div>'; | |
| 2684 | + } | |
| 2685 | + | |
| 2597 | 2686 | // -- Section: Request -- |
| 2598 | 2687 | html += '<div class="vigilante-popup-section">'; |
| 2599 | 2688 | html += '<h4 class="vigilante-popup-section-title">' + this.escapeHtml(s.logRequest || 'Request') + '</h4>'; |
| 2600 | 2689 | html += '<table class="vigilante-details-table">'; |
| @@ -2605,8 +2694,11 @@ | ||
| 2605 | 2694 | html += '<tr><th>' + this.escapeHtml(s.logType || 'Type') + '</th><td>' + this.escapeHtml(details.type) + '</td></tr>'; |
| 2606 | 2695 | html += '<tr><th>' + this.escapeHtml(s.logAction || 'Action') + '</th><td>' + this.escapeHtml(details.action || '-') + '</td></tr>'; |
| 2607 | 2696 | html += '<tr><th>' + this.escapeHtml(s.logSeverity || 'Severity') + '</th><td><span class="vigilante-badge vigilante-badge-' + this.escapeHtml(details.severity || 'info') + '">' + this.escapeHtml(details.severity || 'info') + '</span></td></tr>'; |
| 2608 | 2697 | html += '<tr><th>' + this.escapeHtml(s.logMessage || 'Message') + '</th><td>' + this.escapeHtml(details.message) + '</td></tr>'; |
| 2698 | + if (details.request_uri) { | |
| 2699 | + html += '<tr><th>' + this.escapeHtml(s.logRequestUri || 'Address') + '</th><td class="vigilante-ua-cell"><code>' + this.escapeHtml(details.request_uri) + '</code></td></tr>'; | |
| 2700 | + } | |
| 2609 | 2701 | html += '</table>'; |
| 2610 | 2702 | html += '</div>'; |
| 2611 | 2703 | |
| 2612 | 2704 | // -- Section: Client -- |
| @@ -2680,9 +2772,11 @@ | ||
| 2680 | 2772 | e.preventDefault(); |
| 2681 | 2773 | var $btn = $(e.currentTarget); |
| 2682 | 2774 | var itemType = $btn.data('item-type'); |
| 2683 | 2775 | var listType = $btn.data('list-type'); |
| 2684 | - var value = $btn.data('value'); | |
| 2776 | + // attr(), not data(): jQuery runs JSON.parse on values that look like | |
| 2777 | + // JSON, so a User-Agent such as {"a":1} would arrive as an object. | |
| 2778 | + var value = $btn.attr('data-value'); | |
| 2685 | 2779 | |
| 2686 | 2780 | if (!itemType || !listType || !value) { |
| 2687 | 2781 | return; |
| 2688 | 2782 | } |
| @@ -2775,11 +2869,16 @@ | ||
| 2775 | 2869 | if (text === null || text === undefined) return ''; |
| 2776 | 2870 | if (typeof text !== 'string') { |
| 2777 | 2871 | text = String(text); |
| 2778 | 2872 | } |
| 2779 | - var div = document.createElement('div'); | |
| 2780 | - div.textContent = text; | |
| 2781 | - return div.innerHTML; | |
| 2873 | + // Quotes must be encoded too: this helper is also used in attribute | |
| 2874 | + // position, where an unescaped quote closes the attribute early. | |
| 2875 | + return text | |
| 2876 | + .replace(/&/g, '&') | |
| 2877 | + .replace(/</g, '<') | |
| 2878 | + .replace(/>/g, '>') | |
| 2879 | + .replace(/"/g, '"') | |
| 2880 | + .replace(/'/g, '''); | |
| 2782 | 2881 | }, |
| 2783 | 2882 | |
| 2784 | 2883 | /** |
| 2785 | 2884 | * Debounce function |
| @@ -3324,9 +3423,9 @@ | ||
| 3324 | 3423 | if (response.success && response.data.length > 0) { |
| 3325 | 3424 | var html = '<ul class="vigilante-user-search-list">'; |
| 3326 | 3425 | $.each(response.data, function(i, user) { |
| 3327 | 3426 | html += '<li data-user-id="' + user.ID + '">' + |
| 3328 | - '<img src="' + user.avatar + '" class="vigilante-user-avatar" alt="">' + | |
| 3427 | + '<img src="' + self.escapeHtml(user.avatar) + '" class="vigilante-user-avatar" alt="">' + | |
| 3329 | 3428 | '<div class="vigilante-user-info">' + |
| 3330 | 3429 | '<strong>' + self.escapeHtml(user.display_name) + '</strong><br>' + |
| 3331 | 3430 | '<small>' + self.escapeHtml(user.user_email) + '</small>' + |
| 3332 | 3431 | '</div>' + |
| @@ -4164,10 +4263,17 @@ | ||
| 4164 | 4263 | + '</svg>'; |
| 4165 | 4264 | }, |
| 4166 | 4265 | |
| 4167 | 4266 | /** |
| 4168 | - * When landing on a fix link (#field-* or #vigilante-section-*), | |
| 4169 | - * scroll smoothly and flash-highlight the target. | |
| 4267 | + * When landing on a fix link (#field-*, #vigilante-section-* or any | |
| 4268 | + * other #vigilante-* target), scroll smoothly and flash-highlight it. | |
| 4269 | + * | |
| 4270 | + * The pattern used to name the two families literally, which left the | |
| 4271 | + * one anchor that follows neither out in the cold: the weekly report | |
| 4272 | + * email links to #vigilante-analyzer, so that link landed on the | |
| 4273 | + * dashboard with no scroll and no flash. Widened in 2.11.1 to the | |
| 4274 | + * whole vigilante- prefix, which is still narrow enough that a hash | |
| 4275 | + * written by anything else on the page cannot reach the selector. | |
| 4170 | 4276 | */ |
| 4171 | 4277 | hashFocusFlash: function() { |
| 4172 | 4278 | var hash = window.location.hash; |
| 4173 | 4279 | if (!hash || hash.length < 2) { |
| @@ -4172,10 +4278,10 @@ | ||
| 4172 | 4278 | var hash = window.location.hash; |
| 4173 | 4279 | if (!hash || hash.length < 2) { |
| 4174 | 4280 | return; |
| 4175 | 4281 | } |
| 4176 | - // Accept both section anchors and field anchors. | |
| 4177 | - if (!/^#(?:field-|vigilante-section-)/.test(hash)) { | |
| 4282 | + // Accept field anchors and every anchor the plugin owns. | |
| 4283 | + if (!/^#(?:field-|vigilante-)/.test(hash)) { | |
| 4178 | 4284 | return; |
| 4179 | 4285 | } |
| 4180 | 4286 | |
| 4181 | 4287 | setTimeout(function() { |
| @@ -4197,8 +4303,18 @@ | ||
| 4197 | 4303 | $target.addClass('vigilante-focus-flash'); |
| 4198 | 4304 | setTimeout(function() { |
| 4199 | 4305 | $target.removeClass('vigilante-focus-flash'); |
| 4200 | 4306 | }, 1800); |
| 4307 | + | |
| 4308 | + // And once more when everything has loaded: anything that | |
| 4309 | + // changes height above the target (images, notices, the admin | |
| 4310 | + // bar on a narrow screen) moves it after the first scroll, and | |
| 4311 | + // then the link looks like it only opened the tab. | |
| 4312 | + $(window).one('load', function() { | |
| 4313 | + if ($target[0] && 'scrollIntoView' in $target[0]) { | |
| 4314 | + $target[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| 4315 | + } | |
| 4316 | + }); | |
| 4201 | 4317 | }, 150); |
| 4202 | 4318 | }, |
| 4203 | 4319 | |
| 4204 | 4320 | /** Escape text for HTML. */ |
| @@ -4253,8 +4369,64 @@ | ||
| 4253 | 4369 | complete: function() { |
| 4254 | 4370 | $btn.prop('disabled', false).text(original); |
| 4255 | 4371 | } |
| 4256 | 4372 | }); |
| 4373 | + }); | |
| 4374 | + }); | |
| 4375 | + | |
| 4376 | + /** | |
| 4377 | + * Security Headers settings recovery (2.10.0). | |
| 4378 | + * | |
| 4379 | + * Both buttons are disabled together while the request is in flight, so a | |
| 4380 | + * second click cannot fire a second restore. The page reloads afterwards | |
| 4381 | + * because the whole tab is rendered from the settings that just changed. | |
| 4382 | + */ | |
| 4383 | + $(function() { | |
| 4384 | + var SELECTOR = '#vigilante-recovery-restore, #vigilante-recovery-dismiss'; | |
| 4385 | + | |
| 4386 | + function recoveryRun(action) { | |
| 4387 | + $(SELECTOR).prop('disabled', true); | |
| 4388 | + | |
| 4389 | + $.ajax({ | |
| 4390 | + url: vigilanteAdmin.ajaxUrl, | |
| 4391 | + type: 'POST', | |
| 4392 | + data: { | |
| 4393 | + action: action, | |
| 4394 | + nonce: vigilanteAdmin.nonce | |
| 4395 | + }, | |
| 4396 | + success: function(response) { | |
| 4397 | + if (response.success) { | |
| 4398 | + Vigilante_Admin.showNotice('success', response.data); | |
| 4399 | + setTimeout(function() { | |
| 4400 | + location.reload(); | |
| 4401 | + }, 1200); | |
| 4402 | + return; | |
| 4403 | + } | |
| 4404 | + | |
| 4405 | + Vigilante_Admin.showNotice('error', response.data); | |
| 4406 | + $(SELECTOR).prop('disabled', false); | |
| 4407 | + }, | |
| 4408 | + error: function() { | |
| 4409 | + Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); | |
| 4410 | + $(SELECTOR).prop('disabled', false); | |
| 4411 | + } | |
| 4412 | + }); | |
| 4413 | + } | |
| 4414 | + | |
| 4415 | + $(document).on('click', '#vigilante-recovery-restore', function(e) { | |
| 4416 | + e.preventDefault(); | |
| 4417 | + recoveryRun('vigilante_headers_recovery_restore'); | |
| 4418 | + }); | |
| 4419 | + | |
| 4420 | + $(document).on('click', '#vigilante-recovery-dismiss', function(e) { | |
| 4421 | + e.preventDefault(); | |
| 4422 | + recoveryRun('vigilante_headers_recovery_dismiss'); | |
| 4423 | + }); | |
| 4424 | + | |
| 4425 | + $(document).on('click', '#vigilante-recovery-undo', function(e) { | |
| 4426 | + e.preventDefault(); | |
| 4427 | + $(this).prop('disabled', true); | |
| 4428 | + recoveryRun('vigilante_headers_recovery_undo'); | |
| 4257 | 4429 | }); |
| 4258 | 4430 | }); |
| 4259 | 4431 | |
| 4260 | 4432 | })(jQuery); |