prefix . 'hyx_bd_translations'; } public static function install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table_name = self::table_name(); $legacy_table = $wpdb->prefix . 'btranslate_translations'; $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks the plugin-owned table during activation. $legacy_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $legacy_table ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks for the legacy plugin-owned table during activation. if ( $legacy_exists === $legacy_table && $table_exists !== $table_name ) { $wpdb->query( $wpdb->prepare( 'RENAME TABLE %i TO %i', $legacy_table, $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Migrates persisted translations to the renamed table. } $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table_name} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, identity_key char(64) NOT NULL, source_language varchar(20) NOT NULL, target_language varchar(20) NOT NULL, field_context varchar(191) NOT NULL, source_fingerprint char(64) NOT NULL, translated_value longtext NULL, status varchar(20) NOT NULL, created_at datetime NOT NULL, updated_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY identity_key (identity_key), KEY target_language (target_language), KEY source_fingerprint (source_fingerprint) ) {$charset_collate};"; dbDelta( $sql ); } public function find_valid( $identity_key ) { global $wpdb; return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache. $wpdb->prepare( "SELECT * FROM %i WHERE identity_key = %s AND status = 'complete' LIMIT 1", self::table_name(), $identity_key ), ARRAY_A ); } public function save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, $status ) { global $wpdb; $now = current_time( 'mysql', true ); return $wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Writes to the plugin-owned persistent translation cache. self::table_name(), array( 'identity_key' => $identity_key, 'source_language' => $source_language, 'target_language' => $target_language, 'field_context' => $field_context, 'source_fingerprint' => $source_fingerprint, 'translated_value' => $translated_value, 'status' => $status, 'created_at' => $now, 'updated_at' => $now, ), array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ); } public function clear() { global $wpdb; return $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This explicitly clears the plugin's persistent translation cache. $wpdb->prepare( 'DELETE FROM %i', self::table_name() ) ); } public function get_post_language_status( $post_id, $target_language ) { global $wpdb; return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache. $wpdb->prepare( "SELECT COUNT(*) AS translated_fields, MAX(updated_at) AS last_translated_at FROM %i WHERE target_language = %s AND status = 'complete' AND field_context LIKE %s", self::table_name(), $target_language, 'post:' . absint( $post_id ) . ':%' ), ARRAY_A ); } public function get_completed_item_counts( $target_languages, $post_ids, $term_ids, $since = '' ) { $target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) ); $post_ids = array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) ); $term_ids = array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) ); if ( empty( $target_languages ) ) { return array( 'posts' => 0, 'terms' => 0, ); } $language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) ); return array( 'posts' => $this->get_completed_item_count( 'post', $post_ids, $target_languages, $language_placeholders, $since ), 'terms' => $this->get_completed_item_count( 'term', $term_ids, $target_languages, $language_placeholders, $since ), ); } public function get_failed_items( $target_languages, $post_ids, $term_ids, $since = '' ) { global $wpdb; $target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) ); $item_contexts = array_merge( array_map( static function ( $post_id ) { return 'post:' . absint( $post_id ); }, (array) $post_ids ), array_map( static function ( $term_id ) { return 'term:' . absint( $term_id ); }, (array) $term_ids ) ); if ( empty( $target_languages ) || empty( $item_contexts ) ) { return array(); } $language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) ); $context_placeholders = implode( ',', array_fill( 0, count( $item_contexts ), '%s' ) ); $query_args = array_merge( $target_languages, $item_contexts ); if ( '' !== $since ) { $query_args[] = $since; } $since_clause = '' === $since ? '' : ' AND updated_at >= %s'; return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reads task failures from the plugin-owned translation table. $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN clauses contain only generated %s placeholders; the optional clause is fixed SQL. "SELECT target_language, SUBSTRING_INDEX(field_context, ':', 2) AS item_context, MAX(updated_at) AS failed_at FROM %i WHERE status = 'failed' AND target_language IN ({$language_placeholders}) AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause} GROUP BY target_language, item_context ORDER BY failed_at DESC", array_merge( array( self::table_name() ), $query_args ) ), ARRAY_A ); } private function get_completed_item_count( $item_type, $item_ids, $target_languages, $language_placeholders, $since ) { global $wpdb; if ( empty( $item_ids ) ) { return 0; } $contexts = array_map( static function ( $item_id ) use ( $item_type ) { return $item_type . ':' . $item_id; }, $item_ids ); $context_placeholders = implode( ',', array_fill( 0, count( $contexts ), '%s' ) ); $query_args = array_merge( $target_languages, $contexts ); if ( '' !== $since ) { $query_args[] = $since; } $since_clause = '' === $since ? '' : ' AND updated_at >= %s'; return (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache. $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN clauses contain only generated %s placeholders; the optional clause is a fixed SQL fragment. "SELECT COUNT(DISTINCT CONCAT(target_language, '|', SUBSTRING_INDEX(field_context, ':', 2))) FROM %i WHERE status = 'complete' AND target_language IN ({$language_placeholders}) AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause}", array_merge( array( self::table_name() ), $query_args ) ) ); } }