Plugin Directory

Changeset 3764690


Ignore:
Timestamp:
10/25/2026 01:45:31 PM (2 hours ago)
Author:
rberet
Message:

Release 2.8.6: cleanup safeguards, leftover detection and scheduled directory backups

Location:
rvs-database-doctor
Files:
36 edited
1 copied

Legend:

Unmodified
Added
Removed
  • rvs-database-doctor/tags/2.8.6/CHANGELOG.md

    r3760203 r3764690  
    44
    55The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
     6
     7## [2.8.6] - 2026-10-25
     8
     9### Added
     10- Explicit, batched emptying of Action Scheduler actions and logs, including pending and recurring jobs. Requires typing `PURGE` and deactivating plugins that load Action Scheduler; whole-queue deletion cannot be scheduled.
     11- Directory selection for site backups: choose immediate directories within `wp-content` or keep the entire tree. All authorized database tables remain included, and existing file exclusions still apply.
     12- Automatic backup controls on the Backups tab: save the selected directories and export methods with hourly, daily, weekly or monthly recurrence, and pause, resume or delete backup schedules.
     13
     14### Fixed
     15- Advisor checks WooCommerce availability and the current user's permissions before offering WooCommerce Tools, including cached recommendations. Unavailable tools now lead to Cleanup with guidance about remaining WooCommerce data.
     16- Known table-name signatures no longer count as proof of an active plugin. Leftover WooCommerce and other recognized tables remain visible for review, while active owners and the loaded Action Scheduler library remain protected.
     17- Old completed, canceled and failed Action Scheduler actions with an empty last-attempt date use their scheduled date for retention. Candidate counts, previews and deletion now use the same date rule.
     18
     19### Changed
     20- Expanded potential-leftover table identification with signatures for Formidable Forms, Ninja Forms, FluentCRM, Fluent Support, WP All Import/Export, activity logs, membership, learning, newsletter and other plugins.
     21- Selected-directory backups record their scope in the signed manifest and are excluded from full-site migration packages.
    622
    723## [2.8.5] - 2026-10-22
  • rvs-database-doctor/tags/2.8.6/assets/admin.js

    r3760203 r3764690  
    13431343        });
    13441344
     1345        $(document).on('click', '#rvs-as-purge', function () {
     1346                RVSModal.prompt({ title: RVSDADO.i18n.as_purge_title, message: RVSDADO.i18n.as_purge_message, expected: 'PURGE', danger: true }).then(function (typed) {
     1347                        if (typed !== 'PURGE') return;
     1348                        runJob({ type: 'purge_action_scheduler', params: { confirm: typed }, run_mode: 'fg', output: '#rvs-cleanup-output', container: '.rvsdado-tab-cleanup' });
     1349                });
     1350        });
     1351
    13451352        /* ===== Full-site backup / restore ===== */
     1353        $(document).on('change', '#rvs-site-all-content', function () {
     1354                $('.rvs-site-directory').prop('disabled', this.checked);
     1355        });
     1356        function siteBackupParams() {
     1357                var params = {
     1358                        db_method: $('input[name="rvs-site-db-method"]:checked').val() || 'auto',
     1359                        archive_method: $('input[name="rvs-site-archive-method"]:checked').val() || 'auto'
     1360                };
     1361                if (!$('#rvs-site-all-content').prop('checked')) {
     1362                        params.directories = $('.rvs-site-directory:checked').map(function () { return this.value; }).get();
     1363                        if (!params.directories.length) { RVSToast.error(RVSDADO.i18n.backup_select_dir); return null; }
     1364                }
     1365                return params;
     1366        }
     1367        $(document).on('click', '#rvs-backup-schedule-save', function () {
     1368                var params = siteBackupParams();
     1369                if (!params) return;
     1370                var $button = $(this).prop('disabled', true);
     1371                ajax('rvsdado_schedule_save', { name: $('#rvs-backup-schedule-name').val() || RVSDADO.i18n.backup_schedule_name, job_type: 'full_site_backup', recurrence: $('#rvs-backup-schedule-recurrence').val(), enabled: 1, params: JSON.stringify(params) }).done(function (r) {
     1372                        if (r && r.success) { location.reload(); }
     1373                        else { RVSToast.error((r && r.data && r.data.message) || RVSDADO.i18n.error); }
     1374                }).fail(function () { RVSToast.error(RVSDADO.i18n.request_failed); }).always(function () { $button.prop('disabled', false); });
     1375        });
     1376
    13461377        $(document).on('click', '#rvs-site-backup-run', function () {
    13471378                var bg = $('#rvs-site-backup-bg').prop('checked');
    1348                 var dbMethod = $('input[name="rvs-site-db-method"]:checked').val() || 'auto';
    1349                 var archiveMethod = $('input[name="rvs-site-archive-method"]:checked').val() || 'auto';
     1379                var params = siteBackupParams();
     1380                if (!params) return;
    13501381                $('#rvs-site-bk-output').text((RVSDADO.i18n.site_backup_running || 'Creating full-site backup…') + '\n');
    13511382                runJob({
    13521383                        type: 'full_site_backup',
    1353                         params: { db_method: dbMethod, archive_method: archiveMethod },
     1384                        params: params,
    13541385                        run_mode: bg ? 'bg' : 'fg',
    13551386                        output: '#rvs-site-bk-output',
  • rvs-database-doctor/tags/2.8.6/includes/actions/class-rvs-db-doctor-actions-cleanup.php

    r3754548 r3764690  
    203203        }
    204204
     205        /** Explicit whole-queue reset, separate from routine cleanup and schedules. */
     206        public static function step_purge_action_scheduler( array $params, int $batch_size, array $job ) {
     207                global $wpdb;
     208                if ( 'PURGE' !== ( $params['confirm'] ?? '' ) ) {
     209                        return new WP_Error( 'rvsdado_confirmation_required', 'Type PURGE to empty Action Scheduler actions and logs.' );
     210                }
     211                if ( class_exists( 'ActionScheduler' ) || function_exists( 'as_schedule_single_action' ) ) {
     212                        return new WP_Error( 'rvsdado_scheduler_active', 'Deactivate plugins that load Action Scheduler before emptying its queue. Use old-record cleanup while the scheduler is active.' );
     213                }
     214                // Bounded DELETE avoids long table locks on queues with hundreds of thousands of rows.
     215                foreach ( array( 'actionscheduler_logs', 'actionscheduler_actions' ) as $suffix ) {
     216                        $table = $wpdb->prefix . $suffix;
     217                        if ( ! RVSDADO_Analyzer::table_exists( $table ) ) {
     218                                continue;
     219                        }
     220                        $deleted = (int) RVSDADO_Actions::db_query( $wpdb->prepare( 'DELETE FROM %i LIMIT %d', $table, max( 1, $batch_size ) ) );
     221                        RVSDADO_Analyzer::invalidate_tags( array( 'cleanup' ) );
     222                        if ( $deleted > 0 ) {
     223                                return self::pkt( $deleted, (int) $job['processed'] + $deleted, false );
     224                        }
     225                }
     226                return self::pkt( 0, (int) $job['processed'], true );
     227        }
     228
    205229        public static function step_as_old_actions( array $params, int $batch_size, array $job ): array {
    206230                global $wpdb;
    … …  
    212236                $ids  = RVSDADO_Actions::db_get_col(
    213237                        $wpdb->prepare(
    214                                 "SELECT action_id FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt > '0000-00-00 00:00:00' AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) LIMIT %d",
     238                                "SELECT action_id FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) LIMIT %d",
    215239                                $tbl,
    216240                                $days,
  • rvs-database-doctor/tags/2.8.6/includes/admin/class-rvs-db-doctor-admin-ajax-schedules.php

    r3572373 r3764690  
    1717'params'     => array(),
    1818);
     19if ( isset( $_POST['params'] ) ) {
     20// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded here and validated against the job schema by Scheduler::save().
     21$raw = wp_unslash( $_POST['params'] );
     22$params = is_string( $raw ) ? json_decode( $raw, true ) : null;
     23if ( ! is_string( $raw ) || 0 !== strpos( ltrim( $raw ), '{' ) || ! is_array( $params ) ) {
     24wp_send_json_error( array( 'message' => 'Schedule parameters must be a JSON object.' ) );
     25}
     26$data['params'] = $params;
     27}
    1928// phpcs:enable WordPress.Security.NonceVerification.Missing
    2029$id = RVSDADO_Scheduler::save( $data );
  • rvs-database-doctor/tags/2.8.6/includes/admin/class-rvs-db-doctor-admin-backups-schedules.php

    r3760203 r3764690  
    112112                $sites = array();
    113113                foreach ( RVSDADO_Backup_Site_Archiver::list() as $b ) {
    114                         if ( ! empty( $b['is_full_site'] ) ) {
     114                        if ( ! empty( $b['is_full_site'] ) && empty( $b['site_manifest']['selected_directories'] ) ) {
    115115                                $sites[] = $b;
    116116                        }
    … …  
    204204
    205205                echo '<div class="rvsdado-panel">';
    206                 echo '<h3>' . esc_html__( 'Full-site backup (database + files)', 'rvs-database-doctor' ) . '</h3>';
    207                 echo '<p>' . esc_html__( 'Backs up all authorized database tables plus the entire wp-content tree (plugins, themes, uploads, mu-plugins) into a single ZIP archive. It excludes the plugin backup directory, caches, node_modules, .git, *.log files and symlinks that escape wp-content. It does not include wp-config.php or WordPress core files. Partial restore is available: files only (optionally a single directory such as plugins/akismet) or selected database tables.', 'rvs-database-doctor' ) . '</p>';
     206                echo '<h3>' . esc_html__( 'Site backup (database + selected files)', 'rvs-database-doctor' ) . '</h3>';
     207                echo '<p>' . esc_html__( 'Backs up all authorized database tables plus the entire wp-content tree or selected directories (plugins, themes, uploads, mu-plugins). It excludes the plugin backup directory, caches, node_modules, .git, *.log files and symlinks that escape wp-content. It does not include wp-config.php or WordPress core files. Partial restore is available: files only (optionally a single directory such as plugins/akismet) or selected database tables.', 'rvs-database-doctor' ) . '</p>';
     208
     209                echo '<fieldset><legend><strong>' . esc_html__( 'Directories to back up', 'rvs-database-doctor' ) . '</strong></legend>';
     210                echo '<label><input type="checkbox" id="rvs-site-all-content" checked> ' . esc_html__( 'Entire wp-content, including files in its root', 'rvs-database-doctor' ) . '</label>';
     211                echo '<div id="rvs-site-directories">';
     212                foreach ( RVSDADO_Backup_Site_Archiver::backup_directories() as $directory ) {
     213                        echo '<label style="display:block"><input type="checkbox" class="rvs-site-directory" value="' . esc_attr( $directory ) . '" checked disabled> ' . esc_html( $directory ) . '</label>';
     214                }
     215                echo '</div><p class="description">' . esc_html__( 'Uncheck Entire wp-content to choose directories. Selected-directory backups still include all authorized database tables, exclude root files and cannot be used as full migration packages. Existing cache and backup exclusions always apply.', 'rvs-database-doctor' ) . '</p></fieldset>';
    208216
    209217                echo '<fieldset class="rvs-backup-mode"><legend><strong>' . esc_html__( 'Database export', 'rvs-database-doctor' ) . '</strong></legend>';
    … …  
    229237                }
    230238
    231                 echo '<p><button type="button" class="button button-primary" id="rvs-site-backup-run">' . esc_html__( 'Create full-site backup', 'rvs-database-doctor' ) . '</button> ';
     239                echo '<p><button type="button" class="button button-primary" id="rvs-site-backup-run">' . esc_html__( 'Create site backup', 'rvs-database-doctor' ) . '</button> ';
    232240                echo '<label><input type="checkbox" id="rvs-site-backup-bg"> ' . esc_html__( 'Run in background', 'rvs-database-doctor' ) . '</label></p>';
    233241                echo '<div class="rvsdado-progress" role="progressbar" aria-label="Progress" aria-valuemin="0" aria-valuemax="100"><div class="rvsdado-progress-bar"></div></div>';
    234242                echo '<pre class="rvsdado-output" role="log" aria-live="polite" id="rvs-site-bk-output"></pre>';
    235                 echo '</div>';
     243                $this->render_backup_schedule_controls();
     244                echo '</div>';
     245        }
     246
     247        private function render_backup_schedule_controls(): void {
     248                echo '<h4>' . esc_html__( 'Automatic backups', 'rvs-database-doctor' ) . '</h4>';
     249                echo '<p>' . esc_html__( 'Save the directory selection and export methods above as a recurring background backup. WP-Cron needs site traffic or a server cron trigger; maintenance-window and backup-retention settings also apply.', 'rvs-database-doctor' ) . '</p>';
     250                echo '<p><input id="rvs-backup-schedule-name" aria-label="' . esc_attr__( 'Schedule name', 'rvs-database-doctor' ) . '" placeholder="' . esc_attr__( 'Schedule name', 'rvs-database-doctor' ) . '"> <select id="rvs-backup-schedule-recurrence" aria-label="' . esc_attr__( 'Recurrence', 'rvs-database-doctor' ) . '">';
     251                foreach ( array( 'hourly', 'daily', 'weekly', 'monthly' ) as $recurrence ) {
     252                        echo '<option value="' . esc_attr( $recurrence ) . '"' . selected( $recurrence, 'daily', false ) . '>' . esc_html( $recurrence ) . '</option>';
     253                }
     254                echo '</select> <button type="button" class="button" id="rvs-backup-schedule-save">' . esc_html__( 'Schedule backup', 'rvs-database-doctor' ) . '</button></p>';
     255                foreach ( RVSDADO_Scheduler::get_all() as $schedule ) {
     256                        if ( 'full_site_backup' !== $schedule['job_type'] ) { continue; }
     257                        $params = is_array( $schedule['params'] ) ? $schedule['params'] : json_decode( (string) $schedule['params'], true );
     258                        $selection = isset( $params['directories'] ) ? implode( ', ', $params['directories'] ) : __( 'Entire wp-content', 'rvs-database-doctor' );
     259                        echo '<p><strong>' . esc_html( $schedule['name'] ) . '</strong> — ' . esc_html( $schedule['recurrence'] . ' | ' . $selection ) . ' — ' . esc_html( $schedule['enabled'] ? __( 'Enabled', 'rvs-database-doctor' ) : __( 'Paused', 'rvs-database-doctor' ) ) . ' | ' . esc_html__( 'Next run (UTC):', 'rvs-database-doctor' ) . ' ' . esc_html( (string) $schedule['next_run_at'] ) . ' ';
     260                        echo '<button type="button" class="button rvs-sched-toggle" data-id="' . esc_attr( (string) $schedule['id'] ) . '" data-enabled="' . esc_attr( $schedule['enabled'] ? '0' : '1' ) . '">' . esc_html( $schedule['enabled'] ? __( 'Pause', 'rvs-database-doctor' ) : __( 'Resume', 'rvs-database-doctor' ) ) . '</button> ';
     261                        echo '<button type="button" class="button rvs-sched-delete" data-id="' . esc_attr( (string) $schedule['id'] ) . '">' . esc_html__( 'Delete', 'rvs-database-doctor' ) . '</button></p>';
     262                }
    236263        }
    237264
    … …  
    253280                echo '<p><button type="button" class="button rvs-backup-security-action" data-operation="recheck">' . esc_html__( 'Recheck public access', 'rvs-database-doctor' ) . '</button> ';
    254281                echo '<button type="button" class="button rvs-backup-security-action" data-operation="protect">' . esc_html__( 'Apply Apache protection and recheck', 'rvs-database-doctor' ) . '</button></p>';
    255                 echo '<p class="description">' . esc_html__( 'Apply protection writes deny rules into this directory’s .htaccess when PHP has write permission. Success is verified with an HTTP probe. Nginx requires a server configuration change.', 'rvs-database-doctor' ) . '</p>';
     282                echo '<p class="description">' . esc_html__( 'Apply protection writes deny rules into this directory’s .htaccess when PHP has write permission. Success is verified with an HTTP probe. Nginx requires a server configuration change.', 'rvs-database-doctor' ) . '</p>';
    256283                if ( $allowed ) {
    257284                        echo '<p><strong>' . esc_html__( 'Public-storage override is enabled for this directory.', 'rvs-database-doctor' ) . '</strong> ' . esc_html__( 'Manual, background and automatic backups may write here even if public access is detected. This permission remains enabled until revoked.', 'rvs-database-doctor' ) . '</p>';
    … …  
    263290                echo '<p id="rvs-backup-security-result" role="status" aria-live="polite"></p>';
    264291                echo '<details><summary>' . esc_html__( 'Server configuration and terminal instructions', 'rvs-database-doctor' ) . '</summary>';
    265                 echo '<p>' . esc_html__( 'Apache: place these rules in the backup directory’s .htaccess. The host must allow authorization directives through AllowOverride. The button above preserves custom rules and replaces the plugin’s old generated rules.', 'rvs-database-doctor' ) . '</p>';
     292                echo '<p>' . esc_html__( 'Apache: place these rules in the backup directory’s .htaccess. The host must allow authorization directives through AllowOverride. The button above preserves custom rules and replaces the plugin’s old generated rules.', 'rvs-database-doctor' ) . '</p>';
    266293                echo '<pre class="rvsdado-output">' . esc_html( RVSDADO_Backup::apache_deny_rules() ) . '</pre>';
    267294                $path = wp_parse_url( RVSDADO_Backup::backup_url(), PHP_URL_PATH );
    … …  
    306333
    307334                echo '<div class="rvs-bulk-bar">';
    308                 echo '<input type="text" id="rvs-bk-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" /> ';
     335                echo '<input type="text" id="rvs-bk-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" /> ';
    309336                echo '<button type="button" class="button" id="rvs-bk-select-all">' . esc_html__( 'Select all', 'rvs-database-doctor' ) . '</button> ';
    310337                echo '<button type="button" class="button" id="rvs-bk-select-wp">' . esc_html__( 'Select WP prefix only', 'rvs-database-doctor' ) . '</button> ';
    … …  
    385412                        echo '<td><code>' . esc_html( $b['job_uid'] ) . '</code></td>';
    386413                        echo '<td>' . esc_html( gmdate( 'Y-m-d H:i:s', $b['mtime'] ) ) . '</td>';
    387                         echo '<td>' . esc_html( $method ) . ( $full_site ? ' <strong>(' . esc_html__( 'Full site', 'rvs-database-doctor' ) . ')</strong>' : '' ) . ( $full_site && $site && 'zip' === $site['archive'] ? ' · ZIP' : '' ) . '</td>';
     414                        echo '<td>' . esc_html( $method ) . ( $full_site ? ' <strong>(' . esc_html( ! empty( $site['selected_directories'] ) ? __( 'Selected directories', 'rvs-database-doctor' ) : __( 'Full site', 'rvs-database-doctor' ) ) . ')</strong>' : '' ) . ( $full_site && $site && 'zip' === $site['archive'] ? ' · ZIP' : '' ) . '</td>';
    388415                        echo '<td>' . wp_kses_post( implode( '<br>', $contents ) ) . '</td>';
    389416                        echo '<td>' . esc_html( $size_label ) . '</td>';
    … …  
    416443                        echo '<td><div class="rvs-row-actions">';
    417444                        if ( $full_site ) {
    418                                 echo '<button type="button" class="button button-primary rvs-site-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore…', 'rvs-database-doctor' ) . '</button>';
     445                                echo '<button type="button" class="button button-primary rvs-site-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore…', 'rvs-database-doctor' ) . '</button>';
    419446                        }
    420447                        if ( $restore_ok ) {
    421                                 echo '<button type="button" class="button rvs-backup-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore DB…', 'rvs-database-doctor' ) . '</button>';
     448                                echo '<button type="button" class="button rvs-backup-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore DB…', 'rvs-database-doctor' ) . '</button>';
    422449                        }
    423450                        echo '<button type="button" class="button button-danger rvs-backup-delete" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Delete', 'rvs-database-doctor' ) . '</button>';
  • rvs-database-doctor/tags/2.8.6/includes/admin/class-rvs-db-doctor-admin-cleanup.php

    r3755667 r3764690  
    145145
    146146        private function render_cleanup_orphan_tables( array $orphans ): void {
     147                echo '<h3>' . esc_html__( 'Empty Action Scheduler tables', 'rvs-database-doctor' ) . '</h3>';
     148                echo '<p>' . esc_html__( 'Deletes ALL actions and logs, including pending, recurring and failed jobs, in batches. Other plugins also use Action Scheduler. Deactivate its providers first; use old-record cleanup while they are active. This operation cannot be scheduled.', 'rvs-database-doctor' ) . '</p>';
     149                echo '<button type="button" class="button button-danger" id="rvs-as-purge">' . esc_html__( 'Empty actions and logs
     150', 'rvs-database-doctor' ) . '</button>';
    147151                /* --- Orphan tables (likely from deleted plugins) --- */
    148152                echo '<h3>' . esc_html__( 'Potential leftover tables', 'rvs-database-doctor' ) . '</h3>';
    … …  
    161165                } else {
    162166                        echo '<div class="rvs-bulk-bar">';
    163                         echo '<input type="text" id="rvs-orphan-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" style="margin-right:10px; max-width:200px;" /> ';
     167                        echo '<input type="text" id="rvs-orphan-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" style="margin-right:10px; max-width:200px;" /> ';
    164168                        echo '<button type="button" class="button" id="rvs-orphan-select-high">' . esc_html__( 'Select HIGH only', 'rvs-database-doctor' ) . '</button> ';
    165169                        echo '<button type="button" class="button" id="rvs-orphan-select-all">' . esc_html__( 'Select all', 'rvs-database-doctor' ) . '</button> ';
    … …  
    185189                                $cls  = ( 'high' === $conf ) ? 'critical' : ( 'medium' === $conf ? 'warn' : 'ok' );
    186190                                $status = ! empty( $o['owner_status'] ) ? (string) $o['owner_status'] : 'unknown';
    187                                 $status_label = ucwords( str_replace( '_', ' ', $status ) );
     191                                $status_label = 'known_platform' === $status ? __( 'Known signature; no installed owner identified', 'rvs-database-doctor' ) : ucwords( str_replace( '_', ' ', $status ) );
    188192                                echo '<tr data-confidence="' . esc_attr( $conf ) . '" data-owner-status="' . esc_attr( $status ) . '">';
    189193                                echo '<td><input type="checkbox" class="rvs-orphan-pick" value="' . esc_attr( $o['table_name'] ) . '"></td>';
  • rvs-database-doctor/tags/2.8.6/includes/analyzer/class-rvs-db-doctor-analyzer-cleanup-counts.php

    r3754548 r3764690  
    133133                                $wpdb->prepare(
    134134                                        "SELECT
    135                                                 ( SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_actions,
     135                                                ( SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_actions,
    136136                                                ( SELECT COUNT(*) FROM %i WHERE log_date_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_logs",
    137137                                        $as_actions,
    … …  
    146146                        $out['as_old_actions'] = (int) $wpdb->get_var(
    147147                                $wpdb->prepare(
    148                                         "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)",
     148                                        "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)",
    149149                                        $as_actions,
    150150                                        $as_days
  • rvs-database-doctor/tags/2.8.6/includes/analyzer/class-rvs-db-doctor-analyzer-orphan-tables.php

    r3754548 r3764690  
    2727                $slugs = array();
    2828                foreach ( self::table_owner_registry() as $owner ) {
    29                         if ( in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'known_platform' ), true ) ) {
     29                        if ( in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'runtime_library' ), true ) ) {
    3030                                $slugs = array_merge( $slugs, (array) $owner['identifiers'] );
    3131                        }
    … …  
    174174                        'Ultimate Member'    => array( 'um' ),
    175175                        'BuddyPress'         => array( 'bp', 'buddypress' ),
     176                        'FluentCRM' => array( 'fc', 'fluentcrm' ),
     177                        'Fluent Support' => array( 'fs', 'fluent_support' ),
     178                        'Formidable Forms' => array( 'frm' ),
     179                        'Ninja Forms' => array( 'nf3' ),
     180                        'WP All Import' => array( 'pmxi' ),
     181                        'WP All Export' => array( 'pmxe' ),
     182                        'WP Activity Log' => array( 'wsal' ),
     183                        'Simple History' => array( 'simple_history' ),
     184                        'WP Statistics' => array( 'statistics' ),
     185                        'WP Mail SMTP' => array( 'wpmailsmtp' ),
     186                        'Mailster' => array( 'mailster' ),
     187                        'Newsletter' => array( 'newsletter' ),
     188                        'AffiliateWP' => array( 'affiliate_wp' ),
     189                        'Easy Digital Downloads' => array( 'edd' ),
     190                        'LearnDash' => array( 'learndash' ),
     191                        'LearnPress' => array( 'learnpress' ),
     192                        'MemberPress' => array( 'mepr' ),
     193                        'Paid Memberships Pro' => array( 'pmpro' ),
     194                        'The Events Calendar' => array( 'tec', 'tribe' ),
     195                        'Booking Calendar' => array( 'booking' ),
     196                        'Smush' => array( 'smush' ),
     197                        'W3 Total Cache' => array( 'w3tc' ),
    176198                );
    177199
    … …  
    180202                        $owners[] = array(
    181203                                'name'        => $name,
    182                                 'status'      => 'known_platform',
     204                                'status'      => ( 'Action Scheduler' === $name && class_exists( 'ActionScheduler' ) ) ? 'runtime_library' : 'known_platform',
    183205                                'identifiers' => self::normalize_identifier_list( $ids ),
    184                                 'reason'      => 'known platform alias',
     206                                'reason'      => 'Known table-name signature; installation and ownership must be verified',
    185207                        );
    186208                }
    … …  
    214236                );
    215237
     238                foreach ( self::known_platform_owners() as $known ) {
     239                        $map[ strtolower( $known['name'] ) ] = $known['identifiers'];
     240                }
    216241                $ids = array();
    217242                foreach ( $map as $needle => $aliases ) {
    … …  
    405430                        'inactive_plugin'           => 5,
    406431                        'known_platform'            => 2,
     432                        'runtime_library'           => 10,
    407433                );
    408434                $best = array();
    … …  
    468494
    469495        public static function find_orphan_tables(): array {
    470                 $cache_key = 'rvsdado_orphan_tables_v5';
     496                $cache_key = 'rvsdado_orphan_tables_v6';
    471497                $cached    = get_transient( $cache_key );
    472498                if ( is_array( $cached ) ) {
    … …  
    515541
    516542                        $owner = self::match_table_owner( $slow, $owners );
    517                         if ( ! empty( $owner ) && in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'known_platform' ), true ) ) {
     543                        if ( ! empty( $owner ) && in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'runtime_library' ), true ) ) {
    518544                                continue;
    519545                        }
  • rvs-database-doctor/tags/2.8.6/includes/backup/class-rvs-db-doctor-migration.php

    r3760203 r3764690  
    8181                $site_dir      = (string) $valid['site_dir'];
    8282                $site_manifest = (array) $valid['manifest'];
     83                if ( null !== ( $site_manifest['selected_directories'] ?? null ) ) {
     84                        return new WP_Error( 'rvsdado_partial_backup', 'Migration requires a backup of the entire wp-content tree.' );
     85                }
    8386                $archive_file  = (string) ( $site_manifest['archive']['file'] ?? '' );
    8487                $unified       = '' !== $archive_file
    … …  
    435438        private static function latest_full_site_uid(): string {
    436439                foreach ( RVSDADO_Backup_Site_Archiver::list() as $b ) {
    437                         if ( ! empty( $b['is_full_site'] ) ) {
     440                        if ( ! empty( $b['is_full_site'] ) && empty( $b['site_manifest']['selected_directories'] ) ) {
    438441                                return (string) $b['job_uid'];
    439442                        }
  • rvs-database-doctor/tags/2.8.6/includes/backup/class-rvs-db-doctor-site-archiver.php

    r3760203 r3764690  
    5757         * =================================================================== */
    5858
     59        /** Selectable immediate directories inside wp-content; never follow symlinks. */
     60        public static function backup_directories(): array {
     61                $root = self::content_dir();
     62                $dirs = array();
     63                if ( '' === $root || ! is_dir( $root ) ) {
     64                        return $dirs;
     65                }
     66                foreach ( (array) @scandir( $root ) as $name ) {
     67                        if ( '.' === $name || '..' === $name || ! is_string( $name ) ) {
     68                                continue;
     69                        }
     70                        $path = $root . '/' . $name;
     71                        if ( is_dir( $path ) && ! is_link( $path ) && ! self::is_backup_path( $path ) && '' === self::dir_exclusion( $name, $name ) ) {
     72                                $dirs[] = $name;
     73                        }
     74                }
     75                return $dirs;
     76        }
     77
     78        public static function validate_directories( $value ) {
     79                if ( ! is_array( $value ) || empty( $value ) || array_keys( $value ) !== range( 0, count( $value ) - 1 ) ) {
     80                        return new WP_Error( 'rvsdado_directories', 'Select at least one wp-content directory.' );
     81                }
     82                $allowed = self::backup_directories();
     83                foreach ( $value as $dir ) {
     84                        if ( ! is_string( $dir ) || ! in_array( $dir, $allowed, true ) ) {
     85                                return new WP_Error( 'rvsdado_directories', 'A selected directory is missing, excluded or outside wp-content.' );
     86                        }
     87                }
     88                return array_values( array_unique( $value ) );
     89        }
     90
    5991        /**
    6092         * Resolve the exact file set a full-site backup would archive.
    … …  
    6496         * applied. Nothing is ever resolved outside wp-content.
    6597         *
    66          * @param array $args Reserved; no parameter may widen or change the scope.
     98         * @param array $args Optional directories narrow the file selection within wp-content.
    6799         * @return array
    68100         */
    … …  
    91123                }
    92124                $root_real = rtrim( str_replace( '\\', '/', $root_real ), '/' );
     125                $selected = null;
     126                if ( array_key_exists( 'directories', $args ) ) {
     127                        $selected = self::validate_directories( $args['directories'] );
     128                        if ( is_wp_error( $selected ) ) {
     129                                $out['error'] = $selected->get_error_message();
     130                                return $out;
     131                        }
     132                        $out['selected_directories'] = $selected;
     133                }
    93134
    94135                $files    = array();
    … …  
    115156                                $rel = '' === $rel_dir ? $name : $rel_dir . '/' . $name;
    116157                                $abs = $abs_dir . '/' . $name;
     158                                if ( null !== $selected && '' === $rel_dir && ! in_array( $name, $selected, true ) ) {
     159                                        continue;
     160                                }
    117161
    118162                                // Symlinks: never followed. Escaping or dangling links are
    … …  
    232276                                throw new RuntimeException( 'Unsafe site archive directory.' );
    233277                        }
    234                         $plan = self::plan();
     278                        $plan = self::plan( $args );
    235279                        if ( empty( $plan['ok'] ) ) {
    236280                                throw new RuntimeException( (string) $plan['error'] );
    … …  
    427471                                }
    428472                                $b['site_manifest'] = array(
     473                                        'selected_directories' => $site['selected_directories'] ?? null,
    429474                                        'file_count'  => count( $files ),
    430475                                        'total_bytes' => $bytes,
    … …  
    471516                $site_dir = $dir . '/' . self::SITE_DIR;
    472517                $manifest = RVSDADO_Backup::read_manifest( $site_dir );
    473                 if ( ! is_array( $manifest ) || empty( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
     518                if ( ! is_array( $manifest ) || ! isset( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
    474519                        return new WP_Error( 'rvsdado_no_site_manifest', 'A complete signed full-site manifest is required.' );
    475520                }
    … …  
    594639                $tables_done = isset( $params['tables'] ) && is_array( $params['tables'] ) ? count( $params['tables'] ) : 0;
    595640                $files_total = 0;
    596                 $plan = self::plan();
     641                $plan = self::plan( $params );
    597642                if ( ! empty( $plan['ok'] ) && isset( $plan['files'] ) && is_array( $plan['files'] ) ) {
    598643                        $files_total = count( $plan['files'] );
    … …  
    10281073        private static function build_site_manifest( string $uid, array $plan, string $mode, array $files, string $compression = 'none', string $db_method = '', array $archive_meta = array() ): array {
    10291074                return array(
     1075                        'selected_directories' => $plan['selected_directories'] ?? null,
    10301076                        'scope'       => self::SCOPE,
    10311077                        'format'      => self::FORMAT,
    … …  
    13781424                }
    13791425                $state = self::load_site_state( $site_dir );
    1380                 $plan = isset( $state['plan'] ) && is_array( $state['plan'] ) ? $state['plan'] : self::plan();
     1426                $plan = isset( $state['plan'] ) && is_array( $state['plan'] ) ? $state['plan'] : self::plan( $params );
    13811427                if ( empty( $plan['ok'] ) ) {
    13821428                        return new WP_Error( 'rvsdado_site_backup_failed', (string) $plan['error'] );
  • rvs-database-doctor/tags/2.8.6/includes/class-rvs-db-doctor-actions.php

    r3760203 r3764690  
    307307                        'cleanup_orphan_term_relationships' => array( $orph, 'step_orphan_term_relationships' ),
    308308                        'cleanup_wc_expired_sessions'       => array( $wc, 'step_wc_expired_sessions' ),
     309                        'purge_action_scheduler'            => array( $cleanup, 'step_purge_action_scheduler' ),
    309310                        'cleanup_as_old_actions'            => array( $cleanup, 'step_as_old_actions' ),
    310311                        'cleanup_as_old_logs'               => array( $cleanup, 'step_as_old_logs' ),
    … …  
    390391                $registry['cleanup_revisions']              = array( 'risk' => 'reversible_mutation', 'schema' => array( 'confirm' => 'text' ) );
    391392                $registry['cleanup_old_trash']              = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
     393                $registry['purge_action_scheduler'] = array( 'risk' => 'destructive', 'confirm' => 'PURGE', 'schema' => array( 'confirm' => 'text' ) );
    392394                $registry['cleanup_as_old_actions']         = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
    393395                $registry['cleanup_as_old_logs']            = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
    … …  
    409411                // Full-site backup/restore. Scope is server-derived (all authorized
    410412                // tables + wp-content); method keys only choose how the dump and the
    411                 // file archive are produced and never widen or narrow the scope.
    412                 $registry['full_site_backup']               = array( 'risk' => 'backup', 'schema' => array( 'db_method' => 'key', 'archive_method' => 'key' ) );
     413                // file archive are produced. Optional directories narrow the file scope.
     414                $registry['full_site_backup']               = array( 'risk' => 'backup', 'schema' => array( 'db_method' => 'key', 'archive_method' => 'key', 'directories' => 'backup_directories' ) );
    413415                $registry['full_site_restore']              = array(
    414416                        'risk'    => 'restore',
    … …  
    562564                                }
    563565                                return array_values( array_unique( $value ) );
     566                        case 'backup_directories':
     567                                return RVSDADO_Backup_Site_Archiver::validate_directories( $value );
    564568                        case 'path_list':
    565569                                // Relative wp-content paths (e.g. 'plugins/acme') for partial file
    … …  
    697701                                if ( ! RVSDADO_Analyzer::table_exists( $tbl ) ) return array( 'total' => 0, 'sql' => '-- not present' );
    698702                                $days = isset( $params['days'] ) ? max( 1, (int) $params['days'] ) : self::settings_days_as();
    699                                 $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $tbl, $days ) );
     703                                $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $tbl, $days ) );
    700704                                return array( 'total' => $total, 'sql' => 'DELETE AS old actions' );
    701705                        case 'cleanup_as_old_logs':
  • rvs-database-doctor/tags/2.8.6/includes/class-rvs-db-doctor-admin.php

    r3760203 r3764690  
    196196                                        'restore_modal_selected'   => __( 'of %d selected - only the checked tables are restored.', 'rvs-database-doctor' ),
    197197                                        'restore_modal_authorized' => __( 'Only tables contained in this signed backup can be listed; tables outside your authorized scope are never restored.', 'rvs-database-doctor' ),
     198                                        'as_purge_title'        => __( 'Empty Action Scheduler', 'rvs-database-doctor' ),
     199                                        'as_purge_message'      => __( 'Delete ALL actions and logs, including pending and recurring jobs? Deactivate Action Scheduler providers first. Type PURGE to confirm.', 'rvs-database-doctor' ),
     200                                        'backup_select_dir'     => __( 'Select at least one directory.', 'rvs-database-doctor' ),
     201                                        'backup_schedule_name'  => __( 'Site backup', 'rvs-database-doctor' ),
    198202                                        'site_backup_warn'      => __( 'A full-site backup includes all authorized database tables AND the wp-content files (plugins, themes, uploads). It does not include wp-config.php or WordPress core, and skips caches, logs, VCS folders and symlinks that escape wp-content.', 'rvs-database-doctor' ),
    199203                                        'site_backup_running'   => __( 'Creating full-site backup…', 'rvs-database-doctor' ),
    200                                         'site_backup_done'      => __( 'Full-site backup complete.', 'rvs-database-doctor' ),
     204                                        'site_backup_done'      => __( 'Site backup complete.', 'rvs-database-doctor' ),
    201205                                        'site_restore_warn'     => __( 'A full-site restore replaces the database tables AND overwrites wp-content files from the backup. A pre-restore database backup is created automatically. Type RESTORE to confirm.', 'rvs-database-doctor' ),
    202206                                        'site_restore_confirm'  => __( 'You must type RESTORE to confirm.', 'rvs-database-doctor' ),
  • rvs-database-doctor/tags/2.8.6/includes/class-rvs-db-doctor-advisor.php

    r3754548 r3764690  
    3939                        $cached = get_transient( 'rvsdado_advisor_eval' );
    4040                        if ( is_array( $cached ) && isset( $cached['score'], $cached['tips'] ) ) {
    41                                 return $cached;
     41                                return self::available_actions( $cached );
    4242                        }
    4343                }
    4444                $result = self::evaluate_uncached();
    4545                set_transient( 'rvsdado_advisor_eval', $result, 15 * MINUTE_IN_SECONDS );
     46                return self::available_actions( $result );
     47        }
     48
     49        /** Resolve user permissions and plugin availability outside the shared cache. */
     50        private static function available_actions( array $result ): array {
     51                foreach ( $result['tips'] as &$tip ) {
     52                        if ( false !== strpos( (string) ( $tip['action']['url'] ?? '' ), 'page=wc-status' )
     53                                && ( ! class_exists( 'WooCommerce' ) || ! current_user_can( 'manage_woocommerce' ) ) ) {
     54                                $tip['action'] = array( 'tab' => 'cleanup' );
     55                                $tip['detail'] = __( 'WooCommerce tools are unavailable. Review the remaining WooCommerce tables under Potential leftover tables in Cleanup. Deactivation does not mean the stored orders and products are disposable; back up and verify ownership before removing tables.', 'rvs-database-doctor' );
     56                        }
     57                }
     58                unset( $tip );
    4659                return $result;
    4760        }
  • rvs-database-doctor/tags/2.8.6/includes/class-rvs-db-doctor-analyzer.php

    r3754548 r3764690  
    7272                                delete_transient( 'rvsdado_orphan_tables_v4' );
    7373                                delete_transient( 'rvsdado_orphan_tables_v5' );
     74                                delete_transient( 'rvsdado_orphan_tables_v6' );
    7475                                self::$owner_registry_cache = null;
    7576                        }
  • rvs-database-doctor/tags/2.8.6/includes/class-rvs-db-doctor-runner.php

    r3755680 r3764690  
    267267                return apply_filters(
    268268                        'rvsdado_snapshot_types',
    269                         array( 'optimize_now', 'cleanup_revisions', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'engine_convert', 'optimize_table', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'drop_orphan_tables' )
     269                        array( 'optimize_now', 'cleanup_revisions', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'engine_convert', 'optimize_table', 'purge_action_scheduler', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'drop_orphan_tables' )
    270270                );
    271271        }
    … …  
    308308                return apply_filters(
    309309                        'rvsdado_backup_types',
    310                         array( 'optimize_now', 'cleanup_chain', 'engine_convert', 'index_add', 'index_bulk_add', 'index_drop', 'add_pk', 'fix_collation', 'autoload_update', 'autoload_bulk_update', 'cleanup_expired_transients', 'cleanup_auto_drafts', 'cleanup_spam_comments', 'cleanup_trash_comments', 'cleanup_wc_expired_sessions', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'cleanup_duplicate_postmeta', 'cleanup_stale_session_tokens', 'cleanup_edit_locks', 'cleanup_unused_terms', 'cleanup_plugin_caches', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'cleanup_revisions', 'quarantine_orphan_tables', 'restore_quarantined_tables', 'delete_quarantined_tables', 'drop_orphan_tables' )
     310                        array( 'optimize_now', 'cleanup_chain', 'engine_convert', 'index_add', 'index_bulk_add', 'index_drop', 'add_pk', 'fix_collation', 'autoload_update', 'autoload_bulk_update', 'cleanup_expired_transients', 'cleanup_auto_drafts', 'cleanup_spam_comments', 'cleanup_trash_comments', 'cleanup_wc_expired_sessions', 'purge_action_scheduler', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'cleanup_duplicate_postmeta', 'cleanup_stale_session_tokens', 'cleanup_edit_locks', 'cleanup_unused_terms', 'cleanup_plugin_caches', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'cleanup_revisions', 'quarantine_orphan_tables', 'restore_quarantined_tables', 'delete_quarantined_tables', 'drop_orphan_tables' )
    311311                );
    312312        }
    … …  
    352352                        case 'cleanup_unused_terms':
    353353                                return array( $wpdb->terms, $wpdb->term_taxonomy, $wpdb->term_relationships, $wpdb->termmeta, $wpdb->options );
     354                        case 'purge_action_scheduler':
    354355                        case 'cleanup_as_old_actions':
    355356                                return array_values( array_filter( array( $p . 'actionscheduler_actions', $p . 'actionscheduler_logs' ), array( 'RVSDADO_Analyzer', 'table_exists' ) ) );
  • rvs-database-doctor/tags/2.8.6/languages/rvs-database-doctor.pot

    r3760203 r3764690  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: RVS Database Doctor 2.8.5\n"
     5"Project-Id-Version: RVS Database Doctor 2.8.6\n"
    66"Report-Msgid-Bugs-To: https://rberet.com/wp-plugins/rvs-database-doctor\n"
    77"POT-Creation-Date: 2026-05-02 00:00+0000\n"
  • rvs-database-doctor/tags/2.8.6/readme.txt

    r3760203 r3764690  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 2.8.5
     7Stable tag: 2.8.6
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8181== Changelog ==
    8282
     83= 2.8.6 =
     84* Fixed: Advisor only offers WooCommerce Tools when WooCommerce is loaded and the current user has permission; otherwise it directs users to Cleanup.
     85* Added: batched emptying of all Action Scheduler actions and logs with PURGE confirmation. Requires inactive Action Scheduler providers and cannot be scheduled.
     86* Fixed: retention cleanup handles old actions without a last-attempt date consistently with candidate counts and previews.
     87* Fixed: known table signatures no longer hide leftover WooCommerce and other plugin tables as if their owners were active.
     88* Changed: expanded plugin identification for potential leftover tables while preserving protection for active owners and the Action Scheduler runtime.
     89* Added: selection of wp-content directories for site backups, with the selected scope stored in the signed manifest. Partial backups cannot be used as full migration packages.
     90* Added: hourly, daily, weekly and monthly automatic backups from the Backups tab, preserving selected directories and export methods, with pause, resume and delete controls.
     91
    8392= 2.8.5 =
    8493* Changed: the standalone migration installer imports the database dump in batched multi-statement transactions (up to 2000 statements or 512 KB per round trip, one commit per work slice) instead of one query per statement. Import slices are 5-15 seconds depending on the host limit and the wizard resumes 100 ms after each slice.
    … …  
    234243== Upgrade Notice ==
    235244
     245= 2.8.6 =
     246Fixes WooCommerce Advisor links and leftover-table detection. Adds explicit Action Scheduler queue emptying, selected-directory backups and recurring backup controls. Whole-queue emptying requires inactive Action Scheduler providers.
     247
    236248= 2.8.5 =
    237249Adds full-site migration packages and the standalone rvsmigrate.php installer, single-archive full-site backups, and much faster restore/migration imports. Take a backup before updating.
  • rvs-database-doctor/tags/2.8.6/rvs-database-doctor.php

    r3760203 r3764690  
    44 * Plugin URI:        https://rberet.com/wp-plugins/rvs-database-doctor
    55 * Description:       Safely analyze and optimize WordPress / WooCommerce databases with backups, restore, scheduled jobs, and an actionable health advisor.
    6  * Version:           2.8.5
     6 * Version:           2.8.6
    77 * Requires at least: 6.2
    88 * Requires PHP:      7.4
    … …  
    2222}
    2323
    24 define( 'RVSDADO_VERSION', '2.8.5' );
     24define( 'RVSDADO_VERSION', '2.8.6' );
    2525define( 'RVSDADO_DB_VERSION', '6' );
    2626define( 'RVSDADO_FILE', __FILE__ );
  • rvs-database-doctor/trunk/CHANGELOG.md

    r3760203 r3764690  
    44
    55The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
     6
     7## [2.8.6] - 2026-10-25
     8
     9### Added
     10- Explicit, batched emptying of Action Scheduler actions and logs, including pending and recurring jobs. Requires typing `PURGE` and deactivating plugins that load Action Scheduler; whole-queue deletion cannot be scheduled.
     11- Directory selection for site backups: choose immediate directories within `wp-content` or keep the entire tree. All authorized database tables remain included, and existing file exclusions still apply.
     12- Automatic backup controls on the Backups tab: save the selected directories and export methods with hourly, daily, weekly or monthly recurrence, and pause, resume or delete backup schedules.
     13
     14### Fixed
     15- Advisor checks WooCommerce availability and the current user's permissions before offering WooCommerce Tools, including cached recommendations. Unavailable tools now lead to Cleanup with guidance about remaining WooCommerce data.
     16- Known table-name signatures no longer count as proof of an active plugin. Leftover WooCommerce and other recognized tables remain visible for review, while active owners and the loaded Action Scheduler library remain protected.
     17- Old completed, canceled and failed Action Scheduler actions with an empty last-attempt date use their scheduled date for retention. Candidate counts, previews and deletion now use the same date rule.
     18
     19### Changed
     20- Expanded potential-leftover table identification with signatures for Formidable Forms, Ninja Forms, FluentCRM, Fluent Support, WP All Import/Export, activity logs, membership, learning, newsletter and other plugins.
     21- Selected-directory backups record their scope in the signed manifest and are excluded from full-site migration packages.
    622
    723## [2.8.5] - 2026-10-22
  • rvs-database-doctor/trunk/assets/admin.js

    r3760203 r3764690  
    13431343        });
    13441344
     1345        $(document).on('click', '#rvs-as-purge', function () {
     1346                RVSModal.prompt({ title: RVSDADO.i18n.as_purge_title, message: RVSDADO.i18n.as_purge_message, expected: 'PURGE', danger: true }).then(function (typed) {
     1347                        if (typed !== 'PURGE') return;
     1348                        runJob({ type: 'purge_action_scheduler', params: { confirm: typed }, run_mode: 'fg', output: '#rvs-cleanup-output', container: '.rvsdado-tab-cleanup' });
     1349                });
     1350        });
     1351
    13451352        /* ===== Full-site backup / restore ===== */
     1353        $(document).on('change', '#rvs-site-all-content', function () {
     1354                $('.rvs-site-directory').prop('disabled', this.checked);
     1355        });
     1356        function siteBackupParams() {
     1357                var params = {
     1358                        db_method: $('input[name="rvs-site-db-method"]:checked').val() || 'auto',
     1359                        archive_method: $('input[name="rvs-site-archive-method"]:checked').val() || 'auto'
     1360                };
     1361                if (!$('#rvs-site-all-content').prop('checked')) {
     1362                        params.directories = $('.rvs-site-directory:checked').map(function () { return this.value; }).get();
     1363                        if (!params.directories.length) { RVSToast.error(RVSDADO.i18n.backup_select_dir); return null; }
     1364                }
     1365                return params;
     1366        }
     1367        $(document).on('click', '#rvs-backup-schedule-save', function () {
     1368                var params = siteBackupParams();
     1369                if (!params) return;
     1370                var $button = $(this).prop('disabled', true);
     1371                ajax('rvsdado_schedule_save', { name: $('#rvs-backup-schedule-name').val() || RVSDADO.i18n.backup_schedule_name, job_type: 'full_site_backup', recurrence: $('#rvs-backup-schedule-recurrence').val(), enabled: 1, params: JSON.stringify(params) }).done(function (r) {
     1372                        if (r && r.success) { location.reload(); }
     1373                        else { RVSToast.error((r && r.data && r.data.message) || RVSDADO.i18n.error); }
     1374                }).fail(function () { RVSToast.error(RVSDADO.i18n.request_failed); }).always(function () { $button.prop('disabled', false); });
     1375        });
     1376
    13461377        $(document).on('click', '#rvs-site-backup-run', function () {
    13471378                var bg = $('#rvs-site-backup-bg').prop('checked');
    1348                 var dbMethod = $('input[name="rvs-site-db-method"]:checked').val() || 'auto';
    1349                 var archiveMethod = $('input[name="rvs-site-archive-method"]:checked').val() || 'auto';
     1379                var params = siteBackupParams();
     1380                if (!params) return;
    13501381                $('#rvs-site-bk-output').text((RVSDADO.i18n.site_backup_running || 'Creating full-site backup…') + '\n');
    13511382                runJob({
    13521383                        type: 'full_site_backup',
    1353                         params: { db_method: dbMethod, archive_method: archiveMethod },
     1384                        params: params,
    13541385                        run_mode: bg ? 'bg' : 'fg',
    13551386                        output: '#rvs-site-bk-output',
  • rvs-database-doctor/trunk/includes/actions/class-rvs-db-doctor-actions-cleanup.php

    r3754548 r3764690  
    203203        }
    204204
     205        /** Explicit whole-queue reset, separate from routine cleanup and schedules. */
     206        public static function step_purge_action_scheduler( array $params, int $batch_size, array $job ) {
     207                global $wpdb;
     208                if ( 'PURGE' !== ( $params['confirm'] ?? '' ) ) {
     209                        return new WP_Error( 'rvsdado_confirmation_required', 'Type PURGE to empty Action Scheduler actions and logs.' );
     210                }
     211                if ( class_exists( 'ActionScheduler' ) || function_exists( 'as_schedule_single_action' ) ) {
     212                        return new WP_Error( 'rvsdado_scheduler_active', 'Deactivate plugins that load Action Scheduler before emptying its queue. Use old-record cleanup while the scheduler is active.' );
     213                }
     214                // Bounded DELETE avoids long table locks on queues with hundreds of thousands of rows.
     215                foreach ( array( 'actionscheduler_logs', 'actionscheduler_actions' ) as $suffix ) {
     216                        $table = $wpdb->prefix . $suffix;
     217                        if ( ! RVSDADO_Analyzer::table_exists( $table ) ) {
     218                                continue;
     219                        }
     220                        $deleted = (int) RVSDADO_Actions::db_query( $wpdb->prepare( 'DELETE FROM %i LIMIT %d', $table, max( 1, $batch_size ) ) );
     221                        RVSDADO_Analyzer::invalidate_tags( array( 'cleanup' ) );
     222                        if ( $deleted > 0 ) {
     223                                return self::pkt( $deleted, (int) $job['processed'] + $deleted, false );
     224                        }
     225                }
     226                return self::pkt( 0, (int) $job['processed'], true );
     227        }
     228
    205229        public static function step_as_old_actions( array $params, int $batch_size, array $job ): array {
    206230                global $wpdb;
    … …  
    212236                $ids  = RVSDADO_Actions::db_get_col(
    213237                        $wpdb->prepare(
    214                                 "SELECT action_id FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt > '0000-00-00 00:00:00' AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) LIMIT %d",
     238                                "SELECT action_id FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) LIMIT %d",
    215239                                $tbl,
    216240                                $days,
  • rvs-database-doctor/trunk/includes/admin/class-rvs-db-doctor-admin-ajax-schedules.php

    r3572373 r3764690  
    1717'params'     => array(),
    1818);
     19if ( isset( $_POST['params'] ) ) {
     20// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded here and validated against the job schema by Scheduler::save().
     21$raw = wp_unslash( $_POST['params'] );
     22$params = is_string( $raw ) ? json_decode( $raw, true ) : null;
     23if ( ! is_string( $raw ) || 0 !== strpos( ltrim( $raw ), '{' ) || ! is_array( $params ) ) {
     24wp_send_json_error( array( 'message' => 'Schedule parameters must be a JSON object.' ) );
     25}
     26$data['params'] = $params;
     27}
    1928// phpcs:enable WordPress.Security.NonceVerification.Missing
    2029$id = RVSDADO_Scheduler::save( $data );
  • rvs-database-doctor/trunk/includes/admin/class-rvs-db-doctor-admin-backups-schedules.php

    r3760203 r3764690  
    112112                $sites = array();
    113113                foreach ( RVSDADO_Backup_Site_Archiver::list() as $b ) {
    114                         if ( ! empty( $b['is_full_site'] ) ) {
     114                        if ( ! empty( $b['is_full_site'] ) && empty( $b['site_manifest']['selected_directories'] ) ) {
    115115                                $sites[] = $b;
    116116                        }
    … …  
    204204
    205205                echo '<div class="rvsdado-panel">';
    206                 echo '<h3>' . esc_html__( 'Full-site backup (database + files)', 'rvs-database-doctor' ) . '</h3>';
    207                 echo '<p>' . esc_html__( 'Backs up all authorized database tables plus the entire wp-content tree (plugins, themes, uploads, mu-plugins) into a single ZIP archive. It excludes the plugin backup directory, caches, node_modules, .git, *.log files and symlinks that escape wp-content. It does not include wp-config.php or WordPress core files. Partial restore is available: files only (optionally a single directory such as plugins/akismet) or selected database tables.', 'rvs-database-doctor' ) . '</p>';
     206                echo '<h3>' . esc_html__( 'Site backup (database + selected files)', 'rvs-database-doctor' ) . '</h3>';
     207                echo '<p>' . esc_html__( 'Backs up all authorized database tables plus the entire wp-content tree or selected directories (plugins, themes, uploads, mu-plugins). It excludes the plugin backup directory, caches, node_modules, .git, *.log files and symlinks that escape wp-content. It does not include wp-config.php or WordPress core files. Partial restore is available: files only (optionally a single directory such as plugins/akismet) or selected database tables.', 'rvs-database-doctor' ) . '</p>';
     208
     209                echo '<fieldset><legend><strong>' . esc_html__( 'Directories to back up', 'rvs-database-doctor' ) . '</strong></legend>';
     210                echo '<label><input type="checkbox" id="rvs-site-all-content" checked> ' . esc_html__( 'Entire wp-content, including files in its root', 'rvs-database-doctor' ) . '</label>';
     211                echo '<div id="rvs-site-directories">';
     212                foreach ( RVSDADO_Backup_Site_Archiver::backup_directories() as $directory ) {
     213                        echo '<label style="display:block"><input type="checkbox" class="rvs-site-directory" value="' . esc_attr( $directory ) . '" checked disabled> ' . esc_html( $directory ) . '</label>';
     214                }
     215                echo '</div><p class="description">' . esc_html__( 'Uncheck Entire wp-content to choose directories. Selected-directory backups still include all authorized database tables, exclude root files and cannot be used as full migration packages. Existing cache and backup exclusions always apply.', 'rvs-database-doctor' ) . '</p></fieldset>';
    208216
    209217                echo '<fieldset class="rvs-backup-mode"><legend><strong>' . esc_html__( 'Database export', 'rvs-database-doctor' ) . '</strong></legend>';
    … …  
    229237                }
    230238
    231                 echo '<p><button type="button" class="button button-primary" id="rvs-site-backup-run">' . esc_html__( 'Create full-site backup', 'rvs-database-doctor' ) . '</button> ';
     239                echo '<p><button type="button" class="button button-primary" id="rvs-site-backup-run">' . esc_html__( 'Create site backup', 'rvs-database-doctor' ) . '</button> ';
    232240                echo '<label><input type="checkbox" id="rvs-site-backup-bg"> ' . esc_html__( 'Run in background', 'rvs-database-doctor' ) . '</label></p>';
    233241                echo '<div class="rvsdado-progress" role="progressbar" aria-label="Progress" aria-valuemin="0" aria-valuemax="100"><div class="rvsdado-progress-bar"></div></div>';
    234242                echo '<pre class="rvsdado-output" role="log" aria-live="polite" id="rvs-site-bk-output"></pre>';
    235                 echo '</div>';
     243                $this->render_backup_schedule_controls();
     244                echo '</div>';
     245        }
     246
     247        private function render_backup_schedule_controls(): void {
     248                echo '<h4>' . esc_html__( 'Automatic backups', 'rvs-database-doctor' ) . '</h4>';
     249                echo '<p>' . esc_html__( 'Save the directory selection and export methods above as a recurring background backup. WP-Cron needs site traffic or a server cron trigger; maintenance-window and backup-retention settings also apply.', 'rvs-database-doctor' ) . '</p>';
     250                echo '<p><input id="rvs-backup-schedule-name" aria-label="' . esc_attr__( 'Schedule name', 'rvs-database-doctor' ) . '" placeholder="' . esc_attr__( 'Schedule name', 'rvs-database-doctor' ) . '"> <select id="rvs-backup-schedule-recurrence" aria-label="' . esc_attr__( 'Recurrence', 'rvs-database-doctor' ) . '">';
     251                foreach ( array( 'hourly', 'daily', 'weekly', 'monthly' ) as $recurrence ) {
     252                        echo '<option value="' . esc_attr( $recurrence ) . '"' . selected( $recurrence, 'daily', false ) . '>' . esc_html( $recurrence ) . '</option>';
     253                }
     254                echo '</select> <button type="button" class="button" id="rvs-backup-schedule-save">' . esc_html__( 'Schedule backup', 'rvs-database-doctor' ) . '</button></p>';
     255                foreach ( RVSDADO_Scheduler::get_all() as $schedule ) {
     256                        if ( 'full_site_backup' !== $schedule['job_type'] ) { continue; }
     257                        $params = is_array( $schedule['params'] ) ? $schedule['params'] : json_decode( (string) $schedule['params'], true );
     258                        $selection = isset( $params['directories'] ) ? implode( ', ', $params['directories'] ) : __( 'Entire wp-content', 'rvs-database-doctor' );
     259                        echo '<p><strong>' . esc_html( $schedule['name'] ) . '</strong> — ' . esc_html( $schedule['recurrence'] . ' | ' . $selection ) . ' — ' . esc_html( $schedule['enabled'] ? __( 'Enabled', 'rvs-database-doctor' ) : __( 'Paused', 'rvs-database-doctor' ) ) . ' | ' . esc_html__( 'Next run (UTC):', 'rvs-database-doctor' ) . ' ' . esc_html( (string) $schedule['next_run_at'] ) . ' ';
     260                        echo '<button type="button" class="button rvs-sched-toggle" data-id="' . esc_attr( (string) $schedule['id'] ) . '" data-enabled="' . esc_attr( $schedule['enabled'] ? '0' : '1' ) . '">' . esc_html( $schedule['enabled'] ? __( 'Pause', 'rvs-database-doctor' ) : __( 'Resume', 'rvs-database-doctor' ) ) . '</button> ';
     261                        echo '<button type="button" class="button rvs-sched-delete" data-id="' . esc_attr( (string) $schedule['id'] ) . '">' . esc_html__( 'Delete', 'rvs-database-doctor' ) . '</button></p>';
     262                }
    236263        }
    237264
    … …  
    253280                echo '<p><button type="button" class="button rvs-backup-security-action" data-operation="recheck">' . esc_html__( 'Recheck public access', 'rvs-database-doctor' ) . '</button> ';
    254281                echo '<button type="button" class="button rvs-backup-security-action" data-operation="protect">' . esc_html__( 'Apply Apache protection and recheck', 'rvs-database-doctor' ) . '</button></p>';
    255                 echo '<p class="description">' . esc_html__( 'Apply protection writes deny rules into this directory’s .htaccess when PHP has write permission. Success is verified with an HTTP probe. Nginx requires a server configuration change.', 'rvs-database-doctor' ) . '</p>';
     282                echo '<p class="description">' . esc_html__( 'Apply protection writes deny rules into this directory’s .htaccess when PHP has write permission. Success is verified with an HTTP probe. Nginx requires a server configuration change.', 'rvs-database-doctor' ) . '</p>';
    256283                if ( $allowed ) {
    257284                        echo '<p><strong>' . esc_html__( 'Public-storage override is enabled for this directory.', 'rvs-database-doctor' ) . '</strong> ' . esc_html__( 'Manual, background and automatic backups may write here even if public access is detected. This permission remains enabled until revoked.', 'rvs-database-doctor' ) . '</p>';
    … …  
    263290                echo '<p id="rvs-backup-security-result" role="status" aria-live="polite"></p>';
    264291                echo '<details><summary>' . esc_html__( 'Server configuration and terminal instructions', 'rvs-database-doctor' ) . '</summary>';
    265                 echo '<p>' . esc_html__( 'Apache: place these rules in the backup directory’s .htaccess. The host must allow authorization directives through AllowOverride. The button above preserves custom rules and replaces the plugin’s old generated rules.', 'rvs-database-doctor' ) . '</p>';
     292                echo '<p>' . esc_html__( 'Apache: place these rules in the backup directory’s .htaccess. The host must allow authorization directives through AllowOverride. The button above preserves custom rules and replaces the plugin’s old generated rules.', 'rvs-database-doctor' ) . '</p>';
    266293                echo '<pre class="rvsdado-output">' . esc_html( RVSDADO_Backup::apache_deny_rules() ) . '</pre>';
    267294                $path = wp_parse_url( RVSDADO_Backup::backup_url(), PHP_URL_PATH );
    … …  
    306333
    307334                echo '<div class="rvs-bulk-bar">';
    308                 echo '<input type="text" id="rvs-bk-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" /> ';
     335                echo '<input type="text" id="rvs-bk-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" /> ';
    309336                echo '<button type="button" class="button" id="rvs-bk-select-all">' . esc_html__( 'Select all', 'rvs-database-doctor' ) . '</button> ';
    310337                echo '<button type="button" class="button" id="rvs-bk-select-wp">' . esc_html__( 'Select WP prefix only', 'rvs-database-doctor' ) . '</button> ';
    … …  
    385412                        echo '<td><code>' . esc_html( $b['job_uid'] ) . '</code></td>';
    386413                        echo '<td>' . esc_html( gmdate( 'Y-m-d H:i:s', $b['mtime'] ) ) . '</td>';
    387                         echo '<td>' . esc_html( $method ) . ( $full_site ? ' <strong>(' . esc_html__( 'Full site', 'rvs-database-doctor' ) . ')</strong>' : '' ) . ( $full_site && $site && 'zip' === $site['archive'] ? ' · ZIP' : '' ) . '</td>';
     414                        echo '<td>' . esc_html( $method ) . ( $full_site ? ' <strong>(' . esc_html( ! empty( $site['selected_directories'] ) ? __( 'Selected directories', 'rvs-database-doctor' ) : __( 'Full site', 'rvs-database-doctor' ) ) . ')</strong>' : '' ) . ( $full_site && $site && 'zip' === $site['archive'] ? ' · ZIP' : '' ) . '</td>';
    388415                        echo '<td>' . wp_kses_post( implode( '<br>', $contents ) ) . '</td>';
    389416                        echo '<td>' . esc_html( $size_label ) . '</td>';
    … …  
    416443                        echo '<td><div class="rvs-row-actions">';
    417444                        if ( $full_site ) {
    418                                 echo '<button type="button" class="button button-primary rvs-site-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore…', 'rvs-database-doctor' ) . '</button>';
     445                                echo '<button type="button" class="button button-primary rvs-site-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore…', 'rvs-database-doctor' ) . '</button>';
    419446                        }
    420447                        if ( $restore_ok ) {
    421                                 echo '<button type="button" class="button rvs-backup-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore DB…', 'rvs-database-doctor' ) . '</button>';
     448                                echo '<button type="button" class="button rvs-backup-restore" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Restore DB…', 'rvs-database-doctor' ) . '</button>';
    422449                        }
    423450                        echo '<button type="button" class="button button-danger rvs-backup-delete" data-uid="' . esc_attr( $b['job_uid'] ) . '">' . esc_html__( 'Delete', 'rvs-database-doctor' ) . '</button>';
  • rvs-database-doctor/trunk/includes/admin/class-rvs-db-doctor-admin-cleanup.php

    r3755667 r3764690  
    145145
    146146        private function render_cleanup_orphan_tables( array $orphans ): void {
     147                echo '<h3>' . esc_html__( 'Empty Action Scheduler tables', 'rvs-database-doctor' ) . '</h3>';
     148                echo '<p>' . esc_html__( 'Deletes ALL actions and logs, including pending, recurring and failed jobs, in batches. Other plugins also use Action Scheduler. Deactivate its providers first; use old-record cleanup while they are active. This operation cannot be scheduled.', 'rvs-database-doctor' ) . '</p>';
     149                echo '<button type="button" class="button button-danger" id="rvs-as-purge">' . esc_html__( 'Empty actions and logs
     150', 'rvs-database-doctor' ) . '</button>';
    147151                /* --- Orphan tables (likely from deleted plugins) --- */
    148152                echo '<h3>' . esc_html__( 'Potential leftover tables', 'rvs-database-doctor' ) . '</h3>';
    … …  
    161165                } else {
    162166                        echo '<div class="rvs-bulk-bar">';
    163                         echo '<input type="text" id="rvs-orphan-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" style="margin-right:10px; max-width:200px;" /> ';
     167                        echo '<input type="text" id="rvs-orphan-filter" placeholder="' . esc_attr__( 'Filter tables…', 'rvs-database-doctor' ) . '" style="margin-right:10px; max-width:200px;" /> ';
    164168                        echo '<button type="button" class="button" id="rvs-orphan-select-high">' . esc_html__( 'Select HIGH only', 'rvs-database-doctor' ) . '</button> ';
    165169                        echo '<button type="button" class="button" id="rvs-orphan-select-all">' . esc_html__( 'Select all', 'rvs-database-doctor' ) . '</button> ';
    … …  
    185189                                $cls  = ( 'high' === $conf ) ? 'critical' : ( 'medium' === $conf ? 'warn' : 'ok' );
    186190                                $status = ! empty( $o['owner_status'] ) ? (string) $o['owner_status'] : 'unknown';
    187                                 $status_label = ucwords( str_replace( '_', ' ', $status ) );
     191                                $status_label = 'known_platform' === $status ? __( 'Known signature; no installed owner identified', 'rvs-database-doctor' ) : ucwords( str_replace( '_', ' ', $status ) );
    188192                                echo '<tr data-confidence="' . esc_attr( $conf ) . '" data-owner-status="' . esc_attr( $status ) . '">';
    189193                                echo '<td><input type="checkbox" class="rvs-orphan-pick" value="' . esc_attr( $o['table_name'] ) . '"></td>';
  • rvs-database-doctor/trunk/includes/analyzer/class-rvs-db-doctor-analyzer-cleanup-counts.php

    r3754548 r3764690  
    133133                                $wpdb->prepare(
    134134                                        "SELECT
    135                                                 ( SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_actions,
     135                                                ( SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_actions,
    136136                                                ( SELECT COUNT(*) FROM %i WHERE log_date_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ) AS as_old_logs",
    137137                                        $as_actions,
    … …  
    146146                        $out['as_old_actions'] = (int) $wpdb->get_var(
    147147                                $wpdb->prepare(
    148                                         "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)",
     148                                        "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)",
    149149                                        $as_actions,
    150150                                        $as_days
  • rvs-database-doctor/trunk/includes/analyzer/class-rvs-db-doctor-analyzer-orphan-tables.php

    r3754548 r3764690  
    2727                $slugs = array();
    2828                foreach ( self::table_owner_registry() as $owner ) {
    29                         if ( in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'known_platform' ), true ) ) {
     29                        if ( in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'runtime_library' ), true ) ) {
    3030                                $slugs = array_merge( $slugs, (array) $owner['identifiers'] );
    3131                        }
    … …  
    174174                        'Ultimate Member'    => array( 'um' ),
    175175                        'BuddyPress'         => array( 'bp', 'buddypress' ),
     176                        'FluentCRM' => array( 'fc', 'fluentcrm' ),
     177                        'Fluent Support' => array( 'fs', 'fluent_support' ),
     178                        'Formidable Forms' => array( 'frm' ),
     179                        'Ninja Forms' => array( 'nf3' ),
     180                        'WP All Import' => array( 'pmxi' ),
     181                        'WP All Export' => array( 'pmxe' ),
     182                        'WP Activity Log' => array( 'wsal' ),
     183                        'Simple History' => array( 'simple_history' ),
     184                        'WP Statistics' => array( 'statistics' ),
     185                        'WP Mail SMTP' => array( 'wpmailsmtp' ),
     186                        'Mailster' => array( 'mailster' ),
     187                        'Newsletter' => array( 'newsletter' ),
     188                        'AffiliateWP' => array( 'affiliate_wp' ),
     189                        'Easy Digital Downloads' => array( 'edd' ),
     190                        'LearnDash' => array( 'learndash' ),
     191                        'LearnPress' => array( 'learnpress' ),
     192                        'MemberPress' => array( 'mepr' ),
     193                        'Paid Memberships Pro' => array( 'pmpro' ),
     194                        'The Events Calendar' => array( 'tec', 'tribe' ),
     195                        'Booking Calendar' => array( 'booking' ),
     196                        'Smush' => array( 'smush' ),
     197                        'W3 Total Cache' => array( 'w3tc' ),
    176198                );
    177199
    … …  
    180202                        $owners[] = array(
    181203                                'name'        => $name,
    182                                 'status'      => 'known_platform',
     204                                'status'      => ( 'Action Scheduler' === $name && class_exists( 'ActionScheduler' ) ) ? 'runtime_library' : 'known_platform',
    183205                                'identifiers' => self::normalize_identifier_list( $ids ),
    184                                 'reason'      => 'known platform alias',
     206                                'reason'      => 'Known table-name signature; installation and ownership must be verified',
    185207                        );
    186208                }
    … …  
    214236                );
    215237
     238                foreach ( self::known_platform_owners() as $known ) {
     239                        $map[ strtolower( $known['name'] ) ] = $known['identifiers'];
     240                }
    216241                $ids = array();
    217242                foreach ( $map as $needle => $aliases ) {
    … …  
    405430                        'inactive_plugin'           => 5,
    406431                        'known_platform'            => 2,
     432                        'runtime_library'           => 10,
    407433                );
    408434                $best = array();
    … …  
    468494
    469495        public static function find_orphan_tables(): array {
    470                 $cache_key = 'rvsdado_orphan_tables_v5';
     496                $cache_key = 'rvsdado_orphan_tables_v6';
    471497                $cached    = get_transient( $cache_key );
    472498                if ( is_array( $cached ) ) {
    … …  
    515541
    516542                        $owner = self::match_table_owner( $slow, $owners );
    517                         if ( ! empty( $owner ) && in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'known_platform' ), true ) ) {
     543                        if ( ! empty( $owner ) && in_array( $owner['status'], array( 'active_plugin', 'network_active_plugin', 'mu_plugin', 'active_theme', 'runtime_library' ), true ) ) {
    518544                                continue;
    519545                        }
  • rvs-database-doctor/trunk/includes/backup/class-rvs-db-doctor-migration.php

    r3760203 r3764690  
    8181                $site_dir      = (string) $valid['site_dir'];
    8282                $site_manifest = (array) $valid['manifest'];
     83                if ( null !== ( $site_manifest['selected_directories'] ?? null ) ) {
     84                        return new WP_Error( 'rvsdado_partial_backup', 'Migration requires a backup of the entire wp-content tree.' );
     85                }
    8386                $archive_file  = (string) ( $site_manifest['archive']['file'] ?? '' );
    8487                $unified       = '' !== $archive_file
    … …  
    435438        private static function latest_full_site_uid(): string {
    436439                foreach ( RVSDADO_Backup_Site_Archiver::list() as $b ) {
    437                         if ( ! empty( $b['is_full_site'] ) ) {
     440                        if ( ! empty( $b['is_full_site'] ) && empty( $b['site_manifest']['selected_directories'] ) ) {
    438441                                return (string) $b['job_uid'];
    439442                        }
  • rvs-database-doctor/trunk/includes/backup/class-rvs-db-doctor-site-archiver.php

    r3760203 r3764690  
    5757         * =================================================================== */
    5858
     59        /** Selectable immediate directories inside wp-content; never follow symlinks. */
     60        public static function backup_directories(): array {
     61                $root = self::content_dir();
     62                $dirs = array();
     63                if ( '' === $root || ! is_dir( $root ) ) {
     64                        return $dirs;
     65                }
     66                foreach ( (array) @scandir( $root ) as $name ) {
     67                        if ( '.' === $name || '..' === $name || ! is_string( $name ) ) {
     68                                continue;
     69                        }
     70                        $path = $root . '/' . $name;
     71                        if ( is_dir( $path ) && ! is_link( $path ) && ! self::is_backup_path( $path ) && '' === self::dir_exclusion( $name, $name ) ) {
     72                                $dirs[] = $name;
     73                        }
     74                }
     75                return $dirs;
     76        }
     77
     78        public static function validate_directories( $value ) {
     79                if ( ! is_array( $value ) || empty( $value ) || array_keys( $value ) !== range( 0, count( $value ) - 1 ) ) {
     80                        return new WP_Error( 'rvsdado_directories', 'Select at least one wp-content directory.' );
     81                }
     82                $allowed = self::backup_directories();
     83                foreach ( $value as $dir ) {
     84                        if ( ! is_string( $dir ) || ! in_array( $dir, $allowed, true ) ) {
     85                                return new WP_Error( 'rvsdado_directories', 'A selected directory is missing, excluded or outside wp-content.' );
     86                        }
     87                }
     88                return array_values( array_unique( $value ) );
     89        }
     90
    5991        /**
    6092         * Resolve the exact file set a full-site backup would archive.
    … …  
    6496         * applied. Nothing is ever resolved outside wp-content.
    6597         *
    66          * @param array $args Reserved; no parameter may widen or change the scope.
     98         * @param array $args Optional directories narrow the file selection within wp-content.
    6799         * @return array
    68100         */
    … …  
    91123                }
    92124                $root_real = rtrim( str_replace( '\\', '/', $root_real ), '/' );
     125                $selected = null;
     126                if ( array_key_exists( 'directories', $args ) ) {
     127                        $selected = self::validate_directories( $args['directories'] );
     128                        if ( is_wp_error( $selected ) ) {
     129                                $out['error'] = $selected->get_error_message();
     130                                return $out;
     131                        }
     132                        $out['selected_directories'] = $selected;
     133                }
    93134
    94135                $files    = array();
    … …  
    115156                                $rel = '' === $rel_dir ? $name : $rel_dir . '/' . $name;
    116157                                $abs = $abs_dir . '/' . $name;
     158                                if ( null !== $selected && '' === $rel_dir && ! in_array( $name, $selected, true ) ) {
     159                                        continue;
     160                                }
    117161
    118162                                // Symlinks: never followed. Escaping or dangling links are
    … …  
    232276                                throw new RuntimeException( 'Unsafe site archive directory.' );
    233277                        }
    234                         $plan = self::plan();
     278                        $plan = self::plan( $args );
    235279                        if ( empty( $plan['ok'] ) ) {
    236280                                throw new RuntimeException( (string) $plan['error'] );
    … …  
    427471                                }
    428472                                $b['site_manifest'] = array(
     473                                        'selected_directories' => $site['selected_directories'] ?? null,
    429474                                        'file_count'  => count( $files ),
    430475                                        'total_bytes' => $bytes,
    … …  
    471516                $site_dir = $dir . '/' . self::SITE_DIR;
    472517                $manifest = RVSDADO_Backup::read_manifest( $site_dir );
    473                 if ( ! is_array( $manifest ) || empty( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
     518                if ( ! is_array( $manifest ) || ! isset( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
    474519                        return new WP_Error( 'rvsdado_no_site_manifest', 'A complete signed full-site manifest is required.' );
    475520                }
    … …  
    594639                $tables_done = isset( $params['tables'] ) && is_array( $params['tables'] ) ? count( $params['tables'] ) : 0;
    595640                $files_total = 0;
    596                 $plan = self::plan();
     641                $plan = self::plan( $params );
    597642                if ( ! empty( $plan['ok'] ) && isset( $plan['files'] ) && is_array( $plan['files'] ) ) {
    598643                        $files_total = count( $plan['files'] );
    … …  
    10281073        private static function build_site_manifest( string $uid, array $plan, string $mode, array $files, string $compression = 'none', string $db_method = '', array $archive_meta = array() ): array {
    10291074                return array(
     1075                        'selected_directories' => $plan['selected_directories'] ?? null,
    10301076                        'scope'       => self::SCOPE,
    10311077                        'format'      => self::FORMAT,
    … …  
    13781424                }
    13791425                $state = self::load_site_state( $site_dir );
    1380                 $plan = isset( $state['plan'] ) && is_array( $state['plan'] ) ? $state['plan'] : self::plan();
     1426                $plan = isset( $state['plan'] ) && is_array( $state['plan'] ) ? $state['plan'] : self::plan( $params );
    13811427                if ( empty( $plan['ok'] ) ) {
    13821428                        return new WP_Error( 'rvsdado_site_backup_failed', (string) $plan['error'] );
  • rvs-database-doctor/trunk/includes/class-rvs-db-doctor-actions.php

    r3760203 r3764690  
    307307                        'cleanup_orphan_term_relationships' => array( $orph, 'step_orphan_term_relationships' ),
    308308                        'cleanup_wc_expired_sessions'       => array( $wc, 'step_wc_expired_sessions' ),
     309                        'purge_action_scheduler'            => array( $cleanup, 'step_purge_action_scheduler' ),
    309310                        'cleanup_as_old_actions'            => array( $cleanup, 'step_as_old_actions' ),
    310311                        'cleanup_as_old_logs'               => array( $cleanup, 'step_as_old_logs' ),
    … …  
    390391                $registry['cleanup_revisions']              = array( 'risk' => 'reversible_mutation', 'schema' => array( 'confirm' => 'text' ) );
    391392                $registry['cleanup_old_trash']              = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
     393                $registry['purge_action_scheduler'] = array( 'risk' => 'destructive', 'confirm' => 'PURGE', 'schema' => array( 'confirm' => 'text' ) );
    392394                $registry['cleanup_as_old_actions']         = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
    393395                $registry['cleanup_as_old_logs']            = array( 'risk' => 'reversible_mutation', 'schema' => array( 'days' => 'int', 'confirm' => 'text' ) );
    … …  
    409411                // Full-site backup/restore. Scope is server-derived (all authorized
    410412                // tables + wp-content); method keys only choose how the dump and the
    411                 // file archive are produced and never widen or narrow the scope.
    412                 $registry['full_site_backup']               = array( 'risk' => 'backup', 'schema' => array( 'db_method' => 'key', 'archive_method' => 'key' ) );
     413                // file archive are produced. Optional directories narrow the file scope.
     414                $registry['full_site_backup']               = array( 'risk' => 'backup', 'schema' => array( 'db_method' => 'key', 'archive_method' => 'key', 'directories' => 'backup_directories' ) );
    413415                $registry['full_site_restore']              = array(
    414416                        'risk'    => 'restore',
    … …  
    562564                                }
    563565                                return array_values( array_unique( $value ) );
     566                        case 'backup_directories':
     567                                return RVSDADO_Backup_Site_Archiver::validate_directories( $value );
    564568                        case 'path_list':
    565569                                // Relative wp-content paths (e.g. 'plugins/acme') for partial file
    … …  
    697701                                if ( ! RVSDADO_Analyzer::table_exists( $tbl ) ) return array( 'total' => 0, 'sql' => '-- not present' );
    698702                                $days = isset( $params['days'] ) ? max( 1, (int) $params['days'] ) : self::settings_days_as();
    699                                 $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND last_attempt_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $tbl, $days ) );
     703                                $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status IN ('complete','canceled','failed') AND COALESCE(NULLIF(last_attempt_gmt, '0000-00-00 00:00:00'), NULLIF(scheduled_date_gmt, '0000-00-00 00:00:00')) < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $tbl, $days ) );
    700704                                return array( 'total' => $total, 'sql' => 'DELETE AS old actions' );
    701705                        case 'cleanup_as_old_logs':
  • rvs-database-doctor/trunk/includes/class-rvs-db-doctor-admin.php

    r3760203 r3764690  
    196196                                        'restore_modal_selected'   => __( 'of %d selected - only the checked tables are restored.', 'rvs-database-doctor' ),
    197197                                        'restore_modal_authorized' => __( 'Only tables contained in this signed backup can be listed; tables outside your authorized scope are never restored.', 'rvs-database-doctor' ),
     198                                        'as_purge_title'        => __( 'Empty Action Scheduler', 'rvs-database-doctor' ),
     199                                        'as_purge_message'      => __( 'Delete ALL actions and logs, including pending and recurring jobs? Deactivate Action Scheduler providers first. Type PURGE to confirm.', 'rvs-database-doctor' ),
     200                                        'backup_select_dir'     => __( 'Select at least one directory.', 'rvs-database-doctor' ),
     201                                        'backup_schedule_name'  => __( 'Site backup', 'rvs-database-doctor' ),
    198202                                        'site_backup_warn'      => __( 'A full-site backup includes all authorized database tables AND the wp-content files (plugins, themes, uploads). It does not include wp-config.php or WordPress core, and skips caches, logs, VCS folders and symlinks that escape wp-content.', 'rvs-database-doctor' ),
    199203                                        'site_backup_running'   => __( 'Creating full-site backup…', 'rvs-database-doctor' ),
    200                                         'site_backup_done'      => __( 'Full-site backup complete.', 'rvs-database-doctor' ),
     204                                        'site_backup_done'      => __( 'Site backup complete.', 'rvs-database-doctor' ),
    201205                                        'site_restore_warn'     => __( 'A full-site restore replaces the database tables AND overwrites wp-content files from the backup. A pre-restore database backup is created automatically. Type RESTORE to confirm.', 'rvs-database-doctor' ),
    202206                                        'site_restore_confirm'  => __( 'You must type RESTORE to confirm.', 'rvs-database-doctor' ),
  • rvs-database-doctor/trunk/includes/class-rvs-db-doctor-advisor.php

    r3754548 r3764690  
    3939                        $cached = get_transient( 'rvsdado_advisor_eval' );
    4040                        if ( is_array( $cached ) && isset( $cached['score'], $cached['tips'] ) ) {
    41                                 return $cached;
     41                                return self::available_actions( $cached );
    4242                        }
    4343                }
    4444                $result = self::evaluate_uncached();
    4545                set_transient( 'rvsdado_advisor_eval', $result, 15 * MINUTE_IN_SECONDS );
     46                return self::available_actions( $result );
     47        }
     48
     49        /** Resolve user permissions and plugin availability outside the shared cache. */
     50        private static function available_actions( array $result ): array {
     51                foreach ( $result['tips'] as &$tip ) {
     52                        if ( false !== strpos( (string) ( $tip['action']['url'] ?? '' ), 'page=wc-status' )
     53                                && ( ! class_exists( 'WooCommerce' ) || ! current_user_can( 'manage_woocommerce' ) ) ) {
     54                                $tip['action'] = array( 'tab' => 'cleanup' );
     55                                $tip['detail'] = __( 'WooCommerce tools are unavailable. Review the remaining WooCommerce tables under Potential leftover tables in Cleanup. Deactivation does not mean the stored orders and products are disposable; back up and verify ownership before removing tables.', 'rvs-database-doctor' );
     56                        }
     57                }
     58                unset( $tip );
    4659                return $result;
    4760        }
  • rvs-database-doctor/trunk/includes/class-rvs-db-doctor-analyzer.php

    r3754548 r3764690  
    7272                                delete_transient( 'rvsdado_orphan_tables_v4' );
    7373                                delete_transient( 'rvsdado_orphan_tables_v5' );
     74                                delete_transient( 'rvsdado_orphan_tables_v6' );
    7475                                self::$owner_registry_cache = null;
    7576                        }
  • rvs-database-doctor/trunk/includes/class-rvs-db-doctor-runner.php

    r3755680 r3764690  
    267267                return apply_filters(
    268268                        'rvsdado_snapshot_types',
    269                         array( 'optimize_now', 'cleanup_revisions', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'engine_convert', 'optimize_table', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'drop_orphan_tables' )
     269                        array( 'optimize_now', 'cleanup_revisions', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'engine_convert', 'optimize_table', 'purge_action_scheduler', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'drop_orphan_tables' )
    270270                );
    271271        }
    … …  
    308308                return apply_filters(
    309309                        'rvsdado_backup_types',
    310                         array( 'optimize_now', 'cleanup_chain', 'engine_convert', 'index_add', 'index_bulk_add', 'index_drop', 'add_pk', 'fix_collation', 'autoload_update', 'autoload_bulk_update', 'cleanup_expired_transients', 'cleanup_auto_drafts', 'cleanup_spam_comments', 'cleanup_trash_comments', 'cleanup_wc_expired_sessions', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'cleanup_duplicate_postmeta', 'cleanup_stale_session_tokens', 'cleanup_edit_locks', 'cleanup_unused_terms', 'cleanup_plugin_caches', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'cleanup_revisions', 'quarantine_orphan_tables', 'restore_quarantined_tables', 'delete_quarantined_tables', 'drop_orphan_tables' )
     310                        array( 'optimize_now', 'cleanup_chain', 'engine_convert', 'index_add', 'index_bulk_add', 'index_drop', 'add_pk', 'fix_collation', 'autoload_update', 'autoload_bulk_update', 'cleanup_expired_transients', 'cleanup_auto_drafts', 'cleanup_spam_comments', 'cleanup_trash_comments', 'cleanup_wc_expired_sessions', 'purge_action_scheduler', 'cleanup_as_old_actions', 'cleanup_as_old_logs', 'cleanup_duplicate_postmeta', 'cleanup_stale_session_tokens', 'cleanup_edit_locks', 'cleanup_unused_terms', 'cleanup_plugin_caches', 'cleanup_orphan_postmeta', 'cleanup_orphan_commentmeta', 'cleanup_orphan_term_relationships', 'cleanup_orphan_usermeta', 'cleanup_orphan_termmeta', 'cleanup_oembed_cache', 'cleanup_wc_orphan_order_items', 'cleanup_wc_orphan_order_itemmeta', 'cleanup_old_trash', 'cleanup_revisions', 'quarantine_orphan_tables', 'restore_quarantined_tables', 'delete_quarantined_tables', 'drop_orphan_tables' )
    311311                );
    312312        }
    … …  
    352352                        case 'cleanup_unused_terms':
    353353                                return array( $wpdb->terms, $wpdb->term_taxonomy, $wpdb->term_relationships, $wpdb->termmeta, $wpdb->options );
     354                        case 'purge_action_scheduler':
    354355                        case 'cleanup_as_old_actions':
    355356                                return array_values( array_filter( array( $p . 'actionscheduler_actions', $p . 'actionscheduler_logs' ), array( 'RVSDADO_Analyzer', 'table_exists' ) ) );
  • rvs-database-doctor/trunk/languages/rvs-database-doctor.pot

    r3760203 r3764690  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: RVS Database Doctor 2.8.5\n"
     5"Project-Id-Version: RVS Database Doctor 2.8.6\n"
    66"Report-Msgid-Bugs-To: https://rberet.com/wp-plugins/rvs-database-doctor\n"
    77"POT-Creation-Date: 2026-05-02 00:00+0000\n"
  • rvs-database-doctor/trunk/readme.txt

    r3760203 r3764690  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 2.8.5
     7Stable tag: 2.8.6
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8181== Changelog ==
    8282
     83= 2.8.6 =
     84* Fixed: Advisor only offers WooCommerce Tools when WooCommerce is loaded and the current user has permission; otherwise it directs users to Cleanup.
     85* Added: batched emptying of all Action Scheduler actions and logs with PURGE confirmation. Requires inactive Action Scheduler providers and cannot be scheduled.
     86* Fixed: retention cleanup handles old actions without a last-attempt date consistently with candidate counts and previews.
     87* Fixed: known table signatures no longer hide leftover WooCommerce and other plugin tables as if their owners were active.
     88* Changed: expanded plugin identification for potential leftover tables while preserving protection for active owners and the Action Scheduler runtime.
     89* Added: selection of wp-content directories for site backups, with the selected scope stored in the signed manifest. Partial backups cannot be used as full migration packages.
     90* Added: hourly, daily, weekly and monthly automatic backups from the Backups tab, preserving selected directories and export methods, with pause, resume and delete controls.
     91
    8392= 2.8.5 =
    8493* Changed: the standalone migration installer imports the database dump in batched multi-statement transactions (up to 2000 statements or 512 KB per round trip, one commit per work slice) instead of one query per statement. Import slices are 5-15 seconds depending on the host limit and the wizard resumes 100 ms after each slice.
    … …  
    234243== Upgrade Notice ==
    235244
     245= 2.8.6 =
     246Fixes WooCommerce Advisor links and leftover-table detection. Adds explicit Action Scheduler queue emptying, selected-directory backups and recurring backup controls. Whole-queue emptying requires inactive Action Scheduler providers.
     247
    236248= 2.8.5 =
    237249Adds full-site migration packages and the standalone rvsmigrate.php installer, single-archive full-site backups, and much faster restore/migration imports. Take a backup before updating.
  • rvs-database-doctor/trunk/rvs-database-doctor.php

    r3760203 r3764690  
    44 * Plugin URI:        https://rberet.com/wp-plugins/rvs-database-doctor
    55 * Description:       Safely analyze and optimize WordPress / WooCommerce databases with backups, restore, scheduled jobs, and an actionable health advisor.
    6  * Version:           2.8.5
     6 * Version:           2.8.6
    77 * Requires at least: 6.2
    88 * Requires PHP:      7.4
    … …  
    2222}
    2323
    24 define( 'RVSDADO_VERSION', '2.8.5' );
     24define( 'RVSDADO_VERSION', '2.8.6' );
    2525define( 'RVSDADO_DB_VERSION', '6' );
    2626define( 'RVSDADO_FILE', __FILE__ );
Note: See TracChangeset for help on using the changeset viewer.