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,40 @@
<?php
class WPML_TM_Log implements WPML_TP_API_Log_Interface {
const LOG_WP_OPTION = '_wpml_tp_api_Logger';
const LOG_MAX_SIZE = 500;
public function log( $action, $data = array() ) {
$log_base_data = array(
'timestamp' => false,
'action' => false,
);
$log_item = array_merge( $log_base_data, $data );
$log_item['timestamp'] = date( 'Y-m-d H:i:s' );
$log_item['action'] = $action;
$log = $this->get_log_data();
$log = array_slice( $log, - ( self::LOG_MAX_SIZE - 1 ) );
$log[] = $log_item;
$this->update_log( $log );
}
private function update_log( $log ) {
return update_option( self::LOG_WP_OPTION, $log, false );
}
public function flush_log() {
return update_option( self::LOG_WP_OPTION, [], false );
}
public function get_log_data() {
$log = get_option( self::LOG_WP_OPTION, [] );
return is_array( $log ) ? $log : [];
}
}

View File

@@ -0,0 +1,134 @@
<?php
class WPML_TP_API_Client {
/** @var string */
private $proxy_url;
/** @var WP_Http $http */
private $http;
/** @var WPML_TP_Lock $tp_lock */
private $tp_lock;
/** @var WPML_TP_HTTP_Request_Filter */
private $request_filter;
public function __construct(
$proxy_url,
WP_Http $http,
WPML_TP_Lock $tp_lock,
WPML_TP_HTTP_Request_Filter $request_filter
) {
$this->proxy_url = $proxy_url;
$this->http = $http;
$this->tp_lock = $tp_lock;
$this->request_filter = $request_filter;
}
/**
* @param WPML_TP_API_Request $request
* @param bool $raw_json_response
*
* @return array|mixed|stdClass|string
* @throws WPML_TP_API_Exception
*/
public function send_request( WPML_TP_API_Request $request, $raw_json_response = false ) {
if ( $this->tp_lock->is_locked( $request->get_url() ) ) {
throw new WPML_TP_API_Exception( 'Communication with translation proxy is not allowed.', $request );
}
WPML_TranslationProxy_Com_Log::log_call( $request->get_url(), $request->get_params() );
$response = $this->call_remote_api( $request );
if ( ! $response || is_wp_error( $response ) || ( isset( $response['response']['code'] ) && $response['response']['code'] >= 400 ) ) {
throw new WPML_TP_API_Exception( 'Communication error', $request, $response );
}
if ( isset( $response['headers']['content-type'] ) ) {
$content_type = $response['headers']['content-type'];
$response = $response['body'];
if ( strpos( $content_type, 'zip' ) !== false ) {
$response = gzdecode( $response );
} else {
WPML_TranslationProxy_Com_Log::log_response( $response );
}
$json_response = json_decode( $response );
if ( $json_response ) {
if ( $raw_json_response ) {
$response = $json_response;
} else {
$response = $this->handle_json_response( $request, $json_response );
}
}
}
return $response;
}
/**
* @param WPML_TP_API_Request $request
*
* @return null|string
*/
private function call_remote_api( WPML_TP_API_Request $request ) {
$context = $this->filter_request_params( $request->get_params(), $request->get_method() );
return $this->http->request( $this->proxy_url . $request->get_url(), $context );
}
/**
* @param array $params request parameters
* @param string $method HTTP request method
*
* @return array
*/
private function filter_request_params( $params, $method ) {
return $this->request_filter->build_request_context(
array(
'method' => $method,
'body' => $params,
'sslverify' => true,
'timeout' => 60,
)
);
}
/**
* @param WPML_TP_API_Request $request
* @param stdClass $response
*
* @return mixed
* @throws WPML_TP_API_Exception
*/
private function handle_json_response( WPML_TP_API_Request $request, $response ) {
if ( $request->has_api_response() ) {
if ( ! isset( $response->status->code ) || $response->status->code !== 0 ) {
throw new WPML_TP_API_Exception(
$this->generate_error_message_from_status_field( $response ),
$request,
$response
);
}
$response = $response->response;
}
return $response;
}
private function generate_error_message_from_status_field( $response ) {
$message = '';
if ( isset( $response->status->message ) ) {
if ( isset( $response->status->code ) ) {
$message = '(' . $response->status->code . ') ';
}
$message .= $response->status->message;
} else {
$message = 'Unknown error';
}
return $message;
}
}

View File

