modify_template_slug_on_lang_assigning()`). When this language is re-created later * (deleted by mistake?), it is re-assigned to the templates (as long as it uses the same slug than before its * deletion). * * @since 3.2 */ class PLL_FSE_Recreate_Language extends PLL_FSE_Abstract_Module implements PLL_Module_Interface { /** * Instance of `PLL_Admin_Model`. * * @var PLL_Admin_Model */ protected $model; /** * Returns the module's name. * * @since 3.2 * * @return string */ public static function get_name() { return 'fse_recreate_language'; } /** * Sub-module init. * * @since 3.2 * * @return self */ public function init() { add_action( 'pll_add_language', array( $this, 'reassign_language' ) ); return $this; } /** * Re-assigns a language to templates upon its re-creation. * * @since 3.2 * @global wpdb $wpdb * * @param array $args { * Arguments used to create the language. * * @type string $name Language name (used only for display). * @type string $slug Language code (ideally 2-letters ISO 639-1 language code). * @type string $locale WordPress locale. * @type int $rtl 1 if rtl language, 0 otherwise. * @type int $term_group Language order when displayed. * @type string $no_default_cat Optional, if set, no default category will be created for this language. * @type string $flag Optional, country code, @see flags.php. * } * @return void * * @phpstan-param array{name:string,slug:string,locale:string,rtl:int<0,1>,term_group:positive-int,no_default_cat?:string,flag?:string} $args */ public function reassign_language( $args ) { global $wpdb; if ( empty( $this->options['default_lang'] ) || $args['slug'] === $this->options['default_lang'] ) { // Default language. return; } $def_lang = $this->model->get_default_language(); $new_lang = $this->model->get_language( $args['slug'] ); if ( empty( $def_lang ) || empty( $new_lang ) ) { // Uh? return; } // Post IDs of posts in new language, post names of posts in default language. $post_names_by_post_ids_by_theme_id = $this->get_posts_with_lang_suffix( $new_lang ); if ( empty( $post_names_by_post_ids_by_theme_id ) ) { // No templates found. return; } /** * We must work "by theme" because template slugs can be identical from 1 theme to another. * When updating translation groups, `PLL_Admin_Model->set_translation_in_mass()` cannot be used because it adds * new translations instead of updating the existing ones. */ $set_language_post_ids = array(); // Used to assign the language to newly bound templates. $clean_cache_post_ids = array(); // Used to clean the posts cache. $clean_cache_term_ids = array(); // Used to clean the terms cache. $all_relationships = array(); // Used to assign the translation groups to newly bound templates. $create_groups_post_ids = array(); // Used to create new translation groups when they don't exist yet. foreach ( $post_names_by_post_ids_by_theme_id as $theme_id => $post_names_by_post_ids ) { // Get posts by their post_name and theme ID. $results = $this->get_translation_groups( $post_names_by_post_ids, $theme_id ); if ( empty( $results ) ) { // Should not happen. continue; } // Update translation groups and/or create new ones. $post_ids_by_post_names = array_flip( $post_names_by_post_ids ); // Prevents multiple uses of `array_search()`. $tt_ids = array(); // Used to update the existing translation groups. $query = array( // Used to build the query that will update the existing translation groups. "UPDATE {$wpdb->term_taxonomy} SET description = (", 'CASE term_taxonomy_id', ); foreach ( $results as $result ) { $new_lang_post_id = $post_ids_by_post_names[ $result['post_name'] ]; $set_language_post_ids[] = $new_lang_post_id; if ( empty( $result['tt_id'] ) ) { // The translation group doesn't exist yet. $clean_cache_post_ids[] = $new_lang_post_id; $clean_cache_post_ids[] = $result['post_id']; $create_groups_post_ids[] = array( $def_lang->slug => $result['post_id'], $new_lang->slug => $new_lang_post_id, ); continue; } // The translation group exist: update it. $result['translations'][ $new_lang->slug ] = $new_lang_post_id; $clean_cache_post_ids = array_merge( $clean_cache_post_ids, array_values( $result['translations'] ) ); $tt_ids[] = $result['tt_id']; $all_relationships[] = $wpdb->prepare( '(%d,%d)', $new_lang_post_id, $result['tt_id'] ); $query[] = $wpdb->prepare( 'WHEN %d THEN %s', $result['tt_id'], maybe_serialize( $result['translations'] ) ); } if ( ! empty( $tt_ids ) ) { $query[] = 'END'; $query[] = ')'; $query[] = 'WHERE term_taxonomy_id IN (' . PLL_Db_Tools::prepare_values_list( $tt_ids ) . ')'; $wpdb->query( implode( "\n", $query ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared $clean_cache_term_ids = array_merge( $clean_cache_term_ids, $tt_ids ); } } if ( ! empty( $set_language_post_ids ) ) { // Assign the language. $this->model->post->set_language_in_mass( $set_language_post_ids, $new_lang ); } if ( ! empty( $all_relationships ) ) { // Assign the translation groups. $wpdb->query( "INSERT INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id) VALUES " . implode( ',', $all_relationships ) ); } if ( ! empty( $create_groups_post_ids ) ) { // Create new translation groups. $this->model->post->set_translation_in_mass( $create_groups_post_ids ); } if ( ! empty( $clean_cache_post_ids ) ) { // Clean posts cache. clean_object_term_cache( $clean_cache_post_ids, PLL_FSE_Tools::get_template_post_types() ); } if ( ! empty( $clean_cache_term_ids ) ) { // Clean terms cache. clean_term_cache( $clean_cache_term_ids, '', false ); } } /** * Returns a list of templates that have a post_name suffixed with the given lang slug. * * @since 3.2 * * @param PLL_Language $language A language object. * @return string[][] A set of post IDs and post names, grouped by theme ID (term_taxonomy_id of the * 'wp_theme' taxonomy). Each result uses the post ID as array key and the post name * (without lang suffix) as array value. * Example of returned value: * array( * {theme_id} => array( * {post_id} => '{post_name}', * {post_id} => '{post_name}', * ), * {theme_id} => array( * {post_id} => '{post_name}', * ), * ) */ private function get_posts_with_lang_suffix( PLL_Language $language ) { global $wpdb; $post_types = PLL_Db_Tools::prepare_values_list( PLL_FSE_Tools::get_template_post_types() ); // 'dummy' => 'dummy___fr' => 'dummy\_\_\_fr' => '%\_\_\_fr' - No it's not commented code damnit. $post_slug = ( new PLL_FSE_Template_Slug( 'dummy' ) )->update_language( $language->slug ); $post_slug = str_replace( 'dummy', '%', $wpdb->esc_like( $post_slug ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $wpdb->prepare( " SELECT p.ID as post_id, p.post_name, tt.term_taxonomy_id as theme_id FROM {$wpdb->posts} AS p INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'wp_theme' WHERE p.post_type IN ($post_types) AND post_name LIKE %s", $post_slug ), ARRAY_A ); // phpcs:enable if ( empty( $results ) ) { return array(); } $language_slugs = $this->get_languages_slugs(); $post_names_by_post_ids_by_theme_id = array(); foreach ( $results as $result ) { $theme_id = $this->model->post->sanitize_int_id( $result['theme_id'] ); $post_id = $this->model->post->sanitize_int_id( $result['post_id'] ); $post_name = ( new PLL_FSE_Template_Slug( $result['post_name'], $language_slugs ) )->remove_language(); // We need the post_name without lang suffix to find the template in default language. $post_names_by_post_ids_by_theme_id[ $theme_id ][ $post_id ] = $post_name; } return $post_names_by_post_ids_by_theme_id; } /** * Returns a list of translation groups belonging to templates that use the given post_names within the given theme * ID. This will also return posts that don't have a translation group. * * @since 3.2 * * @param string[] $post_names List of post names. * @param int $theme_id `term_taxonomy_id` of the 'wp_theme' taxonomy. * @return array[] { * Array of arrays containing the translation groups and their `term_taxonomy_id`. * * @type int $post_id Post ID (post in default language). * @type string $post_name Post slug. * @type int $tt_id `term_taxonomy_id` of the 'post_translations' taxonomy.`0` means the term * doesn't exist. * @type string[] $translations List of translations. IDs are not sanitized to keep it simple (the array * can contain other things). * } * * @phpstan-return array,post_name:string,tt_id:int<0,max>,translations:array}> */ private function get_translation_groups( array $post_names, $theme_id ) { global $wpdb; $post_types = PLL_Db_Tools::prepare_values_list( PLL_FSE_Tools::get_template_post_types() ); $post_names = PLL_Db_Tools::prepare_values_list( $post_names ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $wpdb->prepare( " SELECT p.ID as post_id, p.post_name, tt.term_taxonomy_id as tt_id, tt.description as translations FROM {$wpdb->posts} AS p LEFT JOIN ( SELECT tr1.object_id, tt1.term_taxonomy_id, tt1.description FROM {$wpdb->term_relationships} AS tr1 INNER JOIN {$wpdb->term_taxonomy} AS tt1 ON tt1.term_taxonomy_id = tr1.term_taxonomy_id AND tt1.taxonomy = 'post_translations' ) AS tt ON tt.object_id = p.ID INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID AND tr.term_taxonomy_id = %d WHERE p.post_type IN ($post_types) AND post_name IN ($post_names)", $theme_id ), ARRAY_A ); // phpcs:enable return array_map( function ( $result ) { $result['post_id'] = $this->model->post->sanitize_int_id( $result['post_id'] ); $result['tt_id'] = $this->model->term->sanitize_int_id( $result['tt_id'] ); $result['translations'] = $this->format_translations( $result['translations'] ); return $result; }, (array) $results ); } /** * Formats a raw translation group from the database into an array. * * @since 3.2 * * @param mixed $translations Raw translation group from the database. * @return string[] Translation IDs are not sanitized to keep it simple * (the array can contain other things). */ private function format_translations( $translations ) { if ( empty( $translations ) ) { return array(); } $translations = maybe_unserialize( $translations ); return is_array( $translations ) ? $translations : array(); } }