__( 'Database clean-up', 'admin-powerups-for-woocommerce' ) ); } /** * The checks (built fresh, so settings and filters are read as they are now). * * @since 1.0.0 * @return Checks */ public function checks(): Checks { return new Checks( $this ); } /** * A runner over a checks list. * * @since 1.0.0 * @param Checks|null $checks Checks (null: fresh). * @return Runner */ public function runner( ?Checks $checks = null ): Runner { return new Runner( $this, $checks ? $checks : $this->checks() ); } /** * {@inheritDoc} * * @return array> */ public function fields() { $checks = $this->checks(); $safe = $checks->schedulable(); return array( array( 'id' => 'age_heading', 'type' => 'heading', 'label' => __( 'What counts as old', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Rows newer than these are never counted or cleaned. Sessions, transients, WooCommerce logs and scheduled actions follow their owner’s own expiry.', 'admin-powerups-for-woocommerce' ), ), array( 'id' => 'download_log_days', 'type' => 'number', 'label' => __( 'Download logs older than (days)', 'admin-powerups-for-woocommerce' ), 'description' => __( 'At least 30.', 'admin-powerups-for-woocommerce' ), 'default' => 365, 'min' => 30, 'max' => 3650, ), array( 'id' => 'revision_days', 'type' => 'number', 'label' => __( 'Revisions older than (days)', 'admin-powerups-for-woocommerce' ), 'description' => __( 'At least 7.', 'admin-powerups-for-woocommerce' ), 'default' => 90, 'min' => 7, 'max' => 3650, ), array( 'id' => 'trash_days', 'type' => 'number', 'label' => __( 'Trashed posts older than (days)', 'admin-powerups-for-woocommerce' ), 'description' => static function () { /* translators: %s: number of days. */ return sprintf( __( '0 uses WordPress’s trash period (%s days here, at least 7). At least 7.', 'admin-powerups-for-woocommerce' ), number_format_i18n( Definitions\Helpers::wp_trash_days() ) ); }, 'default' => 0, 'min' => 0, 'max' => 3650, ), array( 'id' => 'comment_days', 'type' => 'number', 'label' => __( 'Spam and trashed comments older than (days)', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Spam by the date it was written, trash by the date it was trashed. At least 7.', 'admin-powerups-for-woocommerce' ), 'default' => 30, 'min' => 7, 'max' => 3650, ), array( 'id' => 'schedule_heading', 'type' => 'heading', 'label' => __( 'Weekly clean-up', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Runs in the background once a week and cleans only Safe checks. Each run is listed in the history on the Database clean-up tab.', 'admin-powerups-for-woocommerce' ), ), array( 'id' => 'schedule', 'type' => 'toggle', 'label' => __( 'Clean Safe items weekly', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Works only while this power-up is on.', 'admin-powerups-for-woocommerce' ), 'default' => false, ), array( 'id' => 'scheduled_checks', 'type' => 'checkboxes', 'label' => __( 'Clean these every week', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Only Safe checks can run on their own. Checks marked Slower walk whole tables, so they aren’t selected at first.', 'admin-powerups-for-woocommerce' ), 'options' => $safe, 'default' => $checks->schedule_defaults(), 'slow' => array_values( array_diff( array_keys( $safe ), $checks->schedule_defaults() ) ), ), ); } /** * {@inheritDoc} Whenever the schedule or its selection is saved, the effective selection (sent, * or the current one including defaults) keeps only checks whose object capability the saving * user has. Defaults never include checks that need one. * * @param array $input Raw input keyed by field ID. * @return array */ public function save_settings( array $input ) { if ( ( array_key_exists( 'scheduled_checks', $input ) || array_key_exists( 'schedule', $input ) ) && did_action( 'init' ) ) { $core = $this->checks()->core(); $selected = is_array( $input['scheduled_checks'] ?? null ) ? $input['scheduled_checks'] : (array) $this->setting( 'scheduled_checks', array() ); $input['scheduled_checks'] = array_values( array_filter( Str::scalars( $selected ), static function ( $id ) use ( $core ): bool { $cap = isset( $core[ $id ] ) ? $core[ $id ]->object_cap() : ''; return '' === $cap || current_user_can( $cap ); } ) ); } return parent::save_settings( $input ); } /** * {@inheritDoc} * * @return array> */ public function data() { return array_merge( parent::data(), array( Cleanup::option( 'results', Results::OPTION, __( 'Scan results', 'admin-powerups-for-woocommerce' ), __( 'The counts from the last scan and table sizes. Deleting them just means scanning again.', 'admin-powerups-for-woocommerce' ) ), array( 'id' => 'run', 'kind' => 'cache', 'label' => __( 'Stopped clean-up', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Where a clean-up stopped part-way, so it can carry on. Deleting it is the same as Discard: nothing is undone, and the history notes it. Not while a step runs.', 'admin-powerups-for-woocommerce' ), 'count' => static fn() => Runner::run() ? 1 : 0, 'summary' => static fn( $count ) => $count ? __( 'Stored', 'admin-powerups-for-woocommerce' ) : __( 'Nothing stored', 'admin-powerups-for-woocommerce' ), 'delete' => function () { // Like Discard: takes the lock (busy: try again later) and writes a "stopped" history entry. return is_wp_error( $this->runner()->discard() ) ? 1 : 0; }, ), array_merge( Cleanup::option( 'history', History::OPTION, __( 'Clean-up history', 'admin-powerups-for-woocommerce' ), __( 'What was cleaned, how many rows, who and when (never the rows themselves). Deleting it doesn’t bring anything back.', 'admin-powerups-for-woocommerce' ) ), array( 'confirm' => DevGuard::confirm_word( 'delete' ) ) // The record of permanent deletes: typed word first. ), Cleanup::option( 'schedule', Runner::SCHEDULE, __( 'Weekly clean-up progress', 'admin-powerups-for-woocommerce' ), __( 'Where this week’s background clean-up is, while it’s running.', 'admin-powerups-for-woocommerce' ) ), array( 'id' => 'lock', 'kind' => 'cache', 'label' => __( 'Clean-up lock', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Makes sure only one scan or clean step runs at a time. It expires by itself after 2 minutes; a lock held by a running step can’t be deleted.', 'admin-powerups-for-woocommerce' ), 'count' => static fn() => false === get_option( Runner::LOCK ) ? 0 : 1, 'summary' => static fn( $count ) => $count ? __( 'Stored', 'admin-powerups-for-woocommerce' ) : __( 'Nothing stored', 'admin-powerups-for-woocommerce' ), 'delete' => static function () { // Uninstall removes a held one anyway with every other `apwc_` option. // Only a lock nobody holds any more (expired) can be taken, then released. $token = Lock::acquire( Runner::LOCK, 120 ); if ( '' === $token ) { return 1; } Lock::release( Runner::LOCK, $token ); return 0; }, ), Cleanup::job_history( 'jobs', array( self::WEEKLY, self::WEEKLY_MORE ) ), Cleanup::record( 'cleaned', __( 'Deleted rows', 'admin-powerups-for-woocommerce' ), __( 'Rows you cleaned are gone for good; switching off or removing the power-up doesn’t bring them back.', 'admin-powerups-for-woocommerce' ) ), ) ); } /** * {@inheritDoc} Schedules the weekly job (it does nothing while the schedule setting is off). */ public function activate() { if ( ! function_exists( 'as_schedule_recurring_action' ) ) { return; } if ( ! Jobs::pending( self::WEEKLY ) ) { as_schedule_recurring_action( time() + HOUR_IN_SECONDS, WEEK_IN_SECONDS, self::WEEKLY, array(), Jobs::GROUP ); } } /** * {@inheritDoc} Cancels the jobs and releases the lock. A stopped run is kept, so it can * carry on after switching back on. */ public function deactivate() { if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( self::WEEKLY ); as_unschedule_all_actions( self::WEEKLY_MORE ); } Lock::clear( Runner::LOCK ); } /** * Whether a job of this hook is waiting (a running one doesn't count). * * @since 1.0.0 * @param string $hook Hook. * @return bool */ public static function pending( string $hook ): bool { return Jobs::pending( $hook ); } /** * Queue one follow-up of the weekly clean-up unless one is already waiting. * * @since 1.0.0 * @param int $delay Seconds from now (0: as soon as possible). * @return void */ public static function queue_follow_up( int $delay ): void { Jobs::queue_once( self::WEEKLY_MORE, $delay, Jobs::GROUP ); } /** * {@inheritDoc} */ public function boot() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( self::WEEKLY, array( $this, 'run_weekly' ) ); add_action( self::WEEKLY_MORE, array( $this, 'run_weekly_more' ) ); } /** * Whether the power-up is on right now (Settings' fresh read; see is_enabled_fresh()). * * @since 1.0.0 * @return bool */ public function enabled_now(): bool { return $this->is_enabled_fresh(); } /** * Action Scheduler schedules a recurring job's next run after the current one ends. If the * power-up was switched off (in this or another request) while its weekly job ran, that * next run would stay queued with no callback: while our job runs, watch what is stored and * cancel our own hooks' jobs when the power-up is off. * * @since 1.0.0 * @return void */ private function watch_next_run(): void { if ( ! has_action( 'action_scheduler_stored_action', array( $this, 'drop_stray_weekly' ) ) ) { add_action( 'action_scheduler_stored_action', array( $this, 'drop_stray_weekly' ) ); } } /** * Cancel a just-stored weekly or follow-up job while the power-up is off, or a second weekly * job after an off/on toggle (see watch_next_run()). * * @since 1.0.0 * @param int $action_id Stored action. * @return void */ public function drop_stray_weekly( $action_id ) { if ( ! class_exists( '\ActionScheduler' ) ) { return; } try { $action = \ActionScheduler::store()->fetch_action( (string) $action_id ); $hook = $action->get_hook(); if ( ! in_array( $hook, array( self::WEEKLY, self::WEEKLY_MORE ), true ) ) { return; } if ( ! $this->enabled_now() ) { \ActionScheduler::store()->cancel_action( (string) $action_id ); return; } // Switched off and on again meanwhile: activate() already scheduled a weekly job, so // the running job's next run would make two. Keep only one. if ( self::WEEKLY === $hook ) { $pending = as_get_scheduled_actions( array( 'hook' => self::WEEKLY, 'status' => \ActionScheduler_Store::STATUS_PENDING, 'per_page' => 2, ), 'ids' ); if ( array_diff( array_map( 'intval', (array) $pending ), array( (int) $action_id ) ) ) { \ActionScheduler::store()->cancel_action( (string) $action_id ); } } } catch ( \Throwable $e ) { unset( $e ); } } /** * The weekly job. * * @since 1.0.0 * @return void */ public function run_weekly() { $this->watch_next_run(); if ( ! $this->enabled_now() ) { return; // Switched off meanwhile: the next run is cancelled when Action Scheduler stores it. } $this->runner()->run_scheduled( false ); } /** * A follow-up of the weekly job. * * @since 1.0.0 * @return void */ public function run_weekly_more() { $this->watch_next_run(); if ( ! $this->enabled_now() ) { return; } $this->runner()->run_scheduled( true ); } /* ------------------------------------------------------------------ screens */ /** * {@inheritDoc} * * @param string $view View slug. */ public function render_view( $view ) { if ( function_exists( 'wp_prime_option_caches' ) ) { wp_prime_option_caches( array( Results::OPTION, Runner::RUN, History::OPTION ) ); // One query for the tab's own options. } $checks = $this->checks(); include __DIR__ . '/views/cleanup.php'; } /** * {@inheritDoc} * * @param string $view View slug. */ public function enqueue_view_assets( $view ) { wp_enqueue_style( 'apwc-database-cleanup', Assets::url( 'assets/css/database-cleanup.css' ), array( 'apwc-admin' ), Assets::ver( 'assets/css/database-cleanup.css' ) ); wp_enqueue_script( 'apwc-database-cleanup', Assets::url( 'assets/js/database-cleanup.js' ), array( 'wp-api-fetch', 'wp-i18n', 'apwc-dev-tools', 'apwc-admin' ), Assets::ver( 'assets/js/database-cleanup.js' ), true ); wp_set_script_translations( 'apwc-database-cleanup', 'admin-powerups-for-woocommerce' ); } /** * What a row shows for a check (shared by the tab and the REST answers). * * @since 1.0.0 * @param Check $check Check. * @param array|null $entry Its scan result. * @param array|null $run The site's clean run. * @return array */ public function row_facts( Check $check, ?array $entry, ?array $run ): array { $level = Checks::risk( $check ); $unavailable = $check->unavailable(); $cap = $check->object_cap(); $can_level = DevGuard::can( $this, $level ); $can_cap = '' === $cap || current_user_can( $cap ); $stale = '' === $unavailable ? Results::stale_reason( $check, $entry ) : ''; $mine = $run && $run['check'] === $check->id(); $hidden = ! $can_level && is_multisite(); // Network-wide data: not even the count. return array( 'check' => $check, 'id' => $check->id(), 'level' => $level, 'backup' => Checks::needs_backup( $check ), 'unavailable' => $unavailable, 'entry' => $entry, 'stale' => $stale, 'can' => $can_level && $can_cap, 'hidden' => $hidden, 'configurable' => $check instanceof BaseCheck && '' !== $check->setting(), 'blocked' => ! $can_level ? ( DevGuard::ADVANCED === $level && is_multisite() ? __( 'Needs a network admin', 'admin-powerups-for-woocommerce' ) : __( 'Needs a higher access level', 'admin-powerups-for-woocommerce' ) ) : ( $can_cap ? '' : __( 'Your role can’t delete these', 'admin-powerups-for-woocommerce' ) ), 'run' => $mine ? $run : null, 'other_run' => $run && ! $mine, 'threshold' => $check->threshold(), ); } /** * One check row as HTML. * * @since 1.0.0 * @param Check $check Check. * @param array|null $entry Scan result (null: read it). * @return string Markup, for REST responses. */ public function row_html( Check $check, ?array $entry = null ): string { ob_start(); $this->render_row( $check, $entry ); return (string) ob_get_clean(); } /** * Print one check row as HTML. * * @since 1.0.0 * @param Check $check Check. * @param array|null $entry Scan result (null: read it). * @return void */ public function render_row( Check $check, ?array $entry = null ) { $apwc_row = $this->row_facts( $check, $entry ?? Results::for_check( $check->id() ), Runner::run() ); include __DIR__ . '/views/check-row.php'; } /** * The groups of check rows as HTML. * * @since 1.0.0 * @param Checks|null $checks Checks (null: fresh). * @return string Markup, for REST responses. */ public function groups_html( ?Checks $checks = null ): string { ob_start(); $this->render_groups( $checks ); return (string) ob_get_clean(); } /** * Print the groups of check rows as HTML. * * @since 1.0.0 * @param Checks|null $checks Checks (null: fresh). * @return void */ public function render_groups( ?Checks $checks = null ) { $checks = $checks ? $checks : $this->checks(); $results = Results::get(); $run = Runner::run(); include __DIR__ . '/views/groups.php'; } /** * The history section as HTML. * * @since 1.0.0 * @return string Markup, for REST responses. */ public function history_html(): string { ob_start(); $this->render_history(); return (string) ob_get_clean(); } /** * Print the history section as HTML. * * @since 1.0.0 * @return void */ public function render_history() { $entries = array_slice( History::all(), 0, self::HISTORY_SHOWN ); $users = array(); foreach ( $entries as $entry ) { $users = array_merge( $users, array( $entry['user'], $entry['discarded_by'] ), $entry['users'] ); } $users = array_values( array_unique( array_filter( array_map( 'intval', $users ) ) ) ); if ( $users ) { cache_users( $users ); } include __DIR__ . '/views/history.php'; } /** * "Checked 3 hours ago" line. * * @since 1.0.0 * @param array $results Results. * @return string */ public static function checked_text( array $results ): string { if ( empty( $results['finished'] ) ) { return __( 'Not scanned yet', 'admin-powerups-for-woocommerce' ); } /* translators: %s: time since, e.g. "3 hours". */ return sprintf( __( 'Checked %s ago', 'admin-powerups-for-woocommerce' ), human_time_diff( (int) $results['finished'] ) ); } /** * Threshold as text ("Older than 90 days"), or ''. * * @since 1.0.0 * @param int|null $days Days. * @return string */ public static function threshold_text( ?int $days ): string { if ( null === $days ) { return ''; } /* translators: %s: number of days. */ return sprintf( _n( 'Older than %s day', 'Older than %s days', $days, 'admin-powerups-for-woocommerce' ), number_format_i18n( $days ) ); } /* ------------------------------------------------------------------ REST */ /** * REST routes (registered only while the power-up is on). * * @since 1.0.0 * @return void */ public function register_routes() { $ns = 'apwc/v1'; $token = array( 'type' => 'string', 'default' => '', 'pattern' => '^[a-f0-9]{0,64}$', ); register_rest_route( $ns, '/database-cleanup/scan', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_scan' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::SAFE ), 'args' => array( 'checks' => array( 'type' => 'array', 'default' => array(), 'maxItems' => 100, 'items' => array( 'type' => 'string', 'pattern' => '^' . Checks::ID_PATTERN . '$', ), ), 'token' => $token, ), ) ); register_rest_route( $ns, '/database-cleanup/checks/(?P' . Checks::ID_PATTERN . ')/sample', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_sample' ), 'permission_callback' => $this->check_permission( false ), ) ); register_rest_route( $ns, '/database-cleanup/checks/(?P' . Checks::ID_PATTERN . ')/clean', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_clean' ), 'permission_callback' => $this->check_permission( true ), 'args' => array( 'token' => $token, 'confirm' => array( 'type' => 'string', 'default' => '', ), 'backup' => array( 'type' => 'boolean', 'default' => false, ), ), ) ); register_rest_route( $ns, '/database-cleanup/run', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'rest_discard' ), 'permission_callback' => array( $this, 'discard_permission' ), ) ); } /** * Permission of the per-check routes: access to the power-up first (so others get 403 before * anything else and can't probe IDs), then the check must exist (404), then its object * capability and, for clean, its level (and the typed word for Advanced checks). * * @since 1.0.0 * @param bool $clean True for the clean route, false for the sample. * @return \Closure(\WP_REST_Request=): (bool|WP_Error) */ public function check_permission( bool $clean ): \Closure { return function ( $request = null ) use ( $clean ) { $base = DevGuard::permission( $this, DevGuard::SAFE )( $request ); if ( true !== $base ) { return $base; } $id = $request instanceof WP_REST_Request ? (string) $request['id'] : ''; $check = $this->checks()->get( $id ); if ( ! $check ) { return new WP_Error( 'apwc_dbc_unknown', __( 'There’s no such check.', 'admin-powerups-for-woocommerce' ), array( 'status' => 404 ) ); } $level = Checks::risk( $check ); if ( ! $clean ) { // The sample needs the check's own level (no typed word): network-wide rows stay with network admins. return self::level_and_cap( $this, $level, $check->object_cap() ); } return DevGuard::permission( $this, $level, $check->object_cap(), DevGuard::ADVANCED === $level ? 'delete' : '' )( $request ); }; } /** * Whether the current user has a level and an object capability ('' for none). * * @param AbstractModule $module Power-up. * @param string $level Level. * @param string $cap Object capability. * @return bool */ private static function level_and_cap( AbstractModule $module, string $level, string $cap ): bool { return DevGuard::can( $module, $level ) && ( '' === $cap || current_user_can( $cap ) ); } /** * Permission of DELETE /run: access to the power-up, then the stopped run's own level and * object capability (an unknown check counts as Advanced). Nothing to discard: allowed. * * @since 1.0.0 * @param WP_REST_Request|null $request Request. * @return bool|WP_Error */ public function discard_permission( $request = null ) { $base = DevGuard::permission( $this, DevGuard::SAFE )( $request ); if ( true !== $base ) { return $base; } $run = Runner::run(); if ( ! $run ) { return true; } $check = $this->checks()->get( (string) $run['check'] ); return $check ? self::level_and_cap( $this, Checks::risk( $check ), $check->object_cap() ) : DevGuard::can( $this, DevGuard::ADVANCED ); } /** * POST /database-cleanup/scan: one bounded scan step. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_scan( WP_REST_Request $request ) { $checks = $this->checks(); $ids = array_values( array_filter( (array) $request['checks'], 'is_string' ) ); $result = $this->runner( $checks )->scan_step( $ids, (string) $request['token'] ); if ( is_wp_error( $result ) ) { return $result; } if ( $result['done'] ) { $result['html'] = $this->groups_html( $checks ); $result['checked'] = self::checked_text( Results::get() ); } return rest_ensure_response( $result ); } /** * GET /database-cleanup/checks/{id}/sample: up to 10 example rows, masked. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_sample( WP_REST_Request $request ) { $check = $this->checks()->get( (string) $request['id'] ); if ( ! $check ) { return new WP_Error( 'apwc_dbc_unknown', __( 'There’s no such check.', 'admin-powerups-for-woocommerce' ), array( 'status' => 404 ) ); } $reason = $check->unavailable(); if ( '' !== $reason ) { return new WP_Error( 'apwc_dbc_unavailable', $reason, array( 'status' => 409 ) ); } $sample = $check->sample( 10 ); $columns = array(); foreach ( (array) ( $sample['columns'] ?? array() ) as $key => $label ) { $columns[ (string) $key ] = BaseCheck::cell( $label ); } $rows = array(); foreach ( array_slice( (array) ( $sample['rows'] ?? array() ), 0, 10 ) as $row ) { $line = array(); foreach ( array_keys( $columns ) as $key ) { $line[ $key ] = BaseCheck::cell( is_array( $row ) ? ( $row[ $key ] ?? '' ) : '' ); } $rows[] = $line; } return rest_ensure_response( array( 'columns' => $columns, 'rows' => $rows, ) ); } /** * POST /database-cleanup/checks/{id}/clean: one bounded clean step. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_clean( WP_REST_Request $request ) { $checks = $this->checks(); $check = $checks->get( (string) $request['id'] ); if ( ! $check ) { return new WP_Error( 'apwc_dbc_unknown', __( 'There’s no such check.', 'admin-powerups-for-woocommerce' ), array( 'status' => 404 ) ); } $result = $this->runner( $checks )->clean_step( $check, (string) $request['token'], (bool) $request['backup'] ); if ( is_wp_error( $result ) ) { if ( 'apwc_dbc_busy' !== $result->get_error_code() ) { $data = $result->get_error_data(); $result->add_data( array_merge( is_array( $data ) ? $data : array(), array( 'row_html' => $this->row_html( $check ) ) ) ); } return $result; } if ( $result['done'] ) { $result['row_html'] = $this->row_html( $check ); $result['history_html'] = $this->history_html(); $result['message'] = sprintf( /* translators: 1: number of rows, 2: check name. */ _n( 'Deleted %1$s row: %2$s.', 'Deleted %1$s rows: %2$s.', (int) $result['total'], 'admin-powerups-for-woocommerce' ), number_format_i18n( (int) $result['total'] ), $check->label() ); } return rest_ensure_response( $result ); } /** * DELETE /database-cleanup/run: discard the stopped run. * * @since 1.0.0 * @return \WP_REST_Response|WP_Error */ public function rest_discard() { $checks = $this->checks(); $result = $this->runner( $checks )->discard(); if ( is_wp_error( $result ) ) { return $result; } $check = '' !== $result['check'] ? $checks->get( (string) $result['check'] ) : null; return rest_ensure_response( array( 'check' => $result['check'], 'row_html' => $check ? $this->row_html( $check ) : '', 'history_html' => $this->history_html(), 'message' => __( 'Discarded. What was already deleted stays deleted.', 'admin-powerups-for-woocommerce' ), ) ); } }