WP Manifestindependent plugin directory
manifest / i18n / migrate-polylang-to-wpml

Migrate Polylang to WPML WP Registry grade D+. High-severity findings · 3 findingsWP RegistryHigh-severity findings · 3 findingsOpen the reportD+ #6 in Languages & Localization

Import multilingual data from Polylang to WPML

★ 21stars
260release downloads
0.5.2latest · 2mo ago

Install

Download migrate-polylang-to-wpml.zip From the 0.1 release on GitHub, Jul 20, 2016

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/onthegosystems/migrate-polylang-to-wpml/releases/download/0.1/migrate-polylang-to-wpml.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Import multilingual data from Polylang to WPML

Installation and usage

  • Backup your database! There is a reason why this plugin has the version number 0.1
  • Deactivate Polylang.
  • Activate both WPML Multilingual Blog/CMS and the (Migrate Polylang to WPML) plugins. If you also want to migrate string translations, activate WPML String Translation plugin.
  • Finish the WPML Wizard (you will see it right after activation)
  • If you have custom post types, go to WPML > Translation options (or WPML > Translation Management > Multilingual Content Setup) and set those types to be translatable.
  • Go to Tools > Migrate from Polylang to WPML
  • Click button "Migrate" and wait until the migration process is completed (it might take longer based on the size of the content you are migrating)
  • Done. You can uninstall the (Migrate Polylang to WPML) plugin.

What this plugin does

  • Configures WPML to have the same languages as they were in Polylang.
  • Migrates all your posts, pages and custom post types and their translations.
  • Migrates all your taxonomies (categories, tags and custom taxonomies) and their translations.
  • Migrates your strings (only those which has been translated in Polylang and WPMl String Translation recognized and registered as correct string)
  • Migrate widgets: You will need to activate WPML Widgets plugin.

What this plugin doesn't (yet)

  • Migrate menus: Use the menu synchronization between languages option in WPML.
  • Migrate other settings: This will be implemented progressively. For the time being, you are free to adjust them manually after the migration process is completed.

Support and feedback

Development

The development tools require PHP 7.4 or newer and Composer 2. Install the locked dependencies with:

composer install --no-plugins --no-scripts

Run all local checks with:

composer check

The individual commands are composer test, composer phpcs, and composer phpstan. PHPUnit uses WordPress function mocks, so it does not require a WordPress installation or database. PHPStan analyzes the production plugin at level 3 with WordPress and WPML stubs. PHPCS initially covers the new test and analysis support code; the legacy production files will be introduced separately after their existing violations are addressed.

CI runs the unit tests on the supported endpoints, PHP 7.4 and 8.5. Coding standards and static analysis run on PHP 8.5.

Changelog

0.5.2

  • Fixed taxonomy migration for languages whose Polylang slugs differ from their WPML language codes, including Portuguese and Chinese.
  • Fixed language-code handling for post translation groups, including groups without a translation in the site's default language.
  • Prevented orphaned Polylang taxonomy translation groups from overwriting valid WPML relationships.
  • Improved PHP 8 compatibility when migrating incomplete or corrupt Polylang data.

0.5.1

  • Added capability and nonce checks to all migration and data-deletion AJAX actions.
  • Escaped admin output and sanitized the .htaccess notice dismissal cookie.
  • Prevented direct access to the plugin's PHP files.

0.5.0

  • Automatically set the translation preference to "translate" for translated custom post types.
  • Added the support for duplicated (synced) posts.
  • Fixed a fatal error "Uncaught TypeError: Cannot access offset of type string on string".
  • Fixed several PHP warnings.

0.4

  • Fixed an issue with not terminating import when importing terms.

0.3

  • Resolved an issue with migration stopping on nginx server.

0.2

  • Fixed the migration of Chinese languages and Portuguese (pt-pt and pt-br) languages.
  • Fixed a case when the user has set synchronisation options (comp-3271).

0.1

  • Initial release.

Releases

6 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
0.5.2 latest
Aug 3, 2026 2mo ago
tag only
—
Jul 27, 2026 2mo ago
tag only
—
Jan 2, 2025 1 year ago
tag only
—
0.4
Feb 28, 2020 6 years ago
tag only
—
Jan 24, 2020 6 years ago
tag only
—
0.1
Jul 20, 2016 10 years ago
migrate-polylang-to-wpml.zip
260
D+ grade

Security

Migrate Polylang to WPML 0.1 · audited by WP Registry

High-severity findings.

2 high 1 medium
Audited release
0.1
Findings
3
Worst severity
high
Content hash
434afa67df7645e5e0f5fb21…

