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,49 @@
<?php
namespace WPML\TM\ATE\ClonedSites;
class ApiCommunication {
const SITE_CLONED_ERROR = 426;
/**
* @var Lock
*/
private $lock;
/**
* @param Lock $lock
*/
public function __construct( Lock $lock ) {
$this->lock = $lock;
}
public function handleClonedSiteError( $response ) {
if ( self::SITE_CLONED_ERROR === $response['response']['code'] ) {
$parsedResponse = json_decode( $response['body'], true );
if ( isset( $parsedResponse['errors'] ) ) {
$this->handleClonedDetection( $parsedResponse['errors'] );
}
return new \WP_Error( self::SITE_CLONED_ERROR, 'Site Moved or Copied - Action Required' );
}
return $response;
}
public function checkCloneSiteLock() {
if ( Lock::isLocked() ) {
return new \WP_Error( self::SITE_CLONED_ERROR, 'Site Moved or Copied - Action Required - ATE communication locked.' );
}
return null;
}
public function unlockClonedSite() {
return $this->lock->unlock();
}
private function handleClonedDetection( $error_data ) {
$error = array_pop( $error_data );
$this->lock->lock( $error );
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WPML\TM\ATE\ClonedSites;
class FingerprintGenerator {
const SITE_FINGERPRINT_HEADER = 'SITE-FINGERPRINT';
const NEW_SITE_FINGERPRINT_HEADER = 'NEW-SITE-FINGERPRINT';
public function getSiteFingerprint() {
$siteFingerprint = [
'wp_url' => $this->getSiteUrl(),
];
return json_encode( $siteFingerprint );
}
private function getSiteUrl() {
$siteUrl = defined( 'ATE_CLONED_SITE_URL' ) ? ATE_CLONED_SITE_URL : site_url();
return $this->getDefaultSiteUrl( $siteUrl );
}
private function getDefaultSiteUrl( $siteUrl ) {
global $sitepress;
$filteredSiteUrl = false;
if ( WPML_LANGUAGE_NEGOTIATION_TYPE_DOMAIN === (int) $sitepress->get_setting( 'language_negotiation_type' ) ) {
/* @var WPML_URL_Converter $wpml_url_converter */
global $wpml_url_converter;
$site_url_default_lang = $wpml_url_converter->get_default_site_url();
$filteredSiteUrl = filter_var( $site_url_default_lang, FILTER_SANITIZE_URL );
}
$defaultSiteUrl = $filteredSiteUrl ? $filteredSiteUrl : $siteUrl;
$defaultSiteUrl = defined( 'ATE_CLONED_DEFAULT_SITE_URL' ) ? ATE_CLONED_DEFAULT_SITE_URL : $defaultSiteUrl;
return $defaultSiteUrl;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WPML\TM\ATE\ClonedSites;
class Lock {
const CLONED_SITE_OPTION = 'otgs_wpml_tm_ate_cloned_site_lock';
public function lock( $lockData ) {
if ( $this->isLockDataPresent( $lockData ) ) {
update_option(
self::CLONED_SITE_OPTION,
[
'stored_fingerprint' => $lockData['stored_fingerprint'],
'received_fingerprint' => $lockData['received_fingerprint'],
'fingerprint_confirmed' => $lockData['fingerprint_confirmed'],
],
'no'
);
}
}
private function isLockDataPresent( $lockData ) {
if ( isset( $lockData['stored_fingerprint'] )
&& isset( $lockData['received_fingerprint'] )
&& isset( $lockData['fingerprint_confirmed'] ) ) {
return true;
}
return false;
}
public function unlock() {
delete_option( self::CLONED_SITE_OPTION );
}
public static function isLocked() {
return (bool) get_option( self::CLONED_SITE_OPTION, false ) && \WPML_TM_ATE_Status::is_enabled();
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace WPML\TM\ATE\ClonedSites;
use WPML\FP\Fns;
class Report {
const REPORT_TYPE_COPY = 'copy';
const REPORT_TYPE_MOVE = 'move';
/**
* @var \WPML_TM_AMS_API
*/
private $apiClient;
/**
* @var ApiCommunication
*/
private $apiCommunicationHandler;
/**
* @var \WPML_TM_ATE_Job_Repository
*/
private $ateJobsRepository;
/**
* Update jobs synchronisation
*
* @var \WPML_TP_Sync_Update_Job
*/
private $updateJobs;
/**
* @var \WPML_Translation_Job_Factory
*/
private $translationJobFactory;
/**
* @param \WPML_TM_AMS_API $apiClient
* @param ApiCommunication $apiCommunicationHandler
* @param \WPML_TM_ATE_Job_Repository $ateJobsRepository
* @param \WPML_Translation_Job_Factory $translationJobFactory
*/
public function __construct(
\WPML_TM_AMS_API $apiClient,
ApiCommunication $apiCommunicationHandler,
\WPML_TM_ATE_Job_Repository $ateJobsRepository,
\WPML_TP_Sync_Update_Job $updateJobs,
\WPML_Translation_Job_Factory $translationJobFactory
) {
$this->apiClient = $apiClient;
$this->apiCommunicationHandler = $apiCommunicationHandler;
$this->ateJobsRepository = $ateJobsRepository;
$this->updateJobs = $updateJobs;
$this->translationJobFactory = $translationJobFactory;
}
/**
* @param string $reportType
*
* @return bool
*/
public function report( $reportType ) {
$reportCallback = \wpml_collect( [
self::REPORT_TYPE_COPY => $this->reportCopiedSite(),
self::REPORT_TYPE_MOVE => $this->reportMovedSite(),
] )->get( $reportType, Fns::always( Fns::always( false ) ) );
$reportResult = $reportCallback();
if ($reportResult) {
do_action( 'wpml_tm_ate_synchronize_translators' );
}
return $reportResult;
}
private function reportCopiedSite() {
return function () {
$reportResult = $this->apiClient->reportCopiedSite();
$isConfirmed = $this->apiClient->processCopyReportConfirmation( $reportResult );
if ( $isConfirmed ) {
$jobsInProgress = $this->ateJobsRepository->get_jobs_to_sync();
/** @var \WPML_TM_Post_Job_Entity $jobInProgress */
foreach ( $jobsInProgress as $jobInProgress ) {
$jobInProgress->set_status( ICL_TM_NOT_TRANSLATED );
$this->updateJobs->update_state( $jobInProgress );
$this->translationJobFactory->delete_job_data( $jobInProgress->get_translate_job_id() );
}
$this->apiCommunicationHandler->unlockClonedSite();
}
return $isConfirmed;
};
}
private function reportMovedSite() {
return function () {
$reportResult = $this->apiClient->reportMovedSite();
$movedSuccessfully = $this->apiClient->processMoveReport( $reportResult );
if ( $movedSuccessfully ) {
$this->apiCommunicationHandler->unlockClonedSite();
}
return $movedSuccessfully;
};
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace WPML\TM\ATE\ClonedSites;
class ReportAjax implements \IWPML_Backend_Action, \IWPML_DIC_Action {
/**
* @var Report
*/
private $reportHandler;
/**
* @param Report $reportHandler
*/
public function __construct( Report $reportHandler ) {
$this->reportHandler = $reportHandler;
}
public function add_hooks() {
add_action( 'wp_ajax_wpml_save_cloned_sites_report_type', [ $this, 'reportSiteCloned' ] );
}
public function reportSiteCloned() {
if ( $this->isValidRequest() && $this->reportHandler->report( $_POST['reportType'] ) ) {
wp_send_json_success();
} else {
wp_send_json_error();
}
}
private function isValidRequest() {
return array_key_exists( 'nonce', $_POST )
&& array_key_exists( 'reportType', $_POST )
&& wp_verify_nonce( $_POST['nonce'], 'icl_doc_translation_method_cloned_nonce' );
}
}

View File

@@ -0,0 +1,640 @@
<?php
use WPML\TM\ATE\ClonedSites\FingerprintGenerator;
use WPML\TM\ATE\Log\Entry;
use WPML\TM\ATE\Log\ErrorEvents;
use WPML\TM\ATE\ClonedSites\ApiCommunication as ClonedSitesHandler;
/**
* @author OnTheGo Systems
*/
class WPML_TM_AMS_API {
const HTTP_ERROR_CODE_400 = 400;
private $auth;
private $endpoints;
private $wp_http;
/**
* @var ClonedSitesHandler
*/
private $clonedSitesHandler;
/**
* @var FingerprintGenerator
*/
private $fingerprintGenerator;
/**
* WPML_TM_ATE_API constructor.
*
* @param WP_Http $wp_http
* @param WPML_TM_ATE_Authentication $auth
* @param WPML_TM_ATE_AMS_Endpoints $endpoints
* @param ClonedSitesHandler $clonedSitesHandler
* @param FingerprintGenerator $fingerprintGenerator
*/
public function __construct(
WP_Http $wp_http,
WPML_TM_ATE_Authentication $auth,
WPML_TM_ATE_AMS_Endpoints $endpoints,
ClonedSitesHandler $clonedSitesHandler,
FingerprintGenerator $fingerprintGenerator
) {
$this->wp_http = $wp_http;
$this->auth = $auth;
$this->endpoints = $endpoints;
$this->clonedSitesHandler = $clonedSitesHandler;
$this->fingerprintGenerator = $fingerprintGenerator;
}
/**
* @param string $translator_email
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function enable_subscription( $translator_email ) {
$result = null;
$verb = 'PUT';
$url = $this->endpoints->get_enable_subscription();
$url = str_replace( '{translator_email}', base64_encode( $translator_email ), $url );
$response = $this->signed_request( $verb, $url );
if ( $this->response_has_body( $response ) ) {
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) ) {
$result = json_decode( $response['body'], true );
}
}
return $result;
}
/**
* @param string $translator_email
*
* @return bool|WP_Error
*/
public function is_subscription_activated( $translator_email ) {
$result = null;
$url = $this->endpoints->get_subscription_status();
$url = str_replace( '{translator_email}', base64_encode( $translator_email ), $url );
$url = str_replace( '{WEBSITE_UUID}', $this->auth->get_site_id(), $url );
$response = $this->signed_request( 'GET', $url );
if ( $this->response_has_body( $response ) ) {
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) ) {
$result = json_decode( $response['body'], true );
$result = $result['subscription'];
}
}
return $result;
}
/**
* @return array|mixed|null|object|WP_Error
*
* @throws \InvalidArgumentException Exception.
*/
public function get_status() {
$result = null;
$registration_data = $this->get_registration_data();
$shared = array_key_exists( 'shared', $registration_data ) ? $registration_data['shared'] : null;
if ( $shared ) {
$url = $this->endpoints->get_ams_status();
$url = str_replace( '{SHARED_KEY}', $shared, $url );
$response = $this->request( 'GET', $url );
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) ) {
$registration_data = $this->get_registration_data();
if ( isset( $response_body['activated'] ) && (bool) $response_body['activated'] ) {
$registration_data['status'] = WPML_TM_ATE_Authentication::AMS_STATUS_ACTIVE;
$this->set_registration_data( $registration_data );
}
$result = $response_body;
}
}
}
return $result;
}
/**
* Used to register a manager and, at the same time, create a website in AMS.
* This is called only when registering the site with AMS.
* To register new managers or translators `\WPML_TM_ATE_AMS_Endpoints::get_ams_synchronize_managers`
* and `\WPML_TM_ATE_AMS_Endpoints::get_ams_synchronize_translators` will be used.
*
* @param WP_User $manager The WP_User instance of the manager.
* @param WP_User[] $translators An array of WP_User instances representing the current translators.
* @param WP_User[] $managers An array of WP_User instances representing the current managers.
*
* @return array|bool|null|WP_Error
*/
public function register_manager( WP_User $manager, array $translators, array $managers ) {
static $recreate_site_id = false;
$manager_data = $this->get_user_data( $manager, true );
$translators_data = $this->get_users_data( $translators );
$managers_data = $this->get_users_data( $managers, true );
$result = null;
if ( $manager_data ) {
$url = $this->endpoints->get_ams_register_client();
$params = $manager_data;
$params['website_url'] = get_site_url();
$params['website_uuid'] = wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE, $recreate_site_id );
$params['translators'] = $translators_data;
$params['translation_managers'] = $managers_data;
$response = $this->request( 'POST', $url, $params );
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) && $this->response_has_keys( $response ) ) {
$registration_data = $this->get_registration_data();
$registration_data['user_id'] = $manager->ID;
$registration_data['secret'] = $response_body['secret_key'];
$registration_data['shared'] = $response_body['shared_key'];
$registration_data['status'] = WPML_TM_ATE_Authentication::AMS_STATUS_ENABLED;
$result = $this->set_registration_data( $registration_data );
}
if ( is_wp_error( $result ) && $result->get_error_code() === 409 && ! $recreate_site_id ) {
$recreate_site_id = true;
return $this->register_manager( $manager, $translators, $managers );
}
}
}
return $result;
}
/**
* Gets the data required by AMS to register a user.
*
* @param WP_User $wp_user The user from which data should be extracted.
* @param bool $with_name_details True if name details should be included.
*
* @return array
*/
private function get_user_data( WP_User $wp_user, $with_name_details = false ) {
$data = array();
$data['email'] = $wp_user->user_email;
if ( $with_name_details ) {
$data['display_name'] = $wp_user->display_name;
$data['first_name'] = $wp_user->first_name;
$data['last_name'] = $wp_user->last_name;
} else {
$data['name'] = $wp_user->display_name;
}
return $data;
}
private function prepareClonedSiteArguments( $method ) {
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
FingerprintGenerator::NEW_SITE_FINGERPRINT_HEADER => $this->fingerprintGenerator->getSiteFingerprint(),
];
return [
'method' => $method,
'headers' => $headers,
];
}
/**
* @return array|WP_Error
*/
public function reportCopiedSite() {
return $this->processReport(
$this->endpoints->get_ams_site_copy(),
'POST'
);
}
/**
* @return array|WP_Error
*/
public function reportMovedSite() {
return $this->processReport(
$this->endpoints->get_ams_site_move(),
'PUT'
);
}
/**
* @param array $response Response from reportMovedSite()
*
* @return bool|WP_Error
*/
public function processMoveReport( $response ) {
if ( ! $this->response_has_body( $response ) ) {
return new WP_Error( 'auth_error', 'Unable to report site moved.' );
}
$response_body = json_decode( $response['body'], true );
if ( isset( $response_body['moved_successfully'] ) && (bool) $response_body['moved_successfully'] ) {
return true;
}
return new WP_Error( 'auth_error', 'Unable to report site moved.' );
}
/**
* @param array $response_body body from reportMovedSite() response.
*
* @return bool
*/
private function storeAuthData( $response_body ) {
$setRegistrationDataResult = $this->updateRegistrationData( $response_body );
$setUuidResult = $this->updateSiteUuId( $response_body );
return $setRegistrationDataResult && $setUuidResult;
}
/**
* @param array $response_body body from reportMovedSite() response.
*
* @return bool
*/
private function updateRegistrationData( $response_body ) {
$registration_data = $this->get_registration_data();
$registration_data['secret'] = $response_body['new_secret_key'];
$registration_data['shared'] = $response_body['new_shared_key'];
return $this->set_registration_data( $registration_data );
}
/**
* @param array $response_body body from reportMovedSite() response.
*
* @return bool
*/
private function updateSiteUuId( $response_body ) {
$this->override_site_id( $response_body['new_website_uuid'] );
return update_option(
WPML_Site_ID::SITE_ID_KEY . ':ate',
$response_body['new_website_uuid'],
false
);
}
private function sendSiteReportConfirmation() {
$url = $this->endpoints->get_ams_site_confirm();
$method = 'POST';
$args = $this->prepareClonedSiteArguments( $method );
$url_parts = wp_parse_url( $url );
$registration_data = $this->get_registration_data();
$query['new_shared_key'] = $registration_data['shared'];
$query['token'] = uuid_v5( wp_generate_uuid4(), $url );
$query['new_website_uuid'] = $this->auth->get_site_id();
$url_parts['query'] = http_build_query( $query );
$url = http_build_url( $url_parts );
$signed_url = $this->auth->signUrl( $method, $url );
$response = $this->wp_http->request( $signed_url, $args );
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
return (bool) $response_body['confirmed'];
}
return new WP_Error( 'auth_error', 'Unable confirm site copied.' );
}
/**
* @param string $url
* @param string $method
*
* @return array|WP_Error
*/
private function processReport( $url, $method ) {
$args = $this->prepareClonedSiteArguments( $method );
$url_parts = wp_parse_url( $url );
$registration_data = $this->get_registration_data();
$query['shared_key'] = $registration_data['shared'];
$query['token'] = uuid_v5( wp_generate_uuid4(), $url );
$query['website_uuid'] = $this->auth->get_site_id();
$url_parts['query'] = http_build_query( $query );
$url = http_build_url( $url_parts );
$signed_url = $this->auth->signUrl( $method, $url );
return $this->wp_http->request( $signed_url, $args );
}
/**
* @param array $response Response from reportCopiedSite()
*
* @return bool
*/
public function processCopyReportConfirmation( $response ) {
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
return $this->storeAuthData( $response_body ) && (bool) $this->sendSiteReportConfirmation();
}
return false;
}
/**
* Converts an array of WP_User instances into an array of data nedded by AMS to identify users.
*
* @param WP_User[] $users An array of WP_User instances.
* @param bool $with_name_details True if name details should be included.
*
* @return array
*/
private function get_users_data( array $users, $with_name_details = false ) {
$user_data = array();
foreach ( $users as $user ) {
$wp_user = get_user_by( 'id', $user->ID );
$user_data[] = $this->get_user_data( $wp_user, $with_name_details );
}
return $user_data;
}
/**
* Checks if a reponse has a body.
*
* @param array|\WP_Error $response The response of the remote request.
*
* @return bool
*/
private function response_has_body( $response ) {
return ! is_wp_error( $response ) && array_key_exists( 'body', $response );
}
private function get_errors( array $response ) {
$response_errors = null;
if ( is_wp_error( $response ) ) {
$response_errors = $response;
} elseif ( array_key_exists( 'body', $response ) && $response['response']['code'] >= self::HTTP_ERROR_CODE_400 ) {
$main_error = array();
$errors = array();
$error_message = $response['response']['message'];
$response_body = json_decode( $response['body'], true );
if ( ! $response_body ) {
$error_message = $response['body'];
$main_error = array( $response['body'] );
} elseif ( array_key_exists( 'errors', $response_body ) ) {
$errors = $response_body['errors'];
$main_error = array_shift( $errors );
$error_message = $this->get_error_message( $main_error, $response['body'] );
}
$response_errors = new WP_Error( $main_error['status'], $error_message, $main_error );
foreach ( $errors as $error ) {
$error_message = $this->get_error_message( $error, $response['body'] );
$error_status = isset( $error['status'] ) ? 'ams_error: ' . $error['status'] : '';
$response_errors->add( $error_status, $error_message, $error );
}
}
if ( $response_errors ) {
$entry = new Entry();
$entry->event = ErrorEvents::SERVER_AMS;
$entry->description = $response_errors->get_error_message();
$entry->extraData = [
'errorData' => $response_errors->get_error_data(),
];
wpml_tm_ate_ams_log( $entry );
}
return $response_errors;
}
/**
* @param array $ams_error
* @param string $default
*
* @return string
*/
private function get_error_message( $ams_error, $default ) {
$title = isset( $ams_error['title'] ) ? $ams_error['title'] . ': ' : '';
$details = isset( $ams_error['detail'] ) ? $ams_error['detail'] : $default;
return $title . $details;
}
private function response_has_keys( $response ) {
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
return array_key_exists( 'secret_key', $response_body )
&& array_key_exists( 'shared_key', $response_body );
}
return false;
}
/**
* @return array
*/
public function get_registration_data() {
return get_option( WPML_TM_ATE_Authentication::AMS_DATA_KEY, [] );
}
/**
* @param $registration_data
*
* @return bool
*/
private function set_registration_data( $registration_data ) {
return update_option( WPML_TM_ATE_Authentication::AMS_DATA_KEY, $registration_data );
}
/**
* @param array $managers
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function synchronize_managers( array $managers ) {
$result = null;
$managers_data = $this->get_users_data( $managers, true );
if ( $managers_data ) {
$url = $this->endpoints->get_ams_synchronize_managers();
$url = str_replace( '{WEBSITE_UUID}', wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE ), $url );
$params = array( 'translation_managers' => $managers_data );
$response = $this->signed_request( 'PUT', $url, $params );
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) ) {
$result = $response_body;
}
}
}
return $result;
}
/**
* @param array $translators
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function synchronize_translators( array $translators ) {
$result = null;
$translators_data = $this->get_users_data( $translators );
if ( $translators_data ) {
$url = $this->endpoints->get_ams_synchronize_translators();
$params = array( 'translators' => $translators_data );
$response = $this->signed_request( 'PUT', $url, $params );
if ( $this->response_has_body( $response ) ) {
$response_body = json_decode( $response['body'], true );
$result = $this->get_errors( $response );
if ( ! is_wp_error( $result ) ) {
$result = $response_body;
}
}
}
return $result;
}
/**
* @param string $method
* @param string $url
* @param array|null $params
*
* @return array|WP_Error
*/
private function request( $method, $url, array $params = null ) {
$lock = $this->clonedSitesHandler->checkCloneSiteLock();
if ( $lock ) {
return $lock;
}
$method = strtoupper( $method );
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
FingerprintGenerator::SITE_FINGERPRINT_HEADER => $this->fingerprintGenerator->getSiteFingerprint(),
];
$args = [
'method' => $method,
'headers' => $headers,
];
if ( $params ) {
$args['body'] = wp_json_encode( $params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
}
$response = $this->wp_http->request( $this->add_versions_to_url( $url ), $args );
if ( ! is_wp_error( $response ) ) {
$response = $this->clonedSitesHandler->handleClonedSiteError( $response );
}
return $response;
}
/**
* @param string $verb
* @param string $url
* @param array|null $params
*
* @return array|WP_Error
*/
private function signed_request( $verb, $url, array $params = null ) {
$verb = strtoupper( $verb );
$signed_url = $this->auth->get_signed_url_with_parameters( $verb, $url, $params );
return $this->request( $verb, $signed_url, $params );
}
/**
* @param $url
*
* @return string
*/
private function add_versions_to_url( $url ) {
$url_parts = wp_parse_url( $url );
$query = array();
if ( array_key_exists( 'query', $url_parts ) ) {
parse_str( $url_parts['query'], $query );
}
$query['wpml_core_version'] = ICL_SITEPRESS_VERSION;
$query['wpml_tm_version'] = WPML_TM_VERSION;
$url_parts['query'] = http_build_query( $query );
$url = http_build_url( $url_parts );
return $url;
}
public function override_site_id( $site_id ) {
$this->auth->override_site_id( $site_id );
}
}

