Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4f2ca28
Fix W3 Total Cache DbCache fatal in batch_insert
ilicfilip May 6, 2026
f35ee11
Improve the readme (#76)
jdevalk May 24, 2026
540b22d
Merge pull request #77 from ProgressPlanner/filip/fix-w3tc-dbcache-fatal
ilicfilip Jun 1, 2026
42b8181
Update WPCS to 3.4.1 (security release)
ilicfilip Jul 28, 2026
e17df78
Merge pull request #80 from ProgressPlanner/security/wpcs-3.4.1
aristath Jul 29, 2026
26095ff
Fix deadlocks when concurrent requests write tracked options
ilicfilip Oct 5, 2026
9718e8e
Update PHPStan to 2.3
ilicfilip Oct 6, 2026
b5bb99f
Merge pull request #81 from ProgressPlanner/filip/fix-tracking-deadlock
ilicfilip Oct 8, 2026
1215e50
Merge pull request #82 from ProgressPlanner/filip/phpstan-2.3
jdevalk Oct 8, 2026
037e72b
Release 1.7.0: rename to AAA Autoload Optimizer (#83)
ilicfilip Oct 15, 2026
c80a1e1
Update banners for the AAA Autoload Optimizer rename
ilicfilip Oct 15, 2026
c0b331f
Merge pull request #84 from ProgressPlanner/filip/rename-banners
ilicfilip Oct 15, 2026
963b9ec
Fix autoload toggle not updating collapsed (responsive) rows
ilicfilip Oct 26, 2026
2e6a228
Fix hidden-tab table layout and autoload button for 'off' options
ilicfilip Oct 26, 2026
2c9583a
Escape option names and values when rendering the admin tables
ilicfilip Oct 29, 2026
e3f41a9
Validate tracked option names before storing them
ilicfilip Oct 29, 2026
d4daf91
Make popover and checkbox ids unique across tables
ilicfilip Oct 29, 2026
e8dc5e9
Use a collision-free encoding for option row ids
ilicfilip Oct 30, 2026
eb03191
Only skip option names that can't exist in the options table
ilicfilip Oct 30, 2026
0f133ff
Merge pull request #86 from ProgressPlanner/filip/fix-responsive-row-…
ilicfilip Oct 30, 2026
1c20b46
Merge pull request #88 from ProgressPlanner/filip/fix-duplicate-popov…
ilicfilip Oct 30, 2026
ba6e6a7
Merge pull request #89 from ProgressPlanner/filip/escape-admin-table-…
ilicfilip Oct 30, 2026
76328a3
Release 1.7.1
ilicfilip Oct 30, 2026
b512395
Link the CVE record in the 1.7.1 changelog
ilicfilip Nov 2, 2026
09e2ac7
Merge pull request #90 from ProgressPlanner/filip/release-1.7.1
ilicfilip Nov 2, 2026
15596ac
Merge main into develop after the 1.7.0 squash merge
ilicfilip Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/coverage/
.phpunit.result.cache
node_modules
security-audit-report.md
2 changes: 1 addition & 1 deletion aaa-option-optimizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Plugin Name: AAA Autoload Optimizer
* Plugin URI: https://progressplanner.com/plugins/aaa-option-optimizer/
* Description: Tracks autoloaded options usage and allows the user to optimize them.
* Version: 1.7.0
* Version: 1.7.1
* License: GPL-3.0+
* Author: Team Progress Planner
* Author URI: https://progressplanner.com/
Expand Down
1 change: 1 addition & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ div.dt-container .dt-input {
}
.aaa-option-optimizer-tabs .panel {
display: none;
box-sizing: border-box;
padding: 20px 30px 30px;
background: #fff;
}
Expand Down
212 changes: 152 additions & 60 deletions js/admin-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,48 @@ jQuery( document ).ready( function () {
jQuery( this ).hide();
} );

/**
* Escape a string for safe insertion into HTML text or a double-quoted attribute.
*
* Option names are arbitrary strings that can contain HTML metacharacters, so
* every value interpolated into markup below is passed through this first.
*
* @param {*} value - The value to escape.
* @return {string} The escaped string.
*/
function escapeHtml( value ) {
return String( value ).replace( /[&<>"'`]/g, ( char ) => {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'`': '&#96;',
}[ char ];
} );
}

/**
* Generate row ID for an option name.
*
* The result is used both as a DataTables row id and as an HTML id/attribute
* target, so it must not contain characters that could break out of markup.
* Letters, digits and '_' are kept, every other character is encoded as
* '-<hex code point>-'. A literal '-' never appears otherwise, so distinct
* option names always get distinct IDs (e.g. 'foo:bar' vs 'foo_bar').
*
* @param {string} optionName - The option name.
* @return {string} The row ID.
*/
function generateRowId( optionName ) {
return 'option_' + optionName.replace( /\./g, '_' );
return (
'option_' +
String( optionName ).replace(
/[^A-Za-z0-9_]/gu,
( char ) => '-' + char.codePointAt( 0 ).toString( 16 ) + '-'
)
);
}

/**
Expand Down Expand Up @@ -177,35 +211,52 @@ jQuery( document ).ready( function () {
{
name: 'checkbox',
data: 'name',
render: ( data, type, row ) => renderCheckboxColumn( row ),
render: ( data, type, row, meta ) =>
renderCheckboxColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'select-all',
},
{ name: 'name', data: 'name' },
{ name: 'source', data: 'plugin' },
{ name: 'name', data: 'name', render: DataTable.render.text() },
{ name: 'source', data: 'plugin', render: DataTable.render.text() },
{ name: 'size', data: 'size', searchable: false },
{
name: 'autoload',
data: 'autoload',
className: 'autoload',
searchable: false,
orderable: false,
render: DataTable.render.text(),
},
{
name: 'value',
data: 'value',
render: ( data, type, row ) => renderValueColumn( row ),
render: ( data, type, row, meta ) =>
renderValueColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'actions',
},
];
if ( selector === '#requested_do_not_exist_table' ) {
return [
{ name: 'option', data: 'name' },
{ name: 'source', data: 'plugin', searchable: false },
{ name: 'calls', data: 'count', searchable: false },
{
name: 'option',
data: 'name',
render: DataTable.render.text(),
},
{
name: 'source',
data: 'plugin',
searchable: false,
render: DataTable.render.text(),
},
{
name: 'calls',
data: 'count',
searchable: false,
render: DataTable.render.text(),
},
{
name: 'option_name',
data: 'option_name',
Expand All @@ -221,26 +272,38 @@ jQuery( document ).ready( function () {
{
name: 'checkbox',
data: 'name',
render: ( data, type, row ) => renderCheckboxColumn( row ),
render: ( data, type, row, meta ) =>
renderCheckboxColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'select-all',
},
{ name: 'name', data: 'name' },
{ name: 'source', data: 'plugin' },
{ name: 'name', data: 'name', render: DataTable.render.text() },
{
name: 'source',
data: 'plugin',
render: DataTable.render.text(),
},
{ name: 'size', data: 'size', searchable: false },
{
name: 'autoload',
data: 'autoload',
className: 'autoload',
searchable: false,
orderable: false,
render: DataTable.render.text(),
},
{
name: 'calls',
data: 'count',
searchable: false,
render: DataTable.render.text(),
},
{ name: 'calls', data: 'count', searchable: false },
{
name: 'value',
data: 'value',
render: ( data, type, row ) => renderValueColumn( row ),
render: ( data, type, row, meta ) =>
renderValueColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'actions',
Expand All @@ -251,29 +314,37 @@ jQuery( document ).ready( function () {
{
name: 'checkbox',
data: 'name',
render: ( data, type, row ) => renderCheckboxColumn( row ),
render: ( data, type, row, meta ) =>
renderCheckboxColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'select-all',
},
{ name: 'name', data: 'name' },
{ name: 'source', data: 'plugin' },
{ name: 'name', data: 'name', render: DataTable.render.text() },
{
name: 'source',
data: 'plugin',
render: DataTable.render.text(),
},
{
name: 'size',
data: 'size',
searchable: false,
render: ( data ) => `<span class="num">${ data }</span>`,
render: ( data ) =>
`<span class="num">${ escapeHtml( data ) }</span>`,
},
{
name: 'autoload',
data: 'autoload',
className: 'autoload',
searchable: false,
render: DataTable.render.text(),
},
{
name: 'value',
data: 'value',
render: ( data, type, row ) => renderValueColumn( row ),
render: ( data, type, row, meta ) =>
renderValueColumn( row, meta.settings.sTableId ),
orderable: false,
searchable: false,
className: 'actions',
Expand Down Expand Up @@ -319,36 +390,55 @@ jQuery( document ).ready( function () {
}
}

/**
* Checks whether an autoload value means the option is not autoloaded.
* WordPress 6.6+ stores 'on', 'off', 'auto', 'auto-on' and 'auto-off' next to the legacy 'yes' and 'no'.
*
* @param {string} autoload - The autoload value.
*
* @return {boolean} True if the option is not autoloaded.
*/
function isNotAutoloaded( autoload ) {
return [ 'no', 'off', 'auto-off' ].includes( autoload );
}

/**
* Renders the value column for a row.
*
* @param {Object} row - The row data.
* @param {Object} row - The row data.
* @param {string} tableId - The id of the table the row belongs to.
*
* @return {string} - The HTML for the value column.
*/
function renderValueColumn( row ) {
const popoverContent = `<div id="popover_${ row.name }" popover class="aaa-option-optimizer-popover">
<button class="aaa-option-optimizer-popover__close" popovertarget="popover_${ row.name }" popovertargetaction="hide">X</button>
<p><strong>Value of <code>${ row.name }</code></strong></p>
<pre>${ row.value }</pre>
function renderValueColumn( row, tableId ) {
// Build the popover id from the sanitized row id, never the raw name, so
// it is a safe token both here and in the matching popovertarget below.
// The same option can be listed in several tables, so prefix the table id to keep it unique.
const popoverId = tableId + '-popover_' + generateRowId( row.name );
const optionAttr = escapeHtml( row.name );

const popoverContent = `<div id="${ popoverId }" popover class="aaa-option-optimizer-popover">
<button class="aaa-option-optimizer-popover__close" popovertarget="${ popoverId }" popovertargetaction="hide">X</button>
<p><strong>Value of <code>${ escapeHtml( row.name ) }</code></strong></p>
<pre>${ escapeHtml( row.value ) }</pre>
</div>`;

const actions = [
`<button class="button dashicon" popovertarget="popover_${ row.name }">
`<button class="button dashicon" popovertarget="${ popoverId }">
<span class="dashicons dashicons-search"></span>
${ aaaOptionOptimizer.i18n.showValue }
</button>`,
popoverContent,
row.autoload === 'no'
? `<button class="button dashicon add-autoload" data-option="${ row.name }">
isNotAutoloaded( row.autoload )
? `<button class="button dashicon add-autoload" data-option="${ optionAttr }">
<span class="dashicons dashicons-plus"></span>
${ aaaOptionOptimizer.i18n.addAutoload }
</button>`
: `<button class="button dashicon remove-autoload" data-option="${ row.name }">
: `<button class="button dashicon remove-autoload" data-option="${ optionAttr }">
<span class="dashicons dashicons-minus"></span>
${ aaaOptionOptimizer.i18n.removeAutoload }
</button>`,
`<button class="button button-delete delete-option" data-option="${ row.name }">
`<button class="button button-delete delete-option" data-option="${ optionAttr }">
<span class="dashicons dashicons-trash"></span>
${ aaaOptionOptimizer.i18n.deleteOption }
</button>`,
Expand All @@ -365,21 +455,31 @@ jQuery( document ).ready( function () {
* @return {string} - The HTML for the value column.
*/
function renderNonExistingOptionsColumn( row ) {
return `<button class="button button-primary create-option-false" data-option="${ row.name }">
return `<button class="button button-primary create-option-false" data-option="${ escapeHtml(
row.name
) }">
${ aaaOptionOptimizer.i18n.createOptionFalse }
</button>`;
}

/**
* Renders the checkbox column for a row.
*
* @param {Object} row - The row data.
* @param {Object} row - The row data.
* @param {string} tableId - The id of the table the row belongs to.
*
* @return {string} - The HTML for the value column.
*/
function renderCheckboxColumn( row ) {
return `<label for="select-option-${ row.name }">
<input type="checkbox" id="select-option-${ row.name }" class="select-option" data-option="${ row.name }">
function renderCheckboxColumn( row, tableId ) {
// Build the checkbox id from the sanitized row id, never the raw name, so
// the id and its matching label `for` cannot break out of the markup.
// The same option can be listed in several tables, so prefix the table id to keep it unique.
const checkboxId =
tableId + '-select-option-' + generateRowId( row.name );
return `<label for="${ checkboxId }">
<input type="checkbox" id="${ checkboxId }" class="select-option" data-option="${ escapeHtml(
row.name
) }">
</label>`;
}

Expand Down Expand Up @@ -452,36 +552,21 @@ jQuery( document ).ready( function () {
* @param {string} action - The action performed.
*/
function updateRowOnSuccess( response, table, optionName, action ) {
// Get the row ID for the option name.
const rowId = generateRowId( optionName );
// Look the row up by DataTables row ID, not a CSS selector, so option names with special characters still match.
const row = table.row( '#' + generateRowId( optionName ) );
if ( action === 'delete-option' || action === 'create-option-false' ) {
table
.row( 'tr#' + rowId )
.remove()
.draw( 'full-hold' );
row.remove().draw( 'full-hold' );
} else if (
action === 'add-autoload' ||
action === 'remove-autoload'
) {
const autoloadStatus = action === 'add-autoload' ? 'yes' : 'no';
const buttonHTML =
action === 'add-autoload'
? `<button class="button dashicon remove-autoload" data-option="${ optionName }">
<span class="dashicons dashicons-minus"></span>
${ aaaOptionOptimizer.i18n.removeAutoload }
</button>`
: `<button class="button dashicon add-autoload" data-option="${ optionName }">
<span class="dashicons dashicons-plus"></span>
${ aaaOptionOptimizer.i18n.addAutoload }
</button>`;

jQuery( `tr#${ rowId }` )
.find( 'td.autoload' )
.text( autoloadStatus );
const oldButton = `button.${
action === 'add-autoload' ? 'add' : 'remove'
}-autoload`;
jQuery( `tr#${ rowId } ${ oldButton }` ).replaceWith( buttonHTML );
// Update the row data and redraw, so Responsive child rows (collapsed columns) re-render too.
// Server-side tables re-fetch the page, client-side tables re-render the row.
row.data( {
...row.data(),
autoload: action === 'add-autoload' ? 'yes' : 'no',
} );
table.draw( 'full-hold' );
}
}

Expand Down Expand Up @@ -607,7 +692,7 @@ jQuery( document ).ready( function () {
const dt = table.DataTable();

requestData.option_names.forEach( ( optionName ) => {
dt.row( 'tr#option_' + optionName ).remove();
dt.row( '#' + generateRowId( optionName ) ).remove();
} );

dt.draw( 'full-hold' );
Expand All @@ -632,6 +717,13 @@ jQuery( document ).ready( function () {
}
} );

// Tables in hidden tabs can't measure their columns, so recalculate them once their tab is shown.
jQuery( '.aaa-option-optimizer-tabs > .input' ).on( 'change', function () {
DataTable.tables( { visible: true, api: true } )
.columns.adjust()
.responsive.recalc();
} );

// Migration functionality.
jQuery( '#aaa-start-migration' ).on( 'click', function ( e ) {
e.preventDefault();
Expand Down
Loading
Loading