'category_batch_request_item', 'plural' => 'category_batch_request_items', 'ajax' => false, ) ); $approval_logging_enabled = defined( 'WCPAI_APPROVAL_LOGGING_ENABLED' ) ? (bool) WCPAI_APPROVAL_LOGGING_ENABLED : false; $this->enabled_debug_logging = $approval_logging_enabled || (bool) Settings::get( 'enable_debug_support', false ); // Configure inherited base properties (declared on Base_Batch_Request_Details_Table / Base_Table). $this->table_title = 'Category Batch Request Details:'; // base property (untyped in Base_Table). $this->row_id_field_name = 'category_batch_request_row_id'; // declared in base. $this->nonce_action = 'ai_content_by_parallax_cocat_action'; // declared in base. list( $row_id, $user_selected ) = $this->resolve_row_id( $category_batch_request_row_id, 'category_batch_request_row_id', 'batch_request_id' ); if ( null !== $row_id ) { $this->category_batch_request_row_id = $row_id; $this->user_selected = $user_selected; } } /** * Let the base modal helper know which batch request row is currently active. */ public function get_current_batch_request_row_id(): ?int { return $this->category_batch_request_row_id; } /** * Resolve the primary item ID from a row array. * * @param array $item Row item. * @return int */ protected function get_row_item_id( array $item ): int { return (int) ( $item['term_id'] ?? 0 ); } /** * Checkbox column. * * @param array $item Row item. * @return string */ public function column_cb( $item ) { $tid = (int) ( $item['term_id'] ?? 0 ); if ( $tid && $this->is_rejected( $tid ) ) { return ''; } return sprintf( '', esc_attr( (string) ( $item['term_id'] ?? '' ) ) ); } /** * Default column rendering. * * @param array $item Row item. * @param string $column_name Column name. */ public function column_default( $item, $column_name ) { $ellipsis = static function ( string $val ): string { $short = mb_strimwidth( $val, 0, 140, '…' ); return sprintf( '%s', esc_attr( $val ), esc_html( $short ) ); }; $tid = (int) ( $item['term_id'] ?? 0 ); if ( $tid && $this->is_rejected( $tid ) && ! in_array( $column_name, array( 'status', 'name' ), true ) ) { return ''; } switch ( $column_name ) { case 'status': if ( $tid && $this->is_imported( $tid ) ) { return esc_html( Messages::get( 'details.successfully_imported' ) ); } if ( $tid && $this->is_rejected( $tid ) ) { return sprintf( '%s%s', esc_html( Messages::get( 'details.rejected' ) ), $tid, (int) ( $this->category_batch_request_row_id ?? 0 ), esc_html( Messages::get( 'details.undo_reject' ) ) ); } return esc_html( (string) ( $item['status'] ?? '' ) ); case 'name': $tid = (int) ( $item['term_id'] ?? 0 ); $name = (string) ( $item['name'] ?? '' ); $label = '' !== $name ? $name : ( '#' . $tid ); $edit_link = $tid ? get_edit_term_link( $tid, 'product_cat' ) : ''; return $edit_link ? sprintf( '%s', esc_url( $edit_link ), esc_html( $label ) ) : esc_html( $label ); case 'current_meta_title': return $ellipsis( (string) ( $item['current']['_parallax_meta_title'] ?? '' ) ); case 'current_meta_desc': return $ellipsis( (string) ( $item['current']['_parallax_meta_desc'] ?? '' ) ); case 'current_desc': return $ellipsis( (string) ( $item['current']['description'] ?? '' ) ); case 'opt_name': return $ellipsis( (string) ( $item['optimized']['optimized_name'] ?? '' ) ); case 'opt_meta_title': return $ellipsis( (string) ( $item['optimized']['_parallax_meta_title'] ?? '' ) ); case 'opt_meta_desc': return $ellipsis( (string) ( $item['optimized']['_parallax_meta_desc'] ?? '' ) ); case 'opt_desc': return $ellipsis( (string) ( $item['optimized']['short_description'] ?? '' ) ); case 'actions': return wp_kses_post( $this->actions_html( $item ) ); case 'details': return wp_kses_post( $this->build_view_details_link( $item ) ); default: return isset( $item[ $column_name ] ) ? esc_html( (string) $item[ $column_name ] ) : ''; } } /** * Row actions cell: Approve / Reject. * * @param array $item Row item. */ protected function actions_html( array $item ): string { $tid = (int) ( $item['term_id'] ?? 0 ); if ( ! $tid ) { return ''; } if ( $this->is_imported( $tid ) ) { return $this->build_revert_action_link( $tid, $this->category_batch_request_row_id ); } $has_opt = ! empty( $item['optimized']['_parallax_meta_title'] ) || ! empty( $item['optimized']['_parallax_meta_desc'] ) || ! empty( $item['optimized']['short_description'] ) || ! empty( $item['optimized']['description'] ); if ( ! $has_opt && ( $item['status'] ?? '' ) !== 'Ready' ) { return '' . esc_html( Messages::get( 'details.processing_category' ) ) . ''; } elseif ( ! $has_opt ) { return '' . esc_html( Messages::get( 'details.optimization_failed' ) ) . ''; } $nonce = wp_create_nonce( $this->nonce_action ); $base_url = $this->get_approval_import_base_url(); $approve_url = add_query_arg( array( $this->action_param => 'approve', 'term_id' => $tid, 'category_batch_request_row_id' => (int) ( $this->category_batch_request_row_id ?? 0 ), '_wpnonce' => $nonce, ), $base_url ); $reject_url = add_query_arg( array( $this->action_param => 'reject', 'term_id' => $tid, 'category_batch_request_row_id' => (int) ( $this->category_batch_request_row_id ?? 0 ), '_wpnonce' => $nonce, ), $base_url ); $links = $this->build_single_item_action_links( 'category', $tid, $this->category_batch_request_row_id, Messages::get( 'details.import_data' ), Messages::get( 'details.reject' ), array( 'approve' => $approve_url, 'reject' => $reject_url, ) ); return implode( ' ', $links ); } /** * Provide the "Edit Category" link data for a row in the View Details panel. * * @param array $item Current row item. * @return array{url?: string, label?: string} */ protected function get_item_edit_link_data( array $item ): array { $term_id = (int) ( $item['term_id'] ?? 0 ); if ( ! $term_id ) { return array(); } $edit_url = get_edit_term_link( $term_id, 'product_cat' ); if ( empty( $edit_url ) || is_wp_error( $edit_url ) ) { return array(); } $edit_url = esc_url_raw( (string) $edit_url ); if ( '' === $edit_url ) { return array(); } return array( 'url' => $edit_url, 'label' => Messages::get( 'details.edit_category' ), ); } /** * Empty state message with optional suggestion link. */ public function no_items() { echo esc_html( Messages::get( 'details.no_categories' ) ); if ( $this->suggest_populated_batch_request_row_id ) { printf( wp_kses_post( Messages::raw( 'details.try_recent' ) ), esc_url( add_query_arg( array( 'category_batch_request_row_id' => $this->suggest_populated_batch_request_row_id, ) ) ) ); } } /** * Top-left controls in the table nav: batch request selector. */ protected function render_batch_request_select(): void { $batch_requests = $this->get_batch_requests_by_types( array( 'category', 'categories' ), array( 'id', 'batch_request_id', 'status', 'type', 'metadata', 'created_at' ) ); $this->category_batch_request_row_id = $this->choose_default_batch_request_row_id( $this->category_batch_request_row_id, $batch_requests, function ( array $meta ): array { $ids = $this->get_item_ids_from_metadata( $meta ); return $ids ? $ids : $this->extract_term_ids_from_metadata( $meta ); } ); $this->render_batch_request_select_control( $batch_requests, 'wc-parallax-category-batch-request-select', 'category_batch_request_row_id', Messages::get( 'details.batch_request_label' ), Messages::get( 'details.choose_category' ), $this->category_batch_request_row_id, $this->nonce_action ); } /** * Main data loader. */ public function prepare_items() { $this->process_actions(); $columns = $this->get_columns(); $hidden = array(); $sortable = $this->get_sortable_columns(); $this->_column_headers = array( $columns, $hidden, $sortable ); $per_page = $this->get_items_per_page( 'ai_content_by_parallax_batch_request_category_details_per_page', 20 ); $current_page = $this->get_pagenum(); // If no batch request was explicitly selected and no param is present, pick a sensible default for initial load. if ( ! $this->category_batch_request_row_id ) { $batch_requests = $this->get_batch_requests_by_types( array( 'category', 'categories' ), array( 'id', 'metadata' ) ); $this->category_batch_request_row_id = $this->choose_default_batch_request_row_id( $this->category_batch_request_row_id, $batch_requests, function ( array $meta ): array { $ids = $this->get_item_ids_from_metadata( $meta ); return $ids ? $ids : $this->extract_term_ids_from_metadata( $meta ); } ); $this->write_to_logger( 'prepare_items(): defaulted category_batch_request_row_id=' . $this->category_batch_request_row_id ); } // Load batch request row & metadata (IDs from metadata; optimized values from API). $batch_request_result = $this->category_batch_request_row_id ? BatchRequest::get( $this->category_batch_request_row_id ) : null; $this->batch_request_row = $batch_request_result ? $batch_request_result : array(); $metadata = $this->normalize_metadata( $this->batch_request_row['metadata'] ?? array() ); $batch_request_status = strtolower( (string) ( $this->batch_request_row['status'] ?? 'processing' ) ); $remote = (string) ( $this->batch_request_row['batch_request_id'] ?? '' ); $batch_request_type_raw = (string) ( $this->batch_request_row['type'] ?? 'category' ); $batch_request_type = in_array( strtolower( $batch_request_type_raw ), array( 'category', 'categories' ), true ) ? 'category' : 'product'; // Primary: item_ids from metadata (global order for API). $item_ids = $this->get_item_ids_from_metadata( $metadata ); if ( $this->category_batch_request_row_id && $item_ids ) { $this->cache_batch_request_item_ids( (int) $this->category_batch_request_row_id, $item_ids ); } $this->write_to_logger( sprintf( 'prepare_items(): category_batch_request_row_id=%s remote=%s type=%s item_ids_count=%d', $this->category_batch_request_row_id, $remote, $batch_request_type, count( $item_ids ) ) ); if ( $item_ids ) { $sample = array_slice( $item_ids, 0, 5 ); $this->write_to_logger( 'prepare_items(): item_ids sample first 5 = ' . implode( ',', $sample ) ); } // 1) Determine the set of term IDs to show. $term_ids = $item_ids ? $item_ids : $this->extract_term_ids_from_metadata( $metadata ); // Suggest a populated batch request if user selected an empty one. if ( $this->user_selected && ! $term_ids ) { $this->suggest_populated_batch_request_row_id = $this->find_populated_batch_request_row_id( array( 'category', 'categories' ), function ( array $meta ): array { $ids = $this->get_item_ids_from_metadata( $meta ); return $ids ? $ids : $this->extract_term_ids_from_metadata( $meta ); } ); } // 2) Paginate IDs. $total_items = count( $term_ids ); $offset = max( 0, ( $current_page - 1 ) * $per_page ); $page_ids = array_slice( $term_ids, $offset, $per_page ); // 3) Build rows with CURRENT values from WP. $rows = array(); foreach ( $page_ids as $term_id ) { $term = get_term( (int) $term_id, 'product_cat' ); if ( ! $term || is_wp_error( $term ) ) { continue; } $rows[ $term_id ] = array( 'term_id' => (int) $term_id, 'name' => (string) $term->name, 'is_imported' => false, 'current' => array( '_parallax_meta_title' => (string) get_term_meta( $term_id, '_parallax_meta_title', true ), '_parallax_meta_desc' => (string) get_term_meta( $term_id, '_parallax_meta_desc', true ), 'short_description' => (string) get_term_meta( $term_id, '_parallax_short_desc', true ), 'description' => (string) $term->description, ), 'optimized' => array( '_parallax_meta_title' => '', '_parallax_meta_desc' => '', 'short_description' => '', 'description' => '', 'optimized_name' => '', ), 'status' => 'Pending', ); } // 4) Pull optimized values directly from API and apply (map by global index → item_ids). if ( '' !== $remote && in_array( $batch_request_status, BatchRequestStatus::api_fetchable(), true ) && $page_ids ) { $this->hydrate_rows_from_api_paged_json( $remote, $batch_request_type, $rows, $item_ids ); } // 5) Status: ready if optimized exists OR batch request status is completed-ish. foreach ( $rows as $tid => &$r ) { $is_imported = $this->is_imported( (int) $tid ); $has_opt = ! empty( $r['optimized']['_parallax_meta_title'] ) || ! empty( $r['optimized']['_parallax_meta_desc'] ) || ! empty( $r['optimized']['short_description'] ) || ! empty( $r['optimized']['description'] ); if ( $has_opt ) { $r['status'] = 'Ready'; } elseif ( in_array( $batch_request_status, BatchRequestStatus::hydration_eligible(), true ) ) { $r['status'] = 'Ready'; } else { $r['status'] = $batch_request_status ? ucwords( str_replace( '_', ' ', $batch_request_status ) ) : 'Processing'; } // If previously imported, override display status. if ( $is_imported ) { $r['status'] = Messages::get( 'details.successfully_imported' ); } $r['is_imported'] = $is_imported; } unset( $r ); $this->items = array_values( $rows ); $this->set_pagination_args( array( 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => max( 1, (int) ceil( $total_items / $per_page ) ), ) ); } /** * Extract item IDs (term IDs) from metadata using a prioritized set of keys. * * @param array $meta Metadata array. * @return array Array of term IDs. */ protected function get_item_ids_from_metadata( array $meta ): array { $keys = array( 'item_ids', 'items', 'ids', 'term_ids', 'category_ids', 'categories', 'targets' ); foreach ( $keys as $k ) { if ( ! isset( $meta[ $k ] ) ) { continue; } $cand = $meta[ $k ]; // Special case for 'targets' key, if targets => categories then use that for IDs. if ( 'targets' === $k && is_array( $cand ) && isset( $cand['categories'] ) ) { $cand = $cand['categories']; } $ids = $this->coerce_ids( $cand ); if ( $ids ) { $this->write_to_logger( 'get_item_ids_from_metadata(): using key ' . $k . ' count=' . count( $ids ) ); return array_values( $ids ); } } return array(); } /** * Coerce input data to a list of term IDs. * * @param mixed $value Input value. * @return int[] */ protected function coerce_ids( $value ): array { $out = array(); if ( is_numeric( $value ) ) { $out[] = (int) $value; } elseif ( is_string( $value ) && preg_match_all( '/\d+/', $value, $matches ) ) { foreach ( $matches[0] as $num ) { $out[] = (int) $num; } } elseif ( is_object( $value ) ) { $out = array_merge( $out, $this->coerce_ids( get_object_vars( $value ) ) ); } elseif ( is_array( $value ) ) { foreach ( $value as $v ) { if ( is_numeric( $v ) ) { $out[] = (int) $v; continue; } if ( is_object( $v ) ) { $v = get_object_vars( $v ); } if ( is_array( $v ) ) { foreach ( array( 'id', 'term_id', 'category_id' ) as $ik ) { if ( isset( $v[ $ik ] ) && is_numeric( $v[ $ik ] ) ) { $out[] = (int) $v[ $ik ]; continue 2; } } } } } $out = array_values( array_unique( array_filter( $out, static function ( $id ): bool { return is_int( $id ) && $id > 0; } ) ) ); return $out; } /** * Fallback extractor: deep-scan metadata for term IDs that actually exist. * * @param array $meta Metadata array. * @return array Array of term IDs. */ protected function extract_term_ids_from_metadata( array $meta ): array { foreach ( array( 'term_ids', 'category_ids', 'categories', 'ids', 'targets' ) as $key ) { if ( isset( $meta[ $key ] ) ) { $ids = $this->coerce_ids( $meta[ $key ] ); if ( $ids ) { return $ids; } if ( 'targets' === $key && ! empty( $meta['targets']['categories'] ) ) { $ids = $this->coerce_ids( $meta['targets']['categories'] ); if ( $ids ) { return $ids; } } } } // Deep-scan fallback. $candidates = array(); $this->collect_ints_recursive( $meta, $candidates ); $candidates = array_values( array_unique( array_filter( $candidates, 'is_int' ) ) ); $valid = array(); foreach ( $candidates as $maybe_id ) { $term = get_term( (int) $maybe_id, 'product_cat' ); if ( $term && ! is_wp_error( $term ) && (int) $term->term_id === (int) $maybe_id ) { $valid[] = (int) $maybe_id; } if ( count( $valid ) > 1000 ) { break; } } return array_values( array_unique( $valid ) ); } /** * Normalize the API payload into our standard shape. * * @param array $payload Raw API payload. * @return array Normalized payload. */ protected function normalize_result_payload( array $payload ): array { if ( isset( $payload['result'] ) && is_array( $payload['result'] ) ) { $payload = $payload['result']; } $title = $payload['optimizedMetaTitle'] ?? ''; $desc = $payload['optimizedMetaDescription'] ?? ''; $payload_short = $payload['optimizedShortDescription'] ?? $payload['optimizedCategoryShortDescription'] ?? ''; $short = $payload_short; $primary_long = $payload['optimizedDescription'] ?? $payload['optimizedLongDescription'] ?? $payload['optimizedCategoryDescription'] ?? $payload['optimizedCategoryLongDescription'] ?? ''; $long = $primary_long; $name = $payload['optimizedName'] ?? $payload['optimizedCategoryName'] ?? ''; // Gemini SEO rating fields (may be absent on legacy payloads). $original_rating = $payload['originalRating'] ?? null; $optimized_rating = $payload['optimizedRating'] ?? null; $rating_expl = $payload['ratingImprovementExplanation'] ?? ''; // Alternative containers. $fields = isset( $payload['fields'] ) && is_array( $payload['fields'] ) ? $payload['fields'] : null; $attributes = isset( $payload['attributes'] ) && is_array( $payload['attributes'] ) ? $payload['attributes'] : null; $seo = isset( $payload['seo'] ) && is_array( $payload['seo'] ) ? $payload['seo'] : null; $data = isset( $payload['data'] ) && is_array( $payload['data'] ) ? $payload['data'] : null; $get = static function ( $src, array $keys ): string { if ( ! is_array( $src ) ) { return ''; } foreach ( $keys as $k ) { if ( isset( $src[ $k ] ) && '' !== $src[ $k ] ) { return (string) $src[ $k ]; } } return ''; }; if ( null === $title || '' === $title ) { $title = $this->first_non_empty( $get, array( $payload, $fields, $attributes, $seo, $data ), array( '_parallax_meta_title', 'title', 'meta_title', 'seo_title' ) ); } if ( null === $desc || '' === $desc ) { $desc = $this->first_non_empty( $get, array( $payload, $fields, $attributes, $seo, $data ), array( '_parallax_meta_desc', 'meta_desc', 'meta_description', 'seo_description', 'description_short', ) ); } if ( null === $short || '' === $short ) { $short = $this->first_non_empty( $get, array( $payload, $fields, $attributes, $data ), array( 'short_description', 'description_short', 'excerpt', 'summary' ) ); } if ( null === $long || '' === $long ) { $long = $this->first_non_empty( $get, array( $payload, $fields, $attributes, $data ), array( 'description', 'long_description', 'full_description', 'body', 'content' ) ); } if ( null === $name || '' === $name ) { $name = $this->first_non_empty( $get, array( $payload, $fields, $attributes, $data ), array( 'optimizedName', 'optimizedCategoryName', 'categoryName', 'name' ) ); } $title = trim( (string) $title ); $desc = trim( (string) $desc ); $short = trim( (string) $short ); $long = trim( (string) $long ); $name = trim( (string) $name ); $term_description = trim( (string) $primary_long ); if ( '' === $term_description ) { $term_description = $short; } if ( $this->enabled_debug_logging ) { if ( '' === $long ) { $this->write_to_logger( 'normalize_result_payload: long description missing; leaving empty' ); } $this->write_to_logger( 'normalize_result_payload: lens title=' . strlen( $title ) . ' desc=' . strlen( $desc ) . ' short=' . strlen( $short ) . ' long=' . strlen( $long ) . ' term_desc=' . strlen( $term_description ) . ' name=' . strlen( $name ) ); } return array( '_parallax_meta_title' => $title, '_parallax_meta_desc' => $desc, 'short_description' => $short, 'description' => $long, 'term_description' => $term_description, 'optimized_name' => $name, 'original_rating' => $this->stringify_rating( $original_rating ), 'seo_rating' => $this->stringify_rating( $optimized_rating ), 'rating_improvement_explanation' => is_scalar( $rating_expl ) ? trim( (string) $rating_expl ) : '', ); } /** * Hydrate current-page rows using the batch's item_ids to map API items → term_id. * * @param string $remote_batch_request_id Remote batch request ID. * @param string $batch_request_type Batch request type. * @param array $rows Reference to current page rows. * @param array $item_ids Full ordered list of item IDs from the batch request. */ protected function hydrate_rows_from_api_paged_json( string $remote_batch_request_id, string $batch_request_type, array &$rows, array $item_ids ): void { $logged_item_keys = false; if ( '' === $remote_batch_request_id || ! $rows ) { return; } if ( ! is_array( $item_ids ) || ! $item_ids ) { $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): missing item_ids for batch request ' . $remote_batch_request_id ); return; } for ( $p = 1; $p <= 50; $p++ ) { $raw = MiddlewareRequest::get_batch_request_paged( $remote_batch_request_id, $batch_request_type, $p ); if ( ! is_string( $raw ) || '' === $raw ) { break; } $decoded = json_decode( $raw, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) { $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): JSON decode failed p=' . $p . ' err=' . json_last_error_msg() ); break; } $items = is_array( $decoded['items'] ?? null ) ? $decoded['items'] : array(); $page = (int) ( $decoded['page'] ?? $p ); $page_size = (int) ( $decoded['pageSize'] ?? 0 ); $total = (int) ( $decoded['total'] ?? 0 ); $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): page=' . $page . ' pageSize=' . $page_size . ' total=' . $total . ' items=' . count( $items ) ); foreach ( $items as $idx => $it ) { $global_index = $this->global_index( $page, $page_size, (int) $idx ); $term_id = $item_ids[ $global_index ] ?? null; if ( ! $term_id ) { $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): no term_id for global_index=' . $global_index ); continue; } $term_id = (int) $term_id; $this->maybe_store_parallax_id( (array) $it, $term_id, $batch_request_type ); if ( ! isset( $rows[ $term_id ] ) ) { // Not on current page. continue; } if ( $this->enabled_debug_logging && ! $logged_item_keys ) { $item = is_array( $it ) ? $it : array(); $item_keys = array_keys( $item ); $fields_keys = array(); $attributes_keys = array(); if ( isset( $item['fields'] ) && is_array( $item['fields'] ) ) { $fields_keys = array_keys( $item['fields'] ); } if ( isset( $item['attributes'] ) && is_array( $item['attributes'] ) ) { $attributes_keys = array_keys( $item['attributes'] ); } $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): sample item keys=' . wp_json_encode( $item_keys ) . ' fields keys=' . wp_json_encode( $fields_keys ) . ' attributes keys=' . wp_json_encode( $attributes_keys ) ); $logged_item_keys = true; } $norm = $this->normalize_result_payload( (array) $it ); $title_length = strlen( $norm['_parallax_meta_title'] ?? '' ); $meta_desc_length = strlen( $norm['_parallax_meta_desc'] ?? '' ); $short_length = strlen( $norm['short_description'] ?? '' ); $desc_length = strlen( $norm['description'] ?? '' ); $this->write_to_logger( 'hydrate_rows_from_api_paged_json(): map global=' . $global_index . ' -> term_id=' . $term_id . ' lens T/MD/S/D=' . $title_length . '/' . $meta_desc_length . '/' . $short_length . '/' . $desc_length ); if ( $title_length || $meta_desc_length || $short_length || $desc_length ) { $rows[ $term_id ]['optimized'] = $norm; $rows[ $term_id ]['status'] = 'Ready'; // Mirror the original (pre-optimization) SEO rating into the current side so // the detail popup can render it in the Current column alongside the value. if ( '' !== ( $norm['original_rating'] ?? '' ) ) { $rows[ $term_id ]['current']['seo_rating'] = (string) $norm['original_rating']; } } } if ( $page_size > 0 && $page * $page_size >= $total ) { break; } } } /** * Approve / Reject handler for category rows. */ public function process_actions(): void { // Snapshot of relevant request parameters. $rq = array( 'method' => $this->get_request_method(), 'get' => $this->get_params_snapshot( array( $this->action_param, 'term_id', 'category_batch_request_row_id', '_wpnonce', 'action' ), 'get' ), 'post' => $this->get_params_snapshot( array( 'action', '_wpnonce', 'category_batch_request_item_ids' ), 'post' ), ); $this->write_to_logger( 'process_actions: request snapshot ' . wp_json_encode( $rq ) ); // Accept nonce from either GET (row action link) or POST (bulk). $nonce = $this->get_request_param( '_wpnonce' ); $nonce_ok = false; if ( $nonce ) { $nonce_ok = wp_verify_nonce( $nonce, $this->get_row_action_nonce_action() ) || wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ); } if ( ! $nonce_ok ) { $this->write_to_logger( 'process_actions: nonce missing/invalid - aborting' ); return; } // Prefer our custom GET param for row actions; fallback to list-table action. $action = $this->get_request_param( $this->action_param ) ?? $this->current_action(); $this->write_to_logger( 'process_actions: entry, action=' . $action ); if ( ! $action ) { return; } $category_batch_request_row_id = $this->get_request_int_param( 'category_batch_request_row_id' ) ?? $this->category_batch_request_row_id; $batch_request_result = $category_batch_request_row_id ? BatchRequest::get( $category_batch_request_row_id ) : null; $batch_request = $batch_request_result ? $batch_request_result : array(); $remote = (string) ( $batch_request['batch_request_id'] ?? '' ); $otype = (string) ( $batch_request['type'] ?? 'category' ); $otype = in_array( strtolower( $otype ), array( 'category', 'categories' ), true ) ? 'category' : 'product'; $metadata = $this->normalize_metadata( $batch_request['metadata'] ?? array() ); // Load cached item_ids if possible; fallback to metadata. $item_ids = $category_batch_request_row_id ? $this->get_cached_batch_request_item_ids( $category_batch_request_row_id ) : array(); if ( ! $item_ids ) { $item_ids = $this->get_item_ids_from_metadata( $metadata ); if ( $category_batch_request_row_id && $item_ids ) { $this->cache_batch_request_item_ids( $category_batch_request_row_id, $item_ids ); } } $position_lookup = array(); foreach ( $item_ids as $idx => $id ) { $tid = (int) $id; if ( $tid > 0 && ! isset( $position_lookup[ $tid ] ) ) { $position_lookup[ $tid ] = (int) $idx; } } $this->write_to_logger( 'process_actions: category_batch_request_row_id=' . $category_batch_request_row_id . ' remote=' . $remote . ' type=' . $otype . ' item_ids_count=' . count( $item_ids ) ); // Collect term IDs from request (row or bulk). $term_ids = array(); if ( 'bulk_approve' === $action || 'bulk_reject' === $action ) { $term_ids = $this->get_request_int_array_param( 'category_batch_request_item_ids' ); } elseif ( in_array( $action, array( 'approve', 'reject', 'revert', 'undo_reject' ), true ) ) { $selected_id = $this->get_request_int_param( 'term_id' ); $term_ids = $selected_id ? array( $selected_id ) : array(); } $term_ids = array_values( array_filter( $term_ids ) ); $this->write_to_logger( 'process_actions: term_ids=' . wp_json_encode( $term_ids ) ); // BULK REJECT: delete the whole batch request and bounce. if ( 'bulk_reject' === $action ) { $this->write_to_logger( 'process_actions: BULK REJECT triggered' ); $this->delete_whole_batch_request( $category_batch_request_row_id ); if ( $this->is_ajax_context() ) { return; } $this->redirect_back( array( $this->action_param, 'term_id', '_wpnonce', 'category_batch_request_row_id' ) ); } // SINGLE REJECT: mark item rejected, notify API only; batch and row are preserved. if ( 'reject' === $action ) { $this->write_to_logger( 'process_actions: SINGLE REJECT - notify only, batch preserved' ); foreach ( $term_ids as $tid ) { $this->set_rejected_status( (int) $tid ); } if ( $this->is_ajax_context() ) { return; } $this->redirect_back( array( $this->action_param, 'term_id', '_wpnonce', 'category_batch_request_row_id' ) ); } // UNDO REJECT: clear the rejected flag; item returns to its ready/unimported state. if ( 'undo_reject' === $action && $term_ids ) { foreach ( $term_ids as $tid ) { $this->clear_rejected_status( (int) $tid ); } if ( $this->is_ajax_context() ) { return; } $add = $category_batch_request_row_id ? array( 'category_batch_request_row_id' => $category_batch_request_row_id ) : array(); $this->redirect_back( array( $this->action_param, 'term_id', '_wpnonce' ), $add ); } // REVERT: restore backup/snapshot per selected category. if ( 'revert' === $action && $term_ids ) { foreach ( $term_ids as $tid ) { $tid = (int) $tid; $ok = $this->revert_import( $tid ); $this->write_to_logger( 'process_actions: REVERT tid=' . $tid . ' ok=' . ( $ok ? 'yes' : 'no' ) ); } if ( $this->is_ajax_context() ) { return; } $add = $category_batch_request_row_id ? array( 'category_batch_request_row_id' => $category_batch_request_row_id ) : array(); $this->redirect_back( array( $this->action_param, 'term_id', '_wpnonce' ), $add ); } // APPROVE: write optimized values per category. if ( ( 'approve' === $action || 'bulk_approve' === $action ) && '' !== $remote && $term_ids ) { // process_actions fires before any term mutations for all import entry points (UI bulk, single, CLI), // so take a one-time snapshot here to capture current state and avoid duplicate snapshots per run. $this->ensure_pre_import_snapshots( $term_ids ); $this->write_to_logger( 'process_actions: APPROVE start (remote=' . $remote . ')' ); $optimized_cache = array(); if ( 'bulk_approve' === $action ) { $optimized_cache = $this->fetch_bulk_optimized_by_terms( $remote, $otype, $item_ids, $term_ids, $position_lookup ); $this->write_to_logger( 'approve: prefetched optimized payloads count=' . count( $optimized_cache ) ); } foreach ( $term_ids as $tid ) { $tid = (int) $tid; if ( $this->is_rejected( $tid ) ) { $this->write_to_logger( 'approve: skipping rejected tid=' . $tid ); continue; } // BEFORE values. $before_title = (string) get_term_meta( $tid, '_parallax_meta_title', true ); $before_desc = (string) get_term_meta( $tid, '_parallax_meta_desc', true ); $before_short = (string) get_term_meta( $tid, '_parallax_short_desc', true ); $term_obj = get_term( $tid ); $taxonomy = ( $term_obj && ! is_wp_error( $term_obj ) && isset( $term_obj->taxonomy ) ) ? (string) $term_obj->taxonomy : 'product_cat'; $before_long = ( $term_obj && ! is_wp_error( $term_obj ) ) ? (string) $term_obj->description : ''; $this->write_to_logger( sprintf( '[wc-parallax] approve: BEFORE tid=%d len(title)=%d len(desc)=%d len(short)=%d len(long)=%d', $tid, strlen( $before_title ), strlen( $before_desc ), strlen( $before_short ), strlen( $before_long ) ) ); $this->write_to_logger( 'approve: term description len BEFORE tid=' . $tid . ' len=' . strlen( $before_long ) ); // Deterministic single fetch by position. if ( isset( $optimized_cache[ $tid ] ) ) { $opt = $optimized_cache[ $tid ]; } else { $opt = $this->fetch_single_optimized_by_position( $remote, $otype, $item_ids, $tid, $position_lookup ); } $title = isset( $opt['_parallax_meta_title'] ) ? trim( (string) $opt['_parallax_meta_title'] ) : ''; $desc = isset( $opt['_parallax_meta_desc'] ) ? trim( (string) $opt['_parallax_meta_desc'] ) : ''; $short = isset( $opt['short_description'] ) ? trim( (string) $opt['short_description'] ) : ''; $term_description = isset( $opt['term_description'] ) ? trim( (string) $opt['term_description'] ) : ''; if ( '' !== $term_description ) { $term_description = wp_kses_post( $term_description ); } $oname = isset( $opt['optimized_name'] ) ? trim( (string) $opt['optimized_name'] ) : ''; $this->write_to_logger( 'approve: FETCH tid=' . $tid . ' lens title=' . strlen( $title ) . ' desc=' . strlen( $desc ) . ' short=' . strlen( $short ) . ' term_desc=' . strlen( $term_description ) . ' name=' . strlen( $oname ) ); $this->write_to_logger( 'approve: incoming term description len tid=' . $tid . ' len=' . strlen( $term_description ) ); $wrote = false; $ret_title = null; $ret_desc = null; $ret_short = null; $safe_title = Sanitize::truncate_meta( $title, 'meta_title' ); if ( '' !== $title ) { $ret_title = update_term_meta( $tid, '_parallax_meta_title', wp_kses_post( $safe_title ) ); $wrote = $wrote || (bool) $ret_title; $this->write_to_logger( 'approve: update_term_meta(title) tid=' . $tid . ' return=' . $ret_title ); } if ( '' !== $desc ) { $ret_desc = update_term_meta( $tid, '_parallax_meta_desc', wp_kses_post( $desc ) ); $wrote = $wrote || (bool) $ret_desc; $this->write_to_logger( 'approve: update_term_meta(desc) tid=' . $tid . ' return=' . $ret_desc ); } if ( '' !== $short ) { $ret_short = update_term_meta( $tid, '_parallax_short_desc', wp_kses_post( $short ) ); $wrote = $wrote || (bool) $ret_short; $this->write_to_logger( 'approve: update_term_meta(short) tid=' . $tid . ' return=' . $ret_short ); } // Rename category if provided. if ( '' !== $oname ) { // Note: do NOT set slug unless you explicitly want to change URLs. $res_name = wp_update_term( $tid, $taxonomy, array( 'name' => wp_kses_post( $oname ) ) ); if ( is_wp_error( $res_name ) ) { $this->write_to_logger( 'approve: wp_update_term(name) ERROR tid=' . $tid . ' : ' . $res_name->get_error_message() ); } else { $wrote = true; $this->write_to_logger( 'approve: wp_update_term(name) tid=' . $tid . ' ok new_name="' . $oname . '"' ); } } if ( '' !== $term_description ) { $res = wp_update_term( $tid, $taxonomy, array( 'description' => $term_description ) ); if ( is_wp_error( $res ) ) { $this->write_to_logger( 'approve: wp_update_term(description) ERROR tid=' . $tid . ' : ' . $res->get_error_message() ); } else { $wrote = true; $this->write_to_logger( 'approve: wp_update_term(description) tid=' . $tid . ' ok' ); } } elseif ( '' === $term_description ) { $this->write_to_logger( 'approve: optimized term description missing; skip wp_update_term(description) tid=' . $tid ); } // HARD FALLBACK: if meta still missing, write directly to wp_termmeta. $after_title1 = (string) get_term_meta( $tid, '_parallax_meta_title', true ); $after_desc1 = (string) get_term_meta( $tid, '_parallax_meta_desc', true ); $after_short1 = (string) get_term_meta( $tid, '_parallax_short_desc', true ); if ( '' !== $title && '' === $after_title1 ) { $ok = $this->force_write_termmeta( $tid, '_parallax_meta_title', $safe_title ); $wrote = $wrote || $ok; } if ( '' !== $desc && '' === $after_desc1 ) { $ok = $this->force_write_termmeta( $tid, '_parallax_meta_desc', $desc ); $wrote = $wrote || $ok; } if ( '' !== $short && '' === $after_short1 ) { $ok = $this->force_write_termmeta( $tid, '_parallax_short_desc', $short ); $wrote = $wrote || $ok; } // Clear caches aggressively (term_meta + terms). if ( function_exists( 'wp_cache_delete' ) ) { wp_cache_delete( $tid, 'term_meta' ); wp_cache_delete( 'all_ids', 'terms' ); wp_cache_delete( $tid, 'terms' ); } clean_term_cache( $tid, $taxonomy ); // AFTER values. $after_title = (string) get_term_meta( $tid, '_parallax_meta_title', true ); $after_desc = (string) get_term_meta( $tid, '_parallax_meta_desc', true ); $after_short = (string) get_term_meta( $tid, '_parallax_short_desc', true ); $term_obj2 = get_term( $tid, $taxonomy ); $after_long = ( $term_obj2 && ! is_wp_error( $term_obj2 ) ) ? (string) $term_obj2->description : ''; $this->write_to_logger( sprintf( '[wc-parallax] approve: AFTER tid=%d wrote=%s len(title)=%d len(desc)=%d len(short)=%d len(long)=%d', $tid, $wrote ? 'yes' : 'no', strlen( $after_title ), strlen( $after_desc ), strlen( $after_short ), strlen( $after_long ) ) ); $this->write_to_logger( 'approve: term description len AFTER tid=' . $tid . ' len=' . strlen( $after_long ) ); $this->debug_term_meta_db( $tid, is_int( $ret_title ) ? $ret_title : null, is_int( $ret_desc ) ? $ret_desc : null, is_int( $ret_short ) ? $ret_short : null ); if ( $wrote ) { $this->set_imported_status( $tid ); } } } if ( $this->is_ajax_context() ) { return; } $add = $category_batch_request_row_id ? array( 'category_batch_request_row_id' => $category_batch_request_row_id ) : array(); $this->redirect_back( array( $this->action_param, 'term_id', '_wpnonce' ), $add ); } /** * Capture a fresh original snapshot for categories about to be imported. * Runs once per import execution to avoid duplicate snapshots across hooks. * * @param int[] $term_ids Term IDs slated for import. */ protected function ensure_pre_import_snapshots( array $term_ids ): void { static $snapshot_taken = false; if ( $snapshot_taken ) { return; } $snapshot_taken = true; foreach ( array_unique( array_map( 'absint', $term_ids ) ) as $term_id ) { if ( $term_id <= 0 ) { continue; } $term = get_term( $term_id ); $taxonomy = ( $term && ! is_wp_error( $term ) && isset( $term->taxonomy ) ) ? (string) $term->taxonomy : 'product_cat'; // Force a new snapshot immediately before import to preserve current category data. OriginalSnapshot::save_original_if_missing_term( $term_id, $taxonomy, true ); } } /** * Restore previously captured values for a category. * * @param int $term_id Term id to restore. * @return bool */ protected function revert_import( int $term_id ): bool { $term = get_term( $term_id ); $taxonomy = ( $term && ! is_wp_error( $term ) && isset( $term->taxonomy ) ) ? (string) $term->taxonomy : 'product_cat'; $restored = OriginalSnapshot::restore_term( $term_id, $taxonomy ); $this->clear_import_markers( $term_id ); return $restored; } /** * Fetch term meta for categories. * * @param int $item_id Item id. * @param string $key Meta key. * @return mixed */ protected function get_item_meta( int $item_id, string $key ) { return get_term_meta( $item_id, $key, true ); } /** * Update term meta for categories. * * @param int $item_id Item id. * @param string $key Meta key. * @param mixed $value Meta value. * @return bool */ protected function update_item_meta( int $item_id, string $key, $value ): bool { return (bool) update_term_meta( $item_id, $key, $value ); } /** * Delete term meta for categories. * * @param int $item_id Item id. * @param string $key Meta key. * @return bool */ protected function delete_item_meta( int $item_id, string $key ): bool { return (bool) delete_term_meta( $item_id, $key ); } /** * Return current WP field values for a single category term. * * @param int $item_id Term ID. * @return array */ protected function build_current_values( int $item_id ): array { $term = get_term( $item_id, 'product_cat' ); if ( ! $term || is_wp_error( $term ) ) { return array( '_parallax_meta_title' => '', '_parallax_meta_desc' => '', 'short_description' => '', 'description' => '', ); } return array( '_parallax_meta_title' => (string) get_term_meta( $item_id, '_parallax_meta_title', true ), '_parallax_meta_desc' => (string) get_term_meta( $item_id, '_parallax_meta_desc', true ), 'short_description' => (string) get_term_meta( $item_id, '_parallax_short_desc', true ), 'description' => (string) $term->description, ); } /** * Return the category term display name. * * @param int $item_id Term ID. * @return string */ protected function get_item_display_name( int $item_id ): string { $term = get_term( $item_id, 'product_cat' ); return ( $term && ! is_wp_error( $term ) ) ? (string) $term->name : ( '#' . $item_id ); } /** * Fetch all pages and build a map term_id => normalized optimized payload, * using the batch's item_ids to map global item position -> term_id. * * @param string $remote_batch_request_id Remote batch request ID. * @param string $batch_request_type Batch request type. * @param array $item_ids Full ordered list of item IDs from the batch request. * @return array Map of term_id => normalized optimized payload. */ protected function fetch_optimized_by_item_position( string $remote_batch_request_id, string $batch_request_type, array $item_ids ): array { $out = array(); if ( '' === $remote_batch_request_id || ! $item_ids ) { return $out; } for ( $p = 1; $p <= 50; $p++ ) { $raw = MiddlewareRequest::get_batch_request_paged( $remote_batch_request_id, $batch_request_type, $p ); if ( ! is_string( $raw ) || '' === $raw ) { break; } $decoded = json_decode( $raw, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) { $this->write_to_logger( 'fetch_optimized_by_item_position(): JSON decode failed p=' . $p ); break; } $items = is_array( $decoded['items'] ?? null ) ? $decoded['items'] : array(); $page = (int) ( $decoded['page'] ?? $p ); $page_size = (int) ( $decoded['pageSize'] ?? 0 ); $total = (int) ( $decoded['total'] ?? 0 ); $this->write_to_logger( 'fetch_optimized_by_item_position(): page=' . $page . ' pageSize=' . $page_size . ' total=' . $total . ' items=' . count( $items ) ); foreach ( $items as $idx => $it ) { $global = $this->global_index( $page, $page_size, (int) $idx ); if ( ! array_key_exists( $global, $item_ids ) ) { $this->write_to_logger( 'fetch_optimized_by_item_position(): no term id for global index=' . $global ); continue; } $term_id = (int) $item_ids[ $global ]; $norm = $this->normalize_result_payload( (array) $it ); if ( ( $norm['_parallax_meta_title'] ?? '' ) !== '' || ( $norm['_parallax_meta_desc'] ?? '' ) !== '' || ( $norm['short_description'] ?? '' ) !== '' || ( $norm['description'] ?? '' ) !== '' || ( $norm['term_description'] ?? '' ) !== '' || ( $norm['optimized_name'] ?? '' ) !== '' ) { $out[ $term_id ] = $norm; } else { $this->write_to_logger( 'fetch_optimized_by_item_position(): empty normalized payload for term_id=' . $term_id . ' global=' . $global ); } } if ( $page_size > 0 && $page * $page_size >= $total ) { break; } } return $out; } /** * Fetch the optimized payload for ONE term by using its position in the batch item_ids array. * * @param string $remote_batch_request_id Remote batch request ID. * @param string $batch_request_type Batch request type. * @param array $item_ids Full ordered list of item IDs from the batch request. * @param int $term_id Term ID to fetch. * @param array $position_lookup Optional precomputed position lookup map term_id => position. * @return array Normalized optimized payload for the term, or empty array on failure. */ protected function fetch_single_optimized_by_position( string $remote_batch_request_id, string $batch_request_type, array $item_ids, int $term_id, array $position_lookup = array() ): array { $item_ids = array_values( $item_ids ); if ( '' === $remote_batch_request_id || ! $item_ids ) { $this->write_to_logger( 'single_fetch: missing remote or empty item_ids' ); return array(); } if ( isset( $position_lookup[ $term_id ] ) ) { $pos = $position_lookup[ $term_id ]; } else { $pos = array_search( $term_id, $item_ids, true ); } if ( false === $pos ) { $this->write_to_logger( 'single_fetch: term_id ' . $term_id . ' not found in item_ids (' . count( $item_ids ) . ' ids)' ); return array(); } $raw1 = MiddlewareRequest::get_batch_request_paged( $remote_batch_request_id, $batch_request_type, 1 ); if ( ! is_string( $raw1 ) || '' === $raw1 ) { $this->write_to_logger( 'single_fetch: empty response on p1 for ' . $remote_batch_request_id ); return array(); } $d1 = json_decode( $raw1, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $d1 ) ) { $this->write_to_logger( 'single_fetch: JSON decode fail on p1: ' . json_last_error_msg() ); return array(); } $page_size = (int) ( $d1['pageSize'] ?? 0 ); if ( $page_size <= 0 ) { $page_size = max( 1, (int) ( is_array( $d1['items'] ?? null ) ? count( $d1['items'] ) : 10 ) ); } $total = (int) ( $d1['total'] ?? 0 ); $page = (int) floor( $pos / $page_size ) + 1; $local = (int) ( $pos % $page_size ); $this->write_to_logger( sprintf( '[wc-parallax] single_fetch: tid=%d pos=%d page=%d local=%d pageSize=%d total=%d', $term_id, $pos, $page, $local, $page_size, $total ) ); $raw = MiddlewareRequest::get_batch_request_paged( $remote_batch_request_id, $batch_request_type, $page ); if ( ! is_string( $raw ) || '' === $raw ) { $this->write_to_logger( 'single_fetch: empty response on page=' . $page ); return array(); } $decoded = json_decode( $raw, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) { $this->write_to_logger( 'single_fetch: JSON decode fail page=' . $page . ' err=' . json_last_error_msg() ); return array(); } $items = is_array( $decoded['items'] ?? null ) ? $decoded['items'] : array(); $items_count = count( $items ); $page_id = (int) ( $decoded['page'] ?? $page ); $page_size = (int) ( $decoded['pageSize'] ?? $page_size ); $this->write_to_logger( 'single_fetch: fetched page=' . $page_id . ' pageSize=' . $page_size . ' items=' . $items_count ); if ( ! array_key_exists( $local, $items ) ) { $this->write_to_logger( 'single_fetch: items missing local idx ' . $local . ' (items=' . $items_count . ')' ); return array(); } $raw_item = (array) $items[ $local ]; $dbg_title = isset( $raw_item['optimizedMetaTitle'] ) ? mb_substr( (string) $raw_item['optimizedMetaTitle'], 0, 80 ) : ''; $dbg_desc = isset( $raw_item['optimizedMetaDescription'] ) ? mb_substr( (string) $raw_item['optimizedMetaDescription'], 0, 80 ) : ''; $dbg_short = isset( $raw_item['optimizedShortDescription'] ) ? mb_substr( (string) $raw_item['optimizedShortDescription'], 0, 80 ) : ( isset( $raw_item['optimizedCategoryShortDescription'] ) ? mb_substr( (string) $raw_item['optimizedCategoryShortDescription'], 0, 80 ) : '' ); $dbg_long_desc = isset( $raw_item['optimizedDescription'] ) ? mb_substr( (string) $raw_item['optimizedDescription'], 0, 80 ) : ''; $this->write_to_logger( 'single_fetch: rawItem[' . $local . '] title="' . $dbg_title . '" desc="' . $dbg_desc . '" short="' . $dbg_short . '" long="' . $dbg_long_desc . '"' ); $norm = $this->normalize_result_payload( $raw_item ); $title_length = strlen( $norm['_parallax_meta_title'] ?? '' ); $desc_length = strlen( $norm['_parallax_meta_desc'] ?? '' ); $short_length = strlen( $norm['short_description'] ?? '' ); $desc_long_length = strlen( $norm['description'] ?? '' ); $term_desc_length = strlen( $norm['term_description'] ?? '' ); $this->write_to_logger( 'single_fetch: norm lens title=' . $title_length . ' desc=' . $desc_length . ' short=' . $short_length . ' long=' . $desc_long_length . ' term_desc=' . $term_desc_length ); return $norm; } /** * Fetch optimized payloads for a set of term IDs with minimal remote requests. * * @param string $remote_batch_request_id Remote batch request ID. * @param string $batch_request_type Batch request type. * @param int[] $item_ids Full ordered list of item IDs from the batch request. * @param int[] $target_term_ids Term IDs to fetch. * @param int[] $position_lookup Optional precomputed position lookup map term_id => position. * @return array Map of term_id => normalized optimized payload. */ protected function fetch_bulk_optimized_by_terms( string $remote_batch_request_id, string $batch_request_type, array $item_ids, array $target_term_ids, array $position_lookup ): array { return $this->fetch_bulk_optimized_by_targets( $remote_batch_request_id, $batch_request_type, $item_ids, $target_term_ids, $position_lookup ); } /** * Delete the whole batch request row (and known related items) safely. * * @param int|null $category_batch_request_row_id Batch request row ID. * @return void */ protected function delete_whole_batch_request( ?int $category_batch_request_row_id ): void { if ( ! $category_batch_request_row_id ) { return; } try { if ( method_exists( '\AIContentByParallax\Models\BatchRequest', 'delete_with_items' ) ) { BatchRequest::delete_with_items( $category_batch_request_row_id ); } elseif ( method_exists( '\AIContentByParallax\Models\BatchRequest', 'delete' ) ) { BatchRequest::delete( $category_batch_request_row_id ); } $this->write_to_logger( 'delete_whole_batch_request(): deleted category_batch_request_row_id=' . $category_batch_request_row_id ); } catch ( \Throwable $e ) { $this->write_to_logger( 'delete_whole_batch_request(): failed to delete via model: ' . $e->getMessage() ); } } /** * Build a resolver for terms by name/slug (kept for completeness - not used in main flow). * * @param int[] $term_ids Term IDs to build resolver for. * @return array Map of 'n:{normalized_name}' or 's:{normalized_slug}' => term_id. */ protected function build_resolver_for_terms( array $term_ids ): array { $resolver = array(); foreach ( $term_ids as $tid ) { $term = get_term( (int) $tid, 'product_cat' ); if ( $term && ! is_wp_error( $term ) ) { $name = (string) $term->name; $slug = (string) $term->slug; if ( '' !== $name ) { $resolver[ 'n:' . $this->norm_key( $name ) ] = (int) $tid; } if ( '' !== $slug ) { $resolver[ 's:' . $this->norm_key( $slug ) ] = (int) $tid; } } } return $resolver; } /** * Deep diagnostics for term meta writes (meta API; avoids direct DB reads). * * @param int $term_id Term ID. * @param int|null $meta_id_title Meta ID for title (if known). * @param int|null $meta_id_desc Meta ID for description (if known). * @param int|null $meta_id_short Meta ID for short description (if known). * @return void */ protected function debug_term_meta_db( int $term_id, ?int $meta_id_title, ?int $meta_id_desc, ?int $meta_id_short = null ): void { if ( 0 >= $term_id ) { $this->write_to_logger( '[debug_term_meta_db] invalid term_id=' . (string) $term_id ); return; } $keys = array( '_parallax_meta_title', '_parallax_meta_desc', '_parallax_short_desc', ); // Prime cache once; get_term_meta() will be cheap after this. update_termmeta_cache( array( $term_id ) ); $meta_rows = array(); foreach ( $keys as $key ) { $values = get_term_meta( $term_id, $key, false ); // false = all values. $meta_rows[ $key ] = array( 'count' => is_array( $values ) ? count( $values ) : 0, 'values' => $values, ); } $this->write_to_logger( '[debug_term_meta_db] term_id=' . (string) $term_id . ' meta_id_title=' . ( null === $meta_id_title ? 'null' : (string) $meta_id_title ) . ' meta_id_desc=' . ( null === $meta_id_desc ? 'null' : (string) $meta_id_desc ) . ' meta_id_short=' . ( null === $meta_id_short ? 'null' : (string) $meta_id_short ) . ' rows=' . wp_json_encode( $meta_rows ) ); } /** * Hard fallback: write term meta via WordPress meta API (avoids direct DB writes). * * @param int $term_id Term ID. * @param string $meta_key Meta key. * @param string $value Meta value. * @return bool True on success, false on failure. */ protected function force_write_termmeta( int $term_id, string $meta_key, string $value ): bool { $meta_key = (string) $meta_key; $value = wp_kses_post( $value ); if ( 0 >= $term_id || '' === $meta_key ) { $this->write_to_logger( 'force_write_termmeta: invalid args term_id=' . (string) $term_id . ' key=' . $meta_key ); return false; } /* * update_term_meta() returns: * - true if updated * - int meta_id if added (didn't exist) * - false if failure OR if the value is unchanged * * To avoid treating "unchanged" as failure, compare first. */ $current = get_term_meta( $term_id, $meta_key, true ); if ( $current === $value ) { $this->write_to_logger( 'force_write_termmeta: no-op (unchanged) term_id=' . (string) $term_id . ' key=' . $meta_key ); return true; } $result = update_term_meta( $term_id, $meta_key, $value ); if ( false === $result ) { $this->write_to_logger( 'force_write_termmeta: update_term_meta failed term_id=' . (string) $term_id . ' key=' . $meta_key ); return false; } // If $result is int, WP added a new meta row; if true, it updated an existing row. $this->write_to_logger( 'force_write_termmeta: OK term_id=' . (string) $term_id . ' key=' . $meta_key ); return true; } /** * Ensure pagination preserves the currently selected category batch request. * * @return array */ protected function get_context_query_args(): array { $args = parent::get_context_query_args(); if ( $this->category_batch_request_row_id ) { $args['category_batch_request_row_id'] = (int) $this->category_batch_request_row_id; } return $args; } /** * Optional: header buttons hooked into the modal content. */ protected function show_action_buttons(): void { echo wp_kses_post( $this->display_action_button_link( Messages::get( 'details.import_all' ), '#', array( 'class' => 'button-secondary wc-parallax-run-pending-items', 'data-item-type' => $this->item_type, 'data-batch-request-id' => (int) ( $this->category_batch_request_row_id ?? 0 ), ) ) ); echo wp_kses_post( $this->display_action_button_link( Messages::get( 'details.revert_batch' ), '#', array( 'class' => 'button-secondary wc-parallax-revert-batch', 'data-item-type' => $this->item_type, 'data-batch-request-id' => (int) ( $this->category_batch_request_row_id ?? 0 ), ) ) ); // Import All Selected - visible only when checkboxes are ticked (JS toggles display). echo ' '; echo wp_kses_post( $this->display_action_button_link( Messages::get( 'details.import_selected' ), '#import-selected', array( 'data-batch-request-id' => (int) ( $this->category_batch_request_row_id ?? 0 ), 'data-item-type' => $this->item_type, ), 'button-primary wc-parallax-import-selected' ) ); } }