Plugin Directory

Changeset 3324099


Ignore:
Timestamp:
07/08/2025 07:38:09 AM (8 months ago)
Author:
progressplanner
Message:

Update to version 1.4.0 from GitHub

Location:
aaa-option-optimizer
Files:
7 added
2 deleted
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • aaa-option-optimizer/tags/1.4.0/aaa-option-optimizer.php

    r3287558 r3324099  
    88 * Plugin URI: https://joost.blog/plugins/aaa-option-optimizer/
    99 * Description: Tracks autoloaded options usage and allows the user to optimize them.
    10  * Version: 1.3.2
     10 * Version: 1.4.0
    1111 * License: GPL-3.0+
    1212 * Author: Joost de Valk
  • aaa-option-optimizer/tags/1.4.0/css/style.css

    r3254065 r3324099  
    33    border-spacing: 0;
    44}
     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
    511.aaa_option_table td, .aaa_option_table th {
    612    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
    13/**
    24 * JavaScript for the admin page.
    35 *
    4  * @package Emilia\OptionOptimizer
     6 * @package
    57 */
    68
     
    810 * Initializes the data tables and sets up event handlers.
    911 */
    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',
     12jQuery( 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            },
    21178        ];
    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                },
    79193            ];
    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                },
    99204                { name: 'name', data: 'name' },
    100205                { 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' },
    101236                {
    102237                    name: 'size',
    103238                    data: 'size',
    104239                    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                },
    108248                {
    109249                    name: 'value',
    110250                    data: 'value',
    111                     render: (data, type, row) => renderValueColumn( row ),
     251                    render: ( data, type, row ) => renderValueColumn( row ),
    112252                    orderable: false,
    113253                    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>';
    117512            }
    118513
    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>'
    136528            );
    137529
    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>'
    142536            );
    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;
    187567            }
    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';
    209584            } else {
    210                 action               = button.hasClass( 'add-autoload' ) ? 'add-autoload' : 'remove-autoload';
    211                 route                = 'update-autoload';
    212                 requestData.autoload = ( action === 'add-autoload' ) ? 'yes' : 'no';
     585                endpoint = 'set-autoload-options';
     586                requestData.autoload =
     587                    bulkAction === 'autoload-on' ? 'yes' : 'no';
    213588            }
    214589
    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  
    3636        "option_prefixes": ["chaty_"]
    3737    },
     38    "comment_hacks": {
     39        "name": "Comment Experience by Progress Planner",
     40        "option_prefixes": ["comment_hacks"]
     41    },
    3842    "complianz": {
    3943        "name": "Complianz GDPR",
     
    5660        "option_prefixes": ["elementor_"]
    5761    },
     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    },
    5866    "fewer-tags": {
    5967        "name": "Fewer Tags",
     
    7280        "option_prefixes": ["yst_"]
    7381    },
     82    "google-site-kit": {
     83        "name": "Site Kit by Google",
     84        "option_prefixes": ["googlesitekit_"]
     85    },
    7486    "gravity-forms": {
    7587        "name": "Gravity Forms",
    7688        "option_prefixes": ["gf_","gforms_"]
    7789    },
     90    "woo-billing-with-invoicexpress": {
     91        "name": "Invoicing with InvoiceXpress for WooCommerce",
     92        "option_prefixes": ["hd_wc_ie_"]
     93    },
    7894    "indeed-membership-pro": {
    7995        "name": "Indeed Ultimate Membership Pro",
     
    90106    "litespeed-cache": {
    91107        "name": "LiteSpeed Cache",
    92         "option_prefixes": ["litespeed-", "_litespeed_"]
     108        "option_prefixes": ["litespeed-", "_litespeed_", "litespeed."]
    93109    },
    94110    "loginizer": {
     
    96112        "option_prefixes": ["loginizer_"]
    97113    },
     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    },
    98118    "perfmatters": {
    99119        "name": "Perfmatters",
     
    112132        "option_prefixes": ["porto_"]
    113133    },
     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    },
    114142    "really-simple-ssl": {
    115143        "name": "Really Simple SSL",
     
    132160        "option_prefixes": ["rank_math_"]
    133161    },
     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    },
    134174    "updraft": {
    135175        "name": "Updraft Plus",
     
    140180        "option_prefixes": ["wpseo", "yoast_"]
    141181    },
     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    },
    142194    "woocommerce": {
    143195        "name": "WooCommerce",
    144196        "option_prefixes": ["wc_", "_transient__woocommerce_", "woocommerce_"]
     197    },
     198    "woocommerce-max-quantity": {
     199        "name": "Maximum Quantity for WooCommerce Shops",
     200        "option_prefixes": ["isa_woocommerce_"]
    145201    },
    146202    "wordfence": {
  • aaa-option-optimizer/tags/1.4.0/readme.txt

    r3287558 r3324099  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 1.3.2
     7Stable tag: 1.4.0
    88License: GPL3+
    99License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
     
    5454
    5555== 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.
    5662
    5763= 1.3.2 =
  • aaa-option-optimizer/tags/1.4.0/src/class-admin-page.php

    r3254065 r3324099  
    1414
    1515    /**
    16      * The map plugin to options class.
    17      *
    18      * @var Map_Plugin_To_Options
    19      */
    20     private $map_plugin_to_options;
    21 
    22     /**
    2316     * Register hooks.
    2417     *
     
    2619     */
    2720    public function register_hooks() {
    28         $this->map_plugin_to_options = new Map_Plugin_To_Options();
    29 
    3021        // Register a link to the settings page on the plugins overview page.
    3122        \add_filter( 'plugin_action_links', [ $this, 'filter_plugin_actions' ], 10, 2 );
     
    7162            'manage_options',
    7263            'aaa-option-optimizer',
    73             [ $this, 'render_admin_page' ]
     64            [ $this, 'render_admin_page_ajax' ]
    7465        );
    7566    }
     
    124115                'nonce' => wp_create_nonce( 'wp_rest' ),
    125116                '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'                => [
    134133                        '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ),
    135134                        '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ),
    136135                    ],
    137                     'sInfo'          => sprintf(
     136                    'sInfo'                  => sprintf(
    138137                        // translators: %1$s is the start, %2$s is the end, %3$s is the total, %4$s is the entries.
    139138                        esc_html__( 'Showing %1$s to %2$s of %3$s %4$s', 'aaa-option-optimizer' ),
     
    143142                        '_ENTRIES-TOTAL_'
    144143                    ),
    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(
    147146                        // translators: %1$s is the max, %2$s is the entries-max.
    148147                        esc_html__( '(filtered from %1$s total %2$s)', 'aaa-option-optimizer' ),
     
    150149                        '_ENTRIES-MAX_'
    151150                    ),
    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'                  => [
    154153                        'orderable'        => esc_html__( ': Activate to sort', 'aaa-option-optimizer' ),
    155154                        'orderableReverse' => esc_html__( ': Activate to invert sorting', 'aaa-option-optimizer' ),
     
    198197                    echo '<th class="source">' . esc_html__( 'Source', 'aaa-option-optimizer' ) . '</th>';
    199198                    break;
     199                case 'select-all':
     200                    echo '<th class="select-all"><input type="checkbox" class="select-all-checkbox" /></th>';
     201                    break;
    200202            }
    201203        }
     
    205207
    206208    /**
    207      * Get the length of a value.
    208      *
    209      * @param mixed $value The input value.
    210      *
    211      * @return string
    212      */
    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     /**
    230209     * Renders the admin page.
    231210     *
    232211     * @return void
    233212     */
    234     public function render_admin_page() {
     213    public function render_admin_page_ajax() {
    235214        $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_BOTH
    244         );
    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         }
    281215
    282216        // Start HTML output.
     
    322256        <?php
    323257        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>
    348276        </div>
    349277        <input class="input" name="tabs" type="radio" id="tab-2"/>
     
    352280        <?php
    353281        // Render differences.
    354         if ( ! empty( $non_autoloaded_options ) ) {
    355282            echo '<h2 id="used-not-autoloaded">' . esc_html__( 'Used, but not autoloaded options', 'aaa-option-optimizer' ) . '</h2>';
    356283            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' );
    357284            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>
    380300        </div>
    381301        <input class="input" name="tabs" type="radio" id="tab-3"/>
     
    383303            <div class="panel">
    384304        <?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>
    403322        </div>
    404323        <input class="input" name="tabs" type="radio" id="tab-4"/>
     
    408327            <button id="aaa_get_all_options" class="button button-primary"><?php esc_html_e( 'Get all options', 'aaa-option-optimizer' ); ?></button>
    409328            <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' ] ); ?>
    411330                <tbody>
    412331                    <tr>
     332                        <td class="select-all"></td>
    413333                        <td></td>
    414334                        <td></td>
     
    416336                        <td></td>
    417337                        <td class="actions"></td>
    418                     </tr>   
     338                    </tr>
    419339                </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' ] ); ?>
    421341            </table>
    422342        </div>
     
    424344        <?php
    425345    }
    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 string
    434      */
    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 string
    454      */
    455     private function get_plugin_name( $option ) {
    456         return $this->map_plugin_to_options->get_plugin_name( $option );
    457     }
    458346}
  • aaa-option-optimizer/tags/1.4.0/src/class-rest.php

    r3254065 r3324099  
    122122            ]
    123123        );
     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        );
    124184    }
    125185
     
    148208            $output[] = [
    149209                '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 ),
    151211                '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 ),
    153214                'autoload' => $option->autoload,
    154                 'row_id'   => 'option_' . $option->option_name,
    155215            ];
    156216        }
    157217        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        );
    158551    }
    159552
     
    208601
    209602    /**
     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    /**
    210662     * Create an option with a false value.
    211663     *
     
    221673        return new \WP_Error( 'option_not_created', 'Option could not be created', [ 'status' => 400 ] );
    222674    }
     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    }
    223794}
  • aaa-option-optimizer/trunk/aaa-option-optimizer.php

    r3287558 r3324099  
    88 * Plugin URI: https://joost.blog/plugins/aaa-option-optimizer/
    99 * Description: Tracks autoloaded options usage and allows the user to optimize them.
    10  * Version: 1.3.2
     10 * Version: 1.4.0
    1111 * License: GPL-3.0+
    1212 * Author: Joost de Valk
  • aaa-option-optimizer/trunk/css/style.css

    r3254065 r3324099  
    33    border-spacing: 0;
    44}
     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
    511.aaa_option_table td, .aaa_option_table th {
    612    padding: 5px 10px 5px 5px;
  • aaa-option-optimizer/trunk/js/admin-script.js

    r3265010 r3324099  
     1/* global jQuery, aaaOptionOptimizer, Option, DataTable, alert */
     2
    13/**
    24 * JavaScript for the admin page.
    35 *
    4  * @package Emilia\OptionOptimizer
     6 * @package
    57 */
    68
     
    810 * Initializes the data tables and sets up event handlers.
    911 */
    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',
     12jQuery( 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            },
    21178        ];
    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                },
    79193            ];
    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                },
    99204                { name: 'name', data: 'name' },
    100205                { 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' },
    101236                {
    102237                    name: 'size',
    103238                    data: 'size',
    104239                    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                },
    108248                {
    109249                    name: 'value',
    110250                    data: 'value',
    111                     render: (data, type, row) => renderValueColumn( row ),
     251                    render: ( data, type, row ) => renderValueColumn( row ),
    112252                    orderable: false,
    113253                    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>';
    117512            }
    118513
    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>'
    136528            );
    137529
    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>'
    142536            );
    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;
    187567            }
    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';
    209584            } else {
    210                 action               = button.hasClass( 'add-autoload' ) ? 'add-autoload' : 'remove-autoload';
    211                 route                = 'update-autoload';
    212                 requestData.autoload = ( action === 'add-autoload' ) ? 'yes' : 'no';
     585                endpoint = 'set-autoload-options';
     586                requestData.autoload =
     587                    bulkAction === 'autoload-on' ? 'yes' : 'no';
    213588            }
    214589
    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  
    3636        "option_prefixes": ["chaty_"]
    3737    },
     38    "comment_hacks": {
     39        "name": "Comment Experience by Progress Planner",
     40        "option_prefixes": ["comment_hacks"]
     41    },
    3842    "complianz": {
    3943        "name": "Complianz GDPR",
     
    5660        "option_prefixes": ["elementor_"]
    5761    },
     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    },
    5866    "fewer-tags": {
    5967        "name": "Fewer Tags",
     
    7280        "option_prefixes": ["yst_"]
    7381    },
     82    "google-site-kit": {
     83        "name": "Site Kit by Google",
     84        "option_prefixes": ["googlesitekit_"]
     85    },
    7486    "gravity-forms": {
    7587        "name": "Gravity Forms",
    7688        "option_prefixes": ["gf_","gforms_"]
    7789    },
     90    "woo-billing-with-invoicexpress": {
     91        "name": "Invoicing with InvoiceXpress for WooCommerce",
     92        "option_prefixes": ["hd_wc_ie_"]
     93    },
    7894    "indeed-membership-pro": {
    7995        "name": "Indeed Ultimate Membership Pro",
     
    90106    "litespeed-cache": {
    91107        "name": "LiteSpeed Cache",
    92         "option_prefixes": ["litespeed-", "_litespeed_"]
     108        "option_prefixes": ["litespeed-", "_litespeed_", "litespeed."]
    93109    },
    94110    "loginizer": {
     
    96112        "option_prefixes": ["loginizer_"]
    97113    },
     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    },
    98118    "perfmatters": {
    99119        "name": "Perfmatters",
     
    112132        "option_prefixes": ["porto_"]
    113133    },
     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    },
    114142    "really-simple-ssl": {
    115143        "name": "Really Simple SSL",
     
    132160        "option_prefixes": ["rank_math_"]
    133161    },
     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    },
    134174    "updraft": {
    135175        "name": "Updraft Plus",
     
    140180        "option_prefixes": ["wpseo", "yoast_"]
    141181    },
     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    },
    142194    "woocommerce": {
    143195        "name": "WooCommerce",
    144196        "option_prefixes": ["wc_", "_transient__woocommerce_", "woocommerce_"]
     197    },
     198    "woocommerce-max-quantity": {
     199        "name": "Maximum Quantity for WooCommerce Shops",
     200        "option_prefixes": ["isa_woocommerce_"]
    145201    },
    146202    "wordfence": {
  • aaa-option-optimizer/trunk/readme.txt

    r3287558 r3324099  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 1.3.2
     7Stable tag: 1.4.0
    88License: GPL3+
    99License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
     
    5454
    5555== 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.
    5662
    5763= 1.3.2 =
  • aaa-option-optimizer/trunk/src/class-admin-page.php

    r3254065 r3324099  
    1414
    1515    /**
    16      * The map plugin to options class.
    17      *
    18      * @var Map_Plugin_To_Options
    19      */
    20     private $map_plugin_to_options;
    21 
    22     /**
    2316     * Register hooks.
    2417     *
     
    2619     */
    2720    public function register_hooks() {
    28         $this->map_plugin_to_options = new Map_Plugin_To_Options();
    29 
    3021        // Register a link to the settings page on the plugins overview page.
    3122        \add_filter( 'plugin_action_links', [ $this, 'filter_plugin_actions' ], 10, 2 );
     
    7162            'manage_options',
    7263            'aaa-option-optimizer',
    73             [ $this, 'render_admin_page' ]
     64            [ $this, 'render_admin_page_ajax' ]
    7465        );
    7566    }
     
    124115                'nonce' => wp_create_nonce( 'wp_rest' ),
    125116                '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'                => [
    134133                        '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ),
    135134                        '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ),
    136135                    ],
    137                     'sInfo'          => sprintf(
     136                    'sInfo'                  => sprintf(
    138137                        // translators: %1$s is the start, %2$s is the end, %3$s is the total, %4$s is the entries.
    139138                        esc_html__( 'Showing %1$s to %2$s of %3$s %4$s', 'aaa-option-optimizer' ),
     
    143142                        '_ENTRIES-TOTAL_'
    144143                    ),
    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(
    147146                        // translators: %1$s is the max, %2$s is the entries-max.
    148147                        esc_html__( '(filtered from %1$s total %2$s)', 'aaa-option-optimizer' ),
     
    150149                        '_ENTRIES-MAX_'
    151150                    ),
    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'                  => [
    154153                        'orderable'        => esc_html__( ': Activate to sort', 'aaa-option-optimizer' ),
    155154                        'orderableReverse' => esc_html__( ': Activate to invert sorting', 'aaa-option-optimizer' ),
     
    198197                    echo '<th class="source">' . esc_html__( 'Source', 'aaa-option-optimizer' ) . '</th>';
    199198                    break;
     199                case 'select-all':
     200                    echo '<th class="select-all"><input type="checkbox" class="select-all-checkbox" /></th>';
     201                    break;
    200202            }
    201203        }
     
    205207
    206208    /**
    207      * Get the length of a value.
    208      *
    209      * @param mixed $value The input value.
    210      *
    211      * @return string
    212      */
    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     /**
    230209     * Renders the admin page.
    231210     *
    232211     * @return void
    233212     */
    234     public function render_admin_page() {
     213    public function render_admin_page_ajax() {
    235214        $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_BOTH
    244         );
    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         }
    281215
    282216        // Start HTML output.
     
    322256        <?php
    323257        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>
    348276        </div>
    349277        <input class="input" name="tabs" type="radio" id="tab-2"/>
     
    352280        <?php
    353281        // Render differences.
    354         if ( ! empty( $non_autoloaded_options ) ) {
    355282            echo '<h2 id="used-not-autoloaded">' . esc_html__( 'Used, but not autoloaded options', 'aaa-option-optimizer' ) . '</h2>';
    356283            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' );
    357284            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>
    380300        </div>
    381301        <input class="input" name="tabs" type="radio" id="tab-3"/>
     
    383303            <div class="panel">
    384304        <?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>
    403322        </div>
    404323        <input class="input" name="tabs" type="radio" id="tab-4"/>
     
    408327            <button id="aaa_get_all_options" class="button button-primary"><?php esc_html_e( 'Get all options', 'aaa-option-optimizer' ); ?></button>
    409328            <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' ] ); ?>
    411330                <tbody>
    412331                    <tr>
     332                        <td class="select-all"></td>
    413333                        <td></td>
    414334                        <td></td>
     
    416336                        <td></td>
    417337                        <td class="actions"></td>
    418                     </tr>   
     338                    </tr>
    419339                </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' ] ); ?>
    421341            </table>
    422342        </div>
     
    424344        <?php
    425345    }
    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 string
    434      */
    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 string
    454      */
    455     private function get_plugin_name( $option ) {
    456         return $this->map_plugin_to_options->get_plugin_name( $option );
    457     }
    458346}
  • aaa-option-optimizer/trunk/src/class-rest.php

    r3254065 r3324099  
    122122            ]
    123123        );
     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        );
    124184    }
    125185
     
    148208            $output[] = [
    149209                '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 ),
    151211                '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 ),
    153214                'autoload' => $option->autoload,
    154                 'row_id'   => 'option_' . $option->option_name,
    155215            ];
    156216        }
    157217        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        );
    158551    }
    159552
     
    208601
    209602    /**
     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    /**
    210662     * Create an option with a false value.
    211663     *
     
    221673        return new \WP_Error( 'option_not_created', 'Option could not be created', [ 'status' => 400 ] );
    222674    }
     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    }
    223794}
Note: See TracChangeset for help on using the changeset viewer.