Findings

  • high Subscriber+ can migrate WPML languages and erase Polylang data via ungated AJAX

    missing_capability

    add_submenu_page for Tools > Migrate from Polylang to WPML requires manage_options, but the handlers the Migrate / Erase buttons actually call do not. Six actions are registered as wp_ajax_* only (logged-in): mpw_migrate_languages, mpw_migrate_posts, mpw_migrate_taxonomies, mpw_migrate_strings, mpw_migrate_widgets, mpw_delete_polylang_data. None of the callbacks call current_user_can, check_ajax_referer, or wp_verify_nonce. scripts/ajax.js POSTs only {'action': '...'} — no nonce is localized in 0.1. ajax_delete_polylang_data is ungated even by the environment pre-check and always runs delete_data(): update_option('mpw_polylang_data_deleted', 1), delete_option('polylang'|polylang_wpml_strings|polylang_widget), wp_delete_post on every polylang_mo, and wp_delete_term on the four Polylang taxonomies (the serialized post/term translation maps live in those term descriptions). The five migrate callbacks run when pre_check_ready_all() is true, which only tests that Polylang is off, WPML is on, the WPML wizard finished, and Polylang data has not already been erased — the intended production state of this plugin, and the same boolean for Subscriber as for administrator. Those writers $wpdb->update icl_languages.active, do_action('wpml_set_element_language_details') for every Polylang-linked post and term (WPML then serves that language map on the front end), icl_add_string_translation, and update_option on every widget_% option. ajax_migrate_widgets always update_option('mpw_migration_done', 1) even when the pre-check fails. Observable: a Subscriber POSTs admin-ajax.php?action=mpw_delete_polylang_data (or the migrate chain) and the site's translation groups / WPML language assignments change or the Polylang source data is gone with no undo. The same requests are CSRF against any logged-in cookie because action is taken from $_REQUEST. Upstream 0.5.1 (security hardening, 2026-07-27) added verify_ajax_request() requiring manage_options and nonce mpw_migrate on every one of these actions.

    migrate-polylang-to-wpml.php:27-33,280-353

    Recommendation

    At the start of every wp_ajax_ callback require current_user_can('manage_options') and check_ajax_referer (nonce action such as mpw_migrate). Localize that nonce only on the Tools migration screen, not on every admin page. Fail closed with wp_send_json_error / 403. Do not treat pre_check_ready_all() as authorization. Upgrade to upstream 0.5.1+ which added Migrate_Polylang_To_WPML::verify_ajax_request().

  • high Migration and Polylang-data-deletion AJAX actions have no nonce (CSRF)

    csrf

    The same six wp_ajax_mpw_* actions perform site-wide writes (WPML language activation, wpml_set_element_language_details on all posts/terms, widget option updates, and irreversible Polylang data deletion) with zero wp_verify_nonce / check_ajax_referer / check_admin_referer in the plugin. scripts/ajax.js sends only the action name; 0.1 never calls wp_create_nonce. admin-ajax.php reads action from $_REQUEST, so a cross-site GET or POST (img/form/fetch with cookies) while any user is logged in — Subscriber is enough, because there is also no capability check — runs delete_data() or the migrate writers in that user's session. The victim does not need to visit Tools > Migrate; the Tools page's manage_options gate is not on the AJAX path. Observable: an unauthenticated attacker who can make a logged-in user load a page issues admin-ajax.php?action=mpw_delete_polylang_data and Polylang options/posts/translation terms are erased; action=mpw_migrate_posts rewrites WPML language links used on the front end. Upstream 0.5.1 added check_ajax_referer('mpw_migrate', 'nonce') beside the capability check.

    scripts/ajax.js:9-47

    Recommendation

    Send a localized nonce with every AJAX POST and verify it with check_ajax_referer before any writer. Keep the capability check as well; a nonce is not authorization. Prefer POST-only handling (do not honor a GET of these actions).

  • medium Language getters DELETE wp_icl_translations on any admin page load

    missing_capability

    mpw_polylang_data::get_terms() is a reader in name only: it register_taxonomy()'s the requested Polylang tax, then $wpdb->delete($wpdb->prefix.'icl_translations', array('element_type' => 'tax_'.$tax)) before get_terms(). get_languages() (tax=language) is called from get_default_language_name() and get_additional_languages_names(). Those two methods run from enqueue_scripts whenever is_admin() && !pre_check_wizard_complete() — i.e. on every wp-admin screen, including Subscriber profile.php, for the entire WPML wizard window, with no current_user_can. Each such GET therefore DELETEs all icl_translations rows whose element_type is tax_language (twice per page load). Consumer is WPML's taxonomy language map; wiping those rows during setup unlinks language terms WPML already wrote. The same getter is also invoked from the ungated migrate/delete AJAX path. Later upstream moved this delete into an explicit reset_translations() called only after the 0.5.1 capability+nonce gate.

    classes/class-mpw_polylang_data.php:28-36

    Recommendation

    Do not DELETE from icl_translations inside a getter or from admin_enqueue_scripts. If stale WPML rows must be cleared, do it only inside the manage_options + nonce-gated migrate AJAX, for the specific taxonomies being imported. Restrict tooltip script enqueue to users who can manage_options and to the WPML wizard screens.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.