View File

@@ -0,0 +1,430 @@
<?php
use WPML\TM\ATE\ClonedSites\FingerprintGenerator;
use WPML\TM\ATE\Log\Entry;
use WPML\TM\ATE\Log\ErrorEvents;
use WPML\TM\ATE\ClonedSites\ApiCommunication as ClonedSitesHandler;
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_API {
private $wp_http;
private $auth;
private $endpoints;
/**
* @var ClonedSitesHandler
*/
private $clonedSitesHandler;
/**
* @var FingerprintGenerator
*/
private $fingerprintGenerator;
/**
* WPML_TM_ATE_API constructor.
*
* @param WP_Http $wp_http
* @param WPML_TM_ATE_Authentication $auth
* @param WPML_TM_ATE_AMS_Endpoints $endpoints
*/
public function __construct(
WP_Http $wp_http,
WPML_TM_ATE_Authentication $auth,
WPML_TM_ATE_AMS_Endpoints $endpoints,
ClonedSitesHandler $clonedSitesHandler,
FingerprintGenerator $fingerprintGenerator
) {
$this->wp_http = $wp_http;
$this->auth = $auth;
$this->endpoints = $endpoints;
$this->clonedSitesHandler = $clonedSitesHandler;
$this->fingerprintGenerator = $fingerprintGenerator;
}
/**
* @param array $params
*
* @see https://bitbucket.org/emartini_crossover/ate/wiki/API/V1/jobs/create
*
* @return mixed
* @throws \InvalidArgumentException
*/
public function create_jobs( array $params ) {
return $this->requestWithLog(
$this->endpoints->get_ate_jobs(),
[
'method' => 'POST',
'body' => $params,
]
);
}
/**
* @param int|string|array $ate_job_id
*
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function confirm_received_job( $ate_job_id ) {
return $this->requestWithLog( $this->endpoints->get_ate_confirm_job( $ate_job_id ) );
}
/**
* @param int $job_id
* @param string $return_url
*
* @return string|WP_Error
* @throws \InvalidArgumentException
*/
public function get_editor_url( $job_id, $return_url ) {
$lock = $this->clonedSitesHandler->checkCloneSiteLock();
if ( $lock ) {
return new WP_Error( 'communication_error', 'ATE communication is locked, please update configuration' );
}
$url = $this->endpoints->get_ate_editor();
$url = str_replace(
[
'{job_id}',
'{translator_email}',
'{return_url}',
],
[
$job_id,
urlencode( filter_var( wp_get_current_user()->user_email, FILTER_SANITIZE_URL ) ),
urlencode( filter_var( $return_url, FILTER_SANITIZE_URL ) ),
],
$url
);
return $this->auth->get_signed_url_with_parameters( 'GET', $url, null );
}
/**
* @param int $ate_job_id
* @param WPML_Element_Translation_Job $job_object
*
* @return array
*/
public function clone_job( $ate_job_id, WPML_Element_Translation_Job $job_object ) {
$url = $this->endpoints->get_clone_job( $ate_job_id );
$result = $this->requestWithLog(
$url,
[
'method' => 'POST',
'body' => [
'id' => $ate_job_id,
'notify_url' =>
WPML_TM_REST_ATE_Public::get_receive_ate_job_url( $job_object->get_id() ),
'site_identifier' => wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE ),
'source_id' => wpml_tm_get_records()
->icl_translate_job_by_job_id( $job_object->get_id() )
->rid(),
'permalink' => $job_object->get_url( true ),
'ate_ams_console_url' => wpml_tm_get_ams_ate_console_url(),
],
]
);
if ( $result && ! is_wp_error( $result ) ) {
return [
'id' => $result->job_id,
'ate_status' =>
isset( $result->status ) ? $result->status : WPML_TM_ATE_AMS_Endpoints::ATE_JOB_STATUS_CREATED,
];
} else {
return false;
}
}
/**
* @param int $ate_job_id
*
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function get_job( $ate_job_id ) {
if ( ! $ate_job_id ) {
return null;
}
return $this->requestWithLog( $this->endpoints->get_ate_jobs( $ate_job_id ) );
}
/**
* If `$job_ids` is not an empty array,
* the `$statuses` parameter will be ignored in ATE's endpoint.
*
* @see https://bitbucket.org/emartini_crossover/ate/wiki/API/V1/jobs/status
*
* @param null|array $job_ids
* @param null|array $statuses
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function get_jobs( $job_ids, $statuses = null ) {
return $this->requestWithLog( $this->endpoints->get_ate_jobs( $job_ids, $statuses ) );
}
/**
* @param $wpml_job_ids
*
* @return array|mixed|object|WP_Error|null
*/
public function get_jobs_by_wpml_ids( $wpml_job_ids ) {
return $this->requestWithLog( $this->endpoints->get_ate_jobs_by_wpml_job_ids( $wpml_job_ids ) );
}
/**
* @param array $pairs
* @see https://bitbucket.org/emartini_crossover/ate/wiki/API/V1/migration/migrate
* @return bool
*/
public function migrate_source_id( array $pairs ) {
$lock = $this->clonedSitesHandler->checkCloneSiteLock();
if ( $lock ) {
return false;
}
$verb = 'POST';
$url = $this->auth->get_signed_url_with_parameters( $verb, $this->endpoints->get_source_id_migration(), $pairs );
if ( is_wp_error( $url ) ) {
return $url;
}
$result = $this->wp_http->request(
$url,
array(
'timeout' => 60,
'method' => $verb,
'headers' => $this->json_headers(),
'body' => wp_json_encode( $pairs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
)
);
if ( ! is_wp_error( $result ) ) {
$result = $this->clonedSitesHandler->handleClonedSiteError( $result );
}
return $this->get_response_errors( $result ) === null;
}
private function get_response( $result ) {
$errors = $this->get_response_errors( $result );
if ( is_wp_error( $errors ) ) {
return $errors;
}
return $this->get_response_body( $result );
}
private function get_response_body( $result ) {
if ( is_array( $result ) && array_key_exists( 'body', $result ) && ! is_wp_error( $result ) ) {
$body = json_decode( $result['body'] );
if ( isset( $body->authenticated ) && ! (bool) $body->authenticated ) {
return new WP_Error( 'ate_auth_failed', $body->message );
}
return $body;
}
return $result;
}
private function get_response_errors( $response ) {
$response_errors = null;
if ( is_wp_error( $response ) ) {
$response_errors = $response;
} elseif ( array_key_exists( 'body', $response ) && $response['response']['code'] >= 400 ) {
$errors = array();
$response_body = json_decode( $response['body'], true );
if ( is_array( $response_body ) && array_key_exists( 'errors', $response_body ) ) {
$errors = $response_body['errors'];
}
$response_errors = new WP_Error( $response['response']['code'], $response['response']['message'], $errors );
}
return $response_errors;
}
/**
* @return array
*/
private function json_headers() {
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
FingerprintGenerator::SITE_FINGERPRINT_HEADER => $this->fingerprintGenerator->getSiteFingerprint(),
];
}
/**
* @param array $args
*
* @return string
*/
private function encode_body_args( array $args ) {
return wp_json_encode( $args, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
}
/**
* @param string $xliff_url
*
* @return string
* @throws Requests_Exception
*/
public function get_remote_xliff_content( $xliff_url ) {
/** @var \WP_Error|array $response */
$response = wp_remote_get( $xliff_url );
if ( is_wp_error( $response ) ) {
throw new Requests_Exception( $response->get_error_message(), $response->get_error_code() );
} elseif ( isset( $response['response']['code'] ) && 200 !== (int) $response['response']['code'] ) {
throw new Requests_Exception( $response['response']['message'], $response['response']['code'] );
} elseif ( ! isset( $response['body'] ) || ! trim( $response['body'] ) ) {
throw new Requests_Exception( 'Missing body', 0 );
}
return $response['body'];
}
public function override_site_id( $site_id ) {
$this->auth->override_site_id( $site_id );
}
public function get_website_id( $site_url ) {
$lock = $this->clonedSitesHandler->checkCloneSiteLock();
if ( $lock ) {
return null;
}
$signed_url = $this->auth->get_signed_url_with_parameters( 'GET', $this->endpoints->get_websites() );
if ( is_wp_error( $signed_url ) ) {
return null;
}
$requestArguments = [ 'headers' => $this->json_headers() ];
$response = $this->wp_http->request( $signed_url, $requestArguments );
if ( ! is_wp_error( $response ) ) {
$response = $this->clonedSitesHandler->handleClonedSiteError( $response );
}
$sites = $this->get_response( $response );
foreach ( $sites as $site ) {
if ( $site->url === $site_url ) {
return $site->uuid;
}
}
return null;
}
/**
* @see https://bitbucket.org/emartini_crossover/ate/wiki/API/V1/sync/all
*
* @param array $ateJobIds
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function sync_all( array $ateJobIds ) {
return $this->requestWithLog(
$this->endpoints->get_sync_all(),
[
'method' => 'POST',
'body' => [ 'ids' => $ateJobIds ],
]
);
}
/**
* @see https://bitbucket.org/emartini_crossover/ate/wiki/API/V1/sync/page
*
* @param string $token
* @param int $page
*
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function sync_page( $token, $page ) {
return $this->requestWithLog( $this->endpoints->get_sync_page( $token, $page ) );
}
/**
* @param string $url
* @param array $requestArgs
*
* @return array|mixed|object|string|WP_Error|null
*/
private function request( $url, array $requestArgs = [] ) {
$lock = $this->clonedSitesHandler->checkCloneSiteLock();
if ( $lock ) {
return $lock;
}
$requestArgs = array_merge(
[
'timeout' => 60,
'method' => 'GET',
'headers' => $this->json_headers(),
],
$requestArgs
);
$bodyArgs = isset( $requestArgs['body'] ) && is_array( $requestArgs['body'] )
? $requestArgs['body'] : null;
$signedUrl = $this->auth->get_signed_url_with_parameters( $requestArgs['method'], $url, $bodyArgs );
if ( is_wp_error( $signedUrl ) ) {
return $signedUrl;
}
if ( $bodyArgs ) {
$requestArgs['body'] = $this->encode_body_args( $bodyArgs );
}
$result = $this->wp_http->request( $signedUrl, $requestArgs );
if ( ! is_wp_error( $result ) ) {
$result = $this->clonedSitesHandler->handleClonedSiteError( $result );
}
return $this->get_response( $result );
}
/**
* @param string $url
* @param array $requestArgs
*
* @return array|mixed|object|string|WP_Error|null
*/
private function requestWithLog( $url, array $requestArgs = [] ) {
$response = $this->request( $url, $requestArgs );
if ( is_wp_error( $response ) ) {
$entry = new Entry();
$entry->event = ErrorEvents::SERVER_ATE;
$entry->description = $response->get_error_message();
$entry->extraData = [
'url' => $url,
'requestArgs' => $requestArgs,
];
wpml_tm_ate_ams_log( $entry );
}
return $response;
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Authentication {
const AMS_DATA_KEY = 'WPML_TM_AMS';
const AMS_STATUS_NON_ACTIVE = 'non-active';
const AMS_STATUS_ENABLED = 'enabled';
const AMS_STATUS_ACTIVE = 'active';
/** @var string|null $site_id */
private $site_id = null;
public function get_signed_url_with_parameters( $verb, $url, $params = null ) {
if ( $this->has_keys() ) {
$url = $this->add_required_arguments_to_url( $verb, $url, $params );
return $this->signUrl( $verb, $url, $params );
}
return new WP_Error( 'auth_error', 'Unable to authenticate' );
}
public function signUrl( $verb, $url, $params = null ) {
$url_parts = wp_parse_url( $url );
$query = $this->get_url_query( $url );
$query['signature'] = $this->get_signature( $verb, $url, $params );
$url_parts['query'] = $this->build_query( $query );
return http_build_url( $url_parts );
}
private function get_signature( $verb, $url, array $params = null ) {
if ( $this->has_keys() ) {
$verb = strtolower( $verb );
$url_parts = wp_parse_url( $url );
$query_to_sign = $this->get_url_query( $url );
$body_md5 = null;
if ( $params && 'get' !== $verb ) {
$body_md5 = md5( wp_json_encode( $params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) );
$query_to_sign['body'] = $body_md5;
}
$url_parts_to_sign = $url_parts;
$url_parts_to_sign['query'] = $this->build_query( $query_to_sign );
$url_to_sign = http_build_url( $url_parts_to_sign );
$string_to_sign = strtolower( $verb ) . $url_to_sign;
$sha1 = hash_hmac( 'sha1', $string_to_sign, $this->get_secret(), true );
return base64_encode( $sha1 );
}
return null;
}
public function has_keys() {
return $this->get_secret() && $this->get_shared();
}
private function get_secret() {
return $this->get_ams_data_property( 'secret' );
}
private function get_shared() {
return $this->get_ams_data_property( 'shared' );
}
private function get_ams_data_property( $field ) {
$data = $this->get_ams_data();
if ( array_key_exists( $field, $data ) ) {
return $data[ $field ];
}
return null;
}
/**
* @return array
*/
private function get_ams_data() {
return get_option( self::AMS_DATA_KEY, [] );
}
/**
* @param string $verb
* @param string $url
* @param array|null $params
*
* @return string
*/
private function add_required_arguments_to_url( $verb, $url, array $params = null ) {
$verb = strtolower( $verb );
$url_parts = wp_parse_url( $url );
$query = $this->get_url_query( $url );
if ( $params && 'get' === $verb ) {
foreach ( $params as $key => $value ) {
$query[ $key ] = $value;
}
}
$query['wpml_core_version'] = ICL_SITEPRESS_VERSION;
$query['wpml_tm_version'] = WPML_TM_VERSION;
$query['shared_key'] = $this->get_shared();
$query['token'] = uuid_v5( wp_generate_uuid4(), $url );
$query['website_uuid'] = $this->get_site_id();
$query['ui_language_code'] = apply_filters(
'wpml_get_user_admin_language',
wpml_get_default_language(),
get_current_user_id()
);
$url_parts['query'] = http_build_query( $query );
return http_build_url( $url_parts );
}
/**
* @param string $url
*
* @return array
*/
private function get_url_query( $url ) {
$url_parts = wp_parse_url( $url );
$query = array();
if ( array_key_exists( 'query', $url_parts ) ) {
parse_str( $url_parts['query'], $query );
}
return $query;
}
/**
* @param $query
*
* @return mixed|string
*/
protected function build_query( $query ) {
if ( PHP_VERSION_ID >= 50400 ) {
$final_query = http_build_query( $query, null, '&', PHP_QUERY_RFC3986 );
} else {
$final_query = str_replace(
array( '+', '%7E' ),
array( '%20', '~' ),
http_build_query( $query )
);
}
return $final_query;
}
/**
* @param string|null $site_id
*/
public function override_site_id( $site_id ) {
$this->site_id = $site_id;
}
public function get_site_id() {
return $this->site_id ? $this->site_id : wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE );
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WPML\TM\ATE\Download;
use Exception;
use WPML\TM\ATE\ReturnedJobsQueue;
use WPML_TM_ATE_API;
use WPML_TM_ATE_Jobs;
class Consumer {
/** @var WPML_TM_ATE_API $ateApi */
private $ateApi;
/** @var WPML_TM_ATE_Jobs $ateJobs */
private $ateJobs;
public function __construct( WPML_TM_ATE_API $ateApi, WPML_TM_ATE_Jobs $ateJobs ) {
$this->ateApi = $ateApi;
$this->ateJobs = $ateJobs;
}
/**
* @param Job $job
*
* @return Job|false
* @throws Exception
*/
public function process( Job $job ) {
$xliffContent = $this->ateApi->get_remote_xliff_content( $job->url );
$wpmlJobId = $this->ateJobs->apply( $xliffContent );
if ( $wpmlJobId ) {
$processedJob = clone $job;
$processedJob->wpmlJobId = $wpmlJobId;
ReturnedJobsQueue::remove( $wpmlJobId );
return $processedJob;
}
return false;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace WPML\TM\ATE\Download;
class Job {
/** @var int $ateJobId */
public $ateJobId;
/** @var string $url */
public $url;
/**
* This property is not part of the database data,
* but it can be added when the job is downloaded
* to provide more information to the UI.
*
* @var int $wpmlJobId
*/
public $wpmlJobId;
/**
* @param \stdClass $item
*
* @return Job
*/
public static function fromAteResponse( \stdClass $item ) {
$job = new self();
$job->ateJobId = $item->ate_id;
$job->url = $item->download_link;
return $job;
}
/**
* @param \stdClass $row
*
* @return Job
*/
public static function fromDb( \stdClass $row ) {
$job = new self();
$job->ateJobId = $row->editor_job_id;
$job->url = $row->download_url;
return $job;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace WPML\TM\ATE\Download;
use Exception;
use WPML\Collect\Support\Collection;
use WPML\TM\ATE\Log\Entry;
use WPML\TM\ATE\Log\ErrorEvents;
use WPML_TM_ATE_API;
class Process {
/** @var Queue $queue */
private $queue;
/** @var Consumer $consumer */
private $consumer;
/** @var WPML_TM_ATE_API $ateApi */
private $ateApi;
public function __construct( Queue $queue, Consumer $consumer, WPML_TM_ATE_API $ateApi ) {
$this->queue = $queue;
$this->consumer = $consumer;
$this->ateApi = $ateApi;
}
/**
* @param int $quantity
*
* @return Result
*/
public function run( $quantity = 5 ) {
$result = new Result();
$job = $processedJob = null;
do {
try {
$job = $this->queue->getFirst();
if ( $job ) {
$this->queue->remove( $job );
$processedJob = $this->consumer->process( $job );
if ( ! $processedJob ) {
throw new Exception( 'The translation job could not be applied.' );
}
$result->processedJobs->push( $processedJob );
}
} catch ( Exception $e ) {
$this->logException( $e, $processedJob ?: $job );
}
$processedJob = null;
$quantity--;
} while ( $quantity && $job );
$this->acknowledgeAte( $result->processedJobs );
$result->downloadQueueSize = $this->queue->count();
return $result;
}
private function acknowledgeAte( Collection $processedJobs ) {
if ( $processedJobs->count() ) {
$this->ateApi->confirm_received_job( $processedJobs->pluck( 'ateJobId' )->toArray() );
}
}
/**
* @param Exception $e
* @param Job|null $job
*/
private function logException( Exception $e, Job $job = null ) {
$entry = new Entry();
$entry->description = $e->getMessage();
if ( $job ) {
$entry->ateJobId = $job->ateJobId;
$entry->wpmlJobId = $job->wpmlJobId;
$entry->extraData = [ 'downloadUrl' => $job->url ];
}
if ( $e instanceof \Requests_Exception ) {
$entry->event = ErrorEvents::SERVER_XLIFF;
} else {
$entry->event = ErrorEvents::JOB_DOWNLOAD;
}
wpml_tm_ate_ams_log( $entry );
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace WPML\TM\ATE\Download;
use WPML\Collect\Support\Collection;
use WPML\TM\Upgrade\Commands\CreateAteDownloadQueueTable;
class Queue {
/** @var \wpdb $wpdb */
private $wpdb;
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @param Collection $jobs A collection of `Job`
*/
public function push( Collection $jobs ) {
if ( ! $jobs->count() ) {
return;
}
$prepare = function( Job $job ) {
return $this->wpdb->prepare( '(%d,%s)', $job->ateJobId, $job->url );
};
$columns = '(editor_job_id, download_url)';
$values = $jobs->map( $prepare )->implode( ',' );
$this->wpdb->query(
"INSERT IGNORE INTO {$this->getTableName()} {$columns} VALUES {$values}"
);
}
/**
* @return Collection
*/
public function getEditorJobIds() {
return wpml_collect( $this->wpdb->get_col( "SELECT editor_job_id FROM {$this->getTableName()}" ) );
}
/**
* @return int
*/
public function count() {
return (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$this->getTableName()}" );
}
/** @return Job|null */
public function getFirst() {
$job = null;
$this->wpdb->query( 'START TRANSACTION' );
$row = $this->getFirstUnlockedRow();
if ( $row ) {
$job = Job::fromDb( $row );
$this->lockJob( $job );
}
$this->wpdb->query( 'COMMIT' );
return $job;
}
/**
* @return \stdClass|null
*/
private function getFirstUnlockedRow() {
$oldLockTimestamp = time() - self::getLockExpiration();
return $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT * FROM {$this->getTableName()}
WHERE lock_timestamp IS NULL OR lock_timestamp < %d
LIMIT 1
FOR UPDATE",
$oldLockTimestamp
)
);
}
public function lockJob( Job $job ) {
$this->wpdb->query(
$this->wpdb->prepare(
"UPDATE {$this->getTableName()} SET lock_timestamp=%d WHERE editor_job_id=%d",
time(),
$job->ateJobId
)
);
}
public function remove( Job $job ) {
$this->wpdb->delete(
$this->getTableName(),
[ 'editor_job_id' => $job->ateJobId ],
[ '%d' ]
);
}
/** @return string */
private function getTableName() {
return $this->wpdb->prefix . CreateAteDownloadQueueTable::TABLE_NAME;
}
/** @return int */
public static function getLockExpiration() {
return 3 * MINUTE_IN_SECONDS;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author OnTheGo Systems
*/
namespace WPML\TM\ATE\Download;
use WPML\Collect\Support\Collection;
class Result {
/** @var Collection $processedJobs */
public $processedJobs;
/** @var int $downloadQueueSize */
public $downloadQueueSize = 0;
public function __construct() {
$this->processedJobs = wpml_collect( [] );
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace WPML\TM\ATE\Hooks;
use WPML\TM\ATE\ReturnedJobsQueue;
class ReturnedJobActions implements \IWPML_Action {
/** @var callable :: int->string->void */
private $addToQueue;
/**
* @param callable $addToQueue
*/
public function __construct( callable $addToQueue ) {
$this->addToQueue = $addToQueue;
}
public function add_hooks() {
add_action( 'init', [ $this, 'addToQueue' ] );
}
public function addToQueue() {
if ( isset( $_GET['ate_original_id'] ) ) {
$ateJobId = (int) $_GET['ate_original_id'];
if ( isset( $_GET['complete'] ) ) {
call_user_func( $this->addToQueue, $ateJobId, ReturnedJobsQueue::STATUS_COMPLETED );
} elseif ( isset( $_GET['back'] ) ) {
call_user_func( $this->addToQueue, $ateJobId, ReturnedJobsQueue::STATUS_BACK );
}
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace WPML\TM\ATE\Hooks;
use WPML\TM\ATE\ReturnedJobsQueue;
use function WPML\Container\make;
use function WPML\FP\partialRight;
class ReturnedJobActionsFactory implements \IWPML_Backend_Action_Loader, \IWPML_REST_Action_Loader {
public function create() {
$ateJobs = make( \WPML_TM_ATE_Jobs::class );
$add = partialRight( [ ReturnedJobsQueue::class, 'add' ], [ $ateJobs, 'get_wpml_job_id' ] );
return new ReturnedJobActions( $add );
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_AMS_Check_Website_ID_Factory implements IWPML_Backend_Action_Loader {
/**
* @return \WPML_TM_AMS_Check_Website_ID|null
* @throws \Auryn\InjectionException
*/
public function create() {
$options_manager = \WPML\Container\make( '\WPML\WP\OptionManager' );
if (
WPML_TM_ATE_Status::is_enabled_and_activated() &&
! wpml_is_ajax() &&
! $options_manager->get( 'TM-has-run', 'WPML_TM_AMS_Check_Website_ID' )
) {
return \WPML\Container\make( '\WPML_TM_AMS_Check_Website_ID' );
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
use WPML\WP\OptionManager;
/**
* @author OnTheGo Systems
*/
class WPML_TM_AMS_Check_Website_ID implements IWPML_Action {
/** @var \WPML\WP\OptionManager $option_manager */
private $option_manager;
/** @var WPML_TM_ATE_API $ate_api */
private $ate_api;
/** @var WPML_TM_AMS_API $ams_api */
private $ams_api;
public function __construct(
OptionManager $option_manager,
WPML_TM_ATE_API $ate_api,
WPML_TM_AMS_API $ams_api
) {
$this->option_manager = $option_manager;
$this->ate_api = $ate_api;
$this->ams_api = $ams_api;
}
public function add_hooks() {
add_action( 'wpml_after_tm_loaded', array( $this, 'do_check' ) );
}
/**
* Check if the stored site id is different from the one returned by ams api and
* then:
* 1) test if the ams one works
* 2) Update stored id if test is successful
*/
public function do_check() {
$stored_site_id = wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE );
$site_id_from_ams = $this->ate_api->get_website_id( get_site_url() );
if ( $site_id_from_ams && $stored_site_id !== $site_id_from_ams ) {
if ( $this->does_site_id_work( $site_id_from_ams ) ) {
update_option( WPML_Site_ID::SITE_ID_KEY . ':ate', $site_id_from_ams, false );
}
}
$this->option_manager->set( 'TM-has-run', 'WPML_TM_AMS_Check_Website_ID', true, true );
}
/**
* @param string $site_id
*
* @return bool
*/
private function does_site_id_work( $site_id ) {
$this->ams_api->override_site_id( $site_id );
$response = $this->ams_api->is_subscription_activated( 'any@any.com' );
$is_site_id_error = false;
if ( is_wp_error( $response ) ) {
$error_data = $response->get_error_data( 400 );
$is_site_id_error = isset( $error_data['detail'] ) && $error_data['detail'] === 'Website not found, please validate site identifier';
}
return ! $is_site_id_error;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_AMS_Synchronize_Actions_Factory implements IWPML_Backend_Action_Loader {
/**
* @return IWPML_Action|IWPML_Action[]|null
*/
public function create() {
if ( WPML_TM_ATE_Status::is_enabled_and_activated() ) {
$ams_api = WPML\Container\make( WPML_TM_AMS_API::class );
global $wpdb;
$user_query_factory = new WPML_WP_User_Query_Factory();
$wp_roles = wp_roles();
$translator_records = new WPML_Translator_Records( $wpdb, $user_query_factory, $wp_roles );
$manager_records = new WPML_Translation_Manager_Records( $wpdb, $user_query_factory, $wp_roles );
$admin_translators = new WPML_Translator_Admin_Records( $wpdb, $user_query_factory, $wp_roles );
$user_records = new WPML_TM_AMS_Users( $manager_records, $translator_records, $admin_translators );
$user_factory = new WPML_WP_User_Factory();
$translator_activation_records = new WPML_TM_AMS_Translator_Activation_Records( new WPML_WP_User_Factory() );
return new WPML_TM_AMS_Synchronize_Actions(
$ams_api,
$user_records,
$user_factory,
$translator_activation_records
);
}
return null;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_AMS_Synchronize_Actions implements IWPML_Action {
const ENABLED_FOR_TRANSLATION_VIA_ATE = 'wpml_enabled_for_translation_via_ate';
/**
* @var WPML_TM_AMS_API
*/
private $ams_api;
/**
* @var WPML_TM_AMS_Users
*/
private $ams_user_records;
/**
* @var WPML_WP_User_Factory $user_factory
*/
private $user_factory;
/**
* @var WPML_TM_AMS_Translator_Activation_Records
*/
private $translator_activation_records;
public function __construct(
WPML_TM_AMS_API $ams_api,
WPML_TM_AMS_Users $ams_user_records,
WPML_WP_User_Factory $user_factory,
WPML_TM_AMS_Translator_Activation_Records $translator_activation_records
) {
$this->ams_api = $ams_api;
$this->ams_user_records = $ams_user_records;
$this->user_factory = $user_factory;
$this->translator_activation_records = $translator_activation_records;
}
public function add_hooks() {
add_action( 'wpml_tm_ate_synchronize_translators', array( $this, 'synchronize_translators' ) );
add_action( 'wpml_tm_ate_synchronize_managers', array( $this, 'synchronize_managers' ) );
add_action( 'wpml_tm_ate_enable_subscription', array( $this, 'enable_subscription' ) );
add_action( 'deleted_user', array( $this, 'user_changed' ) );
add_action( 'profile_update', array( $this, 'user_changed' ) );
}
/**
* @throws \InvalidArgumentException
*/
public function synchronize_translators() {
$result = $this->ams_api->synchronize_translators( $this->ams_user_records->get_translators() );
if ( ! is_wp_error( $result ) ) {
$this->translator_activation_records->update( isset( $result['translators'] ) ? $result['translators'] : array() );
}
}
/**
* @throws \InvalidArgumentException
*/
public function synchronize_managers() {
$this->ams_api->synchronize_managers( $this->ams_user_records->get_managers() );
}
public function enable_subscription( $user_id ) {
$user = $this->user_factory->create( $user_id );
if ( ! $user->get_meta( self::ENABLED_FOR_TRANSLATION_VIA_ATE ) ) {
$this->ams_api->enable_subscription( $user->user_email );
$user->update_meta( self::ENABLED_FOR_TRANSLATION_VIA_ATE, true );
}
}
public function user_changed() {
$this->synchronize_managers();
$this->synchronize_translators();
}
}

View File

@@ -0,0 +1,8 @@
<?php
class WPML_TM_AMS_Synchronize_Users_On_Access_Denied_Factory implements IWPML_Backend_Action_Loader {
public function create() {
return new WPML_TM_AMS_Synchronize_Users_On_Access_Denied();
}
}

View File

@@ -0,0 +1,89 @@
<?php
class WPML_TM_AMS_Synchronize_Users_On_Access_Denied {
const ERROR_MESSAGE = 'Authentication error, please contact your translation manager to check your subscription';
/** @var WPML_TM_AMS_Synchronize_Actions */
private $ams_synchronize_actions;
/** @var WPML_TM_ATE_Jobs */
private $ate_jobs;
public function add_hooks() {
if ( WPML_TM_ATE_Status::is_enabled_and_activated() ) {
add_action( 'init', array( $this, 'catch_access_error' ) );
}
}
public function catch_access_error() {
if ( ! $this->ate_redirected_due_to_lack_of_access() ) {
return;
}
$this->get_ams_synchronize_actions()->synchronize_translators();
if ( ! isset( $_GET['ate_job_id'] ) ) {
return;
}
$wpml_job_id = $this->get_ate_jobs()->get_wpml_job_id( $_GET['ate_job_id'] );
if ( ! $wpml_job_id ) {
return;
}
$url = admin_url(
'admin.php?page='
. WPML_TM_FOLDER
. '/menu/translations-queue.php&job_id='
. $wpml_job_id
);
wp_safe_redirect( $url, 302, 'WPML' );
}
/**
* @return bool
*/
private function ate_redirected_due_to_lack_of_access() {
return isset( $_GET['message'] ) && false !== strpos( $_GET['message'], self::ERROR_MESSAGE );
}
/**
* @return IWPML_Action|IWPML_Action[]|WPML_TM_AMS_Synchronize_Actions
*/
private function get_ams_synchronize_actions() {
if ( ! $this->ams_synchronize_actions ) {
$factory = new WPML_TM_AMS_Synchronize_Actions_Factory();
$this->ams_synchronize_actions = $factory->create();
}
return $this->ams_synchronize_actions;
}
/**
* @return WPML_TM_ATE_Jobs
*/
private function get_ate_jobs() {
if ( ! $this->ate_jobs ) {
$ate_jobs_records = wpml_tm_get_ate_job_records();
$this->ate_jobs = new WPML_TM_ATE_Jobs( $ate_jobs_records );
}
return $this->ate_jobs;
}
/**
* @param WPML_TM_AMS_Synchronize_Actions $ams_synchronize_actions
*/
public function set_ams_synchronize_actions( WPML_TM_AMS_Synchronize_Actions $ams_synchronize_actions ) {
$this->ams_synchronize_actions = $ams_synchronize_actions;
}
/**
* @param WPML_TM_ATE_Jobs $ate_jobs
*/
public function set_ate_jobs( WPML_TM_ATE_Jobs $ate_jobs ) {
$this->ate_jobs = $ate_jobs;
}
}

View File

@@ -0,0 +1,21 @@
<?php
class WPML_TM_ATE_API_Error {
public function log( $message ) {
$wpml_admin_notices = wpml_get_admin_notices();
$notice = new WPML_Notice(
WPML_TM_ATE_Jobs_Actions::RESPONSE_ATE_ERROR_NOTICE_ID,
sprintf(
__( 'There was a problem communicating with ATE: %s ', 'wpml-translation-management' ),
'(<i>' . $message . '</i>)'
),
WPML_TM_ATE_Jobs_Actions::RESPONSE_ATE_ERROR_NOTICE_GROUP
);
$notice->set_css_class_types( array( 'warning' ) );
$notice->add_capability_check( array( 'manage_options', 'wpml_manage_translation_management' ) );
$notice->set_flash();
$wpml_admin_notices->add_notice( $notice );
}
}

View File

@@ -0,0 +1,11 @@
<?php
class WPML_TM_ATE_Job_Data_Fallback_Factory implements IWPML_Backend_Action_Loader, IWPML_REST_Action_Loader {
/**
* @return WPML_TM_ATE_Job_Data_Fallback
*/
public function create() {
return \WPML\Container\make( '\WPML_TM_ATE_Job_Data_Fallback' );
}
}

View File

@@ -0,0 +1,39 @@
<?php
use WPML\TM\ATE\JobRecords;
class WPML_TM_ATE_Job_Data_Fallback implements IWPML_Action {
/** @var WPML_TM_ATE_API */
private $ate_api;
/**
* @param WPML_TM_ATE_API $ate_api
*/
public function __construct( WPML_TM_ATE_API $ate_api ) {
$this->ate_api = $ate_api;
}
public function add_hooks() {
add_filter( 'wpml_tm_ate_job_data_fallback', array( $this, 'get_data_from_api' ), 10, 2 );
}
/**
* @param array $data
* @param int $wpml_job_id
*
* @return array
*/
public function get_data_from_api( array $data, $wpml_job_id ) {
$response = $this->ate_api->get_jobs_by_wpml_ids( array( $wpml_job_id ) );
if ( ! $response || is_wp_error( $response ) ) {
return $data;
}
if ( ! isset( $response->{$wpml_job_id}->ate_job_id ) ) {
return $data;
}
return array( JobRecords::FIELD_ATE_JOB_ID => $response->{$wpml_job_id}->ate_job_id );
}
}

View File

@@ -0,0 +1,76 @@
<?php
use function WPML\Container\make;
use WPML\TM\ATE\ReturnedJobsQueue;
/**
* Factory class for \WPML_TM_ATE_Jobs_Actions.
*
* @package wpml\tm
*
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Jobs_Actions_Factory implements IWPML_Backend_Action_Loader {
/**
* The instance of \WPML_Current_Screen.
*
* @var WPML_Current_Screen
*/
private $current_screen;
/**
* It returns an instance of \WPML_TM_ATE_Jobs_Actions or null if ATE is not enabled and active.
*
* @return \WPML_TM_ATE_Jobs_Actions|null
* @throws \Auryn\InjectionException
*/
public function create() {
$ams_ate_factories = wpml_tm_ams_ate_factories();
if ( WPML_TM_ATE_Status::is_enabled() && $ams_ate_factories->is_ate_active() ) {
$sitepress = $this->get_sitepress();
$current_screen = $this->get_current_screen();
$ate_api = $ams_ate_factories->get_ate_api();
$records = wpml_tm_get_ate_job_records();
$ate_jobs = new WPML_TM_ATE_Jobs( $records );
$translator_activation_records = new WPML_TM_AMS_Translator_Activation_Records( new WPML_WP_User_Factory() );
return new WPML_TM_ATE_Jobs_Actions(
$ate_api,
$ate_jobs,
$sitepress,
$current_screen,
$translator_activation_records,
make( \WPML_TM_ATE_Jobs_Sync_Script_Loader::class )
);
}
return null;
}
/**
* The global instance of \Sitepress.
*
* @return SitePress
*/
private function get_sitepress() {
global $sitepress;
return $sitepress;
}
/**
* It gets the instance of \WPML_Current_Screen.
*
* @return \WPML_Current_Screen
*/
private function get_current_screen() {
if ( ! $this->current_screen ) {
$this->current_screen = new WPML_Current_Screen();
}
return $this->current_screen;
}
}

View File

@@ -0,0 +1,619 @@
<?php
use WPML\TM\ATE\JobRecords;
use WPML\FP\Fns;
use WPML\FP\Obj;
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Jobs_Actions implements IWPML_Action {
const RESPONSE_ATE_NOT_ACTIVE_ERROR = 403;
const RESPONSE_ATE_DUPLICATED_SOURCE_ID = 417;
const RESPONSE_ATE_UNEXPECTED_ERROR = 500;
const RESPONSE_ATE_ERROR_NOTICE_ID = 'ate-update-error';
const RESPONSE_ATE_ERROR_NOTICE_GROUP = 'default';
/**
* @var WPML_TM_ATE_API
*/
private $ate_api;
/**
* @var WPML_TM_ATE_Jobs
*/
private $ate_jobs;
/**
* @var WPML_TM_AMS_Translator_Activation_Records
*/
private $translator_activation_records;
/** @var bool */
private $is_second_attempt_to_get_jobs_data = false;
/**
* @var SitePress
*/
private $sitepress;
/**
* @var WPML_Current_Screen
*/
private $current_screen;
/** @var array */
private $trid_original_element_map = array();
/** @var WPML_TM_ATE_Jobs_Sync_Script_Loader */
private $job_sync_script_loader;
/**
* WPML_TM_ATE_Jobs_Actions constructor.
*
* @param \WPML_TM_ATE_API $ate_api
* @param \WPML_TM_ATE_Jobs $ate_jobs
* @param \SitePress $sitepress
* @param \WPML_Current_Screen $current_screen
* @param \WPML_TM_AMS_Translator_Activation_Records $translator_activation_records
* @param WPML_TM_ATE_Jobs_Sync_Script_Loader $job_sync_script_loader
*/
public function __construct(
WPML_TM_ATE_API $ate_api,
WPML_TM_ATE_Jobs $ate_jobs,
SitePress $sitepress,
WPML_Current_Screen $current_screen,
WPML_TM_AMS_Translator_Activation_Records $translator_activation_records,
WPML_TM_ATE_Jobs_Sync_Script_Loader $job_sync_script_loader
) {
$this->ate_api = $ate_api;
$this->ate_jobs = $ate_jobs;
$this->sitepress = $sitepress;
$this->current_screen = $current_screen;
$this->translator_activation_records = $translator_activation_records;
$this->job_sync_script_loader = $job_sync_script_loader;
}
public function add_hooks() {
add_action( 'wpml_added_translation_job', array( $this, 'added_translation_job' ), 10, 2 );
add_action( 'wpml_added_translation_jobs', array( $this, 'added_translation_jobs' ) );
add_action( 'admin_notices', array( $this, 'handle_messages' ) );
add_action( 'current_screen', array( $this, 'update_jobs_on_current_screen' ) );
add_action( 'wp', array( $this, 'update_jobs_on_current_screen' ) );
add_filter( 'wpml_tm_ate_jobs_data', array( $this, 'get_ate_jobs_data_filter' ), 10, 2 );
add_filter( 'wpml_tm_ate_jobs_editor_url', array( $this, 'get_editor_url' ), 10, 3 );
}
public function handle_messages() {
if ( $this->current_screen->id_ends_with( WPML_TM_FOLDER . '/menu/translations-queue' ) ) {
if ( array_key_exists( 'message', $_GET ) ) {
if ( array_key_exists( 'ate_job_id', $_GET ) ) {
$ate_job_id = filter_var( $_GET['ate_job_id'], FILTER_SANITIZE_NUMBER_INT );
$this->resign_job_on_error( $ate_job_id );
}
$message = filter_var( $_GET['message'], FILTER_SANITIZE_STRING );
?>
<div class="error notice-error notice otgs-notice">
<p><?php echo $message; ?></p>
</div>
<?php
}
}
}
/**
* @param int $job_id
* @param string $translation_service
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function added_translation_job( $job_id, $translation_service ) {
$this->added_translation_jobs( array( $translation_service => array( $job_id ) ) );
}
/**
* @param array $jobs
*
* @return bool|void
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function added_translation_jobs( array $jobs ) {
$oldEditor = wpml_tm_load_old_jobs_editor();
$job_ids = Fns::reject( [ $oldEditor, 'shouldStickToWPMLEditor' ], Obj::propOr( [], 'local', $jobs ) );
if ( ! $job_ids ) {
return;
}
$jobs = [];
$rid_to_job_map = [];
foreach ( $job_ids as $job_id ) {
$rid = wpml_tm_get_records()->icl_translate_job_by_job_id( $job_id )->rid();
$rid_to_job_map[ $rid ] = $job_id;
$jobs[] = wpml_tm_create_ATE_job_creation_model( $job_id, $rid );
}
$response = $this->create_jobs( $jobs );
try {
$this->check_response_error( $response );
} catch ( RuntimeException $ex ) {
do_action( 'wpml_tm_basket_add_message', 'error', $ex->getMessage() );
return;
}
$has_valid_response = $response && isset( $response->jobs );
$response_jobs = null;
if ( $has_valid_response ) {
$response_jobs = $response->jobs;
}
if ( $response_jobs ) {
if ( is_object( $response_jobs ) ) {
$response_jobs = json_decode( wp_json_encode( $response_jobs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), true );
}
$response_jobs = $this->map_response_jobs( $response_jobs, $rid_to_job_map );
$this->ate_jobs->warm_cache( array_keys( $response_jobs ) );
foreach ( $response_jobs as $wpml_job_id => $ate_job_id ) {
$this->ate_jobs->store( $wpml_job_id, array( JobRecords::FIELD_ATE_JOB_ID => $ate_job_id ) );
$oldEditor->set( $wpml_job_id, WPML_TM_Editors::ATE );
}
$message = __( '%1$s jobs added to the Advanced Translation Editor.', 'wpml-translation-management' );
$this->add_message( 'updated', sprintf( $message, count( $response_jobs ) ), 'wpml_tm_ate_create_job' );
} else {
$this->add_message(
'error',
__(
'Jobs could not be created in Advanced Translation Editor. Please try again or contact the WPML support for help.',
'wpml-translation-management'
),
'wpml_tm_ate_create_job'
);
}
}
private function map_response_jobs( $responseJobs, $rid_to_job_id_map ) {
$result = [];
foreach ( $responseJobs as $rid => $ate_job_id ) {
if ( isset( $rid_to_job_id_map[ $rid ] ) ) {
$result[ $rid_to_job_id_map[ $rid ] ] = $ate_job_id;
}
}
return $result;
}
/**
* @param string $type
* @param string $message
* @param string|null $id
*/
private function add_message( $type, $message, $id = null ) {
do_action( 'wpml_tm_basket_add_message', $type, $message, $id );
}
/**
* @param WPML_TM_ATE_Models_Job_Create[] $jobs
*
* @return mixed
* @throws \InvalidArgumentException
*/
private function create_jobs( array $jobs ) {
$params = json_decode( wp_json_encode( array( 'jobs' => $jobs ) ), true );
return $this->ate_api->create_jobs( $params );
}
/**
* After implementation of wpmltm-3211 and wpmltm-3391, we should not find missing ATE IDs anymore.
* Some code below seems dead but we'll keep it for now in case we are missing a specific context.
*
* @link https://onthegosystems.myjetbrains.com/youtrack/issue/wpmltm-3211
* @link https://onthegosystems.myjetbrains.com/youtrack/issue/wpmltm-3391
*/
private function get_ate_jobs_data( array $translation_jobs ) {
$ate_jobs_data = array();
$skip_getting_data = false;
$ate_jobs_to_create = array();
$this->ate_jobs->warm_cache( wpml_collect( $translation_jobs )->pluck( 'job_id' )->toArray() );
foreach ( $translation_jobs as $translation_job ) {
if ( $this->is_ate_translation_job( $translation_job ) ) {
$ate_job_id = $this->get_ate_job_id( $translation_job->job_id );
// Start of possibly dead code.
if ( ! $ate_job_id ) {
$ate_jobs_to_create[] = $translation_job->job_id;
$skip_getting_data = true;
}
// End of possibly dead code.
if ( ! $skip_getting_data ) {
$ate_jobs_data[ $translation_job->job_id ] = [ 'ate_job_id' => $ate_job_id ];
}
}
}
// Start of possibly dead code.
if (
! $this->is_second_attempt_to_get_jobs_data &&
$ate_jobs_to_create &&
$this->added_translation_jobs( array( 'local' => $ate_jobs_to_create ) )
) {
$ate_jobs_data = $this->get_ate_jobs_data( $translation_jobs );
$this->is_second_attempt_to_get_jobs_data = true;
}
// End of possibly dead code.
return $ate_jobs_data;
}
/**
* @param string $default_url
* @param int $job_id
* @param null|string $return_url
*
* @return string
* @throws \InvalidArgumentException
*/
public function get_editor_url( $default_url, $job_id, $return_url = null ) {
if ( $this->translator_activation_records->is_current_user_activated() ) {
$ate_job_id = $this->ate_jobs->get_ate_job_id( $job_id );
if ( $ate_job_id ) {
if ( ! $return_url ) {
$return_url = add_query_arg(
array(
'page' => WPML_TM_FOLDER . '/menu/translations-queue.php',
'ate-return-job' => $job_id,
),
admin_url( '/admin.php' )
);
}
$ate_job_url = $this->ate_api->get_editor_url( $ate_job_id, $return_url );
if ( $ate_job_url && ! is_wp_error( $ate_job_url ) ) {
return $ate_job_url;
}
}
}
return $default_url;
}
/**
* @param $ignore
* @param array $translation_jobs
*
* @return array
*/
public function get_ate_jobs_data_filter( $ignore, array $translation_jobs ) {
return $this->get_ate_jobs_data( $translation_jobs );
}
private function get_ate_job_id( $job_id ) {
return $this->ate_jobs->get_ate_job_id( $job_id );
}
public function update_jobs_on_current_screen() {
$load_ate_jobs_synchronization = $this->is_edit_list_page_of_a_translatable_type() ||
$this->is_edit_page_of_a_translatable_type() ||
WPML_TM_Page::is_dashboard() ||
WPML_TM_Page::is_translation_queue();
if ( apply_filters( 'wpml_tm_load_ate_jobs_synchronization', $load_ate_jobs_synchronization ) ) {
$this->job_sync_script_loader->load();
}
}
/**
* @param string $message
*/
private function add_update_error_notice( $message ) {
$error_log = new WPML_TM_ATE_API_Error();
$error_log->log( $message );
}
/**
* @todo: Remove this method in favor of the new SYNC/DOWNLOAD process.
*
* @param bool $updated
* @param array|stdClass $translation_jobs
* @param bool $ignore_errors
*
* @throws \RuntimeException
*
* @return int[] Returns an array of WPML job IDs that translation was applied (XLIFF updated)
*/
public function update_jobs( $updated, $translation_jobs, $ignore_errors = false ) {
/**
* We should only expect an array of objects.
* However, this method can be called by an action and a known issue may cause to pass a single object instead
*
* @see https://developer.wordpress.org/reference/functions/do_action/#comment-2371
*/
if ( is_object( $translation_jobs ) ) {
if ( isset( $translation_jobs->job_id ) ) {
$translation_jobs = array( $translation_jobs );
} else {
$translation_jobs = null;
}
}
$jobs_with_translation_applied = array();
if ( $translation_jobs ) {
$ate_jobs_data = $this->get_ate_jobs_data( $translation_jobs );
if ( ! $ate_jobs_data ) {
return array();
}
$job_ids_map = array();
foreach ( $translation_jobs as $translation_job ) {
if ( $this->is_ate_translation_job( $translation_job ) ) {
$ate_job_id = null;
if ( isset( $ate_jobs_data[ $translation_job->job_id ]['ate_job_id'] ) ) {
$ate_job_id = $ate_jobs_data[ $translation_job->job_id ]['ate_job_id'];
$job_ids_map[ $ate_job_id ] = $translation_job->job_id;
}
}
}
if ( $job_ids_map ) {
$ate_job_ids = array_keys( $job_ids_map );
$response = $this->ate_api->get_jobs( $ate_job_ids );
try {
$this->check_response_error( $response );
} catch ( RuntimeException $e ) {
$this->add_update_error_notice( $e->getMessage() );
}
$processed = json_decode( wp_json_encode( $response ), true );
if ( $processed ) {
foreach ( $processed as $ate_job_id => $ate_job_data ) {
if ( array_key_exists( $ate_job_id, $job_ids_map ) ) {
$wpml_job_id = (int) $job_ids_map[ $ate_job_id ];
if ( $this->is_delivered_job_being_edited( $wpml_job_id, $ate_job_data ) ) {
continue;
}
try {
$is_translations_applied = $this->maybe_apply_translation( $ate_job_data );
} catch ( Exception $e ) {
if ( ! $ignore_errors ) {
throw new RuntimeException( $e->getMessage(), $e->getCode() );
}
continue;
}
if ( $is_translations_applied ) {
$this->ate_jobs->store( $wpml_job_id, $ate_job_data );
if ( $this->must_acknowledge_ATE( $ate_job_data ) ) {
$this->confirm_received_job( $ate_job_id, $ignore_errors );
}
$jobs_with_translation_applied[] = $wpml_job_id;
} else {
$this->ate_jobs->store( $wpml_job_id, $ate_job_data );
if ( isset( $ate_job_data['status_id'] ) ) {
$this->ate_jobs->set_wpml_status_from_ate( $wpml_job_id, (int) $ate_job_data['status_id'] );
}
}
}
}
}
}
}
return $jobs_with_translation_applied;
}
/**
* This situation happens when a job was delivered
* and the translator is editing the job but he did not
* click on the "Redeliver" button yet.
*
* @param int $wpml_job_id
* @param array $ate_job_data
*
* @return bool
*/
private function is_delivered_job_being_edited( $wpml_job_id, array $ate_job_data ) {
return isset( $ate_job_data['status_id'] )
&& WPML_TM_ATE_AMS_Endpoints::ATE_JOB_STATUS_DELIVERED === $ate_job_data['status_id']
&& $this->ate_jobs->is_editing_job( $wpml_job_id );
}
/**
* If we have an XLIFF URL, we will fetch the remote file
* and try to apply it.
*
* @param array $ate_job_data
*
* @return bool
* @throws Requests_Exception
*/
private function maybe_apply_translation( array $ate_job_data ) {
if ( isset( $ate_job_data['translated_xliff'] ) ) {
$xliff_content = $this->ate_api->get_remote_xliff_content( $ate_job_data['translated_xliff'] );
if ( $xliff_content ) {
return $this->ate_jobs->apply( $xliff_content );
}
}
return false;
}
/**
* @param $ate_job_id
* @param $ignore_errors
*
* @return bool
*/
private function confirm_received_job( $ate_job_id, $ignore_errors ) {
$confirmation_response = $this->ate_api->confirm_received_job( $ate_job_id );
try {
$this->check_response_error( $confirmation_response );
return true;
} catch ( Exception $ex ) {
if ( ! $ignore_errors ) {
throw new $ex();
}
return false;
}
}
/**
* @param mixed $response
*
* @throws \RuntimeException
*/
protected function check_response_error( $response ) {
if ( is_wp_error( $response ) ) {
$code = 0;
$message = $response->get_error_message();
if ( $response->error_data && is_array( $response->error_data ) ) {
foreach ( $response->error_data as $http_code => $error_data ) {
$code = $error_data[0]['status'];
$message = '';
switch ( (int) $code ) {
case self::RESPONSE_ATE_NOT_ACTIVE_ERROR:
$wp_admin_url = admin_url( 'admin.php' );
$mcsetup_page = add_query_arg(
array(
'page' => WPML_TM_FOLDER . WPML_Translation_Management::PAGE_SLUG_SETTINGS,
'sm' => 'mcsetup',
),
$wp_admin_url
);
$mcsetup_page .= '#ml-content-setup-sec-1';
$resend_link = '<a href="' . $mcsetup_page . '">'
. esc_html__( 'Resend that email', 'wpml-translation-management' )
. '</a>';
$message .= '<p>'
. esc_html__( 'WPML cannot send these documents to translation because the Advanced Translation Editor is not fully set-up yet.', 'wpml-translation-management' )
. '</p><p>'
. esc_html__( 'Please open the confirmation email that you received and click on the link inside it to confirm your email.', 'wpml-translation-management' )
. '</p><p>'
. $resend_link
. '</p>';
break;
case self::RESPONSE_ATE_DUPLICATED_SOURCE_ID:
case self::RESPONSE_ATE_UNEXPECTED_ERROR:
default:
$message = '<p>'
. __( 'Advanced Translation Editor error:', 'wpml-translation-management' )
. '</p><p>'
. $error_data[0]['message']
. '</p>';
}
$message = '<p>' . $message . '</p>';
}
}
/** @var WP_Error $response */
throw new RuntimeException( $message, $code );
}
}
/**
* @param $ate_job_id
*/
private function resign_job_on_error( $ate_job_id ) {
$job_id = $this->ate_jobs->get_wpml_job_id( $ate_job_id );
if ( $job_id ) {
wpml_load_core_tm()->resign_translator( $job_id );
}
}
/**
* @param $translation_job
*
* @return bool
*/
private function is_ate_translation_job( $translation_job ) {
return 'local' === $translation_job->translation_service
&& WPML_TM_Editors::ATE === $translation_job->editor;
}
/**
* @param $post
*
* @return array|null|WP_Post
*/
private function get_wp_post( $post ) {
if ( ! $post instanceof WP_Post ) {
if ( isset( $post->ID ) ) {
$post = get_post( $post->ID );
} else {
$post = null;
}
}
return $post;
}
/**
* @return bool
*/
private function is_edit_list_page_of_a_translatable_type() {
return $this->current_screen->is_edit_posts_list()
&& $this->sitepress->is_translated_post_type( $this->current_screen->get_post_type() );
}
/**
* @return bool
*/
private function is_edit_page_of_a_translatable_type() {
return $this->current_screen->is_edit_post()
&& $this->sitepress->is_translated_post_type( $this->current_screen->get_post_type() );
}
/**
* @param int $trid
* @param string $element_type
*
* @return mixed
*/
private function get_original_element( $trid, $element_type ) {
if ( ! array_key_exists( $trid, $this->trid_original_element_map ) ) {
$element_translation = $this->sitepress->get_original_element_translation( $trid, $element_type );
if ( $element_translation ) {
$this->trid_original_element_map[ $trid ] = $element_translation;
return $element_translation;
}
}
return $this->trid_original_element_map[ $trid ];
}
/**
* @param $job_status
*
* @return bool
*/
private function must_acknowledge_ATE( $job_status ) {
return $job_status['status_id'] === WPML_TM_ATE_AMS_Endpoints::ATE_JOB_STATUS_DELIVERING;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @todo Perhaps this class is redundant
*
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Jobs_Store_Actions_Factory implements IWPML_Backend_Action_Loader {
/**
* @return IWPML_Action|IWPML_Action[]|null
*/
public function create() {
if ( WPML_TM_ATE_Status::is_enabled() ) {
$ate_jobs_records = wpml_tm_get_ate_job_records();
$ate_jobs = new WPML_TM_ATE_Jobs( $ate_jobs_records );
return new WPML_TM_ATE_Jobs_Store_Actions( $ate_jobs );
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* @todo The hook 'wpml_tm_ate_jobs_store' seems to be never used so this class and its factory may be obsolete
*
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Jobs_Store_Actions implements IWPML_Action {
/**
* @var WPML_TM_ATE_Jobs
*/
private $ate_jobs;
/**
* WPML_TM_ATE_Jobs_Actions constructor.
*
* @param WPML_TM_ATE_Jobs $ate_jobs
*/
public function __construct( WPML_TM_ATE_Jobs $ate_jobs ) {
$this->ate_jobs = $ate_jobs;
}
public function add_hooks() {
add_action( 'wpml_tm_ate_jobs_store', array( $this, 'store' ), 10, 2 );
}
/**
* @param int $wpml_job_id
* @param array $ate_job_data
*
* @return array|null
*/
public function store( $wpml_job_id, $ate_job_data ) {
return $this->ate_jobs->store( $wpml_job_id, $ate_job_data );
}
}

View File

@@ -0,0 +1,84 @@
<?php
use WPML\TM\ATE\Download\Queue;
use WPML\TM\ATE\Sync\Trigger;
use WPML\TM\ATE\ReturnedJobsQueue;
use function WPML\FP\pipe;
use WPML\FP\Relation;
use WPML\FP\Fns;
class WPML_TM_ATE_Jobs_Sync_Script_Loader {
const JS_HANDLER = 'wpml-tm-ate-jobs-sync';
const JS_VARIABLE = 'WPML_ATE_JOBS_SYNC';
/** @var WPML_TM_Scripts_Factory */
private $script_factory;
/** @var WPML_TM_ATE_Job_Repository */
private $ate_jobs_repository;
/** @var Trigger $syncTrigger */
private $syncTrigger;
/** @var Queue $downloadQueue */
private $downloadQueue;
public function __construct(
WPML_TM_Scripts_Factory $script_factory,
WPML_TM_ATE_Job_Repository $ate_jobs_repository,
Trigger $syncTrigger,
Queue $downloadQueue
) {
$this->script_factory = $script_factory;
$this->ate_jobs_repository = $ate_jobs_repository;
$this->syncTrigger = $syncTrigger;
$this->downloadQueue = $downloadQueue;
}
public function load() {
$jobsToSync = $this->ate_jobs_repository->get_jobs_to_sync();
if (
$jobsToSync->count()
|| $this->syncTrigger->isSyncRequired()
|| $this->downloadQueue->count()
) {
wp_register_script(
self::JS_HANDLER,
WPML_TM_URL . '/dist/js/ate/jobs-sync-app.js',
[],
WPML_TM_VERSION
);
$jobIds = $jobsToSync->map_to_property( 'translate_job_id' );
// $isCompletedButNotDownloaded :: int->bool
$isCompletedButNotDownloaded = pipe(
[ ReturnedJobsQueue::class, 'getStatus' ],
Relation::equals( ReturnedJobsQueue::STATUS_COMPLETED )
);
wp_localize_script(
self::JS_HANDLER,
self::JS_VARIABLE,
[
'jobIds' => $jobIds,
'completedInATE' => Fns::filter( $isCompletedButNotDownloaded, $jobIds ),
'strings' => [
'tooltip' => __(
'Processing translation (could take a few minutes)',
'wpml-translation-management'
),
'status' => __( 'Processing translation', 'wpml-translation-management' ),
],
]
);
wp_enqueue_script( self::JS_HANDLER );
$this->script_factory->localize_script( self::JS_HANDLER );
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Post_Edit_Actions_Factory implements IWPML_Backend_Action_Loader {
/**
* @return IWPML_Action|IWPML_Action[]|null
*/
public function create() {
$tm_ate = new WPML_TM_ATE();
$endpoints = WPML\Container\make( 'WPML_TM_ATE_AMS_Endpoints' );
if ( $tm_ate->is_translation_method_ate_enabled() ) {
return new WPML_TM_ATE_Post_Edit_Actions( $endpoints );
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Post_Edit_Actions implements IWPML_Action {
private $endpoints;
/**
* WPML_TM_ATE_Jobs_Actions constructor.
*
* @param WPML_TM_ATE_AMS_Endpoints $endpoints
*/
public function __construct( WPML_TM_ATE_AMS_Endpoints $endpoints ) {
$this->endpoints = $endpoints;
}
public function add_hooks() {
add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
}
public function allowed_redirect_hosts( $hosts ) {
$hosts[] = $this->endpoints->get_AMS_host();
$hosts[] = $this->endpoints->get_ATE_host();
return $hosts;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Required_Actions_Base {
private $ate_enabled;
protected function is_ate_enabled() {
if ( null === $this->ate_enabled ) {
$tm_settings = wpml_get_setting_filter( null, 'translation-management' );
$doc_translation_method = null;
if ( array_key_exists( 'doc_translation_method', $tm_settings ) ) {
$doc_translation_method = $tm_settings['doc_translation_method'];
}
$this->ate_enabled = $doc_translation_method === ICL_TM_TMETHOD_ATE;
}
return $this->ate_enabled;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* \WPML_TM_ATE_Translator_Login factory.
*
* @author OnTheGo Systems
*
* NOTE: This uses the Frontend loader because is_admin() returns false during wp_login
*/
class WPML_TM_ATE_Translator_Login_Factory implements IWPML_Frontend_Action_Loader {
/**
* It returns an instance of WPML_TM_ATE_Translator_Login is ATE is enabled and active.
*
* @return \WPML_TM_ATE_Translator_Logine|\IWPML_Frontend_Action_Loader|null
*/
public function create() {
if ( WPML_TM_ATE_Status::is_enabled_and_activated() ) {
return WPML\Container\make( WPML_TM_ATE_Translator_Login::class );
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Translator_Login implements IWPML_Action {
/** @var WPML_TM_AMS_Translator_Activation_Records */
private $translator_activation_records;
/** @var WPML_Translator_Records */
private $translator_records;
/** @var WPML_TM_AMS_API */
private $ams_api;
public function __construct(
WPML_TM_AMS_Translator_Activation_Records $translator_activation_records,
WPML_Translator_Records $translator_records,
WPML_TM_AMS_API $ams_api
) {
$this->translator_activation_records = $translator_activation_records;
$this->translator_records = $translator_records;
$this->ams_api = $ams_api;
}
public function add_hooks() {
add_action( 'wp_login', array( $this, 'wp_login' ), 10, 2 );
}
public function wp_login( $user_login, $user ) {
if ( $this->translator_records->does_user_have_capability( $user->ID ) ) {
$result = $this->ams_api->is_subscription_activated( $user->user_email );
if ( ! is_wp_error( $result ) ) {
$this->translator_activation_records->set_activated(
$user->user_email,
$result
);
}
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
class WPML_TM_ATE_Translator_Message_Classic_Editor_Factory implements IWPML_Backend_Action_Loader, IWPML_AJAX_Action_Loader {
/**
* @return \WPML_TM_ATE_Translator_Message_Classic_Editor|\IWPML_Action|null
*/
public function create() {
global $wpdb;
if ( $this->is_ajax_or_translation_queue() && $this->is_ate_enabled_and_manager_wizard_completed() && ! $this->is_editing_old_translation_and_te_is_used_for_old_translation() ) {
$email_twig_factory = wpml_tm_get_email_twig_template_factory();
return new WPML_TM_ATE_Translator_Message_Classic_Editor(
new WPML_Translation_Manager_Records(
$wpdb,
wpml_tm_get_wp_user_query_factory(),
wp_roles()
),
wpml_tm_get_wp_user_factory(),
new WPML_TM_ATE_Request_Activation_Email(
new WPML_TM_Email_Notification_View( $email_twig_factory->create() )
)
);
}
return null;
}
/**
* @return bool
*/
private function is_editing_old_translation_and_te_is_used_for_old_translation() {
return array_key_exists( 'job_id', $_GET )
&& filter_var( $_GET['job_id'], FILTER_SANITIZE_STRING )
&& get_option( WPML_TM_Old_Jobs_Editor::OPTION_NAME ) === WPML_TM_Editors::WPML;
}
/**
* @return bool
*/
private function is_ate_enabled_and_manager_wizard_completed() {
return WPML_TM_ATE_Status::is_enabled_and_activated() && (bool) get_option( WPML_TM_Wizard_Options::WIZARD_COMPLETE_FOR_MANAGER, false );
}
/**
* @return bool
*/
private function is_ajax_or_translation_queue() {
return wpml_is_ajax() || WPML_TM_Page::is_translation_queue();
}
}

View File

@@ -0,0 +1,123 @@
<?php
class WPML_TM_ATE_Translator_Message_Classic_Editor implements IWPML_Action {
const ACTION = 'wpml_ate_translator_classic_editor';
const USER_OPTION = 'wpml_ate_translator_classic_editor_minimized';
/** @var WPML_Translation_Manager_Records */
private $translation_manager_records;
/** @var WPML_WP_User_Factory */
private $user_factory;
/** @var WPML_TM_ATE_Request_Activation_Email */
private $activation_email;
public function __construct(
WPML_Translation_Manager_Records $translation_manager_records,
WPML_WP_User_Factory $user_factory,
WPML_TM_ATE_Request_Activation_Email $activation_email
) {
$this->translation_manager_records = $translation_manager_records;
$this->user_factory = $user_factory;
$this->activation_email = $activation_email;
}
public function add_hooks() {
add_action( 'wpml_tm_editor_messages', array( $this, 'classic_editor_message' ) );
add_action( 'wp_ajax_' . self::ACTION, array( $this, 'handle_ajax' ) );
}
public function classic_editor_message() {
$main_message = esc_html__( "This site can use WPML's Advanced Translation Editor, but you did not receive permission to use it. You are still translating with WPML's classic translation editor. Please ask your site's Translation Manager to enable the Advanced Translation Editor for you.", 'wpml-translation-management' );
$learn_more = esc_html__( "Learn more about WPML's Advanced Translation Editor", 'wpml-translation-management' );
$short_message = esc_html__( 'Advanced Translation Editor is disabled.', 'wpml-translation-management' );
$more = esc_html__( 'More', 'wpml-translation-management' );
$request_activation = esc_html__( 'Request activation from', 'wpml-translation-management' );
$show_minimized = (bool) $this->user_factory->create_current()->get_option( self::USER_OPTION );
?>
<div
class="notice notice-info otgs-notice js-classic-editor-notice"
data-nonce="<?php echo wp_create_nonce( self::ACTION ); ?>"
data-action="<?php echo self::ACTION; ?>"
<?php
if ( $show_minimized ) {
?>
style="display: none" <?php } ?>
>
<p><?php echo $main_message; ?></p>
<p><a href="#" class="wpml-external-link" target="_blank"><?php echo $learn_more; ?></a></p>
<p>
<a class="button js-request-activation"><?php echo $request_activation; ?></a> <?php $this->output_translation_manager_list(); ?>
</p>
<p class="js-email-sent" style="display: none"></p>
<a class="js-minimize otgs-notice-toggle">
<?php esc_html_e( 'Minimize', 'wpml-translation-management' ); ?>
</a>
</div>
<div
class="notice notice-info otgs-notice js-classic-editor-notice-minimized"
<?php
if ( ! $show_minimized ) {
?>
style="display: none" <?php } ?>
>
<p><?php echo $short_message; ?> <a class="js-maximize"><?php echo $more; ?></a></p>
</div>
<?php
}
private function output_translation_manager_list() {
$translation_managers = $this->translation_manager_records->get_users_with_capability();
?>
<select class="js-translation-managers">
<?php
foreach ( $translation_managers as $translation_manager ) {
$display_name = $translation_manager->user_login . ' (' . $translation_manager->user_email . ')';
?>
<option
value="<?php echo $translation_manager->ID; ?> "><?php echo $display_name; ?></option>
<?php } ?>
</select>
<?php
}
public function handle_ajax() {
if ( wp_verify_nonce( $_POST['nonce'], self::ACTION ) ) {
$current_user = $this->user_factory->create_current();
switch ( $_POST['command'] ) {
case 'minimize':
$current_user->update_option( self::USER_OPTION, true );
wp_send_json_success( array( 'message' => '' ) );
case 'maximize':
$current_user->update_option( self::USER_OPTION, false );
wp_send_json_success( array( 'message' => '' ) );
case 'requestActivation':
$manager = $this->user_factory->create( (int) $_POST['manager'] );
if ( $this->activation_email->send_email( $manager, $current_user ) ) {
$message = sprintf(
esc_html__( 'An email has been sent to %s', 'wpml-translation-management' ),
$manager->user_login
);
} else {
$message = sprintf(
esc_html__( 'Sorry, the email could not be sent to %s for an unknown reason.', 'wpml-translation-management' ),
$manager->user_login
);
}
wp_send_json_success( array( 'message' => $message ) );
}
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
class WPML_TM_Old_Editor_Factory implements IWPML_Backend_Action_Loader, IWPML_AJAX_Action_Loader {
public function create() {
return new WPML_TM_Old_Editor();
}
}

View File

@@ -0,0 +1,50 @@
<?php
class WPML_TM_Old_Editor implements IWPML_Action {
const ACTION = 'icl_ajx_custom_call';
const CUSTOM_AJAX_CALL = 'icl_doc_translation_method';
const NOTICE_ID = 'wpml-translation-management-old-editor';
const NOTICE_GROUP = 'wpml-translation-management';
public function add_hooks() {
add_action( self::ACTION, array( $this, 'handle_custom_ajax_call' ), 10, 2 );
}
public function handle_custom_ajax_call( $call, $data ) {
if ( self::CUSTOM_AJAX_CALL === $call ) {
if ( ! isset( $data[ WPML_TM_Old_Jobs_Editor::OPTION_NAME ] ) ) {
return;
}
$old_editor = $data[ WPML_TM_Old_Jobs_Editor::OPTION_NAME ];
if ( ! in_array( $old_editor, array( WPML_TM_Editors::WPML, WPML_TM_Editors::ATE ), true ) ) {
return;
}
update_option( WPML_TM_Old_Jobs_Editor::OPTION_NAME, $old_editor );
if ( WPML_TM_Editors::WPML === $old_editor && $this->is_ate_enabled_and_manager_wizard_completed() ) {
$text = __( 'You activated the Advanced Translation Editor for this site, but you are updating an old translation. WPML opened the Standard Translation Editor, so you can update this translation. When you translate new content, you\'ll get the Advanced Translation Editor with all its features. To change your settings, go to WPML Settings.', 'sitepress' );
$notice = new WPML_Notice( self::NOTICE_ID, $text, self::NOTICE_GROUP );
$notice->set_css_class_types( 'notice-info' );
$notice->set_dismissible( true );
$notice->add_display_callback( 'WPML_TM_Page::is_translation_editor_page' );
wpml_get_admin_notices()->add_notice( $notice, true );
} else {
wpml_get_admin_notices()->remove_notice( self::NOTICE_GROUP, self::NOTICE_ID );
}
}
}
/**
* @return bool
*/
private function is_ate_enabled_and_manager_wizard_completed() {
return WPML_TM_ATE_Status::is_enabled_and_activated() && (bool) get_option( WPML_TM_Wizard_Options::WIZARD_COMPLETE_FOR_MANAGER, false );
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WPML\TM\ATE;
use stdClass;
class JobRecord {
/** @var int $wpmlJobId */
public $wpmlJobId;
/** @var int $ateJobId */
public $ateJobId;
/**
* @todo: Remove this property.
*
* @var int $editTimestamp
*/
public $editTimestamp = 0;
public function __construct( stdClass $dbRow = null ) {
if ( $dbRow ) {
$this->wpmlJobId = (int) $dbRow->job_id;
$this->ateJobId = (int) $dbRow->editor_job_id;
}
}
/**
* @todo: Remove the "$editTimestamp" and "is_editing", not handled on WPML side anymore.
*
* The job is considered as being edited if
* the timestamp is not greater than 1 day.
*
* @return bool
*/
public function isEditing() {
$elapsedTime = time() - $this->editTimestamp;
return $elapsedTime < DAY_IN_SECONDS;
}
}

View File

@@ -0,0 +1,205 @@
<?php
namespace WPML\TM\ATE;
use Exception;
use WPML\Collect\Support\Collection;
use WPML_TM_ATE_API_Error;
use WPML_TM_Editors;
class JobRecords {
const FIELD_ATE_JOB_ID = 'ate_job_id';
const FIELD_IS_EDITING = 'is_editing';
/** @var \wpdb $wpdb */
private $wpdb;
/** @var Collection $jobs */
private $jobs;
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
$this->jobs = wpml_collect( [] );
}
/**
* This method will retrieve data from the ATE job ID.
* Beware of the returned data shape which is not standard.
*
* @param int $ateJobId
*
* @return array|null
*/
public function get_data_from_ate_job_id( $ateJobId ) {
$ateJobId = (int) $ateJobId;
$this->warmCache( [], [ $ateJobId ] );
$job = $this->jobs->first(
function( JobRecord $job ) use ( $ateJobId ) {
return $job->ateJobId === $ateJobId;
}
);
if ( $job ) {
/** @var JobRecord $job */
return [
'wpml_job_id' => $job->wpmlJobId,
'ate_job_data' => [
'ate_job_id' => $job->ateJobId,
],
];
}
return null;
}
/**
* @param int $wpmlJobId
* @param array $ateJobData
*/
public function store( $wpmlJobId, array $ateJobData ) {
$ateJobData['job_id'] = (int) $wpmlJobId;
$this->warmCache( [ $wpmlJobId ] );
$job = $this->jobs->get( $wpmlJobId );
if ( ! $job ) {
$job = new JobRecord();
$job->wpmlJobId = (int) $wpmlJobId;
}
if ( isset( $ateJobData[ self::FIELD_ATE_JOB_ID ] ) ) {
$job->ateJobId = $ateJobData[ self::FIELD_ATE_JOB_ID ];
}
$this->persist( $job );
}
/**
* @param JobRecord $job
*/
public function persist( JobRecord $job ) {
$this->jobs->put( $job->wpmlJobId, $job );
$this->wpdb->update(
$this->wpdb->prefix . 'icl_translate_job',
[ 'editor_job_id' => $job->ateJobId ],
[ 'job_id' => $job->wpmlJobId ],
[ '%d' ],
[ '%d' ]
);
}
/**
* This method will load in-memory the required jobs.
*
* @param array $wpmlJobIds
* @param array $ateJobIds
*/
public function warmCache( array $wpmlJobIds, array $ateJobIds = [] ) {
$wpmlJobIds = wpml_collect( $wpmlJobIds )->reject( $this->isAlreadyLoaded( 'wpmlJobId' ) )->toArray();
$ateJobIds = wpml_collect( $ateJobIds )->reject( $this->isAlreadyLoaded( 'ateJobId' ) )->toArray();
$where = [];
if ( $wpmlJobIds ) {
$where[] = 'job_id IN(' . wpml_prepare_in( $wpmlJobIds, '%d' ) . ')';
}
if ( $ateJobIds ) {
$where[] = 'editor_job_id IN(' . wpml_prepare_in( $ateJobIds, '%d' ) . ')';
}
if ( ! $where ) {
return;
}
$whereHasJobIds = implode( ' OR ', $where );
$rows = $this->wpdb->get_results(
"
SELECT job_id, editor_job_id
FROM {$this->wpdb->prefix}icl_translate_job
WHERE editor = '" . WPML_TM_Editors::ATE . "' AND ({$whereHasJobIds})
"
);
foreach ( $rows as $row ) {
$job = new JobRecord( $row );
$this->jobs->put( $job->wpmlJobId, $job );
}
}
/**
* @param $idPropertyName
*
* @return \Closure
*/
private function isAlreadyLoaded( $idPropertyName ) {
$loadedIds = $this->jobs->pluck( $idPropertyName )->values()->toArray();
return function( $jobId ) use ( $loadedIds ) {
return in_array( $jobId, $loadedIds, true );
};
}
/**
* @param int $wpmlJobId
*
* @return int
*/
public function get_ate_job_id( $wpmlJobId ) {
return $this->get( $wpmlJobId )->ateJobId;
}
/**
* @param int $wpmlJobId
*
* @return bool
*/
public function is_editing_job( $wpmlJobId ) {
return $this->get( $wpmlJobId )->isEditing();
}
/**
* @param $wpmlJobId
*
* @return JobRecord
*/
public function get( $wpmlJobId ) {
if ( ! $this->jobs->has( $wpmlJobId ) ) {
$this->warmCache( [ (int) $wpmlJobId ] );
}
/** @var null|JobRecord $job */
$job = $this->jobs->get( $wpmlJobId );
if ( ! $job || ! $job->ateJobId ) {
$this->restoreJobDataFromATE( $wpmlJobId );
$job = $this->jobs->get( $wpmlJobId, new JobRecord() );
}
return $job;
}
/**
* This method will try to recover the job data from ATE server,
* and persist it in the local repository.
*
* @param int $wpmlJobId
*/
private function restoreJobDataFromATE( $wpmlJobId ) {
$data = apply_filters( 'wpml_tm_ate_job_data_fallback', [], $wpmlJobId );
if ( $data ) {
try {
$this->store( $wpmlJobId, $data );
} catch ( Exception $e ) {
$error_log = new WPML_TM_ATE_API_Error();
$error_log->log( $e->getMessage() );
}
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace WPML\TM\ATE\Log;
class Entry {
/**
* @var int $timestamp The log's creation timestamp.
*/
public $timestamp = 0;
/**
* @see ErrorEvents
*
* @var int $event The event code that triggered the log.
*/
public $event = 0;
/**
* @var string $description The details of the log (e.g. exception message).
*/
public $description = '';
/**
* @var int $wpmlJobId [Optional] The WPML Job ID (when applies).
*/
public $wpmlJobId = 0;
/**
* @var int $ateJobId [Optional] The ATE Job ID (when applies).
*/
public $ateJobId = 0;
/**
* @var array $extraData [Optional] Complementary serialized data (e.g. API request/response data).
*/
public $extraData = [];
/**
* @param array $item
*
* @return Entry
*/
public function __construct( array $item = null ) {
if ( $item ) {
$this->timestamp = (int) $item['timestamp'];
$this->event = (int) $item['event'];
$this->description = $item['description'];
$this->wpmlJobId = (int) $item['wpmlJobId'];
$this->ateJobId = (int) $item['ateJobId'];
$this->extraData = (array) $item['extraData'];
}
}
/**
* @return string
*/
public function getFormattedDate() {
return date_i18n( 'Y/m/d g:i:s A', $this->timestamp );
}
/**
* @return string
*/
public function getEventLabel() {
return wpml_collect(
[
ErrorEvents::SERVER_ATE => 'ATE Server Communication',
ErrorEvents::SERVER_AMS => 'AMS Server Communication',
ErrorEvents::SERVER_XLIFF => 'XLIFF Server Communication',
ErrorEvents::JOB_DOWNLOAD => 'Job Download',
]
)->get( $this->event, '' );
}
/**
* @return string
*/
public function getExtraDataToString() {
return json_encode( $this->extraData );
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace WPML\TM\ATE\Log;
class ErrorEvents {
/** Communication errors */
const SERVER_ATE = 1;
const SERVER_AMS = 2;
const SERVER_XLIFF = 3;
/** Internal errors */
const JOB_DOWNLOAD = 10;
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WPML\TM\ATE\Log;
class Hooks implements \IWPML_Backend_Action, \IWPML_DIC_Action {
const SUBMENU_HANDLE = 'wpml-tm-ate-log';
/** @var ViewFactory $viewFactory */
private $viewFactory;
public function __construct( ViewFactory $viewFactory ) {
$this->viewFactory = $viewFactory;
}
public function add_hooks() {
add_action( 'wpml_support_page_after', [ $this, 'renderSupportSection' ] );
add_action( 'admin_menu', [ $this, 'addLogSubmenuPage' ] );
}
public function renderSupportSection() {
$this->viewFactory->create()->renderSupportSection();
}
public function addLogSubmenuPage() {
add_submenu_page(
WPML_PLUGIN_FOLDER . '/menu/support.php',
__( 'Advanced Translation Editor Error Logs', 'wpml-translation-management' ),
'ATE logs',
'manage_options',
self::SUBMENU_HANDLE,
[ $this, 'renderPage' ]
);
}
public function renderPage() {
$this->viewFactory->create()->renderPage();
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace WPML\TM\ATE\Log;
use WPML\Collect\Support\Collection;
use WPML\WP\OptionManager;
class Storage {
const OPTION_GROUP = 'TM\ATE\Log';
const OPTION_NAME = 'logs';
const MAX_ENTRIES = 50;
/** @var OptionManager $optionManager */
private $optionManager;
public function __construct( OptionManager $optionManager ) {
$this->optionManager = $optionManager;
}
public function add( Entry $entry ) {
$entry->timestamp = $entry->timestamp ?: time();
$entries = $this->getAll();
$entries->prepend( $entry );
$newOptionValue = $entries->forPage( 1, self::MAX_ENTRIES )
->map(
function( Entry $entry ) {
return (array) $entry; }
)
->toArray();
$this->optionManager->set( self::OPTION_GROUP, self::OPTION_NAME, $newOptionValue, false );
}
/**
* @return Collection Collection of Entry objects.
*/
public function getAll() {
return wpml_collect( $this->optionManager->get( self::OPTION_GROUP, self::OPTION_NAME, [] ) )
->map(
function( array $item ) {
return new Entry( $item );
}
);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace WPML\TM\ATE\Log;
use WPML\Collect\Support\Collection;
class View {
/** @var Collection $logs */
private $logs;
public function __construct( Collection $logs ) {
$this->logs = $logs;
}
public function renderSupportSection() {
?>
<div class="wrap">
<h2 id="ate-log">
<?php esc_html_e( 'Advanced Translation Editor', 'wpml-translation-management' ); ?>
</h2>
<p>
<a href="<?php echo admin_url( 'admin.php?page=' . Hooks::SUBMENU_HANDLE ); ?>">
<?php echo sprintf( esc_html__( 'Error Logs (%d)', 'wpml-translation-management' ), $this->logs->count() ); ?>
</a>
</p>
</div>
<?php
}
public function renderPage() {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Advanced Translation Editor Error Logs', 'wpml-translation-management' ); ?></h1>
<br>
<table class="wp-list-table widefat fixed striped posts">
<thead><?php $this->renderTableHeader(); ?></thead>
<tbody id="the-list">
<?php
if ( $this->logs->isEmpty() ) {
$this->renderEmptyTable();
} else {
$this->logs->each( [ $this, 'renderTableRow' ] );
}
?>
</tbody>
<tfoot><?php $this->renderTableHeader(); ?></tfoot>
</table>
</div>
<?php
}
private function renderTableHeader() {
?>
<tr>
<th class="date">
<span><?php esc_html_e( 'Date', 'wpml-translation-management' ); ?></span>
</th>
<th class="event">
<span><?php esc_html_e( 'Event', 'wpml-translation-management' ); ?></span>
</th>
<th class="description">
<span><?php esc_html_e( 'Description', 'wpml-translation-management' ); ?></span>
</th>
<th class="wpml-job-id">
<span><?php esc_html_e( 'WPML Job ID', 'wpml-translation-management' ); ?></span>
</th>
<th class="ate-job-id">
<span><?php esc_html_e( 'ATE Job ID', 'wpml-translation-management' ); ?></span>
</th>
<th class="extra-data">
<span><?php esc_html_e( 'Extra data', 'wpml-translation-management' ); ?></span>
</th>
</tr>
<?php
}
public function renderTableRow( Entry $entry ) {
?>
<tr>
<td class="date">
<?php echo esc_html( $entry->getFormattedDate() ); ?>
</td>
<td class="event">
<?php echo esc_html( $entry->getEventLabel() ); ?>
</td>
<td class="description">
<?php echo esc_html( $entry->description ); ?>
</td>
<td class="wpml-job-id">
<?php echo esc_html( $entry->wpmlJobId ); ?>
</td>
<td class="ate-job-id">
<?php echo esc_html( $entry->ateJobId ); ?>
</td>
<td class="extra-data">
<?php echo esc_html( $entry->getExtraDataToString() ); ?>
</td>
</tr>
<?php
}
private function renderEmptyTable() {
?>
<tr>
<td colspan="6" class="title column-title has-row-actions column-primary">
<?php esc_html_e( 'No entries', 'wpml-translation-management' ); ?>
</td>
</tr>
<?php
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace WPML\TM\ATE\Log;
use function WPML\Container\make;
class ViewFactory {
public function create() {
$logs = make( Storage::class )->getAll();
return new View( $logs );
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* @author OnTheGo Systems
*/
namespace WPML\TM\ATE\REST;
use WP_REST_Request;
use WPML\Collect\Support\Collection;
use function WPML\Container\make;
use WPML\TM\ATE\Download\Job;
use WPML\TM\ATE\Download\Process;
use WPML\TM\Jobs\Utils\ElementLinkFactory;
use WPML\TM\REST\Base;
use WPML_TM_ATE_AMS_Endpoints;
class Download extends Base {
const PROCESS_QUANTITY = 5;
/**
* @return array
*/
public function get_routes() {
return [
[
'route' => WPML_TM_ATE_AMS_Endpoints::DOWNLOAD_JOBS,
'args' => [
'methods' => 'POST',
'callback' => [ $this, 'download' ],
],
],
];
}
/**
* @param WP_REST_Request $request
*
* @return array
*/
public function get_allowed_capabilities( WP_REST_Request $request ) {
return [
'manage_options',
'manage_translations',
'translate',
];
}
public function download() {
$result = make( Process::class )->run( self::PROCESS_QUANTITY );
return [
'jobs' => $this->getJobs( $result->processedJobs ),
'downloadQueueSize' => $result->downloadQueueSize,
];
}
private function getJobs( Collection $processedJobs ) {
$jobIds = $processedJobs->pluck( 'wpmlJobId' );
$viewLinks = $jobIds->map( [ wpml_tm_load_job_factory(), 'get_translation_job' ] )
->map( [ ElementLinkFactory::create(), 'getTranslation' ] );
return $jobIds->zip( $viewLinks )
->map(
function ( $pair ) {
return [
'jobId' => (int) $pair[0],
'viewLink' => $pair[1],
];
}
)
->toArray();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace WPML\TM\ATE\REST;
use WP_REST_Request;
use WPML\Rest\Adaptor;
use WPML\TM\ATE\Sync\Arguments;
use WPML\TM\ATE\Sync\Factory;
use WPML\TM\REST\Base;
use WPML_TM_ATE_AMS_Endpoints;
class Sync extends Base {
/**
* @var Factory $factory
*/
private $factory;
public function __construct( Adaptor $adaptor, Factory $factory ) {
$this->factory = $factory;
parent::__construct( $adaptor );
}
/**
* @return array
*/
public function get_routes() {
return [
[
'route' => WPML_TM_ATE_AMS_Endpoints::SYNC_JOBS,
'args' => [
'methods' => 'POST',
'callback' => [ $this, 'sync' ],
'args' => [
'lockKey' => self::getStringType(),
'ateToken' => self::getStringType(),
'page' => self::getIntType(),
'numberOfPages' => self::getIntType(),
],
],
],
];
}
/**
* @param WP_REST_Request $request
*
* @return array
*/
public function get_allowed_capabilities( WP_REST_Request $request ) {
return [
'manage_options',
'manage_translations',
'translate',
];
}
/**
* @param WP_REST_Request $request
*
* @return array
* @throws \Auryn\InjectionException
*/
public function sync( WP_REST_Request $request ) {
$args = new Arguments();
$args->lockKey = $request->get_param( 'lockKey' );
$args->ateToken = $request->get_param( 'ateToken' );
$args->page = $request->get_param( 'nextPage' );
$args->numberOfPages = $request->get_param( 'numberOfPages' );
return (array) $this->factory->create()->run( $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @author OnTheGo Systems
*/
abstract class WPML_TM_ATE_Required_Rest_Base extends WPML_REST_Base {
const REST_NAMESPACE = 'wpml/tm/v1';
/**
* WPML_TM_ATE_Required_Rest_Base constructor.
*/
public function __construct() {
parent::__construct( self::REST_NAMESPACE );
}
/**
* @param WP_REST_Request $request
*
* @return bool
*/
public function validate_permission( WP_REST_Request $request ) {
return WPML_TM_ATE_Status::is_enabled() && parent::validate_permission( $request );
}
/**
* @param string $endpoint
*
* @return string
*/
static function get_url( $endpoint ) {
return get_rest_url( null, '/' . self::REST_NAMESPACE . $endpoint );
}
}

View File

@@ -0,0 +1,12 @@
<?php
class WPML_TM_REST_AMS_Clients_Factory extends WPML_REST_Factory_Loader {
/**
* @return \WPML_TM_REST_AMS_Clients
* @throws \Auryn\InjectionException
*/
public function create() {
return \WPML\Container\make( '\WPML_TM_REST_AMS_Clients' );
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_REST_AMS_Clients extends WPML_REST_Base {
private $api;
private $ams_user_records;
/** @var WPML_TM_AMS_Translator_Activation_Records $translator_activation_records */
private $translator_activation_records;
/**
* @var WPML_TM_ATE_AMS_Endpoints
*/
private $strings;
public function __construct(
WPML_TM_AMS_API $api,
WPML_TM_AMS_Users $ams_user_records,
WPML_TM_AMS_Translator_Activation_Records $translator_activation_records,
WPML_TM_MCS_ATE_Strings $strings
) {
parent::__construct( 'wpml/tm/v1' );
$this->api = $api;
$this->ams_user_records = $ams_user_records;
$this->translator_activation_records = $translator_activation_records;
$this->strings = $strings;
}
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
'/ams/register_manager',
array(
'methods' => 'POST',
'callback' => array( $this, 'register_manager' ),
)
);
parent::register_route(
'/ams/synchronize/translators',
array(
'methods' => 'GET',
'callback' => array( $this, 'synchronize_translators' ),
)
);
parent::register_route(
'/ams/synchronize/managers',
array(
'methods' => 'GET',
'callback' => array( $this, 'synchronize_managers' ),
)
);
parent::register_route(
'/ams/status',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_status' ),
)
);
parent::register_route(
'/ams/console',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_console' ),
)
);
}
/**
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function register_manager() {
$current_user = wp_get_current_user();
$translators = $this->ams_user_records->get_translators();
$managers = $this->ams_user_records->get_managers();
$result = $this->api->register_manager( $current_user, $translators, $managers );
if ( is_wp_error( $result ) ) {
return $result;
}
return array( 'enabled' => $result );
}
/**
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function synchronize_translators() {
$translators = $this->ams_user_records->get_translators();
$result = $this->api->synchronize_translators( $translators );
if ( is_wp_error( $result ) ) {
return $result;
}
$this->translator_activation_records->update( $result['translators'] );
return array( 'result' => $result );
}
/**
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function synchronize_managers() {
$managers = $this->ams_user_records->get_managers();
$result = $this->api->synchronize_managers( $managers );
if ( is_wp_error( $result ) ) {
return $result;
}
return array( 'result' => $result );
}
/**
* @return array|mixed|null|object|WP_Error
* @throws \InvalidArgumentException
*/
public function get_status() {
return $this->api->get_status();
}
public function get_console() {
return $this->strings->get_auto_login();
}
function get_allowed_capabilities( WP_REST_Request $request ) {
return array( 'manage_translations', 'manage_options' );
}
}

View File

@@ -0,0 +1,12 @@
<?php
class WPML_TM_REST_ATE_API_Factory extends WPML_REST_Factory_Loader {
/**
* @return \WPML_TM_REST_ATE_API
* @throws \Auryn\InjectionException
*/
public function create() {
return \WPML\Container\make( '\WPML_TM_REST_ATE_API' );
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_REST_ATE_API extends WPML_TM_ATE_Required_Rest_Base {
const CAPABILITY_CREATE = 'manage_translations';
const CAPABILITY_READ = 'translate';
private $api;
/**
* WPML_TM_REST_AMS_Clients constructor.
*
* @param WPML_TM_ATE_API $api
*/
public function __construct( WPML_TM_ATE_API $api ) {
parent::__construct();
$this->api = $api;
}
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
'/ate/jobs',
array(
'methods' => 'POST',
'callback' => array( $this, 'create_jobs' ),
)
);
parent::register_route(
'/ate/jobs/(?P<ateJobId>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_job' ),
)
);
}
/**
* @param WP_REST_Request $request
*
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function create_jobs( WP_REST_Request $request ) {
return $this->api->create_jobs( $request->get_params() );
}
/**
* @param WP_REST_Request $request
*
* @return array|WP_Error
* @throws \InvalidArgumentException
*/
public function get_job( WP_REST_Request $request ) {
$ate_job_id = $request->get_param( 'ateJobId' );
return $this->api->get_job( $ate_job_id );
}
function get_allowed_capabilities( WP_REST_Request $request ) {
if ( 'GET' === $request->get_method() ) {
return array( self::CAPABILITY_CREATE, self::CAPABILITY_READ );
}
return self::CAPABILITY_CREATE;
}
}

View File

@@ -0,0 +1,14 @@
<?php
class WPML_TM_REST_ATE_Jobs_Factory extends WPML_REST_Factory_Loader {
public function create() {
$ate_jobs_records = wpml_tm_get_ate_job_records();
$ate_jobs = new WPML_TM_ATE_Jobs( $ate_jobs_records );
return new WPML_TM_REST_ATE_Jobs(
$ate_jobs,
wpml_tm_get_ate_jobs_repository()
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_REST_ATE_Jobs extends WPML_TM_ATE_Required_Rest_Base {
const CAPABILITY = 'manage_translations';
private $ate_jobs;
/** @var WPML_TM_ATE_Job_Repository */
private $job_repository;
/**
* WPML_TM_REST_ATE_Jobs constructor.
*
* @param WPML_TM_ATE_Jobs $ate_jobs
* @param WPML_TM_ATE_Job_Repository $job_repository
*/
public function __construct( WPML_TM_ATE_Jobs $ate_jobs, WPML_TM_ATE_Job_Repository $job_repository ) {
parent::__construct();
$this->ate_jobs = $ate_jobs;
$this->job_repository = $job_repository;
}
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
WPML_TM_ATE_AMS_Endpoints::STORE_JOB,
array(
'methods' => 'POST',
'callback' => array( $this, 'store_ate_job' ),
'args' => array(
'wpml_job_id' => array(
'required' => true,
'type' => 'string',
'validate_callback' => array( 'WPML_REST_Arguments_Validation', 'integer' ),
'sanitize_callback' => array( 'WPML_REST_Arguments_Sanitation', 'integer' ),
),
'ate_job_data' => array(
'required' => true,
'type' => 'array',
),
),
)
);
}
/**
* @param WP_REST_Request $request
*
* @return bool
* @throws \InvalidArgumentException
*/
public function store_ate_job( WP_REST_Request $request ) {
$wpml_job_id = $request->get_param( 'wpml_job_id' );
$ate_job_data = $request->get_param( 'ate_job_data' );
$this->ate_jobs->store( $wpml_job_id, $ate_job_data );
return true;
}
function get_allowed_capabilities( WP_REST_Request $request ) {
return self::CAPABILITY;
}
}

View File

@@ -0,0 +1,15 @@
<?php
class WPML_TM_REST_ATE_Public_Factory extends WPML_REST_Factory_Loader {
public function create() {
$job_actions_factory = new WPML_TM_ATE_Jobs_Actions_Factory();
$jobs_actions = $job_actions_factory->create();
if ( $jobs_actions ) {
return new WPML_TM_REST_ATE_Public( $jobs_actions, wpml_load_core_tm() );
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_REST_ATE_Public extends WPML_TM_ATE_Required_Rest_Base {
const CODE_UNPROCESSABLE_ENTITY = 422;
const CODE_OK = 200;
const ENDPOINT_JOBS_RECEIVE = '/ate/jobs/receive/';
/**
* @var WPML_TM_ATE_Jobs_Actions
*/
private $jobs_actions;
/**
* @var TranslationManagement
*/
private $translation_management;
/**
* @param WPML_TM_ATE_Jobs_Actions $jobs_actions
* @param TranslationManagement $translation_management
*/
public function __construct(
WPML_TM_ATE_Jobs_Actions $jobs_actions,
TranslationManagement $translation_management
) {
parent::__construct();
$this->jobs_actions = $jobs_actions;
$this->translation_management = $translation_management;
}
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
self::ENDPOINT_JOBS_RECEIVE . '(?P<wpmlJobId>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'receive_ate_job' ),
'args' => array(
'wpmlJobId' => array(
'required' => true,
'type' => 'int',
'validate_callback' => array( 'WPML_REST_Arguments_Validation', 'integer' ),
'sanitize_callback' => array( 'WPML_REST_Arguments_Sanitation', 'integer' ),
),
),
'permission_callback' => '__return_true',
)
);
}
public function get_allowed_capabilities( WP_REST_Request $request ) {
return array();
}
/**
* @param WP_REST_Request $request
*
* @return WP_REST_Response|WP_Error
*/
public function receive_ate_job( WP_REST_Request $request ) {
$wpml_job_id = $request->get_param( 'wpmlJobId' );
$wpml_job = $this->translation_management->get_translation_job( $wpml_job_id );
if ( ! $wpml_job ) {
return new WP_Error( self::CODE_UNPROCESSABLE_ENTITY );
}
try {
$this->jobs_actions->update_jobs( false, array( $wpml_job ) );
} catch ( Exception $e ) {
return new WP_Error( self::CODE_UNPROCESSABLE_ENTITY );
}
return new WP_REST_Response( null, self::CODE_OK );
}
/**
* @param int $wpml_job_id
*
* @return string
*/
public static function get_receive_ate_job_url( $wpml_job_id ) {
return self::get_url( self::ENDPOINT_JOBS_RECEIVE . $wpml_job_id );
}
}

View File

@@ -0,0 +1,17 @@
<?php
class WPML_TM_REST_ATE_Sync_Jobs_Factory extends WPML_REST_Factory_Loader {
public function create() {
$jobs_action_factory = new WPML_TM_ATE_Jobs_Actions_Factory();
$jobs_action = $jobs_action_factory->create();
if ( $jobs_action ) {
return new WPML_TM_REST_ATE_Sync_Jobs(
wpml_load_core_tm(),
$jobs_action
);
}
return null;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* @todo: Remove this endpoint
*/
class WPML_TM_REST_ATE_Sync_Jobs extends WPML_TM_ATE_Required_Rest_Base {
/** @var array */
private $capabilities = array( 'manage_translations', 'translate' );
/** @var TranslationManagement */
private $tm_core;
/** @var WPML_TM_ATE_Jobs_Actions */
private $jobs_action;
/**
* @param TranslationManagement $tm_core
* @param WPML_TM_ATE_Jobs_Actions $jobs_action
*/
public function __construct( TranslationManagement $tm_core, WPML_TM_ATE_Jobs_Actions $jobs_action ) {
parent::__construct();
$this->tm_core = $tm_core;
$this->jobs_action = $jobs_action;
}
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
'/ate/jobs/old-sync',
array(
'methods' => 'POST',
'callback' => array( $this, 'sync' ),
'args' => array(
'jobIds' => array(
'required' => true,
'type' => 'array',
'validate_callback' => array( 'WPML_REST_Arguments_Validation', 'is_array' ),
'sanitize_callback' => array( 'WPML_REST_Arguments_Sanitation', 'array_of_integers' ),
),
),
)
);
}
public function sync( WP_REST_Request $request ) {
try {
$ate_jobs = array_map( array( $this->tm_core, 'get_translation_job' ), $request->get_param( 'jobIds' ) );
$updated_jobs = array();
if ( $ate_jobs ) {
$updated_jobs = $this->jobs_action->update_jobs( null, $ate_jobs, true );
}
return $updated_jobs;
} catch ( Exception $e ) {
return new WP_Error( 500, $e->getMessage() );
}
}
function get_allowed_capabilities( WP_REST_Request $request ) {
return $this->capabilities;
}
public function validate_permission( WP_REST_Request $request ) {
if ( current_user_can( 'administrator' ) ) {
return true;
}
return parent::validate_permission( $request );
}
}

View File

@@ -0,0 +1,8 @@
<?php
class WPML_TM_REST_XLIFF_Factory extends WPML_REST_Factory_Loader {
public function create() {
return new WPML_TM_REST_XLIFF();
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_REST_XLIFF extends WPML_TM_ATE_Required_Rest_Base {
const CAPABILITY = 'translate';
function add_hooks() {
$this->register_routes();
}
function register_routes() {
parent::register_route(
'/xliff/fetch/(?P<jobId>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'fetch_xliff' ),
)
);
}
/**
* @param WP_REST_Request $request
*
* @return array
* @throws \InvalidArgumentException
*/
public function fetch_xliff( WP_REST_Request $request ) {
$result = null;
$wpml_translation_job_factory = wpml_tm_load_job_factory();
$iclTranslationManagement = wpml_load_core_tm();
$job_id = $request->get_param( 'jobId' );
$writer = new WPML_TM_Xliff_Writer( $wpml_translation_job_factory );
$xliff = base64_encode( $writer->generate_job_xliff( $job_id ) );
$job = $iclTranslationManagement->get_translation_job( (int) $job_id, false, false, 1 );
$result = array(
'content' => $xliff,
'sourceLang' => $job->source_language_code,
'targetLang' => $job->language_code,
);
return $result;
}
function get_allowed_capabilities( WP_REST_Request $request ) {
return self::CAPABILITY;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace WPML\TM\ATE;
use WPML\FP\Obj;
/**
* Class ReturnedJobsQueue
*
* @package WPML\TM\ATE
*
* IMPORTANT!
* In this class `wpmlJobId` represents job_id column in icl_translate_job
*/
class ReturnedJobsQueue {
const OPTION_NAME = 'ATE_RETURNED_JOBS_QUEUE';
const STATUS_COMPLETED = 'complete';
const STATUS_BACK = 'back';
/**
* @param int $ateJobId
* @param string $status
* @param callable $ateIdToWpmlId @see comment in the class description
*/
public static function add( $ateJobId, $status, callable $ateIdToWpmlId ) {
$wpmlId = $ateIdToWpmlId( $ateJobId );
if ( in_array( $status, [ self::STATUS_BACK, self::STATUS_COMPLETED ] ) && $wpmlId ) {
$options = get_option( self::OPTION_NAME, [] );
$options[ $wpmlId ] = $status;
update_option( self::OPTION_NAME, $options );
}
}
/**
* @param int $wpmlJobId @see comment in the class description
*
* @return string|null
*/
public static function getStatus( $wpmlJobId ) {
return Obj::prop( $wpmlJobId, get_option( self::OPTION_NAME, [] ) );
}
/**
* @param $wpmlJobId @see comment in the class description
*/
public static function remove( $wpmlJobId ) {
$options = get_option( self::OPTION_NAME, [] );
if ( isset( $options[ $wpmlJobId ] ) ) {
unset( $options[ $wpmlJobId ] );
update_option( self::OPTION_NAME, $options );
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace WPML\TM\ATE\Sync;
class Arguments {
/** @var string|null $lockKey */
public $lockKey;
/** @var string|null $ateToken */
public $ateToken;
/** @var int|null $page */
public $page;
/** @var int|null $numberOfPages */
public $numberOfPages;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace WPML\TM\ATE\Sync;
use function WPML\Container\make;
use WPML\Utilities\KeyedLock;
class Factory {
const LOCK_NAME = 'ate_sync';
/**
* @return Process
* @throws \Auryn\InjectionException
*/
public function create() {
$lock = make( KeyedLock::class, [ ':name' => self::LOCK_NAME ] );
return make( Process::class, [ ':lock' => $lock ] );
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace WPML\TM\ATE\Sync;
use WPML\TM\ATE\Download\Job;
use WPML\TM\ATE\Download\Queue;
use WPML\Utilities\KeyedLock;
use WPML_TM_ATE_API;
use WPML_TM_ATE_Job_Repository;
class Process {
const LOCK_RELEASE_TIMEOUT = 1 * MINUTE_IN_SECONDS;
/** @var WPML_TM_ATE_API $api */
private $api;
/** @var KeyedLock $lock */
private $lock;
/** @var WPML_TM_ATE_Job_Repository $ateRepository */
private $ateRepository;
/** @var Queue $downloadQueue */
private $downloadQueue;
/** @var Trigger $trigger */
private $trigger;
public function __construct(
WPML_TM_ATE_API $api,
KeyedLock $lock,
WPML_TM_ATE_Job_Repository $ateRepository,
Queue $downloadQueue,
Trigger $trigger
) {
$this->api = $api;
$this->lock = $lock;
$this->ateRepository = $ateRepository;
$this->downloadQueue = $downloadQueue;
$this->trigger = $trigger;
}
/**
* @param Arguments $args
*
* @return Result
*/
public function run( Arguments $args ) {
$result = new Result();
$result->lockKey = $this->lock->create( $args->lockKey, self::LOCK_RELEASE_TIMEOUT );
if ( $result->lockKey ) {
if ( $args->page ) {
$result = $this->runSyncOnPages( $result, $args );
} else {
$result = $this->runSyncInit( $result );
}
if ( ! $result->nextPage ) {
$result->lockKey = false;
$this->lock->release();
$this->trigger->setLastSync();
}
}
$result->downloadQueueSize = $this->downloadQueue->count();
return $result;
}
/**
* This will run the sync on extra pages.
*
* @param Result $result
* @param Arguments $args
*
* @return Result
*/
private function runSyncOnPages( Result $result, Arguments $args ) {
$apiPage = $args->page - 1; // ATE API pagination starts at 0.
$data = $this->api->sync_page( $args->ateToken, $apiPage );
if ( isset( $data->items ) ) {
$this->pushToDownloadQueue( $data->items );
}
if ( $args->numberOfPages > $args->page ) {
$result->nextPage = $args->page + 1;
$result->numberOfPages = $args->numberOfPages;
$result->ateToken = $args->ateToken;
}
return $result;
}
/**
* This will run the first sync iteration.
* We send all the job IDs we want to sync.
*
* @param Result $result
*
* @return Result
*/
private function runSyncInit( Result $result ) {
$ateJobIds = $this->getAteJobIdsToSync();
if ( $ateJobIds || $this->trigger->isSyncRequired() ) {
$data = $this->api->sync_all( $ateJobIds );
if ( isset( $data->items ) ) {
$this->pushToDownloadQueue( $data->items );
}
if ( isset( $data->edited ) ) {
$this->pushToDownloadQueue( $data->edited );
}
if ( isset( $data->next->pagination_token, $data->next->pages_number ) ) {
$result->ateToken = $data->next->pagination_token;
$result->numberOfPages = $data->next->pages_number;
$result->nextPage = 1; // We start pagination at 1 to avoid carrying a falsy value.
}
}
return $result;
}
/**
* @return array
*/
private function getAteJobIdsToSync() {
return wpml_collect( $this->ateRepository->get_jobs_to_sync()->map_to_property( 'editor_job_id' ) )
->diff( $this->downloadQueue->getEditorJobIds() )
->toArray();
}
/**
* @param \stdClass[] $items
*/
private function pushToDownloadQueue( array $items ) {
$jobs = wpml_collect( $items )->map(
function( $item ) {
return Job::fromAteResponse( $item );
}
);
$this->downloadQueue->push( $jobs );
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace WPML\TM\ATE\Sync;
class Result {
/** @var string|false|null $lockKey */
public $lockKey;
/** @var string|null $ateToken */
public $ateToken;
/** @var int|null $nextPage */
public $nextPage;
/** @var int|null $numberOfPages */
public $numberOfPages;
/** @var int $downloadQueueSize */
public $downloadQueueSize = 0;
}

View File

@@ -0,0 +1,80 @@
<?php
namespace WPML\TM\ATE\Sync;
use function get_current_user_id;
use WPML\Collect\Support\Collection;
use WPML\WP\OptionManager;
class Trigger {
const SYNC_TIMEOUT = 10 * MINUTE_IN_SECONDS;
const OPTION_GROUP = 'WPML\TM\ATE\Sync';
const SYNC_LAST = 'last';
const SYNC_REQUIRED_FOR_USERS = 'required_for_users';
/** @var OptionManager $optionManager */
private $optionManager;
public function __construct( OptionManager $optionManager ) {
$this->optionManager = $optionManager;
}
/**
* @return bool
*/
public function isSyncRequired() {
return $this->isUserSyncRequired() || $this->isPeriodicSyncRequired();
}
/**
* @return bool
*/
private function isPeriodicSyncRequired() {
$lastSync = $this->optionManager->get( self::OPTION_GROUP, self::SYNC_LAST, 0 );
return ( time() - self::SYNC_TIMEOUT ) > $lastSync;
}
/**
* @return bool
*/
private function isUserSyncRequired() {
return $this->getUsersNeedSync()->contains( get_current_user_id() );
}
public function setSyncRequiredForCurrentUser() {
$userId = get_current_user_id();
$usersNeedSync = $this->getUsersNeedSync();
if ( ! $usersNeedSync->contains( $userId ) ) {
$usersNeedSync->push( $userId );
$this->setUsersNeedSync( $usersNeedSync );
}
}
public function setLastSync() {
$this->optionManager->set( self::OPTION_GROUP, self::SYNC_LAST, time(), false );
$currentUserId = get_current_user_id();
$usersNeedSync = $this->getUsersNeedSync();
if ( $usersNeedSync->contains( $currentUserId ) ) {
$isCurrentUser = function( $userId ) use ( $currentUserId ) {
return $userId === $currentUserId;
};
$this->setUsersNeedSync( $usersNeedSync->reject( $isCurrentUser ) );
}
}
/**
* @return Collection
*/
private function getUsersNeedSync() {
return wpml_collect( $this->optionManager->get( self::OPTION_GROUP, self::SYNC_REQUIRED_FOR_USERS, [] ) );
}
private function setUsersNeedSync( Collection $usersNeedSync ) {
$this->optionManager->set( self::OPTION_GROUP, self::SYNC_REQUIRED_FOR_USERS, $usersNeedSync->toArray(), false );
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Used for helping building other factories.
*
* @see Usage.
*
* @author OnTheGo Systems
*/
class WPML_TM_AMS_ATE_Factories {
/**
* It returns an cached instance of \WPML_TM_ATE_API.
*
* @return \WPML_TM_ATE_API
*/
public function get_ate_api() {
return WPML\Container\make( WPML_TM_ATE_API::class );
}
/**
* It returns an cached instance of \WPML_TM_ATE_API.
*
* @return \WPML_TM_AMS_API
*/
public function get_ams_api() {
return WPML\Container\make( WPML_TM_AMS_API::class );
}
/**
* If ATE is active, it returns true.
*
* @return bool
*/
public function is_ate_active() {
if ( ! WPML_TM_ATE_Status::is_active() ) {
try {
$this->get_ams_api()->get_status();
return WPML_TM_ATE_Status::is_active();
} catch ( Exception $ex ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
class WPML_TM_AMS_Translator_Activation_Records {
const USER_META = 'ate_activated';
/** @var WPML_WP_User_Factory $user_factory */
private $user_factory;
public function __construct( WPML_WP_User_Factory $user_factory ) {
$this->user_factory = $user_factory;
}
public function is_activated( $user_email ) {
return $this->is_user_activated( $this->user_factory->create_by_email( $user_email ) );
}
public function is_current_user_activated() {
return $this->is_user_activated( $this->user_factory->create_current() );
}
public function is_user_activated( WPML_User $user ) {
return (bool) $user->get_option( self::USER_META );
}
public function set_activated( $user_email, $state ) {
$user = $this->user_factory->create_by_email( $user_email );
if ( $user->ID ) {
return $user->update_option( self::USER_META, $state );
}
}
public function update( array $translators ) {
foreach ( $translators as $translator ) {
$this->set_activated( $translator['email'], $translator['subscription'] );
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
class WPML_TM_AMS_User_Sync {
/**@var WPML_Translation_Manager_Records */
private $manager_records;
/** @var WPML_Translator_Records */
private $translator_records;
/** @var WPML_Translator_Admin_Records $translator_admin_records */
private $translator_admin_records;
public function __construct(
WPML_Translation_Manager_Records $manager_records,
WPML_Translator_Records $translator_records,
WPML_Translator_Admin_Records $translator_admin_records
) {
$this->manager_records = $manager_records;
$this->translator_records = $translator_records;
$this->translator_admin_records = $translator_admin_records;
}
public function get_translators() {
$translators = $this->translator_records->get_users_with_capability();
$translators = array_merge( $translators, $this->get_admins_that_are_not_translators() );
return $translators;
}
public function get_managers() {
return $this->manager_records->get_users_with_capability();
}
private function get_admins_that_are_not_translators() {
return $this->translator_admin_records->search_for_users_without_capability();
}
}

View File

@@ -0,0 +1,38 @@
<?php
class WPML_TM_AMS_Users {
/**@var WPML_Translation_Manager_Records */
private $manager_records;
/** @var WPML_Translator_Records */
private $translator_records;
/** @var WPML_Translator_Admin_Records $translator_admin_records */
private $translator_admin_records;
public function __construct(
WPML_Translation_Manager_Records $manager_records,
WPML_Translator_Records $translator_records,
WPML_Translator_Admin_Records $translator_admin_records
) {
$this->manager_records = $manager_records;
$this->translator_records = $translator_records;
$this->translator_admin_records = $translator_admin_records;
}
public function get_translators() {
$translators = $this->translator_records->get_users_with_capability();
$translators = array_merge( $translators, $this->get_admins_that_are_not_translators() );
return $translators;
}
public function get_managers() {
return $this->manager_records->get_users_with_capability();
}
private function get_admins_that_are_not_translators() {
return $this->translator_admin_records->search_for_users_without_capability();
}
}

View File

@@ -0,0 +1,346 @@
<?php
/**
* @author OnTheGo Systems
*
* AMS: https://git.onthegosystems.com/ate/ams/wikis/home
* ATE: https://git.onthegosystems.com/ate/ams/wikis/home (https://bitbucket.org/emartini_crossover/ate/wiki/browse/API/V1/jobs)
*/
class WPML_TM_ATE_AMS_Endpoints {
const AMS_BASE_URL = 'https://ams.wpml.org';
const ATE_BASE_URL = 'https://ate.wpml.org';
const ATE_JOB_STATUS_CREATED = 0;
const ATE_JOB_STATUS_TRANSLATING = 1;
const ATE_JOB_STATUS_TRANSLATED = 6;
const ATE_JOB_STATUS_DELIVERING = 7;
const ATE_JOB_STATUS_DELIVERED = 8;
/**
* AMS
*/
const ENDPOINTS_AUTO_LOGIN = '/panel/autologin';
const ENDPOINTS_CLIENTS = '/api/wpml/clients';
const ENDPOINTS_CONFIRM = '/api/wpml/jobs/confirm';
const ENDPOINTS_EDITOR = '/api/wpml/jobs/{job_id}/open?translator={translator_email}&return_url={return_url}';
const ENDPOINTS_SUBSCRIPTION = '/api/wpml/websites/translators/{translator_email}/enable';
const ENDPOINTS_SUBSCRIPTION_STATUS = '/api/wpml/websites/{WEBSITE_UUID}/translators/{translator_email}';
const ENDPOINTS_WEBSITES = '/api/wpml/websites';
/**
* AMS CLONED SITES
*/
const ENDPOINTS_SITE_COPY = '/api/wpml/websites/copy';
const ENDPOINTS_SITE_MOVE = '/api/wpml/websites/move';
const ENDPOINTS_SITE_CONFIRM = '/api/wpml/websites/confirm';
/**
* ATE
*/
const ENDPOINTS_JOB = '/api/wpml/job';
const ENDPOINTS_JOBS = '/api/wpml/jobs';
const ENDPOINT_JOBS_BY_WPML_JOB_IDS = '/api/wpml/jobs/wpml';
const ENDPOINTS_MANAGERS = '/api/wpml/websites/translation_managers';
const ENDPOINTS_SITE = '/api/wpml/websites/create_unique';
const ENDPOINTS_STATUS = '/api/wpml/access_keys/{SHARED_KEY}/status';
const ENDPOINTS_TRANSLATORS = '/api/wpml/websites/translators';
const ENDPOINT_SOURCE_ID_MIGRATION = '/api/wpml/migration';
const ENDPOINTS_SYNC_ALL = '/api/wpml/sync/all';
const ENDPOINTS_SYNC_PAGE = '/api/wpml/sync/page';
const ENDPOINTS_CLONE_JOB = '/api/wpml/jobs/%s/clone';
const SERVICE_AMS = 'ams';
const SERVICE_ATE = 'ate';
const STORE_JOB = '/ate/jobs/store';
const SYNC_JOBS = '/ate/jobs/sync';
const DOWNLOAD_JOBS = '/ate/jobs/download';
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_auto_login() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_AUTO_LOGIN );
}
/**
* @param string $service
* @param string $endpoint
* @param array|null $query_string
*
* @return string
* @throws \InvalidArgumentException
*/
public function get_endpoint_url( $service, $endpoint, array $query_string = null ) {
$url = $this->get_base_url( $service ) . $endpoint;
if ( $query_string ) {
$url_parts = wp_parse_url( $url );
$query = array();
if ( array_key_exists( 'query', $url_parts ) ) {
parse_str( $url_parts['query'], $query );
}
foreach ( $query_string as $key => $value ) {
if ( $value ) {
if ( is_scalar( $value ) ) {
$query[ $key ] = $value;
} else {
$query[ $key ] = implode( ',', $value );
}
}
}
$url_parts['query'] = http_build_query( $query );
$url = http_build_url( $url_parts );
}
return $url;
}
/**
* @param $service
*
* @return string
* @throws \InvalidArgumentException
*/
public function get_base_url( $service ) {
switch ( $service ) {
case self::SERVICE_AMS:
return $this->get_AMS_base_url();
case self::SERVICE_ATE:
return $this->get_ATE_base_url();
default:
throw new InvalidArgumentException( $service . ' is not a valid argument' );
}
}
private function get_AMS_base_url() {
return $this->get_service_base_url( self::SERVICE_AMS );
}
private function get_ATE_base_url() {
return $this->get_service_base_url( self::SERVICE_ATE );
}
private function get_service_base_url( $service ) {
$constant_name = strtoupper( $service ) . '_BASE_URL';
$url = constant( __CLASS__ . '::' . $constant_name );
if ( defined( $constant_name ) ) {
$url = constant( $constant_name );
}
if ( getenv( $constant_name ) ) {
$url = getenv( $constant_name );
}
return $url;
}
public function get_AMS_host() {
return $this->get_service_host( self::SERVICE_AMS );
}
public function get_ATE_host() {
return $this->get_service_host( self::SERVICE_ATE );
}
private function get_service_host( $service ) {
$base_url = $this->get_service_base_url( $service );
$url_parts = wp_parse_url( $base_url );
return $url_parts['host'];
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_register_client() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SITE );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_status() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_STATUS );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_synchronize_managers() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_MANAGERS );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_synchronize_translators() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_TRANSLATORS );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_site_copy() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SITE_COPY );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_site_move() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SITE_MOVE );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ams_site_confirm() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SITE_CONFIRM );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_enable_subscription() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SUBSCRIPTION );
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_subscription_status() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_SUBSCRIPTION_STATUS );
}
/**
* @param int|string|array $job_params
*
* @return string
* @throws \InvalidArgumentException
*/
public function get_ate_confirm_job( $job_params = null ) {
$job_id_part = $this->parse_job_params( $job_params );
return $this->get_endpoint_url( self::SERVICE_ATE, self::ENDPOINTS_CONFIRM . $job_id_part );
}
/**
* @param null|int|string|array $job_params
*
* @return string
*/
private function parse_job_params( $job_params ) {
$job_id_part = '';
if ( $job_params ) {
if ( is_array( $job_params ) ) {
$job_ids = implode( ',', $job_params );
} else {
$job_ids = $job_params;
}
$job_id_part = '/' . $job_ids;
}
return $job_id_part;
}
/**
* @return string
* @throws \InvalidArgumentException
*/
public function get_ate_editor() {
return $this->get_endpoint_url( self::SERVICE_ATE, self::ENDPOINTS_EDITOR );
}
/**
* @param null|int|string|array $job_params
* @param null|array $statuses
*
* @return string
* @throws \InvalidArgumentException
*/
public function get_ate_jobs( $job_params = null, array $statuses = null ) {
$job_id_part = $this->parse_job_params( $job_params );
return $this->get_endpoint_url( self::SERVICE_ATE,
self::ENDPOINTS_JOBS . $job_id_part,
array( 'status' => $statuses ) );
}
/**
* @param int() $job_ids
*
* @return string
*/
public function get_ate_jobs_by_wpml_job_ids( $job_ids ) {
return $this->get_endpoint_url( self::SERVICE_ATE,
self::ENDPOINT_JOBS_BY_WPML_JOB_IDS,
array(
'site_identifier' => wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE ),
'wpml_job_ids' => $job_ids,
) );
}
/**
* @return string
*/
public function get_websites() {
return $this->get_endpoint_url( self::SERVICE_AMS, self::ENDPOINTS_WEBSITES );
}
/**
* @return string
*/
public function get_source_id_migration() {
return $this->get_endpoint_url( self::SERVICE_ATE, self::ENDPOINT_SOURCE_ID_MIGRATION );
}
/**
* @return string
*/
public function get_sync_all() {
return $this->get_endpoint_url( self::SERVICE_ATE, self::ENDPOINTS_SYNC_ALL );
}
/**
* @param string $paginationToken
* @param int $page
*
* @return string
*/
public function get_sync_page( $paginationToken, $page ) {
return $this->get_endpoint_url(
self::SERVICE_ATE,
self::ENDPOINTS_SYNC_PAGE,
[
'pagination_token' => $paginationToken,
'page' => $page,
]
);
}
/**
* @param int $job_id
*
* @return string
*/
public function get_clone_job( $job_id ) {
return $this->get_endpoint_url(
self::SERVICE_ATE,
sprintf( self::ENDPOINTS_CLONE_JOB, $job_id )
);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Job {
const ATE_JOB_CREATED = 0;
const ATE_JOB_IN_PROGRESS = 1;
const ATE_JOB_TRANSLATED = 6;
const ATE_JOB_DELIVERING = 7;
const ATE_JOB_DELIVERED = 8;
}

View File

@@ -0,0 +1,191 @@
<?php
use WPML\TM\ATE\JobRecords;
use function WPML\FP\pipe;
use function WPML\FP\partialRight;
use WPML\FP\Obj;
use WPML\FP\Logic;
use WPML\FP\Fns;
use function \WPML\FP\invoke;
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Jobs {
/** @var JobRecords $records */
private $records;
/**
* WPML_TM_ATE_Jobs constructor.
*
* @param JobRecords $records
*/
public function __construct( JobRecords $records ) {
$this->records = $records;
}
/**
* @param int $wpml_job_id
*
* @return int
*/
public function get_ate_job_id( $wpml_job_id ) {
$wpml_job_id = (int) $wpml_job_id;
return $this->records->get_ate_job_id( $wpml_job_id );
}
/**
* @param int $ate_job_id
*
* @return int
*/
public function get_wpml_job_id( $ate_job_id ) {
$ate_job_id = (int) $ate_job_id;
$ate_job = $this->records->get_data_from_ate_job_id( $ate_job_id );
$wpml_job_id = null;
if ( array_key_exists( 'wpml_job_id', $ate_job ) ) {
$wpml_job_id = (int) $ate_job['wpml_job_id'];
}
return $wpml_job_id;
}
/**
* @param int $wpml_job_id
* @param array $ate_job_data
*/
public function store( $wpml_job_id, $ate_job_data ) {
$this->records->store( (int) $wpml_job_id, $ate_job_data );
}
/**
* We update the status from ATE only for non-completed ATE statuses
* in all other cases, we mark the job as completed when we receive it
* from ATE in `WPML_TM_ATE_Jobs::apply` which calls `wpml_tm_save_data`.
*
* @param int $wpml_job_id
* @param int $ate_status
*/
public function set_wpml_status_from_ate( $wpml_job_id, $ate_status ) {
$ate_status = (int) $ate_status;
switch ( $ate_status ) {
case WPML_TM_ATE_AMS_Endpoints::ATE_JOB_STATUS_CREATED:
$wpml_status = ICL_TM_WAITING_FOR_TRANSLATOR;
break;
case WPML_TM_ATE_AMS_Endpoints::ATE_JOB_STATUS_TRANSLATING:
$wpml_status = ICL_TM_IN_PROGRESS;
break;
default:
$wpml_status = null;
}
if ( $wpml_status ) {
WPML_TM_Update_Translation_Status::by_job_id( $wpml_job_id, (int) $wpml_status );
}
}
/**
* @todo: Check possible duplicated code / We already have functionality to import XLIFF files from Translator's queue
*
* @param string $xliff
*
* @return bool|int
* @throws \Requests_Exception|Exception
*/
public function apply( $xliff ) {
$factory = wpml_tm_load_job_factory();
$xliff_factory = new WPML_TM_Xliff_Reader_Factory( $factory );
$xliff_reader = $xliff_factory->general_xliff_reader();
$job_data = $xliff_reader->get_data( $xliff );
if ( is_wp_error( $job_data ) ) {
throw new Requests_Exception( $job_data->get_error_message(), $job_data->get_error_code() );
}
kses_remove_filters();
$job_data = $this->filterJobData( $job_data );
$wpml_job_id = $job_data['job_id'];
try {
$is_saved = wpml_tm_save_data( $job_data, false );
} catch ( Exception $e ) {
throw new Exception(
'The XLIFF file could not be applied to the content of the job ID: ' . $wpml_job_id,
$e->getCode()
);
}
kses_init();
return $is_saved ? $wpml_job_id : false;
}
private function filterJobData( $jobData ) {
/**
* It lets modify $job_data, which is especially usefull when we want to alter `data` of field.
*
* @param array $jobData {
* @type int $job_id
* @type array fields {
* @type string $data Translated content
* @type int $finished
* @type int $tid
* @type string $field_type
* @type string $format
* }
* @type int $complete
* }
* @param callable $getJobTargetLanguage The callback which expects $jobId as parameter
*
* @since 2.10.0
*/
$filteredJobData = apply_filters(
'wpml_tm_ate_job_data_from_xliff',
$jobData,
$this->getJobTargetLanguage()
);
if ( array_key_exists( 'job_id', $filteredJobData ) && array_key_exists( 'fields', $filteredJobData ) ) {
$jobData = $filteredJobData;
}
return $jobData;
}
/**
* getJobTargetLanguage :: void → ( object → string|null )
*
* @return callback
*/
private function getJobTargetLanguage() {
// $getJobEntityById :: int -> \WPML_TM_Job_Entity|false
$getJobEntityById = partialRight( [ wpml_tm_get_jobs_repository(), 'get_job' ], \WPML_TM_Job_Entity::POST_TYPE );
// $getTargetLangIfEntityExists :: \WPML_TM_Job_Entity|false -> string|null
$getTargetLangIfEntityExists = Logic::ifElse( Fns::identity(), invoke( 'get_target_language' ), Fns::always( null ) );
return pipe( Obj::prop( 'rid' ), $getJobEntityById, $getTargetLangIfEntityExists );
}
/**
* @param int $wpml_job_id
*
* @return bool
*/
public function is_editing_job( $wpml_job_id ) {
return $this->records->is_editing_job( $wpml_job_id );
}
/**
* @param array $wpml_job_ids
*/
public function warm_cache( array $wpml_job_ids ) {
$this->records->warmCache( $wpml_job_ids );
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Status {
public static function is_enabled() {
$tm_settings = wpml_get_setting_filter( null, 'translation-management' );
$doc_translation_method = null;
if ( is_array( $tm_settings ) && array_key_exists( 'doc_translation_method', $tm_settings ) ) {
$doc_translation_method = $tm_settings['doc_translation_method'];
}
return $doc_translation_method === ICL_TM_TMETHOD_ATE;
}
public static function is_active() {
$ams_data = get_option( WPML_TM_ATE_Authentication::AMS_DATA_KEY, array() );
if ( $ams_data && array_key_exists( 'status', $ams_data ) ) {
return $ams_data['status'] === WPML_TM_ATE_Authentication::AMS_STATUS_ACTIVE;
}
return false;
}
public static function is_enabled_and_activated() {
return self::is_enabled() && self::is_active();
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE {
const SITE_ID_SCOPE = 'ate';
private $translation_method_ate_enabled;
/**
* @var WPML_TM_ATE_API
*/
private $tm_ate_api;
/**
* @var WPML_TM_ATE_Jobs
*/
private $tm_ate_jobs;
public function is_translation_method_ate_enabled() {
if ( null === $this->translation_method_ate_enabled ) {
$tm_settings = wpml_get_setting_filter( null, 'translation-management' );
$doc_translation_method = null;
if ( $tm_settings && array_key_exists( 'doc_translation_method', $tm_settings ) ) {
$doc_translation_method = $tm_settings['doc_translation_method'];
}
$this->translation_method_ate_enabled = $doc_translation_method === ICL_TM_TMETHOD_ATE;
}
return $this->translation_method_ate_enabled;
}
/**
* @param int $trid
* @param string $language
*
* @return bool
*/
public function is_translation_ready_for_post( $trid, $language ) {
$translation_status_id = $this->get_translation_status_id_for_post( $trid, $language );
return $translation_status_id && ! in_array( $translation_status_id, array( WPML_TM_ATE_Job::ATE_JOB_CREATED, WPML_TM_ATE_Job::ATE_JOB_IN_PROGRESS ), true );
}
/**
* @param int $trid
* @param string $language
*
* @return int|bool
*/
public function get_translation_status_id_for_post( $trid, $language ) {
$status_id = false;
$ate_job = $this->get_job_data_for_post( $trid, $language );
if ( $ate_job && ! is_wp_error( $ate_job ) ) {
$status_id = $ate_job->status_id;
}
return $status_id;
}
/**
* @param int $trid
* @param string $language
*
* @return array|WP_Error
*/
public function get_job_data_for_post( $trid, $language ) {
$tm_ate_api = $this->get_tm_ate_api();
$tm_ate_jobs = $this->get_tm_ate_jobs();
$core_tm = wpml_load_core_tm();
$job_id = $core_tm->get_translation_job_id( $trid, $language );
$editor = $core_tm->get_translation_job_editor( $trid, $language );
if ( \WPML_TM_Editors::ATE !== strtolower( $editor ) ) {
return null;
}
$ate_job_id = $tm_ate_jobs->get_ate_job_id( $job_id );
$ate_job = $tm_ate_api->get_job( $ate_job_id );
return isset( $ate_job->$ate_job_id ) ? $ate_job->$ate_job_id : $ate_job;
}
/**
* @return WPML_TM_ATE_API
*/
private function get_tm_ate_api() {
if ( null === $this->tm_ate_api ) {
$ams_ate_factories = wpml_tm_ams_ate_factories();
$this->tm_ate_api = $ams_ate_factories->get_ate_api();
}
return $this->tm_ate_api;
}
/**
* @return WPML_TM_ATE_Jobs
*/
private function get_tm_ate_jobs() {
if ( null === $this->tm_ate_jobs ) {
$ate_jobs_records = wpml_tm_get_ate_job_records();
$this->tm_ate_jobs = new WPML_TM_ATE_Jobs( $ate_jobs_records );
}
return $this->tm_ate_jobs;
}
}

View File

@@ -0,0 +1,36 @@
<?php
use function WPML\FP\invoke;
class WPML_TM_ATE_Job_Repository {
/** @var WPML_TM_Jobs_Repository */
private $job_repository;
public function __construct( WPML_TM_Jobs_Repository $job_repository ) {
$this->job_repository = $job_repository;
}
/**
* @return WPML_TM_Jobs_Collection
*/
public function get_jobs_to_sync() {
$search_params = new WPML_TM_Jobs_Search_Params();
$search_params->set_scope( WPML_TM_Jobs_Search_Params::SCOPE_LOCAL );
$search_params->set_status( self::get_in_progress_statuses() );
$search_params->set_job_types( [
WPML_TM_Job_Entity::POST_TYPE,
WPML_TM_Job_Entity::PACKAGE_TYPE,
WPML_TM_Job_Entity::STRING_BATCH,
] );
return $this->job_repository
->get( $search_params )
->filter( invoke( 'is_ate_job' ) );
}
/** @return array */
public static function get_in_progress_statuses() {
return array( ICL_TM_WAITING_FOR_TRANSLATOR, ICL_TM_IN_PROGRESS );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Models_Job_Create {
/** @var int */
public $deadline;
/** @var WPML_TM_ATE_Models_Job_File */
public $file;
/** @var bool */
public $notify_enabled;
/** @var string */
public $notify_url;
/** @var int */
public $source_id;
/** @var string */
public $permalink;
/** @var string */
public $site_identifier;
/** @var WPML_TM_ATE_Models_Language */
public $source_language;
/** @var WPML_TM_ATE_Models_Language */
public $target_language;
/** @var string */
public $ate_ams_console_url;
/**
* WPML_TM_ATE_Models_Job_Create constructor.
*
* @param array $args
*
* @throws \Auryn\InjectionException
*/
public function __construct( array $args = array() ) {
foreach ( $args as $key => $value ) {
$this->$key = $value;
}
if ( ! $this->file ) {
$this->file = new WPML_TM_ATE_Models_Job_File();
}
if ( ! $this->source_language ) {
$this->source_language = new WPML_TM_ATE_Models_Language();
}
if ( ! $this->target_language ) {
$this->target_language = new WPML_TM_ATE_Models_Language();
}
$this->ate_ams_console_url = wpml_tm_get_ams_ate_console_url();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Models_Job_File {
public $content;
public $name;
public $type;
/**
* WPML_TM_ATE_Models_Job_File constructor.
*
* @param array $args
*/
public function __construct( array $args = array() ) {
foreach ( $args as $key => $value ) {
$this->$key = $value;
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_ATE_Models_Language {
public $code;
public $name;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @author OnTheGo Systems
*/
class WPML_TM_Job_Created {
public $job_id;
public $rid;
public $translation_service;
public $translator_id;
public $translation_package;
public $batch_options;
public $data;
/**
* WPML_TM_Job_Created constructor.
*
* @param array $args
*/
public function __construct( array $args ) {
foreach ( $args as $key => $value ) {
$this->$key = $value;
}
}
}