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,75 @@
<?php
namespace WPML\ST\TranslationFile;
class EntryQueries {
/**
* @param string $type
*
* @return \Closure
*/
public static function isType( $type ) {
return function ( \WPML_ST_Translations_File_Entry $entry ) use ( $type ) {
return $entry->get_component_type() === $type;
};
}
/**
* @param string $extension
*
* @return \Closure
*/
public static function isExtension( $extension ) {
return function ( \WPML_ST_Translations_File_Entry $file ) use ( $extension ) {
return $file->get_extension() === $extension;
};
}
/**
* @return \Closure
*/
public static function getResourceName() {
return function ( \WPML_ST_Translations_File_Entry $entry ) {
$function = 'get' . ucfirst( $entry->get_component_type() ) . 'Name';
return self::$function( $entry );
};
}
/**
* @return \Closure
*/
public static function getDomain() {
return function( \WPML_ST_Translations_File_Entry $file ) {
return $file->get_domain();
};
}
/**
* @param \WPML_ST_Translations_File_Entry $entry
*
* @return string
*/
private static function getPluginName( \WPML_ST_Translations_File_Entry $entry ) {
$data = get_plugin_data( WPML_PLUGINS_DIR . '/' . $entry->get_component_id(), false, false );
return $data['Name'];
}
/**
* @param \WPML_ST_Translations_File_Entry $entry
*
* @return string
*/
private static function getThemeName( \WPML_ST_Translations_File_Entry $entry ) {
return $entry->get_component_id();
}
/**
* @param \WPML_ST_Translations_File_Entry $entry
*
* @return mixed|string|void
*/
private static function getOtherName( \WPML_ST_Translations_File_Entry $entry ) {
return 'WordPress';
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPML\ST\TranslationFile;
use WPML_ST_Translations_File_Entry;
class QueueFilter {
/** @var array */
private $plugins;
/** @var array */
private $themes;
/** @var array */
private $other;
/**
* @param array $plugins
* @param array $themes
* @param array $other
*/
public function __construct( array $plugins, array $themes, array $other ) {
$this->plugins = $plugins;
$this->themes = $themes;
$this->other = $other;
}
/**
* @param WPML_ST_Translations_File_Entry $file
*
* @return bool
*/
public function isSelected( WPML_ST_Translations_File_Entry $file ) {
$getResourceName = EntryQueries::getResourceName();
$resourceName = $getResourceName( $file );
switch ( $file->get_component_type() ) {
case 'plugin':
return in_array( $resourceName, $this->plugins );
case 'theme':
return in_array( $resourceName, $this->themes );
case 'other':
return in_array( $resourceName, $this->other );
}
return false;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace WPML\ST\MO\Scan\UI;
use WPML\Collect\Support\Collection;
use WPML\ST\MO\Generate\DomainsAndLanguagesRepository;
use WPML\ST\MO\Generate\Process\ProcessFactory;
use WPML_ST_Translations_File_Dictionary_Storage_Table;
use WPML_ST_Translations_File_Dictionary;
use WPML\WP\OptionManager;
use function WPML\Container\make;
use function WPML\FP\partial;
class Factory implements \IWPML_Backend_Action_Loader, \IWPML_Deferred_Action_Loader {
const WPML_VERSION_INTRODUCING_ST_MO_FLOW = '4.3.0';
const OPTION_GROUP = 'ST-MO';
const IGNORE_WPML_VERSION = 'ignore-wpml-version';
/**
* @return callable|null
* @throws \WPML\Auryn\InjectionException
*/
public function create() {
if (
current_user_can( 'manage_options' ) &&
function_exists( 'wpml_is_rest_enabled' ) && wpml_is_rest_enabled()
) {
global $sitepress;
$wp_api = $sitepress->get_wp_api();
$pre_gen_dissmissed = self::isDismissed();
$st_page = $wp_api->is_core_page( 'theme-localization.php' ) || $wp_api->is_string_translation_page();
if (
( $st_page || ( ! $st_page && ! $pre_gen_dissmissed ) )
&& ( DomainsAndLanguagesRepository::hasTranslationFilesTable() || is_network_admin() )
) {
$files_to_import = $st_page ? $this->getFilesToImport() : wpml_collect( [] );
$domains_to_pre_generate_count = self::getDomainsToPreGenerateCount();
if ( $files_to_import->count() || $domains_to_pre_generate_count || isset( $_GET['search'] ) ) {
return partial(
[ UI::class, 'add_hooks' ],
Model::provider( $files_to_import, $domains_to_pre_generate_count, $st_page, is_network_admin() ),
$st_page
);
}
}
}
return null;
}
public function get_load_action() {
return 'init';
}
/**
* @return bool
* @throws \WPML\Auryn\InjectionException
*/
public static function isDismissed() {
return make( OptionManager::class )->get( self::OPTION_GROUP, 'pregen-dismissed', false );
}
/**
* @return Collection
* @throws \WPML\Auryn\InjectionException
*/
private function getFilesToImport() {
/** @var WPML_ST_Translations_File_Dictionary $file_dictionary */
$file_dictionary = make(
WPML_ST_Translations_File_Dictionary::class,
[ 'storage' => WPML_ST_Translations_File_Dictionary_Storage_Table::class ]
);
$file_dictionary->clear_skipped();
return InstalledComponents::filter( wpml_collect( $file_dictionary->get_not_imported_files() ) );
}
/**
* @return bool
*/
private static function isPreGenerationRequired() {
return self::shouldIgnoreWpmlVersion() || self::wpmlStartVersionBeforeMOFlow();
}
/**
* @return bool
*/
private static function wpmlStartVersionBeforeMOFlow() {
return version_compare(
get_option( \WPML_Installation::WPML_START_VERSION_KEY, '0.0.0' ),
self::WPML_VERSION_INTRODUCING_ST_MO_FLOW,
'<'
);
}
/**
* @return int
* @throws \WPML\Auryn\InjectionException
*/
public static function getDomainsToPreGenerateCount() {
return self::isPreGenerationRequired() ? make( ProcessFactory::class )->create()->getPagesCount() : 0;
}
/**
* @return bool
*/
public static function shouldIgnoreWpmlVersion() {
return make( OptionManager::class )->get( self::OPTION_GROUP, self::IGNORE_WPML_VERSION, false );
}
public static function ignoreWpmlVersion() {
make( OptionManager::class )->set( self::OPTION_GROUP, self::IGNORE_WPML_VERSION, true );
}
public static function clearIgnoreWpmlVersion() {
make( OptionManager::class )->set( self::OPTION_GROUP, self::IGNORE_WPML_VERSION, false );
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* @author OnTheGo Systems
*/
namespace WPML\ST\MO\Scan\UI;
use WPML\Collect\Support\Collection;
use WPML_ST_Translations_File_Entry;
class InstalledComponents {
/**
* @param Collection $components Collection of WPML_ST_Translations_File_Entry objects.
*
* @return Collection
*/
public static function filter( Collection $components ) {
return $components
->reject( self::isPluginMissing() )
->reject( self::isThemeMissing() );
}
/**
* WPML_ST_Translations_File_Entry -> bool
*
* @return \Closure
*/
public static function isPluginMissing() {
return function( WPML_ST_Translations_File_Entry $entry ) {
return 'plugin' === $entry->get_component_type()
&& ! is_readable( WPML_PLUGINS_DIR . '/' . $entry->get_component_id() );
};
}
/**
* WPML_ST_Translations_File_Entry -> bool
*
* @return \Closure
*/
public static function isThemeMissing() {
return function( WPML_ST_Translations_File_Entry $entry ) {
return 'theme' === $entry->get_component_type()
&& ! wp_get_theme( $entry->get_component_id() )->exists();
};
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace WPML\ST\MO\Scan\UI;
use WPML\Collect\Support\Collection;
use WPML\ST\MO\Generate\MultiSite\Condition;
use WPML\ST\TranslationFile\EntryQueries;
class Model {
/**
* @param Collection $files_to_scan
* @param int $domains_to_pre_generate_count
* @param bool $is_st_page
* @param bool $is_network_admin
*
* @return \Closure
*/
public static function provider(
Collection $files_to_scan,
$domains_to_pre_generate_count,
$is_st_page,
$is_network_admin
) {
return function () use ( $files_to_scan, $domains_to_pre_generate_count, $is_st_page ) {
return [
'files_to_scan' => [
'count' => $files_to_scan->count(),
'plugins' => self::filterFilesByType( $files_to_scan, 'plugin' ),
'themes' => self::filterFilesByType( $files_to_scan, 'theme' ),
'other' => self::filterFilesByType( $files_to_scan, 'other' ),
],
'domains_to_pre_generate_count' => $domains_to_pre_generate_count,
'file_path' => WP_LANG_DIR . '/wpml',
'is_st_page' => $is_st_page,
'admin_texts_url' => 'admin.php?page=' . WPML_ST_FOLDER . '/menu/string-translation.php&trop=1',
'is_search' => isset( $_GET['search'] ),
'run_ror_all_sites' => ( new Condition() )->shouldRunWithAllSites(),
];
};
}
/**
* @param Collection $files_to_scan
* @param string $type
*
* @return array
*/
private static function filterFilesByType( Collection $files_to_scan, $type ) {
return $files_to_scan->filter( EntryQueries::isType( $type ) )
->map( EntryQueries::getResourceName() )
->unique()
->values()
->toArray();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace WPML\ST\MO\Scan\UI;
use \WPML\ST\WP\App\Resources;
use WPML\LIB\WP\Hooks as WPHooks;
class UI {
public static function add_hooks( callable $getModel, $isSTPage ) {
WPHooks::onAction( 'admin_enqueue_scripts' )
->then( $getModel )
->then( [ self::class, 'localize' ] )
->then( Resources::enqueueApp( 'mo-scan' ) );
if ( ! $isSTPage ) {
WPHooks::onAction( [ 'admin_notices', 'network_admin_notices' ] )
->then( [ self::class, 'add_admin_notice' ] );
}
}
public static function add_admin_notice() {
?>
<div id="wpml-mo-scan-st-page"></div>
<?php
}
public static function localize( $model ) {
return [
'name' => 'wpml_mo_scan_ui_files',
'data' => $model,
];
}
}

View File

@@ -0,0 +1,8 @@
<?php
interface WPML_ST_Translations_File_Scan_Charset_Validation {
/**
* @return bool
*/
public function is_valid();
}

View File

@@ -0,0 +1,18 @@
<?php
class WPML_ST_Translations_File_Scan_Db_Charset_Filter_Factory {
/** @var wpdb $wpdb */
private $wpdb;
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
public function create() {
$charset_validator = new WPML_ST_Translations_File_Scan_Db_Charset_Validation( $this->wpdb, new WPML_ST_Translations_File_Scan_Db_Table_List( $this->wpdb ) );
return $charset_validator->is_valid() ? null : new WPML_ST_Translations_File_Unicode_Characters_Filter();
}
}

View File

@@ -0,0 +1,51 @@
<?php
class WPML_ST_Translations_File_Scan_Db_Charset_Validation implements WPML_ST_Translations_File_Scan_Charset_Validation {
/** @var wpdb */
private $wpdb;
/** @var WPML_ST_Translations_File_Scan_Db_Table_List */
private $table_list;
/**
* @param wpdb $wpdb
* @param WPML_ST_Translations_File_Scan_Db_Table_List $table_list
*/
public function __construct( wpdb $wpdb, WPML_ST_Translations_File_Scan_Db_Table_List $table_list ) {
$this->wpdb = $wpdb;
$this->table_list = $table_list;
}
/**
* @return bool
*/
public function is_valid() {
if ( ! $this->wpdb->has_cap( 'utf8mb4' ) ) {
return false;
}
$chunks = array();
foreach ( $this->table_list->get_tables() as $table ) {
$chunks[] = $this->get_unique_collation_list_from_table( $table );
}
$collations = call_user_func_array( 'array_merge', $chunks );
$collations = array_unique( $collations );
return count( $collations ) < 2;
}
/**
* @param string $table
*
* @return array
*/
private function get_unique_collation_list_from_table( $table ) {
$columns = $this->wpdb->get_results( "SHOW FULL COLUMNS FROM `{$table}` WHERE Collation LIKE 'utf8mb4%'" );
return array_unique( wp_list_pluck( $columns, 'Collation' ) );
}
}

View File

@@ -0,0 +1,23 @@
<?php
class WPML_ST_Translations_File_Scan_Db_Table_List {
/** @var wpdb */
private $wpdb;
/**
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @return array
*/
public function get_tables() {
return array(
$this->wpdb->prefix . 'icl_strings',
$this->wpdb->prefix . 'icl_string_translations',
);
}
}

View File

@@ -0,0 +1,125 @@
<?php
class WPML_ST_Translations_File_Component_Details {
/** @var WPML_ST_Translations_File_Components_Find[] */
private $finders;
/** @var WPML_File $file */
private $file;
/** @var string */
private $plugin_dir;
/** @var string */
private $theme_dir;
/** @var string */
private $languages_plugin_dir;
/** @var string */
private $languages_theme_dir;
/** @var array */
private $cache = array();
/**
* @param WPML_ST_Translations_File_Components_Find_Plugin $plugin_id_finder
* @param WPML_ST_Translations_File_Components_Find_Theme $theme_id_finder
* @param WPML_File $wpml_file
*/
public function __construct(
WPML_ST_Translations_File_Components_Find_Plugin $plugin_id_finder,
WPML_ST_Translations_File_Components_Find_Theme $theme_id_finder,
WPML_File $wpml_file
) {
$this->finders['plugin'] = $plugin_id_finder;
$this->finders['theme'] = $theme_id_finder;
$this->file = $wpml_file;
$this->theme_dir = $this->file->fix_dir_separator( get_theme_root() );
$this->plugin_dir = $this->file->fix_dir_separator( realpath( WPML_PLUGINS_DIR ) );
$wp_content_dir = realpath( WP_CONTENT_DIR );
$this->languages_plugin_dir = $this->file->fix_dir_separator( $wp_content_dir . '/languages/plugins' );
$this->languages_theme_dir = $this->file->fix_dir_separator( $wp_content_dir . '/languages/themes' );
}
/**
* @param string $file_full_path
*
* @return array
*/
public function find_details( $file_full_path ) {
$file_full_path = $this->file->fix_dir_separator( $file_full_path );
if ( ! isset( $this->cache[ $file_full_path ] ) ) {
$type = $this->find_type( $file_full_path );
if ( 'other' === $type ) {
$this->cache[ $file_full_path ] = array( $type, null );
} else {
$this->cache[ $file_full_path ] = array( $type, $this->find_id( $type, $file_full_path ) );
}
}
return $this->cache[ $file_full_path ];
}
/**
* @param string $component_type
* @param string $file_full_path
*
* @return null|string
*/
public function find_id( $component_type, $file_full_path ) {
if ( ! isset( $this->finders[ $component_type ] ) ) {
return null;
}
return $this->finders[ $component_type ]->find_id( $file_full_path );
}
/**
* @param string $file_full_path
*
* @return string
*/
public function find_type( $file_full_path ) {
if ( $this->theme_dir && ( $this->string_contains( $file_full_path, $this->theme_dir ) || $this->string_contains( $file_full_path, $this->languages_theme_dir ) ) ) {
return 'theme';
}
if ( $this->string_contains( $file_full_path, $this->plugin_dir ) || $this->string_contains( $file_full_path, $this->languages_plugin_dir ) ) {
return 'plugin';
}
return 'other';
}
/**
* @param string $file_full_path
*
* @return bool
*/
public function is_component_active( $file_full_path ) {
list( $type, $id ) = $this->find_details( $file_full_path );
if ( 'other' === $type ) {
return true;
}
if ( ! $id ) {
return false;
}
if ( 'plugin' === $type ) {
return is_plugin_active( $id );
} else {
return get_stylesheet_directory() === get_theme_root() . '/' . $id;
}
}
private function string_contains( $haystack, $needle ) {
return false !== strpos( $haystack, $needle );
}
}

View File

@@ -0,0 +1,96 @@
<?php
class WPML_ST_Translations_File_Components_Find_Plugin implements WPML_ST_Translations_File_Components_Find {
/** @var WPML_Debug_BackTrace */
private $debug_backtrace;
/** @var string */
private $plugin_dir;
/** @var array */
private $plugin_ids;
/**
* @param WPML_Debug_BackTrace $debug_backtrace
*/
public function __construct( WPML_Debug_BackTrace $debug_backtrace ) {
$this->debug_backtrace = $debug_backtrace;
$this->plugin_dir = realpath( WPML_PLUGINS_DIR );
}
public function find_id( $file ) {
$directory = $this->find_plugin_directory( $file );
if ( ! $directory ) {
return null;
}
return $this->get_plugin_id_by_directory( $directory );
}
private function find_plugin_directory( $file ) {
if ( false !== strpos( $file, $this->plugin_dir ) ) {
return $this->extract_plugin_directory( $file );
}
return $this->find_plugin_directory_in_backtrace();
}
private function find_plugin_directory_in_backtrace() {
$file = $this->find_file_in_backtrace();
if ( ! $file ) {
return null;
}
return $this->extract_plugin_directory( $file );
}
private function find_file_in_backtrace() {
$stack = $this->debug_backtrace->get_backtrace();
foreach ( $stack as $call ) {
if ( isset( $call['function'] ) && 'load_plugin_textdomain' === $call['function'] ) {
return $call['file'];
}
}
return null;
}
/**
* @param string $file_path
*
* @return string
*/
private function extract_plugin_directory( $file_path ) {
$dir = ltrim( str_replace( $this->plugin_dir, '', $file_path ), DIRECTORY_SEPARATOR );
$dir = explode( DIRECTORY_SEPARATOR, $dir );
return trim( $dir[0], DIRECTORY_SEPARATOR );
}
/**
* @param string $directory
*
* @return string|null
*/
private function get_plugin_id_by_directory( $directory ) {
foreach ( $this->get_plugin_ids() as $plugin_id ) {
if ( 0 === strpos( $plugin_id, $directory . '/' ) ) {
return $plugin_id;
}
}
return null;
}
/**
* @return string[]
*/
private function get_plugin_ids() {
if ( null === $this->plugin_ids ) {
$this->plugin_ids = array_keys( get_plugins() );
}
return $this->plugin_ids;
}
}

View File

@@ -0,0 +1,69 @@
<?php
class WPML_ST_Translations_File_Components_Find_Theme implements WPML_ST_Translations_File_Components_Find {
/** @var WPML_Debug_BackTrace */
private $debug_backtrace;
/** @var WPML_File $file */
private $file;
/** @var string */
private $theme_dir;
/**
* @param WPML_Debug_BackTrace $debug_backtrace
* @param WPML_File $file
*/
public function __construct( WPML_Debug_BackTrace $debug_backtrace, WPML_File $file ) {
$this->debug_backtrace = $debug_backtrace;
$this->file = $file;
$this->theme_dir = $this->file->fix_dir_separator( get_theme_root() );
}
public function find_id( $file ) {
return $this->find_theme_directory( $file );
}
private function find_theme_directory( $file ) {
if ( false !== strpos( $file, $this->theme_dir ) ) {
return $this->extract_theme_directory( $file );
}
return $this->find_theme_directory_in_backtrace();
}
private function find_theme_directory_in_backtrace() {
$file = $this->find_file_in_backtrace();
if ( ! $file ) {
return null;
}
return $this->extract_theme_directory( $file );
}
private function find_file_in_backtrace() {
$stack = $this->debug_backtrace->get_backtrace();
foreach ( $stack as $call ) {
if ( isset( $call['function'] ) && 'load_theme_textdomain' === $call['function'] ) {
return $call['file'];
}
}
return null;
}
/**
* @param string $file_path
*
* @return string
*/
private function extract_theme_directory( $file_path ) {
$file_path = $this->file->fix_dir_separator( $file_path );
$dir = ltrim( str_replace( $this->theme_dir, '', $file_path ), DIRECTORY_SEPARATOR );
$dir = explode( DIRECTORY_SEPARATOR, $dir );
return trim( $dir[0], DIRECTORY_SEPARATOR );
}
}

View File

@@ -0,0 +1,10 @@
<?php
interface WPML_ST_Translations_File_Components_Find {
/**
* @param string $file
*
* @return string|null
*/
public function find_id( $file );
}

View File

@@ -0,0 +1,136 @@
<?php
class WPML_ST_Translations_File_Dictionary_Storage_Table implements WPML_ST_Translations_File_Dictionary_Storage {
/** @var wpdb */
private $wpdb;
/** @var null|array */
private $data;
/** @var WPML_ST_Translations_File_Entry[] */
private $new_data = array();
/** @var WPML_ST_Translations_File_Entry[] */
private $updated_data = array();
/**
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
public function add_hooks() {
add_action( 'shutdown', array( $this, 'persist' ), 11, 0 );
}
public function save( WPML_ST_Translations_File_Entry $file ) {
$this->load_data();
$is_new = ! isset( $this->data[ $file->get_path() ] );
$this->data[ $file->get_path() ] = $file;
if ( $is_new ) {
$this->new_data[] = $file;
} else {
$this->updated_data[] = $file;
}
}
/**
* We have to postpone saving of real data because target table may not be created yet by migration process
*/
public function persist() {
foreach ( $this->new_data as $file ) {
$sql = "INSERT IGNORE INTO {$this->wpdb->prefix}icl_mo_files_domains ( file_path, file_path_md5, domain, status, num_of_strings, last_modified, component_type, component_id ) VALUES ( %s, %s, %s, %s, %d, %d, %s, %s )";
$this->wpdb->query(
$this->wpdb->prepare(
$sql,
array(
$file->get_path(),
$file->get_path_hash(),
$file->get_domain(),
$file->get_status(),
$file->get_imported_strings_count(),
$file->get_last_modified(),
$file->get_component_type(),
$file->get_component_id(),
)
)
);
}
foreach ( $this->updated_data as $file ) {
$this->wpdb->update(
$this->wpdb->prefix . 'icl_mo_files_domains',
$this->file_to_array( $file ),
array(
'file_path_md5' => $file->get_path_hash(),
),
array( '%s', '%s', '%d', '%d' )
);
}
}
/**
* @param WPML_ST_Translations_File_Entry $file
* @param array $data
*
* @return array
*/
private function file_to_array( WPML_ST_Translations_File_Entry $file, array $data = array() ) {
$data['domain'] = $file->get_domain();
$data['status'] = $file->get_status();
$data['num_of_strings'] = $file->get_imported_strings_count();
$data['last_modified'] = $file->get_last_modified();
return $data;
}
public function find( $path = null, $status = null ) {
$this->load_data();
if ( null !== $path ) {
return isset( $this->data[ $path ] ) ? array( $this->data[ $path ] ) : array();
}
if ( null === $status ) {
return array_values( $this->data );
}
if ( ! is_array( $status ) ) {
$status = array( $status );
}
$result = array();
foreach ( $this->data as $file ) {
if ( in_array( $file->get_status(), $status, true ) ) {
$result[] = $file;
}
}
return $result;
}
private function load_data() {
if ( null === $this->data ) {
$this->data = array();
$sql = "SELECT * FROM {$this->wpdb->prefix}icl_mo_files_domains";
$rowset = $this->wpdb->get_results( $sql );
foreach ( $rowset as $row ) {
$file = new WPML_ST_Translations_File_Entry( $row->file_path, $row->domain, $row->status );
$file->set_imported_strings_count( $row->num_of_strings );
$file->set_last_modified( $row->last_modified );
$file->set_component_type( $row->component_type );
$file->set_component_id( $row->component_id );
$this->data[ $file->get_path() ] = $file;
}
}
}
public function reset() {
$this->data = null;
}
}

View File

@@ -0,0 +1,14 @@
<?php
interface WPML_ST_Translations_File_Dictionary_Storage {
public function save( WPML_ST_Translations_File_Entry $file );
/**
* @param null|string $path
* @param null|string $status
*
* @return WPML_ST_Translations_File_Entry[]
*/
public function find( $path = null, $status = null );
}

View File

@@ -0,0 +1,9 @@
<?php
interface IWPML_ST_Translations_File {
/**
* @return WPML_ST_Translations_File_Translation[]
*/
public function get_translations();
}

View File

@@ -0,0 +1,78 @@
<?php
class WPML_ST_Translations_File_JED implements IWPML_ST_Translations_File {
const EMPTY_PROPERTY_NAME = '_empty_';
const DECODED_EOT_CHAR = '"\u0004"';
const PLURAL_SUFFIX_PATTERN = ' [plural %d]';
/** @var string $filepath */
private $filepath;
/** @var string $decoded_eot_char */
private $decoded_eot_char;
public function __construct( $filepath ) {
$this->filepath = $filepath;
$this->decoded_eot_char = json_decode( self::DECODED_EOT_CHAR );
}
/**
* @return WPML_ST_Translations_File_Translation[]
*/
public function get_translations() {
$translations = array();
$data = json_decode( file_get_contents( $this->filepath ) );
if ( isset( $data->locale_data->messages ) ) {
$entries = (array) $data->locale_data->messages;
unset( $entries[ self::EMPTY_PROPERTY_NAME ] );
foreach ( $entries as $str => $str_data ) {
$str_data = (array) $str_data;
if ( ! isset( $str_data[0] ) ) {
continue;
}
list( $str, $context ) = $this->get_string_and_context( $str );
$count_translations = count( $str_data );
$translations[] = new WPML_ST_Translations_File_Translation( $str, $str_data[0], $context );
if ( $count_translations > 1 ) {
/**
* The strings coming after the first element are the plural translations.
* As we don't have the information about the original plural in the JED file,
* we will add a suffix to the original singular string.
*/
for ( $i = 1; $i < $count_translations; $i++ ) {
$plural_str = $str . sprintf( self::PLURAL_SUFFIX_PATTERN, $i );
$translations[] = new WPML_ST_Translations_File_Translation( $plural_str, $str_data[ $i ], $context );
}
}
}
}
return $translations;
}
/**
* The context is the first part of the string separated with the EOT char (\u0004)
*
* @param string $string
*
* @return array
*/
private function get_string_and_context( $string ) {
$context = '';
$parts = explode( $this->decoded_eot_char, $string );
if ( count( $parts ) > 1 ) {
$context = $parts[0];
$string = $parts[1];
}
return array( $string, $context );
}
}

View File

@@ -0,0 +1,146 @@
<?php
class WPML_ST_Translations_File_Locale {
const PATTERN_SEARCH_LANG_JSON = '#DOMAIN_PLACEHOLDER(LOCALES_PLACEHOLDER)-[-_a-z0-9]+\.json$#i';
/** @var \SitePress */
private $sitepress;
/** @var \WPML_Locale */
private $locale;
/**
* @param SitePress $sitepress
* @param WPML_Locale $locale
*/
public function __construct( SitePress $sitepress, WPML_Locale $locale ) {
$this->sitepress = $sitepress;
$this->locale = $locale;
}
/**
* It extracts language code from mo file path, examples
* '/wp-content/languages/admin-pl_PL.mo' => 'pl'
* '/wp-content/plugins/sitepress/sitepress-hr.mo' => 'hr'
* '/wp-content/languages/fr_FR-4gh5e6d3g5s33d6gg51zas2.json' => 'fr_FR'
* '/wp-content/plugins/my-plugin/languages/-my-plugin-fr_FR-my-handler.json' => 'fr_FR'
*
* @param string $filepath
* @param string $domain
*
* @return string
*/
public function get( $filepath, $domain ) {
switch ( $this->get_extension( $filepath ) ) {
case 'mo':
return $this->get_from_mo_file( $filepath );
case 'json':
return $this->get_from_json_file( $filepath, $domain );
default:
return '';
}
}
/**
* @param string $filepath
*
* @return string|null
*/
private function get_extension( $filepath ) {
return wpml_collect( pathinfo( $filepath ) )->get( 'extension', null );
}
/**
* @param string $filepath
*
* @return string
*/
private function get_from_mo_file( $filepath ) {
return $this->get_locales()
->first(
function ( $locale ) use ( $filepath ) {
return strpos( $filepath, $locale . '.mo' );
},
''
);
}
/**
* @param string $filepath
* @param string $domain
*
* @return string
*/
private function get_from_json_file( $filepath, $domain ) {
$original_domain = $this->get_original_domain_for_json( $filepath, $domain );
$domain_replace = 'default' === $original_domain ? '' : $original_domain . '-';
$locales = $this->get_locales()->implode( '|' );
$searches['native-file'] = '#' . $domain_replace . '(' . $locales . ')-[-_a-z0-9]+\.json$#i';
$searches['wpml-file'] = '#' . $domain . '-(' . $locales . ').json#i';
foreach ( $searches as $search ) {
if ( preg_match( $search, $filepath, $matches ) && isset( $matches[1] ) ) {
return $matches[1];
}
}
return '';
}
/**
* We need the original domain name to refine the regex pattern.
* Unfortunately, the domain is concatenated with the script handler
* in the import queue table. That's why we need to retrieve the original
* domain from the registration domain and the filepath.
*
* @param string $filepath
* @param string $domain
*
* @return string
*/
private function get_original_domain_for_json( $filepath, $domain ) {
$filename = basename( $filepath );
/**
* Case of WP JED files:
* - filename: de_DE-73f9977556584a369800e775b48f3dbe.json
* - domain: default-some-script
* => original_domain: default
*/
if ( 0 === strpos( $domain, 'default' ) && 0 !== strpos( $filename, 'default' ) ) {
return 'default';
}
/**
* Case of 3rd part JED files:
* - filename: plugin-domain-de_DE-script-handler.json
* - domain: plugin-domain-script-handler
* => original_domain: plugin-domain
*/
$filename_parts = explode( '-', $filename );
$domain_parts = explode( '-', $domain );
$original_domain_parts = array();
foreach ( $domain_parts as $i => $part ) {
if ( $filename_parts[ $i ] !== $part ) {
break;
}
$original_domain_parts[] = $part;
}
return implode( '-', $original_domain_parts );
}
/**
* @return \WPML\Collect\Support\Collection
*/
private function get_locales() {
return \wpml_collect( $this->sitepress->get_active_languages() )
->keys()
->map( [ $this->locale, 'get_locale' ] );
}
}

View File

@@ -0,0 +1,38 @@
<?php
class WPML_ST_Translations_File_MO implements IWPML_ST_Translations_File {
/** @var string $filepath */
private $filepath;
/**
* @param string $filepath
*/
public function __construct( $filepath ) {
$this->filepath = $filepath;
}
/**
* @return WPML_ST_Translations_File_Translation[]
*/
public function get_translations() {
$translations = array();
$mo = new MO();
$pomo_reader = new POMO_CachedFileReader( $this->filepath );
$mo->import_from_reader( $pomo_reader );
foreach ( $mo->entries as $str => $v ) {
$str = str_replace( "\n", '\n', $v->singular );
$translations[] = new WPML_ST_Translations_File_Translation( $str, $v->translations[0], $v->context );
if ( $v->is_plural ) {
$str = str_replace( "\n", '\n', $v->plural );
$translation = ! empty( $v->translations[1] ) ? $v->translations[1] : $v->translations[0];
$translations[] = new WPML_ST_Translations_File_Translation( $str, $translation, $v->context );
}
}
return $translations;
}
}

View File

@@ -0,0 +1,26 @@
<?php
class WPML_ST_Translations_File_Component_Stats_Update_Hooks {
/** @var WPML_ST_Strings_Stats */
private $string_stats;
/**
* @param WPML_ST_Strings_Stats $string_stats
*/
public function __construct( WPML_ST_Strings_Stats $string_stats ) {
$this->string_stats = $string_stats;
}
public function add_hooks() {
add_action( 'wpml_st_translations_file_post_import', array( $this, 'update_stats' ), 10, 1 );
}
/**
* @param WPML_ST_Translations_File_Entry $file
*/
public function update_stats( WPML_ST_Translations_File_Entry $file ) {
if ( $file->get_component_id() ) {
$this->string_stats->update( $file->get_component_id(), $file->get_component_type(), $file->get_domain() );
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
use WPML\ST\TranslationFile\EntryQueries;
class WPML_ST_Translations_File_Dictionary {
/** @var WPML_ST_Translations_File_Dictionary_Storage */
private $storage;
/**
* @param WPML_ST_Translations_File_Dictionary_Storage $storage
*/
public function __construct( WPML_ST_Translations_File_Dictionary_Storage $storage ) {
$this->storage = $storage;
}
/**
* @param string $file_path
*
* @return WPML_ST_Translations_File_Entry|null
*/
public function find_file_info_by_path( $file_path ) {
$result = $this->storage->find( $file_path );
if ( $result ) {
return current( $result );
}
return null;
}
/**
* @param WPML_ST_Translations_File_Entry $file
*/
public function save( WPML_ST_Translations_File_Entry $file ) {
$this->storage->save( $file );
}
/**
* @return WPML_ST_Translations_File_Entry[]
*/
public function get_not_imported_files() {
return $this->storage->find(
null,
[
WPML_ST_Translations_File_Entry::NOT_IMPORTED,
WPML_ST_Translations_File_Entry::PARTLY_IMPORTED,
]
);
}
public function clear_skipped() {
$skipped = wpml_collect( $this->storage->find( null, [ WPML_ST_Translations_File_Entry::SKIPPED ] ) );
$skipped->each(
function ( WPML_ST_Translations_File_Entry $entry ) {
$entry->set_status( WPML_ST_Translations_File_Entry::NOT_IMPORTED );
$this->storage->save( $entry );
}
);
}
/**
* @return WPML_ST_Translations_File_Entry[]
*/
public function get_imported_files() {
return $this->storage->find( null, WPML_ST_Translations_File_Entry::IMPORTED );
}
/**
* @param null|string $extension
* @param null|string $locale
*
* @return array
*/
public function get_domains( $extension = null, $locale = null ) {
$files = wpml_collect( $this->storage->find() );
if ( $extension ) {
$files = $files->filter( EntryQueries::isExtension( $extension ) );
}
if ( $locale ) {
$files = $files->filter(
function ( WPML_ST_Translations_File_Entry $file ) use ( $locale ) {
return $file->get_file_locale() === $locale;
}
);
}
return $files->map( EntryQueries::getDomain() )
->unique()
->values()
->toArray();
}
}

View File

@@ -0,0 +1,219 @@
<?php
class WPML_ST_Translations_File_Entry {
const NOT_IMPORTED = 'not_imported';
const IMPORTED = 'imported';
const PARTLY_IMPORTED = 'partly_imported';
const FINISHED = 'finished';
const SKIPPED = 'skipped';
const PATTERN_SEARCH_LANG_MO = '#[-]?([a-z]+[_A-Z]*)\.mo$#i';
const PATTERN_SEARCH_LANG_JSON = '#([a-z]+[_A-Z]*)-[-a-z0-9]+\.json$#i';
/** @var string */
private $path;
/** @var string */
private $domain;
/** @var int */
private $status;
/** @var int */
private $imported_strings_count = 0;
/** @var int */
private $last_modified;
/** @var string */
private $component_type;
/** @var string */
private $component_id;
/**
* @param string $path
* @param string $domain
* @param string $status
*/
public function __construct( $path, $domain, $status = self::NOT_IMPORTED ) {
if ( ! is_string( $path ) ) {
throw new InvalidArgumentException( 'MO File path must be string type' );
}
if ( ! is_string( $domain ) ) {
throw new InvalidArgumentException( 'MO File domain must be string type' );
}
$this->path = $this->convert_to_relative_path( $path );
$this->domain = $domain;
$this->validate_status( $status );
$this->status = $status;
}
/**
* We can't rely on ABSPATH in out tests
*
* @param string $path
*
* @return string
*/
private function convert_to_relative_path( $path ) {
$parts = explode( DIRECTORY_SEPARATOR, $this->fix_dir_separator( WP_CONTENT_DIR ) );
return str_replace( WP_CONTENT_DIR, end( $parts ), $path );
}
/**
* @return string
*/
public function get_path() {
return $this->path;
}
public function get_full_path() {
$wp_content_dir = $this->fix_dir_separator( WP_CONTENT_DIR );
$parts = explode( DIRECTORY_SEPARATOR, $wp_content_dir );
return str_replace( end( $parts ), $wp_content_dir, $this->path );
}
/**
* @return string
*/
public function get_path_hash() {
return md5( $this->path );
}
/**
* @return string
*/
public function get_domain() {
return $this->domain;
}
/**
* @return int
*/
public function get_status() {
return $this->status;
}
/**
* @param int $status
*/
public function set_status( $status ) {
$this->validate_status( $status );
$this->status = $status;
}
/**
* @return int
*/
public function get_imported_strings_count() {
return $this->imported_strings_count;
}
/**
* @param int $imported_strings_count
*/
public function set_imported_strings_count( $imported_strings_count ) {
$this->imported_strings_count = (int) $imported_strings_count;
}
/**
* @return int
*/
public function get_last_modified() {
return $this->last_modified;
}
/**
* @param int $last_modified
*/
public function set_last_modified( $last_modified ) {
$this->last_modified = (int) $last_modified;
}
public function __get( $name ) {
if ( in_array( $name, array( 'path', 'domain', 'status', 'imported_strings_count', 'last_modified' ), true ) ) {
return $this->$name;
}
if ( $name === 'path_md5' ) {
return $this->get_path_hash();
}
return null;
}
/**
* It extracts locale from mo file path, examples
* '/wp-content/languages/admin-pl_PL.mo' => 'pl'
* '/wp-content/plugins/sitepress/sitepress-hr.mo' => 'hr'
*
* @return null|string
* @throws RuntimeException
*/
public function get_file_locale() {
return \WPML\Container\make( WPML_ST_Translations_File_Locale::class )->get( $this->get_path(), $this->get_domain() );
}
/**
* @return string
*/
public function get_component_type() {
return $this->component_type;
}
/**
* @param string $component_type
*/
public function set_component_type( $component_type ) {
$this->component_type = $component_type;
}
/**
* @return string
*/
public function get_component_id() {
return $this->component_id;
}
/**
* @param string $component_id
*/
public function set_component_id( $component_id ) {
$this->component_id = $component_id;
}
/**
* @param string $status
*/
private function validate_status( $status ) {
$allowed_statuses = array(
self::NOT_IMPORTED,
self::IMPORTED,
self::PARTLY_IMPORTED,
self::FINISHED,
self::SKIPPED,
);
if ( ! in_array( $status, $allowed_statuses, true ) ) {
throw new InvalidArgumentException( 'Status of MO file is invalid' );
}
}
/**
* @param string $path
*
* @return string
*/
private function fix_dir_separator( $path ) {
return ( '\\' === DIRECTORY_SEPARATOR ) ? str_replace( '/', '\\', $path ) : str_replace( '\\', '/', $path );
}
public function get_extension() {
return pathinfo( $this->path, PATHINFO_EXTENSION );
}
}

View File

@@ -0,0 +1,195 @@
<?php
use WPML\ST\TranslationFile\EntryQueries;
use WPML\ST\TranslationFile\QueueFilter;
class WPML_ST_Translations_File_Queue {
const DEFAULT_LIMIT = 20000;
const TIME_LIMIT = 10; // seconds
const LOCK_FIELD = '_wpml_st_file_scan_in_progress';
/** @var WPML_ST_Translations_File_Dictionary */
private $file_dictionary;
/** @var WPML_ST_Translations_File_Scan */
private $file_scan;
/** @var WPML_ST_Translations_File_Scan_Storage */
private $file_scan_storage;
/** @var WPML_Language_Records */
private $language_records;
/** @var int */
private $limit;
private $transient;
/**
* @param WPML_ST_Translations_File_Dictionary $file_dictionary
* @param WPML_ST_Translations_File_Scan $file_scan
* @param WPML_ST_Translations_File_Scan_Storage $file_scan_storage
* @param WPML_Language_Records $language_records
* @param int $limit
* @param WPML_Transient $transient
*/
public function __construct(
WPML_ST_Translations_File_Dictionary $file_dictionary,
WPML_ST_Translations_File_Scan $file_scan,
WPML_ST_Translations_File_Scan_Storage $file_scan_storage,
WPML_Language_Records $language_records,
$limit,
WPML_Transient $transient
) {
$this->file_dictionary = $file_dictionary;
$this->file_scan = $file_scan;
$this->file_scan_storage = $file_scan_storage;
$this->language_records = $language_records;
$this->limit = $limit;
$this->transient = $transient;
}
/**
* @param QueueFilter|null $queueFilter
*/
public function import( QueueFilter $queueFilter = null ) {
$this->file_dictionary->clear_skipped();
$files = $this->file_dictionary->get_not_imported_files();
if ( count( $files ) ) {
$this->lock();
$start_time = time();
$imported = 0;
foreach ( $files as $file ) {
if ( $imported >= $this->limit || time() - $start_time > self::TIME_LIMIT ) {
break;
}
if ( ! $queueFilter || $queueFilter->isSelected( $file ) ) {
$translations = $this->file_scan->load_translations( $file->get_full_path() );
try {
$number_of_translations = count( $translations );
if ( ! $number_of_translations ) {
throw new RuntimeException( 'File is empty' );
}
$translations = $this->constrain_translations_number(
$translations,
$file->get_imported_strings_count(),
$this->limit - $imported
);
$imported += $imported_in_file = count( $translations );
$this->file_scan_storage->save(
$translations,
$file->get_domain(),
$this->map_language_code( $file->get_file_locale() )
);
$file->set_imported_strings_count( $file->get_imported_strings_count() + $imported_in_file );
if ( $file->get_imported_strings_count() >= $number_of_translations ) {
$file->set_status( WPML_ST_Translations_File_Entry::IMPORTED );
} else {
$file->set_status( WPML_ST_Translations_File_Entry::PARTLY_IMPORTED );
}
} catch ( WPML_ST_Bulk_Strings_Insert_Exception $e ) {
$file->set_status( WPML_ST_Translations_File_Entry::PARTLY_IMPORTED );
break;
} catch ( Exception $e ) {
$file->set_status( WPML_ST_Translations_File_Entry::IMPORTED );
}
} else {
$file->set_status( WPML_ST_Translations_File_Entry::SKIPPED );
}
$this->file_dictionary->save( $file );
do_action( 'wpml_st_translations_file_post_import', $file );
}
$this->unlock();
}
}
/**
* @param string $locale
*
* @return string
*/
private function map_language_code( $locale ) {
$language_code = $this->language_records->get_language_code( $locale );
if ( $language_code ) {
return $language_code;
}
return $locale;
}
/**
* @return bool
*/
public function is_completed() {
return 0 === count( $this->file_dictionary->get_not_imported_files() ) &&
0 < count( $this->file_dictionary->get_imported_files() );
}
/**
* @return string[]
*/
public function get_processed() {
return wp_list_pluck( $this->file_dictionary->get_imported_files(), 'path' );
}
/**
* @return bool
*/
public function is_processing() {
return 0 !== count( $this->file_dictionary->get_not_imported_files() );
}
/**
* @return int
*/
public function get_pending() {
return count( $this->file_dictionary->get_not_imported_files() );
}
public function mark_as_finished() {
foreach ( $this->file_dictionary->get_imported_files() as $file ) {
$file->set_status( WPML_ST_Translations_File_Entry::FINISHED );
$this->file_dictionary->save( $file );
}
}
/**
* @param array $translations
* @param int $offset
* @param int $limit
*
* @return array
*/
private function constrain_translations_number( array $translations, $offset, $limit ) {
if ( $limit > count( $translations ) ) {
return $translations;
}
return array_slice( $translations, $offset, $limit );
}
public function is_locked() {
return (bool) $this->transient->get( self::LOCK_FIELD );
}
private function lock() {
$this->transient->set( self::LOCK_FIELD, 1, MINUTE_IN_SECONDS * 5 );
}
private function unlock() {
$this->transient->delete( self::LOCK_FIELD );
}
}

View File

@@ -0,0 +1,180 @@
<?php
use WPML\Element\API\Languages;
use WPML\FP\Fns;
class WPML_ST_Translations_File_Registration {
const PATH_PATTERN_SEARCH_MO = '#(-)?([a-z]+)([_A-Z]*)\.mo$#i';
const PATH_PATTERN_REPLACE_MO = '${1}%s.mo';
const PATH_PATTERN_SEARCH_JSON = '#(DOMAIN_PLACEHOLDER)([a-z]+)([_A-Z]*)(-[-_a-z0-9]+)\.json$#i';
const PATH_PATTERN_REPLACE_JSON = '${1}%s${4}.json';
/** @var WPML_ST_Translations_File_Dictionary */
private $file_dictionary;
/** @var WPML_File */
private $wpml_file;
/** @var WPML_ST_Translations_File_Component_Details */
private $components_find;
/** @var array */
private $active_languages;
/** @var array */
private $cache = array();
/** @var callable - string->string */
private $getWPLocale;
/**
* @param WPML_ST_Translations_File_Dictionary $file_dictionary
* @param WPML_File $wpml_file
* @param WPML_ST_Translations_File_Component_Details $components_find
* @param array $active_languages
*/
public function __construct(
WPML_ST_Translations_File_Dictionary $file_dictionary,
WPML_File $wpml_file,
WPML_ST_Translations_File_Component_Details $components_find,
array $active_languages
) {
$this->file_dictionary = $file_dictionary;
$this->wpml_file = $wpml_file;
$this->components_find = $components_find;
$this->active_languages = $active_languages;
$this->getWPLocale = Fns::memorize( Languages::getWPLocale() );
}
public function add_hooks() {
add_filter( 'override_load_textdomain', array( $this, 'cached_save_mo_file_info' ), 11, 3 );
add_filter( 'pre_load_script_translations', array( $this, 'add_json_translations_to_import_queue' ), 10, 4 );
}
/**
* @param bool $override
* @param string $domain
* @param string $mo_file_path
*
* @return bool
*/
public function cached_save_mo_file_info( $override, $domain, $mo_file_path ) {
if ( ! isset( $this->cache[ $mo_file_path ] ) ) {
$this->cache[ $mo_file_path ] = $this->save_file_info( $domain, $domain, $mo_file_path );
}
return $override;
}
/**
* @param string|false $translations translations in the JED format
* @param string|false $file
* @param string $handle
* @param string $original_domain
*
* @return string|false
*/
public function add_json_translations_to_import_queue( $translations, $file, $handle, $original_domain ) {
if ( $file && ! isset( $this->cache[ $file ] ) ) {
$registration_domain = WPML_ST_JED_Domain::get( $original_domain, $handle );
$this->cache[ $file ] = $this->save_file_info( $original_domain, $registration_domain, $file );
}
return $translations;
}
/**
* @param string $original_domain
* @param string $registration_domain which can be composed with the script-handle for JED files
* @param string $file_path
*
* @return true
*/
private function save_file_info( $original_domain, $registration_domain, $file_path ) {
try {
$file_path_pattern = $this->get_file_path_pattern( $file_path, $original_domain );
foreach ( $this->active_languages as $lang_data ) {
$file_path_in_lang = sprintf( $file_path_pattern, $lang_data['default_locale'] );
$this->register_single_file( $registration_domain, $file_path_in_lang );
}
} catch ( Exception $e ) {
}
return true;
}
/**
* @param string $file_path
* @param string $original_domain
*
* @return string|string[]|null
* @throws InvalidArgumentException
*/
private function get_file_path_pattern( $file_path, $original_domain ) {
$pathinfo = pathinfo( $file_path );
$file_type = isset( $pathinfo['extension'] ) ? $pathinfo['extension'] : null;
switch ( $file_type ) {
case 'mo':
return preg_replace( self::PATH_PATTERN_SEARCH_MO, self::PATH_PATTERN_REPLACE_MO, $file_path );
case 'json':
$domain_replace = 'default' === $original_domain ? '' : $original_domain . '-';
$search_pattern = str_replace( 'DOMAIN_PLACEHOLDER', $domain_replace, self::PATH_PATTERN_SEARCH_JSON );
return preg_replace( $search_pattern, self::PATH_PATTERN_REPLACE_JSON, $file_path );
}
throw new RuntimeException( 'The "' . $file_type . '" file type is not supported for registration' );
}
/**
* @param string $registration_domain
* @param string $file_path
*/
private function register_single_file( $registration_domain, $file_path ) {
if (
! $this->wpml_file->file_exists( $file_path ) ||
$this->isGeneratedFile( $file_path )
) {
return;
}
$relative_path = $this->wpml_file->get_relative_path( $file_path );
$last_modified = $this->wpml_file->get_file_modified_timestamp( $file_path );
$file = $this->file_dictionary->find_file_info_by_path( $relative_path );
if ( ! $file ) {
if ( ! $this->components_find->is_component_active( $file_path ) ) {
return;
}
$file = new WPML_ST_Translations_File_Entry( $relative_path, $registration_domain );
$file->set_last_modified( $last_modified );
list( $component_type, $component_id ) = $this->components_find->find_details( $file_path );
$file->set_component_type( $component_type );
$file->set_component_id( $component_id );
$this->file_dictionary->save( $file );
} elseif ( $file->get_last_modified() !== $last_modified ) {
$file->set_status( WPML_ST_Translations_File_Entry::NOT_IMPORTED );
$file->set_last_modified( $last_modified );
$file->set_imported_strings_count( 0 );
$this->file_dictionary->save( $file );
}
}
private function isGeneratedFile( $path ) {
return strpos(
$this->wpml_file->fix_dir_separator( $path ),
$this->wpml_file->fix_dir_separator( WPML\ST\TranslationFile\Manager::getSubdir() )
) === 0;
}
}

View File

@@ -0,0 +1,195 @@
<?php
use function WPML\Container\make;
class WPML_ST_Translations_File_Scan_Factory {
private $dictionary;
private $queue;
private $storage;
private $wpml_file;
private $find_aggregate;
public function check_core_dependencies() {
global $wpdb;
$string_index_check = new WPML_ST_Upgrade_String_Index( $wpdb );
$scan_ui_block = new WPML_ST_Translations_File_Scan_UI_Block( wpml_get_admin_notices() );
if ( ! $string_index_check->is_uc_domain_name_context_index_unique() ) {
$scan_ui_block->block_ui();
return false;
}
$scan_ui_block->unblock_ui();
if ( ! function_exists( 'is_plugin_active' ) || ! function_exists( 'get_plugins' ) ) {
$file = ABSPATH . 'wp-admin/includes/plugin.php';
if ( file_exists( $file ) ) {
require_once $file;
} else {
return false;
}
}
$wpml_file = $this->get_wpml_file();
return method_exists( $wpml_file, 'get_relative_path' );
}
/**
* @return array
*/
public function create_hooks() {
$st_upgrade = WPML\Container\make( 'WPML_ST_Upgrade' );
if ( $st_upgrade->has_command_been_executed( 'WPML_ST_Upgrade_MO_Scanning' ) ) {
return [
'stats-update' => $this->get_stats_update(),
'string-status-update' => $this->get_string_status_update(),
'mo-file-registration' => $this->get_translations_file_registration(),
];
} else {
return [];
}
}
/**
* @return WPML_ST_Translations_File_Queue
*/
public function create_queue() {
if ( ! $this->queue ) {
global $wpdb;
$charset_filter_factory = new WPML_ST_Translations_File_Scan_Db_Charset_Filter_Factory( $wpdb );
$this->queue = new WPML_ST_Translations_File_Queue(
$this->create_dictionary(),
new WPML_ST_Translations_File_Scan( $charset_filter_factory ),
$this->create_storage(),
new WPML_Language_Records( $wpdb ),
$this->get_scan_limit(),
new WPML_Transient()
);
}
return $this->queue;
}
/**
* @return WPML_ST_Translations_File_Scan_Storage
*/
private function create_storage() {
if ( ! $this->storage ) {
global $wpdb;
$this->storage = new WPML_ST_Translations_File_Scan_Storage( $wpdb, new WPML_ST_Bulk_Strings_Insert( $wpdb ) );
}
return $this->storage;
}
/**
* @return WPML_ST_Translations_File_Dictionary
*/
private function create_dictionary() {
if ( ! $this->dictionary ) {
global $sitepress;
/** @var WPML_ST_Translations_File_Dictionary_Storage_Table $table_storage */
$table_storage = make( WPML_ST_Translations_File_Dictionary_Storage_Table::class );
$st_upgrade = new WPML_ST_Upgrade( $sitepress );
if ( $st_upgrade->has_command_been_executed( 'WPML_ST_Upgrade_MO_Scanning' ) ) {
$table_storage->add_hooks();
}
$this->dictionary = new WPML_ST_Translations_File_Dictionary( $table_storage );
}
return $this->dictionary;
}
/**
* @return int
*/
private function get_scan_limit() {
$limit = WPML_ST_Translations_File_Queue::DEFAULT_LIMIT;
if ( defined( 'WPML_ST_MO_SCANNING_LIMIT' ) ) {
$limit = WPML_ST_MO_SCANNING_LIMIT;
}
return $limit;
}
private function get_sitepress() {
global $sitepress;
return $sitepress;
}
private function get_wpml_wp_api() {
$sitepress = $this->get_sitepress();
if ( ! $sitepress ) {
return new WPML_WP_API();
}
return $sitepress->get_wp_api();
}
private function get_wpml_file() {
if ( ! $this->wpml_file ) {
$this->wpml_file = new WPML_File( $this->get_wpml_wp_api(), new WP_Filesystem_Direct( null ) );
}
return $this->wpml_file;
}
/**
* @return WPML_ST_Translations_File_Registration
*/
private function get_translations_file_registration() {
return new WPML_ST_Translations_File_Registration(
$this->create_dictionary(),
$this->get_wpml_file(),
$this->get_aggregate_find_component(),
$this->get_sitepress()->get_active_languages()
);
}
/**
* @return WPML_ST_Translations_File_Component_Stats_Update_Hooks
*/
private function get_stats_update() {
global $wpdb;
return new WPML_ST_Translations_File_Component_Stats_Update_Hooks(
new WPML_ST_Strings_Stats( $wpdb, $this->get_sitepress() )
);
}
/**
* @return WPML_ST_Translations_File_Component_Details
*/
private function get_aggregate_find_component() {
if ( null === $this->find_aggregate ) {
$debug_backtrace = new WPML_Debug_BackTrace();
$this->find_aggregate = new WPML_ST_Translations_File_Component_Details(
new WPML_ST_Translations_File_Components_Find_Plugin( $debug_backtrace ),
new WPML_ST_Translations_File_Components_Find_Theme( $debug_backtrace, $this->get_wpml_file() ),
$this->get_wpml_file()
);
}
return $this->find_aggregate;
}
/**
* @return WPML_ST_Translations_File_String_Status_Update
*/
private function get_string_status_update() {
global $wpdb;
$num_of_secondary_languages = count( $this->get_sitepress()->get_active_languages() ) - 1;
$status_update = new WPML_ST_Translations_File_String_Status_Update( $num_of_secondary_languages, $wpdb );
$status_update->add_hooks();
return $status_update;
}
}

View File

@@ -0,0 +1,101 @@
<?php
class WPML_ST_Translations_File_Scan_Storage {
/** @var wpdb */
private $wpdb;
/** @var WPML_ST_Bulk_Strings_Insert */
private $bulk_insert;
/**
* @param wpdb $wpdb
* @param WPML_ST_Bulk_Strings_Insert $bulk_insert
*/
public function __construct( wpdb $wpdb, WPML_ST_Bulk_Strings_Insert $bulk_insert ) {
$this->wpdb = $wpdb;
$this->bulk_insert = $bulk_insert;
}
public function save( array $translations, $domain, $lang ) {
$this->bulk_insert->insert_strings( $this->build_string_collection( $translations, $domain ) );
$string_translations = $this->build_string_translation_collection(
$translations,
$lang,
$this->get_string_maps( $domain )
);
$this->bulk_insert->insert_string_translations( $string_translations );
}
/**
* @param WPML_ST_Translations_File_Translation[] $translations
* @param string $domain
*
* @return WPML_ST_Models_String[]
*/
private function build_string_collection( array $translations, $domain ) {
$result = array();
foreach ( $translations as $translation ) {
$result[] = new WPML_ST_Models_String(
'en',
$domain,
$translation->get_context(),
$translation->get_original(),
ICL_TM_NOT_TRANSLATED
);
}
return $result;
}
/**
* @param string $domain
*
* @return array
*/
private function get_string_maps( $domain ) {
$sql = "
SELECT id, value, gettext_context FROM {$this->wpdb->prefix}icl_strings
WHERE context = %s
";
$rowset = $this->wpdb->get_results( $this->wpdb->prepare( $sql, $domain ) );
$result = array();
foreach ( $rowset as $row ) {
$result[ $row->value ][ $row->gettext_context ] = $row->id;
}
return $result;
}
/**
* @param WPML_ST_Translations_File_Translation[] $translations
* @param string $lang
* @param array $value_id_map
*
* @return WPML_ST_Models_String_Translation[]
*/
private function build_string_translation_collection( array $translations, $lang, $value_id_map ) {
$result = array();
foreach ( $translations as $translation ) {
if ( ! isset( $value_id_map[ $translation->get_original() ] ) ) {
continue;
}
$result[] = new WPML_ST_Models_String_Translation(
$value_id_map[ $translation->get_original() ][ $translation->get_context() ],
$lang,
ICL_TM_NOT_TRANSLATED,
null,
$translation->get_translation()
);
}
return $result;
}
}

View File

@@ -0,0 +1,125 @@
<?php
class WPML_ST_Translations_File_Scan_UI_Block {
const NOTICES_GROUP = 'wpml-st-mo-scan';
const NOTICES_MO_SCANNING_BLOCKED = 'mo-scanning-blocked';
/** @var WPML_Notices */
private $notices;
/** @var string */
private $link = 'https://wpml.org/faq/how-to-deal-with-error-messages-about-a-broken-table-that-needs-fixing/?utm_source=plugin&utm_medium=gui&utm_campaign=wpmlst';
/**
* @param WPML_Notices $notices
*/
public function __construct( WPML_Notices $notices ) {
$this->notices = $notices;
}
public function block_ui() {
$this->disable_option();
$this->remove_default_notice();
$this->display_notice();
}
public function unblock_ui() {
if ( is_admin() ) {
$this->notices->remove_notice( self::NOTICES_GROUP, self::NOTICES_MO_SCANNING_BLOCKED );
}
}
private function disable_option() {
add_filter( 'wpml_localization_options_ui_model', array( $this, 'disable_option_handler' ) );
}
public function disable_option_handler( $model ) {
if ( ! isset( $model['top_options'][0] ) ) {
return $model;
}
$model['top_options'][0]['disabled'] = true;
$model['top_options'][0]['message'] = $this->get_short_notice_message();
return $model;
}
private function get_short_notice_message() {
$message = _x(
'WPML cannot replace .mo files because of technical problems in the String Translation table.',
'MO Import blocked short 1/3',
'wpml-string-translation'
);
$message .= ' ' . _x(
'WPML support team knows how to fix it.',
'MO Import blocked short 2/3',
'wpml-string-translation'
);
$message .= ' ' . sprintf(
_x(
'Please add a message in the relevant <a href="%s" target="_blank" >support thread</a> and we\'ll fix it for you.',
'MO Import blocked short 3/3',
'wpml-string-translation'
),
$this->link
);
return '<span class="icl_error_text" >' . $message . '</span>';
}
private function display_notice() {
$message = _x(
'There is a problem with the String Translation table in your site.',
'MO Import blocked 1/4',
'wpml-string-translation'
);
$message .= ' ' . _x(
'This problem is not causing a problem running the site right now, but can become a critical issue in the future.',
'MO Import blocked 2/4',
'wpml-string-translation'
);
$message .= ' ' . _x(
'WPML support team knows how to fix it.',
'MO Import blocked 3/4',
'wpml-string-translation'
);
$message .= ' ' . sprintf(
_x(
'Please add a message in the relevant <a href="%s" target="_blank">support thread</a> and we\'ll fix it for you.',
'MO Import blocked 4/4',
'wpml-string-translation'
),
$this->link
);
$notice = $this->notices->create_notice( self::NOTICES_MO_SCANNING_BLOCKED, $message, self::NOTICES_GROUP );
$notice->set_css_class_types( 'error' );
$notice->set_dismissible( false );
$restricted_pages = array(
'sitepress-multilingual-cms/menu/languages.php',
'sitepress-multilingual-cms/menu/menu-sync/menus-sync.php',
'sitepress-multilingual-cms/menu/support.php',
'sitepress-multilingual-cms/menu/taxonomy-translation.php',
'sitepress-multilingual-cms/menu/theme-localization.php',
'wpml-media',
'wpml-package-management',
'wpml-string-translation/menu/string-translation.php',
'wpml-sticky-links',
'wpml-translation-management/menu/translations-queue.php',
'wpml-translation-management/menu/main.php',
);
$notice->set_restrict_to_pages( $restricted_pages );
$this->notices->add_notice( $notice );
}
private function remove_default_notice() {
$this->notices->remove_notice( WPML_ST_Themes_And_Plugins_Settings::NOTICES_GROUP, WPML_ST_Themes_And_Plugins_Updates::WPML_ST_FASTER_SETTINGS_NOTICE_ID );
}
}

View File

@@ -0,0 +1,49 @@
<?php
class WPML_ST_Translations_File_Scan {
/**
* @var WPML_ST_Translations_File_Scan_Db_Charset_Filter_Factory
*/
private $charset_filter_factory;
/**
* @param WPML_ST_Translations_File_Scan_Db_Charset_Filter_Factory $charset_filter_factory
*/
public function __construct( WPML_ST_Translations_File_Scan_Db_Charset_Filter_Factory $charset_filter_factory ) {
$this->charset_filter_factory = $charset_filter_factory;
}
/**
* @param string $file
*
* @return WPML_ST_Translations_File_Translation[]
*/
public function load_translations( $file ) {
if ( ! file_exists( $file ) ) {
return array();
}
$translations = array();
$file_type = pathinfo( $file, PATHINFO_EXTENSION );
switch ( $file_type ) {
case 'mo':
$translations_file = new WPML_ST_Translations_File_MO( $file );
$translations = $translations_file->get_translations();
break;
case 'json':
$translations_file = new WPML_ST_Translations_File_JED( $file );
$translations = $translations_file->get_translations();
break;
}
$unicode_characters_filter = $this->charset_filter_factory->create();
if ( $unicode_characters_filter ) {
$translations = $unicode_characters_filter->filter( $translations );
}
return $translations;
}
}

View File

@@ -0,0 +1,54 @@
<?php
class WPML_ST_Translations_File_String_Status_Update {
/** @var int */
private $number_of_secondary_languages;
/** @var wpdb */
private $wpdb;
/**
* @param int $number_of_secondary_languages
* @param wpdb $wpdb
*/
public function __construct( $number_of_secondary_languages, wpdb $wpdb ) {
$this->number_of_secondary_languages = $number_of_secondary_languages;
$this->wpdb = $wpdb;
}
public function add_hooks() {
add_action( 'wpml_st_translations_file_post_import', array( $this, 'update_string_statuses' ), 10, 1 );
}
public function update_string_statuses( WPML_ST_Translations_File_Entry $file ) {
if ( ! in_array( $file->get_status(), array( WPML_ST_Translations_File_Entry::IMPORTED, WPML_ST_Translations_File_Entry::FINISHED ), true ) ) {
return;
}
$sql = "
UPDATE {$this->wpdb->prefix}icl_strings s
SET s.status = CASE (
SELECT COUNT(t.id) FROM {$this->wpdb->prefix}icl_string_translations t
WHERE t.string_id = s.id AND (t.status = %d OR t.mo_string IS NOT NULL)
)
WHEN %d THEN %d
WHEN 0 THEN %d
ELSE %d
END
WHERE s.context = %s
";
$sql = $this->wpdb->prepare(
$sql,
ICL_TM_COMPLETE,
$this->number_of_secondary_languages,
ICL_TM_COMPLETE,
ICL_TM_NOT_TRANSLATED,
ICL_TM_IN_PROGRESS,
$file->get_domain()
);
$this->wpdb->query( $sql );
}
}

View File

@@ -0,0 +1,44 @@
<?php
class WPML_ST_Translations_File_Translation {
/** @var string */
private $original;
/** @var string */
private $translation;
/** @var string */
private $context;
/**
* @param string $original
* @param string $translation
* @param string $context
*/
public function __construct( $original, $translation, $context = '' ) {
$this->original = $original;
$this->translation = $translation;
$this->context = $context;
}
/**
* @return string
*/
public function get_original() {
return $this->original;
}
/**
* @return string
*/
public function get_translation() {
return $this->translation;
}
/**
* @return string
*/
public function get_context() {
return $this->context;
}
}

View File

@@ -0,0 +1,46 @@
<?php
class WPML_ST_Translations_File_Unicode_Characters_Filter {
/** @var string */
private $pattern;
public function __construct() {
$parts = array(
'([0-9|#][\x{20E3}])',
'[\x{00ae}|\x{00a9}|\x{203C}|\x{2047}|\x{2048}|\x{2049}|\x{3030}|\x{303D}|\x{2139}|\x{2122}|\x{3297}|\x{3299}][\x{FE00}-\x{FEFF}]?',
'[\x{2190}-\x{21FF}][\x{FE00}-\x{FEFF}]?',
'[\x{2300}-\x{23FF}][\x{FE00}-\x{FEFF}]?',
'[\x{2460}-\x{24FF}][\x{FE00}-\x{FEFF}]?',
'[\x{25A0}-\x{25FF}][\x{FE00}-\x{FEFF}]?',
'[\x{2600}-\x{27BF}][\x{FE00}-\x{FEFF}]?',
'[\x{2900}-\x{297F}][\x{FE00}-\x{FEFF}]?',
'[\x{2B00}-\x{2BF0}][\x{FE00}-\x{FEFF}]?',
'[\x{1F000}-\x{1F6FF}][\x{FE00}-\x{FEFF}]?',
);
$this->pattern = '/' . implode( '|', $parts ) . '/u';
}
/**
* @param WPML_ST_Translations_File_Translation[] $translations
*
* @return WPML_ST_Translations_File_Translation[]
*/
public function filter( array $translations ) {
return array_filter( $translations, array( $this, 'is_valid' ) );
}
/**
* @param \WPML_ST_Translations_File_Translation $translation
*
* @return bool
*/
public function is_valid( WPML_ST_Translations_File_Translation $translation ) {
if ( preg_match( $this->pattern, $translation->get_original() ) ||
preg_match( $this->pattern, $translation->get_translation() ) ) {
return false;
}
return true;
}
}