?
admin-sync.php 0000666 00000016373 15126445003 0007335 0 ustar 00 <?php
/**
* Manages copy and synchronization of terms and post metas
*
* @since 1.2
*/
class PLL_Admin_Sync extends PLL_Sync {
/**
* Constructor
*
* @since 1.2
*
* @param object $polylang
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 3 );
add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ) );
add_action( 'rest_api_init', array( $this, 'new_post_translation' ) ); // Block editor
add_action( 'add_meta_boxes', array( $this, 'new_post_translation' ), 5 ); // Classic editor, before Types which populates custom fields in same hook with priority 10
}
/**
* Translate post parent if exists when using "Add new" ( translation )
*
* @since 0.6
*
* @param int $post_parent Post parent ID
* @param int $post_id Post ID, unused
* @param array $postarr Array of parsed post data
* @return int
*/
public function wp_insert_post_parent( $post_parent, $post_id, $postarr ) {
if ( isset( $_GET['from_post'], $_GET['new_lang'], $_GET['post_type'] ) ) {
check_admin_referer( 'new-post-translation' );
// Make sure not to impact media translations created at the same time
if ( $_GET['post_type'] === $postarr['post_type'] && ( $id = wp_get_post_parent_id( (int) $_GET['from_post'] ) ) && $parent = $this->model->post->get_translation( $id, sanitize_key( $_GET['new_lang'] ) ) ) {
$post_parent = $parent;
}
}
return $post_parent;
}
/**
* Copy menu order, comment, ping status and optionally the date when creating a new tanslation
*
* @since 2.5
*
* @param array $data An array of slashed post data.
* @return array
*/
public function wp_insert_post_data( $data ) {
if ( isset( $GLOBALS['pagenow'], $_GET['from_post'], $_GET['new_lang'] ) && 'post-new.php' === $GLOBALS['pagenow'] && $this->model->is_translated_post_type( $data['post_type'] ) ) {
check_admin_referer( 'new-post-translation' );
$from_post_id = (int) $_GET['from_post'];
$from_post = get_post( $from_post_id );
foreach ( array( 'menu_order', 'comment_status', 'ping_status' ) as $property ) {
$data[ $property ] = $from_post->$property;
}
// Copy the date only if the synchronization is activated
if ( in_array( 'post_date', $this->options['sync'] ) ) {
$data['post_date'] = $from_post->post_date;
$data['post_date_gmt'] = $from_post->post_date_gmt;
}
}
return $data;
}
/**
* Copy post metas, and taxonomies when using "Add new" ( translation )
*
* @since 2.5
*/
public function new_post_translation() {
global $post;
static $done = array();
if ( isset( $GLOBALS['pagenow'], $_GET['from_post'], $_GET['new_lang'] ) && 'post-new.php' === $GLOBALS['pagenow'] && $this->model->is_translated_post_type( $post->post_type ) ) {
check_admin_referer( 'new-post-translation' );
// Capability check already done in post-new.php
$from_post_id = (int) $_GET['from_post'];
$lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) );
if ( ! $from_post_id || ! $lang || ! empty( $done[ $from_post_id ] ) ) {
return;
}
$done[ $from_post_id ] = true; // Avoid a second duplication in the block editor. Using an array only to allow multiple phpunit tests.
$this->taxonomies->copy( $from_post_id, $post->ID, $lang->slug );
$this->post_metas->copy( $from_post_id, $post->ID, $lang->slug );
if ( is_sticky( $from_post_id ) ) {
stick_post( $post->ID );
}
}
}
/**
* Get post fields to synchronize
*
* @since 2.4
*
* @param object $post Post object
* @return array
*/
protected function get_fields_to_sync( $post ) {
global $wpdb;
$postarr = parent::get_fields_to_sync( $post );
// For new drafts, save the date now otherwise it is overriden by WP. Thanks to JoryHogeveen. See #32.
if ( in_array( 'post_date', $this->options['sync'] ) && isset( $GLOBALS['pagenow'], $_GET['from_post'], $_GET['new_lang'] ) && 'post-new.php' === $GLOBALS['pagenow'] ) {
check_admin_referer( 'new-post-translation' );
unset( $postarr['post_date'] );
unset( $postarr['post_date_gmt'] );
$original = get_post( (int) $_GET['from_post'] );
$wpdb->update(
$wpdb->posts,
array(
'post_date' => $original->post_date,
'post_date_gmt' => $original->post_date_gmt,
),
array( 'ID' => $post->ID )
);
}
if ( isset( $GLOBALS['post_type'] ) ) {
$post_type = $GLOBALS['post_type'];
} elseif ( isset( $_REQUEST['post_type'] ) ) {
$post_type = sanitize_key( $_REQUEST['post_type'] ); // 2nd case for quick edit
}
// Make sure not to impact media translations when creating them at the same time as post
if ( in_array( 'post_parent', $this->options['sync'] ) && ( ! isset( $post_type ) || $post_type !== $post->post_type ) ) {
unset( $postarr['post_parent'] );
}
return $postarr;
}
/**
* Synchronizes post fields in translations
*
* @since 1.2
*
* @param int $post_id post id
* @param object $post post object
* @param array $translations post translations
*/
public function pll_save_post( $post_id, $post, $translations ) {
parent::pll_save_post( $post_id, $post, $translations );
// Sticky posts
if ( in_array( 'sticky_posts', $this->options['sync'] ) ) {
$stickies = get_option( 'sticky_posts' );
if ( isset( $_REQUEST['sticky'] ) && 'sticky' === $_REQUEST['sticky'] ) { // phpcs:ignore WordPress.Security.NonceVerification
$stickies = array_merge( $stickies, array_values( $translations ) );
} else {
$stickies = array_diff( $stickies, array_values( $translations ) );
}
update_option( 'sticky_posts', array_unique( $stickies ) );
}
}
/**
* Some backward compatibility with Polylang < 2.3
* allows to call PLL()->sync->copy_post_metas() and PLL()->sync->copy_taxonomies()
* used for example in Polylang for WooCommerce
* the compatibility is however only partial as the 4th argument $sync is lost
*
* @since 2.3
*
* @param string $func Function name
* @param array $args Function arguments
*/
public function __call( $func, $args ) {
$obj = substr( $func, 5 );
if ( is_object( $this->$obj ) && method_exists( $this->$obj, 'copy' ) ) {
if ( WP_DEBUG ) {
$debug = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
$i = 1 + empty( $debug[1]['line'] ); // The file and line are in $debug[2] if the function was called using call_user_func
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf(
'%1$s was called incorrectly in %3$s on line %4$s: the call to PLL()->sync->%1$s() has been deprecated in Polylang 2.3, use PLL()->sync->%2$s->copy() instead.' . "\nError handler",
esc_html( $func ),
esc_html( $obj ),
esc_html( $debug[ $i ]['file'] ),
absint( $debug[ $i ]['line'] )
)
);
}
return call_user_func_array( array( $this->$obj, 'copy' ), $args );
}
$debug = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf(
'Call to undefined function PLL()->sync->%1$s() in %2$s on line %3$s' . "\nError handler",
esc_html( $func ),
esc_html( $debug[0]['file'] ),
absint( $debug[0]['line'] )
),
E_USER_ERROR
);
}
}
sync-metas.php 0000666 00000027730 15126445003 0007355 0 ustar 00 <?php
/**
* Abstract class to manage the copy and synchronization of metas
*
* @since 2.3
*/
abstract class PLL_Sync_Metas {
public $model;
protected $meta_type, $prev_value, $to_copy;
/**
* Constructor
*
* @since 2.3
*
* @param object $polylang
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
add_filter( "add_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
add_filter( "update_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
add_filter( "delete_{$this->meta_type}_metadata", array( $this, 'can_synchronize_metadata' ), 1, 3 );
$this->add_all_meta_actions();
add_action( "pll_save_{$this->meta_type}", array( $this, 'save_object' ), 10, 3 );
}
/**
* Removes "added_{$this->meta_type}_meta" action
*
* @since 2.3
*/
protected function remove_add_meta_action() {
remove_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ), 10, 4 );
}
/**
* Removes all meta synchronization actions and filters
*
* @since 2.3
*/
protected function remove_all_meta_actions() {
$this->remove_add_meta_action();
remove_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999, 5 );
remove_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ), 10, 4 );
remove_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ), 10, 2 );
remove_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ), 10, 4 );
}
/**
* Adds "added_{$this->meta_type}_meta" action
*
* @since 2.3
*/
protected function restore_add_meta_action() {
add_action( "added_{$this->meta_type}_meta", array( $this, 'add_meta' ), 10, 4 );
}
/**
* Adds meta synchronization actions and filters
*
* @since 2.3
*/
protected function add_all_meta_actions() {
$this->restore_add_meta_action();
add_filter( "update_{$this->meta_type}_metadata", array( $this, 'update_metadata' ), 999, 5 ); // Very late in case a filter prevents the meta to be updated
add_action( "update_{$this->meta_type}_meta", array( $this, 'update_meta' ), 10, 4 );
add_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ), 10, 2 );
add_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ), 10, 4 );
}
/**
* Maybe modify ("translate") a meta value when it is copied or synchronized
*
* @since 2.3
*
* @param mixed $value Meta value
* @param string $key Meta key
* @param int $from Id of the source
* @param int $to Id of the target
* @param string $lang Language of target
* @return mixed
*/
protected function maybe_translate_value( $value, $key, $from, $to, $lang ) {
/**
* Filter a meta value before is copied or synchronized
*
* @since 2.3
*
* @param mixed $value Meta value
* @param string $key Meta key
* @param string $lang Language of target
* @param int $from Id of the source
* @param int $to Id of the target
*/
return apply_filters( "pll_translate_{$this->meta_type}_meta", maybe_unserialize( $value ), $key, $lang, $from, $to );
}
/**
* Get the custom fields to copy or synchronize
*
* @since 2.3
*
* @param int $from Id of the post from which we copy informations
* @param int $to Id of the post to which we paste informations
* @param string $lang Language slug
* @param bool $sync True if it is synchronization, false if it is a copy
* @return array List of meta keys
*/
protected function get_metas_to_copy( $from, $to, $lang, $sync = false ) {
/**
* Filter the custom fields to copy or synchronize
*
* @since 0.6
* @since 1.9.2 The `$from`, `$to`, `$lang` parameters were added.
*
* @param array $keys List of custom fields names
* @param bool $sync True if it is synchronization, false if it is a copy
* @param int $from Id of the post from which we copy informations
* @param int $to Id of the post to which we paste informations
* @param string $lang Language slug
*/
return array_unique( apply_filters( "pll_copy_{$this->meta_type}_metas", array(), $sync, $from, $to, $lang ) );
}
/**
* Disallow modifying synchronized meta if the current user can not modify translations
*
* @since 2.6
*
* @param null|bool $check Whether to allow adding/updating/deleting metadata.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @return null|bool
*/
public function can_synchronize_metadata( $check, $id, $meta_key ) {
if ( ! $this->model->{$this->meta_type}->current_user_can_synchronize( $id ) ) {
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
$to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
if ( in_array( $meta_key, $to_copy ) ) {
return false;
}
}
}
}
return $check;
}
/**
* Synchronize added metas across translations
*
* @since 2.3
*
* @param int $mid Meta id.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
*/
public function add_meta( $mid, $id, $meta_key, $meta_value ) {
static $avoid_recursion = false;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
$to_copy = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
if ( in_array( $meta_key, $to_copy ) ) {
$meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
add_metadata( $this->meta_type, $tr_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value );
}
}
}
$avoid_recursion = false;
}
}
/**
* Stores the previous value when updating metas
*
* @since 2.3
*
* @param null|bool $r Not used
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
* @param mixed $prev_value If specified, only update existing metadata entries with the specified value.
* @return null|bool Unchanged
*/
public function update_metadata( $r, $id, $meta_key, $meta_value, $prev_value ) {
if ( null === $r ) {
$hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
$this->prev_value[ $hash ] = $prev_value;
}
return $r;
}
/**
* Synchronize updated metas across translations
*
* @since 2.3
*
* @param int $mid Meta id.
* @param int $id Object ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value. Must be serializable if non-scalar.
*/
public function update_meta( $mid, $id, $meta_key, $meta_value ) {
static $avoid_recursion = false;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$hash = md5( "$id|$meta_key|" . maybe_serialize( $meta_value ) );
$prev_meta = get_metadata_by_mid( $this->meta_type, $mid );
if ( $prev_meta ) {
$this->remove_add_meta_action(); // We don't want to sync back the new metas
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id && in_array( $meta_key, $this->get_metas_to_copy( $id, $tr_id, $lang, true ) ) ) {
if ( empty( $this->prev_value[ $hash ] ) || $this->prev_value[ $hash ] === $prev_meta->meta_value ) {
$prev_value = $this->maybe_translate_value( $prev_meta->meta_value, $meta_key, $id, $tr_id, $lang );
$meta_value = $this->maybe_translate_value( $meta_value, $meta_key, $id, $tr_id, $lang );
update_metadata( $this->meta_type, $tr_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value );
}
}
}
$this->restore_add_meta_action();
}
unset( $this->prev_value[ $hash ] );
$avoid_recursion = false;
}
}
/**
* Store metas to synchronize before deleting them
*
* @since 2.3
*
* @param array $mids Not used
* @param int $id Object ID.
*/
public function store_metas_to_sync( $mids, $id ) {
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
$this->to_copy[ $id ][ $tr_id ] = $this->get_metas_to_copy( $id, $tr_id, $lang, true );
}
}
/**
* Synchronize deleted meta across translations
*
* @since 2.3
*
* @param array $mids Not used
* @param int $id Object ID.
* @param string $key Meta key.
* @param mixed $value Meta value.
*/
public function delete_meta( $mids, $id, $key, $value ) {
static $avoid_recursion = false;
if ( ! $avoid_recursion ) {
$avoid_recursion = true;
$tr_ids = $this->model->{$this->meta_type}->get_translations( $id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id != $id ) {
if ( in_array( $key, $this->to_copy[ $id ][ $tr_id ] ) ) {
if ( '' !== $value && null !== $value && false !== $value ) { // Same test as WP
$value = $this->maybe_translate_value( $value, $key, $id, $tr_id, $lang );
}
delete_metadata( $this->meta_type, $tr_id, $key, is_string( $value ) ? wp_slash( $value ) : $value );
}
}
}
}
$avoid_recursion = false;
}
/**
* Copy or synchronize metas
*
* @since 2.3
*
* @param int $from Id of the source object
* @param int $to Id of the target object
* @param string $lang Language code of the target object
* @param bool $sync Optional, defaults to true. True if it is synchronization, false if it is a copy
*/
public function copy( $from, $to, $lang, $sync = false ) {
$this->remove_all_meta_actions();
remove_action( "delete_{$this->meta_type}_meta", array( $this, 'store_metas_to_sync' ), 10, 2 );
remove_action( "deleted_{$this->meta_type}_meta", array( $this, 'delete_meta' ), 10, 4 );
$to_copy = $this->get_metas_to_copy( $from, $to, $lang, $sync );
$metas = get_metadata( $this->meta_type, $from );
$tr_metas = get_metadata( $this->meta_type, $to );
foreach ( $to_copy as $key ) {
if ( empty( $metas[ $key ] ) ) {
if ( ! empty( $tr_metas[ $key ] ) ) {
// If the meta key is not present in the source object, delete all values
delete_metadata( $this->meta_type, $to, $key );
}
} else {
if ( ! empty( $tr_metas[ $key ] ) && 1 === count( $metas[ $key ] ) && 1 === count( $tr_metas[ $key ] ) ) {
// One custom field to update
$value = reset( $metas[ $key ] );
$value = maybe_unserialize( $value );
$to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
update_metadata( $this->meta_type, $to, $key, is_string( $to_value ) ? wp_slash( $to_value ) : $to_value );
} else {
// Multiple custom fields, either in the source or the target
if ( ! empty( $tr_metas[ $key ] ) ) {
// The synchronization of multiple values custom fields is easier if we delete all metas first
delete_metadata( $this->meta_type, $to, $key );
}
foreach ( $metas[ $key ] as $value ) {
$value = maybe_unserialize( $value );
$to_value = $this->maybe_translate_value( $value, $key, $from, $to, $lang );
add_metadata( $this->meta_type, $to, $key, is_string( $to_value ) ? wp_slash( $to_value ) : $to_value );
}
}
}
}
$this->add_all_meta_actions();
}
/**
* If synchronized custom fields were previously not synchronized, it is expected
* that saving a post (or term) will synchronize them.
*
* @since 2.3
*
* @param int $object_id Id of the object being asaved
* @param object $obj Not used
* @param array $translations The list of translations object ids
*/
public function save_object( $object_id, $obj, $translations ) {
$src_lang = array_search( $object_id, $translations );
foreach ( $translations as $tr_lang => $tr_id ) {
if ( $tr_id != $object_id ) {
$this->copy( $object_id, $tr_id, $tr_lang, true );
}
}
}
}
sync-tax.php 0000666 00000022341 15126445003 0007031 0 ustar 00 <?php
/**
* A class to manage the sychronization of taxonomy terms across posts translations
*
* @since 2.3
*/
class PLL_Sync_Tax {
/**
* Constructor
*
* @since 2.3
*
* @param object $polylang
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->options = &$polylang->options;
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
add_action( 'pll_save_term', array( $this, 'create_term' ), 10, 3 );
add_action( 'pre_delete_term', array( $this, 'pre_delete_term' ) );
add_action( 'delete_term', array( $this, 'delete_term' ) );
}
/**
* Get the list of taxonomies to copy or to synchronize
*
* @since 1.7
* @since 2.1 The `$from`, `$to`, `$lang` parameters were added.
*
* @param bool $sync True if it is synchronization, false if it is a copy
* @param int $from Id of the post from which we copy informations, optional, defaults to null
* @param int $to Id of the post to which we paste informations, optional, defaults to null
* @param string $lang Language slug, optional, defaults to null
* @return array List of taxonomy names
*/
protected function get_taxonomies_to_copy( $sync, $from = null, $to = null, $lang = null ) {
$taxonomies = ! $sync || in_array( 'taxonomies', $this->options['sync'] ) ? $this->model->get_translated_taxonomies() : array();
if ( ! $sync || in_array( 'post_format', $this->options['sync'] ) ) {
$taxonomies[] = 'post_format';
}
/**
* Filter the taxonomies to copy or synchronize
*
* @since 1.7
* @since 2.1 The `$from`, `$to`, `$lang` parameters were added.
*
* @param array $taxonomies List of taxonomy names
* @param bool $sync True if it is synchronization, false if it is a copy
* @param int $from Id of the post from which we copy informations
* @param int $to Id of the post to which we paste informations
* @param string $lang Language slug
*/
return array_unique( apply_filters( 'pll_copy_taxonomies', $taxonomies, $sync, $from, $to, $lang ) );
}
/**
* When copying or synchronizing terms, translate terms in translatable taxonomies
*
* @since 2.3
*
* @param array $object_id Object ID
* @param array $terms List of terms ids assigned to the source post
* @param string $taxonomy Taxonomy name
* @param string $lang Language slug
* @return array List of terms ids to assign to the target post
*/
protected function maybe_translate_terms( $object_id, $terms, $taxonomy, $lang ) {
if ( is_array( $terms ) && $this->model->is_translated_taxonomy( $taxonomy ) ) {
$newterms = array();
// Convert to term ids if we got tag names
$strings = array_map( 'is_string', $terms );
if ( in_array( true, $strings, true ) ) {
$terms = get_the_terms( $object_id, $taxonomy );
$terms = wp_list_pluck( $terms, 'term_id' );
}
foreach ( $terms as $term ) {
/**
* Filter the translated term when a post translation is created or synchronized
*
* @since 2.3
*
* @param int $tr_term Translated term id
* @param int $term Source term id
* @param string $lang Language slug
*/
if ( $term_id = apply_filters( 'pll_maybe_translate_term', $this->model->term->get_translation( $term, $lang ), $term, $lang ) ) {
$newterms[] = (int) $term_id; // Cast is important otherwise we get 'numeric' tags
}
}
return $newterms;
}
return $terms; // Empty $terms or untranslated taxonomy
}
/**
* Maybe copy taxonomy terms from one post to the other
*
* @since 2.6
*
* @param int $object_id Source object ID.
* @param int $tr_id Target object ID.
* @param string $lang Target language.
* @param array $terms An array of object terms.
* @param string $taxonomy Taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
*/
protected function copy_object_terms( $object_id, $tr_id, $lang, $terms, $taxonomy, $append ) {
$to_copy = $this->get_taxonomies_to_copy( true, $object_id, $tr_id, $lang );
if ( in_array( $taxonomy, $to_copy ) ) {
$newterms = $this->maybe_translate_terms( $object_id, $terms, $taxonomy, $lang );
// For some reasons, the user may have untranslated terms in the translation. Don't forget them.
if ( $this->model->is_translated_taxonomy( $taxonomy ) ) {
$tr_terms = get_the_terms( $tr_id, $taxonomy );
if ( is_array( $tr_terms ) ) {
foreach ( $tr_terms as $term ) {
if ( ! $this->model->term->get_translation( $term->term_id, $this->model->post->get_language( $object_id ) ) ) {
$newterms[] = (int) $term->term_id;
}
}
}
}
wp_set_object_terms( $tr_id, $newterms, $taxonomy, $append );
}
}
/**
* When assigning terms to a post, assign translated terms to the translated posts (synchronisation)
*
* @since 2.3
*
* @param int $object_id Object ID.
* @param array $terms An array of object terms.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
*/
public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
static $avoid_recursion = false;
$taxonomy_object = get_taxonomy( $taxonomy );
// Make sure that the taxonomy is registered for a post type
if ( ! $avoid_recursion && array_filter( $taxonomy_object->object_type, 'post_type_exists' ) ) {
$avoid_recursion = true;
$tr_ids = $this->model->post->get_translations( $object_id );
foreach ( $tr_ids as $lang => $tr_id ) {
if ( $tr_id !== $object_id ) {
if ( $this->model->post->current_user_can_synchronize( $object_id ) ) {
$this->copy_object_terms( $object_id, $tr_id, $lang, $terms, $taxonomy, $append );
} else {
// No permission to synchronize, so let's synchronize in reverse order
$orig_lang = array_search( $object_id, $tr_ids );
$tr_terms = (array) get_the_terms( $tr_id, $taxonomy );
if ( is_array( $tr_terms ) ) {
$tr_terms = wp_list_pluck( $tr_terms, 'term_id' );
$this->copy_object_terms( $tr_id, $object_id, $orig_lang, $tr_terms, $taxonomy, $append );
}
break;
}
}
}
$avoid_recursion = false;
}
}
/**
* Copy terms fron one post to a translation, does not sync
*
* @since 2.3
*
* @param int $from Id of the source post
* @param int $to Id of the target post
* @param string $lang Language slug
*/
public function copy( $from, $to, $lang ) {
remove_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 6 );
// Get taxonomies to sync for this post type
$taxonomies = array_intersect( get_post_taxonomies( $from ), $this->get_taxonomies_to_copy( false, $from, $to, $lang ) );
// Update the term cache to reduce the number of queries in the loop
update_object_term_cache( $from, get_post_type( $from ) );
// Copy
foreach ( $taxonomies as $tax ) {
if ( $terms = get_the_terms( $from, $tax ) ) {
$terms = array_map( 'intval', wp_list_pluck( $terms, 'term_id' ) );
$newterms = $this->maybe_translate_terms( $from, $terms, $tax, $lang );
if ( ! empty( $newterms ) ) {
wp_set_object_terms( $to, $newterms, $tax );
}
}
}
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 6 );
}
/**
* When creating a new term, associate it to posts having translations associated to the translated terms
*
* @since 2.3
*
* @param int $term_id Id of the created term
* @param string $taxonomy Taxonomy
* @param array $translations Ids of the translations of the created term
*/
public function create_term( $term_id, $taxonomy, $translations ) {
if ( doing_action( 'create_term' ) && in_array( $taxonomy, $this->get_taxonomies_to_copy( true ) ) ) {
// Get all posts associated to the translated terms
$tr_posts = get_posts(
array(
'numberposts' => -1,
'nopaging' => true,
'post_type' => 'any',
'post_status' => 'any',
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => $taxonomy,
'field' => 'id',
'terms' => array_merge( array( $term_id ), array_values( $translations ) ),
'include_children' => false,
),
),
)
);
$lang = $this->model->term->get_language( $term_id ); // Language of the created term
$posts = array();
foreach ( $tr_posts as $post_id ) {
$post = $this->model->post->get_translation( $post_id, $lang );
if ( $post ) {
$posts[] = $post;
}
}
$posts = array_unique( $posts );
foreach ( $posts as $post_id ) {
if ( current_user_can( 'assign_term', $term_id ) ) {
wp_set_object_terms( $post_id, $term_id, $taxonomy, true );
}
}
}
}
/**
* Deactivate the synchronization of terms before deleting a term
* to avoid translated terms to be removed from translated posts
*
* @since 2.3.2
*/
public function pre_delete_term() {
remove_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
}
/**
* Re-activate the synchronization of terms after a term is deleted
*
* @since 2.3.2
*/
public function delete_term() {
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 5 );
}
}
settings-sync.php 0000666 00000005117 15126445003 0010077 0 ustar 00 <?php
/**
* Settings class for synchronization settings management
*
* @since 1.8
*/
class PLL_Settings_Sync extends PLL_Settings_Module {
/**
* Constructor
*
* @since 1.8
*
* @param object $polylang polylang object
*/
public function __construct( &$polylang ) {
parent::__construct(
$polylang,
array(
'module' => 'sync',
'title' => __( 'Synchronization', 'polylang' ),
'description' => __( 'The synchronization options allow to maintain exact same values (or translations in the case of taxonomies and page parent) of meta content between the translations of a post or page.', 'polylang' ),
)
);
}
/**
* Deactivates the module
*
* @since 1.8
*/
public function deactivate() {
$this->options['sync'] = array();
update_option( 'polylang', $this->options );
}
/**
* Displays the settings form
*
* @since 1.8
*/
protected function form() {
?>
<ul class="pll-inline-block-list">
<?php
foreach ( self::list_metas_to_sync() as $key => $str ) {
printf(
'<li><label><input name="sync[%s]" type="checkbox" value="1" %s /> %s</label></li>',
esc_attr( $key ),
checked( in_array( $key, $this->options['sync'] ), true, false ),
esc_html( $str )
);
}
?>
</ul>
<?php
}
/**
* Sanitizes the settings before saving
*
* @since 1.8
*
* @param array $options
*/
protected function update( $options ) {
$newoptions['sync'] = empty( $options['sync'] ) ? array() : array_keys( $options['sync'], 1 );
return $newoptions; // take care to return only validated options
}
/**
* Get the row actions
*
* @since 1.8
*
* @return array
*/
protected function get_actions() {
return empty( $this->options['sync'] ) ? array( 'configure' ) : array( 'configure', 'deactivate' );
}
/**
* List the post metas to synchronize
*
* @since 1.0
*
* @return array
*/
public static function list_metas_to_sync() {
return array(
'taxonomies' => __( 'Taxonomies', 'polylang' ),
'post_meta' => __( 'Custom fields', 'polylang' ),
'comment_status' => __( 'Comment status', 'polylang' ),
'ping_status' => __( 'Ping status', 'polylang' ),
'sticky_posts' => __( 'Sticky posts', 'polylang' ),
'post_date' => __( 'Published date', 'polylang' ),
'post_format' => __( 'Post format', 'polylang' ),
'post_parent' => __( 'Page parent', 'polylang' ),
'_wp_page_template' => __( 'Page template', 'polylang' ),
'menu_order' => __( 'Page order', 'polylang' ),
'_thumbnail_id' => __( 'Featured image', 'polylang' ),
);
}
}
sync.php 0000666 00000015661 15126445003 0006246 0 ustar 00 <?php
/**
* Manages copy and synchronization of terms and post metas on front
*
* @since 2.4
*/
class PLL_Sync {
public $taxonomies, $post_metas, $term_meta;
/**
* Constructor
*
* @since 1.2
*
* @param object $polylang
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->options = &$polylang->options;
$this->taxonomies = new PLL_Sync_Tax( $polylang );
$this->post_metas = new PLL_Sync_Post_Metas( $polylang );
$this->term_metas = new PLL_Sync_Term_Metas( $polylang );
add_filter( 'wp_insert_post_parent', array( $this, 'can_sync_post_parent' ), 10, 3 );
add_filter( 'wp_insert_post_data', array( $this, 'can_sync_post_data' ), 10, 2 );
add_action( 'pll_save_post', array( $this, 'pll_save_post' ), 10, 3 );
add_action( 'created_term', array( $this, 'sync_term_parent' ), 10, 3 );
add_action( 'edited_term', array( $this, 'sync_term_parent' ), 10, 3 );
add_action( 'pll_duplicate_term', array( $this->term_metas, 'copy' ), 10, 3 );
if ( $this->options['media_support'] ) {
add_action( 'pll_translate_media', array( $this->taxonomies, 'copy' ), 10, 3 );
add_action( 'pll_translate_media', array( $this->post_metas, 'copy' ), 10, 3 );
add_action( 'edit_attachment', array( $this, 'edit_attachment' ) );
}
add_filter( 'pre_update_option_sticky_posts', array( $this, 'sync_sticky_posts' ), 10, 2 );
}
/**
* Get post fields to synchornize
*
* @since 2.4
*
* @param object $post Post object
* @return array
*/
protected function get_fields_to_sync( $post ) {
$postarr = array();
foreach ( array( 'comment_status', 'ping_status', 'menu_order' ) as $property ) {
if ( in_array( $property, $this->options['sync'] ) ) {
$postarr[ $property ] = $post->$property;
}
}
if ( in_array( 'post_date', $this->options['sync'] ) ) {
$postarr['post_date'] = $post->post_date;
$postarr['post_date_gmt'] = $post->post_date_gmt;
}
if ( in_array( 'post_parent', $this->options['sync'] ) ) {
$postarr['post_parent'] = wp_get_post_parent_id( $post->ID );
}
return $postarr;
}
/**
* Prevents synchronized post parent modification if the current user hasn't enough rights
*
* @since 2.6
*
* @param int $post_parent Post parent ID
* @param int $post_id Post ID, unused
* @param array $postarr Array of parsed post data
* @return int
*/
public function can_sync_post_parent( $post_parent, $post_id, $postarr ) {
if ( ! empty( $postarr['ID'] ) && ! $this->model->post->current_user_can_synchronize( $postarr['ID'] ) ) {
$tr_ids = $this->model->post->get_translations( $postarr['ID'] );
$orig_lang = array_search( $postarr['ID'], $tr_ids );
foreach ( $tr_ids as $tr_id ) {
if ( $tr_id !== $postarr['ID'] && $post = get_post( $tr_id ) ) {
$post_parent = $post->post_parent;
break;
}
}
}
return $post_parent;
}
/**
* Prevents synchronized post data modification if the current user hasn't enough rights
*
* @since 2.6
*
* @param array $data An array of slashed post data.
* @param array $postarr An array of sanitized, but otherwise unmodified post data.
* @return array
*/
public function can_sync_post_data( $data, $postarr ) {
if ( ! empty( $postarr['ID'] ) && ! $this->model->post->current_user_can_synchronize( $postarr['ID'] ) ) {
foreach ( $this->model->post->get_translations( $postarr['ID'] ) as $tr_id ) {
if ( $tr_id !== $postarr['ID'] && $post = get_post( $tr_id ) ) {
$to_sync = $this->get_fields_to_sync( $post );
$data = array_merge( $data, $to_sync );
break;
}
}
}
return $data;
}
/**
* Synchronizes post fields in translations
*
* @since 2.4
*
* @param int $post_id post id
* @param object $post post object
* @param array $translations post translations
*/
public function pll_save_post( $post_id, $post, $translations ) {
global $wpdb;
if ( $this->model->post->current_user_can_synchronize( $post_id ) ) {
$postarr = $this->get_fields_to_sync( $post );
if ( ! empty( $postarr ) ) {
foreach ( $translations as $lang => $tr_id ) {
if ( ! $tr_id || $tr_id === $post_id ) {
continue;
}
$tr_arr = $postarr;
unset( $tr_arr['post_parent'] );
// Do not udpate the translation parent if the user set a parent with no translation
if ( isset( $postarr['post_parent'] ) ) {
$post_parent = $postarr['post_parent'] ? $this->model->post->get_translation( $postarr['post_parent'], $lang ) : 0;
if ( ! ( $postarr['post_parent'] && ! $post_parent ) ) {
$tr_arr['post_parent'] = $post_parent;
}
}
// Update all the row at once
// Don't use wp_update_post to avoid infinite loop
$wpdb->update( $wpdb->posts, $tr_arr, array( 'ID' => $tr_id ) );
clean_post_cache( $tr_id );
}
}
}
}
/**
* Synchronize term parent in translations
* Calling clean_term_cache *after* this is mandatory otherwise the $taxonomy_children option is not correctly updated
*
* @since 2.3
*
* @param int $term_id Term id.
* @param int $tt_id Term taxonomy id, not used.
* @param string $taxonomy Taxonomy name.
*/
public function sync_term_parent( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
if ( is_taxonomy_hierarchical( $taxonomy ) && $this->model->is_translated_taxonomy( $taxonomy ) ) {
$term = get_term( $term_id );
$translations = $this->model->term->get_translations( $term_id );
foreach ( $translations as $lang => $tr_id ) {
if ( ! empty( $tr_id ) && $tr_id !== $term_id ) {
$tr_parent = $this->model->term->get_translation( $term->parent, $lang );
$wpdb->update(
$wpdb->term_taxonomy,
array( 'parent' => isset( $tr_parent ) ? $tr_parent : 0 ),
array( 'term_taxonomy_id' => get_term( (int) $tr_id, $taxonomy )->term_taxonomy_id )
);
clean_term_cache( $tr_id, $taxonomy ); // OK since WP 3.9
}
}
}
}
/**
* Synchronizes terms and metas in translations for media
*
* @since 1.8
*
* @param int $post_id post id
*/
public function edit_attachment( $post_id ) {
$this->pll_save_post( $post_id, get_post( $post_id ), $this->model->post->get_translations( $post_id ) );
}
/**
* Synchronize sticky posts
*
* @since 2.3
*
* @param array $value New option value
* @param array $old_value Old option value
* @return array
*/
public function sync_sticky_posts( $value, $old_value ) {
if ( in_array( 'sticky_posts', $this->options['sync'] ) ) {
// Stick post
if ( $sticked = array_diff( $value, $old_value ) ) {
$translations = $this->model->post->get_translations( reset( $sticked ) );
$value = array_unique( array_merge( $value, array_values( $translations ) ) );
}
// Unstick post
if ( $unsticked = array_diff( $old_value, $value ) ) {
$translations = $this->model->post->get_translations( reset( $unsticked ) );
$value = array_unique( array_diff( $value, array_values( $translations ) ) );
}
}
return $value;
}
}
sync-term-metas.php 0000666 00000000521 15126445003 0010307 0 ustar 00 <?php
/**
* A class to manage copy and synchronization of term metas
*
* @since 2.3
*/
class PLL_Sync_Term_Metas extends PLL_Sync_Metas {
/**
* Constructor
*
* @since 2.3
*
* @param object $polylang
*/
public function __construct( &$polylang ) {
$this->meta_type = 'term';
parent::__construct( $polylang );
}
}