aisiteark_manage/aisiteark_admin), but existing installs granted * the OLD names to roles, so every plugin action would fail its current_user_can() check until * the grants are moved across. Guarded by its OWN flag so it runs even on installs that already * completed the data rename above (whose FLAG is set) but never had their caps fixed. * * @return void */ private static function maybe_fix_caps() { if ( get_option( self::CAP_FLAG ) ) { return; } try { $roles = wp_roles(); if ( $roles ) { // Preserve any custom role-based grants (Pro): move the cap on whichever roles held it. $map = array( 'aisv_manage' => Capabilities::MANAGE, 'aisv_admin' => Capabilities::ADMIN, ); foreach ( array_keys( $roles->roles ) as $rn ) { $role = get_role( $rn ); if ( ! $role ) { continue; } foreach ( $map as $old => $new ) { if ( $role->has_cap( $old ) ) { $role->add_cap( $new ); $role->remove_cap( $old ); } } } } // Belt-and-suspenders: the administrator always gets the new caps, even if the old ones // were missing (fresh install, or a role that was reset). Capabilities::add_to_roles(); update_option( self::CAP_FLAG, defined( 'AISITEARK_VERSION' ) ? AISITEARK_VERSION : '1', true ); } catch ( \Throwable $e ) { return; // Retry next load. } } /** * Detect a pre-rename install and, if found, move its data onto the new names. * * @return void */ private static function run() { global $wpdb; $old_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . 'aisv_backups' ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $old_opt = $wpdb->get_var( "SELECT option_id FROM {$wpdb->options} WHERE option_name = 'aisv_version' LIMIT 1" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( $old_table || $old_opt ) { self::rename_options( $wpdb ); self::rename_tables( $wpdb ); self::clear_old_cron(); // The options cache still holds the old names for this request — flush so the plugin // reads the migrated values immediately (one-time cost). wp_cache_flush(); } update_option( self::FLAG, defined( 'AISITEARK_VERSION' ) ? AISITEARK_VERSION : '1', true ); } /** * Rename every `aisv_*` option, transient and site-transient row to `aisiteark_*`. * * @param \wpdb $wpdb Database. * @return void */ private static function rename_options( $wpdb ) { $likes = array( 'aisv\_%', '_transient_aisv\_%', '_transient_timeout_aisv\_%', '_site_transient_aisv\_%', '_site_transient_timeout_aisv\_%', ); foreach ( $likes as $like ) { $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( (array) $names as $name ) { $new = preg_replace( '/aisv_/', 'aisiteark_', (string) $name, 1 ); if ( $new === $name ) { continue; } // No clobber: if the target already exists (a partial prior run), leave it. $exists = $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $new ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( $exists ) { continue; } $wpdb->update( $wpdb->options, array( 'option_name' => $new ), array( 'option_name' => $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } } } /** * Rename the two custom tables, preserving all rows. * * @param \wpdb $wpdb Database. * @return void */ private static function rename_tables( $wpdb ) { foreach ( array( 'backups', 'jobs' ) as $t ) { $old = $wpdb->prefix . 'aisv_' . $t; $new = $wpdb->prefix . 'aisiteark_' . $t; $have_old = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $have_new = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $new ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( $have_old && ! $have_new ) { $wpdb->query( "RENAME TABLE `{$old}` TO `{$new}`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table names are internal (wpdb prefix + fixed suffix), backtick-quoted; no user input. } } } /** * Drop cron events registered under the old hook names; the load-time reconcile re-registers * the equivalents under the new names from the (now migrated) settings. * * @return void */ private static function clear_old_cron() { foreach ( array( 'aisv_backup_cron', 'aisv_plugin_backup_cron', 'aisv_cleanup', 'aisv_job_continue' ) as $hook ) { wp_clear_scheduled_hook( $hook ); } } }