@@ -0,0 +1,46 @@
<?php
class WPML_TP_API_Exception extends Exception {
public function __construct( $message, WPML_TP_API_Request $request = null, $response = null ) {
if ( $request ) {
$message .= ' ' . $this->get_exception_message(
$request->get_url(),
$request->get_method(),
$request->get_params(),
$response
);
}
parent::__construct( $message );
}
private function get_exception_message( $url, $method, $params, $response ) {
return 'Details: |'
. ' url: '
. '`'
. $url
. '`'
. ' method: '
. '`'
. $method
. '`'
. ' param: '
. '`'
. json_encode( $this->filter_params( $params ) )
. '`'
. ' response: '
. '`'
. json_encode( $response )
. '`';
}
/**
* @param array $params
*
* @return array mixed
*/
private function filter_params( $params ) {
return wpml_collect( $params )->forget( 'accesskey' )->toArray();
}
}

View File

@@ -0,0 +1,7 @@
<?php
interface WPML_TP_API_Log_Interface {
public function log( $action, $data = array() );
}

View File

@@ -0,0 +1,131 @@
<?php
class WPML_TP_API_Request {
const API_VERSION = 1.1;
/** @var string */
private $url;
/** @var array */
private $params = array( 'api_version' => self::API_VERSION );
/** @var string */
private $method = 'GET';
/** @var bool */
private $has_api_response = true;
/**
* @param string $url
*/
public function __construct( $url ) {
if ( empty( $url ) ) {
throw new InvalidArgumentException( 'Url cannot be empty' );
}
$this->url = $url;
}
/**
* @param array $params
*/
public function set_params( array $params ) {
$this->params = array_merge( $this->params, $params );
}
/**
* @param string $method
*/
public function set_method( $method ) {
if ( ! in_array( $method, array( 'GET', 'POST', 'PUT', 'DELETE', 'HEAD' ), true ) ) {
throw new InvalidArgumentException( 'HTTP request method has invalid value' );
}
$this->method = $method;
}
/**
* @param bool $has_api_response
*/
public function set_has_api_response( $has_api_response ) {
$this->has_api_response = (bool) $has_api_response;
}
/**
* @return string
*/
public function get_url() {
$url = $this->url;
if ( $this->get_params() ) {
list( $url, $params_used_in_path ) = $this->add_parameters_to_path( $url, $this->get_params() );
if ( 'GET' === $this->get_method() ) {
$url = $this->add_query_parameters( $params_used_in_path, $url );
}
}
return $url;
}
/**
* @return array
*/
public function get_params() {
return $this->params;
}
/**
* @return string
*/
public function get_method() {
return $this->method;
}
/**
* @return bool
*/
public function has_api_response() {
return $this->has_api_response;
}
private function add_parameters_to_path( $url, array $params ) {
$used_params = array();
if ( preg_match_all( '/\{.+?\}/', $url, $symbs ) ) {
foreach ( $symbs[0] as $symb ) {
$without_braces = preg_replace( '/\{|\}/', '', $symb );
if ( preg_match_all( '/\w+/', $without_braces, $indexes ) ) {
foreach ( $indexes[0] as $index ) {
if ( isset( $params[ $index ] ) ) {
$used_params[] = $index;
$value = $params[ $index ];
$url = preg_replace( preg_quote( "/$symb/" ), $value, $url );
}
}
}
}
}
return array( $url, $used_params );
}
/**
* @param $params_used_in_path
* @param $url
*
* @return string
*/
private function add_query_parameters( $params_used_in_path, $url ) {
$url .= '?' . preg_replace(
'/\%5B\d+\%5D/',
'%5B%5D',
wpml_http_build_query(
array_diff_key(
$this->get_params(),
array_fill_keys( $params_used_in_path, 1 )
)
)
);
return $url;
}
}

View File

