HEX
Server: Apache
System: Linux ebs-147252 5.10.0-30-amd64 #1 SMP Debian 5.10.218-1 (2024-06-01) x86_64
User: jumuhardware (65594)
PHP: 8.2.20
Disabled: link,symlink,passthru,exec,system,shell_exec,proc_open,popen,pcntl_exec,socket_bind,stream_socket_server,pcntl_fork,pcntl_rfork
Upload Files
File: /home/wwwroot/jumuhardware/wwwroot/wp-content/plugins/wpml-to-polylang/wpml-to-polylang.php
<?php
/**
 * WPML to Polylang
 *
 * @package              wpml-to-polylang
 * @author               WP SYNTEX
 * @license              GPL-3.0-or-later
 *
 * @wordpress-plugin
 * Plugin name:          WPML to Polylang
 * Plugin URI:           https://polylang.pro
 * Description:          Import multilingual data from WPML into Polylang
 * Version:              0.4
 * Requires at least:    4.9
 * Requires PHP:         5.6
 * Author:               WP SYNTEX
 * Author URI:           https://polylang.pro
 * Text Domain:          wpml-to-polylang
 * Domain Path:          /languages
 * License:              GPL v3 or later
 * License URI:          https://www.gnu.org/licenses/gpl-3.0.txt
 *
 * Copyright 2013-2020 Frédéric Demarle
 * Copyright 2021 WP SYNTEX
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * A class to manage migration from WPML to Polylang.
 *
 * @since 0.1
 *
 * @see http://wpml.org/documentation/support/wpml-tables/
 */
class WPML_To_Polylang {

	/**
	 * @var PLL_Admin_Model
	 */
	protected $model;

	/**
	 * WPML settings.
	 *
	 * @var array
	 */
	protected $icl_settings;

	/**
	 * Constructor
	 *
	 * @since 0.1
	 */
	public function __construct() {
		// Adds the link to the languages panel in the WordPress admin menu.
		add_action( 'admin_menu', array( $this, 'add_menus' ) );

		if ( is_admin() && isset( $_GET['page'] ) && 'wpml-importer' === $_GET['page'] && class_exists( 'PLL_Admin_Model' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			add_filter( 'pll_model', array( $this, 'pll_model' ) );
			$this->icl_settings = get_option( 'icl_sitepress_settings' );
		}
	}

	/**
	 * Uses PLL_Admin_Model to be able to create languages.
	 *
	 * @since 0.2
	 *
	 * @return string
	 */
	public function pll_model() {
		return 'PLL_Admin_Model';
	}

	/**
	 * Adds the link to the languages panel in the WordPress admin menu.
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function add_menus() {
		load_plugin_textdomain( 'wpml-to-polylang', false, basename( dirname( __FILE__ ) ) . '/languages' ); // Plugin i18n.
		$title = __( 'WPML importer', 'wpml-to-polylang' );
		add_submenu_page( 'tools.php', $title, $title, 'manage_options', 'wpml-importer', array( $this, 'tools_page' ) );
	}

	/**
	 * Displays the import page.
	 * Processes the import action.
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function tools_page() {
		?>
		<div class="wrap">
			<h2> <?php esc_html_e( 'WPML Importer', 'wpml-to-polylang' ); ?></h2>
			<?php

			if ( isset( $_POST['pll_action'] ) && 'import' === $_POST['pll_action'] ) {
				check_admin_referer( 'wpml-importer', '_wpnonce_wpml-importer' );
				$this->import();
				?>
				<p><?php esc_html_e( 'Import from WPML to Polylang should have been successul!', 'wpml-to-polylang' ); ?></p>
				<?php
			} else {
				global $sitepress, $wp_version;

				$min_wp_version  = '4.9';
				$min_pll_version = '2.8';
				$checks = array();

				$checks[] = array(
					/* translators: %s is the WordPress version */
					sprintf( __( 'You are using WordPress %s or later', 'wpml-to-polylang' ), $min_wp_version ),
					version_compare( $wp_version, $min_wp_version, '>=' ) ? 1 : 0,
				);

				$checks[] = array(
					__( 'WPML is installed on this website', 'wpml-to-polylang' ),
					false !== get_option( 'icl_sitepress_settings' ) ? 1 : 0,
				);

				$checks[] = array(
					__( 'WPML is deactivated', 'wpml-to-polylang' ),
					empty( $sitepress ) ? 1 : 0,
				);

