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,46 @@
<?php
class WPML_TM_General_Xliff_Import extends WPML_TM_Job_Factory_User {
/**
* @var WPML_TM_Xliff_Reader_Factory $xliff_reader_factory
*/
private $xliff_reader_factory;
/**
* WPML_TM_General_Xliff_Import constructor.
*
* @param WPML_Translation_Job_Factory $job_factory
* @param WPML_TM_Xliff_Reader_Factory $xliff_reader_factory
*/
public function __construct( &$job_factory, &$xliff_reader_factory ) {
parent::__construct( $job_factory );
$this->xliff_reader_factory = &$xliff_reader_factory;
}
/**
* Imports the data in the xliff string into an array representation
* that fits to the given target translation id.
*
* @param string $xliff_string
* @param int $target_translation_id
*
* @return WP_Error|array
*/
public function import( $xliff_string, $target_translation_id ) {
$xliff_reader = $this->xliff_reader_factory->general_xliff_reader();
$job_data = $xliff_reader->get_data( $xliff_string );
if ( is_wp_error( $job_data ) ) {
$job = $this->job_factory->job_by_translation_id( $target_translation_id );
if ( $job
&& ( $id_string = $xliff_reader->get_xliff_job_identifier( $xliff_string ) ) !== false
) {
$job_data = $xliff_reader->get_data( str_replace( $id_string,
$job->get_id() . '-' . md5( $job->get_id() . $job->get_original_element_id() ),
$xliff_string ) );
}
}
return $job_data;
}
}

View File

@@ -0,0 +1,34 @@
<?php
class WPML_TM_General_Xliff_Reader extends WPML_TM_Xliff_Reader {
public function get_xliff_job_identifier( $content ) {
$xliff = $this->load_xliff( $content );
if ( is_wp_error( $xliff ) ) {
$identifier = false;
} else {
$identifier = $this->identifier_from_xliff( $xliff );
}
return $identifier;
}
/**
* Retrieve the translation from a XLIFF
*
* @param string $content The XLIFF representing a job
*
* @return WP_Error|array
*/
public function get_data( $content ) {
$xliff = $this->load_xliff( $content );
if ( is_wp_error( $xliff ) ) {
$data = $xliff;
} else {
$job = $this->get_job_for_xliff( $xliff );
$data = is_wp_error( $job ) ? $job : $this->generate_job_data( $xliff, $job );
}
return $data;
}
}

View File

@@ -0,0 +1,31 @@
<?php
class WPML_TM_String_Xliff_Reader extends WPML_TM_Xliff_Reader {
/**
* Retrieve the string translations from a XLIFF
*
* @param string $content The XLIFF representing a set of strings
*
* @return WP_Error|array The string translation representation or WP_Error
* on failure
*/
public function get_data( $content ) {
$xliff = $this->load_xliff( $content );
$data = array();
if ( $xliff && ! $xliff instanceof WP_Error ) {
/** @var SimpleXMLElement $node */
foreach ( $xliff->{'file'}->{'body'}->children() as $node ) {
$target = $this->get_xliff_node_target( $node );
if ( ! $target && $target !== '0' ) {
return $this->invalid_xliff_error();
}
$target = $this->replace_xliff_new_line_tag_with_new_line( $target );
$attr = $node->attributes();
$data[ (string) $attr['id'] ] = $target;
}
}
return $data;
}
}

View File

