: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
require dirname( __FILE__ ) . '/wpml-menu-sync-functionality.class.php';
require dirname( __FILE__ ) . '/menu-item-sync.class.php';
class ICLMenusSync extends WPML_Menu_Sync_Functionality {
public $is_preview = false;
public $sync_data = false;
public $string_translation_links = array();
public $operations = array();
/** @var WPML_Menu_Item_Sync $menu_item_sync */
* @param SitePress $sitepress
* @param WPML_Post_Translation $post_translations
* @param WPML_Term_Translation $term_translations
function __construct( &$sitepress, &$wpdb, &$post_translations, &$term_translations ) {
parent::__construct( $sitepress, $wpdb, $post_translations, $term_translations );
$this->menu_item_sync = new WPML_Menu_Item_Sync( $this->sitepress, $this->wpdb, $this->post_translations, $this->term_translations );
add_action( 'init', array( $this, 'init' ), 20 );
if ( isset( $_GET['updated'] ) ) {
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
function init( $previous_menu = false ) {
$this->sitepress->switch_lang( $this->sitepress->get_default_language() );
$action = filter_input( INPUT_POST, 'action' );
$nonce = (string) filter_input( INPUT_POST, '_icl_nonce_menu_sync' );
if ( $action && ! wp_verify_nonce( $nonce, '_icl_nonce_menu_sync' ) ) {
wp_send_json_error( 'Invalid nonce' );
$this->menu_item_sync->cleanup_broken_page_items();
if ( $action === 'icl_msync_preview' ) {
$this->is_preview = true;
$this->sync_data = isset( $_POST['sync'] ) ? array_map( 'stripslashes_deep', $_POST['sync'] ) : false;
$previous_menu = isset( $_SESSION[ 'wpml_menu_sync_menu' ] ) ? $_SESSION[ 'wpml_menu_sync_menu' ] : null;
$this->menus = $previous_menu;
$_SESSION[ 'wpml_menu_sync_menu' ] = $this->menus;
$this->sitepress->switch_lang();
function get_menu_names() {
global $sitepress, $wpdb;
$menus = $wpdb->get_results( $wpdb->prepare( "
SELECT tm.term_id, tm.name FROM {$wpdb->terms} tm
JOIN {$wpdb->term_taxonomy} tx ON tx.term_id = tm.term_id
JOIN {$wpdb->prefix}icl_translations tr ON tr.element_id = tx.term_taxonomy_id AND tr.element_type='tax_nav_menu'
WHERE tr.language_code=%s
", $sitepress->get_default_language() ) );
foreach ( $menus as $menu ) {
$menu_names[] = $menu->name;
function get_menus_tree() {
global $sitepress, $wpdb;
$menus = $wpdb->get_results( $wpdb->prepare( "
SELECT tm.term_id, tm.name FROM {$wpdb->terms} tm
JOIN {$wpdb->term_taxonomy} tx ON tx.term_id = tm.term_id
JOIN {$wpdb->prefix}icl_translations tr ON tr.element_id = tx.term_taxonomy_id AND tr.element_type='tax_nav_menu'
WHERE tr.language_code=%s
", $sitepress->get_default_language() ) );
foreach ( $menus as $menu ) {
$this->menus[ $menu->term_id ] = array(
'items' => $this->get_menu_items( $menu->term_id, true ),
'translations' => $this->get_menu_translations( $menu->term_id )
$this->add_ghost_entries();
$this->set_new_menu_order();
private function get_menu_options( $menu_id ) {
$menu_options = get_option( 'nav_menu_options' );
'auto_add' => isset( $menu_options['auto_add'] ) && in_array(
$menu_options['auto_add']
public function add_ghost_entries() {
if ( is_array( $this->menus ) ) {
foreach ( $this->menus as $menu_id => $menu ) {
if ( ! is_array( $menu['translations'] ) ) {
foreach ( $menu['translations'] as $language => $tmenu ) {
if ( ! empty( $tmenu ) ) {
$valid_items = array_filter(
$this->menus[ $menu_id ]['items'],
function ( $item ) use ( $language ) {
return $item && isset( $item['translations'][ $language ]['ID'] );
foreach ( $tmenu['items'] as $titem ) {
// Has a place in the default menu?
foreach ( $valid_items as $item ) {
if ( (int) $item['translations'][ $language ]['ID'] === (int) $titem['ID'] ) {
$this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'][] = array(
'title' => $titem['title'],
'menu_order' => $titem['menu_order'],
public function set_new_menu_order() {
if ( ! is_array( $this->menus ) ) {
foreach ( $this->menus as $menu_id => $menu ) {
$menu_index_by_lang = array();
foreach ( $menu['items'] as $item_id => $item ) {
$valid_translations = array_filter(
return $item && $item['ID'];
foreach ( $valid_translations as $language => $item_translation ) {
$new_menu_order = empty( $menu_index_by_lang[ $language ] ) ? 1 : $menu_index_by_lang[ $language ] + 1;
$menu_index_by_lang[ $language ] = $new_menu_order;
$this->menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['menu_order_new'] = $new_menu_order;
function do_sync( array $data ) {
$this->menus = isset( $this->menus ) ? $this->menus : array();
$this->menus = empty( $data['menu_translation'] ) ? $this->menus : $this->menu_item_sync->sync_menu_translations( $data['menu_translation'],
if ( ! empty( $data['options_changed'] ) ) {
$this->menu_item_sync->sync_menu_options( $data['options_changed'] );
if ( ! empty( $data['del'] ) ) {
$this->menu_item_sync->sync_deleted_menus( $data['del'] );
$this->menus = empty( $data['mov'] ) ? $this->menus : $this->menu_item_sync->sync_moved_items( $data['mov'],
$this->menus = empty( $data['add'] ) ? $this->menus : $this->menu_item_sync->sync_added_items( $data['add'],
if ( ! empty( $data['label_changed'] ) ) {
$this->menu_item_sync->sync_caption( $data['label_changed'] );
if ( ! empty( $data['url_changed'] ) ) {
$this->menu_item_sync->sync_urls( $data['url_changed'] );
if ( ! empty( $data['label_missing'] ) ) {
$this->menu_item_sync->sync_missing_captions( $data['label_missing'] );
if ( ! empty( $data['url_missing'] ) ) {
$this->menu_item_sync->sync_urls_to_add( $data['url_missing'] );
$this->menus = isset( $this->menus ) ? $this->menu_item_sync->sync_menu_order( $this->menus ) : $this->menus;
$this->menu_item_sync->cleanup_broken_page_items();
function render_items_tree_default( $menu_id, $parent = 0, $depth = 0 ) {
$active_language_codes = array_keys( $sitepress->get_active_languages() );
$default_language = $sitepress->get_default_language();
foreach ( $this->menus[ $menu_id ][ 'items' ] as $item ) {
// deleted items #2 (menu order beyond)
static $d2_items = array();
$deleted_items = array();
if ( isset( $this->menus[ $menu_id ][ 'translation' ] ) && is_array( $this->menus[ $menu_id ][ 'translation' ] ) ) {
foreach ( $this->menus[ $menu_id ][ 'translations' ] as $language => $tmenu ) {
if ( ! isset( $d2_items[ $language ] ) ) {
$d2_items[ $language ] = array();
if ( ! empty( $this->menus[ $menu_id ][ 'translations' ][ $language ][ 'deleted_items' ] ) ) {
foreach ( $this->menus[ $menu_id ][ 'translations' ][ $language ][ 'deleted_items' ] as $deleted_item ) {
if ( ! in_array( $deleted_item[ 'ID' ],
$d2_items[ $language ] ) && $deleted_item[ 'menu_order' ] > count( $this->menus[ $menu_id ][ 'items' ] )
$deleted_items[ $language ][ ] = $deleted_item;
$d2_items[ $language ][ ] = $deleted_item[ 'ID' ];
<?php foreach ( $sitepress->get_active_languages() as $language ): if ( $language[ 'code' ] == $default_language ) {
<?php if ( isset( $deleted_items[ $language[ 'code' ] ] ) ): ?>
<?php foreach ( $deleted_items[ $language[ 'code' ] ] as $deleted_item ): ?>
<?php echo str_repeat( ' - ', $depth ) ?><span
class="icl_msync_item icl_msync_del"><?php echo esc_html( $deleted_item[ 'title' ] ) ?></span>
name="sync[del][<?php echo esc_attr( $menu_id ) ?>][<?php echo esc_attr( $language[ 'code' ] ) ?>][<?php echo esc_attr( $deleted_item[ 'ID' ] ) ?>]"
value="<?php echo esc_attr( $deleted_item[ 'title' ] ) ?>"/>
<?php $this->operations[ 'del' ] = empty( $this->operations[ 'del' ] ) ? 1
: $this->operations[ 'del' ] ++; ?>
static $mo_added = array();
$deleted_items = array();
if ( isset( $this->menus[ $menu_id ][ 'translation' ] ) && is_array( $this->menus[ $menu_id ][ 'translation' ] ) ) {
foreach ( $this->menus[ $menu_id ][ 'translations' ] as $language => $tmenu ) {
if ( ! isset( $mo_added[ $language ] ) ) {
$mo_added[ $language ] = array();
if ( ! empty( $this->menus[ $menu_id ][ 'translations' ][ $language ][ 'deleted_items' ] ) ) {
foreach ( $this->menus[ $menu_id ][ 'translations' ][ $language ][ 'deleted_items' ] as $deleted_item ) {
if ( ! in_array( $item[ 'menu_order' ],
$mo_added[ $language ] ) && $deleted_item[ 'menu_order' ] == $item[ 'menu_order' ]
$deleted_items[ $language ] = $deleted_item;
$mo_added[ $language ][ ] = $item[ 'menu_order' ];
$this->render_deleted_items( $deleted_items, $need_sync, $depth, $menu_id );
if ( $item[ 'parent' ] == $parent ) {
echo str_repeat( ' - ', $depth ) . $item[ 'title' ];
foreach ( $active_language_codes as $lang_code ) {
if ( $lang_code === $default_language ) {
$item_translation = $item[ 'translations' ][ $lang_code ];
$item_id = $item[ 'ID' ];
echo str_repeat( ' - ', $depth );
if ( ! empty( $item_translation[ 'ID' ] ) ) {
// item translation exists
$item_sync_needed = false;
if ( $item_translation[ 'menu_order' ] != $item_translation[ 'menu_order_new' ] || $item_translation[ 'depth' ] != $item[ 'depth' ] ) { // MOVED
echo '<span class="icl_msync_item icl_msync_mov">' . esc_html( $item_translation[ 'title' ] ) . '</span>';
echo '<input type="hidden" name="sync[mov][' . esc_attr( $menu_id ) . '][' . esc_attr( $item[ 'ID' ] ) . '][' . esc_attr( $lang_code ) . '][' . esc_attr( $item_translation[ 'menu_order_new' ] ) . ']" value="' . esc_attr( $item_translation[ 'title' ] ) . '" />';
$this->operations[ 'mov' ] = empty( $this->operations[ 'mov' ] ) ? 1
: $this->operations[ 'mov' ] ++;
$item_sync_needed = true;
if ( $item_translation[ 'label_missing' ] ) {
$this->index_changed( 'label_missing',
$item_translation[ 'title' ],
$item_sync_needed = true;
if ( $item_translation[ 'label_changed' ] ) {
$this->index_changed( 'label_changed',
$item_translation[ 'title' ],
$item_sync_needed = true;
if ( $item_translation[ 'url_missing' ] ) {
$this->index_changed( 'url_missing',
$item_translation[ 'url' ],
$item_sync_needed = true;
if ( $item_translation[ 'url_changed' ] ) {
$this->index_changed( 'url_changed',
$item_translation[ 'url' ],
$item_sync_needed = true;
if ( ! $item_sync_needed ) { // NO CHANGE
echo esc_html( $item_translation[ 'title' ] );
} elseif ( $item_translation && 'custom' === $item_translation['object_type'] ) {
// item translation does not exist but is a custom item that will be created
echo '<span class="icl_msync_item icl_msync_add">' . esc_html( $item_translation[ 'title' ] ) . ' @' . esc_html( $lang_code ) . '</span>';
echo '<input type="hidden" name="sync[add][' . esc_attr( $menu_id ) . '][' . esc_attr( $item[ 'ID' ] ) . '][' . esc_attr( $lang_code ) . ']" value="' . esc_attr( $item_translation[ 'title' ] . ' @' . $lang_code ) . '" />';
$this->operations[ 'add' ] = empty( $this->operations[ 'add' ] ) ? 1
: $this->operations[ 'add' ] ++;
} elseif ( ! empty( $item_translation[ 'object_id' ] ) ) {
// item translation does not exist but translated object does
if ( $item_translation[ 'parent_not_translated' ] ) {
echo '<span class="icl_msync_item icl_msync_not">' . esc_html( $item_translation[ 'title' ] ) . '</span>';
$this->operations[ 'not' ] = empty( $this->operations[ 'not' ] ) ? 1
: $this->operations[ 'not' ] ++;
} elseif ( ! icl_object_id( $item[ 'ID' ], 'nav_menu_item', false, $lang_code ) ) {
// item translation does not exist but translated object does
echo '<span class="icl_msync_item icl_msync_add">' . esc_html( $item_translation[ 'title' ] ) . '</span>';
echo '<input type="hidden" name="sync[add][' . esc_attr( $menu_id ) . '][' . esc_attr( $item[ 'ID' ] ) . '][' . esc_attr( $lang_code ) . ']" value="' . esc_attr( $item_translation[ 'title' ] ) . '" />';
$this->operations[ 'add' ] = empty( $this->operations[ 'add' ] ) ? 1
: $this->operations[ 'add' ] ++;
// item translation and object translation do not exist
echo '<i class="inactive">' . esc_html__( 'Not translated', 'sitepress' ) . '</i>';
if ( $this->_item_has_children( $menu_id, $item[ 'ID' ] ) ) {
$need_sync += $this->render_items_tree_default( $menu_id, $item[ 'ID' ], $depth + 1 );
$this->render_option_update( $active_language_codes, $default_language, $menu_id, $need_sync );
private function render_option_update( $active_language_codes, $default_language, $menu_id, &$need_sync ) {
foreach ( $active_language_codes as $lang_code ) {
if ( $lang_code === $default_language ) {
esc_html_e( 'Menu Option: auto_add', 'sitepress' );
$menu_options = $this->get_menu_options( $menu_id );
$translated_id = $this->get_translated_menu( $menu_id, $lang_code );
if ( ! isset( $translated_id[ 'id' ] ) || $menu_options != $this->get_menu_options( $translated_id[ 'id' ] ) ) {
$this->index_changed( 'options_changed',
$menu_options[ 'auto_add' ],
echo esc_html( $menu_options[ 'auto_add' ] );
private function render_deleted_items( $deleted_items, &$need_sync, $depth, $menu_id ) {
<?php foreach ( $sitepress->get_active_languages() as $language ): if ( $language[ 'code' ] === $sitepress->get_default_language() ) {
<?php if ( isset( $deleted_items[ $language[ 'code' ] ] ) ): ?>
<?php echo str_repeat( ' - ', $depth ) ?><span
class="icl_msync_item icl_msync_del"><?php echo esc_html( $deleted_items[ $language[ 'code' ] ][ 'title' ] ) ?></span>
name="sync[del][<?php echo esc_attr( $menu_id ) ?>][<?php echo esc_attr( $language[ 'code' ] ) ?>][<?php echo esc_attr( $deleted_items[ $language[ 'code' ] ][ 'ID' ] ) ?>]"
value="<?php echo esc_attr( $deleted_items[ $language[ 'code' ] ][ 'title' ] ) ?>"/>
<?php $this->operations[ 'del' ] = empty( $this->operations[ 'del' ] ) ? 1
: $this->operations[ 'del' ] ++; ?>
private function index_changed( $index, $item_id, $item_translation, $menu_id, $lang_code, $change = true ) {
$this->string_translation_links[ $this->menus[ $menu_id ][ 'name' ] ] = 1;
$additional_class = $change ? 'icl_msync_' . $index : '';
echo '<span class="icl_msync_item ' . esc_attr( $additional_class ) . '">'
. ( ! $item_translation ? 0 : esc_html( $item_translation ) )
. '<input type="hidden" name="sync[' . esc_attr( $index ) . '][' . esc_attr( $menu_id ) . '][' . esc_attr( $item_id ) . '][' . esc_attr( $lang_code ) . ']" value="'
. esc_attr( $item_translation ) . '" />';
$this->operations[ $index ] = empty( $this->operations[ $index ] ) ? 1 : $this->operations[ $index ] ++;
function _item_has_children( $menu_id, $item_id )
foreach ( $this->menus[ $menu_id ][ 'items' ] as $item ) {
if ( $item[ 'parent' ] == $item_id ) {
function get_item_depth( $menu_id, $item_id ) {