@@ -0,0 +1,24 @@
<?php
abstract class WPML_TP_API {
/** @var WPML_TP_API_Client */
protected $client;
/** @var WPML_TP_Project */
protected $project;
/** @var WPML_TP_API_Log_Interface */
protected $logger;
public function __construct( WPML_TP_API_Client $client, WPML_TP_Project $project, WPML_TP_API_Log_Interface $logger = null ) {
$this->client = $client;
$this->project = $project;
$this->logger = $logger;
}
protected function log( $action, array $params = array() ) {
if ( null !== $this->logger ) {
$this->logger->log( $action, $params );
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
class WPML_TP_Batch_Sync_API extends WPML_TP_API {
const INIT_SYNC = '/batches/sync.json';
const CHECK_STATUS = '/batches/sync/status.json';
/**
* @param array $batch_ids
*
* @return int[]
* @throws WPML_TP_API_Exception
*/
public function init_synchronization( array $batch_ids ) {
$request = new WPML_TP_API_Request( self::INIT_SYNC );
$request->set_params(
array(
'batch_id' => $batch_ids,
'accesskey' => $this->project->get_access_key(),
)
);
return $this->handle_response( $request );
}
/**
* @return int[]
* @throws WPML_TP_API_Exception
*/
public function check_progress() {
$request = new WPML_TP_API_Request( self::CHECK_STATUS );
$request->set_params( array( 'accesskey' => $this->project->get_access_key() ) );
return $this->handle_response( $request );
}
/**
* @param WPML_TP_API_Request $request
*
* @return array
* @throws WPML_TP_API_Exception
*/
private function handle_response( WPML_TP_API_Request $request ) {
$result = $this->client->send_request( $request, true );
if ( ! $result || empty( $result ) || ! isset( $result->queued_batches ) ) {
throw new WPML_TP_API_Exception( 'Batch synchronization could not be initialized' );
}
return array_map( 'intval', $result->queued_batches );
}
}

View File

@@ -0,0 +1,55 @@
<?php
class WPML_TP_Job_States {
const RECEIVED = 'received';
const WAITING_TRANSLATIONS = 'waiting_translation';
const TRANSLATION_READY = 'translation_ready';
const DELIVERED = 'delivered';
const CANCELLED = 'cancelled';
const ANY = 'any';
/**
* @return array
*/
public static function get_possible_states() {
return array(
self::RECEIVED,
self::WAITING_TRANSLATIONS,
self::TRANSLATION_READY,
self::DELIVERED,
self::CANCELLED,
self::ANY,
);
}
/**
* @return string
*/
public static function get_default_state() {
return self::WAITING_TRANSLATIONS;
}
/**
* @return array
*/
public static function get_finished_states() {
return array(
self::TRANSLATION_READY,
self::DELIVERED,
self::CANCELLED,
);
}
public static function map_tp_state_to_local( $tp_state ) {
switch ( $tp_state ) {
case self::TRANSLATION_READY:
case self::DELIVERED:
return ICL_TM_TRANSLATION_READY_TO_DOWNLOAD;
case self::CANCELLED:
return ICL_TM_NOT_TRANSLATED;
default:
return ICL_TM_IN_PROGRESS;
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
class WPML_TP_Job_Status {
/** @var int */
private $tp_id;
/** @var int */
private $batch_id;
/** @var string */
private $status;
/** @var int */
private $revision;
/** @var WPML_TM_Job_TS_Status|null */
private $ts_status;
/**
* @param int $tp_id
* @param int $batch_id
* @param string $state
* @param WPML_TM_Job_TS_Status|null $ts_status
* @param int $revision
*/
public function __construct( $tp_id, $batch_id, $state, $revision = 1, $ts_status = null ) {
$this->tp_id = (int) $tp_id;
$this->batch_id = (int) $batch_id;
if ( ! in_array( $state, WPML_TP_Job_States::get_possible_states(), true ) ) {
$state = 'any';
}
$this->status = $state;
$this->revision = (int) $revision;
$this->ts_status = $ts_status;
}
/**
* @return int
*/
public function get_tp_id() {
return $this->tp_id;
}
/**
* @return int
*/
public function get_batch_id() {
return $this->batch_id;
}
/**
* @return string
*/
public function get_status() {
return $this->status;
}
/**
* @return int
*/
public function get_revision() {
return $this->revision;
}
/**
* @return WPML_TM_Job_TS_Status|null
*/
public function get_ts_status() {
return $this->ts_status;
}
}

View File

@@ -0,0 +1,136 @@
<?php
class WPML_TP_Jobs_API extends WPML_TP_API {
const CHUNK_SIZE = 100;
/**
* @param int[] $tp_job_ids
*
* @return WPML_TP_Job_Status[]
* @throws WPML_TP_API_Exception
*/
public function get_jobs_statuses( array $tp_job_ids ) {
$this->log( 'Get jobs status', $tp_job_ids );
$chunks = array();
while ( $tp_job_ids ) {
$chunk_ids = array_splice( $tp_job_ids, 0, self::CHUNK_SIZE );
$chunks[] = $this->get_chunk_of_job_statuses( $chunk_ids );
}
$result = call_user_func_array( 'array_merge', $chunks );
return array_map( array( $this, 'build_job_status' ), $result );
}
private function get_chunk_of_job_statuses( $tp_job_ids ) {
$request = new WPML_TP_API_Request( '/jobs.json' );
$request->set_params(
array(
'filter' => array(
'job_ids' => array_filter( $tp_job_ids, 'is_int' ),
'archived' => 1,
),
'accesskey' => $this->project->get_access_key(),
)
);
return $this->client->send_request( $request );
}
/**
* @param array $cms_ids
* @param bool $archived
*
* @return array|mixed|stdClass|string
* @throws WPML_TP_API_Exception
*/
public function get_jobs_per_cms_ids( array $cms_ids, $archived = false ) {
$request = new WPML_TP_API_Request( '/jobs.json' );
$filter = array(
'filter' => array(
'cms_ids' => $cms_ids,
),
'accesskey' => $this->project->get_access_key(),
);
if ( $archived ) {
$filter['filter']['archived'] = (int) $archived;
$request->set_params( $filter );
}
return $this->client->send_request( $request );
}
/**
* @param WPML_TM_Job_Entity $job
* @param string $state
* @param string $post_url
*
* @throws WPML_TP_API_Exception
*/
public function update_job_state(
WPML_TM_Job_Entity $job,
$state = WPML_TP_Job_States::DELIVERED,
$post_url = null
) {
$params = array(
'job_id' => $job->get_tp_id(),
'project_id' => $this->project->get_id(),
'accesskey' => $this->project->get_access_key(),
'job' => array(
'state' => $state,
),
);
if ( $post_url ) {
$params['job']['url'] = $post_url;
}
$request = new WPML_TP_API_Request( '/jobs/{job_id}.json' );
$request->set_params( $params );
$request->set_method( 'PUT' );
$this->client->send_request( $request );
}
private function build_job_status( stdClass $raw_data ) {
return new WPML_TP_Job_Status(
$raw_data->id,
isset( $raw_data->batch->id ) ? $raw_data->batch->id : 0,
$raw_data->job_state,
isset( $raw_data->translation_revision ) ? $raw_data->translation_revision : 1,
$raw_data->ts_status ? new WPML_TM_Job_TS_Status(
$raw_data->ts_status->status,
$raw_data->ts_status->links
) : null
);
}
/**
* @return WPML_TP_Job_Status[]
* @throws WPML_TP_API_Exception
*/
public function get_revised_jobs() {
$this->log( 'Get revised jobs' );
$request = new WPML_TP_API_Request( '/jobs.json' );
$request->set_params(
array(
'filter' => array(
'job_state' => WPML_TP_Job_States::TRANSLATION_READY,
'archived' => 0,
'revision_greater_than' => 1,
),
'accesskey' => $this->project->get_access_key(),
)
);
$result = $this->client->send_request( $request );
return array_map( array( $this, 'build_job_status' ), $result );
}
}

View File

@@ -0,0 +1,105 @@
<?php
use WPML\TM\TranslationProxy\Services\Project\Project;
use WPML\TM\TranslationProxy\Services\Project\SiteDetails;
class WPML_TP_Project_API extends WPML_TP_API {
const API_VERSION = 1.1;
const PROJECTS_ENDPOINT = '/projects.json';
/**
* @throws WPML_TP_API_Exception
*/
public function refresh_language_pairs() {
$this->log( 'Refresh language pairs -> Request sent' );
$request = new WPML_TP_API_Request( self::PROJECTS_ENDPOINT );
$request->set_method( 'PUT' );
$request->set_params(
[
'project' => [ 'refresh_language_pairs' => 1 ],
'refresh_language_pairs' => 1,
'project_id' => $this->project->get_id(),
'accesskey' => $this->project->get_access_key(),
]
);
$this->client->send_request( $request );
}
/**
* @param stdClass $service
* @param SiteDetails $site_details
*
* @return stdClass
* @throws WPML_TP_API_Exception
*/
public function create_project( \stdClass $service, SiteDetails $site_details ) {
$project_data = array_merge(
$site_details->getBlogInfo(),
[
'delivery_method' => $site_details->getDeliveryMethod(),
'sitekey' => WP_Installer_API::get_site_key( 'wpml' ),
'client_external_id' => WP_Installer_API::get_ts_client_id(),
]
);
$params = [
'api_version' => self::API_VERSION,
'service' => [ 'id' => $service->id ],
'project' => $project_data,
'custom_fields' => $service->custom_fields_data,
'client' => $site_details->getClientData(),
'linked_by' => \TranslationProxy::get_service_linked_by_suid( $service->suid ),
];
$request = new WPML_TP_API_Request( self::PROJECTS_ENDPOINT );
$request->set_method( 'POST' );
$request->set_params( $params );
return $this->client->send_request( $request );
}
/**
* @param Project $project
* @param \stdClass $credentials
*
* @throws WPML_TP_API_Exception
*/
public function update_project_credentials( Project $project, \stdClass $credentials ) {
$request = new WPML_TP_API_Request( self::PROJECTS_ENDPOINT );
$request->set_method( 'PUT' );
$request->set_params(
[
'api_version' => self::API_VERSION,
'accesskey' => $project->accessKey,
'project' => [
'custom_fields' => (array) $credentials,
],
]
);
$this->client->send_request( $request );
}
/**
* @param Project $project
*
* @return array|mixed|stdClass|string
* @throws WPML_TP_API_Exception
*/
public function get_extra_fields( Project $project ) {
$params = [
'accesskey' => $project->accessKey,
'api_version' => self::API_VERSION,
'project_id' => $project->id,
];
$request = new WPML_TP_API_Request( '/projects/{project_id}/extra_fields.json' );
$request->set_method( 'GET' );
$request->set_params( $params );
return $this->client->send_request( $request );
}
}

View File

@@ -0,0 +1,21 @@
<?php
class WPML_TP_Services {
public function get_current_project() {
return TranslationProxy::get_current_project();
}
public function get_current_service() {
return TranslationProxy::get_current_service();
}
/**
* @param $service_id
* @param bool $custom_fields
*
* @throws WPMLTranslationProxyApiException
*/
public function select_service( $service_id, $custom_fields = false ) {
TranslationProxy::select_service( $service_id, $custom_fields );
}
}

View File

@@ -0,0 +1,50 @@
<?php
class WPML_TP_XLIFF_API extends WPML_TP_API {
/** @var WPML_TP_Xliff_Parser */
private $xliff_parser;
public function __construct(
WPML_TP_API_Client $client,
WPML_TP_Project $project,
WPML_TP_API_Log_Interface $logger,
WPML_TP_Xliff_Parser $xliff_parser
) {
parent::__construct( $client, $project, $logger );
$this->xliff_parser = $xliff_parser;
}
/**
* @param int $tp_job_id
* @param bool $parse
*
* @return WPML_TP_Translation_Collection|string
* @throws WPML_TP_API_Exception
*/
public function get_remote_translations( $tp_job_id, $parse = true ) {
$request = new WPML_TP_API_Request( '/jobs/{job_id}/xliff.json' );
$request->set_params(
array(
'job_id' => $tp_job_id,
'accesskey' => $this->project->get_access_key(),
)
);
$result = $this->client->send_request( $request );
if ( ! $result || empty( $result ) || false === strpos( $result, 'xliff' ) ) {
throw new WPML_TP_API_Exception( 'XLIFF file could not be fetched for tp_job: ' . $tp_job_id, $request );
}
$result = apply_filters( 'wpml_tm_data_from_pro_translation', $result );
if ( ! $parse ) {
return $result;
}
$xliff = @simplexml_load_string( $result );
if ( ! $xliff ) {
throw new WPML_TP_API_Exception( 'XLIFF file could not be parsed.' );
}
return $this->xliff_parser->parse( $xliff );
}
}

View File

@@ -0,0 +1,55 @@
<?php
class WPML_TP_Xliff_Parser {
/**
* @param SimpleXMLElement $xliff
*
* @return WPML_TP_Translation_Collection
*/
public function parse( SimpleXMLElement $xliff ) {
$source_lang = (string) $xliff->file->attributes()->{'source-language'};
$target_lang = (string) $xliff->file->attributes()->{'target-language'};
$translations = array();
foreach ( $xliff->file->body->children() as $node ) {
$translations[] = new WPML_TP_Translation(
(string) $node->attributes()->id,
$this->get_cdata_value( $node, 'source' ),
$this->get_cdata_value( $node, 'target' )
);
}
return new WPML_TP_Translation_Collection(
$translations,
$source_lang,
$target_lang
);
}
/**
* @param SimpleXMLElement $xliff_node
* @param string $field
*
* @return string
*/
protected function get_cdata_value( SimpleXMLElement $xliff_node, $field ) {
$value = '';
if ( isset( $xliff_node->$field->mrk ) ) {
$value = (string) $xliff_node->$field->mrk;
} elseif ( isset( $xliff_node->$field ) ) {
$value = (string) $xliff_node->$field;
}
return self::restore_new_line( $value );
}
/**
* @param string $string
*
* @return string
*/
public static function restore_new_line( $string ) {
return preg_replace( '/<br class="xliff-newline"\s*\/>/i', "\n", $string );
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPMLTranslationProxyApiException extends Exception {
public function __construct( $message, $code = 0 ) {
WPML_TranslationProxy_Com_Log::log_error( $message );
parent::__construct( $message, $code );
}
}