diff --git a/inc/configs/parameters.yml b/inc/configs/parameters.yml index b264e0f..87fdfa0 100644 --- a/inc/configs/parameters.yml +++ b/inc/configs/parameters.yml @@ -9,6 +9,8 @@ parameters: - "smartcat_debug_mode" - "smartcat_events_enabled" - "smartcat_regform_showed" + - "last_cron_send" + - "last_cron_check" #Manual update statistics switch - "statistic_queue_active" #Сallback Authorize code diff --git a/inc/configs/services.yml b/inc/configs/services.yml index 3aaff10..bcb7e18 100644 --- a/inc/configs/services.yml +++ b/inc/configs/services.yml @@ -21,22 +21,6 @@ services: class: SmartCAT\WP\Cron\ClearDeletedProject tags: [installable, cron] - core.queue.callback: - class: SmartCAT\WP\Queue\Callback - tags: [queue] - - core.queue.statistic: - class: SmartCAT\WP\Queue\Statistic - tags: [queue] - - core.queue.publication: - class: SmartCAT\WP\Queue\Publication - tags: [queue] - - core.queue.post: - class: SmartCAT\WP\Queue\CreatePost - tags: [queue] - core.admin.columns: class: SmartCAT\WP\Admin\AdditionalActions tags: [hook] diff --git a/inc/smartcat/Admin/StatisticsAjax.php b/inc/smartcat/Admin/StatisticsAjax.php index b618160..0116daa 100644 --- a/inc/smartcat/Admin/StatisticsAjax.php +++ b/inc/smartcat/Admin/StatisticsAjax.php @@ -14,7 +14,6 @@ use Psr\Container\ContainerInterface; use SmartCAT\WP\Connector; use SmartCAT\WP\DB\Repository\StatisticRepository; -use SmartCAT\WP\Queue\Statistic; use SmartCAT\WP\WP\HookInterface; use SmartCAT\WP\WP\Options; @@ -56,25 +55,6 @@ static public function start_refresh_statistic() { if ( ! $options->get( 'statistic_queue_active' ) ) { - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); - $statistics = $statistic_repository->get_sended(); - - if ( count( $statistics ) > 0 ) { - $options->set( 'statistic_queue_active', true ); - /** @var Statistic $queue */ - $queue = $container->get( 'core.queue.statistic' ); - foreach ( $statistics as $statistic ) { - if ( $statistic->get_error_count() > 0 ) { - $statistic->set_error_count( 0 ); - $statistic_repository->persist( $statistic ); - } - - $queue->push_to_queue( $statistic->get_document_id() ); - } - $statistic_repository->flush(); - $queue->save()->dispatch(); - } } $ajax_response->send_success( 'ok' ); diff --git a/inc/smartcat/Connector.php b/inc/smartcat/Connector.php index e424be8..053b142 100644 --- a/inc/smartcat/Connector.php +++ b/inc/smartcat/Connector.php @@ -11,11 +11,11 @@ namespace SmartCAT\WP; +use Psr\Container\ContainerInterface; use SmartCAT\WP\Cron\CronInterface; use SmartCAT\WP\DB\Setup\SetupInterface; use SmartCAT\WP\Helpers\SmartCAT; use SmartCAT\WP\Helpers\Utils; -use SmartCAT\WP\Queue\QueueAbstract; use SmartCAT\WP\WP\HookInterface; use SmartCAT\WP\WP\InitInterface; use SmartCAT\WP\WP\Notice; @@ -81,16 +81,6 @@ private function register_hooks() { } } - /** - * @throws \Exception - */ - private function init_queue() { - $services = self::get_container()->findTaggedServiceIds( 'queue' ); - foreach ( $services as $service => $tags ) { - $this->from_container( $service ); - } - } - /** * @throws \Exception */ @@ -138,14 +128,6 @@ public function plugin_deactivate() { $object->plugin_deactivate(); } } - // Stopping queues. - $hooks = self::get_container()->findTaggedServiceIds( 'queue' ); - foreach ( $hooks as $hook => $tags ) { - $object = $this->from_container( $hook ); - if ( $object instanceof QueueAbstract ) { - $object->cancel_process(); - } - } } /** @@ -190,7 +172,6 @@ static public function plugin_uninstall() { * @throws \Exception */ public function plugin_init( $query ) { - $this->init_queue(); $hooks = self::get_container()->findTaggedServiceIds( 'initable' ); foreach ( $hooks as $hook => $tags ) { $object = self::get_container()->get( $hook ); @@ -221,6 +202,15 @@ public function plugin_admin_notice( $query ) { $notice->add_error( __( 'Smartcat credentials are incorrect. Login failed.', 'translation-connectors' ), false ); } } + + /** @var ContainerInterface */ + $container = self::get_container(); + /** @var Options $options */ + $options = $container->get( 'core.options' ); + + if ( abs( time() - intval( $options->get( 'last_cron_send' ) ) ) > 600 || abs( time() - intval( $options->get( 'last_cron_check' ) ) ) > 600 ) { + $notice->add_warning( __( 'It looks like cron service is not working properly. Lag is more than 10 minutes. Please check it.', 'translation-connectors' ), false ); + } } } diff --git a/inc/smartcat/Cron/CheckProjectsStatus.php b/inc/smartcat/Cron/CheckProjectsStatus.php index d5d8f83..3d40105 100644 --- a/inc/smartcat/Cron/CheckProjectsStatus.php +++ b/inc/smartcat/Cron/CheckProjectsStatus.php @@ -11,13 +11,17 @@ namespace SmartCAT\WP\Cron; +use Http\Client\Common\Exception\ClientErrorException; use Psr\Container\ContainerInterface; +use SmartCat\Client\Model\ProjectModel; use SmartCAT\WP\Connector; use SmartCAT\WP\DB\Entity\Statistics; use SmartCAT\WP\DB\Repository\StatisticRepository; use SmartCAT\WP\DB\Repository\TaskRepository; use SmartCAT\WP\Helpers\Logger; +use SmartCAT\WP\Helpers\ProjectManager; use SmartCAT\WP\Helpers\SmartCAT; +use SmartCAT\WP\WP\Options; /** * Class CheckProjectsStatus @@ -25,6 +29,16 @@ * @package SmartCAT\WP\Cron */ class CheckProjectsStatus extends CronAbstract { + + /** @var SmartCAT $smartcat */ + private $smartcat; + /** @var StatisticRepository $statistic_repository */ + private $statistic_repository; + /** @var TaskRepository $task_repository */ + private $task_repository; + /** @var Options */ + private $options; + /** * @return mixed */ @@ -45,61 +59,160 @@ public function run() { return; } - Logger::event( 'cron', 'Checking documents started' ); - /** @var ContainerInterface $container */ $container = Connector::get_container(); - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); + $this->smartcat = $container->get( 'smartcat' ); + $this->options = $container->get( 'core.options' ); + $this->statistic_repository = $container->get( 'entity.repository.statistic' ); + $this->task_repository = $container->get( 'entity.repository.task' ); - /** @var TaskRepository $task_repository */ - $task_repository = $container->get( 'entity.repository.task' ); + $this->options->set( 'last_cron_check', time() ); - /** @var SmartCAT $smartcat */ - $smartcat = $container->get( 'smartcat' ); + Logger::event( 'cron', 'Checking documents started' ); - $statistics = $statistic_repository->get_sended(); + $statistics = $this->statistic_repository->get_by_status( [ Statistics::STATUS_SENDED, Statistics::STATUS_EXPORT ] ); $count = count( $statistics ); Logger::event( 'cron', "Find $count documents to check" ); - foreach ( $statistics as $statistic ) { - if ( $statistic->get_status() !== Statistics::STATUS_SENDED ) { - continue; - } + try { + foreach ( $statistics as $statistic ) { + $task = $this->task_repository->get_one_by_id( $statistic->get_task_id() ); + $project = $this->smartcat->getProjectManager()->projectGet( $task->get_project_id() ); + + if ( $this->canceled_check( $statistic, $project ) ) { + continue; + } + + if ( $statistic->get_status() === Statistics::STATUS_EXPORT ) { + $this->create_post( $statistic ); + continue; + } - $task = $task_repository->get_one_by_id( $statistic->get_task_id() ); - $project = $smartcat->getProjectManager()->projectGet( $task->get_project_id() ); + if ( $statistic->get_status() === Statistics::STATUS_SENDED ) { + $document = $this->smartcat->getDocumentManager()->documentGet( + [ 'documentId' => $statistic->get_document_id() ] + ); - if ( $project->getStatus() === 'canceled' ) { - $stat_update = $statistic_repository->get_all_by( [ 'taskId' => $statistic->get_task_id() ] ); + $stages = $document->getWorkflowStages(); + $progress = 0; - foreach ( $stat_update as $stat ) { - $stat->set_status( Statistics::STATUS_CANCELED ); - $statistic_repository->save( $stat ); + foreach ( $stages as $stage ) { + $progress += $stage->getProgress(); + } + + $progress = round( $progress / count( $stages ), 2 ); + $statistic->set_progress( $progress ); + $this->statistic_repository->save( $statistic ); + + if ( $document->getStatus() === 'completed' ) { + $this->export_request( $statistic ); + } } + } + } catch (\Exception $e) { + Logger::error( + "Document {$statistic->get_document_id()}, main error", + "Message: {$e->getMessage()}" + ); + } - continue; + Logger::event( 'cron', 'Checking documents ended' ); + } + + /** + * @param Statistics $statistic + * @param ProjectModel $project + * + * @return bool + */ + private function canceled_check( $statistic, $project ) { + if ( $project->getStatus() === 'canceled' ) { + $stat_update = $this->statistic_repository->get_all_by( [ 'taskId' => $statistic->get_task_id() ] ); + + foreach ( $stat_update as $stat ) { + $stat->set_status( Statistics::STATUS_CANCELED ); + $this->statistic_repository->save( $stat ); } - $document = $smartcat->getDocumentManager()->documentGet( - [ 'documentId' => $statistic->get_document_id() ] - ); + return true; + } - $stages = $document->getWorkflowStages(); - $progress = 0; + return false; + } - foreach ( $stages as $stage ) { - $progress += $stage->getProgress(); + /** + * @param Statistics $statistic + * + * @return void + */ + private function export_request( $statistic ) { + try { + Logger::event( 'exporting', "Export request '{$statistic->get_document_id()}'" ); + + $task = $this->smartcat->getDocumentExportManager() + ->documentExportRequestExport( [ 'documentIds' => [ $statistic->get_document_id() ] ] ); + if ( $task->getId() ) { + $statistic->set_export_id( $task->getId() ); + $statistic->set_status( Statistics::STATUS_EXPORT ); + $this->statistic_repository->update( $statistic ); } - $progress = round( $progress / count( $stages ), 2 ); - $statistic->set_progress( $progress ); + Logger::event( 'exporting', "Export request '{$statistic->get_document_id()}' done" ); + } catch ( ClientErrorException $e ) { + if ( $e->getResponse()->getStatusCode() === 404 ) { + $this->statistic_repository->delete( $statistic ); + Logger::event( 'exporting', "Deleted '{$statistic->get_document_id()}'" ); + } else { + Logger::error( + "Document {$statistic->get_document_id()}, request download", + "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" + ); + } + } catch ( \Throwable $e ) { + Logger::error( + "Document {$statistic->get_document_id()}, request download", + "Message: {$e->getMessage()}" + ); + } + } - $statistic_repository->save( $statistic ); + /** + * @param Statistics $statistic + * + * @return void + */ + private function create_post( $statistic ) { + Logger::event( 'createPost', "Start create post '{$statistic->get_document_id()}'" ); + + try { + $result = $this->smartcat->getDocumentExportManager()->documentExportDownloadExportResult( $statistic->get_export_id() ); + if ( 204 === $result->getStatusCode() ) { + Logger::event( 'createPost', "Export not done yet '{$statistic->get_document_id()}'" ); + } elseif ( 200 === $result->getStatusCode() ) { + Logger::event( 'createPost', "Download document '{$statistic->get_document_id()}'" ); + ProjectManager::publish( $statistic, $result->getBody()->getContents() ); + $statistic->set_status( Statistics::STATUS_COMPLETED )->set_export_id( null ); + Logger::event( 'createPost', "Generated post for '{$statistic->get_document_id()}' and status = {$statistic->get_status()}" ); + } + } catch ( ClientErrorException $e ) { + if ( 404 === $e->getResponse()->getStatusCode() ) { + $statistic->set_status( Statistics::STATUS_SENDED ); + } else { + $statistic->set_status( Statistics::STATUS_FAILED ); + } + Logger::error( + "Document {$statistic->get_document_id()}, download translate error", + "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" + ); + } catch ( \Throwable $e ) { + $statistic->set_status( Statistics::STATUS_SENDED ); + Logger::error( "Document {$statistic->get_document_id()}, download translate error", "Message: {$e->getMessage()}" ); + } finally { + $this->statistic_repository->save( $statistic ); } - Logger::event( 'cron', 'Checking documents ended' ); + Logger::event( 'createPost', "End create post '{$statistic->get_document_id()}'" ); } } diff --git a/inc/smartcat/Cron/SendToSmartCAT.php b/inc/smartcat/Cron/SendToSmartCAT.php index 9e7d504..3f29c43 100644 --- a/inc/smartcat/Cron/SendToSmartCAT.php +++ b/inc/smartcat/Cron/SendToSmartCAT.php @@ -20,6 +20,7 @@ use SmartCAT\WP\Helpers\Logger; use SmartCAT\WP\Helpers\SmartCAT; use SmartCAT\WP\Helpers\Utils; +use SmartCAT\WP\WP\Options; /** * Class SendToSmartCAT @@ -47,11 +48,16 @@ public function run() { return; } - Logger::event( 'cron', 'Sending to Smartсat started' ); - /** @var ContainerInterface $container */ $container = Connector::get_container(); + /** @var Options $options */ + $options = $container->get( 'core.options' ); + + $options->set( 'last_cron_send', time() ); + + Logger::event( 'cron', 'Sending to Smartсat started' ); + /** @var TaskRepository $task_repository */ $task_repository = $container->get( 'entity.repository.task' ); diff --git a/inc/smartcat/DB/Entity/Statistics.php b/inc/smartcat/DB/Entity/Statistics.php index 4570ef2..c7e2149 100644 --- a/inc/smartcat/DB/Entity/Statistics.php +++ b/inc/smartcat/DB/Entity/Statistics.php @@ -35,8 +35,8 @@ class Statistics extends EntityAbstract { protected $document_id; /** @var string */ protected $status; - /** @var integer */ - protected $error_count = 0; + /** @var string */ + protected $export_id = null; const STATUS_NEW = 'new'; const STATUS_FAILED = 'failed'; @@ -73,7 +73,7 @@ public function attributes(): array { 'targetPostID' => 'target_post_id', 'documentID' => 'document_id', 'status' => 'status', - 'errorCount' => 'error_count', + 'exportID' => 'export_id', ]; } @@ -240,30 +240,19 @@ public function set_status( $status ) { } /** - * @return int - */ - public function get_error_count() { - return intval( $this->error_count ); - } - - /** - * @param int $error_count - * - * @return Statistics + * @return string */ - public function set_error_count( $error_count ) { - $this->error_count = intval( $error_count ); - - return $this; + public function get_export_id() { + return $this->export_id; } /** - * @param int $inc + * @param string $export_id * * @return Statistics */ - public function inc_error_count( $inc = 1 ) { - $this->set_error_count( $this->get_error_count() + $inc ); + public function set_export_id( $export_id ) { + $this->export_id = $export_id; return $this; } diff --git a/inc/smartcat/DB/Repository/StatisticRepository.php b/inc/smartcat/DB/Repository/StatisticRepository.php index e8f74b4..ac32883 100644 --- a/inc/smartcat/DB/Repository/StatisticRepository.php +++ b/inc/smartcat/DB/Repository/StatisticRepository.php @@ -76,7 +76,7 @@ public function get_by_status( $status, array $documents = [] ) { $wpdb = $this->get_wp_db(); if ( is_array( $status ) ) { - $status = implode( "', OR status='", $status ); + $status = implode( "' OR status='", $status ); } $query = "SELECT * FROM $table_name WHERE status='$status'"; diff --git a/inc/smartcat/DB/Setup/TablesUpdate.php b/inc/smartcat/DB/Setup/TablesUpdate.php index 3687eb0..71a2100 100644 --- a/inc/smartcat/DB/Setup/TablesUpdate.php +++ b/inc/smartcat/DB/Setup/TablesUpdate.php @@ -53,6 +53,10 @@ public function install() { if ( version_compare( Utils::get_plugin_version(), '2.0.4', '<' ) ) { $this->v204(); } + + if ( version_compare( Utils::get_plugin_version(), '2.1.0', '<' ) ) { + $this->v210(); + } } /** @@ -150,6 +154,16 @@ private function v204() { $this->create_table( $sql ); } + + /** + * Update to version 2.1.0 + */ + private function v210() { + $statistic_table_name = $this->prefix . 'smartcat_connector_statistic'; + $this->exec( "ALTER TABLE {$statistic_table_name} ADD COLUMN exportID VARCHAR (255) DEFAULT NULL;" ); + $this->exec( "ALTER TABLE {$statistic_table_name} DROP COLUMN errorCount;" ); + } + /** * Main rollback function */ diff --git a/inc/smartcat/Handler/SmartCATCallbackHandler.php b/inc/smartcat/Handler/SmartCATCallbackHandler.php index cbb0899..519d148 100644 --- a/inc/smartcat/Handler/SmartCATCallbackHandler.php +++ b/inc/smartcat/Handler/SmartCATCallbackHandler.php @@ -13,7 +13,8 @@ use SmartCat\Client\Model\CallbackPropertyModel; use SmartCAT\WP\Connector; -use SmartCAT\WP\DB\Repository\StatisticRepository; +use SmartCAT\WP\Cron\CheckProjectsStatus; +use SmartCAT\WP\Cron\SendToSmartCAT; use SmartCAT\WP\Helpers\SmartCAT; use SmartCAT\WP\WP\HookInterface; use SmartCAT\WP\WP\Options; @@ -23,6 +24,7 @@ /** * Обработка запросов от callback smartCAT * Class SmartCATCallbackHandler + * * @package SmartCAT\WP\Handler */ class SmartCATCallbackHandler implements PluginInterface, HookInterface { @@ -32,19 +34,25 @@ class SmartCATCallbackHandler implements PluginInterface, HookInterface { /** @var ContainerInterface */ private $container; + /** + * SmartCATCallbackHandler constructor. + */ public function __construct() { $this->container = Connector::get_container(); } - public function register_rest_route( - /** @noinspection PhpUnusedParameterInspection */ - \WP_REST_Server $server - ) { - register_rest_route( self::ROUTE_PREFIX, '/(?.+)/(?.+)', [ - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'handle' ], - ] ); - + /** + * @param \WP_REST_Server $server + */ + public function register_rest_route( \WP_REST_Server $server ) { + register_rest_route( + self::ROUTE_PREFIX, + '/(?.+)/(?.+)', + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'handle' ], + ] + ); } /** @@ -55,36 +63,28 @@ public function register_rest_route( * @return array|\WP_Error */ public function handle( \WP_REST_Request $request ) { - if ( $request->get_param( 'type' ) == 'document' && $request->get_param( 'method' ) == 'status' ) { + if ( $request->get_param( 'type' ) === 'document' && $request->get_param( 'method' ) === 'status' ) { /** @var Options $options */ $options = $this->container->get( 'core.options' ); - if ( $request->get_header( 'authorization' ) == $options->get_and_decrypt( 'callback_authorisation_token' ) ) { - $body = $request->get_body(); - $documents = json_decode( $body ); - if ( is_array( $documents ) && count( $documents ) > 0 ) { - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $this->container->get( 'entity.repository.statistic' ); - $resulting_documents = $statistic_repository->get_sended( $documents ); - - /** @var \SmartCAT\WP\Queue\Callback $queue */ - $queue = $this->container->get( 'core.queue.callback' ); - - foreach ( $resulting_documents as $document ) { - if ( $document->get_error_count() > 0 ) { - $document->set_error_count( 0 ); - $statistic_repository->persist( $document ); - } - $queue->push_to_queue( $document->get_document_id() ); - } - - $statistic_repository->flush(); - $queue->save()->dispatch(); + if ( $request->get_header( 'authorization' ) === $options->get_and_decrypt( 'callback_authorisation_token' ) ) { + if ( abs( time() - intval( $options->get( 'last_cron_check' ) ) ) > 300 ) { + /** @var CheckProjectsStatus $cron_checker */ + $cron_checker = $this->container->get( 'core.cron.check' ); + $cron_checker->run(); + } + + if ( abs( time() - intval( $options->get( 'last_cron_send' ) ) ) > 300 ) { + /** @var SendToSmartCAT $cron_checker */ + $cron_checker = $this->container->get( 'core.cron.send' ); + $cron_checker->run(); } } else { - $response = new \WP_Error( 'rest_forbidden', + $response = new \WP_Error( + 'rest_forbidden', __( 'Sorry, you are not allowed to do that.', 'translation-connectors' ), - [ 'status' => 403 ] ); + [ 'status' => 403 ] + ); return $response; } @@ -93,33 +93,44 @@ public function handle( \WP_REST_Request $request ) { return [ 'message' => 'ok' ]; } + /** + * @throws \Exception + */ public function plugin_activate() { - $authorisation_token = "Bearer " . base64_encode( openssl_random_pseudo_bytes( 32 ) ); + $authorisation_token = 'Bearer ' . base64_encode( openssl_random_pseudo_bytes( 32 ) ); /** @var Options $options */ $options = $this->container->get( 'core.options' ); $options->set_and_encrypt( 'callback_authorisation_token', $authorisation_token ); $this->register_callback(); } + /** + * @throws \Exception + */ public function register_callback() { if ( SmartCAT::is_active() ) { /** @var Options $options */ $options = $this->container->get( 'core.options' ); /** @var SmartCAT $sc */ - $sc = $this->container->get( 'smartcat' ); + $sc = $this->container->get( 'smartcat' ); $callback_model = new CallbackPropertyModel(); $callback_model->setUrl( get_site_url() . '/wp-json/' . self::ROUTE_PREFIX ); - $callback_model->setAdditionalHeaders( [ + $callback_model->setAdditionalHeaders( [ - 'name' => 'Authorization', - 'value' => $options->get_and_decrypt( 'callback_authorisation_token' ) + [ + 'name' => 'Authorization', + 'value' => $options->get_and_decrypt( 'callback_authorisation_token' ), + ], ] - ] ); + ); $sc->getCallbackManager()->callbackUpdate( $callback_model ); } } + /** + * @throws \Exception + */ public function plugin_deactivate() { if ( SmartCAT::is_active() ) { /** @var SmartCAT $sc */ @@ -128,10 +139,15 @@ public function plugin_deactivate() { } } + /** + * + */ public function plugin_uninstall() { - } + /** + * @return mixed|void + */ public function register_hooks() { add_action( 'rest_api_init', [ $this, 'register_rest_route' ] ); } diff --git a/inc/smartcat/Helpers/ProjectManager.php b/inc/smartcat/Helpers/ProjectManager.php new file mode 100644 index 0000000..ced68e9 --- /dev/null +++ b/inc/smartcat/Helpers/ProjectManager.php @@ -0,0 +1,144 @@ + + * @copyright (c) 2019 Smartcat. All Rights Reserved. + * @license GNU General Public License version 3 or later; see LICENSE.txt + * @link http://smartcat.ai + */ + +namespace SmartCAT\WP\Helpers; + +use Psr\Container\ContainerInterface; +use SmartCAT\WP\Connector; +use SmartCAT\WP\DB\Entity\Statistics; +use SmartCAT\WP\DITrait; +use SmartCAT\WP\Helpers\Language\LanguageConverter; + +/** + * Class ProjectManager + * + * @package SmartCAT\WP\Helpers + */ +class ProjectManager { + use DITrait; + /** + * @param Statistics $statistics + * @param string $content + */ + public static function publish( $statistics, $content ) { + /** @var ContainerInterface $container */ + $container = Connector::get_container(); + + $html = new \DOMDocument(); + $html->loadHTML( $content ); + $title = $html->getElementsByTagName( 'title' )->item( 0 )->nodeValue; + $body = ''; + $children = $html->getElementsByTagName( 'body' )->item( 0 )->childNodes; + foreach ( $children as $child ) { + $body .= $child->ownerDocument->saveHTML( $child ); + } + + $replace_count = 0; + $iteration = 0; + + do { + $body = preg_replace_callback( + '%(.*?)<\/sc-shortcode-\1>%s', + function( $matches ) { + if ( 'true' === $matches[3] ) { + return "[{$matches[2]}{$matches[4]}]"; + } else { + return "[{$matches[2]}{$matches[4]}]{$matches[5]}[/{$matches[2]}]"; + } + }, + $body, + -1, + $replace_count + ); + + $iteration++; + + if ( $iteration >= 50 ) { + Logger::warning( 'Limit exceeded', "Shortcodes replacing iteration limit exceeded in returned post from SC '{$title}'" ); + } + } while ( $replace_count && ( $iteration < 50 ) ); + + if ( $statistics->get_target_post_id() ) { + Logger::event( 'createPost', "Updating post {$statistics->get_target_post_id()} '{$statistics->get_document_id()}'" ); + wp_update_post( + [ + 'ID' => $statistics->get_target_post_id(), + 'post_title' => $title, + 'post_content' => $body, + 'post_status' => 'draft', + ] + ); + } else { + Logger::event( 'createPost', "Generate new post '{$statistics->get_document_id()}'" ); + $post = get_post( $statistics->get_post_id() ); + $thumbnail_id = get_post_meta( $statistics->get_post_id(), '_thumbnail_id', true ); + $target_post_id = wp_insert_post( + [ + 'post_title' => $title, + 'menu_order' => $post->menu_order, + 'comment_status' => $post->comment_status, + 'ping_status' => $post->ping_status, + 'pinged' => $post->pinged, + 'post_content' => $body, + 'post_status' => 'draft', + 'post_author' => $post->post_author, + 'post_password' => $post->post_password, + 'post_type' => $post->post_type, + 'meta_input' => [ '_thumbnail_id' => $thumbnail_id ], + ] + ); + $statistics->set_target_post_id( $target_post_id ); + + /** @noinspection PhpUndefinedFunctionInspection */ + pll_set_post_language( $target_post_id, $statistics->get_target_language() ); + + /** @var LanguageConverter $converter */ + $converter = $container->get( 'language.converter' ); + $slug_list = $converter->get_polylang_locales_to_slugs(); + + if ( isset( $slug_list[ $statistics->get_target_language() ] ) ) { + /** @noinspection PhpUndefinedFunctionInspection */ + $pll_info = pll_get_post_translations( $statistics->get_post_id() ); + $slug = $slug_list[ $statistics->get_target_language() ]; + $pll_info[ $slug ] = $target_post_id; + /** @noinspection PhpUndefinedFunctionInspection */ + pll_save_post_translations( $pll_info ); + + $categories = wp_get_post_categories( $statistics->get_post_id() ); + $tags = wp_get_post_tags( 1, [ 'fields' => 'ids' ] ); + + $translate_categories = []; + foreach ( $categories as $category ) { + /** @noinspection PhpUndefinedFunctionInspection */ + $translate_category = pll_get_term_translations( $category ); + if ( isset( $translate_category[ $slug ] ) ) { + $translate_categories[] = $translate_category[ $slug ]; + } + } + + $translate_tags = []; + foreach ( $tags as $tag ) { + /** @noinspection PhpUndefinedFunctionInspection */ + $translate_tag = pll_get_term_translations( $tag ); + if ( isset( $translate_tag[ $slug ] ) ) { + $translate_tags[] = $translate_tag[ $slug ]; + } + } + if ( count( $translate_categories ) > 0 ) { + wp_set_post_categories( $target_post_id, $translate_categories ); + } + if ( count( $translate_tags ) > 0 ) { + wp_set_post_tags( $target_post_id, $translate_tags ); + } + } + } + } +} diff --git a/inc/smartcat/Queue/Callback.php b/inc/smartcat/Queue/Callback.php deleted file mode 100644 index 7cf8606..0000000 --- a/inc/smartcat/Queue/Callback.php +++ /dev/null @@ -1,145 +0,0 @@ - - * @copyright (c) 2019 Smartcat. All Rights Reserved. - * @license GNU General Public License version 3 or later; see LICENSE.txt - * @link http://smartcat.ai - */ - -namespace SmartCAT\WP\Queue; - -use Http\Client\Common\Exception\ClientErrorException; -use Psr\Container\ContainerInterface; -use SmartCAT\WP\Connector; -use SmartCAT\WP\DB\Repository\StatisticRepository; -use SmartCAT\WP\Helpers\Logger; -use SmartCAT\WP\Helpers\SmartCAT; - -/** - * Class Callback - * - * @package SmartCAT\WP\Queue - */ -class Callback extends QueueAbstract { - - public function update_statistic( $item ) { - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); - - /** @var SmartCAT $sc */ - $sc = $container->get( 'smartcat' ); - - $statistics = $statistic_repository->get_one_by( [ 'documentID' => $item ] ); - - try { - if ( $statistics ) { - Logger::event( "callback", "Queue update_statistic '{$statistics->get_document_id()}'"); - $document = $sc->getDocumentManager()->documentGet( [ 'documentId' => $statistics->get_document_id() ] ); - $stages = $document->getWorkflowStages(); - $progress = 0; - - foreach ( $stages as $stage ) { - $progress += $stage->getProgress(); - } - - $progress = round( $progress / count( $stages ), 2 ); - $statistics->set_progress( $progress ) - ->set_error_count( 0 ); - - Logger::event( "callback", "Set statistics config for '{$statistics->get_document_id()}'"); - $statistic_repository->update( $statistics ); - - if ( $document->getStatus() == 'completed' ) { - /** @var Publication $queue */ - $queue = $container->get( 'core.queue.publication' ); - $queue->push_to_queue( $item ); - Logger::event( "callback", "Pushed to publication '{$statistics->get_document_id()}'"); - - } - - Logger::event( "callback", "End Queue update_statistic '{$statistics->get_document_id()}'"); - } else { - Logger::event( "callback", "Document '{$item}' not from WP"); - } - } catch ( ClientErrorException $e ) { - if ( $e->getResponse()->getStatusCode() == 404 ) { - $statistic_repository->delete( $statistics ); - Logger::event( "callback", "Deleted '{$statistics->get_document_id()}'"); - } else { - Logger::error( "Document update statistic", - "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" ); - } - } catch ( \Throwable $e ) { - Logger::error( "Document update statistic", "Error: {$e->getMessage()}" ); - } - } - - protected $action = 'smartcat_callback_async'; - - /** - * Task - * - * Override this method to perform any actions required on each - * queue item. Return the modified item for further processing - * in the next pass through. Or, return false to remove the - * item from the queue. - * - * @param mixed $item Queue item to iterate over - * - * @return mixed - */ - protected function task( $item ) { - if ( ! SmartCAT::is_active() ) { - sleep( 10 ); - - return $item; - } - try { - $this->update_statistic( $item ); - } catch ( ClientErrorException $e ) { - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); - - $statistics = $statistic_repository->get_one_by( [ 'documentID' => $item ] ); - if ( $statistics && $statistics->get_error_count() < 360 ) { - $statistics->inc_error_count(); - $statistic_repository->update( $statistics ); - sleep( 10 ); - - Logger::event( "callback", "New {$statistics->get_error_count()} try '{$statistics->get_document_id()}'"); - return $item; - } - Logger::error( "Document $item, update statistic", - "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" ); - - } catch ( \Throwable $e ) { - Logger::error( "Document $item, update statistic", "Error: {$e->getMessage()}" ); - } - - return false; - } - - /** - * Complete - * - * Override if applicable, but ensure that the below actions are - * performed, or, call parent::complete(). - */ - protected function complete() { - parent::complete(); - /** @var Publication $queue */ - $container = Connector::get_container(); - $queue = $container->get( 'core.queue.publication' ); - $queue->save()->dispatch(); - // Show notice to user or perform some other arbitrary task... - } -} \ No newline at end of file diff --git a/inc/smartcat/Queue/CreatePost.php b/inc/smartcat/Queue/CreatePost.php deleted file mode 100644 index f482ded..0000000 --- a/inc/smartcat/Queue/CreatePost.php +++ /dev/null @@ -1,234 +0,0 @@ - - * @copyright (c) 2019 Smartcat. All Rights Reserved. - * @license GNU General Public License version 3 or later; see LICENSE.txt - * @link http://smartcat.ai - */ - -namespace SmartCAT\WP\Queue; - -use Http\Client\Common\Exception\ClientErrorException; -use Psr\Container\ContainerInterface; -use SmartCAT\WP\Connector; -use SmartCAT\WP\DB\Entity\Statistics; -use SmartCAT\WP\DB\Repository\StatisticRepository; -use SmartCAT\WP\Helpers\Language\LanguageConverter; -use SmartCAT\WP\Helpers\Logger; -use SmartCAT\WP\Helpers\SmartCAT; - -/** - * Class CreatePost - * - * @package SmartCAT\WP\Queue - */ -class CreatePost extends QueueAbstract { - protected $action = 'smartcat_createpost_async'; - - /** - * Task - * - * Override this method to perform any actions required on each - * queue item. Return the modified item for further processing - * in the next pass through. Or, return false to remove the - * item from the queue. - * - * @param mixed $item Queue item to iterate over - * - * @return mixed - */ - protected function task( $item ) { - if ( ! SmartCAT::is_active() ) { - sleep( 10 ); - - return $item; - } - - Logger::event( "createPost", "Start queue '{$item['documentID']}'"); - - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); - - /** @var SmartCAT $sc */ - $sc = $container->get( 'smartcat' ); - - $statistics = $statistic_repository->get_one_by( [ 'documentID' => $item['documentID'] ] ); - if ( is_null( $statistics ) ) { - Logger::error( - 'CreatePost - Statistics object is empty', - 'Can\'t get statistics by documentID - ' . $item['documentID'] - ); - return false; - } - - try { - $result = $sc->getDocumentExportManager()->documentExportDownloadExportResult( $item['taskID'] ); - if ( 204 === $result->getStatusCode() ) { - sleep( 1 ); - Logger::event( 'createPost', "Export not done yet '{$item['documentID']}'"); - return $item; - } elseif ( 200 === $result->getStatusCode() ) { - Logger::event( 'createPost', "Download document '{$item['documentID']}'"); - $response_body = $result->getBody()->getContents(); - $html = new \DOMDocument(); - $html->loadHTML( $response_body ); - $title = $html->getElementsByTagName( 'title' )->item( 0 )->nodeValue; - $body = ''; - $children = $html->getElementsByTagName( 'body' )->item( 0 )->childNodes; - foreach ( $children as $child ) { - $body .= $child->ownerDocument->saveHTML( $child ); - } - - $replace_count = 0; - $iteration = 0; - - do { - $body = preg_replace_callback( - '%(.*?)<\/sc-shortcode-\1>%s', - function( $matches ) { - if ( 'true' === $matches[3] ) { - return "[{$matches[2]}{$matches[4]}]"; - } else { - return "[{$matches[2]}{$matches[4]}]{$matches[5]}[/{$matches[2]}]"; - } - }, - $body, - -1, - $replace_count - ); - - $iteration++; - - if ( $iteration >= 50 ) { - Logger::warning( 'Limit exceeded', "Shortcodes replacing iteration limit exceeded in returned post from SC '{$title}'" ); - } - } while ( $replace_count && ( $iteration < 50 ) ); - - if ( $statistics->get_target_post_id() ) { - Logger::event( 'createPost', "Updating post {$statistics->get_target_post_id()} '{$item['documentID']}'" ); - wp_update_post( - [ - 'ID' => $statistics->get_target_post_id(), - 'post_title' => $title, - 'post_content' => $body, - 'post_status' => 'draft', - ] - ); - } else { - Logger::event( 'createPost', "Generate new post '{$item['documentID']}'" ); - $post = get_post( $statistics->get_post_id() ); - $thumbnail_id = get_post_meta( $statistics->get_post_id(), '_thumbnail_id', true ); - $target_post_id = wp_insert_post( - [ - 'post_title' => $title, - 'menu_order' => $post->menu_order, - 'comment_status' => $post->comment_status, - 'ping_status' => $post->ping_status, - 'pinged' => $post->pinged, - 'post_content' => $body, - 'post_status' => 'draft', - 'post_author' => $post->post_author, - 'post_password' => $post->post_password, - 'post_type' => $post->post_type, - 'meta_input' => [ '_thumbnail_id' => $thumbnail_id ], - ] - ); - $statistics->set_target_post_id( $target_post_id ); - - /** @noinspection PhpUndefinedFunctionInspection */ - pll_set_post_language( $target_post_id, $statistics->get_target_language() ); - - /** @var LanguageConverter $converter */ - $converter = $container->get( 'language.converter' ); - $slug_list = $converter->get_polylang_locales_to_slugs(); - - if ( isset( $slug_list[ $statistics->get_target_language() ] ) ) { - /** @noinspection PhpUndefinedFunctionInspection */ - $pll_info = pll_get_post_translations( $statistics->get_post_id() ); - $slug = $slug_list[ $statistics->get_target_language() ]; - $pll_info[ $slug ] = $target_post_id; - /** @noinspection PhpUndefinedFunctionInspection */ - pll_save_post_translations( $pll_info ); - - $categories = wp_get_post_categories( $statistics->get_post_id() ); - $tags = wp_get_post_tags( 1, [ 'fields' => 'ids' ] ); - - $translate_categories = []; - foreach ( $categories as $category ) { - /** @noinspection PhpUndefinedFunctionInspection */ - $translate_category = pll_get_term_translations( $category ); - if ( isset( $translate_category[ $slug ] ) ) { - $translate_categories[] = $translate_category[ $slug ]; - } - } - - $translate_tags = []; - foreach ( $tags as $tag ) { - /** @noinspection PhpUndefinedFunctionInspection */ - $translate_tag = pll_get_term_translations( $tag ); - if ( isset( $translate_tag[ $slug ] ) ) { - $translate_tags[] = $translate_tag[ $slug ]; - } - } - if ( count( $translate_categories ) > 0 ) { - wp_set_post_categories( $target_post_id, $translate_categories ); - } - if ( count( $translate_tags ) > 0 ) { - wp_set_post_tags( $target_post_id, $translate_tags ); - } - } - } - - $statistics->set_status( Statistics::STATUS_COMPLETED ); - Logger::event( 'createPost', "Set completed status for statistic id '{$statistics->get_id()}'" ); - $statistic_repository->save( $statistics ); - Logger::event( 'createPost', "Generated post for '{$item['documentID']}' and status = {$statistics->get_status()}" ); - } - } catch ( ClientErrorException $e ) { - if ( 404 === $e->getResponse()->getStatusCode() ) { - $statistics->set_status( Statistics::STATUS_SENDED ); - $statistic_repository->save( $statistics ); - /** @var Publication $queue */ - $queue = $container->get( 'core.queue.publication' ); - Logger::event( 'createPost', "Pushed to publication '{$item['documentID']}'" ); - $queue->push_to_queue( $item['documentID'] )->save()->dispatch(); - } elseif ( $statistics->get_error_count() < 360 ) { - $statistics->inc_error_count(); - $statistic_repository->save( $statistics ); - sleep( 2 ); - - Logger::event( 'createPost', "New {$statistics->get_error_count()} try of '{$item['documentID']}'" ); - return $item; - } - Logger::error( - "Document {$item['documentID']}, download translate error", - "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" - ); - } catch ( \Throwable $e ) { - $statistics->set_status( Statistics::STATUS_SENDED ); - $statistic_repository->save( $statistics ); - Logger::error( "Document {$item['documentID']}, download translate error", "Message: {$e->getMessage()}" ); - } - - Logger::event( 'createPost', "End queue '{$item['documentID']}'" ); - - return false; - } - - /** - * Complete - * - * Override if applicable, but ensure that the below actions are - * performed, or, call parent::complete(). - */ - protected function complete() { - parent::complete(); - // Show notice to user or perform some other arbitrary task... - } -} diff --git a/inc/smartcat/Queue/Publication.php b/inc/smartcat/Queue/Publication.php deleted file mode 100644 index 6d6541c..0000000 --- a/inc/smartcat/Queue/Publication.php +++ /dev/null @@ -1,130 +0,0 @@ - - * @copyright (c) 2019 Smartcat. All Rights Reserved. - * @license GNU General Public License version 3 or later; see LICENSE.txt - * @link http://smartcat.ai - */ - -namespace SmartCAT\WP\Queue; - -use Http\Client\Common\Exception\ClientErrorException; -use Psr\Container\ContainerInterface; -use SmartCAT\WP\Connector; -use SmartCAT\WP\DB\Entity\Statistics; -use SmartCAT\WP\DB\Repository\StatisticRepository; -use SmartCAT\WP\Helpers\Logger; -use SmartCAT\WP\Helpers\SmartCAT; - -/** - * Class Publication - * - * @package SmartCAT\WP\Queue - */ -class Publication extends QueueAbstract { - protected $action = 'smartcat_publication_async'; - - /** - * Task - * - * Override this method to perform any actions required on each - * queue item. Return the modified item for further processing - * in the next pass through. Or, return false to remove the - * item from the queue. - * - * @param mixed $item Queue item to iterate over - * - * @return mixed - */ - protected function task( $item ) - { - // Actions to perform - if ( ! SmartCAT::is_active() ) { - sleep( 10 ); - - return $item; - } - - Logger::event( "publication", "End queue Start queue '{$item}'"); - - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - - /** @var StatisticRepository $statistic_repository */ - $statistic_repository = $container->get( 'entity.repository.statistic' ); - - /** @var SmartCAT $sc */ - $sc = $container->get( 'smartcat' ); - - $statistics = $statistic_repository->get_one_by( [ 'documentID' => $item ] ); - try { - if ( $statistics && $statistics->get_status() == Statistics::STATUS_SENDED ) { - Logger::event( "publication", "Export '{$item}'"); - - $task = $sc->getDocumentExportManager() - ->documentExportRequestExport( [ 'documentIds' => [ $statistics->get_document_id() ] ] ); - if ( $task->getId() ) { - $statistics->set_status( Statistics::STATUS_EXPORT ) - ->set_error_count( 0 ); - $statistic_repository->update( $statistics ); - /** @var CreatePost $queue */ - $queue = $container->get( 'core.queue.post' ); - - Logger::event( "publication", "Pushing to CreatePost '{$item}'"); - $queue->push_to_queue( [ - 'documentID' => $statistics->get_document_id(), - 'taskID' => $task->getId() - ] ); - Logger::event( "publication", "Pushed to CreatePost '{$item}'"); - } - } - } catch ( ClientErrorException $e ) { - $status_code = $e->getResponse()->getStatusCode(); - if ( $status_code == 404 ) { - $statistic_repository->delete( $statistics ); - Logger::event( "publication", "Deleted '{$item}'"); - } else { - if ( $statistics->get_error_count() < 360 ) { - $statistics->inc_error_count(); - $statistic_repository->update( $statistics ); - sleep( 10 ); - - Logger::event( "publication", "New {$statistics->get_error_count()} try of '{$item}'"); - return $item; - } - Logger::error( - "Document $item, start download translate", - "API error code: {$status_code}. API error message: {$e->getResponse()->getBody()->getContents()}" - ); - } - } catch ( \Throwable $e ) { - Logger::error( - "Document {$item}, publication translate","Message: {$e->getMessage()}" - ); - } - - Logger::event( "publication", "End queue '{$item}'"); - - return false; - } - - /** - * Complete - * - * Override if applicable, but ensure that the below actions are - * performed, or, call parent::complete(). - */ - protected function complete() - { - parent::complete(); - // Show notice to user or perform some other arbitrary task... - /** @var CreatePost $queue */ - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - $queue = $container->get( 'core.queue.post' ); - $queue->save()->dispatch(); - } -} diff --git a/inc/smartcat/Queue/QueueAbstract.php b/inc/smartcat/Queue/QueueAbstract.php deleted file mode 100644 index 626bf2e..0000000 --- a/inc/smartcat/Queue/QueueAbstract.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @copyright (c) 2019 Smartcat. All Rights Reserved. - * @license GNU General Public License version 3 or later; see LICENSE.txt - * @link http://smartcat.ai - */ - -namespace SmartCAT\WP\Queue; - -abstract class QueueAbstract extends \WP_Background_Process -{ -} diff --git a/inc/smartcat/Queue/Statistic.php b/inc/smartcat/Queue/Statistic.php deleted file mode 100644 index bacb764..0000000 --- a/inc/smartcat/Queue/Statistic.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @copyright (c) 2019 Smartcat. All Rights Reserved. - * @license GNU General Public License version 3 or later; see LICENSE.txt - * @link http://smartcat.ai - */ - -namespace SmartCAT\WP\Queue; - -use Http\Client\Common\Exception\ClientErrorException; -use Psr\Container\ContainerInterface; -use SmartCAT\WP\Connector; -use SmartCAT\WP\Helpers\Logger; -use SmartCAT\WP\Helpers\SmartCAT; -use SmartCAT\WP\WP\Options; - -/** - * Class Statistic - * - * @package SmartCAT\WP\Queue - */ -class Statistic extends QueueAbstract { - protected $action = 'smartcat_statistic_async'; - - /** - * Task - * - * Override this method to perform any actions required on each - * queue item. Return the modified item for further processing - * in the next pass through. Or, return false to remove the - * item from the queue. - * - * @param mixed $item Queue item to iterate over - * - * @return mixed - */ - protected function task( $item ) { - if ( SmartCAT::is_active() ) { - try { - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - - /** @var \SmartCAT\WP\Queue\Callback $queue */ - $queue = $container->get( 'core.queue.callback' ); - Logger::event( "statistic", "Send to update statistic '{$item}'"); - $queue->update_statistic( $item ); - - } catch ( ClientErrorException $e ) { - Logger::error( "Document '$item', update statistic", "API error code: {$e->getResponse()->getStatusCode()}. API error message: {$e->getResponse()->getBody()->getContents()}" ); - } catch ( \Throwable $e ) { - Logger::error( "Document '$item', update statistic","Message: {$e->getMessage()}" ); - } - } - - return false; - } - - /** - * Complete - * - * Override if applicable, but ensure that the below actions are - * performed, or, call parent::complete(). - */ - protected function complete() { - parent::complete(); - /** @var ContainerInterface $container */ - $container = Connector::get_container(); - /** @var Publication $queue */ - $queue = $container->get( 'core.queue.publication' ); - $queue->save()->dispatch(); - /** @var Options $options */ - $options = $container->get( 'core.options' ); - $options->set( 'statistic_queue_active', false ); - - // Show notice to user or perform some other arbitrary task... - } -} \ No newline at end of file diff --git a/readme.txt b/readme.txt index 7ff4252..aa3cdc1 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Requires at least: 4.8 Tested up to: 5.2.2 Requires PHP: 7.0 Requires PHP extensions: dom, openssl, json -Stable tag: 2.0.6 +Stable tag: 2.1.0 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -75,6 +75,9 @@ Please wait a few minutes. Data exchange between the plugin and the translation == Changelog == += 2.1.0 (2019-08-28) = +* Disable callbacks, setting up cron only + = 2.0.6 (2019-08-21) = * Callback fix diff --git a/translation-connectors.php b/translation-connectors.php index 3d692fd..9aec40a 100644 --- a/translation-connectors.php +++ b/translation-connectors.php @@ -8,7 +8,7 @@ * Plugin Name: Smartcat Translation Manager * Plugin URI: https://www.smartcat.ai/api/ * Description: WordPress integration to translation connectors. - * Version: 2.0.6 + * Version: 2.1.0 * Author: Smartcat * Author URI: https://www.smartcat.ai * License: GPL-3.0