admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'adparity-lite' ), 'working' => __( 'Checking…', 'adparity-lite' ), 'failed' => __( 'The batch stopped. Reload the page to resume; completed rows are saved.', 'adparity-lite' ), ) ); } private static function key(): string { return 'adparitylite_report_' . get_current_user_id(); } private static function guard(): void { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You cannot manage this store.', 'adparity-lite' ), '', array( 'response' => 403 ) ); } if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) ) { wp_die( esc_html__( 'Use a POST request for this action.', 'adparity-lite' ), '', array( 'response' => 405 ) ); } check_admin_referer( 'adparity-lite' ); } private static function ready(): bool { return ! is_multisite() && defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '11.0', '>=' ) && function_exists( 'wc_get_product' ); } private static function redirect(): void { wp_safe_redirect( admin_url( 'admin.php?page=adparity-lite' ) ); exit; } private static function locked( callable $operation ) { $key = 'adparitylite_lock_' . get_current_user_id(); $existing = (int) get_option( $key, 0 ); if ( $existing && $existing < time() - 60 ) { delete_option( $key ); } if ( ! add_option( $key, time(), '', false ) ) { throw new \DomainException( __( 'Another batch is running. Wait a moment and reload.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } try { return $operation(); } finally { delete_option( $key ); } } public function upload(): void { self::guard(); try { self::locked( static function () { if ( ! self::ready() ) { throw new \DomainException( __( 'Use WooCommerce 11.0 or later on a single-site installation.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } $file = $_FILES['feed'] ?? array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Validated as a PHP upload; never moved to public storage. if ( ! is_array( $file ) || ! isset( $file['error'], $file['size'], $file['tmp_name'], $file['name'] ) || UPLOAD_ERR_OK !== $file['error'] || ! is_string( $file['tmp_name'] ) || ! is_string( $file['name'] ) || ! is_numeric( $file['size'] ) || $file['size'] > Feed::MAX_BYTES || ! is_uploaded_file( $file['tmp_name'] ) ) { throw new \DomainException( __( 'Upload failed. Check the 5 MiB limit and your PHP upload limits.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } $extension = strtolower( pathinfo( sanitize_file_name( $file['name'] ), PATHINFO_EXTENSION ) ); $data = file_get_contents( $file['tmp_name'], false, null, 0, Feed::MAX_BYTES + 1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Private PHP upload, bounded read. if ( false === $data ) { throw new \DomainException( __( 'The uploaded file could not be read.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } $rows = Feed::parse( $data, $extension ); $mode = isset( $_POST['mode'] ) && is_string( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- All callers verify the nonce with guard() or check_ajax_referer() before reading input. if ( ! in_array( $mode, array( 'id', 'sku' ), true ) ) { throw new \DomainException( __( 'Choose an identifier mode.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } $counts = array_count_values( array_column( $rows, 'id' ) ); $report = array( 'token' => wp_generate_uuid4(), 'created' => time(), 'mode' => $mode, 'rows' => $rows, 'counts' => $counts, 'results' => array(), 'cursor' => 0, ); set_transient( self::key(), $report, self::TTL ); } ); } catch ( \DomainException $exception ) { set_transient( 'adparitylite_error_' . get_current_user_id(), $exception->getMessage(), 120 ); } self::redirect(); } private static function process( string $token ): array { return self::locked( static function () use ( $token ) { $report = get_transient( self::key() ); if ( ! self::ready() || ! $report || ! hash_equals( $report['token'], $token ) ) { throw new \DomainException( __( 'This audit expired or was replaced. Upload the feed again.', 'adparity-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception data is escaped at the admin HTML/JSON boundary. } $audit = new Audit(); $end = min( count( $report['rows'] ), $report['cursor'] + 10 ); while ( $report['cursor'] < $end ) { $row = $report['rows'][ $report['cursor'] ]; $report['results'][] = $audit->row( $row, $report['mode'], $report['counts'][ $row['id'] ] > 1 ); ++$report['cursor']; } // Preserve the original retention deadline instead of extending it on every batch. set_transient( self::key(), $report, max( 1, $report['created'] + self::TTL - time() ) ); return array( 'done' => $report['cursor'], 'total' => count( $report['rows'] ), ); } ); } private static function token(): string { return isset( $_POST['token'] ) && is_string( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- All callers verify the nonce with guard() or check_ajax_referer() before reading input. } public function ajax_step(): void { if ( ! current_user_can( self::CAP ) ) { wp_send_json_error( array( 'message' => __( 'You cannot manage this store.', 'adparity-lite' ) ), 403 ); } if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) ) { wp_send_json_error( array( 'message' => __( 'Use a POST request for this action.', 'adparity-lite' ) ), 405 ); } check_ajax_referer( 'adparity-lite', 'nonce' ); try { wp_send_json_success( self::process( self::token() ) ); } catch ( \DomainException $exception ) { wp_send_json_error( array( 'message' => $exception->getMessage() ), 409 ); } } public function step(): void { self::guard(); try { self::process( self::token() ); } catch ( \DomainException $exception ) { set_transient( 'adparitylite_error_' . get_current_user_id(), $exception->getMessage(), 120 ); } self::redirect(); } public function clear(): void { self::guard(); try { self::locked( static function () { delete_transient( self::key() ); } ); } catch ( \DomainException $exception ) { set_transient( 'adparitylite_error_' . get_current_user_id(), $exception->getMessage(), 120 ); } self::redirect(); } private static function action_form( string $action, string $label, string $button_class = 'button', string $token = '' ): void { ?>
! in_array( $filter, array( 'error', 'review', 'checked' ), true ) || $r['status'] === $filter ) ); $page = isset( $_GET['ap_page'] ) && is_scalar( $_GET['ap_page'] ) ? max( 1, absint( $_GET['ap_page'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination. ?># |
|
|