first commit

This commit is contained in:
2023-09-12 21:41:04 +02:00
commit 3361a7f053
13284 changed files with 2116755 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
use WPML\FP\Curryable;
use WPML\FP\Fns;
use WPML\FP\Lst;
use WPML\FP\Obj;
use WPML\FP\Str;
use function WPML\FP\pipe;
/**
* Class BatchBuilder
*
* @method static callable|\WPML_TM_Translation_Batch|null buildPostsBatch( ...$data, ...$sourceLanguage, ...$translators ) - Curried :: array->string->array->\WPML_TM_Translation_Batch|null
* @method static callable|\WPML_TM_Translation_Batch|null buildStringsBatch( ...$data, ...$sourceLanguage, ...$translators ) - Curried :: array->string->array->\WPML_TM_Translation_Batch|null
* @method static callable|array getPostElements( ...$postsForTranslation, ...$sourceLanguage ) - Curried :: array->string->array
* @method static callable|array getStringElements( ...$stringsForTranslation, ...$sourceLanguage ) - Curried :: array->string->array
*/
class BatchBuilder {
use Curryable;
public static function init() {
self::curryN( 'buildPostsBatch', 3, function ( array $data, $sourceLanguage, array $translators ) {
return self::build(
'Translation-%s-%s',
self::getPostElements(),
$data,
$sourceLanguage,
$translators
);
} );
self::curryN( 'buildStringsBatch', 3, function ( array $data, $sourceLanguage, array $translators ) {
return self::build(
'Strings translation-%s-%s',
self::getStringElements(),
$data,
$sourceLanguage,
$translators
);
} );
self::curryN( 'getPostElements', 2, function ( $postsForTranslation, $sourceLanguage ) {
$elements = [];
foreach ( $postsForTranslation as $postId => $postData ) {
$elements[] = new \WPML_TM_Translation_Batch_Element(
$postId,
$postData['type'],
$sourceLanguage,
array_fill_keys( $postData['target_languages'], \TranslationManagement::TRANSLATE_ELEMENT_ACTION ),
Obj::propOr( [], 'media', $postData )
);
}
return $elements;
} );
self::curryN( 'getStringElements', 2, function ( $stringsForTranslation, $sourceLanguage ) {
$elements = [];
$setTranslateAction = pipe(
Fns::map( pipe( Lst::makePair( \TranslationManagement::TRANSLATE_ELEMENT_ACTION ), Lst::reverse() ) ),
Lst::fromPairs()
);
foreach ( $stringsForTranslation as $stringId => $targetLanguages ) {
$elements[] = new \WPML_TM_Translation_Batch_Element(
$stringId,
'string',
$sourceLanguage,
$setTranslateAction( $targetLanguages )
);
}
return $elements;
} );
}
/**
* @param string $batchNameTemplate
* @param callable $buildElementStrategy
* @param array $data
* @param string $sourceLanguage
* @param array $translators
*
* @return \WPML_TM_Translation_Batch|null
*/
private static function build( $batchNameTemplate, callable $buildElementStrategy, array $data, $sourceLanguage, array $translators ) {
$targetLanguagesString = pipe( Lst::flatten(), 'array_unique', Lst::join( '|' ) );
$idsHash = pipe( 'array_keys', Lst::join( '-' ), 'md5', Str::sub( 16 ) );
$batchName = sprintf(
$batchNameTemplate,
$targetLanguagesString( $data ),
$idsHash( $data )
);
$elements = apply_filters(
'wpml_tm_batch_factory_elements',
$buildElementStrategy( $data, $sourceLanguage ),
$batchName
);
return $elements ? new \WPML_TM_Translation_Batch( $elements, $batchName, $translators, null ) : null;
}
}
BatchBuilder::init();

View File