@@ -0,0 +1,503 @@
<?php
/**
* Class WPML_TM_Validate_HTML
*/
class WPML_TM_Validate_HTML {
/** @var string Validated html */
private $html = '';
/** @var array Tags currently open */
private $tags = array();
/** @var int Number of errors */
private $error_count = 0;
/**
* Get validated html.
*
* @return string
*/
public function get_html() {
return $this->html;
}
/**
* Validate html.
*
* @param string $html HTML to process.
*
* @return int Number of errors.
*/
public function validate( $html ) {
$html = $this->hide_wp_bugs( $html );
$html = $this->hide_cdata( $html );
$html = $this->hide_comments( $html );
$html = $this->hide_self_closing_tags( $html );
$html = $this->hide_scripts( $html );
$html = $this->hide_styles( $html );
$processed_html = '';
$html_arr = array(
'processed' => $processed_html,
'next' => $html,
);
while ( '' !== $html_arr['next'] ) {
$html_arr = $this->validate_next( $html_arr['next'] );
if ( $html_arr ) {
$processed_html .= $html_arr['processed'];
}
}
$html = $processed_html;
$html = $this->restore_styles( $html );
$html = $this->restore_scripts( $html );
$html = $this->restore_self_closing_tags( $html );
$html = $this->restore_comments( $html );
$html = $this->restore_wp_bugs( $html );
$this->html = $html;
return $this->error_count;
}
/**
* Validate first tag in html flow and return processed html and rest.
* In processed part broken html is replaced by wpml comment.
*
* @param string $html HTML to process.
*
* @return array|null
*/
private function validate_next( $html ) {
$regs = array();
// Get first opening or closing tag.
$pattern = '<\s*?([a-z]+|/[a-z]+)((?:.|\s)*?)>';
mb_eregi( $pattern, $html, $regs );
if ( $regs ) {
$full_tag = $regs[0];
$pos = mb_strpos( $html, $full_tag );
$next_html = mb_substr( $html, $pos + mb_strlen( $full_tag ) );
$tag = $regs[1];
$result = true;
if ( '/' === mb_substr( $tag, 0, 1 ) ) {
$result = $this->close_tag( mb_substr( $tag, 1 ) );
} else {
$this->open_tag( $tag );
}
if ( $result ) {
$processed_html = mb_substr( $html, 0, $pos + mb_strlen( $full_tag ) );
} else {
$processed_html = mb_substr( $html, 0, $pos ) . '<!-- wpml:html_fragment ' . $full_tag . ' -->';
}
return array(
'processed' => $processed_html,
'next' => $next_html,
);
}
return array(
'processed' => $html,
'next' => '',
);
}
/**
* Convert WP bugs into wpml commented bugs.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_wp_bugs( $html ) {
// WP bug fix for comments - in case you REALLY meant to type '< !--'
$html = str_replace( '< !--', '< !--', $html );
// WP bug fix for LOVE <3 (and other situations with '<' before a number)
$pattern = '<([0-9]{1})';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_wp_bug_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert WP bugs into wpml commented bugs.
*
* @param array $matches
*
* @return string
*/
public function hide_wp_bug_callback( $matches ) {
return '<!-- wpml:wp_bug ' . $matches[0] . ' -->';
}
/**
* Convert wpml commented bugs to WP bugs.
*
* @param $html
*
* @return false|string
*/
private function restore_wp_bugs( $html ) {
$pattern = '<!-- wpml:wp_bug (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_bug_callback' ), $html, 'msri' );
$html = str_replace( '< !--', '< !--', $html );
return $html;
}
/**
* Callback to convert wpml commented bugs to WP bugs.
*
* @param array $matches
*
* @return mixed
*/
public function restore_bug_callback( $matches ) {
return $matches[1];
}
/**
* Convert HTML comments into wpml comments.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_comments( $html ) {
$pattern = '<!--(.*?)-->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_comment_callback' ), $html, 'msri' );
$pattern = '<!((?!-- wpml:).*?)>';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_declaration_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert HTML comment to wpml comment.
*
* @param array $matches
*
* @return string
*/
public function hide_comment_callback( $matches ) {
return '<!-- wpml:html_comment ' . base64_encode( $matches[0] ) . ' -->';
}
/**
* Callback to convert HTML declaration to wpml declaration.
*
* @param array $matches
*
* @return string
*/
public function hide_declaration_callback( $matches ) {
return '<!-- wpml:html_declaration ' . base64_encode( $matches[0] ) . ' -->';
}
/**
* Convert wpml comments to HTML comments.
*
* @param string $html
*
* @return string
*/
private function restore_comments( $html ) {
$pattern = '<!-- wpml:html_comment (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
$pattern = '<!-- wpml:html_declaration (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert wpml base64 encoded content.
*
* @param array $matches
*
* @return string
*/
public function restore_encoded_content_callback( $matches ) {
return base64_decode( $matches[1] );
}
/**
* Convert self-closing tags to wpml self-closing tags.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_self_closing_tags( $html ) {
$self_closing_tags = array(
'area',
'base',
'basefont',
'br',
'col',
'command',
'embed',
'frame',
'hr',
'img',
'input',
'isindex',
'link',
'meta',
'param',
'source',
'track',
'wbr',
'command',
'keygen',
'menuitem',
'path',
'polyline',
);
foreach ( $self_closing_tags as $self_closing_tag ) {
$pattern = '<\s*?' . $self_closing_tag . '((?:.|\s)*?)(>|/>)';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_sct_callback' ), $html, 'msri' );
}
$pattern = '<\s*?[^>]*/>';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_sct_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert self-closing tags to wpml self-closing tags.
*
* @param $matches
*
* @return string
*/
public function hide_sct_callback( $matches ) {
return '<!-- wpml:html_self_closing_tag ' . str_replace( array( '<', '>' ), '', $matches[0] ) . ' -->';
}
/**
* Convert wpml self-closing tags to HTML self-closing tags.
*
* @param $html
*
* @return false|string
*/
private function restore_self_closing_tags( $html ) {
$pattern = '<!-- wpml:html_self_closing_tag (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_sct_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert wpml self-closing tags to self-closing tags.
*
* @param $matches
*
* @return string
*/
public function restore_sct_callback( $matches ) {
return '<' . $matches[1] . '>';
}
/**
* Convert wpml comments to initial HTML.
*
* @param $html
*
* @return false|string
*/
public function restore_html( $html ) {
$html = $this->restore_cdata( $html );
$html = $this->restore_html_fragments( $html );
return $html;
}
/**
* Convert wpml fragments to HTML fragments.
*
* @param $html
*
* @return false|string
*/
private function restore_html_fragments( $html ) {
$pattern = '<!-- wpml:html_fragment (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_html_fragment_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert wpml fragment to HTML fragment.
*
* @param $matches
*
* @return string
*/
public function restore_html_fragment_callback( $matches ) {
return $matches[1];
}
/**
* Convert scripts to wpml scripts.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_scripts( $html ) {
$pattern = '<\s*?script\s*?>((?:.|\s)*?)</script\s*?>';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_script_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert script to wpml script.
*
* @param array $matches
*
* @return string
*/
public function hide_script_callback( $matches ) {
return '<!-- wpml:script ' . base64_encode( $matches[0] ) . ' -->';
}
/**
* Convert wpml scripts to scripts.
*
* @param $html
*
* @return false|string
*/
private function restore_scripts( $html ) {
$pattern = '<!-- wpml:script (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
return $html;
}
/**
* Convert CDATA to wpml cdata.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_cdata( $html ) {
$pattern = '<!\[CDATA\[((?:.|\s)*?)\]\]>';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_cdata_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert CDATA to wpml cdata.
*
* @param array $matches
*
* @return string
*/
public function hide_cdata_callback( $matches ) {
return '<!-- wpml:cdata ' . base64_encode( $matches[0] ) . ' -->';
}
/**
* Convert wpml cdata to CDATA.
*
* @param $html
*
* @return false|string
*/
private function restore_cdata( $html ) {
$pattern = '<!-- wpml:cdata (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
return $html;
}
/**
* Convert styles to wpml scripts.
*
* @param string $html HTML to process.
*
* @return false|string
*/
private function hide_styles( $html ) {
$pattern = '<\s*?style\s*?>((?:.|\s)*?)</style\s*?>';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_style_callback' ), $html, 'msri' );
return $html;
}
/**
* Callback to convert style to wpml style.
*
* @param array $matches
*
* @return string
*/
public function hide_style_callback( $matches ) {
return '<!-- wpml:style ' . base64_encode( $matches[0] ) . ' -->';
}
/**
* Convert wpml styles to styles.
*
* @param $html
*
* @return false|string
*/
private function restore_styles( $html ) {
$pattern = '<!-- wpml:style (.*?) -->';
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
return $html;
}
/**
* Open tag encountered in html.
*
* @param string $tag Tag name.
*/
private function open_tag( $tag ) {
$tag = mb_strtolower( $tag );
array_push( $this->tags, $tag );
}
/**
* Close tag encountered in html.
*
* @param string $tag Tag name.
*
* @return bool Closed successfully.
*/
private function close_tag( $tag ) {
$tag = mb_strtolower( $tag );
$last_tag = end( $this->tags );
if ( $last_tag === $tag ) {
array_pop( $this->tags );
return true;
} else {
$this->error_count ++;
if ( in_array( $tag, $this->tags, true ) ) {
do {
array_pop( $this->tags );
} while ( $this->tags && end( $this->tags ) !== $tag );
array_pop( $this->tags );
}
return false;
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_XLIFF_Factory {
const WPML_XLIFF_DEFAULT_VERSION = WPML_XLIFF_DEFAULT_VERSION;
const CREATE_FOR_WRITE = 'WPML_TM_Xliff_Writer';
const CREATE_FOR_FRONT_END = 'WPML_TM_Xliff_Frontend';
public function create_writer( $xliff_version = self::WPML_XLIFF_DEFAULT_VERSION ) {
return new WPML_TM_Xliff_Writer( wpml_tm_load_job_factory(), $xliff_version, wpml_tm_xliff_shortcodes() );
}
public function create_frontend() {
global $sitepress;
$support_info = new WPML_TM_Support_Info();
return new WPML_TM_Xliff_Frontend( wpml_tm_load_job_factory(), $sitepress, $support_info->is_simplexml_extension_loaded() );
}
}

View File

@@ -0,0 +1,785 @@
<?php
/**
* WPML_TM_Xliff_Frontend class file
*
* @package wpml-translation-management
*/
use WPML\FP\Obj;
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once WPML_TM_PATH . '/inc/wpml_zip.php';
/**
* Class WPML_TM_Xliff_Frontend
*/
class WPML_TM_Xliff_Frontend extends WPML_TM_Xliff_Shared {
/**
* Success admin notices
*
* @var array
*/
private $success;
/**
* Attachments
*
* @var array
*/
private $attachments = array();
/**
* SitePress instance
*
* @var SitePress
*/
private $sitepress;
/**
* Name of archive
*
* @var string
*/
private $export_archive_name;
/**
* Priority of late initialisation
*
* @var int
*/
private $late_init_priority = 9999;
/**
* Is simple xml turned on
*
* @var bool
*/
private $simplexml_on;
/**
* WPML_TM_Xliff_Frontend constructor
*
* @param WPML_Translation_Job_Factory $job_factory Job factory.
* @param SitePress $sitepress SitePress instance.
* @param boolean $simplexml_on Is simple xml turned on.
*/
public function __construct( WPML_Translation_Job_Factory $job_factory, SitePress $sitepress, $simplexml_on ) {
parent::__construct( $job_factory );
$this->sitepress = $sitepress;
$this->simplexml_on = $simplexml_on;
}
/**
* Get available xliff versions
*
* @return array
*/
public function get_available_xliff_versions() {
return array(
'10' => '1.0',
'11' => '1.1',
'12' => '1.2',
);
}
/**
* Get init priority
*
* @return int
*/
public function get_init_priority() {
return isset( $_POST['xliff_upload'] ) ||
( isset( $_GET['wpml_xliff_action'] ) && $_GET['wpml_xliff_action'] === 'download' ) ||
isset( $_POST['wpml_xliff_export_all_filtered'] ) ?
$this->get_late_init_priority() : 10;
}
/**
* Get late init priority
*
* @return int
*/
public function get_late_init_priority() {
return $this->late_init_priority;
}
/**
* Init class
*
* @return bool
* @throws Exception Throws an exception in case of errors.
*/
public function init() {
$this->attachments = array();
$this->error = null;
if ( $this->sitepress->get_wp_api()->is_admin() ) {
add_action( 'admin_head', array( $this, 'js_scripts' ) );
add_action(
'wp_ajax_set_xliff_options',
array(
$this,
'ajax_set_xliff_options',
),
10,
2
);
if ( ! $this->sitepress->get_setting( 'xliff_newlines' ) ) {
$this->sitepress->set_setting( 'xliff_newlines', WPML_XLIFF_TM_NEWLINES_ORIGINAL, true );
}
if ( ! $this->sitepress->get_setting( 'tm_xliff_version' ) ) {
$this->sitepress->set_setting( 'tm_xliff_version', '12', true );
}
if ( 1 < count( $this->sitepress->get_languages( false, true ) ) ) {
add_filter(
'wpml_translation_queue_actions',
array(
$this,
'translation_queue_add_actions',
)
);
add_action(
'wpml_xliff_select_actions',
array(
$this,
'translation_queue_xliff_select_actions',
),
10,
4
);
add_action(
'wpml_translation_queue_after_display',
array(
$this,
'translation_queue_after_display',
),
10,
2
);
add_action(
'wpml_translator_notification',
array(
$this,
'translator_notification',
),
10,
0
);
add_filter(
'wpml_new_job_notification',
array(
$this,
'new_job_notification',
),
10,
2
);
add_filter(
'wpml_new_job_notification_attachments',
array(
$this,
'new_job_notification_attachments',
)
);
}
if (
isset( $_GET['wpml_xliff_action'] ) &&
$_GET['wpml_xliff_action'] === 'download' &&
wp_verify_nonce( $_GET['nonce'], 'xliff-export' )
) {
$archive = $this->get_xliff_archive(
Obj::propOr( '', 'xliff_version', $_GET ),
explode( ',', Obj::propOr( '', 'jobs', $_GET ) )
);
$this->stream_xliff_archive( $archive );
}
add_action( 'wp_ajax_wpml_xliff_upload', function () {
if ( $this->import_xliff( Obj::propOr( [], 'import', $_FILES ) ) ) {
wp_send_json_success( $this->success );
} else {
wp_send_json_error( $this->error );
}
} );
}
return true;
}
/**
* Set xliff options
*/
public function ajax_set_xliff_options() {
check_ajax_referer( 'icl_xliff_options_form_nonce', 'security' );
$newlines = isset( $_POST['icl_xliff_newlines'] ) ? (int) $_POST['icl_xliff_newlines'] : 0;
$this->sitepress->set_setting( 'xliff_newlines', $newlines, true );
$version = isset( $_POST['icl_xliff_version'] ) ? (int) $_POST['icl_xliff_version'] : 0;
$this->sitepress->set_setting( 'tm_xliff_version', $version, true );
wp_send_json_success(
array(
'message' => 'OK',
'newlines_saved' => $newlines,
'version_saved' => $version,
)
);
}
/**
* New job notification
*
* @param array $mail Email content.
* @param int $job_id Job id.
*
* @return array
*/
public function new_job_notification( $mail, $job_id ) {
$tm_settings = $this->sitepress->get_setting( 'translation-management', array() );
if ( isset( $tm_settings['notification']['include_xliff'] ) && $tm_settings['notification']['include_xliff'] ) {
$xliff_version = $this->get_user_xliff_version();
$xliff_file = $this->get_xliff_file( $job_id, $xliff_version );
$temp_dir = get_temp_dir();
$file_name = $temp_dir . get_bloginfo( 'name' ) . '-translation-job-' . $job_id . '.xliff';
$fh = fopen( $file_name, 'w' );
if ( $fh ) {
fwrite( $fh, $xliff_file );
fclose( $fh );
$mail['attachment'] = $file_name;
$this->attachments[ $job_id ] = $file_name;
$mail['body'] .= __( ' - A xliff file is attached.', 'wpml-translation-management' );
}
}
return $mail;
}
/**
* Get zip name from jobs
*
* @param array $job_ids Job ids.
*
* @return string
*/
private function get_zip_name_from_jobs( $job_ids ) {
$min_job = min( $job_ids );
$max_job = max( $job_ids );
if ( $max_job === $min_job ) {
return get_bloginfo( 'name' ) . '-translation-job-' . $max_job . '.zip';
} else {
return get_bloginfo( 'name' ) . '-translation-job-' . $min_job . '-' . $max_job . '.zip';
}
}
/**
* New job notification attachments
*
* @param array $attachments Job notification attachments.
*
* @return array
*/
public function new_job_notification_attachments( $attachments ) {
$found = false;
$archive = new wpml_zip();
foreach ( $attachments as $index => $attachment ) {
if ( in_array( $attachment, $this->attachments, true ) ) {
$fh = fopen( $attachment, 'r' );
$xliff_file = fread( $fh, filesize( $attachment ) );
fclose( $fh );
$archive->addFile( $xliff_file, basename( $attachment ) );
unset( $attachments[ $index ] );
$found = true;
}
}
if ( $found ) {
// Add the zip file to the attachments.
$archive_data = $archive->getZipData();
$temp_dir = get_temp_dir();
$file_name = $temp_dir . $this->get_zip_name_from_jobs( array_keys( $this->attachments ) );
$fh = fopen( $file_name, 'w' );
fwrite( $fh, $archive_data );
fclose( $fh );
$attachments[] = $file_name;
}
return $attachments;
}
/**
* Get xliff file
*
* @param int $job_id Job id.
* @param string $xliff_version Xliff version.
*
* @return string
*/
private function get_xliff_file( $job_id, $xliff_version = WPML_XLIFF_DEFAULT_VERSION ) {
return wpml_tm_xliff_factory()
->create_writer( $xliff_version )
->generate_job_xliff( $job_id );
}
/**
* Get xliff archive
*
* @param string $xliff_version Xliff version.
* @param array|null $job_ids Job ids.
*
* @return wpml_zip
*
* @throws Exception Throws an exception in case of errors.
*/
public function get_xliff_archive( $xliff_version, $job_ids = array() ) {
global $wpdb, $current_user;
if ( empty( $job_ids ) && isset( $_GET['xliff_export_data'] ) ) {
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$data = json_decode( base64_decode( $_GET['xliff_export_data'] ) );
// phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$job_ids = isset( $data->job ) ? array_keys( (array) $data->job ) : array();
}
$archive = new wpml_zip();
foreach ( $job_ids as $job_id ) {
$xliff_file = $this->get_xliff_file( $job_id, $xliff_version );
// Assign the job to this translator.
$rid = $wpdb->get_var(
$wpdb->prepare(
"SELECT rid
FROM {$wpdb->prefix}icl_translate_job
WHERE job_id=%d ",
$job_id
)
);
$data_value = array( 'translator_id' => $current_user->ID );
$data_where = array( 'job_id' => $job_id );
$wpdb->update( $wpdb->prefix . 'icl_translate_job', $data_value, $data_where );
$data_where = array( 'rid' => $rid );
$wpdb->update( $wpdb->prefix . 'icl_translation_status', $data_value, $data_where );
$archive->addFile( $xliff_file, get_bloginfo( 'name' ) . '-translation-job-' . $job_id . '.xliff' );
}
$this->export_archive_name = $this->get_zip_name_from_jobs( $job_ids );
$archive->finalize();
return $archive;
}
/**
* Stream xliff archive
*
* @param wpml_zip $archive Zip archive.
*
* @throws Exception Throws an exception in case of errors.
*/
private function stream_xliff_archive( $archive ) {
if ( is_a( $archive, 'wpml_zip' ) ) {
if ( defined( 'WPML_SAVE_XLIFF_PATH' ) && trim( WPML_SAVE_XLIFF_PATH ) ) {
$this->save_zip_file( WPML_SAVE_XLIFF_PATH, $archive );
}
$archive->sendZip( $this->export_archive_name );
}
exit;
}
/**
* Save zip file
*
* @param string $path Where to save the archive.
* @param wpml_zip $archive Zip archive.
*/
private function save_zip_file( $path, $archive ) {
$path = trailingslashit( $path );
if ( ! is_dir( $path ) ) {
$result = mkdir( $path );
if ( ! $result ) {
return;
}
}
$archive->setZipFile( $path . $this->export_archive_name );
}
/**
* Stops any redirects from happening when we call the
* translation manager to save the translations.
*
* @return null
*/
public function stop_redirect() {
return null;
}
/**
* Import xliff file
*
* @param array $file Xliff file data.
*
* @return bool|WP_Error
*/
private function import_xliff( $file ) {
global $current_user;
// We don't want any redirects happening when we save the translation.
add_filter( 'wp_redirect', array( $this, 'stop_redirect' ) );
$this->success = array();
$contents = array();
if ( 0 === (int) $file['size'] ) {
$this->error = new WP_Error( 'empty_file', __( 'You are trying to import an empty file.', 'wpml-translation-management' ) );
return false;
} elseif ( isset( $file['tmp_name'] ) && $file['tmp_name'] ) {
$fh = fopen( $file['tmp_name'], 'r' );
$data = fread( $fh, 4 );
fclose( $fh );
if ( $data[0] == 'P' && $data[1] == 'K' && $data[2] == chr( 03 ) && $data[3] == chr( 04 ) ) {
if ( class_exists( 'ZipArchive' ) ) {
$z = new ZipArchive();
$zopen = $z->open( $file['tmp_name'], 4 );
if ( true !== $zopen ) {
$this->error = new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.', 'wpml-translation-management' ) );
return false;
}
$empty_files = array();
for ( $i = 0; $i < $z->numFiles; $i ++ ) {
if ( ! $info = $z->statIndex( $i ) ) {
$this->error = new WP_Error( 'stat_failed', __( 'Could not retrieve file from archive.', 'wpml-translation-management' ) );
return false;
}
$content = $z->getFromIndex( $i );
if ( false === (bool) $content ) {
$empty_files[] = $info['name'];
}
$contents[ $info['name'] ] = $content;
}
if ( $empty_files ) {
$this->error = new WP_Error( 'extract_failed', __( 'The archive contains one or more empty files.', 'wpml-translation-management' ), $empty_files );
return false;
}
} else {
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
$archive = new PclZip( $file['tmp_name'] );
// Is the archive valid?
$archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING );
if ( false == $archive_files ) {
$this->error = new WP_Error( 'incompatible_archive', __( 'You are trying to import an incompatible Archive.', 'wpml-translation-management' ), $archive->errorInfo( true ) );
return false;
}
if ( 0 === count( $archive_files ) ) {
$this->error = new WP_Error( 'empty_archive', __( 'You are trying to import an empty archive.', 'wpml-translation-management' ) );
return false;
}
$empty_files = array();
foreach ( $archive_files as $content ) {
if ( false === (bool) $content['content'] ) {
$empty_files[] = $content['filename'];
}
$contents[ $content['filename'] ] = $content['content'];
}
if ( $empty_files ) {
$this->error = new WP_Error( 'extract_failed', __( 'The archive contains one or more empty files.', 'wpml-translation-management' ), $empty_files );
return false;
}
}
} else {
$fh = fopen( $file['tmp_name'], 'r' );
$data = fread( $fh, $file['size'] );
fclose( $fh );
$contents[ $file['name'] ] = $data;
}
foreach ( $contents as $name => $content ) {
if ( $this->validate_file_name( $name ) ) {
list( $job, $job_data ) = $this->validate_file( $name, $content, $current_user );
if ( null !== $this->error ) {
return $job_data;
}
kses_remove_filters();
wpml_tm_save_data( $job_data );
kses_init();
// translators: %s: job id.
$this->success[] = $job->job_id;
}
}
return true;
}
return false;
}
/**
* Translation queue actions
*
* @param array $actions Actions.
* @param string $action_name Action name.
* @param array $translation_jobs Translation jobs.
* @param string $action
*/
public function translation_queue_xliff_select_actions( $actions, $action_name, $translation_jobs, $action ) {
if ( $this->has_translation_jobs( $translation_jobs ) && sizeof( $actions ) > 0 ) :
$currentAction = $action ?: false;
?>
<div class="alignleft actions">
<select name="<?php echo esc_html( $action_name ); ?>">
<option
value="-1" <?php echo $currentAction == false ? "selected='selected'" : ''; ?>><?php _e( 'Bulk Actions' ); ?></option>
<?php foreach ( $actions as $key => $action ) : ?>
<option <?php disabled( ! $this->simplexml_on ); ?>
value="<?php echo $key; ?>" <?php echo $currentAction == $key && $this->simplexml_on ? "selected='selected'" : ''; ?>><?php echo $action; ?></option>
<?php endforeach; ?>
</select>
<input
id="js-wpml-do-action"
type="submit" value="<?php esc_attr_e( 'Apply' ); ?>"
name="do<?php echo esc_html( $action_name ); ?>"
class="button-secondary action"/>
</div>
<?php
endif;
}
/**
* Has translation jobs
*
* @param array $translation_jobs Translation jobs.
*
* @return bool
*/
private function has_translation_jobs( $translation_jobs ) {
return $translation_jobs && array_key_exists( 'jobs', $translation_jobs ) && $translation_jobs['jobs'];
}
/**
* Get xliff version select options
*
* @return string
*/
private function get_xliff_version_select_options() {
$output = '';
$user_version = (int) $this->get_user_xliff_version();
foreach ( $this->get_available_xliff_versions() as $value => $label ) {
$user_version = false === $user_version ? $value : $user_version;
$output .= '<option value="' . $value . '"';
$output .= $user_version === $value ? 'selected="selected"' : '';
$output .= '>XLIFF ' . $label . '</option>';
}
return $output;
}
/**
* Adds the various possible XLIFF versions to translations queue page's export actions on display
*
* @param array $actions Actions.
*
* @return array
*/
public function translation_queue_add_actions( $actions ) {
foreach ( $this->get_available_xliff_versions() as $key => $value ) {
// translators: %s: XLIFF version.
$actions[ $key ] = sprintf( __( 'Export XLIFF %s', 'wpml-translation-management' ), $value );
}
return $actions;
}
/**
* Show error messages in admin notices
*/
public function admin_notices_error() {
if ( is_wp_error( $this->error ) ) {
?>
<div class="message error">
<p><?php echo $this->error->get_error_message(); ?></p>
<?php
if ( $this->error->get_error_data() ) {
?>
<ol>
<li><?php echo implode( '</li><li>', $this->error->get_error_data() ); ?></li>
</ol>
<?php
}
?>
</div>
<?php
}
}
/**
* Show success messages in admin notices
*/
public function admin_notices_success() {
?>
<div class="message updated"><p>
<ul>
<?php
foreach ( $this->success as $message ) {
echo '<li>' . $message . '</li>';
}
?>
</ul>
</p></div>
<?php
}
/**
* Check translation queue after display
*
* @param array $translation_jobs Translation jobs.
*/
public function translation_queue_after_display( $translation_jobs = array() ) {
if ( ! $this->has_translation_jobs( $translation_jobs ) ) {
return;
}
$export_label = esc_html__( 'Export all jobs:', 'wpml-translation-management' );
$cookie_filters = WPML_Translations_Queue::get_cookie_filters();
if ( $cookie_filters ) {
$type = __( 'All types', 'wpml-translation-management' );
if ( ! empty( $cookie_filters['type'] ) ) {
$post_slug = preg_replace( '/^post_|^package_/', '', $cookie_filters['type'], 1 );
$post_types = $this->sitepress->get_translatable_documents( true );
$post_types = apply_filters( 'wpml_get_translatable_types', $post_types );
if ( array_key_exists( $post_slug, $post_types ) ) {
$type = $post_types[ $post_slug ]->label;
}
}
$from = ! empty( $cookie_filters['from'] )
? $this->sitepress->get_display_language_name( $cookie_filters['from'] )
: __( 'Any language', 'wpml-translation-management' );
$to = ! empty( $cookie_filters['to'] )
? $this->sitepress->get_display_language_name( $cookie_filters['to'] )
: __( 'Any language', 'wpml-translation-management' );
$status = ! empty( $cookie_filters['status'] ) && (int) $cookie_filters['status'] !== ICL_TM_WAITING_FOR_TRANSLATOR
? TranslationManagement::status2text( $cookie_filters['status'] )
: ( ! empty( $cookie_filters['status'] ) ? __( 'Available to translate', 'wpml-translation-management' ) : 'All statuses' );
$export_label = sprintf(
// translators: %1: post type, %2: from language, %3: to language, %4: status.
esc_html__( 'Export all filtered jobs of %1$s from %2$s to %3$s in %4$s:', 'wpml-translation-management' ),
'<b>' . $type . '</b>',
'<b>' . $from . '</b>',
'<b>' . $to . '</b>',
'<b>' . $status . '</b>'
);
}
?>
<br/>
<table class="widefat">
<thead>
<tr>
<th><?php esc_html_e( 'Import / Export XLIFF', 'wpml-translation-management' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<?php if ( ! $this->simplexml_on ) : ?>
<div class="otgs-notice error">
<p>
<strong><?php esc_html_e( 'SimpleXML missing!', 'wpml-translation-management' ); ?></strong>
</p>
<p>
<?php esc_html_e( 'SimpleXML extension is required for using XLIFF files in WPML Translation Management.', 'wpml-translation-management' ); ?>
<a href="https://wpml.org/?page_id=716"><?php esc_html_e( 'WPML Minimum Requirements', 'wpml-translation-management' ); ?></a>
</p>
</div>
<?php endif; ?>
<form method="post" id="translation-xliff-export-all-filtered" action="">
<label for="wpml_xliff_export_all_filtered"><?php echo $export_label; ?></label>
<select name="xliff_version" class="select" <?php disabled( ! $this->simplexml_on ); ?>>
<?php
echo $this->get_xliff_version_select_options();
?>
</select>
<input
type="submit"
value="<?php esc_attr_e( 'Export', 'wpml-translation-management' ); ?>"
<?php
disabled( ! $this->simplexml_on );
?>
name="wpml_xliff_export_all_filtered" id="xliff_download"
class="button-secondary action"/>
<input
type="hidden" value="<?php echo wp_create_nonce( 'xliff-export-all-filtered' ); ?>"
name="nonce">
</form>
<hr>
<form enctype="multipart/form-data" method="post" id="translation-xliff-upload" action="">
<label for="upload-xliff-file"><?php _e( 'Select the xliff file or zip file to upload from your computer:&nbsp;', 'wpml-translation-management' ); ?></label>
<input
type="file" id="upload-xliff-file"
name="import" <?php disabled( ! $this->simplexml_on ); ?> />
<input
type="submit" value="<?php _e( 'Upload', 'wpml-translation-management' ); ?>"
name="xliff_upload" id="xliff_upload" class="button-secondary action"
<?php
disabled( ! $this->simplexml_on );
?>
/>
</form>
</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Print online js script
*/
public function js_scripts() {
?>
<script type="text/javascript">
var wpml_xliff_ajax_nonce = '<?php echo wp_create_nonce( 'icl_xliff_options_form_nonce' ); ?>';
</script>
<?php
}
/**
* Provide translator notification
*/
public function translator_notification() {
$checked = $this->sitepress->get_setting( 'include_xliff_in_notification' ) ? 'checked="checked"' : '';
?>
<input
type="checkbox" name="include_xliff" id="icl_include_xliff"
value="1" <?php echo $checked; ?>/>
<label
for="icl_include_xliff"><?php _e( 'Include XLIFF files in notification emails', 'wpml-translation-management' ); ?></label>
<?php
}
/**
* Get user xliff version
*
* @return bool|string
*/
private function get_user_xliff_version() {
return $this->sitepress->get_setting( 'tm_xliff_version', false );
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_XLIFF_Post_Type extends WPML_TM_XLIFF_Phase {
private $post_type;
public function __construct( $post_type = '' ) {
$this->post_type = $post_type;
}
/**
* @return string
*/
protected function get_data() {
return $this->post_type;
}
protected function get_phase_name() {
return 'post_type';
}
protected function get_process_name() {
return 'Post type';
}
}

View File

@@ -0,0 +1,25 @@
<?php
class WPML_TM_Xliff_Reader_Factory extends WPML_TM_Job_Factory_User {
/**
* @return WPML_TM_General_Xliff_Reader
*/
public function general_xliff_reader() {
return new WPML_TM_General_Xliff_Reader( $this->job_factory );
}
public function general_xliff_import() {
return new WPML_TM_General_Xliff_Import( $this->job_factory, $this );
}
/**
* @return WPML_TM_String_Xliff_Reader
*/
public function string_xliff_reader() {
return new WPML_TM_String_Xliff_Reader( $this->job_factory );
}
}

View File

@@ -0,0 +1,34 @@
<?php
abstract class WPML_TM_Xliff_Reader extends WPML_TM_Xliff_Shared {
/**
* @param string $content Xliff file string content
*
* @return array
*/
abstract public function get_data( $content );
/**
* Parse a XML containing the XLIFF
*
* @param string $content
*
* @return SimpleXMLElement|WP_Error The parsed XLIFF or a WP error in case it could not be parsed
*/
public function load_xliff( $content ) {
try {
$xml = simplexml_load_string( $content );
} catch ( Exception $e ) {
$xml = false;
}
return $xml ? $xml
: new WP_Error(
'not_xml_file',
sprintf(
__( 'The xliff file could not be read.', 'wpml-translation-management' )
)
);
}
}

View File

@@ -0,0 +1,261 @@
<?php
abstract class WPML_TM_Xliff_Shared extends WPML_TM_Job_Factory_User {
/** @var WP_Error $error */
protected $error;
/** @var WPML_TM_Validate_HTML */
private $validator = null;
/**
* @param $string
*
* @return mixed
*/
protected function replace_xliff_new_line_tag_with_new_line( $string ) {
return WPML_TP_Xliff_Parser::restore_new_line( $string );
}
/**
* @param SimpleXMLElement $xliff
*
* @return string
*/
protected function identifier_from_xliff( $xliff ) {
$file_attributes = $xliff->{'file'}->attributes();
return (string) $file_attributes['original'];
}
/**
* @param SimpleXMLElement $xliff
*
* @return stdClass|WP_Error
*/
public function get_job_for_xliff( SimpleXMLElement $xliff ) {
$identifier = $this->identifier_from_xliff( $xliff );
$job_identifier_parts = explode( '-', $identifier );
if ( count( $job_identifier_parts ) === 2 && is_numeric( $job_identifier_parts[0] ) ) {
$job_id = $job_identifier_parts[0];
$job_id = apply_filters( 'wpml_job_id', $job_id );
$md5 = $job_identifier_parts[1];
/** @var stdClass $job */
$job = $this->job_factory->get_translation_job( (int) $job_id, false, 1, false );
if ( ! $job || $md5 !== md5( $job_id . $job->original_doc_id ) ) {
$job = $this->does_not_belong_error();
}
} else {
$job = $this->invalid_xliff_error();
}
return $job;
}
/**
* @param $xliff_node
*
* @return string
*/
protected function get_xliff_node_target( $xliff_node ) {
$target = '';
if ( isset( $xliff_node->target->mrk ) ) {
$target = (string) $xliff_node->target->mrk;
} elseif ( isset( $xliff_node->target ) ) {
$target = (string) $xliff_node->target;
}
return $target;
}
/**
* @param $validator WPML_TM_Validate_HTML
*/
public function set_validator( $validator ) {
$this->validator = $validator;
}
/**
* @return WPML_TM_Validate_HTML
*/
private function get_validator() {
if ( null === $this->validator ) {
$this->set_validator( new WPML_TM_Validate_HTML() );
}
return $this->validator;
}
protected function generate_job_data( SimpleXMLElement $xliff, $job ) {
$data = [
'job_id' => $job->job_id,
'rid' => $job->rid,
'fields' => [],
'complete' => 1,
];
foreach ( $xliff->file->body->children() as $node ) {
$attr = $node->attributes();
$type = (string) $attr['id'];
$target = $this->get_xliff_node_target( $node );
if ( 'html' === (string) $attr['datatype'] ) {
$target = $this->get_validator()->restore_html( $target );
}
if ( ! $this->is_valid_target( $target ) ) {
return $this->invalid_xliff_error( array( 'target' ) );
}
foreach ( $job->elements as $element ) {
if ( $element->field_type === $type ) {
$target = $this->replace_xliff_new_line_tag_with_new_line( $target );
$field = array();
$field['data'] = $target;
$field['finished'] = 1;
$field['tid'] = $element->tid;
$field['field_type'] = $element->field_type;
$field['format'] = $element->field_format;
$data['fields'][ $element->field_type ] = $field;
break;
}
}
}
return $data;
}
/**
* Validate XLIFF target on reading XLIFF.
*
* @param $target string
*
* @return bool
*/
private function is_valid_target( $target ) {
return $target || '0' === $target;
}
protected function validate_file( $name, $content, $current_user ) {
$xml = $this->check_xml_file( $name, $content );
$this->error = null;
if ( is_wp_error( $xml ) ) {
$this->error = $xml;
return null;
}
$job = $this->get_job_for_xliff( $xml );
if ( is_wp_error( $job ) ) {
$this->error = $job;
return null;
}
if ( ! $this->is_user_the_job_owner( $current_user, $job ) ) {
$this->error = $this->not_the_job_owner_error( $job );
return null;
}
$job_data = $this->generate_job_data( $xml, $job );
if ( is_wp_error( $job_data ) ) {
$this->error = $job_data;
return null;
}
return array( $job, $job_data );
}
/**
* @param string $filename
* @return bool
*/
function validate_file_name( $filename ) {
$ignored_files = apply_filters( 'wpml_xliff_ignored_files', array( '__MACOSX' ) );
return ! ( preg_match( '/(\/)/', $filename ) || in_array( $filename, $ignored_files, false ) );
}
protected function is_user_the_job_owner( $current_user, $job ) {
return (int) $current_user->ID === (int) $job->translator_id;
}
protected function not_the_job_owner_error( $job ) {
$message = sprintf( __( 'The translation job (%s) doesn\'t belong to you.', 'wpml-translation-management' ), $job->job_id );
return new WP_Error( 'not_your_job', $message );
}
/**
* @param string $name
* @param string $content
*
* @return bool|SimpleXMLElement|WP_Error
*/
protected function check_xml_file( $name, $content ) {
set_error_handler( array( $this, 'error_handler' ) );
try {
$xml = simplexml_load_string( $content );
} catch ( Exception $e ) {
$xml = false;
}
restore_error_handler();
if ( ! $xml || ! isset( $xml->file ) ) {
$xml = $this->not_xml_file_error( $name );
}
return $xml;
}
/**
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
*
* @throws ErrorException
*/
protected function error_handler( $errno, $errstr, $errfile, $errline ) {
throw new ErrorException( $errstr, $errno, 1, $errfile, $errline );
}
/**
* @param string $name
*
* @return WP_Error
*/
protected function not_xml_file_error( $name ) {
$message = sprintf( __( '"%s" is not a valid XLIFF file.', 'wpml-translation-management' ), $name );
return new WP_Error( 'not_xml_file', $message );
}
/**
* @param array $missing_data
*
* @return WP_Error
*/
protected function invalid_xliff_error( array $missing_data = array() ) {
$message = __( 'The uploaded xliff file does not seem to be properly formed.', 'wpml-translation-management' );
if ( $missing_data ) {
$message .= '<br>' . __( 'Missing or wrong data:', 'wpml-translation-management' );
if ( count( $missing_data ) > 1 ) {
$message .= '<ol>';
$message .= '<li><strong>' . implode( '</strong></li><li><strong>', $missing_data ) . '</strong></li>';
$message .= '</ol>';
} else {
$message .= ' <strong>' . $missing_data[0] . '</strong>';
}
}
return new WP_Error( 'xliff_invalid', $message );
}
/**
* @return WP_Error
*/
protected function does_not_belong_error() {
return new WP_Error( 'xliff_does_not_match', __( "The uploaded xliff file doesn't belong to this system.", 'wpml-translation-management' ) );
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_XLIFF_Shortcodes extends WPML_TM_XLIFF_Phase {
const SHORTCODE_STORE_OPTION_KEY = 'wpml_xliff_shortcodes';
/**
* @return string
*/
protected function get_data() {
return implode( ',', $this->get_shortcodes() );
}
/**
* @return array
*/
private function get_shortcodes() {
global $shortcode_tags;
$registered_shortcodes = array();
if ( $shortcode_tags ) {
$registered_shortcodes = array_keys( $shortcode_tags );
}
$stored_shortcodes = get_option( self::SHORTCODE_STORE_OPTION_KEY, array() );
return $this->get_sanitized_shortcodes( $registered_shortcodes, $stored_shortcodes );
}
private function get_sanitized_shortcodes( array $shortcodes1, array $shortcodes2 ) {
return array_unique( array_filter( apply_filters( 'wpml_shortcode_list', array_merge( $shortcodes1, $shortcodes2 ) ) ) );
}
protected function get_phase_name() {
return 'shortcodes';
}
protected function get_process_name() {
return 'Shortcodes identification';
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_XLIFF_Translator_Notes extends WPML_TM_XLIFF_Phase {
private $post_id;
public function __construct( $post_id = 0 ) {
$this->post_id = $post_id;
}
/**
* @return string
*/
protected function get_data() {
if ( $this->post_id ) {
return WPML_TM_Translator_Note::get( $this->post_id );
} else {
return '';
}
}
protected function get_phase_name() {
return 'notes';
}
protected function get_process_name() {
return 'Notes';
}
}

View File

@@ -0,0 +1,474 @@
<?php
/**
* @package wpml-core
*/
use WPML\FP\Obj;
use WPML\TM\Jobs\FieldId;
class WPML_TM_Xliff_Writer {
const TAB = "\t";
protected $job_factory;
private $xliff_version;
private $xliff_shortcodes;
private $translator_notes;
/**
* WPML_TM_xliff constructor.
*
* @param WPML_Translation_Job_Factory $job_factory
* @param string $xliff_version
* @param \WPML_TM_XLIFF_Shortcodes|null $xliff_shortcodes
*/
public function __construct( WPML_Translation_Job_Factory $job_factory, $xliff_version = TRANSLATION_PROXY_XLIFF_VERSION, WPML_TM_XLIFF_Shortcodes $xliff_shortcodes = null ) {
$this->job_factory = $job_factory;
$this->xliff_version = $xliff_version;
if ( ! $xliff_shortcodes ) {
$xliff_shortcodes = wpml_tm_xliff_shortcodes();
}
$this->xliff_shortcodes = $xliff_shortcodes;
$this->translator_notes = new WPML_TM_XLIFF_Translator_Notes();
}
/**
* Generate a XLIFF file for a given job.
*
* @param int $job_id
*
* @return resource XLIFF representation of the job
*/
public function get_job_xliff_file( $job_id ) {
return $this->generate_xliff_file( $this->generate_job_xliff( $job_id ) );
}
/**
* Generate a XLIFF string for a given post or external type (e.g. package) job.
*
* @param int $job_id
*
* @return string XLIFF representation of the job
*/
public function generate_job_xliff( $job_id ) {
/** @var TranslationManagement $iclTranslationManagement */
global $iclTranslationManagement;
// don't include not-translatable and don't auto-assign
$job = $iclTranslationManagement->get_translation_job( (int) $job_id, false, false, 1 );
$translation_units = $this->get_job_translation_units_data( $job );
$original = $job_id . '-' . md5( $job_id . $job->original_doc_id );
$original_post_type = isset( $job->original_post_type ) ? $job->original_post_type : null;
$external_file_url = $this->get_external_url( $job );
$this->get_translator_notes( $job );
$xliff = $this->generate_xliff(
$original,
$job->source_language_code,
$job->language_code,
$translation_units,
$external_file_url,
$original_post_type
);
return $xliff;
}
/**
* Generate a XLIFF file for a given set of strings.
*
* @param array $strings
* @param string $source_language
* @param string $target_language
*
* @return resource XLIFF file
*/
public function get_strings_xliff_file( $strings, $source_language, $target_language ) {
$strings = $this->pre_populate_strings_with_translation_memory( $strings, $source_language, $target_language );
return $this->generate_xliff_file(
$this->generate_xliff(
uniqid( 'string-', true ),
$source_language,
$target_language,
$this->generate_strings_translation_units_data( $strings )
)
);
}
private function generate_xliff(
$original_id,
$source_language,
$target_language,
array $translation_units = array(),
$external_file_url = null,
$original_post_type = null
) {
$xliff = new WPML_TM_XLIFF( $this->get_xliff_version(), '1.0', 'utf-8' );
$phase_group = array();
$phase_group = array_merge( $phase_group, $this->xliff_shortcodes->get() );
$phase_group = array_merge( $phase_group, $this->translator_notes->get() );
$post_type_phase = new WPML_TM_XLIFF_Post_Type( $original_post_type );
$phase_group = array_merge( $phase_group, $post_type_phase->get() );
$string = $xliff
->setFileAttributes(
array(
'original' => $original_id,
'source-language' => $source_language,
'target-language' => $target_language,
'datatype' => 'plaintext',
)
)
->setReferences(
array(
'external-file' => $external_file_url,
)
)
->setPhaseGroup( $phase_group )
->setTranslationUnits( $translation_units )
->toString();
return $string;
}
private function get_xliff_version() {
switch ( $this->xliff_version ) {
case '10':
return '1.0';
case '11':
return '1.1';
case '12':
default:
return '1.2';
}
}
/**
* Generate translation units for a given set of strings.
*
* The units are the actual content to be translated
* Represented as a source and a target
*
* @param array $strings
*
* @return array The translation units representation
*/
private function generate_strings_translation_units_data( $strings ) {
$translation_units = array();
foreach ( $strings as $string ) {
$id = 'string-' . $string->id;
$translation_units[] = $this->get_translation_unit_data( $id, 'string', $string->value, $string->translation, $string->translated_from_memory );
}
return $translation_units;
}
/**
* @param stdClass[] $strings
* @param string $source_lang
* @param string $target_lang
*
* @return stdClass[]
*/
private function pre_populate_strings_with_translation_memory( $strings, $source_lang, $target_lang ) {
$strings_to_translate = wp_list_pluck( $strings, 'value' );
$original_translated_map = $this->get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang );
foreach ( $strings as &$string ) {
$string->translated_from_memory = false;
$string->translation = $string->value;
if ( array_key_exists( $string->value, $original_translated_map ) ) {
$string->translation = $original_translated_map[ $string->value ];
$string->translated_from_memory = true;
}
}
return $strings;
}
/**
* @param array $strings_to_translate
* @param string $source_lang
* @param string $target_lang
*
* @return array
*/
private function get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang ) {
$args = array(
'strings' => $strings_to_translate,
'source_lang' => $source_lang,
'target_lang' => $target_lang,
);
$strings_in_memory = apply_filters( 'wpml_st_get_translation_memory', array(), $args );
if ( $strings_in_memory ) {
return wp_list_pluck( $strings_in_memory, 'translation', 'original' );
}
return array();
}
/**
* Generate translation units.
*
* The units are the actual content to be translated
* Represented as a source and a target
*
* @param stdClass $job
*
* @return array The translation units data
*/
private function get_job_translation_units_data( $job ) {
$translation_units = array();
/** @var array $elements */
$elements = $job->elements;
if ( $elements ) {
$elements = $this->pre_populate_elements_with_translation_memory( $elements, $job->source_language_code, $job->language_code );
foreach ( $elements as $element ) {
if ( 1 === (int) $element->field_translate ) {
$field_data_translated = base64_decode( $element->field_data_translated );
$field_data = base64_decode( $element->field_data );
/**
* It modifies the content of a single field data which represents, for example, one paragraph in post content.
*
* @since 2.10.0
* @param string $field_data
*/
$field_data = apply_filters( 'wpml_tm_xliff_unit_field_data', $field_data );
if ( 0 === strpos( $element->field_type, 'field-' ) ) {
$field_data_translated = apply_filters(
'wpml_tm_xliff_export_translated_cf',
$field_data_translated,
$element
);
$field_data = apply_filters(
'wpml_tm_xliff_export_original_cf',
$field_data,
$element
);
}
// check for untranslated fields and copy the original if required.
if ( ! null === $field_data_translated || '' === $field_data_translated ) {
$field_data_translated = $this->remove_invalid_chars( $field_data );
}
if ( $this->is_valid_unit_content( $field_data ) ) {
$translation_units[] = $this->get_translation_unit_data(
$element->field_type,
$element->field_type,
$field_data,
$field_data_translated,
$element->translated_from_memory,
$element->field_wrap_tag,
$this->get_field_title( $element, $job )
);
}
}
}
}
return $translation_units;
}
/**
* @param \stdClass $field
* @param \stdClass $job
*
* @return string
*/
private function get_field_title( $field, $job ) {
$result = apply_filters( 'wpml_tm_adjust_translation_fields', [ (array) $field ], $job, null );
return Obj::pathOr( '', [ 0, 'title' ], $result );
}
/**
* @param array $elements
* @param string $source_lang
* @param string $target_lang
*
* @return array
*/
private function pre_populate_elements_with_translation_memory( array $elements, $source_lang, $target_lang ) {
$strings_to_translate = array();
foreach ( $elements as &$element ) {
if ( preg_match( '/^package-string/', $element->field_type ) ) {
$strings_to_translate[ $element->tid ] = base64_decode( $element->field_data );
}
$element->translated_from_memory = FieldId::is_any_term_field( $element->field_type )
&& $element->field_data_translated
&& $element->field_data != $element->field_data_translated;
}
$original_translated_map = $this->get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang );
if ( $original_translated_map ) {
foreach ( $elements as &$element ) {
if ( array_key_exists( $element->tid, $strings_to_translate )
&& array_key_exists( $strings_to_translate[ $element->tid ], $original_translated_map )
) {
$element->field_data_translated = base64_encode( $original_translated_map[ $strings_to_translate[ $element->tid ] ] );
$element->translated_from_memory = true;
}
}
}
return $elements;
}
/**
* Get translation unit data.
*
* @param string $field_id Field ID.
* @param string $field_name Field name.
* @param string $field_data Field content.
* @param string $field_data_translated Field translated content.
* @param boolean $is_translated_from_memory Boolean flag - is translated from memory.
* @param string $field_wrap_tag Field wrap tag (h1...h6, etc.)
* @param string $title
*
* @return array
*/
private function get_translation_unit_data(
$field_id,
$field_name,
$field_data,
$field_data_translated,
$is_translated_from_memory = false,
$field_wrap_tag = '',
$title = ''
) {
global $sitepress;
$field_data = $this->remove_invalid_chars( $field_data );
$translation_unit = array();
$field_data = $this->remove_line_breaks_inside_tags( $field_data );
$field_data_translated = $this->remove_line_breaks_inside_tags( $field_data_translated );
if ( $sitepress->get_setting( 'xliff_newlines' ) === WPML_XLIFF_TM_NEWLINES_REPLACE ) {
$field_data = $this->replace_new_line_with_tag( $field_data );
$field_data_translated = $this->replace_new_line_with_tag( $field_data_translated );
}
if ( $title ) {
$translation_unit['attributes']['extradata'] = $title;
}
$translation_unit['attributes']['resname'] = $field_name;
$translation_unit['attributes']['restype'] = 'string';
$translation_unit['attributes']['datatype'] = 'html';
$translation_unit['attributes']['id'] = $field_id;
$translation_unit['source'] = array( 'content' => $field_data );
$translation_unit['target'] = array( 'content' => $field_data_translated );
$translation_unit['note'] = array( 'content' => $field_wrap_tag );
if ( $is_translated_from_memory ) {
$translation_unit['target']['attributes'] = array(
'state' => 'needs-review-translation',
'state-qualifier' => 'tm-suggestion',
);
}
return $translation_unit;
}
/**
* @param string $string
*
* @return string
*/
protected function replace_new_line_with_tag( $string ) {
return str_replace( array( "\n", "\r" ), array( '<br class="xliff-newline" />', '' ), $string );
}
private function remove_line_breaks_inside_tags( $string ) {
return preg_replace_callback( '/(<[^>]*>)/m', array( $this, 'remove_line_breaks_inside_tag_callback' ), $string );
}
/**
* @param array $matches
*
* @return string
*/
private function remove_line_breaks_inside_tag_callback( array $matches ) {
$tag_string = preg_replace( '/([\n\r\t ]+)/', ' ', $matches[0] );
$tag_string = preg_replace( '/(<[\s]+)/', '<', $tag_string );
return preg_replace( '/([\s]+>)/', '>', $tag_string );
}
/**
* @param string $string
*
* Remove all characters below 0x20 except for 0x09, 0x0A and 0x0D
* @see https://www.w3.org/TR/xml/#charsets
*
* @return string
*/
private function remove_invalid_chars( $string ) {
return preg_replace( '/[\x00-\x08\x0B-\x0C\x0E-\x1F]/', '', $string );
}
/**
* Save a xliff string to a temporary file and return the file ressource
* handle
*
* @param string $xliff_content
*
* @return resource XLIFF
*/
private function generate_xliff_file( $xliff_content ) {
$file = fopen( 'php://temp', 'rb+' );
fwrite( $file, $xliff_content );
rewind( $file );
return $file;
}
/**
* @param $job
*
* @return false|null|string
*/
private function get_external_url( $job ) {
$external_file_url = null;
if ( isset( $job->original_doc_id ) && 'post' === $job->element_type_prefix ) {
$external_file_url = get_permalink( $job->original_doc_id );
return $external_file_url;
}
return $external_file_url;
}
/**
* @param $content
*
* @return bool
*/
private function is_valid_unit_content( $content ) {
$content = preg_replace( '/[^#\w]*/u', '', $content );
return $content || '0' === $content;
}
private function get_translator_notes( $job ) {
$this->translator_notes = new WPML_TM_XLIFF_Translator_Notes(
isset( $job->original_doc_id ) ? $job->original_doc_id : 0
);
}
}

View File

@@ -0,0 +1,22 @@
<?php
abstract class WPML_TM_XLIFF_Phase {
public function get() {
$data = $this->get_data();
if ( $data ) {
return array(
$this->get_phase_name() => array(
'process-name' => $this->get_process_name(),
'note' => $data,
),
);
}
return array();
}
abstract protected function get_data();
abstract protected function get_phase_name();
abstract protected function get_process_name();
}

View File

@@ -0,0 +1,251 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_XLIFF {
/** @var DOMElement */
private $body;
/** @var DOMDocument */
private $dom;
/** @var DOMDocumentType */
private $dtd;
/** @var DOMElement */
private $file;
/** @var DOMElement */
private $file_header;
/** @var DOMElement */
private $file_reference;
/** @var DOMElement */
private $phase_group;
/** @var DOMElement */
private $root;
/** @var array */
private $trans_units;
/** @var string */
private $xliff_version;
/**
* WPML_TM_XLIFF constructor.
*
* @param string $xliff_version
* @param string $xml_version
* @param string $xml_encoding
*/
public function __construct( $xliff_version = '1.2', $xml_version = '1.0', $xml_encoding = 'utf-8' ) {
$this->dom = new DOMDocument( $xml_version, $xml_encoding );
$this->root = $this->dom->createElement( 'xliff' );
$this->file = $this->dom->createElement( 'file' );
$this->file_header = $this->dom->createElement( 'header' );
$this->body = $this->dom->createElement( 'body' );
$this->xliff_version = $xliff_version;
}
/**
* @param array $attributes
*
* @return $this
*/
public function setFileAttributes( $attributes ) {
foreach ( $attributes as $name => $value ) {
$this->file->setAttribute( $name, $value );
}
return $this;
}
/**
* @param array $args
*
* @return $this
*/
public function setPhaseGroup( array $args ) {
if ( $args ) {
$phase_items = array();
foreach ( $args as $name => $data ) {
if (
$name
&& array_key_exists( 'process-name', $data )
&& array_key_exists( 'note', $data )
&& $data['note']
) {
$phase = $this->dom->createElement( 'phase' );
$phase->setAttribute( 'phase-name', $name );
$phase->setAttribute( 'process-name', $data['process-name'] );
$note = $this->dom->createElement( 'note' );
$note->nodeValue = $data['note'];
$phase->appendChild( $note );
$phase_items[] = $phase;
}
}
if ( $phase_items ) {
$this->phase_group = $this->dom->createElement( 'phase-group' );
foreach ( $phase_items as $phase_item ) {
$this->phase_group->appendChild( $phase_item );
}
}
}
return $this;
}
/**
* @param array $references
*
* @return $this
*/
public function setReferences( array $references ) {
if ( $references ) {
foreach ( $references as $name => $value ) {
if ( ! $value ) {
continue;
}
if ( ! $this->file_reference ) {
$this->file_reference = $this->dom->createElement( 'reference' );
}
$reference = $this->dom->createElement( $name );
$reference->setAttribute( 'href', $value );
$this->file_reference->appendChild( $reference );
}
}
return $this;
}
// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
/**
* Set translation units for xliff.
*
* @param array $trans_units Translation units.
*
* @return $this
*/
public function setTranslationUnits( $trans_units ) {
// phpcs:enable
if ( $trans_units ) {
foreach ( $trans_units as $trans_unit ) {
$trans_unit_element = $this->dom->createElement( 'trans-unit' );
if ( array_key_exists( 'attributes', $trans_unit ) && is_array( $trans_unit['attributes'] ) ) {
foreach ( $trans_unit['attributes'] as $name => $value ) {
$trans_unit_element->setAttribute( $name, $value );
}
}
$this->appendData( 'source', $trans_unit, $trans_unit_element );
$this->appendData( 'target', $trans_unit, $trans_unit_element );
if ( $trans_unit['note']['content'] ) {
$note = $this->dom->createElement( 'note' );
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$note->nodeValue = 'wrap_tag:' . $trans_unit['note']['content'];
// phpcs:enable
$trans_unit_element->appendChild( $note );
}
$this->trans_units[] = $trans_unit_element;
}
}
return $this;
}
/**
* @param string $type
* @param array $trans_unit
* @param DOMElement $trans_unit_element
*/
private function appendData( $type, $trans_unit, $trans_unit_element ) {
if ( array_key_exists( $type, $trans_unit ) ) {
$source = $this->dom->createElement( $type );
$datatype = isset( $trans_unit['attributes']['datatype'] ) ? $trans_unit['attributes']['datatype'] : '';
$source_cdata = $this->dom->createCDATASection(
$this->validate( $datatype, $trans_unit[ $type ]['content'] )
);
$source->appendChild( $source_cdata );
if ( array_key_exists( 'attributes', $trans_unit[ $type ] ) ) {
foreach ( $trans_unit[ $type ]['attributes'] as $name => $value ) {
$source->setAttribute( $name, $value );
}
}
$trans_unit_element->appendChild( $source );
}
}
/**
* Validate content.
*
* @param string $datatype Type of content data.
* @param string $content Content.
*
* @return string
*/
private function validate( $datatype, $content ) {
if ( 'html' === $datatype ) {
$validator = new WPML_TM_Validate_HTML();
$validator->validate( $content );
return $validator->get_html();
}
return $content;
}
public function toString() {
$this->compose();
return trim( $this->dom->saveXML() );
}
private function compose() {
$this->dom->xmlStandalone = false;
$this->setRoot( $this->xliff_version );
if ( $this->dtd ) {
$this->dom->appendChild( $this->dtd );
}
if ( $this->phase_group ) {
$this->file_header->appendChild( $this->phase_group );
}
if ( $this->file_reference ) {
$this->file_header->appendChild( $this->file_reference );
}
if ( $this->trans_units ) {
foreach ( $this->trans_units as $trans_unit ) {
$this->body->appendChild( $trans_unit );
}
}
$this->file->appendChild( $this->file_header );
$this->file->appendChild( $this->body );
$this->root->appendChild( $this->file );
$this->dom->appendChild( $this->root );
}
private function setRoot( $version ) {
if ( $version === '1.0' ) {
$implementation = new DOMImplementation();
$this->dtd = $implementation->createDocumentType(
'xliff',
'-//XLIFF//DTD XLIFF//EN',
'http://www.oasis-open.org/committees/xliff/documents/xliff.dtd'
);
}
$this->root->setAttribute( 'version', $version );
$this->root->setAttribute( 'xmlns', 'urn:oasis:names:tc:xliff:document:' . $version );
}
}