Changeset 3324099
- Timestamp:
- 07/08/2025 07:38:09 AM (8 months ago)
- Location:
- aaa-option-optimizer
- Files:
-
- 7 added
- 2 deleted
- 16 edited
- 1 copied
-
assets/banner-1544x500.jpg (deleted)
-
assets/banner-1544x500.png (added)
-
assets/banner-772x250.jpg (deleted)
-
assets/banner-772x250.png (added)
-
assets/icon-128x128.png (modified) (previous)
-
assets/icon-256x256.png (modified) (previous)
-
assets/icon.svg (added)
-
tags/1.4.0 (copied) (copied from aaa-option-optimizer/trunk)
-
tags/1.4.0/aaa-option-optimizer.php (modified) (1 diff)
-
tags/1.4.0/css/style.css (modified) (1 diff)
-
tags/1.4.0/js/admin-script.js (modified) (2 diffs)
-
tags/1.4.0/known-plugins/known-plugins.json (modified) (8 diffs)
-
tags/1.4.0/package-lock.json (added)
-
tags/1.4.0/package.json (added)
-
tags/1.4.0/readme.txt (modified) (2 diffs)
-
tags/1.4.0/src/class-admin-page.php (modified) (14 diffs)
-
tags/1.4.0/src/class-rest.php (modified) (4 diffs)
-
trunk/aaa-option-optimizer.php (modified) (1 diff)
-
trunk/css/style.css (modified) (1 diff)
-
trunk/js/admin-script.js (modified) (2 diffs)
-
trunk/known-plugins/known-plugins.json (modified) (8 diffs)
-
trunk/package-lock.json (added)
-
trunk/package.json (added)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/class-admin-page.php (modified) (14 diffs)
-
trunk/src/class-rest.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
aaa-option-optimizer/tags/1.4.0/aaa-option-optimizer.php
r3287558 r3324099 8 8 * Plugin URI: https://joost.blog/plugins/aaa-option-optimizer/ 9 9 * Description: Tracks autoloaded options usage and allows the user to optimize them. 10 * Version: 1. 3.210 * Version: 1.4.0 11 11 * License: GPL-3.0+ 12 12 * Author: Joost de Valk -
aaa-option-optimizer/tags/1.4.0/css/style.css
r3254065 r3324099 3 3 border-spacing: 0; 4 4 } 5 .aaa-option-optimizer-tabs .dt-layout-cell.dt-start:has(.dt-length) { 6 display: flex; 7 justify-content: flex-start; 8 gap: 20px; 9 } 10 5 11 .aaa_option_table td, .aaa_option_table th { 6 12 padding: 5px 10px 5px 5px; -
aaa-option-optimizer/tags/1.4.0/js/admin-script.js
r3265010 r3324099 1 /* global jQuery, aaaOptionOptimizer, Option, DataTable, alert */ 2 1 3 /** 2 4 * JavaScript for the admin page. 3 5 * 4 * @package Emilia\OptionOptimizer6 * @package 5 7 */ 6 8 … … 8 10 * Initializes the data tables and sets up event handlers. 9 11 */ 10 jQuery( document ).ready( 11 function ($) { 12 /** 13 * Array of table selectors to initialize. 14 * 15 * @type {string[]} 16 */ 17 const tablesToInitialize = [ 18 '#unused_options_table', 19 '#used_not_autoloaded_table', 20 '#requested_do_not_exist_table', 12 jQuery( document ).ready( function () { 13 /** 14 * Array of table selectors to initialize. 15 * 16 * @type {string[]} 17 */ 18 const tablesToInitialize = [ 19 '#unused_options_table', 20 '#used_not_autoloaded_table', 21 '#requested_do_not_exist_table', 22 ]; 23 24 jQuery( '#all_options_table' ).hide(); 25 jQuery( '#aaa_get_all_options' ).on( 'click', function ( e ) { 26 e.preventDefault(); 27 jQuery( '#all_options_table' ).show(); 28 initializeDataTable( '#all_options_table' ); 29 jQuery( this ).hide(); 30 } ); 31 32 /** 33 * Generate row ID for an option name. 34 * 35 * @param {string} optionName - The option name. 36 * @return {string} The row ID. 37 */ 38 function generateRowId( optionName ) { 39 return 'option_' + optionName.replace( /\./g, '_' ); 40 } 41 42 /** 43 * Initializes the DataTable for the given selector. 44 * 45 * @param {string} selector - The table selector. 46 */ 47 function initializeDataTable( selector ) { 48 const options = { 49 pageLength: 25, 50 autoWidth: false, 51 responsive: true, 52 columns: getColumns( selector ), 53 rowId( data ) { 54 return generateRowId( data.name ); 55 }, 56 initComplete() { 57 this.api().columns( 'source:name' ).every( setupColumnFilters ); 58 }, 59 language: aaaOptionOptimizer.i18n, 60 }; 61 62 if ( selector === '#unused_options_table' ) { 63 options.ajax = { 64 url: 65 aaaOptionOptimizer.root + 66 'aaa-option-optimizer/v1/unused-options', 67 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 68 type: 'GET', 69 dataSrc: 'data', 70 }; 71 options.serverSide = true; 72 options.processing = true; 73 ( options.language = { 74 sZeroRecords: aaaOptionOptimizer.i18n.noAutoloadedButNotUsed, 75 } ), 76 ( options.initComplete = function () { 77 getBulkActionsForm( selector, [ 'autoload-off' ] ).call( 78 this 79 ); 80 this.api() 81 .columns( 'source:name' ) 82 .every( setupColumnFilters ); 83 } ); 84 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 85 } 86 87 if ( selector === '#used_not_autoloaded_table' ) { 88 options.ajax = { 89 url: 90 aaaOptionOptimizer.root + 91 'aaa-option-optimizer/v1/used-not-autoloaded-options', 92 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 93 type: 'GET', 94 dataSrc: 'data', 95 }; 96 options.serverSide = true; 97 options.processing = true; 98 options.language = { 99 sZeroRecords: aaaOptionOptimizer.i18n.noUsedButNotAutoloaded, 100 }; 101 options.initComplete = function () { 102 getBulkActionsForm( selector, [ 'autoload-on' ] ).call( this ); 103 this.api().columns( 'source:name' ).every( setupColumnFilters ); 104 }; 105 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 106 } 107 108 if ( selector === '#requested_do_not_exist_table' ) { 109 options.ajax = { 110 url: 111 aaaOptionOptimizer.root + 112 'aaa-option-optimizer/v1/options-that-do-not-exist', 113 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 114 type: 'GET', 115 dataSrc: 'data', 116 }; 117 options.serverSide = true; 118 options.processing = true; 119 } 120 121 if ( selector === '#all_options_table' ) { 122 options.ajax = { 123 url: 124 aaaOptionOptimizer.root + 125 'aaa-option-optimizer/v1/all-options', 126 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 127 type: 'GET', 128 dataSrc: 'data', 129 }; 130 options.initComplete = function () { 131 getBulkActionsForm( selector, [ 132 'autoload-on', 133 'autoload-off', 134 ] ).call( this ); 135 this.api().columns( 'source:name' ).every( setupColumnFilters ); 136 }; 137 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 138 } 139 140 new DataTable( selector, options ).columns.adjust().responsive.recalc(); 141 } 142 143 /** 144 * Retrieves the columns configuration based on the selector. 145 * 146 * @param {string} selector - The table selector. 147 * 148 * @return {Object[]} - The columns configuration. 149 */ 150 function getColumns( selector ) { 151 const commonColumns = [ 152 { 153 name: 'checkbox', 154 data: 'name', 155 render: ( data, type, row ) => renderCheckboxColumn( row ), 156 orderable: false, 157 searchable: false, 158 className: 'select-all', 159 }, 160 { name: 'name', data: 'name' }, 161 { name: 'source', data: 'plugin' }, 162 { name: 'size', data: 'size', searchable: false }, 163 { 164 name: 'autoload', 165 data: 'autoload', 166 className: 'autoload', 167 searchable: false, 168 orderable: false, 169 }, 170 { 171 name: 'value', 172 data: 'value', 173 render: ( data, type, row ) => renderValueColumn( row ), 174 orderable: false, 175 searchable: false, 176 className: 'actions', 177 }, 21 178 ]; 22 23 $( '#all_options_table' ).hide(); 24 $( '#aaa_get_all_options' ).on( 25 'click', 26 function (e) { 27 e.preventDefault(); 28 $( '#all_options_table' ).show(); 29 initializeDataTable( '#all_options_table' ); 30 $( this ).hide(); 31 } 32 ); 33 34 /** 35 * Initializes the DataTable for the given selector. 36 * 37 * @param {string} selector - The table selector. 38 */ 39 function initializeDataTable(selector) { 40 const options = { 41 pageLength: 25, 42 autoWidth: false, 43 responsive: true, 44 columns: getColumns( selector ), 45 initComplete: function () { 46 this.api().columns( 'source:name' ).every( setupColumnFilters ); 47 } 48 }; 49 50 if (selector === '#all_options_table') { 51 options.ajax = { 52 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/all-options', 53 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 54 type: 'GET', 55 dataSrc: 'data' 56 }; 57 options.rowId = 'row_id'; 58 } 59 60 options.language = aaaOptionOptimizer.i18n; 61 62 const dataTable = new DataTable( selector, options ).columns.adjust().responsive.recalc();; 63 } 64 65 /** 66 * Retrieves the columns configuration based on the selector. 67 * 68 * @param {string} selector - The table selector. 69 * 70 * @returns {Object[]} - The columns configuration. 71 */ 72 function getColumns(selector) { 73 const commonColumns = [ 74 { name: 'name' }, 75 { name: 'source' }, 76 { name: 'size', searchable: false }, 77 { name: 'autoload', className: 'autoload', searchable: false }, 78 { name: 'actions', searchable: false, orderable: false } 179 if ( selector === '#requested_do_not_exist_table' ) { 180 return [ 181 { name: 'option', data: 'name' }, 182 { name: 'source', data: 'plugin', searchable: false }, 183 { name: 'calls', data: 'count', searchable: false }, 184 { 185 name: 'option_name', 186 data: 'option_name', 187 render: ( data, type, row ) => 188 renderNonExistingOptionsColumn( row ), 189 searchable: false, 190 orderable: false, 191 className: 'actions', 192 }, 79 193 ]; 80 81 if (selector === '#requested_do_not_exist_table') { 82 return [ 83 { name: 'name' }, 84 { name: 'source', searchable: false }, 85 { name: 'calls', searchable: false }, 86 { name: 'actions', searchable: false, orderable: false } 87 ]; 88 } else if (selector === '#used_not_autoloaded_table') { 89 return [ 90 { name: 'name' }, 91 { name: 'source' }, 92 { name: 'size', searchable: false }, 93 { name: 'autoload', className: 'autoload', searchable: false }, 94 { name: 'calls', searchable: false }, 95 { name: 'actions', searchable: false, orderable: false } 96 ] 97 } else if (selector === '#all_options_table') { 98 return [ 194 } else if ( selector === '#used_not_autoloaded_table' ) { 195 return [ 196 { 197 name: 'checkbox', 198 data: 'name', 199 render: ( data, type, row ) => renderCheckboxColumn( row ), 200 orderable: false, 201 searchable: false, 202 className: 'select-all', 203 }, 99 204 { name: 'name', data: 'name' }, 100 205 { name: 'source', data: 'plugin' }, 206 { name: 'size', data: 'size', searchable: false }, 207 { 208 name: 'autoload', 209 data: 'autoload', 210 className: 'autoload', 211 searchable: false, 212 orderable: false, 213 }, 214 { name: 'calls', data: 'count', searchable: false }, 215 { 216 name: 'value', 217 data: 'value', 218 render: ( data, type, row ) => renderValueColumn( row ), 219 orderable: false, 220 searchable: false, 221 className: 'actions', 222 }, 223 ]; 224 } else if ( selector === '#all_options_table' ) { 225 return [ 226 { 227 name: 'checkbox', 228 data: 'name', 229 render: ( data, type, row ) => renderCheckboxColumn( row ), 230 orderable: false, 231 searchable: false, 232 className: 'select-all', 233 }, 234 { name: 'name', data: 'name' }, 235 { name: 'source', data: 'plugin' }, 101 236 { 102 237 name: 'size', 103 238 data: 'size', 104 239 searchable: false, 105 render: data => '<span class="num">' + data + '</span>' 106 }, 107 { name: 'autoload', data: 'autoload', className: 'autoload', searchable: false }, 240 render: ( data ) => '<span class="num">' + data + '</span>', 241 }, 242 { 243 name: 'autoload', 244 data: 'autoload', 245 className: 'autoload', 246 searchable: false, 247 }, 108 248 { 109 249 name: 'value', 110 250 data: 'value', 111 render: ( data, type, row) => renderValueColumn( row ),251 render: ( data, type, row ) => renderValueColumn( row ), 112 252 orderable: false, 113 253 searchable: false, 114 className: 'actions' 115 } 116 ]; 254 className: 'actions', 255 }, 256 ]; 257 } 258 259 return commonColumns; 260 } 261 262 /** 263 * Sets up the column filters for the DataTable. 264 */ 265 function setupColumnFilters() { 266 const column = this; 267 const select = document.createElement( 'select' ); 268 select.add( 269 new Option( aaaOptionOptimizer.i18n.filterBySource, '', true, true ) 270 ); 271 column.footer().replaceChildren( select ); 272 273 select.addEventListener( 'change', function () { 274 column.search( select.value, { exact: true } ).draw(); 275 } ); 276 277 column 278 .data() 279 .unique() 280 .sort() 281 .each( function ( d ) { 282 select.add( new Option( d ) ); 283 } ); 284 } 285 286 /** 287 * Renders the value column for a row. 288 * 289 * @param {Object} row - The row data. 290 * 291 * @return {string} - The HTML for the value column. 292 */ 293 function renderValueColumn( row ) { 294 const popoverContent = 295 '<div id="popover_' + 296 row.name + 297 '" popover class="aaa-option-optimizer-popover">' + 298 '<button class="aaa-option-optimizer-popover__close" popovertarget="popover_' + 299 row.name + 300 '" popovertargetaction="hide">X</button>' + 301 '<p><strong>Value of <code>' + 302 row.name + 303 '</code></strong></p>' + 304 '<pre>' + 305 row.value + 306 '</pre>' + 307 '</div>'; 308 309 const actions = [ 310 '<button class="button dashicon" popovertarget="popover_' + 311 row.name + 312 '"><span class="dashicons dashicons-search"></span>' + 313 aaaOptionOptimizer.i18n.showValue + 314 '</button>', 315 popoverContent, 316 row.autoload === 'no' 317 ? '<button class="button dashicon add-autoload" data-option="' + 318 row.name + 319 '"><span class="dashicons dashicons-plus"></span>' + 320 aaaOptionOptimizer.i18n.addAutoload + 321 '</button>' 322 : '<button class="button dashicon remove-autoload" data-option="' + 323 row.name + 324 '"><span class="dashicons dashicons-minus"></span>' + 325 aaaOptionOptimizer.i18n.removeAutoload + 326 '</button>', 327 '<button class="button button-delete delete-option" data-option="' + 328 row.name + 329 '"><span class="dashicons dashicons-trash"></span>' + 330 aaaOptionOptimizer.i18n.deleteOption + 331 '</button >', 332 ]; 333 334 return actions.join( '' ); 335 } 336 337 /** 338 * Renders the value column for a row. 339 * 340 * @param {Object} row - The row data. 341 * 342 * @return {string} - The HTML for the value column. 343 */ 344 function renderNonExistingOptionsColumn( row ) { 345 return ( 346 '<button class="button button-primary create-option-false" data-option="' + 347 row.name + 348 '">' + 349 aaaOptionOptimizer.i18n.createOptionFalse + 350 '</button>' 351 ); 352 } 353 354 /** 355 * Renders the checkbox column for a row. 356 * 357 * @param {Object} row - The row data. 358 * 359 * @return {string} - The HTML for the value column. 360 */ 361 function renderCheckboxColumn( row ) { 362 return ( 363 '<label for="select-option-' + 364 row.name + 365 '">' + 366 '<input type="checkbox" id="select-option-' + 367 row.name + 368 '" class="select-option" data-option="' + 369 row.name + 370 '">' + 371 '</label>' 372 ); 373 } 374 375 jQuery( '#aaa-option-reset-data' ).on( 'click', function ( e ) { 376 e.preventDefault(); 377 jQuery.ajax( { 378 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/reset', 379 method: 'POST', 380 beforeSend: ( xhr ) => 381 xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 382 success: ( 383 response // eslint-disable-line no-unused-vars 384 ) => 385 ( window.location = 386 window.location.href + '&tracking_reset=true' ), 387 error: ( response ) => 388 console.error( 'Failed to reset tracking.', response ), // eslint-disable-line no-console 389 } ); 390 } ); 391 392 /** 393 * Handles the table actions (add-autoload, remove-autoload, delete-option). 394 * 395 * @param {Event} e - The click event. 396 */ 397 function handleTableActions( e ) { 398 e.preventDefault(); 399 const button = jQuery( this ); 400 const table = button.closest( 'table' ).DataTable(); 401 const optionName = button.data( 'option' ); 402 403 const requestData = { option_name: optionName }; 404 let action = ''; 405 let route = ''; 406 407 if ( button.hasClass( 'create-option-false' ) ) { 408 action = route = 'create-option-false'; 409 } else if ( button.hasClass( 'delete-option' ) ) { 410 action = route = 'delete-option'; 411 } else { 412 action = button.hasClass( 'add-autoload' ) 413 ? 'add-autoload' 414 : 'remove-autoload'; 415 route = 'update-autoload'; 416 requestData.autoload = action === 'add-autoload' ? 'yes' : 'no'; 417 } 418 419 jQuery.ajax( { 420 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/' + route, 421 method: 'POST', 422 beforeSend: ( xhr ) => 423 xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 424 data: requestData, 425 success: ( response ) => 426 updateRowOnSuccess( response, table, optionName, action ), 427 error: ( response ) => 428 // eslint-disable-next-line no-console 429 console.error( 430 'Failed to ' + action + ' for ' + optionName + '.', 431 response 432 ), 433 } ); 434 } 435 436 /** 437 * Updates the row on successful AJAX response. 438 * 439 * @param {Object} response - The AJAX response. 440 * @param {DataTable} table - The DataTable instance. 441 * @param {string} optionName - The option name. 442 * @param {string} action - The action performed. 443 */ 444 function updateRowOnSuccess( response, table, optionName, action ) { 445 // Get the row ID for the option name. 446 const rowId = generateRowId( optionName ); 447 if ( action === 'delete-option' || action === 'create-option-false' ) { 448 table 449 .row( 'tr#' + rowId ) 450 .remove() 451 .draw( 'full-hold' ); 452 } else if ( 453 action === 'add-autoload' || 454 action === 'remove-autoload' 455 ) { 456 const autoloadStatus = action === 'add-autoload' ? 'yes' : 'no'; 457 const buttonHTML = 458 action === 'add-autoload' 459 ? '<button class="button dashicon remove-autoload" data-option="' + 460 optionName + 461 '"><span class="dashicons dashicons-minus"></span>' + 462 aaaOptionOptimizer.i18n.removeAutoload + 463 '</button>' 464 : '<button class="button dashicon add-autoload" data-option="' + 465 optionName + 466 '"><span class="dashicons dashicons-plus"></span>' + 467 aaaOptionOptimizer.i18n.addAutoload + 468 '</button>'; 469 470 jQuery( 'tr#' + rowId ) 471 .find( 'td.autoload' ) 472 .text( autoloadStatus ); 473 const oldButton = 474 'button.' + 475 ( action === 'add-autoload' ? 'add' : 'remove' ) + 476 '-autoload'; 477 jQuery( 'tr#' + rowId + ' ' + oldButton ).replaceWith( buttonHTML ); 478 } 479 } 480 481 // AJAX Event Handling (add-autoload, remove-autoload, delete-option). 482 jQuery( 'table tbody' ).on( 483 'click', 484 '.add-autoload, .remove-autoload, .delete-option, .create-option-false', 485 handleTableActions 486 ); 487 488 // Select all options. 489 jQuery( '.select-all-checkbox' ).on( 'change', function () { 490 const table = jQuery( this ).closest( 'table' ); 491 const selectValue = jQuery( this ).prop( 'checked' ); 492 const selectedOptions = table.find( 'input.select-option' ); 493 selectedOptions.prop( 'checked', selectValue ); 494 } ); 495 496 // Generates bulk actions form for DataTable. 497 function getBulkActionsForm( selector, options ) { 498 return function () { 499 const container = jQuery( this.api().table().container() ); 500 501 const form = jQuery( 502 '<form class="aaaoo-bulk-form" action="#" method="post" style="display:flex;gap:10px;"></form>' 503 ); 504 505 let selectOptions = ''; 506 507 if ( options.includes( 'autoload-on' ) ) { 508 selectOptions = 509 '<option value="autoload-on">' + 510 aaaOptionOptimizer.i18n.addAutoload + 511 '</option>'; 117 512 } 118 513 119 return commonColumns; 120 } 121 122 /** 123 * Sets up the column filters for the DataTable. 124 */ 125 function setupColumnFilters() { 126 const column = this; 127 const select = document.createElement( 'select' ); 128 select.add( new Option( aaaOptionOptimizer.i18n.filterBySource, '', true, true ) ); 129 column.footer().replaceChildren( select ); 130 131 select.addEventListener( 132 'change', 133 function () { 134 column.search( select.value, { exact: true } ).draw(); 135 } 514 if ( options.includes( 'autoload-off' ) ) { 515 selectOptions += 516 '<option value="autoload-off">' + 517 aaaOptionOptimizer.i18n.removeAutoload + 518 '</option>'; 519 } 520 521 const select = jQuery( 522 '<select class="aaaoo-bulk-select"><option value="">' + 523 aaaOptionOptimizer.i18n.bulkActions + 524 selectOptions + 525 '</option><option value="delete">' + 526 aaaOptionOptimizer.i18n.delete + 527 '</option></select>' 136 528 ); 137 529 138 column.data().unique().sort().each( 139 function (d) { 140 select.add( new Option( d ) ); 141 } 530 const button = jQuery( 531 '<button type="submit" class="button aaaoo-apply-bulk-action" data-table="' + 532 selector + 533 '">' + 534 aaaOptionOptimizer.i18n.apply + 535 '</button>' 142 536 ); 143 } 144 145 /** 146 * Renders the value column for a row. 147 * 148 * @param {Object} row - The row data. 149 * 150 * @returns {string} - The HTML for the value column. 151 */ 152 function renderValueColumn(row) { 153 const popoverContent = '<div id="popover_' + row.name + '" popover class="aaa-option-optimizer-popover">' + 154 '<button class="aaa-option-optimizer-popover__close" popovertarget="popover_' + row.name + '" popovertargetaction="hide">X</button>' + 155 '<p><strong>Value of <code>' + row.name + '</code></strong></p>' + 156 '<pre>' + row.value + '</pre>' + 157 '</div>'; 158 159 const actions = [ 160 '<button class="button dashicon" popovertarget="popover_' + row.name + '"><span class="dashicons dashicons-search"></span>' + aaaOptionOptimizer.i18n.showValue + '</button>', 161 popoverContent, 162 row.autoload === 'no' ? 163 '<button class="button dashicon add-autoload" data-option="' + row.name + '"><span class="dashicons dashicons-plus"></span>' + aaaOptionOptimizer.i18n.addAutoload + '</button>' : 164 '<button class="button dashicon remove-autoload" data-option="' + row.name + '"><span class="dashicons dashicons-minus"></span>' + aaaOptionOptimizer.i18n.removeAutoload + '</button>', 165 '<button class="button button-delete delete-option" data-option="' + row.name + '"><span class="dashicons dashicons-trash"></span>' + aaaOptionOptimizer.i18n.deleteOption + '</button >' 166 ]; 167 168 return actions.join( '' ); 169 } 170 171 $( '#aaa-option-reset-data' ).on( 172 'click', 173 function (e) { 174 e.preventDefault(); 175 $.ajax( 176 { 177 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/reset', 178 method: 'POST', 179 beforeSend: xhr => xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 180 success: response => window.location = window.location.href + '&tracking_reset=true', 181 error: response => console.error( 182 'Failed to reset tracking.', 183 response 184 ) 185 } 186 ); 537 538 form.append( select, button ); 539 540 // Add the form to the .dt-start cell 541 container.find( '.dt-layout-cell.dt-start ' ).prepend( form ); 542 543 // Move .dt-length to .dt-layout-cell.dt-end 544 // const lengthSelector = container.find(".dt-length"); // same as div.dt-length 545 // const targetEndCell = container.find(".dt-layout-cell.dt-end"); 546 // if (lengthSelector.length && targetEndCell.length) { 547 // targetEndCell.append(lengthSelector); 548 // } 549 }; 550 } 551 552 // Apply bulk action. 553 jQuery( '.aaa-option-optimizer-tabs' ).on( 554 'click', 555 '.aaaoo-apply-bulk-action', 556 function ( e ) { 557 e.preventDefault(); 558 const button = jQuery( this ); 559 const select = jQuery( button ).siblings( '.aaaoo-bulk-select' ); 560 const bulkAction = select.val(); 561 const table = jQuery( button.data( 'table' ) ); 562 const selectedOptions = table.find( 'input.select-option:checked' ); 563 564 if ( ! bulkAction ) { 565 alert( aaaOptionOptimizer.i18n.noBulkActionSelected ); // eslint-disable-line no-alert 566 return; 187 567 } 188 ); 189 190 /** 191 * Handles the table actions (add-autoload, remove-autoload, delete-option). 192 * 193 * @param {Event} e - The click event. 194 */ 195 function handleTableActions(e) { 196 e.preventDefault(); 197 const button = $( this ); 198 const table = button.closest( 'table' ).DataTable(); 199 const optionName = button.data( 'option' ); 200 201 let requestData = { option_name: optionName }; 202 let action = ''; 203 let route = ''; 204 205 if ( button.hasClass( 'create-option-false' ) ) { 206 action = route = 'create-option-false'; 207 } else if ( button.hasClass( 'delete-option' ) ) { 208 action = route = 'delete-option'; 568 569 if ( selectedOptions.length === 0 ) { 570 alert( aaaOptionOptimizer.i18n.noOptionsSelected ); // eslint-disable-line no-alert 571 return; 572 } 573 574 // For now we only have delete in bulk action. 575 576 const requestData = { 577 option_names: Array.from( selectedOptions ).map( ( option ) => 578 option.getAttribute( 'data-option' ) 579 ), 580 }; 581 582 if ( bulkAction === 'delete' ) { 583 endpoint = 'delete-options'; 209 584 } else { 210 action = button.hasClass( 'add-autoload' ) ? 'add-autoload' : 'remove-autoload';211 r oute = 'update-autoload';212 requestData.autoload = ( action === 'add-autoload' )? 'yes' : 'no';585 endpoint = 'set-autoload-options'; 586 requestData.autoload = 587 bulkAction === 'autoload-on' ? 'yes' : 'no'; 213 588 } 214 589 215 $.ajax( 216 { 217 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/' + route, 218 method: 'POST', 219 beforeSend: xhr => xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 220 data: requestData, 221 success: response => updateRowOnSuccess( response, table, optionName, action ), 222 error: response => console.error( 223 'Failed to ' + action + ' for ' + optionName + '.', 224 response 225 ) 226 } 227 ); 228 } 229 230 /** 231 * Updates the row on successful AJAX response. 232 * 233 * @param {Object} response - The AJAX response. 234 * @param {DataTable} table - The DataTable instance. 235 * @param {string} optionName - The option name. 236 * @param {string} action - The action performed. 237 */ 238 function updateRowOnSuccess(response, table, optionName, action) { 239 if ( action === 'delete-option' || action === 'create-option-false' ) { 240 table.row( 'tr#option_' + optionName ).remove().draw( 'full-hold' ); 241 } else if ( action === 'add-autoload' || action === 'remove-autoload' ) { 242 const autoloadStatus = action === 'add-autoload' ? 'yes' : 'no'; 243 const buttonHTML = action === 'add-autoload' ? 244 '<button class="button dashicon remove-autoload" data-option="' + optionName + '"><span class="dashicons dashicons-minus"></span>' + aaaOptionOptimizer.i18n.removeAutoload + '</button>': 245 '<button class="button dashicon add-autoload" data-option="' + optionName + '"><span class="dashicons dashicons-plus"></span>' + aaaOptionOptimizer.i18n.addAutoload + '</button>'; 246 247 $( 'tr#option_' + optionName ).find( 'td.autoload' ).text( autoloadStatus ); 248 const oldButton = 'button.' + ( action === 'add-autoload' ? 'add' : 'remove' ) + '-autoload'; 249 $( 'tr#option_' + optionName + ' ' + oldButton ).replaceWith( buttonHTML ); 250 } 251 } 252 253 // AJAX Event Handling (add-autoload, remove-autoload, delete-option). 254 $( 'table tbody' ).on( 'click', '.add-autoload, .remove-autoload, .delete-option, .create-option-false', handleTableActions ); 255 256 // Initialize data tables. 257 tablesToInitialize.forEach( 258 function (selector) { 259 if ($( selector ).length) { 260 initializeDataTable( selector ); 261 } 262 } 263 ); 264 } 265 ); 590 jQuery.ajax( { 591 url: 592 aaaOptionOptimizer.root + 593 'aaa-option-optimizer/v1/' + 594 endpoint, 595 method: 'POST', 596 beforeSend: ( xhr ) => 597 xhr.setRequestHeader( 598 'X-WP-Nonce', 599 aaaOptionOptimizer.nonce 600 ), 601 data: requestData, 602 success: () => { 603 const dt = table.DataTable(); 604 605 requestData.option_names.forEach( ( optionName ) => { 606 dt.row( 'tr#option_' + optionName ).remove(); 607 } ); 608 609 dt.draw( 'full-hold' ); 610 611 // Clear the select-all checkbox. 612 table 613 .find( '.select-all-checkbox' ) 614 .prop( 'checked', false ); 615 }, 616 error: ( response ) => { 617 // eslint-disable-next-line no-console 618 console.error( 'Failed to delete options.', response ); 619 }, 620 } ); 621 } 622 ); 623 624 // Initialize data tables. 625 tablesToInitialize.forEach( function ( selector ) { 626 if ( jQuery( selector ).length ) { 627 initializeDataTable( selector ); 628 } 629 } ); 630 } ); -
aaa-option-optimizer/tags/1.4.0/known-plugins/known-plugins.json
r3254065 r3324099 36 36 "option_prefixes": ["chaty_"] 37 37 }, 38 "comment_hacks": { 39 "name": "Comment Experience by Progress Planner", 40 "option_prefixes": ["comment_hacks"] 41 }, 38 42 "complianz": { 39 43 "name": "Complianz GDPR", … … 56 60 "option_prefixes": ["elementor_"] 57 61 }, 62 "enhanced-e-commerce-for-woocommerce-store": { 63 "name": "Conversios: Google Analytics 4 (GA4), Google Ads, Microsoft Ads, and Multi-Channel Conversion Tracking", 64 "option_prefixes": ["ee_"] 65 }, 58 66 "fewer-tags": { 59 67 "name": "Fewer Tags", … … 72 80 "option_prefixes": ["yst_"] 73 81 }, 82 "google-site-kit": { 83 "name": "Site Kit by Google", 84 "option_prefixes": ["googlesitekit_"] 85 }, 74 86 "gravity-forms": { 75 87 "name": "Gravity Forms", 76 88 "option_prefixes": ["gf_","gforms_"] 77 89 }, 90 "woo-billing-with-invoicexpress": { 91 "name": "Invoicing with InvoiceXpress for WooCommerce", 92 "option_prefixes": ["hd_wc_ie_"] 93 }, 78 94 "indeed-membership-pro": { 79 95 "name": "Indeed Ultimate Membership Pro", … … 90 106 "litespeed-cache": { 91 107 "name": "LiteSpeed Cache", 92 "option_prefixes": ["litespeed-", "_litespeed_" ]108 "option_prefixes": ["litespeed-", "_litespeed_", "litespeed."] 93 109 }, 94 110 "loginizer": { … … 96 112 "option_prefixes": ["loginizer_"] 97 113 }, 114 "multibanco-ifthen-software-gateway-for-woocommerce": { 115 "name": "Multibanco, MB WAY, Credit card, Apple Pay, Google Pay, Payshop, Cofidis Pay, and PIX (ifthenpay) for WooCommerce", 116 "option_prefixes": ["cofidispay_ifthen_", "creditcard_ifthen_", "gateway_ifthen_", "mbway_ifthen_", "multibanco_ifthen_", "payshop_"] 117 }, 98 118 "perfmatters": { 99 119 "name": "Perfmatters", … … 112 132 "option_prefixes": ["porto_"] 113 133 }, 134 "product-assembly-cost": { 135 "name": "Product Assembly / Gift Wrap / ... Cost for WooCommerce", 136 "option_prefixes": ["product_assembly_"] 137 }, 138 "progress-planner": { 139 "name": "Progress Planner", 140 "option_prefixes": ["progress_planner_"] 141 }, 114 142 "really-simple-ssl": { 115 143 "name": "Really Simple SSL", … … 132 160 "option_prefixes": ["rank_math_"] 133 161 }, 162 "shop-as-client": { 163 "name": "Shop as Client for WooCommerce", 164 "option_prefixes": ["shop_as_client"] 165 }, 166 "simple-woo-checkout-blocks-cf": { 167 "name": "Simple Checkout Fields Manager for WooCommerce", 168 "option_prefixes": ["swcbcf_"] 169 }, 170 "simple-woocommerce-order-approval": { 171 "name": "Simple WooCommerce Order Approval", 172 "option_prefixes": ["swoa_"] 173 }, 134 174 "updraft": { 135 175 "name": "Updraft Plus", … … 140 180 "option_prefixes": ["wpseo", "yoast_"] 141 181 }, 182 "feed-kuantokusta-for-woocommerce": { 183 "name": "Feed KuantoKusta for WooCommerce", 184 "option_prefixes": ["wc_feed_kuantokusta_"] 185 }, 186 "woo-dpd-pickup": { 187 "name": "DPD / Geopost Pickup and Lockers network for WooCommerce", 188 "option_prefixes": ["woo_dpd_pickup"] 189 }, 190 "woo-dpd-portugal": { 191 "name": "DPD Portugal for WooCommerce", 192 "option_prefixes": ["woo_dpd_portugal"] 193 }, 142 194 "woocommerce": { 143 195 "name": "WooCommerce", 144 196 "option_prefixes": ["wc_", "_transient__woocommerce_", "woocommerce_"] 197 }, 198 "woocommerce-max-quantity": { 199 "name": "Maximum Quantity for WooCommerce Shops", 200 "option_prefixes": ["isa_woocommerce_"] 145 201 }, 146 202 "wordfence": { -
aaa-option-optimizer/tags/1.4.0/readme.txt
r3287558 r3324099 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1. 3.27 Stable tag: 1.4.0 8 8 License: GPL3+ 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.en.html … … 54 54 55 55 == Changelog == 56 57 = 1.4.0 = 58 59 * Performance improvements. 60 * Added bulk-actions to allow optimizing & deleting options in bulk. 61 * Added more known plugins. 56 62 57 63 = 1.3.2 = -
aaa-option-optimizer/tags/1.4.0/src/class-admin-page.php
r3254065 r3324099 14 14 15 15 /** 16 * The map plugin to options class.17 *18 * @var Map_Plugin_To_Options19 */20 private $map_plugin_to_options;21 22 /**23 16 * Register hooks. 24 17 * … … 26 19 */ 27 20 public function register_hooks() { 28 $this->map_plugin_to_options = new Map_Plugin_To_Options();29 30 21 // Register a link to the settings page on the plugins overview page. 31 22 \add_filter( 'plugin_action_links', [ $this, 'filter_plugin_actions' ], 10, 2 ); … … 71 62 'manage_options', 72 63 'aaa-option-optimizer', 73 [ $this, 'render_admin_page ' ]64 [ $this, 'render_admin_page_ajax' ] 74 65 ); 75 66 } … … 124 115 'nonce' => wp_create_nonce( 'wp_rest' ), 125 116 'i18n' => [ 126 'filterBySource' => esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 127 'showValue' => esc_html__( 'Show', 'aaa-option-optimizer' ), 128 'addAutoload' => esc_html__( 'Add autoload', 'aaa-option-optimizer' ), 129 'removeAutoload' => esc_html__( 'Remove autoload', 'aaa-option-optimizer' ), 130 'deleteOption' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 131 132 'search' => esc_html__( 'Search:', 'aaa-option-optimizer' ), 133 'entries' => [ 117 'filterBySource' => esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 118 'showValue' => esc_html__( 'Show', 'aaa-option-optimizer' ), 119 'addAutoload' => esc_html__( 'Add autoload', 'aaa-option-optimizer' ), 120 'removeAutoload' => esc_html__( 'Remove autoload', 'aaa-option-optimizer' ), 121 'deleteOption' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 122 'createOptionFalse' => esc_html__( 'Create option with value false', 'aaa-option-optimizer' ), 123 'noAutoloadedButNotUsed' => esc_html__( 'All autoloaded options are in use.', 'aaa-option-optimizer' ), 124 'noUsedButNotAutoloaded' => esc_html__( 'All options that are used are autoloaded.', 'aaa-option-optimizer' ), 125 'noOptionsSelected' => esc_html__( 'No options selected.', 'aaa-option-optimizer' ), 126 'bulkActions' => esc_html__( 'Bulk actions', 'aaa-option-optimizer' ), 127 'noBulkActionSelected' => esc_html__( 'No action selected.', 'aaa-option-optimizer' ), 128 'delete' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 129 'apply' => esc_html__( 'Apply', 'aaa-option-optimizer' ), 130 131 'search' => esc_html__( 'Search:', 'aaa-option-optimizer' ), 132 'entries' => [ 134 133 '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ), 135 134 '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ), 136 135 ], 137 'sInfo' => sprintf(136 'sInfo' => sprintf( 138 137 // translators: %1$s is the start, %2$s is the end, %3$s is the total, %4$s is the entries. 139 138 esc_html__( 'Showing %1$s to %2$s of %3$s %4$s', 'aaa-option-optimizer' ), … … 143 142 '_ENTRIES-TOTAL_' 144 143 ), 145 'sInfoEmpty' => esc_html__( 'Showing 0 to 0 of 0 entries', 'aaa-option-optimizer' ),146 'sInfoFiltered' => sprintf(144 'sInfoEmpty' => esc_html__( 'Showing 0 to 0 of 0 entries', 'aaa-option-optimizer' ), 145 'sInfoFiltered' => sprintf( 147 146 // translators: %1$s is the max, %2$s is the entries-max. 148 147 esc_html__( '(filtered from %1$s total %2$s)', 'aaa-option-optimizer' ), … … 150 149 '_ENTRIES-MAX_' 151 150 ), 152 'sZeroRecords' => esc_html__( 'No matching records found', 'aaa-option-optimizer' ),153 'oAria' => [151 'sZeroRecords' => esc_html__( 'No matching records found', 'aaa-option-optimizer' ), 152 'oAria' => [ 154 153 'orderable' => esc_html__( ': Activate to sort', 'aaa-option-optimizer' ), 155 154 'orderableReverse' => esc_html__( ': Activate to invert sorting', 'aaa-option-optimizer' ), … … 198 197 echo '<th class="source">' . esc_html__( 'Source', 'aaa-option-optimizer' ) . '</th>'; 199 198 break; 199 case 'select-all': 200 echo '<th class="select-all"><input type="checkbox" class="select-all-checkbox" /></th>'; 201 break; 200 202 } 201 203 } … … 205 207 206 208 /** 207 * Get the length of a value.208 *209 * @param mixed $value The input value.210 *211 * @return string212 */213 private function get_length( $value ) {214 if ( empty( $value ) ) {215 return '0.00';216 }217 if ( is_array( $value ) || is_object( $value ) ) {218 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- intended here.219 $length = strlen( serialize( $value ) );220 } elseif ( is_string( $value ) || is_numeric( $value ) ) {221 $length = strlen( strval( $value ) );222 }223 if ( ! isset( $length ) ) {224 return '0.00';225 }226 return number_format( ( $length / 1024 ), 2 );227 }228 229 /**230 209 * Renders the admin page. 231 210 * 232 211 * @return void 233 212 */ 234 public function render_admin_page () {213 public function render_admin_page_ajax() { 235 214 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 236 $all_options = wp_load_alloptions();237 // Filter out transients.238 $autoload_options = array_filter(239 $all_options,240 function ( $value, $key ) {241 return strpos( $key, '_transient_' ) === false;242 },243 ARRAY_FILTER_USE_BOTH244 );245 246 $unused_options = [];247 $non_autoloaded_options = [];248 249 // Get the autoloaded options that aren't used.250 foreach ( $autoload_options as $option => $value ) {251 if ( isset( $option_optimizer['used_options'][ $option ] ) ) {252 continue;253 }254 $unused_options[ $option ] = $value;255 }256 257 // Determine the options that _are_ used, but not auto-loaded.258 foreach ( $option_optimizer['used_options'] as $option => $count ) {259 if ( isset( $autoload_options[ $option ] ) ) {260 continue;261 }262 $non_autoloaded_options[ $option ] = $count;263 }264 265 // Some of the options that are used but not auto-loaded, may not exist.266 if ( ! empty( $non_autoloaded_options ) ) {267 $options_that_do_not_exist = [];268 $non_autoloaded_options_full = [];269 foreach ( $non_autoloaded_options as $option => $count ) {270 $value = get_option( $option, 'aaa-no-return-value' );271 if ( $value === 'aaa-no-return-value' ) {272 $options_that_do_not_exist[ $option ] = $count;273 continue;274 }275 $non_autoloaded_options_full[ $option ] = [276 'count' => $count,277 'value' => $value,278 ];279 }280 }281 215 282 216 // Start HTML output. … … 322 256 <?php 323 257 echo '<h2 id="unused-autoloaded">' . esc_html__( 'Unused, but autoloaded', 'aaa-option-optimizer' ) . '</h2>'; 324 if ( ! empty( $unused_options ) ) { 325 echo '<p>' . esc_html__( 'The following options are autoloaded on each pageload, but AAA Option Optimizer has not been able to detect them being used.', 'aaa-option-optimizer' ); 326 echo '<table style="width:100%" id="unused_options_table" class="aaa_option_table">'; 327 $this->table_section( 'thead', [ 'option', 'source', 'size', 'autoload', 'actions' ] ); 328 echo '<tbody>'; 329 foreach ( $unused_options as $option => $value ) { 330 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '"><td>' . esc_html( $option ) . '</td>'; 331 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 332 echo '<td><span class="num">' . esc_html( $this->get_length( $value ) ) . '</span></td>'; 333 echo '<td class="autoload">yes</td>'; 334 echo '<td class="actions">'; 335 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped in get_value_button. 336 echo $this->get_value_button( $option, $value ); 337 echo '<button class="button button-primary remove-autoload" data-option="' . esc_attr( $option ) . '"><span class="dashicons dashicons-minus"></span> ' . esc_html__( 'Remove autoload', 'aaa-option-optimizer' ) . '</button> '; 338 echo ' <button class="button button-delete delete-option" data-option="' . esc_attr( $option ) . '"><span class="dashicons dashicons-trash"></span> ' . esc_html__( 'Delete', 'aaa-option-optimizer' ) . '</button>'; 339 echo '</td></tr>'; 340 } 341 echo '</tbody>'; 342 $this->table_section( 'tfoot', [ 'option', 'source', 'size', 'autoload', 'actions' ] ); 343 echo '</table>'; 344 } else { 345 echo '<p>' . esc_html__( 'All autoloaded options are in use.', 'aaa-option-optimizer' ) . '</p>'; 346 } 347 ?> 258 echo '<p>' . esc_html__( 'The following options are autoloaded on each pageload, but AAA Option Optimizer has not been able to detect them being used.', 'aaa-option-optimizer' ); 259 echo '<table style="width:100%" id="unused_options_table" class="aaa_option_table">'; 260 $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); 261 ?> 262 <tbody> 263 <tr> 264 <td class="select-all"></td> 265 <td></td> 266 <td></td> 267 <td></td> 268 <td></td> 269 <td class="actions"></td> 270 </tr> 271 </tbody> 272 <?php 273 $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); 274 ?> 275 </table> 348 276 </div> 349 277 <input class="input" name="tabs" type="radio" id="tab-2"/> … … 352 280 <?php 353 281 // Render differences. 354 if ( ! empty( $non_autoloaded_options ) ) {355 282 echo '<h2 id="used-not-autoloaded">' . esc_html__( 'Used, but not autoloaded options', 'aaa-option-optimizer' ) . '</h2>'; 356 283 echo '<p>' . esc_html__( 'The following options are *not* autoloaded on each pageload, but AAA Option Optimizer has detected that they are being used. If one of the options below has been called a lot and is not very big, you might consider adding autoload to that option.', 'aaa-option-optimizer' ); 357 284 echo '<table style="width:100%;" id="used_not_autoloaded_table" class="aaa_option_table">'; 358 $this->table_section( 'thead', [ 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 359 echo '<tbody>'; 360 foreach ( $non_autoloaded_options_full as $option => $arr ) { 361 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '">'; 362 echo '<td>' . esc_html( $option ) . '</td>'; 363 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 364 echo '<td><span class="num">' . esc_html( $this->get_length( $arr['value'] ) ) . '</span></td>'; 365 echo '<td class="autoload">no</td>'; 366 echo '<td>' . esc_html( $arr['count'] ) . '</td>'; 367 echo '<td class="actions">'; 368 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped in get_value_button. 369 echo $this->get_value_button( $option, $arr['value'] ); 370 echo '<button class="button button-primary add-autoload" data-option="' . esc_attr( $option ) . '">' . esc_html__( 'Add autoload', 'aaa-option-optimizer' ) . '</button> '; 371 echo '</td></tr>'; 372 } 373 echo '</tbody>'; 374 $this->table_section( 'tfoot', [ 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 375 echo '</table>'; 376 } else { 377 echo '<p>' . esc_html__( 'All options that are used are autoloaded.', 'aaa-option-optimizer' ) . '</p>'; 378 } 379 ?> 285 $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 286 ?> 287 <tbody> 288 <tr> 289 <td class="select-all"></td> 290 <td></td> 291 <td></td> 292 <td></td> 293 <td></td> 294 <td></td> 295 <td class="actions"></td> 296 </tr> 297 </tbody> 298 <?php $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); ?> 299 </table> 380 300 </div> 381 301 <input class="input" name="tabs" type="radio" id="tab-3"/> … … 383 303 <div class="panel"> 384 304 <?php 385 if ( ! empty( $options_that_do_not_exist ) ) { 386 echo '<h2 id="requested-do-not-exist">' . esc_html__( 'Requested options that do not exist', 'aaa-option-optimizer' ) . '</h2>'; 387 echo '<p>' . esc_html__( 'The following options are requested sometimes, but AAA Option Optimizer has detected that they do not exist. If one of the options below has been called a lot, it might help to create it with a value of false.', 'aaa-option-optimizer' ); 388 echo '<table width="100%" id="requested_do_not_exist_table" class="aaa_option_table">'; 389 $this->table_section( 'thead', [ 'option', 'source', 'calls', 'actions' ] ); 390 echo '<tbody>'; 391 foreach ( $options_that_do_not_exist as $option => $count ) { 392 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '">'; 393 echo '<td>' . esc_html( $option ) . '</td>'; 394 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 395 echo '<td>' . esc_html( $count ) . '</td>'; 396 echo '<td class="actions"><button class="button button-primary create-option-false" data-option="' . esc_attr( $option ) . '">' . esc_html__( 'Create option with value false', 'aaa-option-optimizer' ) . '</button> '; 397 } 398 echo '</tbody>'; 399 $this->table_section( 'tfoot', [ 'option', 'source', 'calls', 'actions' ] ); 400 echo '</table>'; 401 } 402 ?> 305 echo '<h2 id="requested-do-not-exist">' . esc_html__( 'Requested options that do not exist', 'aaa-option-optimizer' ) . '</h2>'; 306 echo '<p>' . esc_html__( 'The following options are requested sometimes, but AAA Option Optimizer has detected that they do not exist. If one of the options below has been called a lot, it might help to create it with a value of false.', 'aaa-option-optimizer' ); 307 echo '<table width="100%" id="requested_do_not_exist_table" class="aaa_option_table">'; 308 $this->table_section( 'thead', [ 'option', 'source', 'calls', 'actions' ] ); 309 ?> 310 <tbody> 311 <tr> 312 <td></td> 313 <td></td> 314 <td></td> 315 <td class="actions"></td> 316 </tr> 317 </tbody> 318 <?php 319 $this->table_section( 'tfoot', [ 'option', 'source', 'calls', 'actions' ] ); 320 ?> 321 </table> 403 322 </div> 404 323 <input class="input" name="tabs" type="radio" id="tab-4"/> … … 408 327 <button id="aaa_get_all_options" class="button button-primary"><?php esc_html_e( 'Get all options', 'aaa-option-optimizer' ); ?></button> 409 328 <table class="aaa_option_table" id="all_options_table" style="display:none;"> 410 <?php $this->table_section( 'thead', [ ' option', 'source', 'size', 'autoload', 'actions' ] ); ?>329 <?php $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); ?> 411 330 <tbody> 412 331 <tr> 332 <td class="select-all"></td> 413 333 <td></td> 414 334 <td></td> … … 416 336 <td></td> 417 337 <td class="actions"></td> 418 </tr> 338 </tr> 419 339 </tbody> 420 <?php $this->table_section( 'tfoot', [ ' option', 'source', 'size', 'autoload', 'actions' ] ); ?>340 <?php $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); ?> 421 341 </table> 422 342 </div> … … 424 344 <?php 425 345 } 426 427 /**428 * Get html to show a popover.429 *430 * @param string $name The name of the option, used in the id of the popover.431 * @param mixed $value The value to show.432 *433 * @return string434 */435 private function get_value_button( string $name, $value ): string {436 $string = is_string( $value ) ? $value : wp_json_encode( $value );437 $id = 'aaa-option-optimizer-' . esc_attr( $name );438 return '439 <button class="button" popovertarget="' . $id . '"><span class="dashicons dashicons-search"></span> ' . esc_html__( 'Show', 'aaa-option-optimizer' ) . '</button>440 <div id="' . $id . '" popover class="aaa-option-optimizer-popover">441 <button class="aaa-option-optimizer-popover__close" popovertarget="' . $id . '" popovertargetaction="hide">X</button>' .442 // translators: %s is the name of the option.443 '<p><strong>' . sprintf( esc_html__( 'Value of %s', 'aaa-option-optimizer' ), '<code>' . esc_html( $name ) . '</code>' ) . '</strong></p>444 <pre>' . htmlentities( $string, ENT_QUOTES | ENT_SUBSTITUTE ) . '</pre>445 </div>';446 }447 448 /**449 * Find plugin in known plugin prefixes list.450 *451 * @param string $option The option name.452 *453 * @return string454 */455 private function get_plugin_name( $option ) {456 return $this->map_plugin_to_options->get_plugin_name( $option );457 }458 346 } -
aaa-option-optimizer/tags/1.4.0/src/class-rest.php
r3254065 r3324099 122 122 ] 123 123 ); 124 125 \register_rest_route( 126 'aaa-option-optimizer/v1', 127 '/unused-options', 128 [ 129 'methods' => 'GET', 130 'callback' => [ $this, 'get_unused_options' ], 131 'permission_callback' => function () { 132 return current_user_can( 'manage_options' ); 133 }, 134 ] 135 ); 136 137 \register_rest_route( 138 'aaa-option-optimizer/v1', 139 '/used-not-autoloaded-options', 140 [ 141 'methods' => 'GET', 142 'callback' => [ $this, 'get_used_not_autoloaded_options' ], 143 'permission_callback' => function () { 144 return current_user_can( 'manage_options' ); 145 }, 146 ] 147 ); 148 149 \register_rest_route( 150 'aaa-option-optimizer/v1', 151 '/options-that-do-not-exist', 152 [ 153 'methods' => 'GET', 154 'callback' => [ $this, 'get_options_that_do_not_exist' ], 155 'permission_callback' => function () { 156 return current_user_can( 'manage_options' ); 157 }, 158 ] 159 ); 160 161 \register_rest_route( 162 'aaa-option-optimizer/v1', 163 '/delete-options', 164 [ 165 'methods' => 'POST', 166 'callback' => [ $this, 'delete_options' ], 167 'permission_callback' => function () { 168 return current_user_can( 'manage_options' ); 169 }, 170 ] 171 ); 172 173 \register_rest_route( 174 'aaa-option-optimizer/v1', 175 '/set-autoload-options', 176 [ 177 'methods' => 'POST', 178 'callback' => [ $this, 'set_autoload_options' ], 179 'permission_callback' => function () { 180 return current_user_can( 'manage_options' ); 181 }, 182 ] 183 ); 124 184 } 125 185 … … 148 208 $output[] = [ 149 209 'name' => $option->option_name, 150 'plugin' => $this-> map_plugin_to_options->get_plugin_name( $option->option_name ),210 'plugin' => $this->get_plugin_name( $option->option_name ), 151 211 'value' => htmlentities( $option->option_value, ENT_QUOTES | ENT_SUBSTITUTE ), 152 'size' => number_format( strlen( $option->option_value ) / 1024, 2 ), 212 'size' => $this->get_length( $option->option_value ), 213 'raw_size' => strlen( $option->option_value ), 153 214 'autoload' => $option->autoload, 154 'row_id' => 'option_' . $option->option_name,155 215 ]; 156 216 } 157 217 return new \WP_REST_Response( [ 'data' => $output ], 200 ); 218 } 219 220 /** 221 * Get unused options. 222 * 223 * @return \WP_Error|\WP_REST_Response 224 */ 225 public function get_unused_options() { 226 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 227 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 228 } 229 230 global $wpdb; 231 232 // Load used options from option_optimizer. 233 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 234 $used_options = $option_optimizer['used_options']; 235 236 $query = " 237 SELECT option_name 238 FROM {$wpdb->options} 239 WHERE autoload IN ( '" . implode( "', '", esc_sql( \wp_autoload_values_to_autoload() ) ) . "' ) 240 AND option_name NOT LIKE '%_transient_%' 241 "; 242 243 // Search. 244 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 245 if ( '' !== $search ) { 246 $query .= " AND option_name LIKE '%" . esc_sql( $search ) . "%'"; 247 } 248 249 // Get autoloaded, non-transient option names. 250 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 251 $query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 252 ); 253 254 // Find unused autoloaded option names. 255 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 256 $unused_keys = array_diff_key( $autoload_option_keys, $used_options ); 257 $total_unused = count( $unused_keys ); 258 259 // Sort order. 260 [ $order_column, $order_dir ] = $this->get_sort_params(); 261 262 // Pagination. 263 [ $offset, $limit ] = $this->get_pagination_params(); 264 $paged_option_names = array_keys( $unused_keys ); 265 266 // Optimize by slicing early for default sort, since we can sort $autoloaded_option_names in advance. 267 if ( 'name' === $order_column && SORT_ASC === $order_dir ) { 268 $paged_option_names = array_slice( $paged_option_names, $offset, $limit ); 269 } 270 271 $response_data = []; 272 273 if ( ! empty( $paged_option_names ) ) { 274 $placeholders = implode( ',', array_fill( 0, count( $paged_option_names ), '%s' ) ); 275 $value_query = " 276 SELECT option_name, option_value 277 FROM {$wpdb->options} 278 WHERE option_name IN ( {$placeholders} ) 279 "; 280 281 $results = $wpdb->get_results( $wpdb->prepare( $value_query, ...$paged_option_names ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 282 283 // Format output. 284 foreach ( $results as $row ) { 285 $response_data[] = [ 286 'name' => $row->option_name, 287 'plugin' => $this->get_plugin_name( $row->option_name ), 288 'value' => htmlentities( $row->option_value, ENT_QUOTES | ENT_SUBSTITUTE ), 289 'size' => $this->get_length( $row->option_value ), 290 'raw_size' => strlen( $row->option_value ), 291 'autoload' => 'yes', 292 ]; 293 } 294 295 // Sorting, skip if "name" column is sorted in ascending order since that is the default. 296 if ( ! ( 'name' === $order_column && SORT_ASC === $order_dir ) ) { 297 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 298 299 // Now we can slice after sort. 300 $response_data = array_slice( $response_data, $offset, $limit ); 301 } 302 } 303 304 // Return response. 305 return new \WP_REST_Response( 306 [ 307 'draw' => intval( $_GET['draw'] ?? 0 ), 308 'recordsTotal' => $total_unused, 309 'recordsFiltered' => $total_unused, 310 'data' => $response_data, 311 ], 312 200 313 ); 314 } 315 316 /** 317 * Get used, but not autoloaded options. 318 * 319 * @return \WP_Error|\WP_REST_Response 320 */ 321 public function get_used_not_autoloaded_options() { 322 if ( 323 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 324 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 325 ) { 326 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 327 } 328 329 global $wpdb; 330 331 // Load used options from option_optimizer. 332 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 333 $used_options = $option_optimizer['used_options']; 334 335 if ( empty( $used_options ) ) { 336 return new \WP_REST_Response( 337 [ 338 'draw' => intval( $_GET['draw'] ?? 0 ), 339 'recordsTotal' => 0, 340 'recordsFiltered' => 0, 341 'data' => [], 342 ], 343 200 344 ); 345 } 346 347 // Get all autoloaded, non-transient option names. 348 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 349 " 350 SELECT option_name 351 FROM {$wpdb->options} 352 WHERE autoload IN ( '" . implode( "', '", esc_sql( wp_autoload_values_to_autoload() ) ) . "' ) 353 AND option_name NOT LIKE '%_transient_%' 354 ORDER BY option_name ASC 355 " 356 ); 357 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 358 359 // Find used options that are not autoloaded. 360 $non_autoloaded_used_keys = array_diff_key( $used_options, $autoload_option_keys ); 361 362 // Search. 363 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 364 if ( '' !== $search ) { 365 $non_autoloaded_used_keys = array_filter( 366 $non_autoloaded_used_keys, 367 function ( $option_name ) use ( $search ) { 368 return false !== stripos( $option_name, $search ); 369 }, 370 ARRAY_FILTER_USE_KEY 371 ); 372 } 373 374 if ( empty( $non_autoloaded_used_keys ) ) { 375 return new \WP_REST_Response( 376 [ 377 'draw' => intval( $_GET['draw'] ?? 0 ), 378 'recordsTotal' => 0, 379 'recordsFiltered' => 0, 380 'data' => [], 381 ], 382 200 383 ); 384 } 385 386 // Sort order. 387 [ $order_column, $order_dir ] = $this->get_sort_params(); 388 389 // Pagination. 390 [ $offset, $limit ] = $this->get_pagination_params(); 391 392 // We can't slice early here, because we can't sort $used_options in advance. 393 $paged_option_names = array_keys( $non_autoloaded_used_keys ); 394 395 $response_data = []; 396 397 // $paged_option_names is not empty. 398 // Fetch values directly from DB without using get_option(). 399 $placeholders = implode( ',', array_fill( 0, count( $paged_option_names ), '%s' ) ); 400 $sql = " 401 SELECT option_name, option_value 402 FROM {$wpdb->options} 403 WHERE option_name IN ( {$placeholders} )"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 404 405 $results = $wpdb->get_results( $wpdb->prepare( $sql, ...$paged_option_names ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 406 407 foreach ( $results as $row ) { 408 $response_data[] = [ 409 'name' => $row->option_name, 410 'plugin' => $this->get_plugin_name( $row->option_name ), 411 'value' => htmlentities( maybe_serialize( $row->option_value ), ENT_QUOTES | ENT_SUBSTITUTE ), 412 'size' => $this->get_length( $row->option_value ), 413 'raw_size' => strlen( $row->option_value ), 414 'autoload' => 'no', 415 'count' => $used_options[ $row->option_name ] ?? 0, 416 ]; 417 } 418 419 $total_filtered = count( $response_data ); 420 421 // Sort and slice after. 422 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 423 $response_data = array_slice( $response_data, $offset, $limit ); 424 425 // Return response. 426 return new \WP_REST_Response( 427 [ 428 'draw' => intval( $_GET['draw'] ?? 0 ), 429 'recordsTotal' => $total_filtered, 430 'recordsFiltered' => $total_filtered, 431 'data' => $response_data, 432 ], 433 200 434 ); 435 } 436 437 /** 438 * Get options that do not exist. 439 * Some of the options that are used but not auto-loaded, may not exist. 440 * 441 * @return \WP_Error|\WP_REST_Response 442 */ 443 public function get_options_that_do_not_exist() { 444 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 445 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 446 } 447 448 global $wpdb; 449 450 // Load used options. 451 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 452 $used_options = $option_optimizer['used_options']; 453 454 if ( empty( $used_options ) ) { 455 return new \WP_REST_Response( 456 [ 457 'draw' => intval( $_GET['draw'] ?? 0 ), 458 'recordsTotal' => 0, 459 'recordsFiltered' => 0, 460 'data' => [], 461 ], 462 200 463 ); 464 } 465 466 // Get autoloaded, non-transient option names. 467 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 468 " 469 SELECT option_name 470 FROM {$wpdb->options} 471 WHERE autoload IN ('yes', 'on', 'true', '1') 472 AND option_name NOT LIKE '%_transient_%' 473 " 474 ); 475 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 476 477 // Get used options that are not autoloaded. 478 $non_autoloaded_keys = array_diff_key( $used_options, $autoload_option_keys ); 479 480 // Search. 481 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 482 if ( '' !== $search ) { 483 $non_autoloaded_keys = array_filter( 484 $non_autoloaded_keys, 485 function ( $option_name ) use ( $search ) { 486 return stripos( $option_name, $search ) !== false; 487 }, 488 ARRAY_FILTER_USE_KEY 489 ); 490 } 491 492 if ( empty( $non_autoloaded_keys ) ) { 493 return new \WP_REST_Response( 494 [ 495 'draw' => intval( $_GET['draw'] ?? 0 ), 496 'recordsTotal' => 0, 497 'recordsFiltered' => 0, 498 'data' => [], 499 ], 500 200 501 ); 502 } 503 504 // Check which of them actually exist in the options table. 505 $option_names = array_keys( $non_autoloaded_keys ); 506 $placeholders = implode( ',', array_fill( 0, count( $option_names ), '%s' ) ); 507 508 $existing_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 509 $wpdb->prepare( 510 "SELECT option_name FROM {$wpdb->options} WHERE option_name IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 511 ...$option_names 512 ) 513 ); 514 $existing_keys = array_fill_keys( $existing_option_names, true ); 515 516 // Filter only those that do NOT exist. 517 $response_data = []; 518 foreach ( $non_autoloaded_keys as $option => $count ) { 519 if ( ! isset( $existing_keys[ $option ] ) ) { 520 $response_data[] = [ 521 'name' => $option, 522 'plugin' => $this->get_plugin_name( $option ), 523 'count' => $count, 524 'option_name' => $option, 525 ]; 526 } 527 } 528 529 $total_filtered = count( $response_data ); 530 531 // Pagination. 532 [ $offset, $limit ] = $this->get_pagination_params(); 533 534 // Sort order. 535 [ $order_column, $order_dir ] = $this->get_sort_params(); 536 537 // Sort and slice after. 538 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 539 $response_data = array_slice( $response_data, $offset, $limit ); 540 541 // Return response. 542 return new \WP_REST_Response( 543 [ 544 'draw' => intval( $_GET['draw'] ?? 0 ), 545 'recordsTotal' => $total_filtered, 546 'recordsFiltered' => $total_filtered, 547 'data' => $response_data, 548 ], 549 200 550 ); 158 551 } 159 552 … … 208 601 209 602 /** 603 * Delete multiple options. 604 * 605 * @param \WP_REST_Request $request The REST request object. 606 * 607 * @return \WP_Error|\WP_REST_Response 608 */ 609 public function delete_options( $request ) { 610 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 611 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 612 } 613 614 $option_names = $request['option_names']; 615 foreach ( $option_names as $option_name ) { 616 delete_option( $option_name ); 617 } 618 return new \WP_REST_Response( [ 'success' => true ], 200 ); 619 } 620 621 /** 622 * Set autoload status of multiple options. 623 * 624 * @param \WP_REST_Request $request The REST request object. 625 * 626 * @return \WP_Error|\WP_REST_Response 627 */ 628 public function set_autoload_options( $request ) { 629 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 630 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 631 } 632 633 $autoload = \sanitize_text_field( \wp_unslash( $request['autoload'] ) ); 634 635 if ( ! in_array( $autoload, [ 'yes', 'on', 'no', 'off','auto', 'auto-on', 'auto-off' ], true ) ) { 636 return new \WP_Error( 'invalid_autoload_value', 'Invalid autoload value', [ 'status' => 400 ] ); 637 } 638 639 $autoload_values = \wp_autoload_values_to_autoload(); 640 $bool_autoload = false; 641 if ( in_array( $autoload, $autoload_values, true ) ) { 642 $bool_autoload = true; 643 } 644 645 $option_names = $request['option_names']; 646 647 foreach ( $option_names as $option_name ) { 648 $option_value = get_option( $option_name ); 649 650 // If the option does not exist, skip it. 651 if ( false === $option_value ) { 652 continue; 653 } 654 655 delete_option( $option_name ); 656 add_option( $option_name, $option_value, '', $bool_autoload ); 657 } 658 return new \WP_REST_Response( [ 'success' => true ], 200 ); 659 } 660 661 /** 210 662 * Create an option with a false value. 211 663 * … … 221 673 return new \WP_Error( 'option_not_created', 'Option could not be created', [ 'status' => 400 ] ); 222 674 } 675 676 /** 677 * Sort response data array by given column and direction. 678 * 679 * @param array<int, array<string, mixed>> $data The data array to sort. 680 * @param string $column The column key to sort by. 681 * @param int $direction SORT_ASC or SORT_DESC. 682 * 683 * @return array<int, array<string, mixed>> The sorted array. 684 */ 685 protected function sort_response_data_by_column( array $data, string $column, int $direction ): array { 686 687 usort( 688 $data, 689 function ( $a, $b ) use ( $column, $direction ) { 690 $val_a = $a[ "raw_$column" ] ?? $a[ $column ] ?? ''; 691 $val_b = $b[ "raw_$column" ] ?? $b[ $column ] ?? ''; 692 693 if ( is_numeric( $val_a ) && is_numeric( $val_b ) ) { 694 $val_a = floatval( $val_a ); 695 $val_b = floatval( $val_b ); 696 697 return $direction === SORT_DESC ? $val_b <=> $val_a : $val_a <=> $val_b; 698 } 699 700 return $direction === SORT_DESC 701 ? strnatcasecmp( $val_b, $val_a ) 702 : strnatcasecmp( $val_a, $val_b ); 703 } 704 ); 705 706 return $data; 707 } 708 709 /** 710 * Get pagination parameters from $_GET. 711 * 712 * @return array{0: int, 1: int} { 713 * @type int $offset Pagination offset. 714 * @type int $limit Number of items per page. 715 * } 716 */ 717 protected function get_pagination_params(): array { 718 if ( 719 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 720 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 721 ) { 722 return [ 0, 25 ]; // Fallback default. 723 } 724 725 $offset = isset( $_GET['start'] ) ? intval( $_GET['start'] ) : 0; 726 $limit = isset( $_GET['length'] ) ? intval( $_GET['length'] ) : 25; 727 728 return [ $offset, $limit ]; 729 } 730 731 /** 732 * Get sort column and direction from DataTables-style request. 733 * 734 * @return array{0: string, 1: int} [ string $column, int $direction (SORT_ASC|SORT_DESC) ] 735 */ 736 public function get_sort_params(): array { 737 if ( 738 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 739 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 740 ) { 741 return [ 'name', SORT_ASC ]; // Fallback default. 742 } 743 744 if ( 745 ! isset( $_GET['order'][0]['column'], $_GET['columns'] ) 746 || ! is_array( $_GET['columns'] ) 747 || ! isset( $_GET['columns'][ $_GET['order'][0]['column'] ]['data'] ) 748 ) { 749 return [ 'name', SORT_ASC ]; // Fallback default. 750 } 751 752 $column_index = isset( $_GET['order'][0]['column'] ) ? intval( $_GET['order'][0]['column'] ) : 0; 753 $column_data = isset( $_GET['columns'][ $column_index ]['data'] ) ? \sanitize_text_field( \wp_unslash( $_GET['columns'][ $column_index ]['data'] ) ) : 'name'; 754 755 $dir = strtolower( \sanitize_text_field( \wp_unslash( $_GET['order'][0]['dir'] ?? 'asc' ) ) ); 756 $dir_flag = $dir === 'desc' ? SORT_DESC : SORT_ASC; 757 758 return [ $column_data, $dir_flag ]; 759 } 760 761 /** 762 * Get the length of a value. 763 * 764 * @param mixed $value The input value. 765 * 766 * @return string 767 */ 768 private function get_length( $value ) { 769 if ( empty( $value ) ) { 770 return '0.00'; 771 } 772 if ( is_array( $value ) || is_object( $value ) ) { 773 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- intended here. 774 $length = strlen( serialize( $value ) ); 775 } elseif ( is_string( $value ) || is_numeric( $value ) ) { 776 $length = strlen( strval( $value ) ); 777 } 778 if ( ! isset( $length ) ) { 779 return '0.00'; 780 } 781 return number_format( ( $length / 1024 ), 2 ); 782 } 783 784 /** 785 * Find plugin in known plugin prefixes list. 786 * 787 * @param string $option The option name. 788 * 789 * @return string 790 */ 791 private function get_plugin_name( $option ) { 792 return $this->map_plugin_to_options->get_plugin_name( $option ); 793 } 223 794 } -
aaa-option-optimizer/trunk/aaa-option-optimizer.php
r3287558 r3324099 8 8 * Plugin URI: https://joost.blog/plugins/aaa-option-optimizer/ 9 9 * Description: Tracks autoloaded options usage and allows the user to optimize them. 10 * Version: 1. 3.210 * Version: 1.4.0 11 11 * License: GPL-3.0+ 12 12 * Author: Joost de Valk -
aaa-option-optimizer/trunk/css/style.css
r3254065 r3324099 3 3 border-spacing: 0; 4 4 } 5 .aaa-option-optimizer-tabs .dt-layout-cell.dt-start:has(.dt-length) { 6 display: flex; 7 justify-content: flex-start; 8 gap: 20px; 9 } 10 5 11 .aaa_option_table td, .aaa_option_table th { 6 12 padding: 5px 10px 5px 5px; -
aaa-option-optimizer/trunk/js/admin-script.js
r3265010 r3324099 1 /* global jQuery, aaaOptionOptimizer, Option, DataTable, alert */ 2 1 3 /** 2 4 * JavaScript for the admin page. 3 5 * 4 * @package Emilia\OptionOptimizer6 * @package 5 7 */ 6 8 … … 8 10 * Initializes the data tables and sets up event handlers. 9 11 */ 10 jQuery( document ).ready( 11 function ($) { 12 /** 13 * Array of table selectors to initialize. 14 * 15 * @type {string[]} 16 */ 17 const tablesToInitialize = [ 18 '#unused_options_table', 19 '#used_not_autoloaded_table', 20 '#requested_do_not_exist_table', 12 jQuery( document ).ready( function () { 13 /** 14 * Array of table selectors to initialize. 15 * 16 * @type {string[]} 17 */ 18 const tablesToInitialize = [ 19 '#unused_options_table', 20 '#used_not_autoloaded_table', 21 '#requested_do_not_exist_table', 22 ]; 23 24 jQuery( '#all_options_table' ).hide(); 25 jQuery( '#aaa_get_all_options' ).on( 'click', function ( e ) { 26 e.preventDefault(); 27 jQuery( '#all_options_table' ).show(); 28 initializeDataTable( '#all_options_table' ); 29 jQuery( this ).hide(); 30 } ); 31 32 /** 33 * Generate row ID for an option name. 34 * 35 * @param {string} optionName - The option name. 36 * @return {string} The row ID. 37 */ 38 function generateRowId( optionName ) { 39 return 'option_' + optionName.replace( /\./g, '_' ); 40 } 41 42 /** 43 * Initializes the DataTable for the given selector. 44 * 45 * @param {string} selector - The table selector. 46 */ 47 function initializeDataTable( selector ) { 48 const options = { 49 pageLength: 25, 50 autoWidth: false, 51 responsive: true, 52 columns: getColumns( selector ), 53 rowId( data ) { 54 return generateRowId( data.name ); 55 }, 56 initComplete() { 57 this.api().columns( 'source:name' ).every( setupColumnFilters ); 58 }, 59 language: aaaOptionOptimizer.i18n, 60 }; 61 62 if ( selector === '#unused_options_table' ) { 63 options.ajax = { 64 url: 65 aaaOptionOptimizer.root + 66 'aaa-option-optimizer/v1/unused-options', 67 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 68 type: 'GET', 69 dataSrc: 'data', 70 }; 71 options.serverSide = true; 72 options.processing = true; 73 ( options.language = { 74 sZeroRecords: aaaOptionOptimizer.i18n.noAutoloadedButNotUsed, 75 } ), 76 ( options.initComplete = function () { 77 getBulkActionsForm( selector, [ 'autoload-off' ] ).call( 78 this 79 ); 80 this.api() 81 .columns( 'source:name' ) 82 .every( setupColumnFilters ); 83 } ); 84 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 85 } 86 87 if ( selector === '#used_not_autoloaded_table' ) { 88 options.ajax = { 89 url: 90 aaaOptionOptimizer.root + 91 'aaa-option-optimizer/v1/used-not-autoloaded-options', 92 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 93 type: 'GET', 94 dataSrc: 'data', 95 }; 96 options.serverSide = true; 97 options.processing = true; 98 options.language = { 99 sZeroRecords: aaaOptionOptimizer.i18n.noUsedButNotAutoloaded, 100 }; 101 options.initComplete = function () { 102 getBulkActionsForm( selector, [ 'autoload-on' ] ).call( this ); 103 this.api().columns( 'source:name' ).every( setupColumnFilters ); 104 }; 105 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 106 } 107 108 if ( selector === '#requested_do_not_exist_table' ) { 109 options.ajax = { 110 url: 111 aaaOptionOptimizer.root + 112 'aaa-option-optimizer/v1/options-that-do-not-exist', 113 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 114 type: 'GET', 115 dataSrc: 'data', 116 }; 117 options.serverSide = true; 118 options.processing = true; 119 } 120 121 if ( selector === '#all_options_table' ) { 122 options.ajax = { 123 url: 124 aaaOptionOptimizer.root + 125 'aaa-option-optimizer/v1/all-options', 126 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 127 type: 'GET', 128 dataSrc: 'data', 129 }; 130 options.initComplete = function () { 131 getBulkActionsForm( selector, [ 132 'autoload-on', 133 'autoload-off', 134 ] ).call( this ); 135 this.api().columns( 'source:name' ).every( setupColumnFilters ); 136 }; 137 options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. 138 } 139 140 new DataTable( selector, options ).columns.adjust().responsive.recalc(); 141 } 142 143 /** 144 * Retrieves the columns configuration based on the selector. 145 * 146 * @param {string} selector - The table selector. 147 * 148 * @return {Object[]} - The columns configuration. 149 */ 150 function getColumns( selector ) { 151 const commonColumns = [ 152 { 153 name: 'checkbox', 154 data: 'name', 155 render: ( data, type, row ) => renderCheckboxColumn( row ), 156 orderable: false, 157 searchable: false, 158 className: 'select-all', 159 }, 160 { name: 'name', data: 'name' }, 161 { name: 'source', data: 'plugin' }, 162 { name: 'size', data: 'size', searchable: false }, 163 { 164 name: 'autoload', 165 data: 'autoload', 166 className: 'autoload', 167 searchable: false, 168 orderable: false, 169 }, 170 { 171 name: 'value', 172 data: 'value', 173 render: ( data, type, row ) => renderValueColumn( row ), 174 orderable: false, 175 searchable: false, 176 className: 'actions', 177 }, 21 178 ]; 22 23 $( '#all_options_table' ).hide(); 24 $( '#aaa_get_all_options' ).on( 25 'click', 26 function (e) { 27 e.preventDefault(); 28 $( '#all_options_table' ).show(); 29 initializeDataTable( '#all_options_table' ); 30 $( this ).hide(); 31 } 32 ); 33 34 /** 35 * Initializes the DataTable for the given selector. 36 * 37 * @param {string} selector - The table selector. 38 */ 39 function initializeDataTable(selector) { 40 const options = { 41 pageLength: 25, 42 autoWidth: false, 43 responsive: true, 44 columns: getColumns( selector ), 45 initComplete: function () { 46 this.api().columns( 'source:name' ).every( setupColumnFilters ); 47 } 48 }; 49 50 if (selector === '#all_options_table') { 51 options.ajax = { 52 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/all-options', 53 headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, 54 type: 'GET', 55 dataSrc: 'data' 56 }; 57 options.rowId = 'row_id'; 58 } 59 60 options.language = aaaOptionOptimizer.i18n; 61 62 const dataTable = new DataTable( selector, options ).columns.adjust().responsive.recalc();; 63 } 64 65 /** 66 * Retrieves the columns configuration based on the selector. 67 * 68 * @param {string} selector - The table selector. 69 * 70 * @returns {Object[]} - The columns configuration. 71 */ 72 function getColumns(selector) { 73 const commonColumns = [ 74 { name: 'name' }, 75 { name: 'source' }, 76 { name: 'size', searchable: false }, 77 { name: 'autoload', className: 'autoload', searchable: false }, 78 { name: 'actions', searchable: false, orderable: false } 179 if ( selector === '#requested_do_not_exist_table' ) { 180 return [ 181 { name: 'option', data: 'name' }, 182 { name: 'source', data: 'plugin', searchable: false }, 183 { name: 'calls', data: 'count', searchable: false }, 184 { 185 name: 'option_name', 186 data: 'option_name', 187 render: ( data, type, row ) => 188 renderNonExistingOptionsColumn( row ), 189 searchable: false, 190 orderable: false, 191 className: 'actions', 192 }, 79 193 ]; 80 81 if (selector === '#requested_do_not_exist_table') { 82 return [ 83 { name: 'name' }, 84 { name: 'source', searchable: false }, 85 { name: 'calls', searchable: false }, 86 { name: 'actions', searchable: false, orderable: false } 87 ]; 88 } else if (selector === '#used_not_autoloaded_table') { 89 return [ 90 { name: 'name' }, 91 { name: 'source' }, 92 { name: 'size', searchable: false }, 93 { name: 'autoload', className: 'autoload', searchable: false }, 94 { name: 'calls', searchable: false }, 95 { name: 'actions', searchable: false, orderable: false } 96 ] 97 } else if (selector === '#all_options_table') { 98 return [ 194 } else if ( selector === '#used_not_autoloaded_table' ) { 195 return [ 196 { 197 name: 'checkbox', 198 data: 'name', 199 render: ( data, type, row ) => renderCheckboxColumn( row ), 200 orderable: false, 201 searchable: false, 202 className: 'select-all', 203 }, 99 204 { name: 'name', data: 'name' }, 100 205 { name: 'source', data: 'plugin' }, 206 { name: 'size', data: 'size', searchable: false }, 207 { 208 name: 'autoload', 209 data: 'autoload', 210 className: 'autoload', 211 searchable: false, 212 orderable: false, 213 }, 214 { name: 'calls', data: 'count', searchable: false }, 215 { 216 name: 'value', 217 data: 'value', 218 render: ( data, type, row ) => renderValueColumn( row ), 219 orderable: false, 220 searchable: false, 221 className: 'actions', 222 }, 223 ]; 224 } else if ( selector === '#all_options_table' ) { 225 return [ 226 { 227 name: 'checkbox', 228 data: 'name', 229 render: ( data, type, row ) => renderCheckboxColumn( row ), 230 orderable: false, 231 searchable: false, 232 className: 'select-all', 233 }, 234 { name: 'name', data: 'name' }, 235 { name: 'source', data: 'plugin' }, 101 236 { 102 237 name: 'size', 103 238 data: 'size', 104 239 searchable: false, 105 render: data => '<span class="num">' + data + '</span>' 106 }, 107 { name: 'autoload', data: 'autoload', className: 'autoload', searchable: false }, 240 render: ( data ) => '<span class="num">' + data + '</span>', 241 }, 242 { 243 name: 'autoload', 244 data: 'autoload', 245 className: 'autoload', 246 searchable: false, 247 }, 108 248 { 109 249 name: 'value', 110 250 data: 'value', 111 render: ( data, type, row) => renderValueColumn( row ),251 render: ( data, type, row ) => renderValueColumn( row ), 112 252 orderable: false, 113 253 searchable: false, 114 className: 'actions' 115 } 116 ]; 254 className: 'actions', 255 }, 256 ]; 257 } 258 259 return commonColumns; 260 } 261 262 /** 263 * Sets up the column filters for the DataTable. 264 */ 265 function setupColumnFilters() { 266 const column = this; 267 const select = document.createElement( 'select' ); 268 select.add( 269 new Option( aaaOptionOptimizer.i18n.filterBySource, '', true, true ) 270 ); 271 column.footer().replaceChildren( select ); 272 273 select.addEventListener( 'change', function () { 274 column.search( select.value, { exact: true } ).draw(); 275 } ); 276 277 column 278 .data() 279 .unique() 280 .sort() 281 .each( function ( d ) { 282 select.add( new Option( d ) ); 283 } ); 284 } 285 286 /** 287 * Renders the value column for a row. 288 * 289 * @param {Object} row - The row data. 290 * 291 * @return {string} - The HTML for the value column. 292 */ 293 function renderValueColumn( row ) { 294 const popoverContent = 295 '<div id="popover_' + 296 row.name + 297 '" popover class="aaa-option-optimizer-popover">' + 298 '<button class="aaa-option-optimizer-popover__close" popovertarget="popover_' + 299 row.name + 300 '" popovertargetaction="hide">X</button>' + 301 '<p><strong>Value of <code>' + 302 row.name + 303 '</code></strong></p>' + 304 '<pre>' + 305 row.value + 306 '</pre>' + 307 '</div>'; 308 309 const actions = [ 310 '<button class="button dashicon" popovertarget="popover_' + 311 row.name + 312 '"><span class="dashicons dashicons-search"></span>' + 313 aaaOptionOptimizer.i18n.showValue + 314 '</button>', 315 popoverContent, 316 row.autoload === 'no' 317 ? '<button class="button dashicon add-autoload" data-option="' + 318 row.name + 319 '"><span class="dashicons dashicons-plus"></span>' + 320 aaaOptionOptimizer.i18n.addAutoload + 321 '</button>' 322 : '<button class="button dashicon remove-autoload" data-option="' + 323 row.name + 324 '"><span class="dashicons dashicons-minus"></span>' + 325 aaaOptionOptimizer.i18n.removeAutoload + 326 '</button>', 327 '<button class="button button-delete delete-option" data-option="' + 328 row.name + 329 '"><span class="dashicons dashicons-trash"></span>' + 330 aaaOptionOptimizer.i18n.deleteOption + 331 '</button >', 332 ]; 333 334 return actions.join( '' ); 335 } 336 337 /** 338 * Renders the value column for a row. 339 * 340 * @param {Object} row - The row data. 341 * 342 * @return {string} - The HTML for the value column. 343 */ 344 function renderNonExistingOptionsColumn( row ) { 345 return ( 346 '<button class="button button-primary create-option-false" data-option="' + 347 row.name + 348 '">' + 349 aaaOptionOptimizer.i18n.createOptionFalse + 350 '</button>' 351 ); 352 } 353 354 /** 355 * Renders the checkbox column for a row. 356 * 357 * @param {Object} row - The row data. 358 * 359 * @return {string} - The HTML for the value column. 360 */ 361 function renderCheckboxColumn( row ) { 362 return ( 363 '<label for="select-option-' + 364 row.name + 365 '">' + 366 '<input type="checkbox" id="select-option-' + 367 row.name + 368 '" class="select-option" data-option="' + 369 row.name + 370 '">' + 371 '</label>' 372 ); 373 } 374 375 jQuery( '#aaa-option-reset-data' ).on( 'click', function ( e ) { 376 e.preventDefault(); 377 jQuery.ajax( { 378 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/reset', 379 method: 'POST', 380 beforeSend: ( xhr ) => 381 xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 382 success: ( 383 response // eslint-disable-line no-unused-vars 384 ) => 385 ( window.location = 386 window.location.href + '&tracking_reset=true' ), 387 error: ( response ) => 388 console.error( 'Failed to reset tracking.', response ), // eslint-disable-line no-console 389 } ); 390 } ); 391 392 /** 393 * Handles the table actions (add-autoload, remove-autoload, delete-option). 394 * 395 * @param {Event} e - The click event. 396 */ 397 function handleTableActions( e ) { 398 e.preventDefault(); 399 const button = jQuery( this ); 400 const table = button.closest( 'table' ).DataTable(); 401 const optionName = button.data( 'option' ); 402 403 const requestData = { option_name: optionName }; 404 let action = ''; 405 let route = ''; 406 407 if ( button.hasClass( 'create-option-false' ) ) { 408 action = route = 'create-option-false'; 409 } else if ( button.hasClass( 'delete-option' ) ) { 410 action = route = 'delete-option'; 411 } else { 412 action = button.hasClass( 'add-autoload' ) 413 ? 'add-autoload' 414 : 'remove-autoload'; 415 route = 'update-autoload'; 416 requestData.autoload = action === 'add-autoload' ? 'yes' : 'no'; 417 } 418 419 jQuery.ajax( { 420 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/' + route, 421 method: 'POST', 422 beforeSend: ( xhr ) => 423 xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 424 data: requestData, 425 success: ( response ) => 426 updateRowOnSuccess( response, table, optionName, action ), 427 error: ( response ) => 428 // eslint-disable-next-line no-console 429 console.error( 430 'Failed to ' + action + ' for ' + optionName + '.', 431 response 432 ), 433 } ); 434 } 435 436 /** 437 * Updates the row on successful AJAX response. 438 * 439 * @param {Object} response - The AJAX response. 440 * @param {DataTable} table - The DataTable instance. 441 * @param {string} optionName - The option name. 442 * @param {string} action - The action performed. 443 */ 444 function updateRowOnSuccess( response, table, optionName, action ) { 445 // Get the row ID for the option name. 446 const rowId = generateRowId( optionName ); 447 if ( action === 'delete-option' || action === 'create-option-false' ) { 448 table 449 .row( 'tr#' + rowId ) 450 .remove() 451 .draw( 'full-hold' ); 452 } else if ( 453 action === 'add-autoload' || 454 action === 'remove-autoload' 455 ) { 456 const autoloadStatus = action === 'add-autoload' ? 'yes' : 'no'; 457 const buttonHTML = 458 action === 'add-autoload' 459 ? '<button class="button dashicon remove-autoload" data-option="' + 460 optionName + 461 '"><span class="dashicons dashicons-minus"></span>' + 462 aaaOptionOptimizer.i18n.removeAutoload + 463 '</button>' 464 : '<button class="button dashicon add-autoload" data-option="' + 465 optionName + 466 '"><span class="dashicons dashicons-plus"></span>' + 467 aaaOptionOptimizer.i18n.addAutoload + 468 '</button>'; 469 470 jQuery( 'tr#' + rowId ) 471 .find( 'td.autoload' ) 472 .text( autoloadStatus ); 473 const oldButton = 474 'button.' + 475 ( action === 'add-autoload' ? 'add' : 'remove' ) + 476 '-autoload'; 477 jQuery( 'tr#' + rowId + ' ' + oldButton ).replaceWith( buttonHTML ); 478 } 479 } 480 481 // AJAX Event Handling (add-autoload, remove-autoload, delete-option). 482 jQuery( 'table tbody' ).on( 483 'click', 484 '.add-autoload, .remove-autoload, .delete-option, .create-option-false', 485 handleTableActions 486 ); 487 488 // Select all options. 489 jQuery( '.select-all-checkbox' ).on( 'change', function () { 490 const table = jQuery( this ).closest( 'table' ); 491 const selectValue = jQuery( this ).prop( 'checked' ); 492 const selectedOptions = table.find( 'input.select-option' ); 493 selectedOptions.prop( 'checked', selectValue ); 494 } ); 495 496 // Generates bulk actions form for DataTable. 497 function getBulkActionsForm( selector, options ) { 498 return function () { 499 const container = jQuery( this.api().table().container() ); 500 501 const form = jQuery( 502 '<form class="aaaoo-bulk-form" action="#" method="post" style="display:flex;gap:10px;"></form>' 503 ); 504 505 let selectOptions = ''; 506 507 if ( options.includes( 'autoload-on' ) ) { 508 selectOptions = 509 '<option value="autoload-on">' + 510 aaaOptionOptimizer.i18n.addAutoload + 511 '</option>'; 117 512 } 118 513 119 return commonColumns; 120 } 121 122 /** 123 * Sets up the column filters for the DataTable. 124 */ 125 function setupColumnFilters() { 126 const column = this; 127 const select = document.createElement( 'select' ); 128 select.add( new Option( aaaOptionOptimizer.i18n.filterBySource, '', true, true ) ); 129 column.footer().replaceChildren( select ); 130 131 select.addEventListener( 132 'change', 133 function () { 134 column.search( select.value, { exact: true } ).draw(); 135 } 514 if ( options.includes( 'autoload-off' ) ) { 515 selectOptions += 516 '<option value="autoload-off">' + 517 aaaOptionOptimizer.i18n.removeAutoload + 518 '</option>'; 519 } 520 521 const select = jQuery( 522 '<select class="aaaoo-bulk-select"><option value="">' + 523 aaaOptionOptimizer.i18n.bulkActions + 524 selectOptions + 525 '</option><option value="delete">' + 526 aaaOptionOptimizer.i18n.delete + 527 '</option></select>' 136 528 ); 137 529 138 column.data().unique().sort().each( 139 function (d) { 140 select.add( new Option( d ) ); 141 } 530 const button = jQuery( 531 '<button type="submit" class="button aaaoo-apply-bulk-action" data-table="' + 532 selector + 533 '">' + 534 aaaOptionOptimizer.i18n.apply + 535 '</button>' 142 536 ); 143 } 144 145 /** 146 * Renders the value column for a row. 147 * 148 * @param {Object} row - The row data. 149 * 150 * @returns {string} - The HTML for the value column. 151 */ 152 function renderValueColumn(row) { 153 const popoverContent = '<div id="popover_' + row.name + '" popover class="aaa-option-optimizer-popover">' + 154 '<button class="aaa-option-optimizer-popover__close" popovertarget="popover_' + row.name + '" popovertargetaction="hide">X</button>' + 155 '<p><strong>Value of <code>' + row.name + '</code></strong></p>' + 156 '<pre>' + row.value + '</pre>' + 157 '</div>'; 158 159 const actions = [ 160 '<button class="button dashicon" popovertarget="popover_' + row.name + '"><span class="dashicons dashicons-search"></span>' + aaaOptionOptimizer.i18n.showValue + '</button>', 161 popoverContent, 162 row.autoload === 'no' ? 163 '<button class="button dashicon add-autoload" data-option="' + row.name + '"><span class="dashicons dashicons-plus"></span>' + aaaOptionOptimizer.i18n.addAutoload + '</button>' : 164 '<button class="button dashicon remove-autoload" data-option="' + row.name + '"><span class="dashicons dashicons-minus"></span>' + aaaOptionOptimizer.i18n.removeAutoload + '</button>', 165 '<button class="button button-delete delete-option" data-option="' + row.name + '"><span class="dashicons dashicons-trash"></span>' + aaaOptionOptimizer.i18n.deleteOption + '</button >' 166 ]; 167 168 return actions.join( '' ); 169 } 170 171 $( '#aaa-option-reset-data' ).on( 172 'click', 173 function (e) { 174 e.preventDefault(); 175 $.ajax( 176 { 177 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/reset', 178 method: 'POST', 179 beforeSend: xhr => xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 180 success: response => window.location = window.location.href + '&tracking_reset=true', 181 error: response => console.error( 182 'Failed to reset tracking.', 183 response 184 ) 185 } 186 ); 537 538 form.append( select, button ); 539 540 // Add the form to the .dt-start cell 541 container.find( '.dt-layout-cell.dt-start ' ).prepend( form ); 542 543 // Move .dt-length to .dt-layout-cell.dt-end 544 // const lengthSelector = container.find(".dt-length"); // same as div.dt-length 545 // const targetEndCell = container.find(".dt-layout-cell.dt-end"); 546 // if (lengthSelector.length && targetEndCell.length) { 547 // targetEndCell.append(lengthSelector); 548 // } 549 }; 550 } 551 552 // Apply bulk action. 553 jQuery( '.aaa-option-optimizer-tabs' ).on( 554 'click', 555 '.aaaoo-apply-bulk-action', 556 function ( e ) { 557 e.preventDefault(); 558 const button = jQuery( this ); 559 const select = jQuery( button ).siblings( '.aaaoo-bulk-select' ); 560 const bulkAction = select.val(); 561 const table = jQuery( button.data( 'table' ) ); 562 const selectedOptions = table.find( 'input.select-option:checked' ); 563 564 if ( ! bulkAction ) { 565 alert( aaaOptionOptimizer.i18n.noBulkActionSelected ); // eslint-disable-line no-alert 566 return; 187 567 } 188 ); 189 190 /** 191 * Handles the table actions (add-autoload, remove-autoload, delete-option). 192 * 193 * @param {Event} e - The click event. 194 */ 195 function handleTableActions(e) { 196 e.preventDefault(); 197 const button = $( this ); 198 const table = button.closest( 'table' ).DataTable(); 199 const optionName = button.data( 'option' ); 200 201 let requestData = { option_name: optionName }; 202 let action = ''; 203 let route = ''; 204 205 if ( button.hasClass( 'create-option-false' ) ) { 206 action = route = 'create-option-false'; 207 } else if ( button.hasClass( 'delete-option' ) ) { 208 action = route = 'delete-option'; 568 569 if ( selectedOptions.length === 0 ) { 570 alert( aaaOptionOptimizer.i18n.noOptionsSelected ); // eslint-disable-line no-alert 571 return; 572 } 573 574 // For now we only have delete in bulk action. 575 576 const requestData = { 577 option_names: Array.from( selectedOptions ).map( ( option ) => 578 option.getAttribute( 'data-option' ) 579 ), 580 }; 581 582 if ( bulkAction === 'delete' ) { 583 endpoint = 'delete-options'; 209 584 } else { 210 action = button.hasClass( 'add-autoload' ) ? 'add-autoload' : 'remove-autoload';211 r oute = 'update-autoload';212 requestData.autoload = ( action === 'add-autoload' )? 'yes' : 'no';585 endpoint = 'set-autoload-options'; 586 requestData.autoload = 587 bulkAction === 'autoload-on' ? 'yes' : 'no'; 213 588 } 214 589 215 $.ajax( 216 { 217 url: aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/' + route, 218 method: 'POST', 219 beforeSend: xhr => xhr.setRequestHeader( 'X-WP-Nonce', aaaOptionOptimizer.nonce ), 220 data: requestData, 221 success: response => updateRowOnSuccess( response, table, optionName, action ), 222 error: response => console.error( 223 'Failed to ' + action + ' for ' + optionName + '.', 224 response 225 ) 226 } 227 ); 228 } 229 230 /** 231 * Updates the row on successful AJAX response. 232 * 233 * @param {Object} response - The AJAX response. 234 * @param {DataTable} table - The DataTable instance. 235 * @param {string} optionName - The option name. 236 * @param {string} action - The action performed. 237 */ 238 function updateRowOnSuccess(response, table, optionName, action) { 239 if ( action === 'delete-option' || action === 'create-option-false' ) { 240 table.row( 'tr#option_' + optionName ).remove().draw( 'full-hold' ); 241 } else if ( action === 'add-autoload' || action === 'remove-autoload' ) { 242 const autoloadStatus = action === 'add-autoload' ? 'yes' : 'no'; 243 const buttonHTML = action === 'add-autoload' ? 244 '<button class="button dashicon remove-autoload" data-option="' + optionName + '"><span class="dashicons dashicons-minus"></span>' + aaaOptionOptimizer.i18n.removeAutoload + '</button>': 245 '<button class="button dashicon add-autoload" data-option="' + optionName + '"><span class="dashicons dashicons-plus"></span>' + aaaOptionOptimizer.i18n.addAutoload + '</button>'; 246 247 $( 'tr#option_' + optionName ).find( 'td.autoload' ).text( autoloadStatus ); 248 const oldButton = 'button.' + ( action === 'add-autoload' ? 'add' : 'remove' ) + '-autoload'; 249 $( 'tr#option_' + optionName + ' ' + oldButton ).replaceWith( buttonHTML ); 250 } 251 } 252 253 // AJAX Event Handling (add-autoload, remove-autoload, delete-option). 254 $( 'table tbody' ).on( 'click', '.add-autoload, .remove-autoload, .delete-option, .create-option-false', handleTableActions ); 255 256 // Initialize data tables. 257 tablesToInitialize.forEach( 258 function (selector) { 259 if ($( selector ).length) { 260 initializeDataTable( selector ); 261 } 262 } 263 ); 264 } 265 ); 590 jQuery.ajax( { 591 url: 592 aaaOptionOptimizer.root + 593 'aaa-option-optimizer/v1/' + 594 endpoint, 595 method: 'POST', 596 beforeSend: ( xhr ) => 597 xhr.setRequestHeader( 598 'X-WP-Nonce', 599 aaaOptionOptimizer.nonce 600 ), 601 data: requestData, 602 success: () => { 603 const dt = table.DataTable(); 604 605 requestData.option_names.forEach( ( optionName ) => { 606 dt.row( 'tr#option_' + optionName ).remove(); 607 } ); 608 609 dt.draw( 'full-hold' ); 610 611 // Clear the select-all checkbox. 612 table 613 .find( '.select-all-checkbox' ) 614 .prop( 'checked', false ); 615 }, 616 error: ( response ) => { 617 // eslint-disable-next-line no-console 618 console.error( 'Failed to delete options.', response ); 619 }, 620 } ); 621 } 622 ); 623 624 // Initialize data tables. 625 tablesToInitialize.forEach( function ( selector ) { 626 if ( jQuery( selector ).length ) { 627 initializeDataTable( selector ); 628 } 629 } ); 630 } ); -
aaa-option-optimizer/trunk/known-plugins/known-plugins.json
r3254065 r3324099 36 36 "option_prefixes": ["chaty_"] 37 37 }, 38 "comment_hacks": { 39 "name": "Comment Experience by Progress Planner", 40 "option_prefixes": ["comment_hacks"] 41 }, 38 42 "complianz": { 39 43 "name": "Complianz GDPR", … … 56 60 "option_prefixes": ["elementor_"] 57 61 }, 62 "enhanced-e-commerce-for-woocommerce-store": { 63 "name": "Conversios: Google Analytics 4 (GA4), Google Ads, Microsoft Ads, and Multi-Channel Conversion Tracking", 64 "option_prefixes": ["ee_"] 65 }, 58 66 "fewer-tags": { 59 67 "name": "Fewer Tags", … … 72 80 "option_prefixes": ["yst_"] 73 81 }, 82 "google-site-kit": { 83 "name": "Site Kit by Google", 84 "option_prefixes": ["googlesitekit_"] 85 }, 74 86 "gravity-forms": { 75 87 "name": "Gravity Forms", 76 88 "option_prefixes": ["gf_","gforms_"] 77 89 }, 90 "woo-billing-with-invoicexpress": { 91 "name": "Invoicing with InvoiceXpress for WooCommerce", 92 "option_prefixes": ["hd_wc_ie_"] 93 }, 78 94 "indeed-membership-pro": { 79 95 "name": "Indeed Ultimate Membership Pro", … … 90 106 "litespeed-cache": { 91 107 "name": "LiteSpeed Cache", 92 "option_prefixes": ["litespeed-", "_litespeed_" ]108 "option_prefixes": ["litespeed-", "_litespeed_", "litespeed."] 93 109 }, 94 110 "loginizer": { … … 96 112 "option_prefixes": ["loginizer_"] 97 113 }, 114 "multibanco-ifthen-software-gateway-for-woocommerce": { 115 "name": "Multibanco, MB WAY, Credit card, Apple Pay, Google Pay, Payshop, Cofidis Pay, and PIX (ifthenpay) for WooCommerce", 116 "option_prefixes": ["cofidispay_ifthen_", "creditcard_ifthen_", "gateway_ifthen_", "mbway_ifthen_", "multibanco_ifthen_", "payshop_"] 117 }, 98 118 "perfmatters": { 99 119 "name": "Perfmatters", … … 112 132 "option_prefixes": ["porto_"] 113 133 }, 134 "product-assembly-cost": { 135 "name": "Product Assembly / Gift Wrap / ... Cost for WooCommerce", 136 "option_prefixes": ["product_assembly_"] 137 }, 138 "progress-planner": { 139 "name": "Progress Planner", 140 "option_prefixes": ["progress_planner_"] 141 }, 114 142 "really-simple-ssl": { 115 143 "name": "Really Simple SSL", … … 132 160 "option_prefixes": ["rank_math_"] 133 161 }, 162 "shop-as-client": { 163 "name": "Shop as Client for WooCommerce", 164 "option_prefixes": ["shop_as_client"] 165 }, 166 "simple-woo-checkout-blocks-cf": { 167 "name": "Simple Checkout Fields Manager for WooCommerce", 168 "option_prefixes": ["swcbcf_"] 169 }, 170 "simple-woocommerce-order-approval": { 171 "name": "Simple WooCommerce Order Approval", 172 "option_prefixes": ["swoa_"] 173 }, 134 174 "updraft": { 135 175 "name": "Updraft Plus", … … 140 180 "option_prefixes": ["wpseo", "yoast_"] 141 181 }, 182 "feed-kuantokusta-for-woocommerce": { 183 "name": "Feed KuantoKusta for WooCommerce", 184 "option_prefixes": ["wc_feed_kuantokusta_"] 185 }, 186 "woo-dpd-pickup": { 187 "name": "DPD / Geopost Pickup and Lockers network for WooCommerce", 188 "option_prefixes": ["woo_dpd_pickup"] 189 }, 190 "woo-dpd-portugal": { 191 "name": "DPD Portugal for WooCommerce", 192 "option_prefixes": ["woo_dpd_portugal"] 193 }, 142 194 "woocommerce": { 143 195 "name": "WooCommerce", 144 196 "option_prefixes": ["wc_", "_transient__woocommerce_", "woocommerce_"] 197 }, 198 "woocommerce-max-quantity": { 199 "name": "Maximum Quantity for WooCommerce Shops", 200 "option_prefixes": ["isa_woocommerce_"] 145 201 }, 146 202 "wordfence": { -
aaa-option-optimizer/trunk/readme.txt
r3287558 r3324099 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1. 3.27 Stable tag: 1.4.0 8 8 License: GPL3+ 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.en.html … … 54 54 55 55 == Changelog == 56 57 = 1.4.0 = 58 59 * Performance improvements. 60 * Added bulk-actions to allow optimizing & deleting options in bulk. 61 * Added more known plugins. 56 62 57 63 = 1.3.2 = -
aaa-option-optimizer/trunk/src/class-admin-page.php
r3254065 r3324099 14 14 15 15 /** 16 * The map plugin to options class.17 *18 * @var Map_Plugin_To_Options19 */20 private $map_plugin_to_options;21 22 /**23 16 * Register hooks. 24 17 * … … 26 19 */ 27 20 public function register_hooks() { 28 $this->map_plugin_to_options = new Map_Plugin_To_Options();29 30 21 // Register a link to the settings page on the plugins overview page. 31 22 \add_filter( 'plugin_action_links', [ $this, 'filter_plugin_actions' ], 10, 2 ); … … 71 62 'manage_options', 72 63 'aaa-option-optimizer', 73 [ $this, 'render_admin_page ' ]64 [ $this, 'render_admin_page_ajax' ] 74 65 ); 75 66 } … … 124 115 'nonce' => wp_create_nonce( 'wp_rest' ), 125 116 'i18n' => [ 126 'filterBySource' => esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 127 'showValue' => esc_html__( 'Show', 'aaa-option-optimizer' ), 128 'addAutoload' => esc_html__( 'Add autoload', 'aaa-option-optimizer' ), 129 'removeAutoload' => esc_html__( 'Remove autoload', 'aaa-option-optimizer' ), 130 'deleteOption' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 131 132 'search' => esc_html__( 'Search:', 'aaa-option-optimizer' ), 133 'entries' => [ 117 'filterBySource' => esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 118 'showValue' => esc_html__( 'Show', 'aaa-option-optimizer' ), 119 'addAutoload' => esc_html__( 'Add autoload', 'aaa-option-optimizer' ), 120 'removeAutoload' => esc_html__( 'Remove autoload', 'aaa-option-optimizer' ), 121 'deleteOption' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 122 'createOptionFalse' => esc_html__( 'Create option with value false', 'aaa-option-optimizer' ), 123 'noAutoloadedButNotUsed' => esc_html__( 'All autoloaded options are in use.', 'aaa-option-optimizer' ), 124 'noUsedButNotAutoloaded' => esc_html__( 'All options that are used are autoloaded.', 'aaa-option-optimizer' ), 125 'noOptionsSelected' => esc_html__( 'No options selected.', 'aaa-option-optimizer' ), 126 'bulkActions' => esc_html__( 'Bulk actions', 'aaa-option-optimizer' ), 127 'noBulkActionSelected' => esc_html__( 'No action selected.', 'aaa-option-optimizer' ), 128 'delete' => esc_html__( 'Delete', 'aaa-option-optimizer' ), 129 'apply' => esc_html__( 'Apply', 'aaa-option-optimizer' ), 130 131 'search' => esc_html__( 'Search:', 'aaa-option-optimizer' ), 132 'entries' => [ 134 133 '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ), 135 134 '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ), 136 135 ], 137 'sInfo' => sprintf(136 'sInfo' => sprintf( 138 137 // translators: %1$s is the start, %2$s is the end, %3$s is the total, %4$s is the entries. 139 138 esc_html__( 'Showing %1$s to %2$s of %3$s %4$s', 'aaa-option-optimizer' ), … … 143 142 '_ENTRIES-TOTAL_' 144 143 ), 145 'sInfoEmpty' => esc_html__( 'Showing 0 to 0 of 0 entries', 'aaa-option-optimizer' ),146 'sInfoFiltered' => sprintf(144 'sInfoEmpty' => esc_html__( 'Showing 0 to 0 of 0 entries', 'aaa-option-optimizer' ), 145 'sInfoFiltered' => sprintf( 147 146 // translators: %1$s is the max, %2$s is the entries-max. 148 147 esc_html__( '(filtered from %1$s total %2$s)', 'aaa-option-optimizer' ), … … 150 149 '_ENTRIES-MAX_' 151 150 ), 152 'sZeroRecords' => esc_html__( 'No matching records found', 'aaa-option-optimizer' ),153 'oAria' => [151 'sZeroRecords' => esc_html__( 'No matching records found', 'aaa-option-optimizer' ), 152 'oAria' => [ 154 153 'orderable' => esc_html__( ': Activate to sort', 'aaa-option-optimizer' ), 155 154 'orderableReverse' => esc_html__( ': Activate to invert sorting', 'aaa-option-optimizer' ), … … 198 197 echo '<th class="source">' . esc_html__( 'Source', 'aaa-option-optimizer' ) . '</th>'; 199 198 break; 199 case 'select-all': 200 echo '<th class="select-all"><input type="checkbox" class="select-all-checkbox" /></th>'; 201 break; 200 202 } 201 203 } … … 205 207 206 208 /** 207 * Get the length of a value.208 *209 * @param mixed $value The input value.210 *211 * @return string212 */213 private function get_length( $value ) {214 if ( empty( $value ) ) {215 return '0.00';216 }217 if ( is_array( $value ) || is_object( $value ) ) {218 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- intended here.219 $length = strlen( serialize( $value ) );220 } elseif ( is_string( $value ) || is_numeric( $value ) ) {221 $length = strlen( strval( $value ) );222 }223 if ( ! isset( $length ) ) {224 return '0.00';225 }226 return number_format( ( $length / 1024 ), 2 );227 }228 229 /**230 209 * Renders the admin page. 231 210 * 232 211 * @return void 233 212 */ 234 public function render_admin_page () {213 public function render_admin_page_ajax() { 235 214 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 236 $all_options = wp_load_alloptions();237 // Filter out transients.238 $autoload_options = array_filter(239 $all_options,240 function ( $value, $key ) {241 return strpos( $key, '_transient_' ) === false;242 },243 ARRAY_FILTER_USE_BOTH244 );245 246 $unused_options = [];247 $non_autoloaded_options = [];248 249 // Get the autoloaded options that aren't used.250 foreach ( $autoload_options as $option => $value ) {251 if ( isset( $option_optimizer['used_options'][ $option ] ) ) {252 continue;253 }254 $unused_options[ $option ] = $value;255 }256 257 // Determine the options that _are_ used, but not auto-loaded.258 foreach ( $option_optimizer['used_options'] as $option => $count ) {259 if ( isset( $autoload_options[ $option ] ) ) {260 continue;261 }262 $non_autoloaded_options[ $option ] = $count;263 }264 265 // Some of the options that are used but not auto-loaded, may not exist.266 if ( ! empty( $non_autoloaded_options ) ) {267 $options_that_do_not_exist = [];268 $non_autoloaded_options_full = [];269 foreach ( $non_autoloaded_options as $option => $count ) {270 $value = get_option( $option, 'aaa-no-return-value' );271 if ( $value === 'aaa-no-return-value' ) {272 $options_that_do_not_exist[ $option ] = $count;273 continue;274 }275 $non_autoloaded_options_full[ $option ] = [276 'count' => $count,277 'value' => $value,278 ];279 }280 }281 215 282 216 // Start HTML output. … … 322 256 <?php 323 257 echo '<h2 id="unused-autoloaded">' . esc_html__( 'Unused, but autoloaded', 'aaa-option-optimizer' ) . '</h2>'; 324 if ( ! empty( $unused_options ) ) { 325 echo '<p>' . esc_html__( 'The following options are autoloaded on each pageload, but AAA Option Optimizer has not been able to detect them being used.', 'aaa-option-optimizer' ); 326 echo '<table style="width:100%" id="unused_options_table" class="aaa_option_table">'; 327 $this->table_section( 'thead', [ 'option', 'source', 'size', 'autoload', 'actions' ] ); 328 echo '<tbody>'; 329 foreach ( $unused_options as $option => $value ) { 330 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '"><td>' . esc_html( $option ) . '</td>'; 331 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 332 echo '<td><span class="num">' . esc_html( $this->get_length( $value ) ) . '</span></td>'; 333 echo '<td class="autoload">yes</td>'; 334 echo '<td class="actions">'; 335 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped in get_value_button. 336 echo $this->get_value_button( $option, $value ); 337 echo '<button class="button button-primary remove-autoload" data-option="' . esc_attr( $option ) . '"><span class="dashicons dashicons-minus"></span> ' . esc_html__( 'Remove autoload', 'aaa-option-optimizer' ) . '</button> '; 338 echo ' <button class="button button-delete delete-option" data-option="' . esc_attr( $option ) . '"><span class="dashicons dashicons-trash"></span> ' . esc_html__( 'Delete', 'aaa-option-optimizer' ) . '</button>'; 339 echo '</td></tr>'; 340 } 341 echo '</tbody>'; 342 $this->table_section( 'tfoot', [ 'option', 'source', 'size', 'autoload', 'actions' ] ); 343 echo '</table>'; 344 } else { 345 echo '<p>' . esc_html__( 'All autoloaded options are in use.', 'aaa-option-optimizer' ) . '</p>'; 346 } 347 ?> 258 echo '<p>' . esc_html__( 'The following options are autoloaded on each pageload, but AAA Option Optimizer has not been able to detect them being used.', 'aaa-option-optimizer' ); 259 echo '<table style="width:100%" id="unused_options_table" class="aaa_option_table">'; 260 $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); 261 ?> 262 <tbody> 263 <tr> 264 <td class="select-all"></td> 265 <td></td> 266 <td></td> 267 <td></td> 268 <td></td> 269 <td class="actions"></td> 270 </tr> 271 </tbody> 272 <?php 273 $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); 274 ?> 275 </table> 348 276 </div> 349 277 <input class="input" name="tabs" type="radio" id="tab-2"/> … … 352 280 <?php 353 281 // Render differences. 354 if ( ! empty( $non_autoloaded_options ) ) {355 282 echo '<h2 id="used-not-autoloaded">' . esc_html__( 'Used, but not autoloaded options', 'aaa-option-optimizer' ) . '</h2>'; 356 283 echo '<p>' . esc_html__( 'The following options are *not* autoloaded on each pageload, but AAA Option Optimizer has detected that they are being used. If one of the options below has been called a lot and is not very big, you might consider adding autoload to that option.', 'aaa-option-optimizer' ); 357 284 echo '<table style="width:100%;" id="used_not_autoloaded_table" class="aaa_option_table">'; 358 $this->table_section( 'thead', [ 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 359 echo '<tbody>'; 360 foreach ( $non_autoloaded_options_full as $option => $arr ) { 361 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '">'; 362 echo '<td>' . esc_html( $option ) . '</td>'; 363 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 364 echo '<td><span class="num">' . esc_html( $this->get_length( $arr['value'] ) ) . '</span></td>'; 365 echo '<td class="autoload">no</td>'; 366 echo '<td>' . esc_html( $arr['count'] ) . '</td>'; 367 echo '<td class="actions">'; 368 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped in get_value_button. 369 echo $this->get_value_button( $option, $arr['value'] ); 370 echo '<button class="button button-primary add-autoload" data-option="' . esc_attr( $option ) . '">' . esc_html__( 'Add autoload', 'aaa-option-optimizer' ) . '</button> '; 371 echo '</td></tr>'; 372 } 373 echo '</tbody>'; 374 $this->table_section( 'tfoot', [ 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 375 echo '</table>'; 376 } else { 377 echo '<p>' . esc_html__( 'All options that are used are autoloaded.', 'aaa-option-optimizer' ) . '</p>'; 378 } 379 ?> 285 $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); 286 ?> 287 <tbody> 288 <tr> 289 <td class="select-all"></td> 290 <td></td> 291 <td></td> 292 <td></td> 293 <td></td> 294 <td></td> 295 <td class="actions"></td> 296 </tr> 297 </tbody> 298 <?php $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'calls', 'actions' ] ); ?> 299 </table> 380 300 </div> 381 301 <input class="input" name="tabs" type="radio" id="tab-3"/> … … 383 303 <div class="panel"> 384 304 <?php 385 if ( ! empty( $options_that_do_not_exist ) ) { 386 echo '<h2 id="requested-do-not-exist">' . esc_html__( 'Requested options that do not exist', 'aaa-option-optimizer' ) . '</h2>'; 387 echo '<p>' . esc_html__( 'The following options are requested sometimes, but AAA Option Optimizer has detected that they do not exist. If one of the options below has been called a lot, it might help to create it with a value of false.', 'aaa-option-optimizer' ); 388 echo '<table width="100%" id="requested_do_not_exist_table" class="aaa_option_table">'; 389 $this->table_section( 'thead', [ 'option', 'source', 'calls', 'actions' ] ); 390 echo '<tbody>'; 391 foreach ( $options_that_do_not_exist as $option => $count ) { 392 echo '<tr id="option_' . esc_attr( str_replace( ':', '', str_replace( '.', '', $option ) ) ) . '">'; 393 echo '<td>' . esc_html( $option ) . '</td>'; 394 echo '<td>' . esc_html( $this->get_plugin_name( $option ) ) . '</td>'; 395 echo '<td>' . esc_html( $count ) . '</td>'; 396 echo '<td class="actions"><button class="button button-primary create-option-false" data-option="' . esc_attr( $option ) . '">' . esc_html__( 'Create option with value false', 'aaa-option-optimizer' ) . '</button> '; 397 } 398 echo '</tbody>'; 399 $this->table_section( 'tfoot', [ 'option', 'source', 'calls', 'actions' ] ); 400 echo '</table>'; 401 } 402 ?> 305 echo '<h2 id="requested-do-not-exist">' . esc_html__( 'Requested options that do not exist', 'aaa-option-optimizer' ) . '</h2>'; 306 echo '<p>' . esc_html__( 'The following options are requested sometimes, but AAA Option Optimizer has detected that they do not exist. If one of the options below has been called a lot, it might help to create it with a value of false.', 'aaa-option-optimizer' ); 307 echo '<table width="100%" id="requested_do_not_exist_table" class="aaa_option_table">'; 308 $this->table_section( 'thead', [ 'option', 'source', 'calls', 'actions' ] ); 309 ?> 310 <tbody> 311 <tr> 312 <td></td> 313 <td></td> 314 <td></td> 315 <td class="actions"></td> 316 </tr> 317 </tbody> 318 <?php 319 $this->table_section( 'tfoot', [ 'option', 'source', 'calls', 'actions' ] ); 320 ?> 321 </table> 403 322 </div> 404 323 <input class="input" name="tabs" type="radio" id="tab-4"/> … … 408 327 <button id="aaa_get_all_options" class="button button-primary"><?php esc_html_e( 'Get all options', 'aaa-option-optimizer' ); ?></button> 409 328 <table class="aaa_option_table" id="all_options_table" style="display:none;"> 410 <?php $this->table_section( 'thead', [ ' option', 'source', 'size', 'autoload', 'actions' ] ); ?>329 <?php $this->table_section( 'thead', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); ?> 411 330 <tbody> 412 331 <tr> 332 <td class="select-all"></td> 413 333 <td></td> 414 334 <td></td> … … 416 336 <td></td> 417 337 <td class="actions"></td> 418 </tr> 338 </tr> 419 339 </tbody> 420 <?php $this->table_section( 'tfoot', [ ' option', 'source', 'size', 'autoload', 'actions' ] ); ?>340 <?php $this->table_section( 'tfoot', [ 'select-all', 'option', 'source', 'size', 'autoload', 'actions' ] ); ?> 421 341 </table> 422 342 </div> … … 424 344 <?php 425 345 } 426 427 /**428 * Get html to show a popover.429 *430 * @param string $name The name of the option, used in the id of the popover.431 * @param mixed $value The value to show.432 *433 * @return string434 */435 private function get_value_button( string $name, $value ): string {436 $string = is_string( $value ) ? $value : wp_json_encode( $value );437 $id = 'aaa-option-optimizer-' . esc_attr( $name );438 return '439 <button class="button" popovertarget="' . $id . '"><span class="dashicons dashicons-search"></span> ' . esc_html__( 'Show', 'aaa-option-optimizer' ) . '</button>440 <div id="' . $id . '" popover class="aaa-option-optimizer-popover">441 <button class="aaa-option-optimizer-popover__close" popovertarget="' . $id . '" popovertargetaction="hide">X</button>' .442 // translators: %s is the name of the option.443 '<p><strong>' . sprintf( esc_html__( 'Value of %s', 'aaa-option-optimizer' ), '<code>' . esc_html( $name ) . '</code>' ) . '</strong></p>444 <pre>' . htmlentities( $string, ENT_QUOTES | ENT_SUBSTITUTE ) . '</pre>445 </div>';446 }447 448 /**449 * Find plugin in known plugin prefixes list.450 *451 * @param string $option The option name.452 *453 * @return string454 */455 private function get_plugin_name( $option ) {456 return $this->map_plugin_to_options->get_plugin_name( $option );457 }458 346 } -
aaa-option-optimizer/trunk/src/class-rest.php
r3254065 r3324099 122 122 ] 123 123 ); 124 125 \register_rest_route( 126 'aaa-option-optimizer/v1', 127 '/unused-options', 128 [ 129 'methods' => 'GET', 130 'callback' => [ $this, 'get_unused_options' ], 131 'permission_callback' => function () { 132 return current_user_can( 'manage_options' ); 133 }, 134 ] 135 ); 136 137 \register_rest_route( 138 'aaa-option-optimizer/v1', 139 '/used-not-autoloaded-options', 140 [ 141 'methods' => 'GET', 142 'callback' => [ $this, 'get_used_not_autoloaded_options' ], 143 'permission_callback' => function () { 144 return current_user_can( 'manage_options' ); 145 }, 146 ] 147 ); 148 149 \register_rest_route( 150 'aaa-option-optimizer/v1', 151 '/options-that-do-not-exist', 152 [ 153 'methods' => 'GET', 154 'callback' => [ $this, 'get_options_that_do_not_exist' ], 155 'permission_callback' => function () { 156 return current_user_can( 'manage_options' ); 157 }, 158 ] 159 ); 160 161 \register_rest_route( 162 'aaa-option-optimizer/v1', 163 '/delete-options', 164 [ 165 'methods' => 'POST', 166 'callback' => [ $this, 'delete_options' ], 167 'permission_callback' => function () { 168 return current_user_can( 'manage_options' ); 169 }, 170 ] 171 ); 172 173 \register_rest_route( 174 'aaa-option-optimizer/v1', 175 '/set-autoload-options', 176 [ 177 'methods' => 'POST', 178 'callback' => [ $this, 'set_autoload_options' ], 179 'permission_callback' => function () { 180 return current_user_can( 'manage_options' ); 181 }, 182 ] 183 ); 124 184 } 125 185 … … 148 208 $output[] = [ 149 209 'name' => $option->option_name, 150 'plugin' => $this-> map_plugin_to_options->get_plugin_name( $option->option_name ),210 'plugin' => $this->get_plugin_name( $option->option_name ), 151 211 'value' => htmlentities( $option->option_value, ENT_QUOTES | ENT_SUBSTITUTE ), 152 'size' => number_format( strlen( $option->option_value ) / 1024, 2 ), 212 'size' => $this->get_length( $option->option_value ), 213 'raw_size' => strlen( $option->option_value ), 153 214 'autoload' => $option->autoload, 154 'row_id' => 'option_' . $option->option_name,155 215 ]; 156 216 } 157 217 return new \WP_REST_Response( [ 'data' => $output ], 200 ); 218 } 219 220 /** 221 * Get unused options. 222 * 223 * @return \WP_Error|\WP_REST_Response 224 */ 225 public function get_unused_options() { 226 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 227 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 228 } 229 230 global $wpdb; 231 232 // Load used options from option_optimizer. 233 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 234 $used_options = $option_optimizer['used_options']; 235 236 $query = " 237 SELECT option_name 238 FROM {$wpdb->options} 239 WHERE autoload IN ( '" . implode( "', '", esc_sql( \wp_autoload_values_to_autoload() ) ) . "' ) 240 AND option_name NOT LIKE '%_transient_%' 241 "; 242 243 // Search. 244 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 245 if ( '' !== $search ) { 246 $query .= " AND option_name LIKE '%" . esc_sql( $search ) . "%'"; 247 } 248 249 // Get autoloaded, non-transient option names. 250 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 251 $query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 252 ); 253 254 // Find unused autoloaded option names. 255 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 256 $unused_keys = array_diff_key( $autoload_option_keys, $used_options ); 257 $total_unused = count( $unused_keys ); 258 259 // Sort order. 260 [ $order_column, $order_dir ] = $this->get_sort_params(); 261 262 // Pagination. 263 [ $offset, $limit ] = $this->get_pagination_params(); 264 $paged_option_names = array_keys( $unused_keys ); 265 266 // Optimize by slicing early for default sort, since we can sort $autoloaded_option_names in advance. 267 if ( 'name' === $order_column && SORT_ASC === $order_dir ) { 268 $paged_option_names = array_slice( $paged_option_names, $offset, $limit ); 269 } 270 271 $response_data = []; 272 273 if ( ! empty( $paged_option_names ) ) { 274 $placeholders = implode( ',', array_fill( 0, count( $paged_option_names ), '%s' ) ); 275 $value_query = " 276 SELECT option_name, option_value 277 FROM {$wpdb->options} 278 WHERE option_name IN ( {$placeholders} ) 279 "; 280 281 $results = $wpdb->get_results( $wpdb->prepare( $value_query, ...$paged_option_names ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 282 283 // Format output. 284 foreach ( $results as $row ) { 285 $response_data[] = [ 286 'name' => $row->option_name, 287 'plugin' => $this->get_plugin_name( $row->option_name ), 288 'value' => htmlentities( $row->option_value, ENT_QUOTES | ENT_SUBSTITUTE ), 289 'size' => $this->get_length( $row->option_value ), 290 'raw_size' => strlen( $row->option_value ), 291 'autoload' => 'yes', 292 ]; 293 } 294 295 // Sorting, skip if "name" column is sorted in ascending order since that is the default. 296 if ( ! ( 'name' === $order_column && SORT_ASC === $order_dir ) ) { 297 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 298 299 // Now we can slice after sort. 300 $response_data = array_slice( $response_data, $offset, $limit ); 301 } 302 } 303 304 // Return response. 305 return new \WP_REST_Response( 306 [ 307 'draw' => intval( $_GET['draw'] ?? 0 ), 308 'recordsTotal' => $total_unused, 309 'recordsFiltered' => $total_unused, 310 'data' => $response_data, 311 ], 312 200 313 ); 314 } 315 316 /** 317 * Get used, but not autoloaded options. 318 * 319 * @return \WP_Error|\WP_REST_Response 320 */ 321 public function get_used_not_autoloaded_options() { 322 if ( 323 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 324 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 325 ) { 326 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 327 } 328 329 global $wpdb; 330 331 // Load used options from option_optimizer. 332 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 333 $used_options = $option_optimizer['used_options']; 334 335 if ( empty( $used_options ) ) { 336 return new \WP_REST_Response( 337 [ 338 'draw' => intval( $_GET['draw'] ?? 0 ), 339 'recordsTotal' => 0, 340 'recordsFiltered' => 0, 341 'data' => [], 342 ], 343 200 344 ); 345 } 346 347 // Get all autoloaded, non-transient option names. 348 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 349 " 350 SELECT option_name 351 FROM {$wpdb->options} 352 WHERE autoload IN ( '" . implode( "', '", esc_sql( wp_autoload_values_to_autoload() ) ) . "' ) 353 AND option_name NOT LIKE '%_transient_%' 354 ORDER BY option_name ASC 355 " 356 ); 357 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 358 359 // Find used options that are not autoloaded. 360 $non_autoloaded_used_keys = array_diff_key( $used_options, $autoload_option_keys ); 361 362 // Search. 363 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 364 if ( '' !== $search ) { 365 $non_autoloaded_used_keys = array_filter( 366 $non_autoloaded_used_keys, 367 function ( $option_name ) use ( $search ) { 368 return false !== stripos( $option_name, $search ); 369 }, 370 ARRAY_FILTER_USE_KEY 371 ); 372 } 373 374 if ( empty( $non_autoloaded_used_keys ) ) { 375 return new \WP_REST_Response( 376 [ 377 'draw' => intval( $_GET['draw'] ?? 0 ), 378 'recordsTotal' => 0, 379 'recordsFiltered' => 0, 380 'data' => [], 381 ], 382 200 383 ); 384 } 385 386 // Sort order. 387 [ $order_column, $order_dir ] = $this->get_sort_params(); 388 389 // Pagination. 390 [ $offset, $limit ] = $this->get_pagination_params(); 391 392 // We can't slice early here, because we can't sort $used_options in advance. 393 $paged_option_names = array_keys( $non_autoloaded_used_keys ); 394 395 $response_data = []; 396 397 // $paged_option_names is not empty. 398 // Fetch values directly from DB without using get_option(). 399 $placeholders = implode( ',', array_fill( 0, count( $paged_option_names ), '%s' ) ); 400 $sql = " 401 SELECT option_name, option_value 402 FROM {$wpdb->options} 403 WHERE option_name IN ( {$placeholders} )"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 404 405 $results = $wpdb->get_results( $wpdb->prepare( $sql, ...$paged_option_names ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 406 407 foreach ( $results as $row ) { 408 $response_data[] = [ 409 'name' => $row->option_name, 410 'plugin' => $this->get_plugin_name( $row->option_name ), 411 'value' => htmlentities( maybe_serialize( $row->option_value ), ENT_QUOTES | ENT_SUBSTITUTE ), 412 'size' => $this->get_length( $row->option_value ), 413 'raw_size' => strlen( $row->option_value ), 414 'autoload' => 'no', 415 'count' => $used_options[ $row->option_name ] ?? 0, 416 ]; 417 } 418 419 $total_filtered = count( $response_data ); 420 421 // Sort and slice after. 422 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 423 $response_data = array_slice( $response_data, $offset, $limit ); 424 425 // Return response. 426 return new \WP_REST_Response( 427 [ 428 'draw' => intval( $_GET['draw'] ?? 0 ), 429 'recordsTotal' => $total_filtered, 430 'recordsFiltered' => $total_filtered, 431 'data' => $response_data, 432 ], 433 200 434 ); 435 } 436 437 /** 438 * Get options that do not exist. 439 * Some of the options that are used but not auto-loaded, may not exist. 440 * 441 * @return \WP_Error|\WP_REST_Response 442 */ 443 public function get_options_that_do_not_exist() { 444 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 445 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 446 } 447 448 global $wpdb; 449 450 // Load used options. 451 $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); 452 $used_options = $option_optimizer['used_options']; 453 454 if ( empty( $used_options ) ) { 455 return new \WP_REST_Response( 456 [ 457 'draw' => intval( $_GET['draw'] ?? 0 ), 458 'recordsTotal' => 0, 459 'recordsFiltered' => 0, 460 'data' => [], 461 ], 462 200 463 ); 464 } 465 466 // Get autoloaded, non-transient option names. 467 $autoloaded_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 468 " 469 SELECT option_name 470 FROM {$wpdb->options} 471 WHERE autoload IN ('yes', 'on', 'true', '1') 472 AND option_name NOT LIKE '%_transient_%' 473 " 474 ); 475 $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); 476 477 // Get used options that are not autoloaded. 478 $non_autoloaded_keys = array_diff_key( $used_options, $autoload_option_keys ); 479 480 // Search. 481 $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; 482 if ( '' !== $search ) { 483 $non_autoloaded_keys = array_filter( 484 $non_autoloaded_keys, 485 function ( $option_name ) use ( $search ) { 486 return stripos( $option_name, $search ) !== false; 487 }, 488 ARRAY_FILTER_USE_KEY 489 ); 490 } 491 492 if ( empty( $non_autoloaded_keys ) ) { 493 return new \WP_REST_Response( 494 [ 495 'draw' => intval( $_GET['draw'] ?? 0 ), 496 'recordsTotal' => 0, 497 'recordsFiltered' => 0, 498 'data' => [], 499 ], 500 200 501 ); 502 } 503 504 // Check which of them actually exist in the options table. 505 $option_names = array_keys( $non_autoloaded_keys ); 506 $placeholders = implode( ',', array_fill( 0, count( $option_names ), '%s' ) ); 507 508 $existing_option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 509 $wpdb->prepare( 510 "SELECT option_name FROM {$wpdb->options} WHERE option_name IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 511 ...$option_names 512 ) 513 ); 514 $existing_keys = array_fill_keys( $existing_option_names, true ); 515 516 // Filter only those that do NOT exist. 517 $response_data = []; 518 foreach ( $non_autoloaded_keys as $option => $count ) { 519 if ( ! isset( $existing_keys[ $option ] ) ) { 520 $response_data[] = [ 521 'name' => $option, 522 'plugin' => $this->get_plugin_name( $option ), 523 'count' => $count, 524 'option_name' => $option, 525 ]; 526 } 527 } 528 529 $total_filtered = count( $response_data ); 530 531 // Pagination. 532 [ $offset, $limit ] = $this->get_pagination_params(); 533 534 // Sort order. 535 [ $order_column, $order_dir ] = $this->get_sort_params(); 536 537 // Sort and slice after. 538 $response_data = $this->sort_response_data_by_column( $response_data, $order_column, $order_dir ); 539 $response_data = array_slice( $response_data, $offset, $limit ); 540 541 // Return response. 542 return new \WP_REST_Response( 543 [ 544 'draw' => intval( $_GET['draw'] ?? 0 ), 545 'recordsTotal' => $total_filtered, 546 'recordsFiltered' => $total_filtered, 547 'data' => $response_data, 548 ], 549 200 550 ); 158 551 } 159 552 … … 208 601 209 602 /** 603 * Delete multiple options. 604 * 605 * @param \WP_REST_Request $request The REST request object. 606 * 607 * @return \WP_Error|\WP_REST_Response 608 */ 609 public function delete_options( $request ) { 610 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 611 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 612 } 613 614 $option_names = $request['option_names']; 615 foreach ( $option_names as $option_name ) { 616 delete_option( $option_name ); 617 } 618 return new \WP_REST_Response( [ 'success' => true ], 200 ); 619 } 620 621 /** 622 * Set autoload status of multiple options. 623 * 624 * @param \WP_REST_Request $request The REST request object. 625 * 626 * @return \WP_Error|\WP_REST_Response 627 */ 628 public function set_autoload_options( $request ) { 629 if ( ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) ) { 630 return new \WP_REST_Response( [ 'error' => 'Invalid nonce' ], 403 ); 631 } 632 633 $autoload = \sanitize_text_field( \wp_unslash( $request['autoload'] ) ); 634 635 if ( ! in_array( $autoload, [ 'yes', 'on', 'no', 'off','auto', 'auto-on', 'auto-off' ], true ) ) { 636 return new \WP_Error( 'invalid_autoload_value', 'Invalid autoload value', [ 'status' => 400 ] ); 637 } 638 639 $autoload_values = \wp_autoload_values_to_autoload(); 640 $bool_autoload = false; 641 if ( in_array( $autoload, $autoload_values, true ) ) { 642 $bool_autoload = true; 643 } 644 645 $option_names = $request['option_names']; 646 647 foreach ( $option_names as $option_name ) { 648 $option_value = get_option( $option_name ); 649 650 // If the option does not exist, skip it. 651 if ( false === $option_value ) { 652 continue; 653 } 654 655 delete_option( $option_name ); 656 add_option( $option_name, $option_value, '', $bool_autoload ); 657 } 658 return new \WP_REST_Response( [ 'success' => true ], 200 ); 659 } 660 661 /** 210 662 * Create an option with a false value. 211 663 * … … 221 673 return new \WP_Error( 'option_not_created', 'Option could not be created', [ 'status' => 400 ] ); 222 674 } 675 676 /** 677 * Sort response data array by given column and direction. 678 * 679 * @param array<int, array<string, mixed>> $data The data array to sort. 680 * @param string $column The column key to sort by. 681 * @param int $direction SORT_ASC or SORT_DESC. 682 * 683 * @return array<int, array<string, mixed>> The sorted array. 684 */ 685 protected function sort_response_data_by_column( array $data, string $column, int $direction ): array { 686 687 usort( 688 $data, 689 function ( $a, $b ) use ( $column, $direction ) { 690 $val_a = $a[ "raw_$column" ] ?? $a[ $column ] ?? ''; 691 $val_b = $b[ "raw_$column" ] ?? $b[ $column ] ?? ''; 692 693 if ( is_numeric( $val_a ) && is_numeric( $val_b ) ) { 694 $val_a = floatval( $val_a ); 695 $val_b = floatval( $val_b ); 696 697 return $direction === SORT_DESC ? $val_b <=> $val_a : $val_a <=> $val_b; 698 } 699 700 return $direction === SORT_DESC 701 ? strnatcasecmp( $val_b, $val_a ) 702 : strnatcasecmp( $val_a, $val_b ); 703 } 704 ); 705 706 return $data; 707 } 708 709 /** 710 * Get pagination parameters from $_GET. 711 * 712 * @return array{0: int, 1: int} { 713 * @type int $offset Pagination offset. 714 * @type int $limit Number of items per page. 715 * } 716 */ 717 protected function get_pagination_params(): array { 718 if ( 719 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 720 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 721 ) { 722 return [ 0, 25 ]; // Fallback default. 723 } 724 725 $offset = isset( $_GET['start'] ) ? intval( $_GET['start'] ) : 0; 726 $limit = isset( $_GET['length'] ) ? intval( $_GET['length'] ) : 25; 727 728 return [ $offset, $limit ]; 729 } 730 731 /** 732 * Get sort column and direction from DataTables-style request. 733 * 734 * @return array{0: string, 1: int} [ string $column, int $direction (SORT_ASC|SORT_DESC) ] 735 */ 736 public function get_sort_params(): array { 737 if ( 738 ! isset( $_SERVER['HTTP_X_WP_NONCE'] ) || 739 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ), 'wp_rest' ) 740 ) { 741 return [ 'name', SORT_ASC ]; // Fallback default. 742 } 743 744 if ( 745 ! isset( $_GET['order'][0]['column'], $_GET['columns'] ) 746 || ! is_array( $_GET['columns'] ) 747 || ! isset( $_GET['columns'][ $_GET['order'][0]['column'] ]['data'] ) 748 ) { 749 return [ 'name', SORT_ASC ]; // Fallback default. 750 } 751 752 $column_index = isset( $_GET['order'][0]['column'] ) ? intval( $_GET['order'][0]['column'] ) : 0; 753 $column_data = isset( $_GET['columns'][ $column_index ]['data'] ) ? \sanitize_text_field( \wp_unslash( $_GET['columns'][ $column_index ]['data'] ) ) : 'name'; 754 755 $dir = strtolower( \sanitize_text_field( \wp_unslash( $_GET['order'][0]['dir'] ?? 'asc' ) ) ); 756 $dir_flag = $dir === 'desc' ? SORT_DESC : SORT_ASC; 757 758 return [ $column_data, $dir_flag ]; 759 } 760 761 /** 762 * Get the length of a value. 763 * 764 * @param mixed $value The input value. 765 * 766 * @return string 767 */ 768 private function get_length( $value ) { 769 if ( empty( $value ) ) { 770 return '0.00'; 771 } 772 if ( is_array( $value ) || is_object( $value ) ) { 773 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- intended here. 774 $length = strlen( serialize( $value ) ); 775 } elseif ( is_string( $value ) || is_numeric( $value ) ) { 776 $length = strlen( strval( $value ) ); 777 } 778 if ( ! isset( $length ) ) { 779 return '0.00'; 780 } 781 return number_format( ( $length / 1024 ), 2 ); 782 } 783 784 /** 785 * Find plugin in known plugin prefixes list. 786 * 787 * @param string $option The option name. 788 * 789 * @return string 790 */ 791 private function get_plugin_name( $option ) { 792 return $this->map_plugin_to_options->get_plugin_name( $option ); 793 } 223 794 }
Note: See TracChangeset
for help on using the changeset viewer.