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 );
}
}