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,137 @@
<?php
namespace WPML\TM\Editor;
use WPML\FP\Str;
use WPML\FP\Cast;
use WPML\FP\Obj;
use WPML\FP\Relation;
use WPML\LIB\WP\Option;
use WPML\TM\ATE\ClonedSites\ApiCommunication;
use WPML\UIPage;
use function WPML\FP\pipe;
class ATEDetailedErrorMessage {
const ERROR_DETAILS_OPTION = 'wpml_ate_error_details';
/**
* Parses error data and saves it to options table.
*
* @param $errorResponse
*
* @return void
*/
public static function saveDetailedError( $errorResponse ) {
$errorCode = $errorResponse->get_error_code();
$errorMessage = $errorResponse->get_error_message();
$errorData = $errorResponse->get_error_data( $errorCode );
$errorDetails = [
'code' => $errorCode,
'message' => $errorMessage,
'error_data' => $errorData,
];
self::saveErrorDetailsInOptions( $errorDetails );
}
/**
* Returns single or multiple formatted error message depending on errors array existence in response.
*
* @param string $appendText
*
* @return string|null
*/
public static function readDetailedError( $appendText = null ) {
$errorDetails = Option::getOr( self::ERROR_DETAILS_OPTION, [] );
$detailedError = self::hasValidExplainedMessage( $errorDetails )
? self::formattedDetailedErrors( $errorDetails, $appendText )
: (
self::isSiteMigrationError( $errorDetails )
? self::formattedSiteMigrationError( $errorDetails )
: null
);
self::deleteErrorDetailsFromOptions(); // deleting the option after message is ready to be displayed.
return $detailedError;
}
/**
* Checks if valid explained message exists in error response.
*
* @param array $errorDetails
*
* @return mixed
*/
private static function hasValidExplainedMessage( $errorDetails ) {
$hasExplainedMessage = pipe(
Obj::path( [ 'error_data', 0, 'explained_message' ] ),
Str::len(),
Cast::toBool()
);
return $hasExplainedMessage( $errorDetails );
}
/**
* Checks if error is "Site moved or copied" (Happens when error code is 426)
*
* @param array $errorDetails
*
* @return mixed
*/
private static function isSiteMigrationError( $errorDetails ) {
$isSiteMigrationError = pipe(
Obj::prop( 'code' ),
Cast::toInt(),
Relation::equals( ApiCommunication::SITE_CLONED_ERROR )
);
return $isSiteMigrationError( $errorDetails );
}
/**
* The purpose of this function is to avoid the case when redirect is happened and data saved in this static class is lost
*
* @return void
*/
private static function saveErrorDetailsInOptions( $errorDetails ) {
Option::updateWithoutAutoLoad( self::ERROR_DETAILS_OPTION, $errorDetails );
}
/**
* Deletes the error details from options.
*
* @return void
*/
private static function deleteErrorDetailsFromOptions() {
Option::delete( self::ERROR_DETAILS_OPTION );
}
/**
* Returns multiple formatted error messages from errors array.
*
* @param array $errorDetails
* @param string $appendText
*
* @return string
*/
private static function formattedDetailedErrors( array $errorDetails, $appendText ) {
$appendText = $appendText ? '<div>' . $appendText . '</div>' : '';
$allErrors = '<div>';
foreach ( Obj::prop( 'error_data', $errorDetails ) as $error ) {
$allErrors .= '<div>' . Obj::propOr( '', 'explained_message', $error ) . '</div>';
}
return $allErrors . $appendText . '</div>';
}
private static function formattedSiteMigrationError( $errorDetails ) {
return '<div>' . Obj::prop( 'message', $errorDetails ) . '</div>';
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace WPML\TM\Editor;
use WPML\LIB\WP\Option;
class ATERetry {
/**
* @param int $jobId
*
* @return bool
*/
public static function hasFailed( $jobId ) {
return self::getCount( $jobId ) >= 0;
}
/**
* @param int $jobId
*
* @return int
*/
public static function getCount( $jobId ) {
return (int) Option::getOr( self::getOptionName( $jobId ), - 1 );
}
/**
* @param int $jobId
*/
public static function incrementCount( $jobId ) {
Option::update( self::getOptionName( $jobId ), self::getCount( $jobId ) + 1 );
}
/**
* @param int $jobId
*/
public static function reset( $jobId ) {
Option::delete( self::getOptionName( $jobId ) );
}
/**
* @param int $jobId
*
* @return string
*/
public static function getOptionName( $jobId ) {
return sprintf( 'wpml-ate-job-retry-counter-%d', $jobId );
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace WPML\TM\Editor;
class ClassicEditorActions {
public function addHooks() {
add_action( 'wp_ajax_wpml_save_job_ajax', [ $this, 'saveJob' ] );
}
public function saveJob() {
if ( ! wpml_is_action_authenticated( 'wpml_save_job' ) ) {
wp_send_json_error( 'Permission denied.' );
return;
}
$data = [];
$post_data = \WPML_TM_Post_Data::strip_slashes_for_single_quote( $_POST['data'] );
parse_str( $post_data, $data );
/**
* It filters job data
*
* @param array $data
*/
$data = apply_filters( 'wpml_translation_editor_save_job_data', $data );
$job = \WPML\Container\make( \WPML_TM_Editor_Job_Save::class );
$job_details = [
'job_type' => $data['job_post_type'],
'job_id' => $data['job_post_id'],
'target' => $data['target_lang'],
'translation_complete' => isset( $data['complete'] ) ? true : false,
];
$job = apply_filters( 'wpml-translation-editor-fetch-job', $job, $job_details );
$ajax_response = $job->save( $data );
$ajax_response->send_json();
}
}

View File

@@ -0,0 +1,348 @@
<?php
namespace WPML\TM\Editor;
use WPML\FP\Either;
use WPML\FP\Fns;
use WPML\FP\Left;
use WPML\FP\Logic;
use WPML\FP\Obj;
use WPML\FP\Relation;
use WPML\FP\Right;
use WPML\LIB\WP\User;
use WPML\Setup\Option as SetupOption;
use WPML\TM\API\Jobs;
use WPML\TM\ATE\Log\Entry;
use WPML\TM\ATE\Log\Storage;
use WPML\TM\ATE\Review\ReviewStatus;
use WPML\TM\ATE\Sync\Trigger;
use WPML\TM\Jobs\Manual;
use WPML\TM\Menu\TranslationQueue\CloneJobs;
use function WPML\Container\make;
use function WPML\FP\curryN;
use function WPML\FP\invoke;
use function WPML\FP\pipe;
class Editor {
const ATE_JOB_COULD_NOT_BE_CREATED = 101;
const ATE_EDITOR_URL_COULD_NOT_BE_FETCHED = 102;
const ATE_IS_NOT_ACTIVE = 103;
/** @var CloneJobs */
private $clone_jobs;
/** @var Manual */
private $manualJobs;
/**
* Editor constructor.
*
* @param CloneJobs $clone_jobs
* @param Manual $manualJobs
*/
public function __construct( CloneJobs $clone_jobs, Manual $manualJobs ) {
$this->clone_jobs = $clone_jobs;
$this->manualJobs = $manualJobs;
}
/**
* @param array $params
*
* @return array
*/
public function open( $params ) {
$shouldOpenCTE = function ( $jobObject ) use ( $params ) {
if ( ! \WPML_TM_ATE_Status::is_enabled() ) {
return true;
}
if ( $this->isNewJobCreated( $params, $jobObject ) ) {
return wpml_tm_load_old_jobs_editor()->shouldStickToWPMLEditor( $jobObject->get_id() );
}
return wpml_tm_load_old_jobs_editor()->editorForTranslationsPreviouslyCreatedUsingCTE() === \WPML_TM_Editors::WPML &&
wpml_tm_load_old_jobs_editor()->get_current_editor( $jobObject->get_id() ) === \WPML_TM_Editors::WPML;
};
/**
* It maybe needed when a job was translated via the Translation Proxy before and now, we want to open it in the editor.
*
* @param \WPML_Element_Translation_Job $jobObject
*
* @return \WPML_Element_Translation_Job
*/
$maybeUpdateTranslationServiceColumn = function ( $jobObject ) {
if ( $jobObject->get_translation_service() !== 'local' ) {
$jobObject->set_basic_data_property( 'translation_service', 'local' );
Jobs::setTranslationService( $jobObject->get_id(), 'local' );
}
return $jobObject;
};
return Either::of( $params )
->map( [ $this->manualJobs, 'createOrReuse' ] )
->filter( Logic::isTruthy() )
->filter( invoke( 'user_can_translate' )->with( User::getCurrent() ) )
->map( $maybeUpdateTranslationServiceColumn )
->map( Logic::ifElse( $shouldOpenCTE, $this->displayCTE(), $this->tryToDisplayATE( $params ) ) )
->getOrElse( [ 'editor' => \WPML_TM_Editors::NONE, 'jobObject' => null ] );
}
/**
* @param array $params
* @param \WPML_Element_Translation_Job $jobObject
*
* @return array
*/
private function tryToDisplayATE( $params = null, $jobObject = null ) {
$fn = curryN( 2, function ( $params, $jobObject ) {
$handleNotActiveATE = Logic::ifElse(
[ \WPML_TM_ATE_Status::class, 'is_active' ],
Either::of(),
pipe( $this->handleATEJobCreationError( $params, self::ATE_IS_NOT_ACTIVE ), Either::left() )
);
/**
* Create a new ATE job when somebody clicks the "pencil" icon to edit existing translation.
*
* @param \WPML_Element_Translation_Job $jobObject
*
* @return Either<\WPML_Element_Translation_Job>
*/
$cloneCompletedATEJob = function ( $jobObject ) use ( $params ) {
if ( $this->isValidATEJob( $jobObject ) && (int) $jobObject->get_status_value() === ICL_TM_COMPLETE ) {
$sentFrom = isset( $params['preview'] ) ? Jobs::SENT_FROM_REVIEW : Jobs::SENT_MANUALLY;
return $this->clone_jobs->cloneCompletedATEJob( $jobObject, $sentFrom )
->bimap( $this->handleATEJobCreationError( $params, self::ATE_JOB_COULD_NOT_BE_CREATED ), Fns::identity() );
}
return Either::of( $jobObject );
};
$handleMissingATEJob = function ( $jobObject ) use ( $params ) {
// ATE editor is already set. All fine, we can proceed.
if ( $this->isValidATEJob( $jobObject ) ) {
return Either::of( $jobObject );
}
/**
* The new job has been created because either there was no translation at all or translation was "needs update".
* The ATE job could not be created inside WPML_TM_ATE_Jobs_Actions::added_translation_jobs ,and we have to return the error message.
*/
if ( $this->isNewJobCreated( $params, $jobObject ) ) {
return Either::left( $this->handleATEJobCreationError( $params, self::ATE_JOB_COULD_NOT_BE_CREATED, $jobObject ) );
}
/**
* It creates a corresponding job in ATE for already existing WPML job in such situations:
* 1. Previously job was created in CTE, but a user selected the setting to translate existing CTE jobs in ATE
* 2. The job used to be handled by the Translation Proxy or the native WP editor
* 3. ATE job could not be created before and user clicked "Retry" button
* 4. Job was sent via basket and ATE job could not be created
*/
return $this->createATECounterpartForExistingWPMLJob( $params, $jobObject );
};
return Either::of( $jobObject )
->chain( $handleNotActiveATE )
->chain( $cloneCompletedATEJob )
->chain( $handleMissingATEJob )
->map( Fns::tap( pipe( invoke( 'get_id' ), Jobs::setStatus( Fns::__, ICL_TM_IN_PROGRESS ) ) ) )
->map( $this->openATE( $params ) )
->coalesce( Fns::identity(), Fns::identity() )
->get();
} );
return call_user_func_array( $fn, func_get_args() );
}
/**
* @param \WPML_Element_Translation_Job $jobObject
*
* @return array
*/
private function displayCTE( $jobObject = null ) {
$fn = curryN( 1, function ( $jobObject ) {
wpml_tm_load_old_jobs_editor()->set( $jobObject->get_id(), \WPML_TM_Editors::WPML );
return [ 'editor' => \WPML_TM_Editors::WPML, 'jobObject' => $jobObject ];
} );
return call_user_func_array( $fn, func_get_args() );
}
/**
* @param \WPML_Element_Translation_Job $jobObject
*
* @return void
*/
private function maybeSetReviewStatus( $jobObject ) {
if ( Relation::propEq( 'review_status', ReviewStatus::NEEDS_REVIEW, $jobObject->to_array() ) ) {
Jobs::setReviewStatus( $jobObject->get_id(), SetupOption::shouldTranslateEverything() ? ReviewStatus::EDITING : null );
}
}
/**
* It returns an url to place where a user should be redirected. The url contains a job id and error's code.
*
* @param array $params
* @param int $code
* @param \WPML_Element_Translation_Job $jobObject
*
* @return array
*/
private function handleATEJobCreationError( $params = null, $code = null, $jobObject = null ) {
$fn = curryN( 3, function ( $params, $code, $jobObject ) {
ATERetry::incrementCount( $jobObject->get_id() );
$retryCount = ATERetry::getCount( $jobObject->get_id() );
if ( $retryCount > 0 ) {
Storage::add( Entry::retryJob( $jobObject->get_id(),
[
'retry_count' => ATERetry::getCount( $jobObject->get_id() )
]
) );
}
return [
'editor' => \WPML_TM_Editors::ATE,
'url' => add_query_arg( [ 'ateJobCreationError' => $code, 'jobId' => $jobObject->get_id() ], $this->getReturnUrl( $params ) )
];
} );
return call_user_func_array( $fn, func_get_args() );
}
/**
* It asserts a job's editor.
*
* @param string $editor
* @param \WPML_Element_Translation_Job $jobObject
*
* @return bool
*/
private function isJobEditorEqualTo( $editor, $jobObject ) {
return $jobObject->get_basic_data_property( 'editor' ) === $editor;
}
/**
* It checks if we created a new entry in wp_icl_translate_job table.
* It happens when none translation for a specific language has existed so far or when a translation has been "needs update".
*
* @param array $params
* @param \WPML_Element_Translation_Job $jobObject
*
* @return bool
*/
private function isNewJobCreated( $params , $jobObject ) {
return (int) $jobObject->get_id() !== (int) Obj::prop( 'job_id', $params );
}
/**
* @param array $params
* @param \WPML_Element_Translation_Job $jobObject
*
* @return callable|Left<array>|Right<\WPML_Element_Translation_Job>
*/
private function createATECounterpartForExistingWPMLJob( $params, $jobObject ) {
if ( $this->clone_jobs->cloneWPMLJob( $jobObject->get_id() ) ) {
ATERetry::reset( $jobObject->get_id() );
$jobObject->set_basic_data_property( 'editor', \WPML_TM_Editors::ATE );
return Either::of( $jobObject );
}
return Either::left( $this->handleATEJobCreationError( $params, self::ATE_JOB_COULD_NOT_BE_CREATED, $jobObject ) );
}
/**
* At this stage, we know that a corresponding job in ATE is created and we should open ATE editor.
* We are trying to do that.
*
* @param array $params
* @param \WPML_Element_Translation_Job $jobObject
*
* @return false|mixed
*/
private function openATE( $params = null, $jobObject = null ) {
$fn = curryN( 2, function ( $params, $jobObject ) {
$this->maybeSetReviewStatus( $jobObject );
$editor_url = apply_filters( 'wpml_tm_ate_jobs_editor_url', '', $jobObject->get_id(), $this->getReturnUrl( $params ) );
if ( $editor_url ) {
$response['editor'] = \WPML_TM_Editors::ATE;
$response['url'] = $editor_url;
$response['jobObject'] = $jobObject;
return $response;
}
return $this->handleATEJobCreationError( $params, self::ATE_EDITOR_URL_COULD_NOT_BE_FETCHED, $jobObject );
} );
return call_user_func_array( $fn, func_get_args() );
}
/**
* @return string
*/
private function getReturnUrl( $params ) {
$return_url = '';
if ( array_key_exists( 'return_url', $params ) ) {
$return_url = filter_var( $params['return_url'], FILTER_SANITIZE_URL );
$return_url_parts = wp_parse_url( $return_url );
$admin_url = get_admin_url();
$admin_url_parts = wp_parse_url( $admin_url );
if ( strpos( $return_url_parts['path'], $admin_url_parts['path'] ) === 0 ) {
$admin_url_parts['path'] = $return_url_parts['path'];
} else {
$admin_url_parts = $return_url_parts;
}
$admin_url_parts['query'] = $this->prepareQueryParameters(
Obj::propOr( '', 'query', $return_url_parts ),
Obj::prop( 'lang', $params )
);
$return_url = http_build_url( $admin_url_parts );
}
return $return_url;
}
private function prepareQueryParameters( $query, $returnLanguage ) {
$parameters = [];
parse_str( $query, $parameters );
unset( $parameters['ate_original_id'] );
unset( $parameters['back'] );
unset( $parameters['complete'] );
if ( $returnLanguage ) {
// We need the lang parameter to display the post list in the language which was used before ATE.
$parameters['lang'] = $returnLanguage;
}
return http_build_query( $parameters );
}
/**
* @param \WPML_Element_Translation_Job $jobObject
*
* @return bool
*/
private function isValidATEJob( \WPML_Element_Translation_Job $jobObject ) {
return $this->isJobEditorEqualTo( \WPML_TM_Editors::ATE, $jobObject ) &&
(int) $jobObject->get_basic_data_property( 'editor_job_id' ) > 0;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace WPML\TM\Editor;
use WPML\FP\Cast;
use WPML\FP\Fns;
use WPML\FP\Logic;
use WPML\FP\Obj;
use WPML\FP\Relation;
use WPML\LIB\WP\Hooks;
use WPML\LIB\WP\Option;
use WPML\TM\API\Jobs;
use WPML\UIPage;
use function WPML\Container\make;
use function WPML\FP\pipe;
class ManualJobCreationErrorNotice implements \IWPML_Backend_Action {
const RETRY_LIMIT = 3;
public function add_hooks() {
if ( \WPML_TM_ATE_Status::is_enabled() ) {
Hooks::onAction( 'wp_loaded' )
->then( function () {
/** @var \WPML_Notices $notices */
$notices = make( \WPML_Notices::class );
if ( isset( $_GET['ateJobCreationError'] ) ) {
$notice = $notices->create_notice( __CLASS__, $this->getContent( $_GET ) );
$notice->set_css_class_types( 'error' );
$notice->set_dismissible( false );
$notices->add_notice( $notice );
} else {
$notices->remove_notice( 'default', __CLASS__ );
}
} );
}
}
/**
* @param array $params
*
* @return string
*/
private function getContent( array $params ) {
$isATENotActiveError = pipe( Obj::prop( 'ateJobCreationError' ), Cast::toInt(), Relation::equals( Editor::ATE_IS_NOT_ACTIVE ) );
$isRetryLimitExceeded = pipe( Obj::prop( 'jobId' ), [ ATERetry::class, 'getCount' ], Relation::gt( self::RETRY_LIMIT ) );
return Logic::cond( [
[ $isATENotActiveError, [ self::class, 'ateNotActiveMessage' ] ],
[ $isRetryLimitExceeded, [ self::class, 'retryMessage' ] ],
[ Fns::always( true ), [ self::class, 'retryFailedMessage' ] ]
], $params );
}
public static function retryMessage( array $params ) {
$returnUrl = \remove_query_arg( [ 'ateJobCreationError', 'jobId' ], Jobs::getCurrentUrl() );
$jobEditUrl = Jobs::getEditUrl( $returnUrl, Obj::prop( 'jobId', $params ) );
$fallbackErrorMessage = sprintf(
'<div class="wpml-display-flex wpml-display-flex-center">%1$s <a class="button wpml-margin-left-sm" href="%2$s">%3$s</a></div>',
__( "WPML didn't manage to translate this page.", 'wpml-translation-management' ),
$jobEditUrl,
__( 'Try again', 'wpml-translation-management' )
);
$tryAgainTextLink = sprintf( '<a href="%1$s">%2$s</a>',
$jobEditUrl,
__( 'Try again', 'wpml-translation-management' ) );
$ateApiErrorMessage = ATEDetailedErrorMessage::readDetailedError( $tryAgainTextLink );
return $ateApiErrorMessage ?: $fallbackErrorMessage;
}
public static function retryFailedMessage() {
$fallbackErrorMessage = '<div>' .
sprintf(
__( 'WPML tried to translate this page three times and failed. To get it fixed, contact %s', 'wpml-translation-management' ),
'<a target=\'_blank\' href="https://wpml.org/forums/forum/english-support/">' . __( 'WPML support', 'wpml-translation-management' ) . '</a>'
) . '</div>';
$ateApiErrorMessage = ATEDetailedErrorMessage::readDetailedError();
return $ateApiErrorMessage ?: $fallbackErrorMessage;
}
public static function ateNotActiveMessage() {
return '<div>' .
sprintf(
__( 'WPMLs Advanced Translation Editor is enabled but not activated. Go to %s to resolve the issue.', 'wpml-translation-management' ),
'<a href="' . UIPage::getTMDashboard() . '">' . __( 'WPML Translation Management Dashboard', 'wpml-translation-management' ) . '</a>'
)
. '</div>';
}
}

View File

@@ -0,0 +1,8 @@
<?php
class WPML_TM_Editors {
const ATE = 'ate';
const WPML = 'wpml';
const WP = 'wp';
const NONE = 'none';
}

View File

@@ -0,0 +1,83 @@
<?php
class WPML_TM_Old_Jobs_Editor {
const OPTION_NAME = 'wpml-old-jobs-editor';
/** @var wpdb */
private $wpdb;
/** @var WPML_Translation_Job_Factory */
private $job_factory;
public function __construct( WPML_Translation_Job_Factory $job_factory ) {
global $wpdb;
$this->wpdb = $wpdb;
$this->job_factory = $job_factory;
}
/**
* @param int $job_id
*
* @return null|string
*/
public function get( $job_id ) {
$current_editor = $this->get_current_editor( $job_id );
if ( WPML_TM_Editors::NONE === $current_editor || WPML_TM_Editors::ATE === $current_editor ) {
return $current_editor;
} else {
return get_option( self::OPTION_NAME, null );
}
}
/**
* @param int $job_id
*
* @return bool
*/
public function shouldStickToWPMLEditor( $job_id ) {
$sql = "
SELECT job.editor
FROM {$this->wpdb->prefix}icl_translate_job job
WHERE job.job_id < %d AND job.rid = (
SELECT rid FROM {$this->wpdb->prefix}icl_translate_job WHERE job_id = %s
)
ORDER BY job.job_id DESC
";
$previousJobEditor = $this->wpdb->get_var( $this->wpdb->prepare( $sql, $job_id, $job_id ) );
return $previousJobEditor === WPML_TM_Editors::WPML && get_option( self::OPTION_NAME, null ) === WPML_TM_Editors::WPML;
}
/**
* @return string
*/
public function editorForTranslationsPreviouslyCreatedUsingCTE( ) {
return get_option( self::OPTION_NAME, WPML_TM_Editors::WPML );
}
public function set( $job_id, $editor ) {
$data = [ 'editor' => $editor ];
if ( $editor !== WPML_TM_Editors::ATE ) {
$data['editor_job_id'] = null;
}
$this->job_factory->update_job_data( $job_id, $data );
}
/**
* @param int $job_id
*
* @return null|string
*/
public function get_current_editor( $job_id ) {
$sql = "SELECT editor FROM {$this->wpdb->prefix}icl_translate_job WHERE job_id = %d";
return $this->wpdb->get_var( $this->wpdb->prepare( $sql, $job_id ) );
}
}