				$checks[] = array(
					/* translators: %s is the Polylang version */
					sprintf( __( 'Polylang %s or later is activated', 'wpml-to-polylang' ), $min_pll_version ),
					defined( 'POLYLANG_VERSION' ) && version_compare( POLYLANG_VERSION, $min_pll_version, '>=' ) ? 1 : 0,
				);

				if ( $checks[3][1] ) {
					$this->model = $GLOBALS['polylang']->model;

					$checks[] = array(
						__( 'No language has been created with Polylang', 'wpml-to-polylang' ),
						$this->model->get_languages_list() ? 0 : 1,
					);
				}

				// html form.
				?>
				<div class="form-wrap">
					<form id="import" method="post" action="admin.php?page=wpml-importer">
					<input type="hidden" name="pll_action" value="import" />
					<?php wp_nonce_field( 'wpml-importer', '_wpnonce_wpml-importer' ); ?>
					<table class="form-table">
					<?php
					foreach ( $checks as $check ) {
						printf(
							'<tr><th style="width:300px">%s</th><td style="color:%s">%s</td></tr>',
							esc_html( $check[0] ),
							$check[1] ? 'green' : 'red',
							esc_html( $check[1] ? __( 'OK', 'wpml-to-polylang' ) : __( 'KO', 'wpml-to-polylang' ) )
						);

						if ( ! $check[1] ) {
							$deactivated = true;
						}
					}
					?>
					</table>
					<?php
					$attr = empty( $deactivated ) ? array() : array( 'disabled' => 'disabled' );
					submit_button( __( 'Import', 'wpml-to-polylang' ), 'primary', 'submit', true, $attr ); // Since WP 3.1.
					?>
					</form>
				</div><!-- form-wrap -->
				<?php
			}
			?>
		</div><!-- wrap -->
		<?php
	}

	/**
	 * Dispatches the different import steps.
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function import() {
		global $wpdb;

		set_time_limit( 0 );

		$this->model = $GLOBALS['polylang']->model;
		$this->add_languages();

		// Get WPML translations.
		$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}icl_translations" );

		/*
		 * Get the correspondance between term_taxonomy_id and term_id ( duplicates are discarded ).
		 * Required as WPML stores term_taxonomy_id in icl_translations while Polylang translates term_id.
		 * Thanks to Nickness. See http://wordpress.org/support/topic/wpml-languages-import-is-broken-with-last-polylang-update-15-16
		 */
		$_taxonomies = array( 'category', 'post_tag', 'nav_menu' );
		if ( ! empty( $this->icl_settings['taxonomies_sync_option'] ) ) {
			$_taxonomies = array_merge( $_taxonomies, array_keys( array_filter( $this->icl_settings['taxonomies_sync_option'] ) ) );
		}

		$taxonomies = array();
		foreach ( $_taxonomies as $tax ) {
			$taxonomies[] = $wpdb->prepare( '%s', $tax );
		}
		$term_ids = $wpdb->get_results( "SELECT term_taxonomy_id, term_id FROM {$wpdb->term_taxonomy} WHERE taxonomy IN (" . implode( ', ', $taxonomies ) . ')', OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Migrate languages and translations.
		$this->process_post_term_languages( $results, $term_ids );
		$this->process_post_term_translations( $results, $term_ids );

		// In some cases, there is no language assigned in icl_translations table, but WPML displays the default language anyway.
		$nolang = $this->model->get_objects_with_no_lang();
		if ( $nolang ) {
			if ( ! empty( $nolang['posts'] ) ) {
				$this->model->set_language_in_mass( 'post', $nolang['posts'], $this->icl_settings['default_language'] );
			}
			if ( ! empty( $nolang['terms'] ) ) {
				$this->model->set_language_in_mass( 'term', $nolang['terms'], $this->icl_settings['default_language'] );
			}
		}

		// Migrate strings translations and options.
		$this->process_strings_translations();
		$this->process_options();

		// Make sure that the page on front and page for posts are correctly included in the cached language objects.
		if ( 'page' === get_option( 'show_on_front' ) ) {
			wp_cache_delete( get_option( 'page_on_front' ), 'language_relationships' );
			wp_cache_delete( get_option( 'page_on_front' ), 'post_translations_relationships' );
			wp_cache_delete( get_option( 'page_for_posts' ), 'language_relationships' );
			wp_cache_delete( get_option( 'page_for_posts' ), 'post_translations_relationships' );
			$this->model->clean_languages_cache();
		}

		flush_rewrite_rules();
	}

	/**
	 * Creates the Polylang languages.
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function add_languages() {
		global $wpdb;

		// Get Polylang predefined languages list.
		$languages = include POLYLANG_DIR . '/settings/languages.php';

		// Get WPML languages.
		$wpml_languages = $wpdb->get_results(
			"SELECT l.code AS slug, l.default_locale AS locale, lt.name
			FROM {$wpdb->prefix}icl_languages AS l
			INNER JOIN {$wpdb->prefix}icl_languages_translations AS lt ON l.code = lt.language_code
			WHERE l.active = 1 AND lt.language_code = lt.display_language_code",
			ARRAY_A
		);

		foreach ( $wpml_languages as $lang ) {
			// FIXME WPML defines a language order ( since 2.8.1 ) http://wpml.org/2013/04/wpml-2-8/.
			// This is lost as I don't know how it is stored.
			$lang['term_group'] = 0;
			$lang['no_default_cat'] = 1; // Prevent the creation of a new default category.

			// We need a flag and can be more exhaustive for the rtl languages list.
			$lang['rtl'] = 'rtl' === $languages[ $lang['locale'] ]['dir'] ? 1 : 0;
			$lang['flag'] = $languages[ $lang['locale'] ]['flag'];

			$this->model->add_language( $lang );
		}

		/** @var int[] */
		$term_ids = get_terms( 'term_translations', array( 'hide_empty' => false, 'fields' => 'ids' ) );
		if ( is_array( $term_ids ) ) {
			// Delete the translation group of the default category to avoid a conflict later.
			foreach ( $term_ids as $term_id ) {
				wp_delete_term( $term_id, 'term_translations' );
			}
		}

		$this->model->clean_languages_cache(); // Update the languages list.
	}

	/**
	 * Assigns languages to posts and terms.
	 *
	 * @since 0.1
	 *
	 * @param stdClass[] $results  icl_translations table entries.
	 * @param stdClass[] $term_ids Correspondances between term_id and term_taxonomy_id.
	 * @return void
	 */
	public function process_post_term_languages( $results, $term_ids ) {
		global $wpdb;

		$languages = array();
		$post_languages = array();
		$term_languages = array();

		$default_cat = (int) get_option( 'default_category' );

		foreach ( $this->model->get_languages_list() as $lang ) {
			$languages[ $lang->slug ] = $lang;
		}

		// Posts and terms languages.
		foreach ( $results as $r ) {
			if ( ! empty( $r->language_code ) && ! empty( $languages[ $r->language_code ] ) ) {
				if ( 0 === strpos( $r->element_type, 'post_' ) && $this->is_translated_post_type( substr( $r->element_type, 5 ) ) ) {
					$post_languages[] = $wpdb->prepare( '(%d, %d)', (int) $r->element_id, (int) $languages[ $r->language_code ]->term_taxonomy_id );
				}

				if ( 0 === strpos( $r->element_type, 'tax_' ) && $this->is_translated_taxonomy( substr( $r->element_type, 4 ) ) && (int) $term_ids[ $r->element_id ]->term_id !== $default_cat ) {
					$term_languages[] = $wpdb->prepare( '(%d, %d)', (int) $term_ids[ $r->element_id ]->term_id, (int) $languages[ $r->language_code ]->tl_term_taxonomy_id );
				}
			}
		}

		$post_languages = array_unique( $post_languages );

		if ( ! empty( $post_languages ) ) {
			$wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id) VALUES " . implode( ',', $post_languages ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		$term_languages = array_unique( $term_languages );

		if ( ! empty( $term_languages ) ) {
			$wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id) VALUES " . implode( ',', $term_languages ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		foreach ( $this->model->get_languages_list() as $lang ) {
			$lang->update_count();
		}
	}

	/**
	 * Creates translations groups.
	 *
	 * @since 0.1
	 *
	 * @param stdClass[] $results  icl_translations table entries.
	 * @param stdClass[] $term_ids Correspondances between term_id and term_taxonomy_id.
	 * @return void
	 */
	public function process_post_term_translations( $results, $term_ids ) {
		global $wpdb;

		$languages = array();
		$icl_translations = array();

		foreach ( $this->model->get_languages_list() as $lang ) {
			$languages[ $lang->slug ] = $lang;
		}

		// Arrange translations in a convenient way.
		foreach ( $results as $r ) {
			if ( ! empty( $r->language_code ) && ! empty( $languages[ $r->language_code ] ) ) {
				if ( 'tax_nav_menu' === $r->element_type && ! empty( $term_ids[ $r->element_id ] ) ) {
					$icl_translations['nav_menu'][ $r->trid ][ $r->language_code ] = (int) $term_ids[ $r->element_id ]->term_id;
				}

				if ( 0 === strpos( $r->element_type, 'post_' ) && $this->is_translated_post_type( substr( $r->element_type, 5 ) ) ) {
					$icl_translations['post'][ $r->trid ][ $r->language_code ] = (int) $r->element_id;
				}

				if ( 0 === strpos( $r->element_type, 'tax_' ) && $this->is_translated_taxonomy( substr( $r->element_type, 4 ) ) && ! empty( $term_ids[ $r->element_id ] ) ) {
					$icl_translations['term'][ $r->trid ][ $r->language_code ] = (int) $term_ids[ $r->element_id ]->term_id;
				}
			}
		}

		foreach ( array( 'post', 'term' ) as $type ) {
			$terms = array();
			$slugs = array();
			$description = array();
			$count = array();
			$tts = array();
			$trs = array();

			if ( empty( $icl_translations[ $type ] ) ) {
				continue;
			}

			foreach ( $icl_translations[ $type ] as $t ) {
				$t = array_filter( $t ); // It looks like WPML can have 0 as translation id.
				if ( empty( $t ) ) {
					continue;
				}

				$term = uniqid( 'pll_' ); // The term name.
				$terms[] = $wpdb->prepare( '(%s, %s)', $term, $term );
				$slugs[] = $wpdb->prepare( '%s', $term );
				$description[ $term ] = serialize( $t );
				$count[ $term ] = count( $t );
			}

			$terms = array_unique( $terms );

			// Insert terms.
			if ( ! empty( $terms ) ) {
				$wpdb->query( "INSERT INTO $wpdb->terms (slug, name) VALUES " . implode( ',', $terms ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// Get all terms with their term_id.
			$terms = $wpdb->get_results( "SELECT term_id, slug FROM $wpdb->terms WHERE slug IN (" . implode( ',', $slugs ) . ')' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			// Prepare terms taxonomy relationship.
			foreach ( $terms as $term ) {
				$tts[] = $wpdb->prepare( '(%d, %s, %s, %d)', $term->term_id, $type . '_translations', $description[ $term->slug ], $count[ $term->slug ] );
			}
			$tts = array_unique( $tts );

			// Insert term_taxonomy.
			if ( ! empty( $tts ) ) {
				$wpdb->query( "INSERT INTO $wpdb->term_taxonomy (term_id, taxonomy, description, count) VALUES " . implode( ',', $tts ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// Get all terms with term_taxonomy_id.
			$terms = get_terms( $type . '_translations', array( 'hide_empty' => false ) );

			// Prepare objects relationships.
			if ( is_array( $terms ) ) {
				foreach ( $terms as $term ) {
					$translations = unserialize( $term->description );
					foreach ( $translations as $object_id ) {
						if ( ! empty( $object_id ) ) {
							$trs[] = $wpdb->prepare( '(%d, %d)', $object_id, $term->term_taxonomy_id );
						}
					}
				}
			}

			$trs = array_unique( $trs );

			// Insert term_relationships.
			if ( ! empty( $trs ) ) {
				$wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id) VALUES " . implode( ',', $trs ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}
		}

		// Nav menus.
		$options = get_option( 'polylang' );
		$theme = get_option( 'stylesheet' );
		$locations = get_nav_menu_locations(); // FIXME does not work.

		if ( ! empty( $locations ) ) {
			foreach ( $locations as $loc => $menu ) {
				foreach ( $icl_translations['nav_menu'] as $trid ) {
					if ( in_array( $menu, $trid ) ) {
						$options['nav_menus'][ $theme ][ $loc ] = $trid;
					}
				}
			}
		}
		update_option( 'polylang', $options );
	}

	/**
	 * Adds strings translations
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function process_strings_translations() {
		global $wpdb;

		$string_translations = array();

		/**
		 * WPML string translations.
		 *
		 * @var stdClass[]
		 */
		$results = $wpdb->get_results(
			"SELECT s.value AS string, st.language, st.value AS translation
			FROM {$wpdb->prefix}icl_strings AS s
			INNER JOIN {$wpdb->prefix}icl_string_translations AS st ON st.string_id = s.id"
		);

		// Order them in a convenient way.
		foreach ( $results as $st ) {
			if ( ! empty( $st->string ) ) {
				$string_translations[ $st->language ][] = array( $st->string, $st->translation );
			}
		}

		// Save Polylang string translations.
		if ( ! empty( $string_translations ) ) {
			foreach ( $string_translations as $lang => $strings ) {
				$language = $this->model->get_language( $lang );
				if ( $language ) {
					$mo = new PLL_MO();
					foreach ( $strings as $msg ) {
						$mo->add_entry( $mo->make_entry( $msg[0], $msg[1] ) );
					}
					$mo->export_to_db( $language );
				}
			}
		}
	}

	/**
	 * Defines Polylang options
	 *
	 * @since 0.1
	 *
	 * @return void
	 */
	public function process_options() {
		$options = get_option( 'polylang' );

		$options['rewrite'] = 1; // Remove /language/ in permalinks ( was the opposite before 0.7.2 ).
		$options['hide_default'] = 1; // Remove URL language information for default language.
		$options['redirect_lang'] = 1; // Redirect the language page to the homepage.

		// Default language.
		$options['default_lang'] = $this->icl_settings['default_language'];

		// Urls modifications.
		switch ( $this->icl_settings['language_negotiation_type'] ) {
			case 1:
				$options['force_lang'] = 1;
				break;
			case 2:
				$options['force_lang'] = 3;
				break;
			case 3:
			default:
				$options['force_lang'] = 0;
				break;
		}

		// Domains.
		$options['domains'] = isset( $this->icl_settings['language_domains'] ) ? $this->icl_settings['language_domains'] : array();

		// Post types.
		if ( ! empty( $this->icl_settings['custom_posts_sync_option'] ) ) {
			$post_types = array_keys( array_filter( $this->icl_settings['custom_posts_sync_option'] ) );
			$post_types = array_diff( $post_types, array( 'post', 'page', 'attachment', 'wp_block' ) );
			$options['post_types'] = $post_types;
		}

		// Taxonomies.
		if ( ! empty( $this->icl_settings['taxonomies_sync_option'] ) ) {
			$taxonomies = array_keys( array_filter( $this->icl_settings['taxonomies_sync_option'] ) );
			$taxonomies = array_diff( $taxonomies, array( 'category', 'post_tag' ) );
			$options['taxonomies'] = $taxonomies;
		}

		// Sync.
		$sync = array(
			'sync_page_ordering'  => 'menu_order',
			'sync_page_parent'    => 'post_parent',
			'sync_page_template'  => '_wp_page_template',
			'sync_ping_status'    => 'ping_status',
			'sync_comment_status' => 'comment_status',
			'sync_sticky_flag'    => 'sticky_posts',
		);

		$options['sync'] = array();
		foreach ( $sync as $wpml_opt => $pll_opt ) {
			if ( ! empty( $this->icl_settings[ $wpml_opt ] ) ) {
				$options['sync'][] = $pll_opt;
			}
		}
		update_option( 'polylang', $options );

		// Default category in default language.
		update_option( 'default_category', (int) $this->icl_settings['default_categories'][ $this->icl_settings['default_language'] ] );
	}

	/**
	 * Returns true if WPML manages post type language and translation
	 *
	 * @since 0.1
	 *
	 * @param string $type Post type name.
	 * @return bool
	 */
	public function is_translated_post_type( $type ) {
		return in_array( $type, array( 'post', 'page' ) ) || ! empty( $this->icl_settings['custom_posts_sync_option'][ $type ] );
	}

	/**
	 * Returns true if WPML manages taxonomy language and translation
	 *
	 * @since 0.1
	 *
	 * @param string $tax Taxonomy name.
	 * @return bool
	 */
	public function is_translated_taxonomy( $tax ) {
		return in_array( $tax, array( 'category', 'post_tag' ) ) || ! empty( $this->icl_settings['taxonomies_sync_option'][ $tax ] );
	}
}

new WPML_To_Polylang();