@@ -0,0 +1,112 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
use Exception;
use WPML\API\Sanitize;
use WPML\FP\Fns;
use WPML\FP\Lst;
use WPML\FP\Obj;
use WPML\LIB\WP\User;
use WPML\TM\API\Jobs;
use function WPML\Container\make;
use function WPML\FP\pipe;
abstract class Elements {
/**
* @param callable $sendBatch
* @param Messages $messages
* @param callable $buildBatch
* @param array $data
* @param string $type
*/
public static function dispatch(
callable $sendBatch,
Messages $messages,
callable $buildBatch,
$data,
$type
) {
$translationActions = filter_var_array(
Obj::propOr( [], 'tr_action', $data ),
FILTER_SANITIZE_NUMBER_INT
);
$sourceLanguage = Sanitize::stringProp( 'translate_from', $data );
$targetLanguages = self::getTargetLanguages( $translationActions );
$translators = self::getTranslators( $sourceLanguage, $targetLanguages );
$elementsForTranslation = self::getElements( $messages, $data[ $type ], $targetLanguages );
$batch = $buildBatch( $elementsForTranslation, $sourceLanguage, $translators );
$batch && $sendBatch( $messages, $batch );
}
private static function getTargetLanguages( $translationActions ) {
return array_keys(
array_filter( $translationActions, function ( $action ) {
return (int) $action === \TranslationManagement::TRANSLATE_ELEMENT_ACTION;
} )
);
}
private static function getTranslators( $sourceLanguage, $targetLanguages ) {
$records = make( \WPML_Translator_Records::class );
$getTranslator = function ( $lang ) use ( $sourceLanguage, $records ) {
$translators = $records->get_users_with_languages( $sourceLanguage, [ $lang ] );
return count( $translators ) ? $translators[0] : User::getCurrent();
};
$translators = wpml_collect( $targetLanguages )
->map( $getTranslator )
->map( Obj::prop( 'ID') );
return Lst::zipObj( $targetLanguages, $translators->toArray() );
}
private static function getElements(
Messages $messages,
$data,
$targetLanguages
) {
$getElementsToTranslate = pipe( Fns::filter( Obj::prop( 'checked' ) ), Lst::keyBy( 'checked' ) );
$elementsIds = $getElementsToTranslate( $data );
list( $elementsToTranslation, $ignoredElementsMessages ) = static::filterElements(
$messages,
$elementsIds,
$targetLanguages
);
$messages->showForPosts( $ignoredElementsMessages, 'information' );
return array_filter( $elementsToTranslation, pipe( Obj::prop( 'target_languages' ), Lst::length() ) );
}
/**
* @param int $elementId
* @param string $elementType
* @param string $language
*
* @return bool
*/
protected static function hasInProgressJob( $elementId, $elementType, $language ) {
$job = Jobs::getElementJob( $elementId, $elementType, $language );
return $job && ICL_TM_IN_PROGRESS === (int) $job->status && ! $job->needs_update;
}
/**
* @param Messages $messages
* @param array $elementsData
* @param array $targetLanguages
*
* phpcs:disable Squiz.Commenting.FunctionComment.InvalidNoReturn
* @return array
* @throws Exception Throws an exception if the method is not properly extended.
*/
protected static function filterElements( Messages $messages, $elementsData, $targetLanguages ) {
throw new Exception( ' this method is mandatory' );
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
class Messages {
/**
* @param \WP_Post $post
* @param string $language
*
* @return string
*/
public function ignoreOriginalPostMessage( $post, $language ) {
return sprintf(
__(
'Post "%1$s" will be ignored for %2$s, because it is an original post.',
'wpml-translation-management'
),
$post->post_title,
$language
);
}
/**
* @param \WP_Post $post
* @param string $language
*
* @return string
*/
public function ignoreInProgressPostMessage( $post, $language ) {
return sprintf(
__(
'Post "%1$s" will be ignored for %2$s, because translation is already in progress.',
'wpml-translation-management'
),
$post->post_title,
$language
);
}
/**
* @param \WPML_ST_String $string
* @param $language
*
* @return string
*/
public function ignoreInProgressStringMessage( \WPML_ST_String $string, $language ) {
return sprintf(
__(
'String "%1$s" will be ignored for %2$s, because translation is already waiting for translator.',
'wpml-translation-management'
),
$string->get_value(),
$language
);
}
/**
* @param \WPML_Package $package
* @param string $language
*
* @return string
*/
public function ignoreInProgressPackageMessage( $package, $language ) {
return sprintf(
__(
'Package "%1$s" will be ignored for %2$s, because translation is already in progress.',
'wpml-translation-management'
),
$package->title,
$language
);
}
/**
* @param \WPML_Package $package
* @param string $language
*
* @return string
*/
public function ignoreOriginalPackageMessage( $package, $language ) {
return sprintf(
__(
'Package "%1$s" will be ignored for %2$s, because it is an original post.',
'wpml-translation-management'
),
$package->title,
$language
);
}
/**
* @param array $messages
* @param string $type
*/
public function showForPosts( array $messages, $type ) {
$this->show(
'translation-basket-notification',
[ WPML_TM_FOLDER . '/menu/main.php' ],
'translation-basket-notification',
$messages,
$type
);
}
/**
* @param array $messages
* @param string $type
*/
public function showForStrings( array $messages, $type ) {
if ( defined( 'WPML_ST_FOLDER' ) ) {
$this->show(
'string-translation-top',
[ WPML_ST_FOLDER . '/menu/string-translation.php' ],
'string-translation-top',
$messages,
$type
);
}
}
/**
* @param string $id
* @param array $pages
* @param string $group
* @param array $messages
* @param string $type
*/
private function show( $id, array $pages, $group, array $messages, $type ) {
if ( $messages ) {
$messageArgs = [
'id' => $id,
'text' => '<ul><li>' . implode( '</li><li>', $messages ) . '</li></ul>',
'classes' => 'small',
'type' => $type,
'group' => $group,
'admin_notice' => true,
'hide_per_user' => false,
'dismiss_per_user' => false,
'limit_to_page' => $pages,
'show_once' => true,
];
\ICL_AdminNotifier::add_message( $messageArgs );
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
use WPML\Element\API\Languages;
class Packages extends Elements {
public static function dispatch(
callable $sendBatch,
Messages $messages,
callable $buildBatch,
$data,
$type = 'package'
) {
parent::dispatch( $sendBatch, $messages, $buildBatch, $data, $type );
}
protected static function filterElements( Messages $messages, $packagesData, $targetLanguages ) {
$ignoredPackagesMessages = [];
$packagesToTranslation = [];
foreach ( $packagesData as $packageId => $packageData ) {
$packagesToTranslation[ $packageId ] = [
'type' => $packageData['type'],
'target_languages' => []
];
$package = apply_filters( 'wpml_get_translatable_item', null, $packageId, 'package' );
$packageLang = apply_filters( 'wpml_language_for_element', Languages::getDefaultCode(), $package );
foreach ( $targetLanguages as $language ) {
if ( $packageLang === $language ) {
$ignoredPackagesMessages [] = $messages->ignoreOriginalPackageMessage( $package, $language );
continue;
}
if ( self::hasInProgressJob(
$package->ID,
$package->get_element_type_prefix() . '_' . $package->kind_slug,
$language
) ) {
$ignoredPackagesMessages [] = $messages->ignoreInProgressPackageMessage( $package, $language );
continue;
}
$packagesToTranslation[ $packageId ]['target_languages'] [] = $language;
}
}
return [ $packagesToTranslation, $ignoredPackagesMessages ];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
use WPML\Element\API\Post;
use WPML\FP\Obj;
use WPML\FP\Str;
class Posts extends Elements {
public static function dispatch(
callable $sendBatch,
Messages $messages,
callable $buildBatch,
$data,
$type = 'post'
) {
parent::dispatch( $sendBatch, $messages, $buildBatch, $data, $type );
}
protected static function filterElements( Messages $messages, $postsData, $targetLanguages ) {
$ignoredPostsMessages = [];
$postsToTranslation = [];
foreach ( $postsData as $postId => $postData ) {
$postsToTranslation[ $postId ] = [
'type' => $postData['type'],
'media' => Obj::propOr( [], 'media-translation', $postData ),
'target_languages' => []
];
$post = self::getPost( $postId );
$postLang = Post::getLang( $postId );
foreach ( $targetLanguages as $language ) {
if ( $postLang === $language ) {
$ignoredPostsMessages [] = $messages->ignoreOriginalPostMessage( $post, $language );
continue;
}
if ( self::hasInProgressJob( $post->ID, $post->post_type, $language ) ) {
$ignoredPostsMessages [] = $messages->ignoreInProgressPostMessage( $post, $language );
continue;
}
$postsToTranslation[ $postId ]['target_languages'] [] = $language;
}
}
return [ $postsToTranslation, $ignoredPostsMessages ];
}
private static function getPost( $postId ) {
return Str::includes( 'external_', $postId ) ?
apply_filters( 'wpml_get_translatable_item', null, $postId ) :
get_post( $postId );
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace WPML\TM\Jobs\Dispatch;
use WPML\FP\Lst;
use WPML\FP\Obj;
use WPML\LIB\WP\User;
use function WPML\Container\make;
class Strings {
/**
* @param callable $sendBatch
* @param \WPML\TM\Jobs\Dispatch\Messages $messages
* @param callable $buildBatch
* @param $stringIds
* @param $sourceLanguage
* @param $targetLanguages
*/
public static function dispatch(
callable $sendBatch,
Messages $messages,
callable $buildBatch,
$stringIds,
$sourceLanguage,
$targetLanguages
) {
$stringsForTranslation = self::filterStringsForTranslation( $messages, $stringIds, $targetLanguages );
$translators = array_fill_keys( $targetLanguages, User::getCurrentId() );
$batch = $buildBatch( $stringsForTranslation, $sourceLanguage, $translators );
$batch && $sendBatch( $messages, $batch );
}
private static function filterStringsForTranslation( Messages $messages, $stringIds, $targetLanguages ) {
$stringsToTranslation = [];
$ignoredStringsMessages = [];
/** @var \WPML_ST_String_Factory $stringFactory */
$stringFactory = make( \WPML_ST_String_Factory::class );
foreach ( $stringIds as $stringId ) {
$stringsToTranslation[ $stringId ] = [];
$string = $stringFactory->find_by_id( $stringId );
$statuses = wpml_collect( $string->get_translation_statuses() )->keyBy( 'language' )->map( Obj::prop( 'status' ) );
foreach ( $targetLanguages as $language ) {
if ( (int) Obj::prop( $language, $statuses ) === ICL_TM_WAITING_FOR_TRANSLATOR ) {
$ignoredStringsMessages[] = $messages->ignoreInProgressStringMessage( $string, $language );
} else {
$stringsToTranslation[ $stringId ] [] = $language;
}
}
}
$messages->showForStrings( $ignoredStringsMessages, 'information' );
return array_filter( $stringsToTranslation, Lst::length() );
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace WPML\TM\Jobs;
use WPML\FP\Obj;
use WPML\FP\Str;
use WPML\FP\Relation;
use WPML\FP\Fns;
use function WPML\FP\pipe;
class ExtraFieldDataInEditor implements \IWPML_Backend_Action {
const MAX_ALLOWED_SINGLE_LINE_LENGTH = 50;
/** @var \WPML_Custom_Field_Editor_Settings */
private $customFieldEditorSettings;
public function __construct( \WPML_Custom_Field_Editor_Settings $customFieldEditorSettings ) {
$this->customFieldEditorSettings = $customFieldEditorSettings;
}
public function add_hooks() {
add_filter( 'wpml_tm_adjust_translation_fields', [ $this, 'appendTitleAndStyle' ], 1, 3 );
}
public function appendTitleAndStyle( array $fields, $job, $originalPost ) {
$appendTitleAndStyleStrategy = $this->isExternalElement( $job ) ?
$this->appendToExternalField( $originalPost ) :
$this->addTitleAndAdjustStyle( $job, $originalPost );
return Fns::map( pipe( $appendTitleAndStyleStrategy, $this->adjustFieldStyleForUnsafeContent() ), $fields );
}
private function addTitleAndAdjustStyle( $job, $originalPost ) {
return function ( $field ) use ( $job, $originalPost ) {
if ( FieldId::is_a_custom_field( $field['field_type'] ) ) {
return $this->appendToCustomField( $field, $job, $originalPost );
} elseif ( FieldId::is_a_term( $field['field_type'] ) ) {
return $this->appendToTerm( $field );
}
return $this->appendToRegularField( $field );
};
}
private function isExternalElement( $job ) {
return isset( $job->element_type_prefix ) && wpml_load_core_tm()->is_external_type( $job->element_type_prefix );
}
private function appendToExternalField( $originalPost ) {
return function ( $field ) use ( $originalPost ) {
$field['title'] = apply_filters( 'wpml_tm_editor_string_name', $field['field_type'], $originalPost );
$field['field_style'] = $this->applyStyleFilter(
Obj::propOr( '', 'field_style', $field ),
$field['field_type'],
$originalPost
);
return $field;
};
}
private function appendToCustomField( $field, $job, $originalPost ) {
$title = $this->getCustomFieldTitle( $field );
$style = $this->getCustomFieldStyle( $field );
$field = (array) apply_filters( 'wpml_editor_cf_to_display', (object) $field, $job );
$field['title'] = $title;
$field['field_style'] = $this->getAdjustedFieldStyle( $field, $style );
$field['field_style'] = $this->applyStyleFilter( $field['field_style'], $field['field_type'], $originalPost );
return $field;
}
private function appendToTerm( $field ) {
$field['title'] = '';
return $field;
}
private function applyStyleFilter( $style, $type, $originalPost ) {
return (string) apply_filters( 'wpml_tm_editor_string_style', $style, $type, $originalPost );
}
private function appendToRegularField( $field ) {
$field['title'] = \wpml_collect(
[
'title' => __( 'Title', 'wpml-translation-management' ),
'body' => __( 'Body', 'wpml-translation-management' ),
'excerpt' => __( 'Excerpt', 'wpml-translation-management' ),
]
)->get( $field['field_type'], $field['field_type'] );
if ( $field['field_type'] === 'excerpt' ) {
$field['field_style'] = '1';
}
return $field;
}
private function getCustomFieldTitle( $field ) {
$unfiltered_type = \WPML_TM_Field_Type_Sanitizer::sanitize( $field['field_type'] );
$element_field_type = $unfiltered_type;
/**
* @deprecated Use `wpml_editor_custom_field_name` filter instead
* @since 3.2
*/
$element_field_type = apply_filters( 'icl_editor_cf_name', $element_field_type );
$element_field_type = apply_filters( 'wpml_editor_custom_field_name', $element_field_type );
return $this->customFieldEditorSettings->filter_name( $unfiltered_type, $element_field_type );
}
private function getCustomFieldStyle( $field ) {
$type = \WPML_TM_Field_Type_Sanitizer::sanitize( $field['field_type'] );
$style = Str::includes( "\n", $field['field_data'] ) ? 1 : 0;
/**
* @deprecated Use `wpml_editor_custom_field_style` filter instead
* @since 3.2
*/
$style = apply_filters( 'icl_editor_cf_style', $style, $type );
$style = apply_filters( 'wpml_editor_custom_field_style', $style, $type );
return $this->customFieldEditorSettings->filter_style( $type, $style );
}
private function getAdjustedFieldStyle( array $field, $style ) {
/**
* wpml_tm_editor_max_allowed_single_line_length filter
*
* Filters the value of `\WPML_Translation_Editor_UI::MAX_ALLOWED_SINGLE_LINE_LENGTH`
*
* @param int MAX_ALLOWED_SINGLE_LINE_LENGTH The length of the string, after which it must use a multiline input
* @param array $field The generic field data
* @param array $custom_field_data The custom field specific data
*
* @since 2.3.1
*/
$maxAllowedLength = (int) apply_filters(
'wpml_tm_editor_max_allowed_single_line_length',
self::MAX_ALLOWED_SINGLE_LINE_LENGTH,
$field,
[ $field['title'], $style, $field ]
);
return 0 === (int) $style && strlen( $field['field_data'] ) > $maxAllowedLength ? '1' : $style;
}
private function adjustFieldStyleForUnsafeContent() {
return function ( array $field ) {
if ( Relation::propEq( 'field_style', '2', $field ) ) {
$black_list = [ 'script', 'style', 'iframe' ];
$black_list_pattern = '#</?(' . implode( '|', $black_list ) . ')[^>]*>#i';
if ( preg_replace( $black_list_pattern, '', $field['field_data'] ) !== $field['field_data'] ) {
$field['field_style'] = '1';
}
}
return $field;
};
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace WPML\TM\Jobs;
class ExtraFieldDataInEditorFactory implements \IWPML_Backend_Action_Loader {
/**
* @return ExtraFieldDataInEditor
*/
public function create() {
return new ExtraFieldDataInEditor(
new \WPML_Custom_Field_Editor_Settings(
new \WPML_Custom_Field_Setting_Factory( wpml_load_core_tm() )
)
);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace WPML\TM\Jobs;
use WPML\Collect\Support\Traits\Macroable;
use WPML\FP\Fns;
use WPML\FP\Logic;
use WPML\FP\Lst;
use WPML\FP\Str;
use function WPML\FP\curryN;
use function WPML\FP\pipe;
/**
* Class FieldId
*
* @package WPML\TM\Jobs
* @method static callable|int get_term_id( ...$field ) - Curried :: string → int
* @method static callable|int is_a_term( ...$field ) - Curried :: string → bool
* @method static callable|int is_a_term_description( ...$field ) - Curried :: string → bool
* @method static callable|int is_a_term_meta( ...$field ) - Curried :: string → bool
* @method static callable|int is_a_custom_field( ...$field ) - Curried :: string → bool
* @method static callable|int is_any_term_field( ...$field ) - Curried :: string → bool
* @method static callable|string forTerm( ...$termId ) - Curried :: int → string
* @method static callable|string forTermDescription( ...$termId ) - Curried :: int → string
* @method static callable|string forTermMeta( ...$termId, $key ) - Curried :: int → string → string
* @method static callable|string getTermMetaKey( ...$field ) - Curried :: string → string
*/
class FieldId {
use Macroable;
const TERM_PREFIX = 't_';
const TERM_DESCRIPTION_PREFIX = 'tdesc_';
const TERM_META_FIELD_PREFIX = 'tfield-';
const CUSTOM_FIELD_PREFIX = 'field-';
public static function init() {
self::macro( 'is_a_term', Str::startsWith( self::TERM_PREFIX ) );
self::macro( 'is_a_term_description', Str::startsWith( self::TERM_DESCRIPTION_PREFIX ) );
self::macro( 'is_a_term_meta', Str::startsWith( self::TERM_META_FIELD_PREFIX ) );
self::macro( 'is_a_custom_field', Str::startsWith( self::CUSTOM_FIELD_PREFIX ) );
self::macro(
'is_any_term_field',
Logic::anyPass( [ self::is_a_term(), self::is_a_term_description(), self::is_a_term_meta() ] )
);
self::macro(
'get_term_id',
curryN(
1,
Logic::cond(
[
[ self::is_a_term(), Str::sub( Str::len( self::TERM_PREFIX ) ) ],
[ self::is_a_term_description(), Str::sub( Str::len( self::TERM_DESCRIPTION_PREFIX ) ) ],
[ Fns::always( true ), pipe( Str::split( '-' ), Lst::last() ) ],
]
)
)
);
self::macro( 'forTerm', Str::concat( self::TERM_PREFIX ) );
self::macro( 'forTermDescription', Str::concat( self::TERM_DESCRIPTION_PREFIX ) );
self::macro(
'forTermMeta',
curryN(
2,
function ( $termId, $key ) {
return 'tfield-' . $key . '-' . $termId;
}
)
);
// getTermMetaKey :: string tfield-K-E-Y-ID → string K-E-Y ( KEY can have hyphens )
self::macro(
'getTermMetaKey',
curryN(
1,
pipe(
Str::sub( Str::len( self::TERM_META_FIELD_PREFIX ) ), // K-E-Y-ID
Str::split( '-' ), // [ K, E, Y, ID ]
Lst::dropLast( 1 ), // [ K, E, Y ]
Lst::join( '-' ) // K-E-Y
)
)
);
}
}
FieldId::init();

View File

@@ -0,0 +1,193 @@
<?php
namespace WPML\TM\Jobs;
use WPML\FP\Fns;
use WPML\FP\Obj;
use WPML\FP\Str;
use function WPML\FP\pipe;
class TermMeta {
/**
* It returns translated term description stored inside wp_icl_translate
*
* @param int $iclTranslateJobId
* @param int $termTaxonomyId
*
* @return string
*/
public static function getTermDescription( $iclTranslateJobId, $termTaxonomyId ) {
global $wpdb;
$sql = "SELECT field_data_translated
FROM {$wpdb->prefix}icl_translate
WHERE job_id = %d AND field_type = 'tdesc_%d'";
$description = $wpdb->get_var( $wpdb->prepare( $sql, $iclTranslateJobId, $termTaxonomyId ) );
return $description ? base64_decode( $description ) : '';
}
/**
* It returns term meta stored inside wp_icl_translate table.
*
* Data has such format:
* [
* (object)[
* field_type => 'some_scalar_field',
* field_data_translated => 'Translated value'
* ],
* (object)[
* field_type => 'some_array_valued_field_like_checkboxes'
* field_data_translated => [
* 'Translated option 1', 'Translated option 2', 'Translated option 3'
* ]
* ],
* (object)[
* field_type => 'another_array_valued_field_like_checkboxes'
* field_data_translated => [
* 'option1' => ['Translated option 1'],
* 'option2' => ['Translated option 2'],
* ]
* ]
* ]
*
* @param int $iclTranslateJobId
* @param int $term_taxonomy_id
*
* @return array
*/
public static function getTermMeta( $iclTranslateJobId, $term_taxonomy_id ) {
return array_merge(
self::geRegularTermMeta( $iclTranslateJobId, $term_taxonomy_id ),
self::getTermMetaWithArrayValue( $iclTranslateJobId, $term_taxonomy_id )
);
}
/**
* It returns term meta which have scalar values
*
* @param int $iclTranslateJobId
* @param int $termTaxonomyId
*
* @return mixed[]
*/
private static function geRegularTermMeta( $iclTranslateJobId, $termTaxonomyId ) {
global $wpdb;
$sql = "SELECT field_data_translated, field_type
FROM {$wpdb->prefix}icl_translate
WHERE job_id = %d AND field_type LIKE 'tfield-%-%d'";
$rowset = $wpdb->get_results( $wpdb->prepare( $sql, $iclTranslateJobId, $termTaxonomyId ) );
return Fns::map( Obj::over( Obj::lensProp( 'field_data_translated' ), 'base64_decode' ), $rowset );
}
/**
* It returns term meta with array values grouped by term name.
*
* Custom field created by Toolset Types example:
*
* A term has checkboxes field with options: A, B, and C. They are stored in wp_icl_translate table as 3 entries under such field_type:
* - tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1_0
* - tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-6cdwwdwdwdwdwwdwddwd2bb12fc2d1c4-1_0
* - tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-611111wdwdwdwwdwddwd2bb12fc2d1c4-1_0
*
* Options translations are A fr, B fr, and C fr.
*
* Our goal is to group them into one entry:
* (object) [
* field_type => 'tfield-wpcf-jakub-checkboxes-13'
* field_data_translated => [
* wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1 => [
* 0 => 'A fr',
* ],
* wpcf-fields-checkboxes-option-6cdwwdwdwdwdwwdwddwd2bb12fc2d1c4-1 => [
* 0 => 'B fr',
* ],
* wpcf-fields-checkboxes-option-611111wdwdwdwwdwddwd2bb12fc2d1c4-1 => [
* 0 => 'C fr',
* ],
* ]
* ]
*
* Custom field created by ACF example:
*
* ACF stores data in a slightly different way. Again, A term has checkboxes field with options A, B, C with the same translations A fr, B fr, C fr.
* They are stored in wp_icl_translate in this way:
* - tfield-jakub_checkboxes-13_0
* - tfield-jakub_checkboxes-13_1
* - tfield-jakub_checkboxes-13_2
*
* Our goal is to group them into one entry:
* (object)[
* field_type => 'tfield-jakub-checkboxes-13',
* field_data_translated => [
* 0 => 'A fr',
* 1 => 'B fr',
* 2 => 'C fr'
* ]
* ]
*
* @param int $iclTranslateJobId
* @param int $termTaxonomyId
*
* @return mixed[]
*/
private static function getTermMetaWithArrayValue( $iclTranslateJobId, $termTaxonomyId ) {
global $wpdb;
$sql = "SELECT field_data_translated, field_type
FROM {$wpdb->prefix}icl_translate
WHERE job_id = %d AND field_type LIKE 'tfield-%-%d_%'";
$rowset = $wpdb->get_results( $wpdb->prepare( $sql, $iclTranslateJobId, $termTaxonomyId ) );
/**
* From field type like: tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1_0
* extracts core field name: wpcf-jakub-checkboxes-13
*/
$extractFieldName = pipe( Obj::prop( 'field_type' ), Str::match( '/tfield-(.*)-\d/U' ), Obj::prop( 1 ) );
/**
* From field type like: tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1_0
* extracts option name part: wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1_0
*/
$extractOptions = function ( $row, $fieldName ) {
return Str::pregReplace( "/tfield-{$fieldName}-\d+_/U", '', $row->field_type );
};
$groupOptions = function ( $carry, $row ) use ( $extractFieldName, $extractOptions ) {
$fieldName = $extractFieldName( $row );
if ( ! isset( $carry[ $fieldName ] ) ) {
$carry[ $fieldName ] = [];
}
$options = $extractOptions( $row, $fieldName );
/**
* If field_type is: tfield-wpcf-jakub-checkboxes-13_wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1_0
* then meta keys are: [wpcf-jakub-checkboxes-13, wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1, 0]
*/
$metaKeys = array_merge( [ $fieldName ], explode( '_', $options ) );
/**
* Builds array like:
* [wpcf-jakub-checkboxes-13 => [wpcf-fields-checkboxes-option-6c88acb978ec7f24eb6a2bb12fc2d1c4-1 => [0 => Translation_value ] ] ]
*
* If there are already data under wpcf-jakub-checkboxes-13 key, they are preserve too. The new values are appended.
*/
return Utils::insertUnderKeys( $metaKeys, $carry, base64_decode( $row->field_data_translated ) );
};
$recreateJobElement = function ( $data, $fieldType ) use ( $termTaxonomyId ) {
return (object) [
'field_type' => 'tfield-' . $fieldType . '-' . $termTaxonomyId,
'field_data_translated' => $data,
];
};
return Obj::values( Fns::map( $recreateJobElement, Fns::reduce( $groupOptions, [], $rowset ) ) );
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace WPML\TM\Jobs;
class Utils {
/**
* Inserts an element into an array, nested by keys.
* Input ['a', 'b'] for the keys, an empty array for $array and $x for the value would lead to
* [ 'a' => ['b' => $x ] ] being returned.
*
* @param string[] $keys indexes ordered from highest to lowest level.
* @param mixed[] $array array into which the value is to be inserted.
* @param mixed $value to be inserted.
*
* @return mixed[]
*/
public static function insertUnderKeys( $keys, $array, $value ) {
$array[ $keys[0] ] = count( $keys ) === 1
? $value
: self::insertUnderKeys(
array_slice( $keys, 1 ),
( isset( $array[ $keys[0] ] ) ? $array[ $keys[0] ] : [] ),
$value
);
return $array;
}
}

View File

@@ -0,0 +1,506 @@
<?php
use \WPML\FP\Fns;
use \WPML\TM\Jobs\FieldId;
use \WPML\FP\Logic;
use \WPML\FP\Lst;
use \WPML\FP\Either;
use \WPML\LIB\WP\Post;
use function \WPML\FP\curryN;
use function \WPML\FP\pipe;
use function \WPML\FP\invoke;
use WPML\TM\Jobs\Utils;
use WPML\FP\Relation;
/**
* Class WPML_Element_Translation_Package
*
* @package wpml-core
*/
class WPML_Element_Translation_Package extends WPML_Translation_Job_Helper {
/** @var WPML_WP_API $wp_api */
private $wp_api;
/**
* The constructor.
*
* @param WPML_WP_API $wp_api An instance of the WP API.
*/
public function __construct( WPML_WP_API $wp_api = null ) {
global $sitepress;
if ( $wp_api ) {
$this->wp_api = $wp_api;
} else {
$this->wp_api = $sitepress->get_wp_api();
}
}
/**
* Create translation package
*
* @param \WPML_Package|\WP_Post|int $post
*
* @return array<string,string|array<string,string>>
*/
public function create_translation_package( $post ) {
$package = array();
$post = is_numeric( $post ) ? get_post( $post ) : $post;
if ( apply_filters( 'wpml_is_external', false, $post ) ) {
/** @var stdClass $post */
$post_contents = (array) $post->string_data;
$original_id = isset( $post->post_id ) ? $post->post_id : $post->ID;
$type = 'external';
if ( isset( $post->title ) ) {
$package['title'] = apply_filters( 'wpml_tm_external_translation_job_title', $post->title, $original_id );
}
if ( empty( $package['title'] ) ) {
$package['title'] = sprintf(
/* translators: The placeholder will be replaced with a number (an ID) */
__( 'External package ID: %d', 'wpml-translation-management' ),
$original_id
);
}
} else {
$home_url = get_home_url();
$package['url'] = htmlentities( $home_url . '?' . ( 'page' === $post->post_type ? 'page_id' : 'p' ) . '=' . ( $post->ID ) );
$package['title'] = $post->post_title;
$post_contents = array(
'title' => $post->post_title,
'body' => $post->post_content,
'excerpt' => $post->post_excerpt,
);
if ( wpml_get_setting_filter( false, 'translated_document_page_url' ) === 'translate' ) {
$post_contents['URL'] = $post->post_name;
}
$original_id = $post->ID;
$custom_fields_to_translate = \WPML\TM\Settings\Repository::getCustomFieldsToTranslate();
if ( ! empty( $custom_fields_to_translate ) ) {
$package = $this->add_custom_field_contents(
$package,
$post,
$custom_fields_to_translate,
$this->get_tm_setting( array( 'custom_fields_encoding' ) )
);
}
$post_contents = array_merge( $post_contents, $this->get_taxonomy_fields( $post ) );
$type = 'post';
}
$package['contents']['original_id'] = array(
'translate' => 0,
'data' => $original_id,
);
$package['type'] = $type;
$package['contents'] = $this->buildEntries( $package['contents'], $post_contents );
return apply_filters( 'wpml_tm_translation_job_data', $package, $post );
}
private function buildEntries( $contents, $entries, $parentKey = '' ) {
foreach ( $entries as $key => $entry ) {
$fullKey = $parentKey ? $parentKey . '_' . $key : $key;
if ( is_array( $entry ) ) {
$contents = $this->buildEntries( $contents, $entry, $fullKey );
} else {
$contents[ $fullKey ] = [
'translate' => 1,
'data' => base64_encode( $entry ),
'format' => 'base64',
];
}
}
return $contents;
}
/**
* @param array $translation_package
* @param int $job_id
* @param array $prev_translation
*/
public function save_package_to_job( array $translation_package, $job_id, $prev_translation ) {
global $wpdb;
$show = $wpdb->hide_errors();
foreach ( $translation_package['contents'] as $field => $value ) {
$job_translate = array(
'job_id' => $job_id,
'content_id' => 0,
'field_type' => $field,
'field_wrap_tag' => isset( $value['wrap_tag'] ) ? $value['wrap_tag'] : '',
'field_format' => isset( $value['format'] ) ? $value['format'] : '',
'field_translate' => $value['translate'],
'field_data' => $value['data'],
'field_data_translated' => '',
'field_finished' => 0,
);
if ( array_key_exists( $field, $prev_translation ) ) {
$job_translate['field_data_translated'] = $prev_translation[ $field ]->get_translation();
$job_translate['field_finished'] = $prev_translation[ $field ]->is_finished( $value['data'] );
}
$job_translate = $this->filter_non_translatable_fields( $job_translate );
$wpdb->insert( $wpdb->prefix . 'icl_translate', $job_translate );
}
$wpdb->show_errors( $show );
}
/**
* @param array $job_translate
*
* @return mixed|void
*/
private function filter_non_translatable_fields( $job_translate ) {
if ( $job_translate['field_translate'] ) {
$data = $job_translate['field_data'];
if ( 'base64' === $job_translate['field_format'] ) {
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$data = base64_decode( $data );
}
$is_translatable = ! WPML_String_Functions::is_not_translatable( $data ) && apply_filters( 'wpml_translation_job_post_meta_value_translated', 1, $job_translate['field_type'] );
$is_translatable = (bool) apply_filters( 'wpml_tm_job_field_is_translatable', $is_translatable, $job_translate );
if ( ! $is_translatable ) {
$job_translate['field_translate'] = 0;
$job_translate['field_data_translated'] = $job_translate['field_data'];
$job_translate['field_finished'] = 1;
}
}
return $job_translate;
}
/**
* @param object $job
* @param int $post_id
* @param array $fields
*/
public function save_job_custom_fields( $job, $post_id, $fields ) {
$decode_translation = function ( $translation ) {
// always decode html entities eg decode &amp; to &.
return html_entity_decode( str_replace( '&#0A;', "\n", $translation ) );
};
$get_field_id = function( $field_name, $el_data ) {
if (
strpos( $el_data->field_data, (string) $field_name ) === 0
&& 1 === preg_match( '/field-(.*?)-name/U', $el_data->field_type, $match )
&& 1 === preg_match( '/field-' . $field_name . '-[0-9].*?-name/', $el_data->field_type )
) {
return $match[1];
}
return null;
};
$field_names = [];
foreach ( $fields as $field_name => $val ) {
if ( '' === (string) $field_name ) {
continue;
}
// find it in the translation.
foreach ( $job->elements as $el_data ) {
$field_id_string = $get_field_id( $field_name, $el_data );
if ( $field_id_string ) {
$field_names[ $field_name ] = isset( $field_names[ $field_name ] )
? $field_names[ $field_name ] : [];
$field_translation = false;
foreach ( $job->elements as $v ) {
if ( 'field-' . $field_id_string === $v->field_type ) {
$field_translation = $this->decode_field_data(
$v->field_data_translated,
$v->field_format
);
}
if ( 'field-' . $field_id_string . '-type' === $v->field_type ) {
$field_type = $v->field_data;
break;
}
}
if ( false !== $field_translation && isset( $field_type ) && 'custom_field' === $field_type ) {
$field_id_string = $this->remove_field_name_from_start( $field_name, $field_id_string );
$meta_keys = wpml_collect( explode( '-', $field_id_string ) )
->map( [ 'WPML_TM_Field_Type_Encoding', 'decode_hyphen' ] )
->prepend( $field_name )
->toArray();
$field_names = Utils::insertUnderKeys( $meta_keys, $field_names, $decode_translation( $field_translation ) );
}
}
}
}
$this->save_custom_field_values( $field_names, $post_id, $job->original_doc_id );
}
/**
* Remove the field from the start of the string.
*
* @param string $field_name The field to remove.
* @param string $field_id_string The full field identifier.
* @return string
*/
private function remove_field_name_from_start( $field_name, $field_id_string ) {
return preg_replace( '#' . $field_name . '-?#', '', $field_id_string, 1 );
}
/**
* @param array $fields_in_job
* @param int $post_id
* @param int $original_post_id
*/
private function save_custom_field_values( $fields_in_job, $post_id, $original_post_id ) {
$encodings = $this->get_tm_setting( array( 'custom_fields_encoding' ) );
foreach ( $fields_in_job as $name => $contents ) {
$this->wp_api->delete_post_meta( $post_id, $name );
$contents = (array) $contents;
$single = count( $contents ) === 1;
$encoding = isset( $encodings[ $name ] ) ? $encodings[ $name ] : '';
foreach ( $contents as $value ) {
$value = self::preserve_numerics( $value, $name, $original_post_id, $single );
$value = $encoding ? WPML_Encoding::encode( $value, $encoding ) : $value;
$value = apply_filters( 'wpml_encode_custom_field', $value, $name );
$value = $this->prevent_strip_slash_on_json( $value, $encoding );
$this->wp_api->add_post_meta( $post_id, $name, $value, $single );
}
}
}
/**
* The core function `add_post_meta` always performs
* a `stripslashes_deep` on the value. We need to escape
* once more before to call the function.
*
* @param string $value
* @param string $encoding
*
* @return string
*/
private function prevent_strip_slash_on_json( $value, $encoding ) {
if ( in_array( 'json', explode( ',', $encoding ), true ) ) {
$value = wp_slash( $value );
}
return $value;
}
/**
* @param array $package
* @param object $post
* @param array $fields_to_translate
* @param array $fields_encoding
*
* @return array
*/
private function add_custom_field_contents( $package, $post, $fields_to_translate, $fields_encoding ) {
foreach ( $fields_to_translate as $key ) {
$encoding = isset( $fields_encoding[ $key ] ) ? $fields_encoding[ $key ] : '';
$custom_fields_values = array_values( array_filter( get_post_meta( $post->ID, $key ) ) );
foreach ( $custom_fields_values as $index => $custom_field_val ) {
$custom_field_val = apply_filters( 'wpml_decode_custom_field', $custom_field_val, $key );
$package = $this->add_single_field_content( $package, $key, array( $index ), $custom_field_val, $encoding );
}
}
return $package;
}
/**
* For array valued custom fields cf is given in the form field-{$field_name}-join('-', $indicies)
*
* @param array $package
* @param string $key
* @param array $custom_field_index
* @param array|stdClass|string $custom_field_val
* @param string $encoding
*
* @return array
*/
private function add_single_field_content( $package, $key, $custom_field_index, $custom_field_val, $encoding ) {
if ( $encoding && is_scalar( $custom_field_val ) ) {
$custom_field_val = WPML_Encoding::decode( $custom_field_val, $encoding );
$encoding = '';
}
if ( is_scalar( $custom_field_val ) ) {
list( $cf, $key_index ) = WPML_TM_Field_Type_Encoding::encode( $key, $custom_field_index );
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$package['contents'][ $cf ] = array(
'translate' => 1,
'data' => base64_encode( $custom_field_val ),
'format' => 'base64',
);
$package['contents'][ $cf . '-name' ] = array(
'translate' => 0,
'data' => $key_index,
);
$package['contents'][ $cf . '-type' ] = array(
'translate' => 0,
'data' => 'custom_field',
);
} else {
foreach ( (array) $custom_field_val as $ind => $value ) {
$package = $this->add_single_field_content(
$package,
$key,
array_merge( $custom_field_index, array( $ind ) ),
$value,
$encoding
);
}
}
return $package;
}
/**
* Ensure that any numerics are preserved in the given value. eg any string like '10'
* will be converted to an integer if the corresponding original value was an integer.
*
* @param mixed $value
* @param string $name
* @param string|int $original_post_id
* @param bool $single
*
* @return mixed
*/
public static function preserve_numerics( $value, $name, $original_post_id, $single ) {
$get_original = function () use ( $original_post_id, $name, $single ) {
$meta = get_post_meta( $original_post_id, $name, $single );
return apply_filters( 'wpml_decode_custom_field', $meta, $name );
};
if ( is_numeric( $value ) && is_int( $get_original() ) ) {
$value = intval( $value );
} elseif ( is_array( $value ) ) {
$value = self::preserve_numerics_recursive( $get_original(), $value );
}
return $value;
}
/**
* Ensure that any numerics are preserved in the given value. eg any string like '10'
* will be converted to an integer if the corresponding original value was an integer.
*
* @param mixed $original
* @param mixed $value
*
* @return mixed
*/
private static function preserve_numerics_recursive( $original, $value ) {
if ( is_array( $original ) ) {
foreach ( $original as $key => $data ) {
if ( isset( $value[ $key ] ) ) {
if ( is_array( $data ) ) {
$value[ $key ] = self::preserve_numerics_recursive( $data, $value[ $key ] );
} elseif ( is_int( $data ) && is_numeric( $value[ $key ] ) ) {
$value[ $key ] = intval( $value[ $key ] );
}
}
}
}
return $value;
}
private function get_taxonomy_fields( $post ) {
global $sitepress;
$termMetaKeysToTranslate = self::getTermMetaKeysToTranslate();
// $getTermFields :: WP_Term → [[fieldId, fieldVal]]
$getTermFields = function ( $term ) {
return [
[ FieldId::forTerm( $term->term_taxonomy_id ), $term->name ],
[ FieldId::forTermDescription( $term->term_taxonomy_id ), $term->description ],
];
};
// $getTermMetaFields :: [metakeys] → WP_Term → [[fieldId, fieldVal]]
$getTermMetaFields = curryN(
2,
function ( $termMetaKeysToTranslate, $term ) {
// $getMeta :: int → string → object
$getMeta = curryN(
2,
function ( $termId, $key ) {
return (object) [
'id' => $termId,
'key' => $key,
'meta' => get_term_meta( $termId, $key ),
];
}
);
// $hasMeta :: object → bool
$hasMeta = function ( $termData ) {
return isset( $termData->meta[0] );
};
// $makeField :: object → [fieldId, $fieldVal]
$makeField = function ( $termData ) {
return [ FieldId::forTermMeta( $termData->id, $termData->key ), $termData->meta[0] ];
};
// $get :: [metakeys] → [[fieldId, $fieldVal]]
$get = pipe(
Fns::map( $getMeta( $term->term_taxonomy_id ) ),
Fns::filter( $hasMeta ),
Fns::map( $makeField )
);
return $get( $termMetaKeysToTranslate );
}
);
// $getAll :: [WP_Term] → [[fieldId, fieldVal]]
$getAll = Fns::converge( Lst::concat(), [ $getTermFields, $getTermMetaFields( $termMetaKeysToTranslate ) ] );
return wpml_collect( $sitepress->get_translatable_taxonomies( false, $post->post_type ) ) // [taxonomies]
->map( Post::getTerms( $post->ID ) ) // [Either false|WP_Error [WP_Term]]
->filter( Fns::isRight() ) // [Right[WP_Term]]
->map( invoke( 'get' ) ) // [[WP_Term]]
->flatten() // [WP_Term]
->map( $getAll ) // [[fieldId, fieldVal]]
->mapWithKeys( Lst::fromPairs() ) // [fieldId => fieldVal]
->toArray();
}
public static function getTermMetaKeysToTranslate() {
$fieldTranslation = new WPML_Custom_Field_Setting_Factory( self::get_core_translation_management() );
$settingsFactory = self::get_core_translation_management()->settings_factory();
$translatableMetaKeys = pipe(
[ $settingsFactory, 'term_meta_setting' ],
invoke( 'status' ),
Relation::equals( WPML_TRANSLATE_CUSTOM_FIELD )
);
return wpml_collect( $fieldTranslation->get_term_meta_keys() )
->filter( $translatableMetaKeys )
->values()
->toArray();
}
}

View File

@@ -0,0 +1,133 @@
<?php
use WPML\FP\Obj;
use \WPML\FP\Relation;
use WPML\FP\Lst;
use function WPML\FP\pipe;
class WPML_TM_Field_Content_Action extends WPML_TM_Job_Factory_User {
/** @var int $job_id */
protected $job_id;
/**
* WPML_TM_Field_Content_Action constructor.
*
* @param WPML_Translation_Job_Factory $job_factory
* @param int $job_id
*
* @throws \InvalidArgumentException
*/
public function __construct( $job_factory, $job_id ) {
parent::__construct( $job_factory );
if ( ! ( is_int( $job_id ) && $job_id > 0 ) ) {
throw new InvalidArgumentException( 'Invalid job id provided, received: ' . serialize( $job_id ) );
}
$this->job_id = $job_id;
}
/**
* Returns an array containing job fields
*
* @return array
* @throws \RuntimeException
*/
public function run() {
try {
$job = $this->job_factory->get_translation_job( $this->job_id, false, 1 );
if ( ! $job ) {
throw new RuntimeException( 'No job found for id: ' . $this->job_id );
}
return $this->content_from_elements( $job );
} catch ( Exception $e ) {
throw new RuntimeException(
'Could not retrieve field contents for job_id: ' . $this->job_id,
0,
$e
);
}
}
/**
* Extracts the to be retrieved content from given job elements
*
* @param stdClass $job
*
* @return array
*/
private function content_from_elements( $job ) {
/**
* @var array $elements
* @var array $previous_version_element
* @var stdClass $element
*/
$elements = $job->elements;
$previous_version_elements = isset( $job->prev_version ) ? $job->prev_version->elements : array();
$data = array();
foreach ( $elements as $index => $element ) {
$previous_element = $this->find_previous_version_element( $element, $previous_version_elements, $index );
$data[] = array(
'field_type' => sanitize_title( str_replace( WPML_TM_Field_Type_Encoding::CUSTOM_FIELD_KEY_SEPARATOR, '-', $element->field_type ) ),
'tid' => $element->tid,
'field_style' => $element->field_type === 'body' ? '2' : '0',
'field_finished' => $element->field_finished,
'field_data' => $this->sanitize_field_content( $element->field_data ),
'field_data_translated' => $this->sanitize_field_content( $element->field_data_translated ),
'diff' => $this->get_diff( $element, $previous_element ),
'field_wrap_tag' => $element->field_wrap_tag,
);
}
return $data;
}
private function find_previous_version_element( $element, $previous_version_elements, $index ) {
$findByMatchingFieldType = Lst::find( pipe( Obj::prop( 'field_type' ), Relation::equals( $element->field_type ) ) );
$findByIndex = Obj::prop( $index );
return $findByMatchingFieldType( $previous_version_elements ) ?: $findByIndex( $previous_version_elements );
}
private function has_diff( $element, $previous_element ) {
if ( null === $previous_element ) {
return false;
}
$current_data = $this->sanitize_field_content( $element->field_data );
$previous_data = $this->sanitize_field_content( $previous_element->field_data );
return $current_data !== $previous_data;
}
private function get_diff( $element, $previous_element ) {
if ( null === $previous_element || ! $this->has_diff( $element, $previous_element ) ) {
return null;
}
$current_data = $this->sanitize_field_content( $element->field_data );
$previous_data = $this->sanitize_field_content( $previous_element->field_data );
return wp_text_diff( $previous_data, $current_data, $element->field_format );
}
/**
* @param string $content base64-encoded translation job field content
*
* @return string base64-decoded field content, with linebreaks turned into
* paragraph html tags
*/
private function sanitize_field_content( $content ) {
$decoded = base64_decode( $content );
if ( ! $this->is_html( $decoded ) && false !== strpos( $decoded, '\n' ) ) {
$decoded = wpautop( $decoded );
}
return $decoded;
}
private function is_html( $string ) {
return $string !== strip_tags( $string );
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* Class WPML_TM_Field_Type_Encoding
*/
class WPML_TM_Field_Type_Encoding {
const CUSTOM_FIELD_KEY_SEPARATOR = ':::';
/**
* @param string $custom_field_name
* @param array $attributes
*
* @return array
*/
public static function encode( $custom_field_name, $attributes ) {
$encoded_index = $custom_field_name;
foreach ( $attributes as $index ) {
$encoded_index .= '-' . self::encode_hyphen( $index );
}
return array( 'field-' . $encoded_index, $encoded_index );
}
/**
* @param string $string
*
* @return string
*/
public static function encode_hyphen( $string ) {
return str_replace( '-', self::CUSTOM_FIELD_KEY_SEPARATOR, $string );
}
/**
* Get the custom field name and the attributes from the custom field job.
*
* @param string $custom_field_job_type - e.g: field-my_custom_field-0-attribute.
*
* @return array An array with field name and attributes
*/
public static function decode( $custom_field_job_type ) {
$custom_field_name = '';
$attributes = array();
$parts = explode( '-', $custom_field_job_type );
$count = count( $parts );
if ( $count > 2 && 'field' === $parts[0] ) {
$custom_field_name = $parts[1];
$complete_custom_field_name_found = false;
for ( $i = 2; $i < $count; $i ++ ) {
if ( ! $complete_custom_field_name_found && is_numeric( $parts[ $i ] ) ) {
$complete_custom_field_name_found = true;
continue;
}
if ( ! $complete_custom_field_name_found ) {
$custom_field_name .= '-' . $parts[ $i ];
} else {
$attributes[] = self::decode_hyphen( $parts[ $i ] );
}
}
}
return array( $custom_field_name, $attributes );
}
/**
* @param string $string
*
* @return string
*/
public static function decode_hyphen( $string ) {
return str_replace( self::CUSTOM_FIELD_KEY_SEPARATOR, '-', $string );
}
}

View File

@@ -0,0 +1,20 @@
<?php
class WPML_TM_Job_Action_Factory extends WPML_TM_Job_Factory_User {
/**
* @param int $job_id
*
* @return WPML_TM_Field_Content_Action
* @throws \InvalidArgumentException
*/
public function field_contents( $job_id ) {
return new WPML_TM_Field_Content_Action( $this->job_factory, $job_id );
}
public function save_action( array $data ) {
return new WPML_Save_Translation_Data_Action( $data, $this->job_factory->tm_records() );
}
}

View File

@@ -0,0 +1,17 @@
<?php
abstract class WPML_TM_Job_Action {
/** @var WPML_TM_Job_Action_Factory $job_action_factory */
protected $job_action_factory;
/**
* WPML_TM_Job_Action constructor.
*
* @param WPML_TM_Job_Action_Factory $job_action_factory
*/
public function __construct( &$job_action_factory ) {
$this->job_action_factory = &$job_action_factory;
}
}

View File

@@ -0,0 +1,150 @@
<?php
use \WPML\TM\Jobs\FieldId;
class WPML_TM_Job_Layout {
private $layout = array();
private $custom_fields = array();
private $grouped_custom_fields = array();
private $terms = array();
private $wp_api;
public $wpdb;
public function __construct( wpdb $wpdb, WPML_WP_API $wp_api ) {
$this->wpdb = $wpdb;
$this->wp_api = $wp_api;
}
public function get_wpdb() {
return $this->wpdb;
}
public function run( array $fields, $tm_instance = null ) {
foreach ( $fields as $field ) {
$this->layout[] = $field['field_type'];
}
$this->order_main_fields();
$this->extract_terms();
$this->extract_custom_fields( $tm_instance );
$this->append_terms();
$this->append_grouped_custom_fields();
$this->append_custom_fields();
return apply_filters( 'wpml_tm_job_layout', array_values( $this->layout ) );
}
private function order_main_fields() {
$ordered_elements = array();
foreach ( array( 'title', 'body', 'excerpt' ) as $type ) {
foreach ( $this->layout as $key => $element ) {
if ( $element === $type ) {
unset( $this->layout[ $key ] );
$ordered_elements[] = $type;
}
}
}
$this->layout = array_merge( $ordered_elements, $this->layout );
}
private function extract_custom_fields( $tm_instance ) {
foreach ( $this->layout as $key => $field ) {
if ( FieldId::is_a_custom_field( $field ) ) {
$group = $this->get_group_custom_field_belongs_to( $field, $tm_instance );
if ( $group ) {
if ( ! isset( $this->grouped_custom_fields[ $group ] ) ) {
$this->grouped_custom_fields[ $group ] = array();
}
$this->grouped_custom_fields[ $group ][] = $field;
} else {
$this->custom_fields[] = $field;
}
unset( $this->layout[ $key ] );
}
}
}
private function get_group_custom_field_belongs_to( $field, $tm_instance ) {
$group = '';
if ( $tm_instance ) {
$settings = new WPML_Custom_Field_Editor_Settings( new WPML_Custom_Field_Setting_Factory( $tm_instance ) );
$group = $settings->get_group( WPML_TM_Field_Type_Sanitizer::sanitize( $field ) );
}
return $group;
}
private function extract_terms() {
foreach ( $this->layout as $key => $field ) {
if ( FieldId::is_any_term_field( $field ) ) {
$this->terms[] = $field;
unset( $this->layout[ $key ] );
}
}
}
private function append_grouped_custom_fields() {
foreach ( $this->grouped_custom_fields as $group => $fields ) {
$data = array(
'field_type' => 'tm-section',
'title' => $group,
'fields' => $fields,
'empty' => false,
'empty_message' => '',
'sub_title' => '',
);
$this->layout[] = $data;
}
}
private function append_custom_fields() {
if ( count( $this->custom_fields ) ) {
$data = array(
'field_type' => 'tm-section',
'title' => __( 'Custom Fields', 'wpml-translation-management' ),
'fields' => $this->custom_fields,
'empty' => false,
'empty_message' => '',
'sub_title' => '',
);
$this->layout[] = $data;
}
}
private function append_terms() {
if ( count( $this->terms ) ) {
$taxonomy_fields = [];
foreach ( $this->terms as $term ) {
$term_id = FieldId::get_term_id( $term );
$query = $this->wpdb->prepare( "SELECT taxonomy FROM {$this->wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", $term_id );
$taxonomy = $this->wpdb->get_var( $query );
if ( ! isset( $taxonomy_fields[ $taxonomy ] ) ) {
$taxonomy_fields[ $taxonomy ] = [];
}
$taxonomy_fields[ $taxonomy ][] = $term;
}
foreach ( $taxonomy_fields as $taxonomy => $fields ) {
$taxonomy = $this->wp_api->get_taxonomy( $taxonomy );
$data = array(
'field_type' => 'tm-section',
'title' => $taxonomy->labels->name,
'fields' => $fields,
'empty' => false,
'empty_message' => '',
'sub_title' => __( 'Changes in these translations will affect terms in general! (Not only for this post)', 'wpml-translation-management' ),
);
$this->layout[] = $data;
}
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
class WPML_TM_Translator_Note {
const META_FIELD_KEY = '_icl_translator_note';
public static function get( $post_id ) {
return get_post_meta( $post_id, self::META_FIELD_KEY, true );
}
public static function update( $post_id, $note ) {
update_post_meta( $post_id, self::META_FIELD_KEY, $note );
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Class WPML_TM_Unsent_Jobs_Notice
*
* @group unsent-jobs-notification
*/
class WPML_TM_Unsent_Jobs {
/**
* @var WPML_TM_Blog_Translators
*/
private $blog_translators;
/**
* @var SitePress
*/
private $sitepress;
/**
* WPML_TM_Unsent_Jobs constructor.
*
* @param WPML_TM_Blog_Translators $blog_translators
* @param SitePress $sitepress
*/
public function __construct( WPML_TM_Blog_Translators $blog_translators, SitePress $sitepress ) {
$this->blog_translators = $blog_translators;
$this->sitepress = $sitepress;
}
public function add_hooks() {
add_action( 'wpml_tm_assign_job_notification', array( $this, 'prepare_unsent_job_for_notice' ) );
add_action( 'wpml_tm_new_job_notification', array( $this, 'prepare_unsent_job_for_notice' ), 10, 2 );
add_action( 'wpml_tm_local_string_sent', array( $this, 'prepare_unsent_job_for_notice' ) );
}
/**
* @param WPML_Translation_Job $job
* @param null $translator_id
*/
public function prepare_unsent_job_for_notice( WPML_Translation_Job $job, $translator_id = null ) {
if ( $translator_id ) {
$translators = array( get_userdata( $translator_id ) );
} else {
$translators = $this->blog_translators->get_blog_translators(
array(
'from' => $job->get_source_language_code(),
'to' => $job->get_language_code(),
)
);
}
foreach ( $translators as $translator ) {
$args = array(
'job' => $job,
'event' => WPML_User_Jobs_Notification_Settings::is_new_job_notification_enabled( $translator->ID ) ? 'sent' : 'unsent',
);
do_action( 'wpml_tm_jobs_translator_notification', $args );
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Class WPML_TM_Unsent_Jobs_Notifications_Hooks
*/
class WPML_TM_Unsent_Jobs_Notice_Hooks {
/** @var string */
protected $dismissed_option_key;
/**
* @var WPML_TM_Unsent_Jobs_Notice
*/
private $wpml_tm_notice_email_notice;
/**
* @var WPML_Notices
*/
private $wpml_admin_notices;
/**
* @var WPML_WP_API
*/
private $wp_api;
/**
* WPML_TM_Unsent_Jobs_Notice_Hooks constructor.
*
* @param WPML_TM_Unsent_Jobs_Notice $wpml_tm_notice_email_notice
* @param WPML_WP_API $wp_api
* @param string $dismissed_option_key
*/
public function __construct( WPML_TM_Unsent_Jobs_Notice $wpml_tm_notice_email_notice, WPML_WP_API $wp_api, $dismissed_option_key ) {
$this->wpml_tm_notice_email_notice = $wpml_tm_notice_email_notice;
$this->wpml_admin_notices = wpml_get_admin_notices();
$this->wp_api = $wp_api;
$this->dismissed_option_key = $dismissed_option_key;
}
public function add_hooks() {
add_action( 'wpml_tm_jobs_translator_notification', array( $this, 'email_for_job' ) );
add_action( 'wpml_tm_basket_committed', array( $this, 'add_notice' ) );
add_action( 'shutdown', array( $this, 'remove_notice' ) );
}
/**
* @param array $args
*/
public function email_for_job( $args ) {
$job_set = array_key_exists( 'job', $args ) && $args['job'];
$event_set = array_key_exists( 'event', $args ) && $args['event'];
if ( $job_set && $event_set ) {
if ( 'unsent' === $args['event'] ) {
$this->wpml_tm_notice_email_notice->add_job( $args );
} else {
$this->wpml_tm_notice_email_notice->remove_job( $args );
}
}
}
public function add_notice() {
$this->wpml_tm_notice_email_notice->add_notice( $this->wpml_admin_notices, $this->get_dismissed_option_key() );
}
public function remove_notice() {
if ( $this->wp_api->is_jobs_tab() ) {
$this->wpml_admin_notices->remove_notice( WPML_TM_Unsent_Jobs_Notice::NOTICE_GROUP_ID, WPML_TM_Unsent_Jobs_Notice::NOTICE_ID );
}
}
/**
* @return string
*/
private function get_dismissed_option_key() {
return $this->dismissed_option_key;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Class WPML_TM_Unsent_Jobs_Notice_Template
*/
class WPML_TM_Unsent_Jobs_Notice_Template {
const TEMPLATE_FILE = 'jobs-not-notified.twig';
/**
* @var WPML_Twig_Template
*/
private $template_service;
/**
* WPML_TM_Unsent_Jobs_Notice_Template constructor.
*
* @param IWPML_Template_Service $template_service
*/
public function __construct( IWPML_Template_Service $template_service ) {
$this->template_service = $template_service;
}
/**
* @param array $jobs
*
* @return string
*/
public function get_notice_body( $jobs ) {
$model = $this->get_notice_model( $jobs );
return $this->template_service->show( $model, self::TEMPLATE_FILE );
}
/**
* @param array $jobs
*
* @return array
*/
private function get_notice_model( $jobs ) {
$translators_tab = 'admin.php?page=' . WPML_TM_FOLDER . '/menu/main.php&sm=translators';
$jobs_formatted = $this->get_formatted_jobs( $jobs );
$model = array(
'strings' => array(
'title' => esc_html__( 'Translations may delay because translators did not receive notifications', 'wpml-translation-management' ),
'body' => esc_html__( 'You have sent documents to translation. WPML can send notification emails to assigned translators, but translators for some languages have selected not to receive this notification.', 'wpml-translation-management' ),
'jobs' => $jobs_formatted,
'bottom' => sprintf(
esc_html__( 'You should contact your %1$s and ask them to enable the notification emails which will allow them to see when there is new work waiting for them. To enable notifications, translators need to log-in to this site, go to their user profile page and change the related option in the WPML language settings section.', 'wpml-translation-management' ),
'<a href="' . admin_url( $translators_tab ) . '">' . esc_html__( 'translators' ) . '</a> '
),
),
);
return $model;
}
/**
* @param array $jobs
*
* @return array
*/
private function get_formatted_jobs( $jobs ) {
$jobs_formatted = array();
foreach ( $jobs as $job ) {
$jobs_formatted[] = sprintf( __( 'Job %1$s: %2$s - %3$s', 'wpml-translation-management' ), $job['job_id'], $job['lang_from'], $job['lang_to'] );
}
return $jobs_formatted;
}
}

View File

@@ -0,0 +1,160 @@
<?php
use WPML\Core\Twig_Loader_Filesystem;
use WPML\Core\Twig_Environment;
/**
* Class WPML_TM_Unsent_Jobs_Notice
*/
class WPML_TM_Unsent_Jobs_Notice {
const OPT_JOBS_NOT_NOTIFIED = '_wpml_jobs_not_notified';
const NOTICE_ID = 'job-not-notified';
const NOTICE_GROUP_ID = 'tm-jobs-notification';
/**
* @var string
*/
private $body;
/**
* @var WPML_WP_API
*/
private $wp_api;
/**
* @var WPML_TM_Unsent_Jobs_Notice_Template
*/
private $notice_template;
/**
* WPML_TM_Unsent_Jobs_Notice constructor.
*
* @param WPML_WP_API $wp_api
* @param WPML_TM_Unsent_Jobs_Notice_Template|null $notice_template
*/
public function __construct( WPML_WP_API $wp_api, WPML_TM_Unsent_Jobs_Notice_Template $notice_template = null ) {
$this->wp_api = $wp_api;
$this->notice_template = $notice_template;
}
private function prepare_notice_body() {
$this->body = $this->get_notice_template()->get_notice_body( $this->get_jobs() );
}
/**
* @return null|WPML_TM_Unsent_Jobs_Notice_Template
*/
private function get_notice_template() {
if ( ! $this->notice_template ) {
$template_paths = array(
WPML_TM_PATH . '/templates/notices/',
);
$twig_loader = new Twig_Loader_Filesystem( $template_paths );
$environment_args = array();
if ( WP_DEBUG ) {
$environment_args['debug'] = true;
}
$twig = new Twig_Environment( $twig_loader, $environment_args );
$twig_service = new WPML_Twig_Template( $twig );
$this->notice_template = new WPML_TM_Unsent_Jobs_Notice_Template( $twig_service );
}
return $this->notice_template;
}
/**
* @param WPML_Notices $wpml_admin_notices
*/
public function add_notice( WPML_Notices $wpml_admin_notices, $dismissed_option_key ) {
if ( $this->get_jobs() ) {
$this->prepare_notice_body();
$notice = new WPML_Notice( self::NOTICE_ID, $this->body, 'tm-jobs-notification' );
$notice->set_css_class_types( 'info' );
$notice->add_display_callback( array( $this->wp_api, 'is_jobs_tab' ) );
$this->add_actions( $notice );
$this->remove_notice_from_dismissed_list( self::NOTICE_GROUP_ID, $dismissed_option_key );
$wpml_admin_notices->add_notice( $notice );
$this->update_jobs_option( array() );
}
}
private function remove_notice_from_dismissed_list( $notice_group_id, $dismissed_option_key ) {
$dismissed_notices = get_option( $dismissed_option_key );
if ( is_array( $dismissed_notices ) ) {
foreach ( (array) $dismissed_notices as $key => $notices ) {
if ( $key === $notice_group_id ) {
unset( $dismissed_notices[ $key ] );
}
}
update_option( $dismissed_option_key, $dismissed_notices );
}
}
/**
* @param WPML_Notice $notice
*/
private function add_actions( WPML_Notice $notice ) {
$dismiss_action = new WPML_Notice_Action( __( 'Dismiss', 'wpml-translation-management' ), '#', true, false, false, true );
$notice->add_action( $dismiss_action );
}
/**
* @param array $args
*/
public function add_job( $args ) {
$job_id = $args['job']->get_id();
$lang_from = $args['job']->get_source_language_code( true );
$lang_to = $args['job']->get_language_code( true );
$jobs = $this->get_jobs();
if ( ! wp_filter_object_list( $jobs, array( 'job_id' => $job_id ) ) ) {
$jobs[] = array(
'job_id' => $job_id,
'lang_from' => $lang_from,
'lang_to' => $lang_to,
);
$this->update_jobs_option( $jobs );
}
}
/**
* @param array $args
*/
public function remove_job( $args ) {
$job_id = $args['job']->get_id();
$unsent_jobs = $this->get_jobs();
if ( $unsent_jobs ) {
foreach ( $unsent_jobs as $key => $unsent_job ) {
if ( $unsent_job['job_id'] === $job_id ) {
unset( $unsent_jobs[ $key ] );
}
}
}
$this->update_jobs_option( $unsent_jobs );
}
/**
* @param array $jobs
*/
private function update_jobs_option( $jobs ) {
update_option( self::OPT_JOBS_NOT_NOTIFIED, $jobs );
}
/**
* @return array
*/
private function get_jobs() {
return get_option( self::OPT_JOBS_NOT_NOTIFIED );
}
}