__( 'Generate', 'admin-powerups-for-woocommerce' ), self::CLEANUP => __( 'Clean up', 'admin-powerups-for-woocommerce' ), ); } /** * {@inheritDoc} * * @since 1.0.0 * @return array> */ public function data() { return array_merge( parent::data(), array( array( 'id' => 'run', 'kind' => 'cache', 'label' => __( 'Run in progress', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Where a running generate or clean-up is up to. Deleting it stops the run; what it made stays tracked.', 'admin-powerups-for-woocommerce' ), 'count' => static fn() => false === get_option( Runner::OPTION ) ? 0 : 1, 'summary' => static fn( $count ) => $count ? __( 'Stored', 'admin-powerups-for-woocommerce' ) : __( 'Nothing stored', 'admin-powerups-for-woocommerce' ), 'delete' => static function () { Runner::abort(); return 0; }, ), Cleanup::option( 'batches', Batches::OPTION, __( 'Batch list', 'admin-powerups-for-woocommerce' ), __( 'Labels and counts only. Generated data stays tracked by its markers.', 'admin-powerups-for-woocommerce' ) ), array( 'id' => 'lock', 'kind' => 'cache', 'label' => __( 'Step lock', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Makes sure only one step runs at a time. Removed after each step.', '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 () { Lock::clear( Runner::LOCK ); return 0; }, ), $this->generated_item(), array_merge( Cleanup::record( 'key', __( 'Signing key', 'admin-powerups-for-woocommerce' ), __( 'A random key only this power-up uses to tag what it makes, so Clean up can tell its test data from yours. It stays with the generated data, also when the plugin is removed: without it that data could no longer be cleaned up.', 'admin-powerups-for-woocommerce' ) ), array( // uninstall.php never sweeps an option a record item names here. 'option' => Marker::KEY_OPTION, 'count' => static fn() => false === get_option( Marker::KEY_OPTION ) ? 0 : 1, 'summary' => static fn( $count ) => $count ? __( 'Stored', 'admin-powerups-for-woocommerce' ) : __( 'Nothing stored', 'admin-powerups-for-woocommerce' ), ) ), ) ); } /** * The "Generated test data" record for the Data section (count and summary share one read). * * @return array */ private function generated_item(): array { $counts = null; $read = static function () use ( &$counts ) { if ( null === $counts ) { $counts = Marker::counts(); } return $counts; }; return array_merge( Cleanup::record( 'generated', __( 'Generated test data', 'admin-powerups-for-woocommerce' ), __( 'Products, customers, orders, coupons and reviews made here, each tagged with its batch. Delete them in the Clean up tab. They stay when the power-up is off or the plugin is removed.', 'admin-powerups-for-woocommerce' ) ), array( 'count' => static function () use ( &$counts, $read ) { $counts = null; // A fresh read each time the section counts. return Marker::total( $read() ); }, 'summary' => static function ( $count ) use ( $read ) { if ( ! $count ) { return __( 'Nothing stored', 'admin-powerups-for-woocommerce' ); } $batches = count( $read() ); return sprintf( /* translators: 1: e.g. "12 objects", 2: e.g. "2 batches". */ __( '%1$s in %2$s', 'admin-powerups-for-woocommerce' ), /* translators: %s: number of objects. */ sprintf( _n( '%s object', '%s objects', $count, 'admin-powerups-for-woocommerce' ), number_format_i18n( $count ) ), /* translators: %s: number of batches. */ sprintf( _n( '%s batch', '%s batches', $batches, 'admin-powerups-for-woocommerce' ), number_format_i18n( $batches ) ) ); }, ) ); } /** * {@inheritDoc} REST routes; adopting accounts Guest accounts creates for test orders; keeping * our meta off products staff duplicate; following sales WooCommerce records on test orders. */ public function boot() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( 'apwc_guest_order_converted', array( $this, 'adopt_guest_account' ), 10, 3 ); add_filter( 'woocommerce_duplicate_product_exclude_meta', array( $this, 'exclude_meta' ) ); add_action( 'woocommerce_recorded_sales', array( Counters::class, 'recorded_sales' ) ); } /** * {@inheritDoc} Creates the signing key (Marker::key()). */ public function activate() { Marker::key(); } /** * {@inheritDoc} Stops a running generate or clean-up (what it made stays tracked). */ public function deactivate() { Runner::abort(); } /** * `woocommerce_duplicate_product_exclude_meta`: a product duplicated from a test product is * the merchant's own, so it gets none of our meta. (Signatures protect it anyway, also while * the power-up is off.) * * @since 1.0.0 * @param mixed $keys Meta keys not copied. * @return array */ public function exclude_meta( $keys ) { return array_values( array_unique( array_merge( is_array( $keys ) ? $keys : array(), array( Marker::META, Marker::SIG, Marker::SALES, Marker::USAGE ) ) ) ); } /** * `apwc_guest_order_converted`: an account Guest accounts created for a test order joins the * order's batch, so cleaning up the batch removes it too. * * @since 1.0.0 * @param mixed $user_id Customer ID. * @param mixed $order Order. * @param mixed $is_new Whether the account was created. * @return void */ public function adopt_guest_account( $user_id, $order, $is_new ) { if ( true !== $is_new || ! $order instanceof \WC_Order || (int) $user_id <= 0 ) { return; } $batch = (string) $order->get_meta( Marker::META ); $user = get_userdata( (int) $user_id ); $at = $user ? strrchr( (string) $user->user_email, '@' ) : false; // Only a signed test order (a marker written by hand isn't enough) and a test-domain account. if ( ! Marker::valid_batch( $batch ) || Marker::CREATED_VIA !== $order->get_created_via() || ! Marker::verified( 'order', (int) $order->get_id(), $batch ) || false === $at || ! Data\Words::is_reserved_domain( substr( $at, 1 ) ) || '' !== (string) get_user_meta( (int) $user_id, Marker::user_key(), true ) ) { return; } update_user_meta( (int) $user_id, Marker::user_key(), $batch ); Marker::sign( 'user', (int) $user_id, $batch ); $entry = Batches::get( $batch ); if ( $entry ) { $made = $entry['made']; $made['customers'] = (int) ( $made['customers'] ?? 0 ) + 1; Batches::update( $batch, array( 'made' => $made ) ); } } /** * Tab URL. * * @since 1.0.0 * @param string $view View slug. * @param array $args Query args. * @return string */ public static function url( string $view = self::GENERATE, array $args = array() ) { $args = array_filter( $args, static fn( $v, $k ) => '' !== $v && null !== $v && ! ( 'paged' === $k && (int) $v <= 1 ), ARRAY_FILTER_USE_BOTH ); return add_query_arg( $args, AdminPage::url( 'test-data', $view ) ); } /** * Per-run maxima (the `apwc_test_data_limits` filter can only lower the ceilings). * * @since 1.0.0 * @return array */ public static function maxima(): array { /** * Filter the most items of each type one test data run may make. Values can only lower the * hard ceilings (products 1,000, customers 5,000, orders 10,000, coupons 200, reviews 5,000). * * @since 1.0.0 * @param array $max Type => most items. */ return Limits::maxima( apply_filters( 'apwc_test_data_limits', Limits::CEILINGS ) ); } /** * {@inheritDoc} * * @since 1.0.0 * @param string $view View ID. */ public function render_view( $view ) { $run = $this->run_view(); $types = array_keys( Limits::CEILINGS ); if ( self::CLEANUP === $view ) { $counts = Marker::counts(); // Read once for the table and the count. $missing = Runner::missing( 'delete', $types ); $known = count( Batches::known( $counts ) ); $confirm = DevGuard::confirm_word( 'delete' ); $can_all = DevGuard::can( $this, DevGuard::ADVANCED ); include __DIR__ . '/views/cleanup.php'; return; } $env = Environment::type(); $production = 'production' === $env; $advanced = DevGuard::can( $this, DevGuard::ADVANCED ); $max = self::maxima(); $missing = Runner::missing( 'generate', $types ); $too_many = count( Batches::known() ) >= Batches::MAX; include __DIR__ . '/views/generate.php'; } /** * The active run, as the run box needs it (the token only for the user who started it). * * @since 1.0.0 * @return array|null */ public function run_view(): ?array { $state = Runner::state(); if ( ! $state ) { return null; } $mine = get_current_user_id() === $state['user_id']; $progress = Runner::progress( $state ); $batch = Runner::batch( $state ); $entry = Batches::get( $batch ); $user = get_userdata( (int) $state['user_id'] ); return array( 'kind' => (string) $state['kind'], 'label' => $entry && '' !== $entry['label'] ? $entry['label'] : $batch, 'phase' => Runner::phase_label( (string) $state['kind'], (string) $state['phase'] ), 'verify' => 'verify' === $state['phase'], 'total' => $progress['total'], 'done' => $progress['total'] - $progress['left'], 'stale' => Runner::is_stale( $state ), 'resume' => $mine && Runner::is_stale( $state ), 'mine' => $mine, 'token' => $mine ? (string) $state['token'] : '', 'by' => $user ? $user->display_name : '', 'count' => count( (array) $state['batches'] ), ); } /** * The batches table (Clean up tab, and after a run). * * @since 1.0.0 * @param int $paged Page. * @param array>|null $counts Marker::counts(), when already read. * @return string Markup, for REST responses. */ public function batches_html( int $paged = 0, ?array $counts = null ): string { ob_start(); $this->render_batches( $paged, $counts ); return (string) ob_get_clean(); } /** * Print the batches table (Clean up tab, and after a run). * * @since 1.0.0 * @param int $paged Page. * @param array>|null $counts Marker::counts(), when already read. * @return void */ public function render_batches( int $paged = 0, ?array $counts = null ) { if ( $paged < 1 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only paging. $paged = Paging::page( isset( $_GET['paged'] ) ? sanitize_text_field( wp_unslash( $_GET['paged'] ) ) : 1, self::PER_PAGE ); } $all = Batches::known( $counts ); $total = count( $all ); $pages = max( 1, (int) ceil( $total / self::PER_PAGE ) ); $paged = min( $paged, $pages ); $rows = array_slice( $all, ( $paged - 1 ) * self::PER_PAGE, self::PER_PAGE, true ); $missing = Runner::missing( 'delete', array_keys( Limits::CEILINGS ) ); $running = Runner::state(); include __DIR__ . '/views/batches.php'; } /** * {@inheritDoc} * * @since 1.0.0 * @param string $view View ID. */ public function enqueue_view_assets( $view ) { wp_enqueue_style( 'apwc-test-data', Assets::url( 'assets/css/test-data.css' ), array( 'apwc-admin' ), Assets::ver( 'assets/css/test-data.css' ) ); wp_enqueue_script( 'apwc-test-data', Assets::url( 'assets/js/test-data.js' ), array( 'wp-api-fetch', 'wp-i18n', 'apwc-dev-tools', 'apwc-admin' ), Assets::ver( 'assets/js/test-data.js' ), true ); wp_set_script_translations( 'apwc-test-data', 'admin-powerups-for-woocommerce' ); } /* ------------------------------------------------------------------ REST */ /** * REST routes (registered only while the power-up is on). * * @since 1.0.0 * @return void */ public function register_routes() { $count = array( 'default' => 0, 'validate_callback' => array( $this, 'valid_number' ), ); $token = array( 'type' => 'string', 'default' => '', 'validate_callback' => static fn( $value ) => is_string( $value ) && 1 === preg_match( '/^[A-Za-z0-9]{0,64}$/D', $value ), ); register_rest_route( 'apwc/v1', '/test-data/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_generate' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::REVIEW ), 'args' => array( 'products' => $count, 'customers' => $count, 'orders' => $count, 'coupons' => $count, 'reviews' => $count, 'variable_share' => array_merge( $count, array( 'default' => Limits::OPTIONS['variable_share'][2] ) ), 'guest_share' => array_merge( $count, array( 'default' => Limits::OPTIONS['guest_share'][2] ) ), 'days' => array_merge( $count, array( 'default' => Limits::OPTIONS['days'][2] ) ), 'status' => array( 'type' => 'string', 'enum' => array( 'private', 'publish' ), 'default' => 'private', ), 'label' => array( 'type' => 'string', 'default' => '', ), 'confirm' => array( 'type' => 'string', 'default' => '', ), ), ) ); register_rest_route( 'apwc/v1', '/test-data/step', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_step' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::REVIEW ), 'args' => array( 'token' => $token ), ) ); register_rest_route( 'apwc/v1', '/test-data/stop', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_stop' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::REVIEW ), 'args' => array( 'token' => $token ), ) ); register_rest_route( 'apwc/v1', '/test-data/cleanup', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_cleanup' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::REVIEW ), 'args' => array( // Not `required`: WordPress checks params before permissions, and a missing // param must not answer 400 to someone who has no access at all. 'batch' => array( 'type' => 'string', 'default' => '', 'validate_callback' => static fn( $value ) => '' === $value || Marker::valid_batch( $value ), ), ), ) ); register_rest_route( 'apwc/v1', '/test-data/cleanup-all', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_cleanup_all' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::ADVANCED, '', 'delete' ), 'args' => array( 'confirm' => array( 'type' => 'string', 'default' => '', ), ), ) ); register_rest_route( 'apwc/v1', '/test-data/batches', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_batches' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::SAFE ), 'args' => array( 'paged' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, ), ), ) ); } /** * Validate a count or percentage: a whole number as an int or digits (clamped later). * * @since 1.0.0 * @param mixed $value Value. * @return bool */ public function valid_number( $value ) { if ( is_int( $value ) ) { return true; } // '' is a cleared field: 0 for counts, the default for the others. return is_string( $value ) && 1 === preg_match( '/^\s*(?:-?\d{1,12})?\s*$/D', $value ); } /** * POST /test-data/generate: check everything, start a run. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_generate( WP_REST_Request $request ) { $production = Environment::is_production(); if ( $production ) { if ( ! DevGuard::can( $this, DevGuard::ADVANCED ) ) { return new WP_Error( 'apwc_test_data_production', is_multisite() ? __( 'This site is marked as production. Only a network (super) admin can generate test data here.', 'admin-powerups-for-woocommerce' ) : __( 'This site is marked as production. You can’t generate test data here.', 'admin-powerups-for-woocommerce' ), array( 'status' => 403 ) ); } $confirmed = DevGuard::check_confirm( $request, DevGuard::confirm_word( 'generate' ) ); if ( is_wp_error( $confirmed ) ) { return $confirmed; } } $targets = Limits::clamp( array( 'products' => $request['products'], 'customers' => $request['customers'], 'orders' => $request['orders'], 'coupons' => $request['coupons'], 'reviews' => $request['reviews'], ), self::maxima() ); if ( ! array_sum( $targets ) ) { return new WP_Error( 'apwc_test_data_nothing', __( 'Choose how many of at least one kind to make.', 'admin-powerups-for-woocommerce' ), array( 'status' => 400 ) ); } if ( ! $targets['products'] && ( $targets['orders'] || $targets['reviews'] ) && ! Marker::pool( 'products', 'none', 1 ) ) { return new WP_Error( 'apwc_test_data_no_products', __( 'Orders and reviews need test products. Add products to this run, or keep an earlier batch that has some.', 'admin-powerups-for-woocommerce' ), array( 'status' => 400 ) ); } $missing = Runner::missing( 'generate', array_keys( array_filter( $targets ) ) ); if ( $missing ) { return self::forbidden( $missing ); } $lock = Lock::acquire( Runner::LOCK ); if ( '' === $lock ) { return self::busy(); } try { if ( Runner::state() ) { return self::running(); } if ( count( Batches::known() ) >= Batches::MAX ) { return new WP_Error( 'apwc_test_data_too_many', /* translators: %d: most batches. */ sprintf( __( 'There are already %d batches. Delete some in the Clean up tab first.', 'admin-powerups-for-woocommerce' ), Batches::MAX ), array( 'status' => 409 ) ); } $label = mb_substr( trim( sanitize_text_field( (string) $request['label'] ) ), 0, Limits::LABEL_MAX ); if ( '' === $label ) { /* translators: %s: today's date. */ $label = sprintf( __( 'Test data, %s', 'admin-powerups-for-woocommerce' ), wp_date( (string) get_option( 'date_format' ) ) ); } $options = array( 'label' => $label, 'variable_share' => Limits::option( 'variable_share', $request['variable_share'] ), 'guest_share' => Limits::option( 'guest_share', $request['guest_share'] ), 'days' => Limits::option( 'days', $request['days'] ), 'status' => 'publish' === $request['status'] ? 'publish' : 'private', ); $state = Runner::begin_generate( Marker::new_batch(), $targets, $options, $production ); } finally { Lock::release( Runner::LOCK, $lock ); } return rest_ensure_response( $this->running_response( $state ) ); } /** * POST /test-data/step: one bounded step of the active run. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_step( WP_REST_Request $request ) { $token = (string) $request['token']; $state = Runner::state(); if ( ! $state || ! Runner::is_current( $token ) ) { return self::replaced(); } $checked = $this->check_run( $state ); if ( is_wp_error( $checked ) ) { return $checked; } $lock = Lock::acquire( Runner::LOCK ); if ( '' === $lock ) { return self::busy(); } try { // Checked again under the lock: the run may have been stopped or replaced meanwhile. $state = Runner::state(); if ( ! $state || ! hash_equals( $state['token'], $token ) ) { return self::replaced(); } try { // Inside the try: whatever start() or the step throws, end() puts everything back. Guard::start( $state['kind'], Runner::batch( $state ), $state['production'] || Environment::is_production() ); $state = Runner::step( $state ); } finally { Guard::end(); } } finally { Lock::release( Runner::LOCK, $lock ); } if ( 'done' !== $state['phase'] ) { return rest_ensure_response( $this->running_response( $state ) ); } return rest_ensure_response( $this->done_response( $state ) ); } /** * POST /test-data/stop: stop the active run. Its owner can stop it any time (with the token); * anyone with access can stop a paused one. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_stop( WP_REST_Request $request ) { $state = Runner::state(); if ( $state ) { $token = (string) $request['token']; $owner = '' !== $token && Runner::is_current( $token ) && get_current_user_id() === (int) $state['user_id']; if ( ! $owner && ! Runner::is_stale( $state ) ) { return self::running(); } $lock = Lock::acquire( Runner::LOCK ); if ( '' === $lock ) { return self::busy(); } try { $state = Runner::state(); if ( $state ) { Runner::stop( $state ); } } finally { Lock::release( Runner::LOCK, $lock ); } } return rest_ensure_response( array( 'stopped' => true, 'message' => __( 'Stopped. What was made so far is listed in the Clean up tab.', 'admin-powerups-for-woocommerce' ), 'html' => $this->batches_html( 1 ), ) ); } /** * POST /test-data/cleanup: start deleting one batch. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_cleanup( WP_REST_Request $request ) { $batch = (string) $request['batch']; if ( ! Marker::valid_batch( $batch ) ) { return new WP_Error( 'apwc_test_data_bad_batch', __( 'Choose a batch.', 'admin-powerups-for-woocommerce' ), array( 'status' => 400 ) ); } $counts = Marker::counts(); if ( ! isset( $counts[ $batch ] ) && ! Batches::get( $batch ) ) { return new WP_Error( 'apwc_test_data_unknown_batch', __( 'That batch isn’t here any more.', 'admin-powerups-for-woocommerce' ), array( 'status' => 404 ) ); } return $this->start_cleanup( array( $batch ), $counts ); } /** * POST /test-data/cleanup-all: start deleting every batch (Advanced, typed word checked by the * permission callback). * * @since 1.0.0 * @return \WP_REST_Response|WP_Error */ public function rest_cleanup_all() { $counts = Marker::counts(); $batches = array_keys( Batches::known( $counts ) ); if ( ! $batches ) { return new WP_Error( 'apwc_test_data_empty', __( 'There’s no test data to delete.', 'admin-powerups-for-woocommerce' ), array( 'status' => 400 ) ); } return $this->start_cleanup( array_map( 'strval', $batches ), $counts ); } /** * GET /test-data/batches. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response */ public function rest_batches( WP_REST_Request $request ) { return rest_ensure_response( array( 'html' => $this->batches_html( Paging::page( $request['paged'], self::PER_PAGE ) ) ) ); } /** * Start a clean-up run after the checks. * * @param string[] $batches Batch IDs. * @param array> $counts Marker::counts(). * @return \WP_REST_Response|WP_Error */ private function start_cleanup( array $batches, array $counts ) { $types = array(); foreach ( $batches as $batch ) { $types = array_merge( $types, Runner::types_of( $counts[ $batch ] ?? array() ) ); } $missing = Runner::missing( 'delete', array_unique( $types ) ); if ( $missing ) { return self::forbidden( $missing ); } $lock = Lock::acquire( Runner::LOCK ); if ( '' === $lock ) { return self::busy(); } try { if ( Runner::state() ) { return self::running(); } $state = Runner::begin_cleanup( $batches, $counts, Environment::is_production() ); } finally { Lock::release( Runner::LOCK, $lock ); } return rest_ensure_response( $this->running_response( $state ) ); } /** * Checks every step repeats: the starting user, Advanced on production, object capabilities. * * @param array $state State. * @phpstan-param RunState $state * @return true|WP_Error */ private function check_run( array $state ) { if ( get_current_user_id() !== $state['user_id'] ) { return new WP_Error( 'apwc_test_data_not_yours', __( 'Another admin started this run. Only they can continue it.', 'admin-powerups-for-woocommerce' ), array( 'status' => 403 ) ); } if ( 'generate' === $state['kind'] && ( $state['production'] || Environment::is_production() ) && ! DevGuard::can( $this, DevGuard::ADVANCED ) ) { return new WP_Error( 'apwc_test_data_production', __( 'This site is marked as production. You can’t generate test data here.', 'admin-powerups-for-woocommerce' ), array( 'status' => 403 ) ); } $missing = Runner::missing( 'generate' === $state['kind'] ? 'generate' : 'delete', $state['types'] ); return $missing ? self::forbidden( $missing ) : true; } /** * Response while a run goes on. * * @param array $state State. * @phpstan-param RunState $state * @return array */ private function running_response( array $state ): array { $progress = Runner::progress( $state ); $entry = 'generate' === $state['kind'] ? Batches::get( Runner::batch( $state ) ) : null; return array( 'done' => false, 'kind' => $state['kind'], 'token' => $state['token'], 'batch' => Runner::batch( $state ), 'label' => $entry ? (string) $entry['label'] : '', 'batches' => count( $state['batches'] ), 'phase' => $state['phase'], 'phase_label' => Runner::phase_label( $state['kind'], $state['phase'] ), 'made' => 'generate' === $state['kind'] ? ( $state['made'] ?? array() ) : ( $state['deleted'] ?? array() ), 'total' => $progress['total'], 'left' => $progress['left'], ); } /** * Response when a run finished. * * @param array $state State. * @phpstan-param RunState $state * @return array */ private function done_response( array $state ): array { if ( 'generate' === $state['kind'] ) { $counts = $state['counts'] ?? array(); $made = (int) array_sum( $counts ); $failed = (int) ( $state['made']['failed'] ?? 0 ); $message = sprintf( /* translators: %s: number of objects. */ _n( 'Done. Made %s object. Find it in the Clean up tab.', 'Done. Made %s objects. Find them in the Clean up tab.', $made, 'admin-powerups-for-woocommerce' ), number_format_i18n( $made ) ); if ( $failed ) { $message .= ' ' . sprintf( /* translators: %s: number of items. */ _n( '%s item couldn’t be made.', '%s items couldn’t be made.', $failed, 'admin-powerups-for-woocommerce' ), number_format_i18n( $failed ) ); } $kept = array(); } else { $counts = $state['deleted'] ?? array(); $kept = $state['kept'] ?? array(); $deleted = (int) array_sum( $counts ); $left = 0; foreach ( $kept as $reasons ) { $left += (int) array_sum( $reasons ); } /* translators: %s: number of objects. */ $message = sprintf( _n( 'Deleted %s object.', 'Deleted %s objects.', $deleted, 'admin-powerups-for-woocommerce' ), number_format_i18n( $deleted ) ); if ( $left ) { $message .= ' ' . sprintf( /* translators: %s: number of objects. */ _n( 'Kept %s that real data depends on or that can’t be confirmed as test data; see the notes in the list.', 'Kept %s that real data depends on or that can’t be confirmed as test data; see the notes in the list.', $left, 'admin-powerups-for-woocommerce' ), number_format_i18n( $left ) ); } if ( is_multisite() && ! empty( $counts['customers'] ) && ! is_super_admin() ) { $message .= ' ' . __( 'Accounts were removed from this site; a network admin can delete them under Network Admin → Users (emails end in the test domain).', 'admin-powerups-for-woocommerce' ); } } return array( 'done' => true, 'kind' => $state['kind'], 'batch' => Runner::batch( $state ), 'counts' => $counts, 'kept' => $kept, 'message' => $message, 'html' => $this->batches_html( 1 ), ); } /** * 403 naming the type the user can't work with. * * @param array $missing Type => capability. * @return WP_Error */ private static function forbidden( array $missing ): WP_Error { $labels = self::type_labels(); $type = (string) array_key_first( $missing ); return new WP_Error( 'apwc_test_data_forbidden', /* translators: %s: kind of data, e.g. "customers". */ sprintf( __( 'You don’t have permission to work with %s here.', 'admin-powerups-for-woocommerce' ), $labels[ $type ] ?? $type ), array( 'status' => 403, 'type' => $type, ) ); } /** * Lower-case names of the types, for messages. * * @since 1.0.0 * @return array */ public static function type_labels(): array { return array( 'products' => __( 'products', 'admin-powerups-for-woocommerce' ), 'customers' => __( 'customers', 'admin-powerups-for-woocommerce' ), 'orders' => __( 'orders', 'admin-powerups-for-woocommerce' ), 'coupons' => __( 'coupons', 'admin-powerups-for-woocommerce' ), 'reviews' => __( 'reviews', 'admin-powerups-for-woocommerce' ), ); } /** * 409: a step is running. * * @return WP_Error */ private static function busy(): WP_Error { return new WP_Error( 'apwc_test_data_busy', __( 'The previous step is still running.', 'admin-powerups-for-woocommerce' ), array( 'status' => 409 ) ); } /** * 409: another run is active. * * @return WP_Error */ private static function running(): WP_Error { return new WP_Error( 'apwc_test_data_running', __( 'Another run is in progress. Wait for it to finish, or stop it.', 'admin-powerups-for-woocommerce' ), array( 'status' => 409 ) ); } /** * 409: the run was replaced, stopped or finished. * * @return WP_Error */ private static function replaced(): WP_Error { return new WP_Error( 'apwc_test_data_replaced', __( 'This run was stopped or has finished. Reload the page.', 'admin-powerups-for-woocommerce' ), array( 'status' => 409 ) ); } }