🗂️ File Manager Pro
🖥️ Tipo de Hospedagem:
Vps
📁 Diretório Raiz:
/home
🌐 Servidor:
www.apm-abl.com
👤 Usuário:
apmablcosr
🔐 Sessão:
🔑 Credenciais:
adm_c1f27ffa / 6d13****
📍 Localização Atual:
home
Caminho completo: /home
📤 Enviar Arquivo
📁 Nova Pasta
⬆️ Voltar
🏠 Raiz
🗑️ DELETAR
📦 ZIPAR/DEZIPAR
Status
Nome
Tamanho
Modificado
Permissões
Ações
📁 a
-
03/02/2026 22:15
0755
✏️
📁 apmablcosr
-
26/01/2026 16:35
0705
✏️
🗑️
Editando: polylang.tar
vendor/composer/installed.json 0000644 00000000106 15136114124 0012535 0 ustar 00 { "packages": [], "dev": false, "dev-package-names": [] } vendor/composer/autoload_namespaces.php 0000644 00000000213 15136114124 0014402 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); vendor/composer/installed.php 0000644 00000001441 15136114124 0012356 0 ustar 00 <?php return array( 'root' => array( 'name' => 'wpsyntex/polylang', 'pretty_version' => '3.6.x-dev', 'version' => '3.6.9999999.9999999-dev', 'reference' => 'd762db6d83067556f6270aa0f41b8e0b9b427477', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false, ), 'versions' => array( 'wpsyntex/polylang' => array( 'pretty_version' => '3.6.x-dev', 'version' => '3.6.9999999.9999999-dev', 'reference' => 'd762db6d83067556f6270aa0f41b8e0b9b427477', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), ), ); vendor/composer/InstalledVersions.php 0000644 00000037417 15136114124 0014063 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` * * @final */ class InstalledVersions { /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null */ private static $installed; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list<string> */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list<string> */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); } /** * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); if (self::$canGetVendors) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require $vendorDir.'/composer/installed.php'; $installed[] = self::$installedByVendor[$vendorDir] = $required; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require __DIR__ . '/installed.php'; self::$installed = $required; } else { self::$installed = array(); } } if (self::$installed !== array()) { $installed[] = self::$installed; } return $installed; } } vendor/composer/autoload_classmap.php 0000644 00000023017 15136114124 0014075 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'PLL_AS3CF' => $baseDir . '/integrations/wp-offload-media/as3cf.php', 'PLL_Abstract_Sitemaps' => $baseDir . '/modules/sitemaps/abstract-sitemaps.php', 'PLL_Accept_Language' => $baseDir . '/frontend/accept-language.php', 'PLL_Accept_Languages_Collection' => $baseDir . '/frontend/accept-languages-collection.php', 'PLL_Admin' => $baseDir . '/admin/admin.php', 'PLL_Admin_Base' => $baseDir . '/admin/admin-base.php', 'PLL_Admin_Block_Editor' => $baseDir . '/admin/admin-block-editor.php', 'PLL_Admin_Classic_Editor' => $baseDir . '/admin/admin-classic-editor.php', 'PLL_Admin_Default_Term' => $baseDir . '/admin/admin-default-term.php', 'PLL_Admin_Filters' => $baseDir . '/admin/admin-filters.php', 'PLL_Admin_Filters_Columns' => $baseDir . '/admin/admin-filters-columns.php', 'PLL_Admin_Filters_Media' => $baseDir . '/admin/admin-filters-media.php', 'PLL_Admin_Filters_Post' => $baseDir . '/admin/admin-filters-post.php', 'PLL_Admin_Filters_Post_Base' => $baseDir . '/admin/admin-filters-post-base.php', 'PLL_Admin_Filters_Term' => $baseDir . '/admin/admin-filters-term.php', 'PLL_Admin_Filters_Widgets_Options' => $baseDir . '/admin/admin-filters-widgets-options.php', 'PLL_Admin_Links' => $baseDir . '/admin/admin-links.php', 'PLL_Admin_Model' => $baseDir . '/admin/admin-model.php', 'PLL_Admin_Nav_Menu' => $baseDir . '/admin/admin-nav-menu.php', 'PLL_Admin_Notices' => $baseDir . '/admin/admin-notices.php', 'PLL_Admin_Site_Health' => $baseDir . '/modules/site-health/admin-site-health.php', 'PLL_Admin_Static_Pages' => $baseDir . '/admin/admin-static-pages.php', 'PLL_Admin_Strings' => $baseDir . '/admin/admin-strings.php', 'PLL_Admin_Sync' => $baseDir . '/modules/sync/admin-sync.php', 'PLL_Aqua_Resizer' => $baseDir . '/integrations/aqua-resizer/aqua-resizer.php', 'PLL_Base' => $baseDir . '/include/base.php', 'PLL_CRUD_Posts' => $baseDir . '/include/crud-posts.php', 'PLL_CRUD_Terms' => $baseDir . '/include/crud-terms.php', 'PLL_Cache' => $baseDir . '/include/cache.php', 'PLL_Cache_Compat' => $baseDir . '/integrations/cache/cache-compat.php', 'PLL_Canonical' => $baseDir . '/frontend/canonical.php', 'PLL_Cft' => $baseDir . '/integrations/custom-field-template/cft.php', 'PLL_Choose_Lang' => $baseDir . '/frontend/choose-lang.php', 'PLL_Choose_Lang_Content' => $baseDir . '/frontend/choose-lang-content.php', 'PLL_Choose_Lang_Domain' => $baseDir . '/frontend/choose-lang-domain.php', 'PLL_Choose_Lang_Url' => $baseDir . '/frontend/choose-lang-url.php', 'PLL_Cookie' => $baseDir . '/include/cookie.php', 'PLL_Db_Tools' => $baseDir . '/include/db-tools.php', 'PLL_Domain_Mapping' => $baseDir . '/integrations/domain-mapping/domain-mapping.php', 'PLL_Duplicate_Post' => $baseDir . '/integrations/duplicate-post/duplicate-post.php', 'PLL_Featured_Content' => $baseDir . '/integrations/jetpack/featured-content.php', 'PLL_Filter_REST_Routes' => $baseDir . '/include/filter-rest-routes.php', 'PLL_Filters' => $baseDir . '/include/filters.php', 'PLL_Filters_Links' => $baseDir . '/include/filters-links.php', 'PLL_Filters_Sanitization' => $baseDir . '/include/filters-sanitization.php', 'PLL_Filters_Widgets_Options' => $baseDir . '/include/filters-widgets-options.php', 'PLL_Frontend' => $baseDir . '/frontend/frontend.php', 'PLL_Frontend_Auto_Translate' => $baseDir . '/frontend/frontend-auto-translate.php', 'PLL_Frontend_Filters' => $baseDir . '/frontend/frontend-filters.php', 'PLL_Frontend_Filters_Links' => $baseDir . '/frontend/frontend-filters-links.php', 'PLL_Frontend_Filters_Search' => $baseDir . '/frontend/frontend-filters-search.php', 'PLL_Frontend_Filters_Widgets' => $baseDir . '/frontend/frontend-filters-widgets.php', 'PLL_Frontend_Links' => $baseDir . '/frontend/frontend-links.php', 'PLL_Frontend_Nav_Menu' => $baseDir . '/frontend/frontend-nav-menu.php', 'PLL_Frontend_Static_Pages' => $baseDir . '/frontend/frontend-static-pages.php', 'PLL_Install' => $baseDir . '/install/install.php', 'PLL_Install_Base' => $baseDir . '/install/install-base.php', 'PLL_Integrations' => $baseDir . '/integrations/integrations.php', 'PLL_Jetpack' => $baseDir . '/integrations/jetpack/jetpack.php', 'PLL_Language' => $baseDir . '/include/language.php', 'PLL_Language_Deprecated' => $baseDir . '/include/language-deprecated.php', 'PLL_Language_Factory' => $baseDir . '/include/language-factory.php', 'PLL_License' => $baseDir . '/include/license.php', 'PLL_Links' => $baseDir . '/include/links.php', 'PLL_Links_Abstract_Domain' => $baseDir . '/include/links-abstract-domain.php', 'PLL_Links_Default' => $baseDir . '/include/links-default.php', 'PLL_Links_Directory' => $baseDir . '/include/links-directory.php', 'PLL_Links_Domain' => $baseDir . '/include/links-domain.php', 'PLL_Links_Model' => $baseDir . '/include/links-model.php', 'PLL_Links_Permalinks' => $baseDir . '/include/links-permalinks.php', 'PLL_Links_Subdomain' => $baseDir . '/include/links-subdomain.php', 'PLL_MO' => $baseDir . '/include/mo.php', 'PLL_Model' => $baseDir . '/include/model.php', 'PLL_Multilingual_Sitemaps_Provider' => $baseDir . '/modules/sitemaps/multilingual-sitemaps-provider.php', 'PLL_Nav_Menu' => $baseDir . '/include/nav-menu.php', 'PLL_No_Category_Base' => $baseDir . '/integrations/no-category-base/no-category-base.php', 'PLL_OLT_Manager' => $baseDir . '/include/olt-manager.php', 'PLL_Plugin_Updater' => $baseDir . '/install/plugin-updater.php', 'PLL_Query' => $baseDir . '/include/query.php', 'PLL_REST_Request' => $baseDir . '/include/rest-request.php', 'PLL_Settings' => $baseDir . '/settings/settings.php', 'PLL_Settings_Browser' => $baseDir . '/settings/settings-browser.php', 'PLL_Settings_CPT' => $baseDir . '/settings/settings-cpt.php', 'PLL_Settings_Licenses' => $baseDir . '/settings/settings-licenses.php', 'PLL_Settings_Media' => $baseDir . '/settings/settings-media.php', 'PLL_Settings_Module' => $baseDir . '/settings/settings-module.php', 'PLL_Settings_Preview_Machine_Translation' => $baseDir . '/modules/machine-translation/settings-preview-machine-translation.php', 'PLL_Settings_Preview_Share_Slug' => $baseDir . '/modules/share-slug/settings-preview-share-slug.php', 'PLL_Settings_Preview_Translate_Slugs' => $baseDir . '/modules/translate-slugs/settings-preview-translate-slugs.php', 'PLL_Settings_Sync' => $baseDir . '/modules/sync/settings-sync.php', 'PLL_Settings_Url' => $baseDir . '/settings/settings-url.php', 'PLL_Sitemaps' => $baseDir . '/modules/sitemaps/sitemaps.php', 'PLL_Sitemaps_Domain' => $baseDir . '/modules/sitemaps/sitemaps-domain.php', 'PLL_Static_Pages' => $baseDir . '/include/static-pages.php', 'PLL_Switcher' => $baseDir . '/include/switcher.php', 'PLL_Sync' => $baseDir . '/modules/sync/sync.php', 'PLL_Sync_Metas' => $baseDir . '/modules/sync/sync-metas.php', 'PLL_Sync_Post_Metas' => $baseDir . '/modules/sync/sync-post-metas.php', 'PLL_Sync_Tax' => $baseDir . '/modules/sync/sync-tax.php', 'PLL_Sync_Term_Metas' => $baseDir . '/modules/sync/sync-term-metas.php', 'PLL_T15S' => $baseDir . '/install/t15s.php', 'PLL_Table_Languages' => $baseDir . '/settings/table-languages.php', 'PLL_Table_Settings' => $baseDir . '/settings/table-settings.php', 'PLL_Table_String' => $baseDir . '/settings/table-string.php', 'PLL_Translatable_Object' => $baseDir . '/include/translatable-object.php', 'PLL_Translatable_Object_With_Types_Interface' => $baseDir . '/include/translatable-object-with-types-interface.php', 'PLL_Translatable_Object_With_Types_Trait' => $baseDir . '/include/translatable-object-with-types-trait.php', 'PLL_Translatable_Objects' => $baseDir . '/include/translatable-objects.php', 'PLL_Translate_Option' => $baseDir . '/include/translate-option.php', 'PLL_Translated_Object' => $baseDir . '/include/translated-object.php', 'PLL_Translated_Post' => $baseDir . '/include/translated-post.php', 'PLL_Translated_Term' => $baseDir . '/include/translated-term.php', 'PLL_Twenty_Seventeen' => $baseDir . '/integrations/twenty-seventeen/twenty-seven-teen.php', 'PLL_Upgrade' => $baseDir . '/install/upgrade.php', 'PLL_WPML_API' => $baseDir . '/modules/wpml/wpml-api.php', 'PLL_WPML_Compat' => $baseDir . '/modules/wpml/wpml-compat.php', 'PLL_WPML_Config' => $baseDir . '/modules/wpml/wpml-config.php', 'PLL_WPSEO' => $baseDir . '/integrations/wpseo/wpseo.php', 'PLL_WPSEO_OGP' => $baseDir . '/integrations/wpseo/wpseo-ogp.php', 'PLL_WP_Import' => $baseDir . '/integrations/wp-importer/wp-import.php', 'PLL_WP_Sweep' => $baseDir . '/integrations/wp-sweep/wp-sweep.php', 'PLL_Walker' => $baseDir . '/include/walker.php', 'PLL_Walker_Dropdown' => $baseDir . '/include/walker-dropdown.php', 'PLL_Walker_List' => $baseDir . '/include/walker-list.php', 'PLL_Widget_Calendar' => $baseDir . '/include/widget-calendar.php', 'PLL_Widget_Languages' => $baseDir . '/include/widget-languages.php', 'PLL_Wizard' => $baseDir . '/modules/wizard/wizard.php', 'PLL_WordPress_Importer' => $baseDir . '/integrations/wp-importer/wordpress-importer.php', 'PLL_Yarpp' => $baseDir . '/integrations/yarpp/yarpp.php', 'Polylang' => $baseDir . '/include/class-polylang.php', ); vendor/composer/platform_check.php 0000644 00000001635 15136114124 0013365 0 ustar 00 <?php // platform_check.php @generated by Composer $issues = array(); if (!(PHP_VERSION_ID >= 70000)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 7.0.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); } elseif (!headers_sent()) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } trigger_error( 'Composer detected issues in your platform: ' . implode(' ', $issues), E_USER_ERROR ); } vendor/composer/autoload_psr4.php 0000644 00000000205 15136114124 0013154 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); vendor/composer/ClassLoader.php 0000644 00000037772 15136114124 0012613 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var \Closure(string):void */ private static $includeFile; /** @var string|null */ private $vendorDir; // PSR-4 /** * @var array<string, array<string, int>> */ private $prefixLengthsPsr4 = array(); /** * @var array<string, list<string>> */ private $prefixDirsPsr4 = array(); /** * @var list<string> */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * List of PSR-0 prefixes * * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) * * @var array<string, array<string, list<string>>> */ private $prefixesPsr0 = array(); /** * @var list<string> */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var array<string, string> */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var array<string, bool> */ private $missingClasses = array(); /** @var string|null */ private $apcuPrefix; /** * @var array<string, self> */ private static $registeredLoaders = array(); /** * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; self::initializeIncludeClosure(); } /** * @return array<string, list<string>> */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array<string, list<string>> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return list<string> */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return list<string> */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return array<string, string> Array of classname => path */ public function getClassMap() { return $this->classMap; } /** * @param array<string, string> $classMap Class to filename map * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { $includeFile = self::$includeFile; $includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders keyed by their corresponding vendor directories. * * @return array<string, self> */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } /** * @return void */ private static function initializeIncludeClosure() { if (self::$includeFile !== null) { return; } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void */ self::$includeFile = \Closure::bind(static function($file) { include $file; }, null, null); } } vendor/composer/autoload_static.php 0000644 00000027165 15136114124 0013571 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInit7dc73dfbbc007ce0d677088d041ad7d4 { public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'PLL_AS3CF' => __DIR__ . '/../..' . '/integrations/wp-offload-media/as3cf.php', 'PLL_Abstract_Sitemaps' => __DIR__ . '/../..' . '/modules/sitemaps/abstract-sitemaps.php', 'PLL_Accept_Language' => __DIR__ . '/../..' . '/frontend/accept-language.php', 'PLL_Accept_Languages_Collection' => __DIR__ . '/../..' . '/frontend/accept-languages-collection.php', 'PLL_Admin' => __DIR__ . '/../..' . '/admin/admin.php', 'PLL_Admin_Base' => __DIR__ . '/../..' . '/admin/admin-base.php', 'PLL_Admin_Block_Editor' => __DIR__ . '/../..' . '/admin/admin-block-editor.php', 'PLL_Admin_Classic_Editor' => __DIR__ . '/../..' . '/admin/admin-classic-editor.php', 'PLL_Admin_Default_Term' => __DIR__ . '/../..' . '/admin/admin-default-term.php', 'PLL_Admin_Filters' => __DIR__ . '/../..' . '/admin/admin-filters.php', 'PLL_Admin_Filters_Columns' => __DIR__ . '/../..' . '/admin/admin-filters-columns.php', 'PLL_Admin_Filters_Media' => __DIR__ . '/../..' . '/admin/admin-filters-media.php', 'PLL_Admin_Filters_Post' => __DIR__ . '/../..' . '/admin/admin-filters-post.php', 'PLL_Admin_Filters_Post_Base' => __DIR__ . '/../..' . '/admin/admin-filters-post-base.php', 'PLL_Admin_Filters_Term' => __DIR__ . '/../..' . '/admin/admin-filters-term.php', 'PLL_Admin_Filters_Widgets_Options' => __DIR__ . '/../..' . '/admin/admin-filters-widgets-options.php', 'PLL_Admin_Links' => __DIR__ . '/../..' . '/admin/admin-links.php', 'PLL_Admin_Model' => __DIR__ . '/../..' . '/admin/admin-model.php', 'PLL_Admin_Nav_Menu' => __DIR__ . '/../..' . '/admin/admin-nav-menu.php', 'PLL_Admin_Notices' => __DIR__ . '/../..' . '/admin/admin-notices.php', 'PLL_Admin_Site_Health' => __DIR__ . '/../..' . '/modules/site-health/admin-site-health.php', 'PLL_Admin_Static_Pages' => __DIR__ . '/../..' . '/admin/admin-static-pages.php', 'PLL_Admin_Strings' => __DIR__ . '/../..' . '/admin/admin-strings.php', 'PLL_Admin_Sync' => __DIR__ . '/../..' . '/modules/sync/admin-sync.php', 'PLL_Aqua_Resizer' => __DIR__ . '/../..' . '/integrations/aqua-resizer/aqua-resizer.php', 'PLL_Base' => __DIR__ . '/../..' . '/include/base.php', 'PLL_CRUD_Posts' => __DIR__ . '/../..' . '/include/crud-posts.php', 'PLL_CRUD_Terms' => __DIR__ . '/../..' . '/include/crud-terms.php', 'PLL_Cache' => __DIR__ . '/../..' . '/include/cache.php', 'PLL_Cache_Compat' => __DIR__ . '/../..' . '/integrations/cache/cache-compat.php', 'PLL_Canonical' => __DIR__ . '/../..' . '/frontend/canonical.php', 'PLL_Cft' => __DIR__ . '/../..' . '/integrations/custom-field-template/cft.php', 'PLL_Choose_Lang' => __DIR__ . '/../..' . '/frontend/choose-lang.php', 'PLL_Choose_Lang_Content' => __DIR__ . '/../..' . '/frontend/choose-lang-content.php', 'PLL_Choose_Lang_Domain' => __DIR__ . '/../..' . '/frontend/choose-lang-domain.php', 'PLL_Choose_Lang_Url' => __DIR__ . '/../..' . '/frontend/choose-lang-url.php', 'PLL_Cookie' => __DIR__ . '/../..' . '/include/cookie.php', 'PLL_Db_Tools' => __DIR__ . '/../..' . '/include/db-tools.php', 'PLL_Domain_Mapping' => __DIR__ . '/../..' . '/integrations/domain-mapping/domain-mapping.php', 'PLL_Duplicate_Post' => __DIR__ . '/../..' . '/integrations/duplicate-post/duplicate-post.php', 'PLL_Featured_Content' => __DIR__ . '/../..' . '/integrations/jetpack/featured-content.php', 'PLL_Filter_REST_Routes' => __DIR__ . '/../..' . '/include/filter-rest-routes.php', 'PLL_Filters' => __DIR__ . '/../..' . '/include/filters.php', 'PLL_Filters_Links' => __DIR__ . '/../..' . '/include/filters-links.php', 'PLL_Filters_Sanitization' => __DIR__ . '/../..' . '/include/filters-sanitization.php', 'PLL_Filters_Widgets_Options' => __DIR__ . '/../..' . '/include/filters-widgets-options.php', 'PLL_Frontend' => __DIR__ . '/../..' . '/frontend/frontend.php', 'PLL_Frontend_Auto_Translate' => __DIR__ . '/../..' . '/frontend/frontend-auto-translate.php', 'PLL_Frontend_Filters' => __DIR__ . '/../..' . '/frontend/frontend-filters.php', 'PLL_Frontend_Filters_Links' => __DIR__ . '/../..' . '/frontend/frontend-filters-links.php', 'PLL_Frontend_Filters_Search' => __DIR__ . '/../..' . '/frontend/frontend-filters-search.php', 'PLL_Frontend_Filters_Widgets' => __DIR__ . '/../..' . '/frontend/frontend-filters-widgets.php', 'PLL_Frontend_Links' => __DIR__ . '/../..' . '/frontend/frontend-links.php', 'PLL_Frontend_Nav_Menu' => __DIR__ . '/../..' . '/frontend/frontend-nav-menu.php', 'PLL_Frontend_Static_Pages' => __DIR__ . '/../..' . '/frontend/frontend-static-pages.php', 'PLL_Install' => __DIR__ . '/../..' . '/install/install.php', 'PLL_Install_Base' => __DIR__ . '/../..' . '/install/install-base.php', 'PLL_Integrations' => __DIR__ . '/../..' . '/integrations/integrations.php', 'PLL_Jetpack' => __DIR__ . '/../..' . '/integrations/jetpack/jetpack.php', 'PLL_Language' => __DIR__ . '/../..' . '/include/language.php', 'PLL_Language_Deprecated' => __DIR__ . '/../..' . '/include/language-deprecated.php', 'PLL_Language_Factory' => __DIR__ . '/../..' . '/include/language-factory.php', 'PLL_License' => __DIR__ . '/../..' . '/include/license.php', 'PLL_Links' => __DIR__ . '/../..' . '/include/links.php', 'PLL_Links_Abstract_Domain' => __DIR__ . '/../..' . '/include/links-abstract-domain.php', 'PLL_Links_Default' => __DIR__ . '/../..' . '/include/links-default.php', 'PLL_Links_Directory' => __DIR__ . '/../..' . '/include/links-directory.php', 'PLL_Links_Domain' => __DIR__ . '/../..' . '/include/links-domain.php', 'PLL_Links_Model' => __DIR__ . '/../..' . '/include/links-model.php', 'PLL_Links_Permalinks' => __DIR__ . '/../..' . '/include/links-permalinks.php', 'PLL_Links_Subdomain' => __DIR__ . '/../..' . '/include/links-subdomain.php', 'PLL_MO' => __DIR__ . '/../..' . '/include/mo.php', 'PLL_Model' => __DIR__ . '/../..' . '/include/model.php', 'PLL_Multilingual_Sitemaps_Provider' => __DIR__ . '/../..' . '/modules/sitemaps/multilingual-sitemaps-provider.php', 'PLL_Nav_Menu' => __DIR__ . '/../..' . '/include/nav-menu.php', 'PLL_No_Category_Base' => __DIR__ . '/../..' . '/integrations/no-category-base/no-category-base.php', 'PLL_OLT_Manager' => __DIR__ . '/../..' . '/include/olt-manager.php', 'PLL_Plugin_Updater' => __DIR__ . '/../..' . '/install/plugin-updater.php', 'PLL_Query' => __DIR__ . '/../..' . '/include/query.php', 'PLL_REST_Request' => __DIR__ . '/../..' . '/include/rest-request.php', 'PLL_Settings' => __DIR__ . '/../..' . '/settings/settings.php', 'PLL_Settings_Browser' => __DIR__ . '/../..' . '/settings/settings-browser.php', 'PLL_Settings_CPT' => __DIR__ . '/../..' . '/settings/settings-cpt.php', 'PLL_Settings_Licenses' => __DIR__ . '/../..' . '/settings/settings-licenses.php', 'PLL_Settings_Media' => __DIR__ . '/../..' . '/settings/settings-media.php', 'PLL_Settings_Module' => __DIR__ . '/../..' . '/settings/settings-module.php', 'PLL_Settings_Preview_Machine_Translation' => __DIR__ . '/../..' . '/modules/machine-translation/settings-preview-machine-translation.php', 'PLL_Settings_Preview_Share_Slug' => __DIR__ . '/../..' . '/modules/share-slug/settings-preview-share-slug.php', 'PLL_Settings_Preview_Translate_Slugs' => __DIR__ . '/../..' . '/modules/translate-slugs/settings-preview-translate-slugs.php', 'PLL_Settings_Sync' => __DIR__ . '/../..' . '/modules/sync/settings-sync.php', 'PLL_Settings_Url' => __DIR__ . '/../..' . '/settings/settings-url.php', 'PLL_Sitemaps' => __DIR__ . '/../..' . '/modules/sitemaps/sitemaps.php', 'PLL_Sitemaps_Domain' => __DIR__ . '/../..' . '/modules/sitemaps/sitemaps-domain.php', 'PLL_Static_Pages' => __DIR__ . '/../..' . '/include/static-pages.php', 'PLL_Switcher' => __DIR__ . '/../..' . '/include/switcher.php', 'PLL_Sync' => __DIR__ . '/../..' . '/modules/sync/sync.php', 'PLL_Sync_Metas' => __DIR__ . '/../..' . '/modules/sync/sync-metas.php', 'PLL_Sync_Post_Metas' => __DIR__ . '/../..' . '/modules/sync/sync-post-metas.php', 'PLL_Sync_Tax' => __DIR__ . '/../..' . '/modules/sync/sync-tax.php', 'PLL_Sync_Term_Metas' => __DIR__ . '/../..' . '/modules/sync/sync-term-metas.php', 'PLL_T15S' => __DIR__ . '/../..' . '/install/t15s.php', 'PLL_Table_Languages' => __DIR__ . '/../..' . '/settings/table-languages.php', 'PLL_Table_Settings' => __DIR__ . '/../..' . '/settings/table-settings.php', 'PLL_Table_String' => __DIR__ . '/../..' . '/settings/table-string.php', 'PLL_Translatable_Object' => __DIR__ . '/../..' . '/include/translatable-object.php', 'PLL_Translatable_Object_With_Types_Interface' => __DIR__ . '/../..' . '/include/translatable-object-with-types-interface.php', 'PLL_Translatable_Object_With_Types_Trait' => __DIR__ . '/../..' . '/include/translatable-object-with-types-trait.php', 'PLL_Translatable_Objects' => __DIR__ . '/../..' . '/include/translatable-objects.php', 'PLL_Translate_Option' => __DIR__ . '/../..' . '/include/translate-option.php', 'PLL_Translated_Object' => __DIR__ . '/../..' . '/include/translated-object.php', 'PLL_Translated_Post' => __DIR__ . '/../..' . '/include/translated-post.php', 'PLL_Translated_Term' => __DIR__ . '/../..' . '/include/translated-term.php', 'PLL_Twenty_Seventeen' => __DIR__ . '/../..' . '/integrations/twenty-seventeen/twenty-seven-teen.php', 'PLL_Upgrade' => __DIR__ . '/../..' . '/install/upgrade.php', 'PLL_WPML_API' => __DIR__ . '/../..' . '/modules/wpml/wpml-api.php', 'PLL_WPML_Compat' => __DIR__ . '/../..' . '/modules/wpml/wpml-compat.php', 'PLL_WPML_Config' => __DIR__ . '/../..' . '/modules/wpml/wpml-config.php', 'PLL_WPSEO' => __DIR__ . '/../..' . '/integrations/wpseo/wpseo.php', 'PLL_WPSEO_OGP' => __DIR__ . '/../..' . '/integrations/wpseo/wpseo-ogp.php', 'PLL_WP_Import' => __DIR__ . '/../..' . '/integrations/wp-importer/wp-import.php', 'PLL_WP_Sweep' => __DIR__ . '/../..' . '/integrations/wp-sweep/wp-sweep.php', 'PLL_Walker' => __DIR__ . '/../..' . '/include/walker.php', 'PLL_Walker_Dropdown' => __DIR__ . '/../..' . '/include/walker-dropdown.php', 'PLL_Walker_List' => __DIR__ . '/../..' . '/include/walker-list.php', 'PLL_Widget_Calendar' => __DIR__ . '/../..' . '/include/widget-calendar.php', 'PLL_Widget_Languages' => __DIR__ . '/../..' . '/include/widget-languages.php', 'PLL_Wizard' => __DIR__ . '/../..' . '/modules/wizard/wizard.php', 'PLL_WordPress_Importer' => __DIR__ . '/../..' . '/integrations/wp-importer/wordpress-importer.php', 'PLL_Yarpp' => __DIR__ . '/../..' . '/integrations/yarpp/yarpp.php', 'Polylang' => __DIR__ . '/../..' . '/include/class-polylang.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->classMap = ComposerStaticInit7dc73dfbbc007ce0d677088d041ad7d4::$classMap; }, null, ClassLoader::class); } } vendor/composer/LICENSE 0000644 00000002056 15136114124 0010676 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/composer/autoload_real.php 0000644 00000002161 15136114124 0013212 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInit7dc73dfbbc007ce0d677088d041ad7d4 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit7dc73dfbbc007ce0d677088d041ad7d4', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit7dc73dfbbc007ce0d677088d041ad7d4', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit7dc73dfbbc007ce0d677088d041ad7d4::getInitializer($loader)); $loader->register(true); return $loader; } } vendor/autoload.php 0000644 00000001403 15136114124 0010356 0 ustar 00 <?php // autoload.php @generated by Composer if (PHP_VERSION_ID < 50600) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, $err); } elseif (!headers_sent()) { echo $err; } } trigger_error( $err, E_USER_ERROR ); } require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit7dc73dfbbc007ce0d677088d041ad7d4::getLoader(); js/build/languages-step.js 0000644 00000025052 15136114124 0011536 0 ustar 00 var __webpack_exports__ = {}; /** * @package Polylang */ jQuery( function ( $ ) { var addLanguageForm = $( '.languages-step' ); // Form element. var languageFields = $( '#language-fields' ); // Element where to append hidden fields for creating language. var languagesTable = $( '#languages' ); // Table element contains languages list to create. var languagesListTable = $( '#languages tbody' ); // Table rows with languages list to create. var definedLanguagesListTable = $( '#defined-languages tbody' ); // Table rows with already defined languages list. var languagesList = $( '#lang_list' ); // Select form element with predefined languages without already created languages. var nextStepButton = $( '[name="save_step"]' ); // The button for continuing to the next step. var messagesContainer = $( '#messages' ); // Element where to display error messages. var languagesMap = new Map(); // Languages map object for managing the languages to create. var dialog = $( '#dialog' ); // Dialog box for alerting the language selected has not been added to the list. /** * Add a language in the list to create it in Polylang settings * * @param {object} language The language object */ function addLanguage( language ) { // language properties come from the select dropdown which is built server side and well escaped. // see template view-wizard-step-languages.php. var languageValueHtml = $( '<td />' ).text( language.text ).prepend( language.flagUrl ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend var languageTrashIconHtml = $( '<td />' ) .append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append $( '<span />' ) .addClass( 'dashicons dashicons-trash' ) .attr( 'data-language', language.locale ) .append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append $( '<span />' ) .addClass( 'screen-reader-text' ) .text( pll_wizard_params.i18n_remove_language_icon ) ) ); // see the comment and the hardcoded code above. languageTrashIconHtml and languageValueHtml are safe. var languageLineHtml = $( '<tr />' ).prepend( languageTrashIconHtml ).prepend( languageValueHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend var languageFieldHtml = $( '<input />' ).attr( { type: 'hidden', name: 'languages[]' } ).val( language.locale ); languagesList.val( '' ); languagesList.selectmenu( 'refresh' ); // Refresh jQuery selectmenu widget after changing the value. languagesMap.set( language.locale, language ); // see above how languageLineHtml is built. languagesListTable.append( languageLineHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append // Bind click event on trash icon. languagesListTable.on( 'click', 'span[data-language=' + language.locale + ']', function ( event ) { event.preventDefault(); // Remove line in languages table. $( this ).parents( 'tr' ).remove(); // Remove input field. var languageField = languageFields.children( 'input[value=' + $( this ).data( 'language' ) + ']' ).remove(); // If there is no more languages hide languages table. if ( languagesListTable.children().length <= 0 ) { languagesTable.hide(); } // Remove language from the Map. languagesMap.delete( $( this ).data( 'language' ) ); // Hide error message. hideError(); } ); // see above how languageFieldHtml is built. // Add hidden input field for posting the form. languageFields.append( languageFieldHtml ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append } /** * Display an error message * * @param {string} message The message to display */ function showError( message ) { messagesContainer.empty(); // html is hardcoded and use of jQuery text method which is safe to add message value. // In addition message is i18n value which is initialized server side in PLL_Wizard::add_step_languages and correctly escaped. messagesContainer.prepend( $( '<p/>' ).addClass( 'error' ).text( message ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend } /** * Hide all error messages and fields in error */ function hideError() { messagesContainer.empty(); addLanguageForm.find( '.error' ).removeClass( 'error field-in-error' ); } /** * Style the field to indicate where the error is * * @param {object} field The jQuery element which is in error */ function showFieldInError( field ) { field.addClass( 'error field-in-error' ); } /** * Focus on a specific element * * @param {object} field The jQuery element which will be focused */ function focusOnField( field ) { field.trigger( 'focus' ); } /** * Disable a specific button * * @param {object} button */ function disableButton( button ){ button.prop( 'disabled', true ); // Because the button is disabled we need to add the value of the button to ensure it will pass in the request. addLanguageForm.append( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append $( '<input />' ).prop( { type: 'hidden', name: button.prop( 'name' ), value: button.prop( 'value' ) } ) ); } /** * Remove error when a new selection is done in languages list. */ languagesList.on( 'selectmenuchange', function () { hideError();; } ); /** * Bind click event on "Add language" button */ $( '#add-language' ).on( 'click', function ( event ) { hideError(); var selectedOption = event.currentTarget.form.lang_list.options[event.currentTarget.form.lang_list.selectedIndex]; if ( '' !== selectedOption.value && ! languagesMap.has( selectedOption.value ) ) { addLanguage( { locale: selectedOption.value, text: selectedOption.innerText, name: $( selectedOption ).data( 'language-name' ), flagUrl: $( selectedOption ).data( 'flag-html' ) } ); // Show table of languages. languagesTable.show(); // Put back the focus on the select language field after clicking on "Add language button". focusOnField( $( '#lang_list-button' ) ); } else { var message = pll_wizard_params.i18n_no_language_selected; if ( languagesMap.has( selectedOption.value ) ) { message = pll_wizard_params.i18n_language_already_added; } showError( message ); showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) ); focusOnField( $( '#lang_list-button' ) ); } } ); /** * Bind submit event on "add_lang" form */ addLanguageForm.on( 'submit', function ( event ) { // Verify if there is at least one language. var isLanguagesAlreadyDefined = definedLanguagesListTable.children().length > 0; var selectedLanguage = $( '#lang_list' ).val(); if ( languagesMap.size <= 0 && ! isLanguagesAlreadyDefined ) { if ( '' === selectedLanguage ) { showError( pll_wizard_params.i18n_no_language_added ); showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) ); focusOnField( $( '#lang_list-button' ) ); } else { showError( pll_wizard_params.i18n_add_language_needed ); showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) ); focusOnField( $( '#add-language' ) ); // Put the focus on the "Add language" button. } return false; } // Verify if the language has been added in the list otherwise display a dialog box to confirm what to do. if ( '' !== selectedLanguage ) { // Verify we don't add a duplicate language before opening the dialog box otherwise display an error message. if ( ! languagesMap.has( selectedLanguage ) ) { dialog.dialog( 'open' ); } else { showError( pll_wizard_params.i18n_language_already_added ); showFieldInError( languagesList.next( 'span.ui-selectmenu-button' ) ); focusOnField( $( '#lang_list-button' ) ); } return false; } disableButton( nextStepButton ); } ); // Is there an error return by PHP ? var searchParams = new URLSearchParams( document.location.search ); if ( searchParams.has( 'activate_error' ) ) { // If the error code exists, display it. if ( undefined !== pll_wizard_params[ searchParams.get( 'activate_error' ) ] ) { showError( pll_wizard_params[ searchParams.get( 'activate_error' ) ] ); } } function confirmDialog( what ) { switch ( what ) { case 'yes': var selectedOption = $( '#lang_list' ).children( ':selected' ); addLanguage( { locale: selectedOption[0].value, text: selectedOption[0].innerText, name: $( selectedOption ).data( 'language-name' ), flagUrl: $( selectedOption ).data( 'flag-html' ) } ); break; case 'no': // Empty select form field and submit again the form. languagesList.val( '' ); break; case 'ignore': } dialog.dialog( 'close' ); if ( 'ignore' === what ) { focusOnField( $( '#lang_list-button' ) ); } else { addLanguageForm.submit(); } } // Initialize dialog box in the case a language is selected but not added in the list. dialog.dialog( { autoOpen: false, modal: true, draggable: false, resizable: false, title: pll_wizard_params.i18n_dialog_title, minWidth: 600, maxWidth: '100%', open: function ( event, ui ) { // Change dialog box position for rtl language if ( $( 'body' ).hasClass( 'rtl' ) ) { $( this ).parent().css( { right: $( this ).parent().css( 'left' ), left: 'auto' } ); } // Display language name and flag information in dialog box. $( this ).find( '#dialog-language' ).text( $( '#lang_list' ).children( ':selected' ).first().text() ); // language properties come from the select dropdown #lang_list which is built server side and well escaped. // see template view-wizard-step-languages.php. $( this ).find( '#dialog-language-flag' ).empty().prepend( $( '#lang_list' ).children( ':selected' ).data( 'flag-html' ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend }, buttons: [ { text: pll_wizard_params.i18n_dialog_yes_button, click: function ( event ) { confirmDialog( 'yes' ); } }, { text: pll_wizard_params.i18n_dialog_no_button, click: function ( event ) { confirmDialog( 'no' ); } }, { text: pll_wizard_params.i18n_dialog_ignore_button, click: function ( event ) { confirmDialog( 'ignore' ); } } ] } ) } ); js/build/block-editor.min.js 0000644 00000010164 15136114124 0011755 0 ustar 00 "use strict";var __webpack_exports__={};const languagesList=jQuery(".post_lang_choice"),initializeConfirmationModal=()=>{const{__:t}=wp.i18n,e=jQuery("<div/>",{id:"pll-dialog",style:"display:none;"}).text(t("Are you sure you want to change the language of the current content?","polylang"));languagesList.after(e);const a=new Promise(((a,n)=>{const i=t=>{switch(t){case"yes":languagesList.data("old-value",languagesList.children(":selected").first().val()),a();break;case"no":languagesList.val(languagesList.data("old-value")),n("Cancel")}e.dialog("close")},l={autoOpen:!1,modal:!0,draggable:!1,resizable:!1,title:t("Change language","polylang"),minWidth:600,maxWidth:"100%",open:function(t,e){jQuery("body").hasClass("rtl")&&jQuery(this).parent().css({right:jQuery(this).parent().css("left"),left:"auto"})},close:function(t,e){i("no")},buttons:[{text:t("OK","polylang"),click:function(t){i("yes")}},{text:t("Cancel","polylang"),click:function(t){i("no")}}]};jQuery.ui.version>="1.12.0"?Object.assign(l,{classes:{"ui-dialog":"pll-confirmation-modal"}}):Object.assign(l,{dialogClass:"pll-confirmation-modal"}),e.dialog(l)}));return{dialogContainer:e,dialogResult:a}},initializeLanguageOldValue=()=>{languagesList.attr("data-old-value",languagesList.children(":selected").first().val())};function initMetaboxAutoComplete(){jQuery(".tr_lang").each((function(){var t=jQuery(this).attr("id").substring(8),e=jQuery(this).parent().parent().siblings(".pll-edit-column");jQuery(this).autocomplete({minLength:0,source:ajaxurl+"?action=pll_posts_not_translated&post_language="+jQuery(".post_lang_choice").val()+"&translation_language="+t+"&post_type="+jQuery("#post_type").val()+"&_pll_nonce="+jQuery("#_pll_nonce").val(),select:function(a,n){jQuery("#htr_lang_"+t).val(n.item.id),e.html(n.item.link)}}),jQuery(this).on("blur",(function(){jQuery(this).val()||(jQuery("#htr_lang_"+t).val(0),e.html(e.siblings(".hidden").children().clone()))}))}))}const filterPathMiddleware=(t,e,a)=>{const n=t.path.split("?")[0].replace(/^\/+|\/+$/g,"");return Object.values(e).find((t=>n===t))?a(t):t},filter_path_middleware=filterPathMiddleware;function getCurrentLanguage(){const t=document.querySelector("[name=post_lang_choice]");return null===t?pllDefaultLanguage:t.value}function addLanguageParameter(t){return void 0===t.data||null===t.data?t.path+=(t.path.indexOf("?")>=0?"&lang=":"?lang=")+getCurrentLanguage():t.data.lang=getCurrentLanguage(),t}wp.apiFetch.use((function(t,e){return void 0!==t.url||"undefined"==typeof pllFilteredRoutes?e(t):e(filter_path_middleware(t,pllFilteredRoutes,addLanguageParameter))})),jQuery((function(t){initializeLanguageOldValue(),t(".post_lang_choice").on("change",(function(e){const{select:a,dispatch:n,subscribe:i}=wp.data,l=function(){const t=a("core/editor");return!t.getEditedPostAttribute("title")?.trim()&&!t.getEditedPostContent()&&!t.getEditedPostAttribute("excerpt")?.trim()}(),{addQueryArgs:o}=wp.url,r=initializeConfirmationModal(),{dialogContainer:s}=r;let{dialogResult:u}=r;const c=e.target;var d;location.pathname.match(/post-new.php/gi)&&l&&(d=c.value,-1!=location.search.indexOf("new_lang")?window.location.search=window.location.search.replace(/(?:new_lang=[^&]*)(&)?(.*)/,"new_lang="+d+"$1$2"):window.location.search=window.location.search+(-1!=window.location.search.indexOf("?")?"&":"?")+"new_lang="+d),t(this).data("old-value")===c.value||l?(initializeLanguageOldValue(),u=Promise.resolve()):s.dialog("open"),u.then((()=>{let e={action:"post_lang_choice",lang:c.value,post_type:t("#post_type").val(),post_id:t("#post_ID").val(),_pll_nonce:t("#_pll_nonce").val()};t.post(ajaxurl,e,(function(){!function(){let t=null;const e=a("core/editor").getCurrentPost(),l=new Promise((function(n,l){t=i((function(){const t=a("core/editor").getCurrentPost(),{id:i,status:r,type:s}=t;a("core").getLastEntitySaveError("postType",s,i)&&l(),e.modified!==t.modified&&(location.pathname.match(/post-new.php/gi)&&"auto-draft"!==r&&i&&window.history.replaceState({id:i},"Post "+i,o("post.php",{post:i,action:"edit"})),n())}))}));n("core/editor").savePost(),l.then((function(){window.location.reload()}),(function(){t()})).catch((function(){t()}))}()}))}),(()=>{}))})),initMetaboxAutoComplete()})); js/build/post.min.js 0000644 00000004132 15136114124 0010362 0 ustar 00 var __webpack_exports__={};jQuery((function(e){e.ajaxPrefilter((function(n,t,a){"string"==typeof n.data&&-1!==n.data.indexOf("action=ajax-tag-search")&&(lang=e(':input[name="inline_lang_choice"]').val())&&(n.data="lang="+lang+"&"+n.data)}))})),jQuery((function(e){const n=document.getElementById("the-list");new MutationObserver((n=>{for(const i of n){const o=Array.from(i.addedNodes).filter((e=>e.nodeType===Node.ELEMENT_NODE))[0];if(0<i.addedNodes.length&&o.classList.contains("inline-editor")){const s=Number(o.id.substring(5));if(s>0){const l=o.querySelector('select[name="inline_lang_choice"]'),c=document.querySelector("#lang_"+String(s)).innerHTML;l.value=c,t(c),a(c),l.addEventListener("change",(function(e){const n=e.target.value;t(n),a(n)}))}}function t(n){"undefined"!=typeof pll_term_languages&&e.each(pll_term_languages,(function(t,a){e.each(a,(function(a,i){e.each(i,(function(i){id="#"+a+"-"+pll_term_languages[t][a][i],n==t?e(id).show():e(id).hide()}))}))}))}function a(n){"undefined"!=typeof pll_page_languages&&e.each(pll_page_languages,(function(t,a){e.each(a,(function(a){v=e('#post_parent option[value="'+pll_page_languages[t][a]+'"]'),n==t?v.show():v.hide()}))}))}}})).observe(n,{childList:!0,subtree:!0})})),jQuery((function(e){e(document).ajaxSuccess((function(n,t,a){if("string"==typeof a.data){var i=wpAjax.unserialize(a.data);void 0!==i.action&&"inline-save"==i.action&&function(n){var t=new Array;e(".translation_"+n).each((function(){t.push(e(this).parent().parent().attr("id").substring(5))}));var a={action:"pll_update_post_rows",post_id:n,translations:t.join(","),post_type:e("input[name='post_type']").val(),screen:e("input[name='screen']").val(),_pll_nonce:e("input[name='_inline_edit']").val()};e.post(ajaxurl,a,(function(n){if(n){var t=wpAjax.parseAjaxResponse(n,"pll-ajax-response");e.each(t.responses,(function(){"row"==this.what&&e("#post-"+this.supplemental.post_id).replaceWith(this.data)}))}}))}(i.post_ID)}}))})),jQuery((function(e){e.ajaxPrefilter((function(n,t,a){"string"==typeof n.data&&-1!==n.data.indexOf("action=find_posts")&&(n.data="pll_post_id="+e("#affected").val()+"&"+n.data)}))})); js/build/term.js 0000644 00000017740 15136114124 0007573 0 ustar 00 var __webpack_exports__ = {}; /** * @package Polylang */ /** * Quick edit */ jQuery( function ( $ ) { const handleQuickEditInsertion = ( mutationsList ) => { for ( const mutation of mutationsList ) { const addedNodes = Array.from( mutation.addedNodes ).filter( el => el.nodeType === Node.ELEMENT_NODE ) const form = addedNodes[0]; if ( 0 < mutation.addedNodes.length && form.classList.contains( 'inline-edit-row' ) ) { // WordPress has inserted the quick edit form. const term_id = Number( form.id.substring( 5 ) ); if ( term_id > 0 ) { // Get the language dropdown. const select = form.querySelector( 'select[name="inline_lang_choice"]' ); const lang = document.querySelector( '#lang_' + String( term_id ) ).innerHTML; select.value = lang; // Populates the dropdown with the post language. // Disable the language dropdown for default categories. const default_cat = document.querySelector( `#default_cat_${term_id}` )?.innerHTML; if ( term_id == default_cat ) { select.disabled = true; } } } } } const table = document.getElementById( 'the-list' ); if ( null !== table ) { // Ensure the table is displayed before listening to any change. const config = { childList: true, subtree: true }; const observer = new MutationObserver( handleQuickEditInsertion ); observer.observe( table, config); } } ); /** * Update rows of translated terms when adding / deleting a translation or when the language is modified in quick edit. * Acts on ajaxSuccess event. */ jQuery( function ( $ ) { $( document ).ajaxSuccess( function ( event, xhr, settings ) { function update_rows( term_id ) { // collect old translations var translations = new Array(); $( '.translation_' + term_id ).each( function () { translations.push( $( this ).parent().parent().attr( 'id' ).substring( 4 ) ); } ); var data = { action: 'pll_update_term_rows', term_id: term_id, translations: translations.join( ',' ), taxonomy: $( "input[name='taxonomy']" ).val(), post_type: $( "input[name='post_type']" ).val(), screen: $( "input[name='screen']" ).val(), _pll_nonce: $( '#_pll_nonce' ).val() }; // get the modified rows in ajax and update them $.post( ajaxurl, data, function ( response ) { if ( response ) { // Target a non existing WP HTML id to avoid a conflict with WP ajax requests. var res = wpAjax.parseAjaxResponse( response, 'pll-ajax-response' ); $.each( res.responses, function () { if ( 'row' == this.what ) { // data is built with a call to WP_Terms_List_Table::single_row method // which uses internally other WordPress methods which escape correctly values. // For Polylang language columns the HTML code is correctly escaped in PLL_Admin_Filters_Columns::term_column method. $( "#tag-" + this.supplemental.term_id ).replaceWith( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith } } ); } } ); } var data = wpAjax.unserialize( settings.data ); // what were the data sent by the ajax request? if ( 'undefined' != typeof( data['action'] ) ) { switch ( data['action'] ) { // when adding a term, the new term_id is in the ajax response case 'add-tag': // Target a non existing WP HTML id to avoid a conflict with WP ajax requests. res = wpAjax.parseAjaxResponse( xhr.responseXML, 'pll-ajax-response' ); $.each( res.responses, function () { if ( 'term' == this.what ) { update_rows( this.supplemental.term_id ); } } ); // and also reset translations hidden input fields $( '.htr_lang' ).val( 0 ); break; // when deleting a term case 'delete-tag': update_rows( data['tag_ID'] ); break; // in case the language is modified in quick edit and breaks translations case 'inline-save-tax': update_rows( data['tax_ID'] ); break; } } } ); } ); jQuery( function ( $ ) { // translations autocomplete input box function init_translations() { $( '.tr_lang' ).each( function () { var tr_lang = $( this ).attr( 'id' ).substring( 8 ); var td = $( this ).parent().parent().siblings( '.pll-edit-column' ); $( this ).autocomplete( { minLength: 0, source: ajaxurl + '?action=pll_terms_not_translated' + '&term_language=' + $( '#term_lang_choice' ).val() + '&term_id=' + $( "input[name='tag_ID']" ).val() + '&taxonomy=' + $( "input[name='taxonomy']" ).val() + '&translation_language=' + tr_lang + '&post_type=' + typenow + '&_pll_nonce=' + $( '#_pll_nonce' ).val(), select: function ( event, ui ) { $( '#htr_lang_' + tr_lang ).val( ui.item.id ); // ui.item.link is built and come from server side and is well escaped when necessary td.html( ui.item.link ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html }, } ); // when the input box is emptied $( this ).on( 'blur', function () { if ( ! $( this ).val() ) { $( '#htr_lang_' + tr_lang ).val( 0 ); // Value is retrieved from HTML already generated server side td.html( td.siblings( '.hidden' ).children().clone() ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html } } ); } ); } init_translations(); // ajax for changing the term's language $( '#term_lang_choice' ).on( 'change', function () { var value = $( this ).val(); var lang = $( this ).children( 'option[value="' + value + '"]' ).attr( 'lang' ); var dir = $( '.pll-translation-column > span[lang="' + lang + '"]' ).attr( 'dir' ); var data = { action: 'term_lang_choice', lang: value, from_tag: $( "input[name='from_tag']" ).val(), term_id: $( "input[name='tag_ID']" ).val(), taxonomy: $( "input[name='taxonomy']" ).val(), post_type: typenow, _pll_nonce: $( '#_pll_nonce' ).val() }; $.post( ajaxurl, data, function ( response ) { // Target a non existing WP HTML id to avoid a conflict with WP ajax requests. var res = wpAjax.parseAjaxResponse( response, 'pll-ajax-response' ); $.each( res.responses, function () { switch ( this.what ) { case 'translations': // translations fields // Data is built and come from server side and is well escaped when necessary $( "#term-translations" ).html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html init_translations(); break; case 'parent': // parent dropdown list for hierarchical taxonomies // data correctly escaped in PLL_Admin_Filters_Term::term_lang_choice method which uses wp_dropdown_categories function. $( '#parent' ).replaceWith( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith break; case 'tag_cloud': // popular items // data correctly escaped in PLL_Admin_Filters_Term::term_lang_choice method which uses wp_tag_cloud and wp_generate_tag_cloud functions. $( '.tagcloud' ).replaceWith( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith break; case 'flag': // flag in front of the select dropdown // Data is built and come from server side and is well escaped when necessary $( '.pll-select-flag' ).html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html break; } } ); // Modifies the text direction $( 'body' ).removeClass( 'pll-dir-rtl' ).removeClass( 'pll-dir-ltr' ).addClass( 'pll-dir-' + dir ); } ); } ); } ); js/build/languages-step.min.js 0000644 00000006653 15136114124 0012326 0 ustar 00 var __webpack_exports__={};jQuery((function(a){var e=a(".languages-step"),n=a("#language-fields"),t=a("#languages"),l=a("#languages tbody"),i=a("#defined-languages tbody"),r=a("#lang_list"),s=a('[name="save_step"]'),d=a("#messages"),o=new Map,g=a("#dialog");function u(e){var i=a("<td />").text(e.text).prepend(e.flagUrl),s=a("<td />").append(a("<span />").addClass("dashicons dashicons-trash").attr("data-language",e.locale).append(a("<span />").addClass("screen-reader-text").text(pll_wizard_params.i18n_remove_language_icon))),d=a("<tr />").prepend(s).prepend(i),g=a("<input />").attr({type:"hidden",name:"languages[]"}).val(e.locale);r.val(""),r.selectmenu("refresh"),o.set(e.locale,e),l.append(d),l.on("click","span[data-language="+e.locale+"]",(function(e){e.preventDefault(),a(this).parents("tr").remove();n.children("input[value="+a(this).data("language")+"]").remove();l.children().length<=0&&t.hide(),o.delete(a(this).data("language")),_()})),n.append(g)}function p(e){d.empty(),d.prepend(a("<p/>").addClass("error").text(e))}function _(){d.empty(),e.find(".error").removeClass("error field-in-error")}function c(a){a.addClass("error field-in-error")}function m(a){a.trigger("focus")}r.on("selectmenuchange",(function(){_()})),a("#add-language").on("click",(function(e){_();var n=e.currentTarget.form.lang_list.options[e.currentTarget.form.lang_list.selectedIndex];if(""===n.value||o.has(n.value)){var l=pll_wizard_params.i18n_no_language_selected;o.has(n.value)&&(l=pll_wizard_params.i18n_language_already_added),p(l),c(r.next("span.ui-selectmenu-button")),m(a("#lang_list-button"))}else u({locale:n.value,text:n.innerText,name:a(n).data("language-name"),flagUrl:a(n).data("flag-html")}),t.show(),m(a("#lang_list-button"))})),e.on("submit",(function(n){var t,l=i.children().length>0,d=a("#lang_list").val();return o.size<=0&&!l?(""===d?(p(pll_wizard_params.i18n_no_language_added),c(r.next("span.ui-selectmenu-button")),m(a("#lang_list-button"))):(p(pll_wizard_params.i18n_add_language_needed),c(r.next("span.ui-selectmenu-button")),m(a("#add-language"))),!1):""!==d?(o.has(d)?(p(pll_wizard_params.i18n_language_already_added),c(r.next("span.ui-selectmenu-button")),m(a("#lang_list-button"))):g.dialog("open"),!1):((t=s).prop("disabled",!0),void e.append(a("<input />").prop({type:"hidden",name:t.prop("name"),value:t.prop("value")})))}));var f=new URLSearchParams(document.location.search);function h(n){switch(n){case"yes":var t=a("#lang_list").children(":selected");u({locale:t[0].value,text:t[0].innerText,name:a(t).data("language-name"),flagUrl:a(t).data("flag-html")});break;case"no":r.val("")}g.dialog("close"),"ignore"===n?m(a("#lang_list-button")):e.submit()}f.has("activate_error")&&void 0!==pll_wizard_params[f.get("activate_error")]&&p(pll_wizard_params[f.get("activate_error")]),g.dialog({autoOpen:!1,modal:!0,draggable:!1,resizable:!1,title:pll_wizard_params.i18n_dialog_title,minWidth:600,maxWidth:"100%",open:function(e,n){a("body").hasClass("rtl")&&a(this).parent().css({right:a(this).parent().css("left"),left:"auto"}),a(this).find("#dialog-language").text(a("#lang_list").children(":selected").first().text()),a(this).find("#dialog-language-flag").empty().prepend(a("#lang_list").children(":selected").data("flag-html"))},buttons:[{text:pll_wizard_params.i18n_dialog_yes_button,click:function(a){h("yes")}},{text:pll_wizard_params.i18n_dialog_no_button,click:function(a){h("no")}},{text:pll_wizard_params.i18n_dialog_ignore_button,click:function(a){h("ignore")}}]})})); js/build/classic-editor.js 0000644 00000036331 15136114124 0011526 0 ustar 00 /******/ "use strict"; var __webpack_exports__ = {}; ;// CONCATENATED MODULE: ./js/src/lib/confirmation-modal.js /** * @package Polylang */ const languagesList = jQuery( '.post_lang_choice' ); // Dialog box for alerting the user about a risky changing. const initializeConfirmationModal = () => { // We can't use underscore or lodash in this common code because it depends of the context classic or block editor. // Classic editor underscore is loaded, Block editor lodash is loaded. const { __ } = wp.i18n; // Create dialog container. const dialogContainer = jQuery( '<div/>', { id: 'pll-dialog', style: 'display:none;' } ).text( __( 'Are you sure you want to change the language of the current content?', 'polylang' ) ); // Put it after languages list dropdown. // PHPCS ignore dialogContainer is a new safe HTML code generated above. languagesList.after( dialogContainer ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.after const dialogResult = new Promise( ( confirm, cancel ) => { const confirmDialog = ( what ) => { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent switch ( what ) { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent case 'yes': // Confirm the new language. languagesList.data( 'old-value', languagesList.children( ':selected' ).first().val() ); confirm(); break; case 'no': // Revert to the old language. languagesList.val( languagesList.data( 'old-value' ) ); cancel( 'Cancel' ); break; } dialogContainer.dialog( 'close' ); // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent } // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent // Initialize dialog box in the case a language is selected but not added in the list. const dialogOptions = { autoOpen: false, modal: true, draggable: false, resizable: false, title: __( 'Change language', 'polylang' ), minWidth: 600, maxWidth: '100%', open: function ( event, ui ) { // Change dialog box position for rtl language if ( jQuery( 'body' ).hasClass( 'rtl' ) ) { jQuery( this ).parent().css( { right: jQuery( this ).parent().css( 'left' ), left: 'auto' } ); } }, close: function ( event, ui ) { // When we're closing the dialog box we need to cancel the language change as we click on Cancel button. confirmDialog( 'no' ); }, buttons: [ { text: __( 'OK', 'polylang' ), click: function ( event ) { confirmDialog( 'yes' ); } }, { text: __( 'Cancel', 'polylang' ), click: function ( event ) { confirmDialog( 'no' ); } } ] }; if ( jQuery.ui.version >= '1.12.0' ) { Object.assign( dialogOptions, { classes: { 'ui-dialog': 'pll-confirmation-modal' } } ); } else { Object.assign( dialogOptions, { dialogClass: 'pll-confirmation-modal' } ); // jQuery UI 1.11.4 - WP < 5.6 } dialogContainer.dialog( dialogOptions ); } ); return { dialogContainer, dialogResult }; } const initializeLanguageOldValue = () => { // Keep the old language value to be able to compare to the new one and revert to it if necessary. languagesList.attr( 'data-old-value', languagesList.children( ':selected' ).first().val() ); }; ;// CONCATENATED MODULE: ./js/src/lib/metabox-autocomplete.js /** * @package Polylang */ // Translations autocomplete input box. function initMetaboxAutoComplete() { jQuery('.tr_lang').each( function () { var tr_lang = jQuery(this).attr('id').substring(8); var td = jQuery(this).parent().parent().siblings('.pll-edit-column'); jQuery(this).autocomplete( { minLength: 0, source: ajaxurl + '?action=pll_posts_not_translated' + '&post_language=' + jQuery('.post_lang_choice').val() + '&translation_language=' + tr_lang + '&post_type=' + jQuery('#post_type').val() + '&_pll_nonce=' + jQuery('#_pll_nonce').val(), select: function (event, ui) { jQuery('#htr_lang_' + tr_lang).val(ui.item.id); // ui.item.link is built and come from server side and is well escaped when necessary td.html(ui.item.link); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html }, } ); // when the input box is emptied jQuery(this).on( 'blur', function () { if ( ! jQuery(this).val() ) { jQuery('#htr_lang_' + tr_lang).val(0); // Value is retrieved from HTML already generated server side td.html(td.siblings('.hidden').children().clone()); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html } } ); } ); } ;// CONCATENATED MODULE: ./js/src/classic-editor.js /** * @package Polylang */ // tag suggest in metabox jQuery( function ( $ ) { $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) { var lang = $( '.post_lang_choice' ).val(); if ( 'string' === typeof options.data && -1 !== options.url.indexOf( 'action=ajax-tag-search' ) && lang ) { options.data = 'lang=' + lang + '&' + options.data; } } ); } ); // overrides tagBox.get jQuery( function ( $ ) { // overrides function to add the language tagBox.get = function ( id ) { var tax = id.substr( id.indexOf( '-' ) + 1 ); // add the language in the $_POST variable var data = { action: 'get-tagcloud', lang: $( '.post_lang_choice' ).val(), tax: tax } $.post( ajaxurl, data, function ( r, stat ) { if ( 0 == r || 'success' != stat ) { r = wpAjax.broken; } // @see code from WordPress core https://github.com/WordPress/WordPress/blob/5.2.2/wp-admin/js/tags-box.js#L291 // @see wp_generate_tag_cloud function which generate the escaped HTML https://github.com/WordPress/WordPress/blob/a02b5cc2a8eecb8e076fbb7cf4de7bd2ec8a8eb1/wp-includes/category-template.php#L966-L975 r = $( '<div />' ).addClass( 'the-tagcloud' ).attr( 'id', 'tagcloud-' + tax ).html( r ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html $( 'a', r ).on( 'click', function () { tagBox.flushTags( $( this ).closest( '.inside' ).children( '.tagsdiv' ), this ); return false; } ); var tagCloud = $( '#tagcloud-' + tax ); // add an if else condition to allow modifying the tags outputted when switching the language var v = tagCloud.css( 'display' ); if ( v ) { // See the comment above when r variable is created. $( '#tagcloud-' + tax ).replaceWith( r ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith $( '#tagcloud-' + tax ).css( 'display', v ); } else { // See the comment above when r variable is created. $( '#' + id ).after( r ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.after } } ); } } ); jQuery( function ( $ ) { // collect taxonomies - code partly copied from WordPress var taxonomies = new Array(); $( '.categorydiv' ).each( function () { var this_id = $( this ).attr( 'id' ), taxonomyParts, taxonomy; taxonomyParts = this_id.split( '-' ); taxonomyParts.shift(); taxonomy = taxonomyParts.join( '-' ); taxonomies.push( taxonomy ); // store the taxonomy for future use // add our hidden field in the new category form - for each hierarchical taxonomy // to set the language when creating a new category // html code inserted come from html code itself. $( '#' + taxonomy + '-add-submit' ).before( // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.before $( '<input />' ).attr( 'type', 'hidden' ) .attr( 'id', taxonomy + '-lang' ) .attr( 'name', 'term_lang_choice' ) .attr( 'value', $( '.post_lang_choice' ).val() ) ); } ); // Initialize current language to be able to compare if it changes. initializeLanguageOldValue(); // ajax for changing the post's language in the languages metabox $( '.post_lang_choice' ).on( 'change', function ( event ) { // Initialize the confirmation dialog box. const confirmationModal = initializeConfirmationModal(); const { dialogContainer: dialog } = confirmationModal; let { dialogResult } = confirmationModal; // The selected option in the dropdown list. const selectedOption = event.target; if ( $( this ).data( 'old-value' ) !== selectedOption.value && ! isEmptyPost() ) { dialog.dialog( 'open' ); } else { dialogResult = Promise.resolve(); } // phpcs:disable PEAR.Functions.FunctionCallSignature.EmptyLine dialogResult.then( () => { var lang = selectedOption.options[selectedOption.options.selectedIndex].lang; // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent var dir = $( '.pll-translation-column > span[lang="' + lang + '"]' ).attr( 'dir' ); // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent var data = { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent action: 'post_lang_choice', lang: selectedOption.value, post_type: $( '#post_type' ).val(), taxonomies: taxonomies, post_id: $( '#post_ID' ).val(), _pll_nonce: $( '#_pll_nonce' ).val() } $.post( ajaxurl, data, function ( response ) { // Target a non existing WP HTML id to avoid a conflict with WP ajax requests. var res = wpAjax.parseAjaxResponse( response, 'pll-ajax-response' ); $.each( res.responses, function () { switch ( this.what ) { case 'translations': // translations fields // Data is built and come from server side and is well escaped when necessary $( '.translations' ).html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html initMetaboxAutoComplete(); break; case 'taxonomy': // categories metabox for posts var tax = this.data; // @see wp_terms_checklist https://github.com/WordPress/WordPress/blob/5.2.2/wp-admin/includes/template.php#L175 // @see https://github.com/WordPress/WordPress/blob/5.2.2/wp-admin/includes/class-walker-category-checklist.php#L89-L111 $( '#' + tax + 'checklist' ).html( this.supplemental.all ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html // @see wp_popular_terms_checklist https://github.com/WordPress/WordPress/blob/5.2.2/wp-admin/includes/template.php#L236 $( '#' + tax + 'checklist-pop' ).html( this.supplemental.populars ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html // @see wp_dropdown_categories https://github.com/WordPress/WordPress/blob/5.5.1/wp-includes/category-template.php#L336 // which is called by PLL_Admin_Classic_Editor::post_lang_choice to generate supplemental.dropdown $( '#new' + tax + '_parent' ).replaceWith( this.supplemental.dropdown ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith $( '#' + tax + '-lang' ).val( $( '.post_lang_choice' ).val() ); // hidden field break; case 'pages': // parent dropdown list for pages // @see wp_dropdown_pages https://github.com/WordPress/WordPress/blob/5.2.2/wp-includes/post-template.php#L1186-L1208 // @see https://github.com/WordPress/WordPress/blob/5.2.2/wp-includes/class-walker-page-dropdown.php#L88 $( '#parent_id' ).html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html break; case 'flag': // flag in front of the select dropdown // Data is built and come from server side and is well escaped when necessary $( '.pll-select-flag' ).html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html break; case 'permalink': // Sample permalink var div = $( '#edit-slug-box' ); if ( '-1' != this.data && div.children().length ) { // @see get_sample_permalink_html https://github.com/WordPress/WordPress/blob/5.2.2/wp-admin/includes/post.php#L1425-L1454 div.html( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html } break; } } ); // Update the old language with the new one to be able to compare it in the next changing. initializeLanguageOldValue(); // modifies the language in the tag cloud $( '.tagcloud-link' ).each( function () { var id = $( this ).attr( 'id' ); tagBox.get( id ); } ); // Modifies the text direction $( 'body' ).removeClass( 'pll-dir-rtl' ).removeClass( 'pll-dir-ltr' ).addClass( 'pll-dir-' + dir ); $( '#content_ifr' ).contents().find( 'html' ).attr( 'lang', lang ).attr( 'dir', dir ); $( '#content_ifr' ).contents().find( 'body' ).attr( 'dir', dir ); pll.media.resetAllAttachmentsCollections(); } ) }, () => {} // Do nothing when promise is rejected by clicking the Cancel dialog button. ); // phpcs:enable PEAR.Functions.FunctionCallSignature.EmptyLine function isEmptyPost() { const title = $( 'input#title' ).val(); const content = $( 'textarea#content' ).val(); const excerpt = $( 'textarea#excerpt' ).val(); return ! title && ! content && ! excerpt; } } ); initMetaboxAutoComplete(); } ); /** * @since 3.0 * * @namespace pll */ var pll = window.pll || {}; /** * @since 3.0 * * @namespace pll.media */ _.extend( pll, { media: {} } ); /** * @since 3.0 * * @alias pll.media * @memberOf pll * @namespace */ var media = _.extend( pll.media, /** @lends pll.media.prototype */ { /** * TODO: Find a way to delete references to Attachments collections that are not used anywhere else. * * @type {wp.media.model.Attachments} */ attachmentsCollections : [], /** * Imitates { @see wp.media.query } but log all Attachments collections created. * * @param {Object} [props] * @return {wp.media.model.Attachments} */ query: function ( props ) { var attachments = pll.media.query.delegate( props ); pll.media.attachmentsCollections.push( attachments ); return attachments; }, resetAllAttachmentsCollections: function () { this.attachmentsCollections.forEach( function ( attachmentsCollection ) { /** * First reset the { @see wp.media.model.Attachments } collection. * Then, if it is mirroring a { @see wp.media.model.Query } collection, * refresh this one too, so it will fetch new data from the server, * and then the wp.media.model.Attachments collection will synchronize with the new data. */ attachmentsCollection.reset(); if (attachmentsCollection.mirroring) { attachmentsCollection.mirroring._hasMore = true; attachmentsCollection.mirroring.reset(); } } ); } } ); if ( 'undefined' !== typeof wp && 'undefined' !== typeof wp.media ) { /** * @since 3.0 * * @memberOf pll.media */ media.query = _.extend( media.query, /** @lends pll.media.query prototype */ { /** * @type Function References WordPress { @see wp.media.query } constructor */ delegate: wp.media.query } ) // Substitute WordPress media query shortcut with our decorated function. wp.media.query = media.query } js/build/nav-menu.min.js 0000644 00000003057 15136114124 0011130 0 ustar 00 var __webpack_exports__={};jQuery((function(e){e("#update-nav-menu").on("click",(function(t){t.target&&t.target.className&&-1!=t.target.className.indexOf("item-edit")&&(e("input[value='#pll_switcher'][type=text]").parent().parent().parent().each((function(){var t=e(this).attr("id").substring(19);e(this).children("p:not( .field-move )").remove(),h=e("<input>").attr({type:"hidden",id:"edit-menu-item-title-"+t,name:"menu-item-title["+t+"]",value:pll_data.title}),e(this).append(h),h=e("<input>").attr({type:"hidden",id:"edit-menu-item-url-"+t,name:"menu-item-url["+t+"]",value:"#pll_switcher"}),e(this).append(h),h=e("<input>").attr({type:"hidden",id:"edit-menu-item-pll-detect-"+t,name:"menu-item-pll-detect["+t+"]",value:1}),e(this).append(h),ids=Array("hide_if_no_translation","hide_current","force_home","show_flags","show_names","dropdown");for(var i=0,a=ids.length;i<a;i++)p=e("<p>").attr("class","description"),e(this).prepend(p),label=e("<label>").attr("for","edit-menu-item-"+ids[i]+"-"+t).text(" "+pll_data.strings[ids[i]]),p.append(label),cb=e("<input>").attr({type:"checkbox",id:"edit-menu-item-"+ids[i]+"-"+t,name:"menu-item-"+ids[i]+"["+t+"]",value:1}),(void 0!==pll_data.val[t]&&1==pll_data.val[t][ids[i]]||void 0===pll_data.val[t]&&"show_names"==ids[i])&&cb.prop("checked",!0),label.prepend(cb)})),e(".menu-item-data-object-id").each((function(){var t=e(this).val(),i=["names-","flags-"];e.each(i,(function(a,n){e("#edit-menu-item-show_"+n+t).on("change",(function(){1!=e(this).prop("checked")&&e("#edit-menu-item-show_"+i[1-a]+t).prop("checked",!0)}))}))})))}))})); js/build/block-editor.js 0000644 00000031222 15136114124 0011171 0 ustar 00 /******/ "use strict"; var __webpack_exports__ = {}; ;// CONCATENATED MODULE: ./js/src/lib/confirmation-modal.js /** * @package Polylang */ const languagesList = jQuery( '.post_lang_choice' ); // Dialog box for alerting the user about a risky changing. const initializeConfirmationModal = () => { // We can't use underscore or lodash in this common code because it depends of the context classic or block editor. // Classic editor underscore is loaded, Block editor lodash is loaded. const { __ } = wp.i18n; // Create dialog container. const dialogContainer = jQuery( '<div/>', { id: 'pll-dialog', style: 'display:none;' } ).text( __( 'Are you sure you want to change the language of the current content?', 'polylang' ) ); // Put it after languages list dropdown. // PHPCS ignore dialogContainer is a new safe HTML code generated above. languagesList.after( dialogContainer ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.after const dialogResult = new Promise( ( confirm, cancel ) => { const confirmDialog = ( what ) => { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent switch ( what ) { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent case 'yes': // Confirm the new language. languagesList.data( 'old-value', languagesList.children( ':selected' ).first().val() ); confirm(); break; case 'no': // Revert to the old language. languagesList.val( languagesList.data( 'old-value' ) ); cancel( 'Cancel' ); break; } dialogContainer.dialog( 'close' ); // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent } // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent // Initialize dialog box in the case a language is selected but not added in the list. const dialogOptions = { autoOpen: false, modal: true, draggable: false, resizable: false, title: __( 'Change language', 'polylang' ), minWidth: 600, maxWidth: '100%', open: function ( event, ui ) { // Change dialog box position for rtl language if ( jQuery( 'body' ).hasClass( 'rtl' ) ) { jQuery( this ).parent().css( { right: jQuery( this ).parent().css( 'left' ), left: 'auto' } ); } }, close: function ( event, ui ) { // When we're closing the dialog box we need to cancel the language change as we click on Cancel button. confirmDialog( 'no' ); }, buttons: [ { text: __( 'OK', 'polylang' ), click: function ( event ) { confirmDialog( 'yes' ); } }, { text: __( 'Cancel', 'polylang' ), click: function ( event ) { confirmDialog( 'no' ); } } ] }; if ( jQuery.ui.version >= '1.12.0' ) { Object.assign( dialogOptions, { classes: { 'ui-dialog': 'pll-confirmation-modal' } } ); } else { Object.assign( dialogOptions, { dialogClass: 'pll-confirmation-modal' } ); // jQuery UI 1.11.4 - WP < 5.6 } dialogContainer.dialog( dialogOptions ); } ); return { dialogContainer, dialogResult }; } const initializeLanguageOldValue = () => { // Keep the old language value to be able to compare to the new one and revert to it if necessary. languagesList.attr( 'data-old-value', languagesList.children( ':selected' ).first().val() ); }; ;// CONCATENATED MODULE: ./js/src/lib/metabox-autocomplete.js /** * @package Polylang */ // Translations autocomplete input box. function initMetaboxAutoComplete() { jQuery('.tr_lang').each( function () { var tr_lang = jQuery(this).attr('id').substring(8); var td = jQuery(this).parent().parent().siblings('.pll-edit-column'); jQuery(this).autocomplete( { minLength: 0, source: ajaxurl + '?action=pll_posts_not_translated' + '&post_language=' + jQuery('.post_lang_choice').val() + '&translation_language=' + tr_lang + '&post_type=' + jQuery('#post_type').val() + '&_pll_nonce=' + jQuery('#_pll_nonce').val(), select: function (event, ui) { jQuery('#htr_lang_' + tr_lang).val(ui.item.id); // ui.item.link is built and come from server side and is well escaped when necessary td.html(ui.item.link); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html }, } ); // when the input box is emptied jQuery(this).on( 'blur', function () { if ( ! jQuery(this).val() ) { jQuery('#htr_lang_' + tr_lang).val(0); // Value is retrieved from HTML already generated server side td.html(td.siblings('.hidden').children().clone()); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html } } ); } ); } ;// CONCATENATED MODULE: ./js/src/lib/filter-path-middleware.js /** * @package Polylang */ /** * Filters requests for translatable entities. * This logic is shared across all Polylang plugins. * * @since 3.5 * * @param {APIFetchOptions} options * @param {Array} filteredRoutes * @param {CallableFunction} filter * @returns {APIFetchOptions} */ const filterPathMiddleware = ( options, filteredRoutes, filter ) => { const cleanPath = options.path.split( '?' )[0].replace(/^\/+|\/+$/g, ''); // Get path without query parameters and trim '/'. return Object.values( filteredRoutes ).find( ( path ) => cleanPath === path ) ? filter( options ) : options; } /* harmony default export */ const filter_path_middleware = (filterPathMiddleware); ;// CONCATENATED MODULE: ./js/src/block-editor.js /** * @package Polylang */ /** * Filter REST API requests to add the language in the request * * @since 2.5 */ wp.apiFetch.use( function ( options, next ) { /* * If options.url is defined, this is not a REST request but a direct call to post.php for legacy metaboxes. * If `filteredRoutes` is not defined, return early. */ if ( 'undefined' !== typeof options.url || 'undefined' === typeof pllFilteredRoutes ) { return next( options ); } return next( filter_path_middleware( options, pllFilteredRoutes, addLanguageParameter ) ); } ); /** * Gets the language of the currently edited post, fallback to default language if none is found. * * @since 2.5 * * @return {Element.value} */ function getCurrentLanguage() { const lang = document.querySelector( '[name=post_lang_choice]' ); if ( null === lang ) { return pllDefaultLanguage; } return lang.value; } /** * Adds language parameter according to the current one (query string for GET, body for PUT and POST). * * @since 3.5 * * @param {APIFetchOptions} options * @returns {APIFetchOptions} */ function addLanguageParameter( options ) { if ( 'undefined' === typeof options.data || null === options.data ) { // GET options.path += ( ( options.path.indexOf( '?' ) >= 0 ) ? '&lang=' : '?lang=' ) + getCurrentLanguage(); } else { // PUT, POST options.data.lang = getCurrentLanguage(); } return options; } /** * Handles internals of the metabox: * Language select, autocomplete input field. * * @since 1.5 * * Save post after lang choice is done and redirect to the same page for refreshing all the data. * * @since 2.5 * * Link post saving after refreshing the metabox. * * @since 3.0 */ jQuery( function ( $ ) { // Initialize current language to be able to compare if it changes. initializeLanguageOldValue(); // Ajax for changing the post's language in the languages metabox $( '.post_lang_choice' ).on( 'change', function ( event ) { const { select, dispatch, subscribe } = wp.data; const emptyPost = isEmptyPost(); const { addQueryArgs } = wp.url; // Initialize the confirmation dialog box. const confirmationModal = initializeConfirmationModal(); const { dialogContainer : dialog } = confirmationModal; let { dialogResult } = confirmationModal; const selectedOption = event.target; // The selected option in the dropdown list. // Specific case for empty posts. // Place at the beginning because window.location change triggers automatically page reloading. if ( location.pathname.match( /post-new.php/gi ) && emptyPost ) { reloadPageForEmptyPost( selectedOption.value ); } // Otherwise send an ajax request to refresh the legacy metabox and set the post language with the new language. // It needs a confirmation of the user before changing the language. // Need to wait the ajax response before triggering the block editor post save action. if ( $( this ).data( 'old-value' ) !== selectedOption.value && ! emptyPost ) { dialog.dialog( 'open' ); } else { // Update the old language with the new one to be able to compare it in the next change. // Because the page isn't reloaded in this case. initializeLanguageOldValue(); dialogResult = Promise.resolve(); } dialogResult.then( () => { let data = { // phpcs:ignore PEAR.Functions.FunctionCallSignature.Indent action: 'post_lang_choice', lang: selectedOption.value, post_type: $( '#post_type' ).val(), post_id: $( '#post_ID' ).val(), _pll_nonce: $( '#_pll_nonce' ).val() } // Update post language in database as soon as possible. // Because, in addition of the block editor save process, the legacy metabox uses a post.php process to update the language and is too late compared to the page reload. $.post( ajaxurl, data, function () { blockEditorSavePostAndReloadPage(); } ); }, () => {} // Do nothing when promise is rejected by clicking the Cancel dialog button. ); function isEmptyPost() { const editor = select( 'core/editor' ); return ! editor.getEditedPostAttribute( 'title' )?.trim() && ! editor.getEditedPostContent() && ! editor.getEditedPostAttribute( 'excerpt' )?.trim(); } /** * Reload the block editor page for empty posts. * * @param {string} lang The target language code. */ function reloadPageForEmptyPost( lang ) { // Change the new_lang parameter with the new language value for reloading the page // WPCS location.search is never written in the page, just used to reload page with the right value of new_lang // new_lang input is controlled server side in PHP. The value come from the dropdown list of language returned and escaped server side. // Notice that window.location changing triggers automatically page reloading. if ( -1 != location.search.indexOf( 'new_lang' ) ) { // use regexp non capturing group to replace new_lang parameter no matter where it is and capture other parameters which can be behind it window.location.search = window.location.search.replace( /(?:new_lang=[^&]*)(&)?(.*)/, 'new_lang=' + lang + '$1$2' ); // phpcs:ignore WordPressVIPMinimum.JS.Window.location, WordPressVIPMinimum.JS.Window.VarAssignment } else { window.location.search = window.location.search + ( ( -1 != window.location.search.indexOf( '?' ) ) ? '&' : '?' ) + 'new_lang=' + lang; // phpcs:ignore WordPressVIPMinimum.JS.Window.location, WordPressVIPMinimum.JS.Window.VarAssignment } }; /** * Triggers block editor post save and reload the block editor page when everything is ok. */ function blockEditorSavePostAndReloadPage() { let unsubscribe = null; const previousPost = select( 'core/editor').getCurrentPost(); // Listen if the savePost is completely done by subscribing to its events. const savePostIsDone = new Promise( function ( resolve, reject ) { unsubscribe = subscribe( function () { const post = select( 'core/editor').getCurrentPost(); const { id, status, type } = post; const error = select( 'core' ) .getLastEntitySaveError( 'postType', type, id ); if ( error ) { reject(); } if ( previousPost.modified !== post.modified ) { if ( location.pathname.match( /post-new.php/gi ) && status !== 'auto-draft' && id ) { window.history.replaceState( { id }, 'Post ' + id, addQueryArgs( 'post.php', { post: id, action: 'edit' } ) ); } resolve(); } } ); } ); // Triggers the post save. dispatch( 'core/editor' ).savePost(); // Process savePostIsDone.then( function () { // If the post is well saved, we can reload the page window.location.reload(); }, function () { // If the post save failed unsubscribe(); } ).catch( function () { // If an exception is thrown unsubscribe(); } ); }; } ); initMetaboxAutoComplete(); } ); js/build/admin.js 0000644 00000034526 15136114124 0007715 0 ustar 00 var __webpack_exports__ = {}; /** * @package Polylang */ jQuery( function( $ ) { // languages list table // accessibility to row actions on focus // mainly copy paste of WP code from common.js var transitionTimeout; $( 'table.languages' ).on( { // restricted to languages list table focusin: function() { clearTimeout( transitionTimeout ); var focusedRowActions = $( this ).find( '.row-actions' ); // transitionTimeout is necessary for Firefox, but Chrome won't remove the CSS class without a little help. $( '.row-actions' ).not( this ).removeClass( 'visible' ); focusedRowActions.addClass( 'visible' ); }, focusout: function() { // Tabbing between post title and .row-actions links needs a brief pause, otherwise // the .row-actions div gets hidden in transit in some browsers ( ahem, Firefox ). transitionTimeout = setTimeout( function() { focusedRowActions.removeClass( 'visible' ); }, 30 ); } }, 'tr' ); // acts on the whole tr instead of single td as we have actions links in several columns /** * Common functions and variables for overriding languages and flags dropdown list by a jQuery UI selectmenu widget. */ // Add a boolean variable to be able to check jQuery UI >= 1.12 which is introduced in WP 5.6. // Backward compatibility WP < 5.6 var isJqueryUImin112 = $.ui.version >= '1.12.0'; // Allow to check if a flag list dropdown is present. Not present in the Wizard steps or other settings page. var flagListExist = $( "#flag_list" ).length; // Allow to check if a language list dropdown is present. Not present in other settings page. var langListExist = $( "#lang_list" ).length; // jQuery UI selectmenu widget width option var defaultSelectmenuWidth = '95%'; var wizardSelectmenuWidth = '100%'; // Inject flag image when jQuery UI selectmenu is created or an item is selected. // jQuery UI 1.12 introduce a wrapper inside de li tag which is necessary to selectmenu widget to work correctly. // Mainly copy from the original jQuery UI 1.12 selectmenu widget _renderItem method. // Note this code works fine with jQuery UI 1.11.4 too. var selectmenuRenderItem = function( ul, item ) { var li = $( '<li>' ); var wrapper = $( '<div>'); if ( item.disabled ) { this._addClass( li, null, "ui-state-disabled" ); } this._setText( wrapper, item.label ); // Add the flag from the data attribute in the selected element. wrapper.prepend( $( item.element ).data( 'flag-html' ) ); wrapper.children( 'img' ).addClass( 'ui-icon' ); return li.append( wrapper ).appendTo( ul ); }; // Override selected item to inject flag for jQuery UI less than 1.12. var selectmenuRefreshButtonText = function( selectElement ) { var buttonText = $( selectElement ).selectmenu( 'instance' ).buttonText; buttonText.prepend( $( selectElement ).children( ':selected' ).data( 'flag-html' ) ); buttonText.children( 'img' ).addClass( 'ui-icon' ); }; // Override selected item since jQuery UI 1.12 which introduces extension point method _renderButtonItem. // @see https://api.jqueryui.com/1.12/selectmenu/#method-_renderButtonItem _renderButtonItem documentation. var selectmenuRenderButtonItem = function ( selectElement ) { var buttonItem = $( '<span>' ); this._setText( buttonItem, selectElement.label ); this._addClass( buttonItem, "ui-selectmenu-text" ); // Add the flag from the data attribute in the selected element. buttonItem.prepend( $( selectElement.element ).data( 'flag-html' ) ); buttonItem.children( 'img' ).addClass( 'ui-icon' ); return buttonItem; } /** * Initialize a jQuery UI selectmenu widget on a DOM element * * @param {*} element - The jQuery object representing the DOM element to attach the widget with. * @param {*} config - All the parameters - options and callbacks - necessary to configure the jQuery UI selectmenu widget. * @return {Object} - The jQuery UI selectmenu widget object instance. */ function initializeSelectmenuWidget( element, config ) { // Create the jQuery UI selectmenu widget for flags list dropdown and return its instance. var selectmenuWidgetInstance = element.selectmenu( config ).selectmenu( 'instance' ); // Overrides each item in the jQuery UI selectmenu list by injecting flag image. selectmenuWidgetInstance._renderItem = selectmenuRenderItem; // Override the selected item rendering for jQuery UI 1.12 if ( isJqueryUImin112 ) { selectmenuWidgetInstance._renderButtonItem = selectmenuRenderButtonItem; // Need to refresh to take in account the new button item rendering method after the selectmenu widget instanciaion. selectmenuWidgetInstance.refresh(); } return selectmenuWidgetInstance } /** * Selectmenu widget common parameters for its configuration: options and callbacks. */ // Selectmenu widget options var selectmenuOptions = { width: defaultSelectmenuWidth, classes: { 'ui-selectmenu-menu': 'pll-selectmenu-menu', 'ui-selectmenu-button': 'pll-selectmenu-button', } }; // Selectmenu widget callbacks var selectmenuFlagListCallbacks = {}; // Callbacks when Selectmenu widget create or select event is triggered. var createSelectCallback = function( event, ui ) { selectmenuRefreshButtonText( event.target ); } /** * Overrides the flag dropdown list with our customized jquery ui selectmenu. */ // Callbacks when Selectmenu widget change or open event is triggered. // Needed to correctly refresh the selected element in the list when editing an existing language or when the value change is triggered by the language choice. // jQuery UI 1.11 callback version. var changeOpenCallback = function( event, ui ){ selectmenuRefreshButtonText( $( event.target ).selectmenu( 'refresh' ) ); } // jQueryUI 1.12 callback version. var changeOpenCallbackjQueryUI112 = function( event, ui ){ // Just a refresh of the menu is needed with jQuery UI 1.12 because _renderButtonItem is triggered and then inject correctly the flag. $( event.target ).selectmenu( 'refresh' ); } // There is no need of create and select callbacks with jQuery UI 1.12 because overriding _renderButtonItem method do the job. if ( isJqueryUImin112 ) { selectmenuFlagListCallbacks = { change: changeOpenCallbackjQueryUI112, open: changeOpenCallbackjQueryUI112, }; } else { selectmenuFlagListCallbacks = { create: createSelectCallback, select: createSelectCallback, change: changeOpenCallback, open: changeOpenCallback, }; } // Create the selectmenu widget only if the field is present. if ( flagListExist ) { // Create the jQuery UI selectmenu widget for flags list dropdown and return its instance. var selectmenuFlagList = initializeSelectmenuWidget( $( '#flag_list' ), Object.assign( {}, selectmenuOptions, selectmenuFlagListCallbacks ) ); $( '#lang_list' ).on( 'languageChanged', function( event, flag ) { // Refresh the flag field selectmenuFlagList.element.val( flag ); selectmenuFlagList._trigger( 'change' ); } ); } /** * Language choice in predefined languages in Polylang Languages settings page and wizard. * Overrides the predefined language dropdown list with our customized jQuery ui selectmenu widget. */ /** * Fill the other language form fields from the language element selected in the language list dropdown. * * @param {Object} language - language object of the selected element in the language list dropdown. */ function fillLanguageFields( language ) { $( '#lang_slug' ).val( language.slug ); $( '#lang_locale' ).val( language.locale ); $( 'input[name="rtl"]' ).val( language.rtl ); $( '#lang_name' ).val( language.name ); } /** * Parse selected language element in the language list dropdown. * * @param {object} event - jQuery triggered event. * @return {object} The language object with its named properties. */ function parseSelectedLanguage( event ) { var selectedElement = $('option:selected', event.target); var values = selectedElement.val().split(':') return { slug: values[0], locale: values[1], rtl: [values[2]], flag: values[3], name: selectedElement.text().split(' - ')[0] // At the moment there is no need of the 2nd part because it corresponds on the locale which is already known by splitting the selected element value }; } // Callback when selectmenu widget change event is triggered. var changeCallback = function( event, ui ) { var language = parseSelectedLanguage( event ); fillLanguageFields( language ); $( event.target ).trigger( 'languageChanged', language.flag ); }; // Create the jQuery UI selectmenu widget languages list dropdown and return its instance. var selectmenuLangListCallbacks = {}; // For the wizard we need a 100% width. So we override the previous defined value of selectmenuOptions. if( $( '#lang_list' ).closest( '.pll-wizard-content' ).length > 0 ) { selectmenuOptions = Object.assign( selectmenuOptions, { width: wizardSelectmenuWidth } ); } // There is no need of create and select callbacks with jQuery UI 1.12 because overrinding _renderButtonItem method do the job. if ( isJqueryUImin112 ) { selectmenuLangListCallbacks = { change: changeCallback, }; } else { selectmenuLangListCallbacks = { create: createSelectCallback, select: createSelectCallback, change: changeCallback, }; } if ( langListExist ) { initializeSelectmenuWidget( $( '#lang_list' ), Object.assign( {}, selectmenuOptions, selectmenuLangListCallbacks ) ); } // strings translations // save translations when pressing enter $( '.translation input' ).on( 'keydown', function( event ){ if ( 'Enter' === event.key ) { event.preventDefault(); $( '#submit' ).trigger( 'click' ); } } ); // settings page // click on configure link $( '#the-list' ).on( 'click', '.configure>a', function(){ $( '.pll-configure' ).hide().prev().show(); $( this ).closest( 'tr' ).hide().next().show(); return false; } ); // cancel $( '#the-list' ).on( 'click', '.cancel', function(){ $( this ).closest( 'tr' ).hide().prev().show(); } ); // save settings $( '#the-list' ).on( 'click', '.save', function(){ var tr = $( this ).closest( 'tr' ); var parts = tr.attr( 'id' ).split( '-' ); var data = { action: 'pll_save_options', pll_ajax_settings: true, module: parts[parts.length - 1], _pll_nonce: $( '#_pll_nonce' ).val() }; data = tr.find( ':input' ).serialize() + '&' + $.param( data ); $.post( ajaxurl, data, function( response ) { // Target a non existing WP HTML id to avoid a conflict with WP ajax requests. var res = wpAjax.parseAjaxResponse( response, 'pll-ajax-response' ); $.each( res.responses, function() { /** * Fires after saving the settings, before applying changes to the DOM. * * @since 3.6.0 * * @param {Object} response The response from the AJAX call. * @param {HTMLElement} tr The HTML element containing the fields. */ wp.hooks.doAction( 'pll_settings_saved', this, tr.get( 0 ) ); switch ( this.what ) { case 'license-update': $( '#pll-license-' + this.data ).replaceWith( this.supplemental.html ); break; case 'success': tr.hide().prev().show(); // close only if there is no error case 'error': $( '.settings-error' ).remove(); // remove previous messages if any $( 'h1' ).after( this.data ); // Make notices dismissible // copy paste of common.js from WP 4.2.2 $( '.notice.is-dismissible' ).each( function() { var $this = $( this ), $button = $( '<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>' ), btnText = pll_admin.dismiss_notice || ''; // Ensure plain text $button.find( '.screen-reader-text' ).text( btnText ); // Whitelist because of how the button is built. See above $this.append( $button ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append $button.on( 'click.wp-dismiss-notice', function( event ) { event.preventDefault(); $this.fadeTo( 100, 0, function() { $( this ).slideUp( 100, function() { $( this ).remove(); } ); } ); } ); } ); break; } } ); } ); } ); // act when pressing enter or esc in configurations $( '.pll-configure' ).on( 'keydown', function( event ){ if ( 'Enter' === event.key ) { event.preventDefault(); $( this ).find( '.save' ).trigger( 'click' ); } if ( 'Escape' === event.key ) { event.preventDefault(); $( this ).find( '.cancel' ).trigger( 'click' ); } } ); // settings URL modifications // manages visibility of fields $( "input[name='force_lang']" ).on( 'change', function() { function pll_toggle( a, test ) { test ? a.show() : a.hide(); } var value = $( this ).val(); pll_toggle( $( '#pll-domains-table' ), 3 == value ); pll_toggle( $( "#pll-hide-default" ), 3 > value ); pll_toggle( $( "#pll-rewrite" ), 2 > value ); pll_toggle( $( "#pll-redirect-lang" ), 2 > value ); } ); // settings license // deactivate button $( '.pll-deactivate-license' ).on( 'click', function() { var data = { action: 'pll_deactivate_license', pll_ajax_settings: true, id: $( this ).attr( 'id' ), _pll_nonce: $( '#_pll_nonce' ).val() }; $.post( ajaxurl, data, function( response ){ $( '#pll-license-' + response.id ).replaceWith( response.html ); } ); } ); // Manage closing the metabox. // close postboxes that should be closed $( '.if-js-closed' ).removeClass( 'if-js-closed' ).addClass( 'closed' ); // postboxes setup if ( 'undefined' !== typeof postboxes ) { postboxes.add_postbox_toggles( pagenow ); } } ); js/build/term.min.js 0000644 00000006054 15136114124 0010351 0 ustar 00 var __webpack_exports__={};jQuery((function(t){const a=t=>{for(const a of t){const t=Array.from(a.addedNodes).filter((t=>t.nodeType===Node.ELEMENT_NODE))[0];if(0<a.addedNodes.length&&t.classList.contains("inline-edit-row")){const a=Number(t.id.substring(5));if(a>0){const e=t.querySelector('select[name="inline_lang_choice"]'),n=document.querySelector("#lang_"+String(a)).innerHTML;e.value=n;const l=document.querySelector(`#default_cat_${a}`)?.innerHTML;a==l&&(e.disabled=!0)}}}},e=document.getElementById("the-list");if(null!==e){const t={childList:!0,subtree:!0};new MutationObserver(a).observe(e,t)}})),jQuery((function(t){t(document).ajaxSuccess((function(a,e,n){function l(a){var e=new Array;t(".translation_"+a).each((function(){e.push(t(this).parent().parent().attr("id").substring(4))}));var n={action:"pll_update_term_rows",term_id:a,translations:e.join(","),taxonomy:t("input[name='taxonomy']").val(),post_type:t("input[name='post_type']").val(),screen:t("input[name='screen']").val(),_pll_nonce:t("#_pll_nonce").val()};t.post(ajaxurl,n,(function(a){if(a){var e=wpAjax.parseAjaxResponse(a,"pll-ajax-response");t.each(e.responses,(function(){"row"==this.what&&t("#tag-"+this.supplemental.term_id).replaceWith(this.data)}))}}))}var s=wpAjax.unserialize(n.data);if(void 0!==s.action)switch(s.action){case"add-tag":res=wpAjax.parseAjaxResponse(e.responseXML,"pll-ajax-response"),t.each(res.responses,(function(){"term"==this.what&&l(this.supplemental.term_id)})),t(".htr_lang").val(0);break;case"delete-tag":l(s.tag_ID);break;case"inline-save-tax":l(s.tax_ID)}}))})),jQuery((function(t){function a(){t(".tr_lang").each((function(){var a=t(this).attr("id").substring(8),e=t(this).parent().parent().siblings(".pll-edit-column");t(this).autocomplete({minLength:0,source:ajaxurl+"?action=pll_terms_not_translated&term_language="+t("#term_lang_choice").val()+"&term_id="+t("input[name='tag_ID']").val()+"&taxonomy="+t("input[name='taxonomy']").val()+"&translation_language="+a+"&post_type="+typenow+"&_pll_nonce="+t("#_pll_nonce").val(),select:function(n,l){t("#htr_lang_"+a).val(l.item.id),e.html(l.item.link)}}),t(this).on("blur",(function(){t(this).val()||(t("#htr_lang_"+a).val(0),e.html(e.siblings(".hidden").children().clone()))}))}))}a(),t("#term_lang_choice").on("change",(function(){var e=t(this).val(),n=t(this).children('option[value="'+e+'"]').attr("lang"),l=t('.pll-translation-column > span[lang="'+n+'"]').attr("dir"),s={action:"term_lang_choice",lang:e,from_tag:t("input[name='from_tag']").val(),term_id:t("input[name='tag_ID']").val(),taxonomy:t("input[name='taxonomy']").val(),post_type:typenow,_pll_nonce:t("#_pll_nonce").val()};t.post(ajaxurl,s,(function(e){var n=wpAjax.parseAjaxResponse(e,"pll-ajax-response");t.each(n.responses,(function(){switch(this.what){case"translations":t("#term-translations").html(this.data),a();break;case"parent":t("#parent").replaceWith(this.data);break;case"tag_cloud":t(".tagcloud").replaceWith(this.data);break;case"flag":t(".pll-select-flag").html(this.data)}})),t("body").removeClass("pll-dir-rtl").removeClass("pll-dir-ltr").addClass("pll-dir-"+l)}))}))})); js/build/user.min.js 0000644 00000000704 15136114124 0010354 0 ustar 00 var __webpack_exports__={};jQuery((function(e){var n=e("#description").parent(),i=e("#description").clone(),t=n.children(".description").clone();n.children().remove(),e(".biography").each((function(){lang=e(this).attr("name").split("___"),desc=i.clone(),desc.attr("name","description_"+lang[0]),desc.attr("id","description_"+lang[0]),desc.html(e(this).val()),n.append(e("<div></div>").text(lang[1])),n.append(desc)})),n.append("<br />"),n.append(t)})); js/build/widgets.min.js 0000644 00000003173 15136114124 0011047 0 ustar 00 var __webpack_exports__={};jQuery((function(e){var t,i,n,o=void 0!==wp.blockEditor;function l(t){if(n){t=e(t);var i=o?t.prev("h3"):e(".widget-top .widget-title h3",t),l=e(".pll-lang-choice option:selected",t).val(),d=l&&n.hasOwnProperty(l)?n[l]:null;if(d){d+=" ";var s=e(".pll-lang",i);s.length?s.html(d):(flag=e("<span />").addClass("pll-lang").html(d),i.prepend(flag))}else e(".pll-lang",i).remove()}}if("undefined"!=typeof pll_widgets&&pll_widgets.hasOwnProperty("flags")&&(n=pll_widgets.flags),o)i=".widget",(t=e(".edit-widgets-main-block-list")).on("click",".wp-block-legacy-widget",(function(){l(e(this).find(".widget"))}));else{if(void 0!==wp.customize){function d(e){e.extended(wp.customize.Widgets.WidgetControl)&&(e.embedWidgetContent(),l(e.container.find(".widget")))}t=e("#customize-controls"),i=".customize-control .widget",wp.customize.control.each(d),wp.customize.control.bind("add",d)}else t=e("#widgets-right"),i=".widget";e(i,t).each((function(){l(this)}))}t.on("change",".pll-lang-choice",(function(){l(e(this).parents(".widget"))})),e(".widgets-sortables,.control-section-sidebar,.edit-widgets-main-block-list").on("change",".pll-dropdown",(function(){var t,i=e(this).parent().parent().parent().children(".widget-id").attr("value");t=e(".no-dropdown-"+i),1!=e(this).prop("checked")?t.show():t.hide()}));var s=["-show_flags","-show_names"];e.each(s,(function(t,i){e(".widgets-sortables,.control-section-sidebar,.edit-widgets-main-block-list").on("change",".pll"+i,(function(){var i=e(this).parent().parent().parent().children(".widget-id").attr("value");1!=e(this).prop("checked")&&e("#widget-"+i+s[1-t]).prop("checked",!0)}))}))})); js/build/nav-menu.js 0000644 00000007622 15136114124 0010350 0 ustar 00 var __webpack_exports__ = {}; /** * Handles the options in the language switcher nav menu metabox. * * @package Polylang */ jQuery( function ( $ ) { $( '#update-nav-menu' ).on( 'click', function ( e ) { if ( e.target && e.target.className && -1 != e.target.className.indexOf( 'item-edit' ) ) { $( "input[value='#pll_switcher'][type=text]" ).parent().parent().parent().each( function () { var item = $( this ).attr( 'id' ).substring( 19 ); $( this ).children( 'p:not( .field-move )' ).remove(); // remove default fields we don't need // item is a number part of id of parent menu item built by WordPress // pll_data is built server side with i18n strings without HTML and data retrieved from post meta // the usage of attr method is safe before append call. h = $( '<input>' ).attr( { type: 'hidden', id: 'edit-menu-item-title-' + item, name: 'menu-item-title[' + item + ']', value: pll_data.title } ); $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append h = $( '<input>' ).attr( { type: 'hidden', id: 'edit-menu-item-url-' + item, name: 'menu-item-url[' + item + ']', value: '#pll_switcher' } ); $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append // a hidden field which exits only if our jQuery code has been executed h = $( '<input>' ).attr( { type: 'hidden', id: 'edit-menu-item-pll-detect-' + item, name: 'menu-item-pll-detect[' + item + ']', value: 1 } ); $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append ids = Array( 'hide_if_no_translation', 'hide_current', 'force_home', 'show_flags', 'show_names', 'dropdown' ); // reverse order // add the fields for ( var i = 0, idsLength = ids.length; i < idsLength; i++ ) { p = $( '<p>' ).attr( 'class', 'description' ); // p is hardcoded just above by using attr method which is safe. $( this ).prepend( p ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend // item is a number part of id of parent menu item built by WordPress // pll_data is built server side with i18n strings without HTML label = $( '<label>' ).attr( 'for', 'edit-menu-item-' + ids[ i ] + '-' + item ).text( ' ' + pll_data.strings[ ids[ i ] ] ); p.append( label ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append cb = $( '<input>' ).attr( { type: 'checkbox', id: 'edit-menu-item-' + ids[ i ] + '-' + item, name: 'menu-item-' + ids[ i ] + '[' + item + ']', value: 1 } ); if ( ( typeof( pll_data.val[ item ] ) != 'undefined' && pll_data.val[ item ][ ids[ i ] ] == 1 ) || ( typeof( pll_data.val[ item ] ) == 'undefined' && ids[ i ] == 'show_names' ) ) { // show_names as default value cb.prop( 'checked', true ); } // See reasons above. Checkbox are totally hardcoded here with safe value label.prepend( cb ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend } } ); // disallow unchecking both show names and show flags $( '.menu-item-data-object-id' ).each( function () { var id = $( this ).val(); var options = ['names-', 'flags-']; $.each( options, function ( i, v ) { $( '#edit-menu-item-show_' + v + id ).on( 'change', function () { if ( true != $( this ).prop( 'checked' ) ) { $( '#edit-menu-item-show_' + options[ 1 - i ] + id ).prop( 'checked', true ); } } ); } ); } ); } } ); } ); js/build/widgets.js 0000644 00000010367 15136114124 0010270 0 ustar 00 var __webpack_exports__ = {}; /** * Adds a flag to the widgets filtered by a language. * * @package Polylang */ jQuery( function ( $ ) { var widgets_container, widgets_selector, flags, isBlockEditor = 'undefined' !== typeof wp.blockEditor; if ( 'undefined' !== typeof pll_widgets && pll_widgets.hasOwnProperty( 'flags' ) ) { flags = pll_widgets.flags; } /** * Prepend widget titles with a flag once a language is selected. * * @param {object} widget The widget element. * @return {void} Nothing. */ function add_flag( widget ) { if ( ! flags ) { return; } widget = $( widget ); var title = isBlockEditor ? widget.prev('h3') : $( '.widget-top .widget-title h3', widget ), locale = $( '.pll-lang-choice option:selected', widget ).val(), // Icon is HTML built and come from server side and is well escaped when necessary icon = ( locale && flags.hasOwnProperty( locale ) ) ? flags[ locale ] : null; if ( icon ) { icon += ' '; var current = $( '.pll-lang', title ); if ( current.length ) { current.html( icon ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html } else { flag = $( '<span />' ).addClass( 'pll-lang' ).html( icon ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html // See the comment above about the icon which is safe. So it is also safe to prepend flag which uses icon. title.prepend( flag ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend } } else { $( '.pll-lang', title ).remove(); } } if ( isBlockEditor ) { widgets_container = $( '.edit-widgets-main-block-list' ); widgets_selector = '.widget'; // Update flags when we click on the legacy widget to display its form. widgets_container.on( 'click', '.wp-block-legacy-widget', function () { add_flag( $( this ).find( '.widget' ) ); } ); } else { if ( 'undefined' !== typeof wp.customize ) { widgets_container = $( '#customize-controls' ); widgets_selector = '.customize-control .widget'; /** * WP Customizer add control listener. * * @link https://wordpress.stackexchange.com/questions/256536/callback-after-wordpress-customizer-complete-loading * * @param {object} control The control type. * @return {void} Nothing. */ function customize_add_flag( control ) { if ( ! control.extended( wp.customize.Widgets.WidgetControl ) ) { return; } /* * Make sure the widget's contents are embedded; normally this is done * when the control is expanded, for DOM performance reasons. */ control.embedWidgetContent(); // Now we know for sure the widget is fully embedded. add_flag( control.container.find( '.widget' ) ); } wp.customize.control.each( customize_add_flag ); wp.customize.control.bind( 'add', customize_add_flag ); } else { widgets_container = $( '#widgets-right' ); widgets_selector = '.widget'; } // Add flags on load. $( widgets_selector, widgets_container ).each( function () { add_flag( this ); } ); } // Update flags. widgets_container.on( 'change', '.pll-lang-choice', function () { add_flag( $( this ).parents( '.widget' ) ); } ); function pll_toggle( a, test ) { test ? a.show() : a.hide(); } // Remove all options if dropdown is checked. $( '.widgets-sortables,.control-section-sidebar,.edit-widgets-main-block-list' ).on( 'change', '.pll-dropdown', function () { var this_id = $( this ).parent().parent().parent().children( '.widget-id' ).attr( 'value' ); pll_toggle( $( '.no-dropdown-' + this_id ), true != $( this ).prop( 'checked' ) ); } ); // Disallow unchecking both show names and show flags. var options = ['-show_flags', '-show_names']; $.each( options, function ( i, v ) { $( '.widgets-sortables,.control-section-sidebar,.edit-widgets-main-block-list' ).on( 'change', '.pll' + v, function () { var this_id = $( this ).parent().parent().parent().children( '.widget-id' ).attr( 'value' ); if ( true != $( this ).prop( 'checked' ) ) { $( '#widget-' + this_id + options[ 1 - i ] ).prop( 'checked', true ); } } ); } ); } ); js/build/classic-editor.min.js 0000644 00000012153 15136114124 0012304 0 ustar 00 "use strict";var __webpack_exports__={};const languagesList=jQuery(".post_lang_choice"),initializeConfirmationModal=()=>{const{__:t}=wp.i18n,a=jQuery("<div/>",{id:"pll-dialog",style:"display:none;"}).text(t("Are you sure you want to change the language of the current content?","polylang"));languagesList.after(a);const e=new Promise(((e,l)=>{const n=t=>{switch(t){case"yes":languagesList.data("old-value",languagesList.children(":selected").first().val()),e();break;case"no":languagesList.val(languagesList.data("old-value")),l("Cancel")}a.dialog("close")},i={autoOpen:!1,modal:!0,draggable:!1,resizable:!1,title:t("Change language","polylang"),minWidth:600,maxWidth:"100%",open:function(t,a){jQuery("body").hasClass("rtl")&&jQuery(this).parent().css({right:jQuery(this).parent().css("left"),left:"auto"})},close:function(t,a){n("no")},buttons:[{text:t("OK","polylang"),click:function(t){n("yes")}},{text:t("Cancel","polylang"),click:function(t){n("no")}}]};jQuery.ui.version>="1.12.0"?Object.assign(i,{classes:{"ui-dialog":"pll-confirmation-modal"}}):Object.assign(i,{dialogClass:"pll-confirmation-modal"}),a.dialog(i)}));return{dialogContainer:a,dialogResult:e}},initializeLanguageOldValue=()=>{languagesList.attr("data-old-value",languagesList.children(":selected").first().val())};function initMetaboxAutoComplete(){jQuery(".tr_lang").each((function(){var t=jQuery(this).attr("id").substring(8),a=jQuery(this).parent().parent().siblings(".pll-edit-column");jQuery(this).autocomplete({minLength:0,source:ajaxurl+"?action=pll_posts_not_translated&post_language="+jQuery(".post_lang_choice").val()+"&translation_language="+t+"&post_type="+jQuery("#post_type").val()+"&_pll_nonce="+jQuery("#_pll_nonce").val(),select:function(e,l){jQuery("#htr_lang_"+t).val(l.item.id),a.html(l.item.link)}}),jQuery(this).on("blur",(function(){jQuery(this).val()||(jQuery("#htr_lang_"+t).val(0),a.html(a.siblings(".hidden").children().clone()))}))}))}jQuery((function(t){t.ajaxPrefilter((function(a,e,l){var n=t(".post_lang_choice").val();"string"==typeof a.data&&-1!==a.url.indexOf("action=ajax-tag-search")&&n&&(a.data="lang="+n+"&"+a.data)}))})),jQuery((function(t){tagBox.get=function(a){var e=a.substr(a.indexOf("-")+1),l={action:"get-tagcloud",lang:t(".post_lang_choice").val(),tax:e};t.post(ajaxurl,l,(function(l,n){0!=l&&"success"==n||(l=wpAjax.broken),l=t("<div />").addClass("the-tagcloud").attr("id","tagcloud-"+e).html(l),t("a",l).on("click",(function(){return tagBox.flushTags(t(this).closest(".inside").children(".tagsdiv"),this),!1}));var i=t("#tagcloud-"+e).css("display");i?(t("#tagcloud-"+e).replaceWith(l),t("#tagcloud-"+e).css("display",i)):t("#"+a).after(l)}))}})),jQuery((function(t){var a=new Array;t(".categorydiv").each((function(){var e,l;(e=t(this).attr("id").split("-")).shift(),l=e.join("-"),a.push(l),t("#"+l+"-add-submit").before(t("<input />").attr("type","hidden").attr("id",l+"-lang").attr("name","term_lang_choice").attr("value",t(".post_lang_choice").val()))})),initializeLanguageOldValue(),t(".post_lang_choice").on("change",(function(e){const l=initializeConfirmationModal(),{dialogContainer:n}=l;let{dialogResult:i}=l;const o=e.target;t(this).data("old-value")===o.value||function(){const a=t("input#title").val(),e=t("textarea#content").val(),l=t("textarea#excerpt").val();return!a&&!e&&!l}()?i=Promise.resolve():n.dialog("open"),i.then((()=>{var e=o.options[o.options.selectedIndex].lang,l=t('.pll-translation-column > span[lang="'+e+'"]').attr("dir"),n={action:"post_lang_choice",lang:o.value,post_type:t("#post_type").val(),taxonomies:a,post_id:t("#post_ID").val(),_pll_nonce:t("#_pll_nonce").val()};t.post(ajaxurl,n,(function(a){var n=wpAjax.parseAjaxResponse(a,"pll-ajax-response");t.each(n.responses,(function(){switch(this.what){case"translations":t(".translations").html(this.data),initMetaboxAutoComplete();break;case"taxonomy":var a=this.data;t("#"+a+"checklist").html(this.supplemental.all),t("#"+a+"checklist-pop").html(this.supplemental.populars),t("#new"+a+"_parent").replaceWith(this.supplemental.dropdown),t("#"+a+"-lang").val(t(".post_lang_choice").val());break;case"pages":t("#parent_id").html(this.data);break;case"flag":t(".pll-select-flag").html(this.data);break;case"permalink":var e=t("#edit-slug-box");"-1"!=this.data&&e.children().length&&e.html(this.data)}})),initializeLanguageOldValue(),t(".tagcloud-link").each((function(){var a=t(this).attr("id");tagBox.get(a)})),t("body").removeClass("pll-dir-rtl").removeClass("pll-dir-ltr").addClass("pll-dir-"+l),t("#content_ifr").contents().find("html").attr("lang",e).attr("dir",l),t("#content_ifr").contents().find("body").attr("dir",l),pll.media.resetAllAttachmentsCollections()}))}),(()=>{}))})),initMetaboxAutoComplete()}));var pll=window.pll||{};_.extend(pll,{media:{}});var media=_.extend(pll.media,{attachmentsCollections:[],query:function(t){var a=pll.media.query.delegate(t);return pll.media.attachmentsCollections.push(a),a},resetAllAttachmentsCollections:function(){this.attachmentsCollections.forEach((function(t){t.reset(),t.mirroring&&(t.mirroring._hasMore=!0,t.mirroring.reset())}))}});"undefined"!=typeof wp&&void 0!==wp.media&&(media.query=_.extend(media.query,{delegate:wp.media.query}),wp.media.query=media.query); js/build/user.js 0000644 00000002311 15136114124 0007566 0 ustar 00 var __webpack_exports__ = {}; /** * Adds one biography input field per language in the user profile. * * @package Polylang */ jQuery( function ( $ ) { // biography // FIXME there is probably a more efficient way to do this var td = $( '#description' ).parent(); var d = $( '#description' ).clone(); var span = td.children( '.description' ).clone(); td.children().remove(); $( '.biography' ).each( function () { lang = $( this ).attr( 'name' ).split( '___' ); desc = d.clone(); desc.attr( 'name', 'description_' + lang[0] ); desc.attr( 'id', 'description_' + lang[0] ); // Whitelist because description and lang value is already escaped by the side of PHP desc.html( $( this ).val() ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.html td.append( $( '<div></div>' ).text( lang[1] ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append td.append( desc ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append } ); td.append( '<br />' ); // Whitelist because description come from html code generated by WordPress td.append( span ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append } ); js/build/post.js 0000644 00000013124 15136114124 0007601 0 ustar 00 var __webpack_exports__ = {}; /** * @package Polylang */ /** * Tag suggest in quick edit */ jQuery( function ( $ ) { $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) { if ( 'string' === typeof options.data && -1 !== options.data.indexOf( 'action=ajax-tag-search' ) && ( lang = $( ':input[name="inline_lang_choice"]' ).val() ) ) { options.data = 'lang=' + lang + '&' + options.data; } } ); } ); /** * Quick edit */ jQuery( function ( $ ) { const handleQuickEditInsertion = ( mutationsList ) => { for ( const mutation of mutationsList ) { const addedNodes = Array.from( mutation.addedNodes ).filter( el => el.nodeType === Node.ELEMENT_NODE ) const form = addedNodes[0]; if ( 0 < mutation.addedNodes.length && form.classList.contains( 'inline-editor' ) ) { // WordPress has inserted the quick edit form. const post_id = Number( form.id.substring( 5 ) ); if ( post_id > 0 ) { // Get the language dropdown. const select = form.querySelector( 'select[name="inline_lang_choice"]' ); const lang = document.querySelector( '#lang_' + String( post_id ) ).innerHTML; select.value = lang; // Populates the dropdown with the post language. filter_terms( lang ); // Initial filter for category checklist. filter_pages( lang ); // Initial filter for parent dropdown. // Modify category checklist and parent dropdown on language change. select.addEventListener( 'change', function ( event ) { const newLang = event.target.value; filter_terms( newLang ); filter_pages( newLang ); } ); } } /** * Filters the category checklist. */ function filter_terms( lang ) { if ( "undefined" != typeof( pll_term_languages ) ) { $.each( pll_term_languages, function ( lg, term_tax ) { $.each( term_tax, function ( tax, terms ) { $.each( terms, function ( i ) { id = '#' + tax + '-' + pll_term_languages[ lg ][ tax ][ i ]; lang == lg ? $( id ).show() : $( id ).hide(); } ); } ); } ); } } /** * Filters the parent page dropdown list. */ function filter_pages( lang ) { if ( "undefined" != typeof( pll_page_languages ) ) { $.each( pll_page_languages, function ( lg, pages ) { $.each( pages, function ( i ) { v = $( '#post_parent option[value="' + pll_page_languages[ lg ][ i ] + '"]' ); lang == lg ? v.show() : v.hide(); } ); } ); } } } } const table = document.getElementById( 'the-list' ); const config = { childList: true, subtree: true }; const observer = new MutationObserver( handleQuickEditInsertion ); observer.observe( table, config); } ); /** * Update rows of translated posts when the language is modified in quick edit * Acts on ajaxSuccess event */ jQuery( function ( $ ) { $( document ).ajaxSuccess( function ( event, xhr, settings ) { function update_rows( post_id ) { // collect old translations var translations = new Array(); $( '.translation_' + post_id ).each( function () { translations.push( $( this ).parent().parent().attr( 'id' ).substring( 5 ) ); } ); var data = { action: 'pll_update_post_rows', post_id: post_id, translations: translations.join( ',' ), post_type: $( "input[name='post_type']" ).val(), screen: $( "input[name='screen']" ).val(), _pll_nonce: $( "input[name='_inline_edit']" ).val() // reuse quick edit nonce }; // get the modified rows in ajax and update them $.post( ajaxurl, data, function ( response ) { if ( response ) { // Since WP changeset #52710 parseAjaxResponse() return content to notice the user in a HTML tag with ajax-response id. // Not to disturb this behaviour by executing another ajax request in the ajaxSuccess event, we need to target another unexisting id. var res = wpAjax.parseAjaxResponse( response, 'pll-ajax-response' ); $.each( res.responses, function () { if ( 'row' == this.what ) { // data is built with a call to WP_Posts_List_Table::single_row method // which uses internally other WordPress methods which escape correctly values. // For Polylang language columns the HTML code is correctly escaped in PLL_Admin_Filters_Columns::post_column method. $( "#post-" + this.supplemental.post_id ).replaceWith( this.data ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.replaceWith } } ); } } ); } if ( 'string' == typeof( settings.data ) ) { // Need to check the type due to block editor sometime sending FormData objects var data = wpAjax.unserialize( settings.data ); // what were the data sent by the ajax request? if ( 'undefined' != typeof( data['action'] ) && 'inline-save' == data['action'] ) { update_rows( data['post_ID'] ); } } } ); } ); /** * Media list table * When clicking on attach link, filters find post list per media language */ jQuery( function ( $ ) { $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) { if ( 'string' === typeof options.data && -1 !== options.data.indexOf( 'action=find_posts' ) ) { options.data = 'pll_post_id=' + $( '#affected' ).val() + '&' + options.data; } } ); } ); js/build/admin.min.js 0000644 00000010372 15136114124 0010470 0 ustar 00 var __webpack_exports__={};jQuery((function(e){var t;e("table.languages").on({focusin:function(){clearTimeout(t);var n=e(this).find(".row-actions");e(".row-actions").not(this).removeClass("visible"),n.addClass("visible")},focusout:function(){t=setTimeout((function(){focusedRowActions.removeClass("visible")}),30)}},"tr");var n=e.ui.version>="1.12.0",l=e("#flag_list").length,s=e("#lang_list").length,i=function(t,n){var l=e("<li>"),s=e("<div>");return n.disabled&&this._addClass(l,null,"ui-state-disabled"),this._setText(s,n.label),s.prepend(e(n.element).data("flag-html")),s.children("img").addClass("ui-icon"),l.append(s).appendTo(t)},a=function(t){var n=e(t).selectmenu("instance").buttonText;n.prepend(e(t).children(":selected").data("flag-html")),n.children("img").addClass("ui-icon")},c=function(t){var n=e("<span>");return this._setText(n,t.label),this._addClass(n,"ui-selectmenu-text"),n.prepend(e(t.element).data("flag-html")),n.children("img").addClass("ui-icon"),n};function o(e,t){var l=e.selectmenu(t).selectmenu("instance");return l._renderItem=i,n&&(l._renderButtonItem=c,l.refresh()),l}var r={width:"95%",classes:{"ui-selectmenu-menu":"pll-selectmenu-menu","ui-selectmenu-button":"pll-selectmenu-button"}},u={},d=function(e,t){a(e.target)},p=function(t,n){a(e(t.target).selectmenu("refresh"))},g=function(t,n){e(t.target).selectmenu("refresh")};if(u=n?{change:g,open:g}:{create:d,select:d,change:p,open:p},l){var h=o(e("#flag_list"),Object.assign({},r,u));e("#lang_list").on("languageChanged",(function(e,t){h.element.val(t),h._trigger("change")}))}var f=function(t,n){var l=function(t){var n=e("option:selected",t.target),l=n.val().split(":");return{slug:l[0],locale:l[1],rtl:[l[2]],flag:l[3],name:n.text().split(" - ")[0]}}(t);!function(t){e("#lang_slug").val(t.slug),e("#lang_locale").val(t.locale),e('input[name="rtl"]').val(t.rtl),e("#lang_name").val(t.name)}(l),e(t.target).trigger("languageChanged",l.flag)},v={};e("#lang_list").closest(".pll-wizard-content").length>0&&(r=Object.assign(r,{width:"100%"})),v=n?{change:f}:{create:d,select:d,change:f},s&&o(e("#lang_list"),Object.assign({},r,v)),e(".translation input").on("keydown",(function(t){"Enter"===t.key&&(t.preventDefault(),e("#submit").trigger("click"))})),e("#the-list").on("click",".configure>a",(function(){return e(".pll-configure").hide().prev().show(),e(this).closest("tr").hide().next().show(),!1})),e("#the-list").on("click",".cancel",(function(){e(this).closest("tr").hide().prev().show()})),e("#the-list").on("click",".save",(function(){var t=e(this).closest("tr"),n=t.attr("id").split("-"),l={action:"pll_save_options",pll_ajax_settings:!0,module:n[n.length-1],_pll_nonce:e("#_pll_nonce").val()};l=t.find(":input").serialize()+"&"+e.param(l),e.post(ajaxurl,l,(function(n){var l=wpAjax.parseAjaxResponse(n,"pll-ajax-response");e.each(l.responses,(function(){switch(wp.hooks.doAction("pll_settings_saved",this,t.get(0)),this.what){case"license-update":e("#pll-license-"+this.data).replaceWith(this.supplemental.html);break;case"success":t.hide().prev().show();case"error":e(".settings-error").remove(),e("h1").after(this.data),e(".notice.is-dismissible").each((function(){var t=e(this),n=e('<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>'),l=pll_admin.dismiss_notice||"";n.find(".screen-reader-text").text(l),t.append(n),n.on("click.wp-dismiss-notice",(function(n){n.preventDefault(),t.fadeTo(100,0,(function(){e(this).slideUp(100,(function(){e(this).remove()}))}))}))}))}}))}))})),e(".pll-configure").on("keydown",(function(t){"Enter"===t.key&&(t.preventDefault(),e(this).find(".save").trigger("click")),"Escape"===t.key&&(t.preventDefault(),e(this).find(".cancel").trigger("click"))})),e("input[name='force_lang']").on("change",(function(){function t(e,t){t?e.show():e.hide()}var n=e(this).val();t(e("#pll-domains-table"),3==n),t(e("#pll-hide-default"),3>n),t(e("#pll-rewrite"),2>n),t(e("#pll-redirect-lang"),2>n)})),e(".pll-deactivate-license").on("click",(function(){var t={action:"pll_deactivate_license",pll_ajax_settings:!0,id:e(this).attr("id"),_pll_nonce:e("#_pll_nonce").val()};e.post(ajaxurl,t,(function(t){e("#pll-license-"+t.id).replaceWith(t.html)}))})),e(".if-js-closed").removeClass("if-js-closed").addClass("closed"),"undefined"!=typeof postboxes&&postboxes.add_postbox_toggles(pagenow)})); readme.txt 0000644 00000025152 15136114124 0006545 0 ustar 00 === Polylang === Contributors: Chouby, manooweb, raaaahman, marianne38, sebastienserre, greglone, hugod Donate link: https://polylang.pro Tags: multilingual, translate, translation, language, localization Requires at least: 6.2 Tested up to: 6.5 Requires PHP: 7.0 Stable tag: 3.6.1 License: GPLv3 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html Go multilingual in a simple and efficient way. Keep writing posts and taxonomy terms as usual while defining their languages all at once. == Description == With Polylang fully integrated to WordPress and using only its built-in core features (taxonomies), keep steady performances on your site and create a multilingual site featuring from just one extra language to 10 or more depending on your needs. There is no limit in the number of languages added and WordPress’ language packs are automatically downloaded when ready. = Features = Depending on the type of site you have built or are planning to build, a combination of plugins from the list below might be of interest. All plugins include a wizard allowing to setup them in just a few clicks. ### Polylang Polylang and [Polylang Pro](https://polylang.pro) share the same core providing features such as: * Translating posts, pages, media, categories, post tags, custom post types and taxonomies, RSS feeds; RTL scripts are supported. * The language is either set by the language code in URL, or you can use a different sub-domain or domain per language. * Automatic copy of categories, post tags and other metas when creating a new post or page translation. * Translating classic menus and classic widgets. Also accessible with [Site Editor Classic Features](https://wordpress.org/plugins/fse-classic/) in block themes. * Customizable language switcher available as a classic widget or a classic navigation menu item. * Compatibility with Yoast SEO. ### Polylang Pro Helps optimizing the time spent translating your site with some very useful extra features such as: * Better integration in the new Block Editor. * Language switcher available as a block. * Language options available in the widget block editor. * Template parts translatable in the site editor (FSE). * Duplicate and/or synchronize content across post translations. * Improved compatibility with other plugins such as [ACF Pro](https://polylang.pro/doc/working-with-acf-pro/). * Share the same URL slug for posts or terms across languages. * [Translate URL slugs](https://polylang.pro/doc/translating-urls-slugs/) for categories, author bases, custom post types and more... * Export and import of content in XLIFF format for outsourced professional translation. * **Access to a Premium Support for personalized assistance.** ### Polylang for WooCommerce [Add-on](https://polylang.pro/downloads/polylang-for-woocommerce/) for the compatibility with WooCommerce which provides features such as: * Translating WooCommerce pages (shop, check-out, cart, my account), product categories and global attribute terms directly in the WooCommerce interface. * Translating WooCommerce e-mails and sending them to customers in their language. * Products metadata synchronization. * Compatibility with the native WooCommerce CSV import & export tool. * Compatibility with popular plugins such as WooCommerce Subscriptions, Product Bundles, WooCommerce Bookings, Shipment Tracking and more. * Ability to use the WooCommerce REST API (available with Polylang Pro). * **Access to a Premium Support for personalized assistance.** Neither of them will allow to do automated translation. = Our other free plugins = * [WPML to Polylang](https://wordpress.org/plugins/wpml-to-polylang/) allows migrating from WPML to Polylang. * [DynaMo](https://wordpress.org/plugins/dynamo/) speeds up the translation of WordPress for all non-English sites. * [Site Editor Classic Features](https://wordpress.org/plugins/fse-classic/) allows to use classic widgets (including the Polylang language switcher) and menus in the site editor (FSE). = Credits = Thanks a lot to all translators who [help translating Polylang](https://translate.wordpress.org/projects/wp-plugins/polylang). Thanks a lot to [Alex Lopez](http://www.alexlopez.rocks/) for the design of the logo. Most of the flags included with Polylang are coming from [famfamfam](http://famfamfam.com/) and are public domain. Wherever third party code has been used, credit has been given in the code’s comments. == Installation == 1. Make sure you are using WordPress 6.2 or later and that your server is running PHP 7.0 or later (same requirement as WordPress itself). 1. If you tried other multilingual plugins, deactivate them before activating Polylang, otherwise, you may get unexpected results! 1. Install and activate the plugin as usual from the 'Plugins' menu in WordPress. 1. The [setup wizard](https://polylang.pro/doc/setup-wizard/) is automatically launched to help you get started more easily with Polylang by configuring the main features. == Frequently Asked Questions == = Where to find help ? = * First time users should read [Polylang - Getting started](https://polylang.pro/doc-category/getting-started/), which explains the basics and includes a lot of screenshots. * Read the [documentation](https://polylang.pro/doc/). It includes a [FAQ](https://polylang.pro/doc-category/faq/) and the [documentation for developers](https://polylang.pro/doc-category/developers/). * Search the [community support forum](https://wordpress.org/search/). You will probably find your answers here. * Read the sticky posts in the [community support forum](http://wordpress.org/support/plugin/polylang). * If you still have a problem, open a new thread in the [community support forum](http://wordpress.org/support/plugin/polylang). * [Polylang Pro and Polylang for WooCommerce](https://polylang.pro) users have access to our premium support through helpdesk. = Is Polylang compatible with WooCommerce? = * You need [Polylang for WooCommerce](https://polylang.pro/downloads/polylang-for-woocommerce/), premium addon described above, which will make both plugins work together. == Screenshots == 1. The Polylang languages admin panel 2. The Strings translations admin panel 3. Multilingual media library 4. The Edit Post screen with the Languages metabox == Changelog == = 3.6.1 (2024-04-09) = * Pro: Fix ACF fields not swown after a post was translated with DeepL * Remove rewrite when registering the language taxonomy #1457 * Fix search block not filtered when displayed as button only #1459 * Fix current language not kept when using switch_to_blog() in multisite #1458 = 3.6 (2024-03-18) = * Requires WP 6.2 as minimum version * Add compatibility with WP 6.5 * Pro: Add DeepL machine translation for posts * Pro: Add export and import in XLIFF 2.0/2.1 formats * Pro: Improve translator comments in exported PO files * Pro: Allow to export JSON encoded post and term metas in XLIFF files * Pro: Allow to export block sub-attributes in XLIFF files * Pro: Add footer notes block to XLIFF files * Pro: Single files are now exported directly instead of inside a zip * Pro: Reworked the language switcher navigation block * Pro: Fix language switcher navigation block justification not aligned with core settings in overlay menu (requires WP 6.5) * Pro: Fix a race condition which could lead to display a notice to the wrong user * Pro: Fix a conflict with ACF when rewrite rules are flushed with WP-CLI on a multisite * Pro: Fix import of several metas with same sources but different translations * Add filter `pll_cookie_args` to filter the Polylang cookie arguments #1406 * Fix wrong translated post types and taxononies after a `switch_to_blog()` #1415 * Fix a minor performance issue for the page for posts #1412 * Fix a JS errors after quick edit. Props @mcguffin #1435, #1444 * Fix a possible warning in view-translations-post.php #1439 = 3.5.4 (2024-02-06) = * Pro: Fix an accessibility issue int the navigation language switcher block * Pro: Fix featured image not exported for posts with blocks * Pro: Fix a conflict with the Flatsome builder * Fix a notice when using system CRON. Props arielruminski #1397 * Fix an edge case where a wrong post tag may be assigned to a post #1418 = 3.5.3 (2023-12-11) = * Pro: Fix fatal error with The Events Calendar when rewrite param of event category is set to false * Remove flag alt text in the language switcher when both the flag and language name are displayed #1393 * Fix incorrect string translations when 2 languages are sharing the same locale in a multisite #1378 * Fix posts lists not filtered by the current language when editing a post in the block editor #1386 * Fix error when a tax query is filled with unexpected data #1396 = 3.5.2 (2023-10-25) = * Pro: Fix terms not filtered by the current language in the block editor custom taxonomy component panel * Fix incorrect rewrite rules leading to error 404 for the main site on mutisite #1375 = 3.5.1 (2023-10-17) = * Pro: Fix terms not filtered by the current language in the block editor custom taxonomy component panel * Pro: Fix fatal error when using plain permalinks on multisite * Pro: Fix rewrite rules incorrectly refreshed when saving strings translations * Fix incorrect rewrite rules leading to error 404 on mutisite #1366 * Fix fatal error when using symlinked MU plugins that are not in open_basedir #1368 = 3.5 (2023-10-09) = * Requires WordPress 5.9 as minimum version * Pro: Manage navigation blocks translations in the site editor (requires WP 6.3) * Pro: Manage pages translations in the site editor (requires WP 6.3) * Pro: Manage patterns translations in the site editor (requires WP 6.3) * Pro: Remove compatibility with the navigation screen removed from Gütenberg 15.1 * Pro: Add filter 'pll_export_post_fields' to control post fields exported to XLIFF files * Pro: Do not set default translation option to "translate" for ACF fields created before Polylang Pro is activated * Pro: Fix Polylang not set as recently active when automatically deactivated by Polylang Pro * Don't output javascript type for themes supporting html5 #1332 * Hook WP_Query automatic translation to 'parse_query' instead of 'pre_get_posts' #1339 * Improve preload paths management for the block editor #1341 * Fix rewrite rules in WP 6.4 #1345 * Fix: always assign the default language to new posts and terms if no language is specified #1351 * Fix 'polylang' option not correctly created when a new site is created on a multisite #1319 * Fix front page display switched to "Your latest posts" when deleting a static home page translation #1311 * Fix wrong language assigned to terms #1336 * Fix error when updating a translated option while the blog is switched on a multisite #1342 See [changelog.txt](https://plugins.svn.wordpress.org/polylang/trunk/changelog.txt) for older changelog polylang.php 0000644 00000005411 15136114124 0007101 0 ustar 00 <?php /** * Polylang * * @package Polylang * @author WP SYNTEX * @license GPL-3.0-or-later * * @wordpress-plugin * Plugin Name: Polylang * Plugin URI: https://polylang.pro * Description: Adds multilingual capability to WordPress * Version: 3.6.1 * Requires at least: 6.2 * Requires PHP: 7.0 * Author: WP SYNTEX * Author URI: https://polylang.pro * Text Domain: polylang * Domain Path: /languages * License: GPL v3 or later * License URI: https://www.gnu.org/licenses/gpl-3.0.txt * * Copyright 2011-2019 Frédéric Demarle * Copyright 2019-2024 WP SYNTEX * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ if ( ! defined( 'ABSPATH' ) ) { exit; // Don't access directly. } if ( defined( 'POLYLANG_VERSION' ) ) { // The user is attempting to activate a second plugin instance, typically Polylang and Polylang Pro. require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-includes/pluggable.php'; if ( is_plugin_active( plugin_basename( __FILE__ ) ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); // Deactivate this plugin. // WP does not allow us to send a custom meaningful message, so just tell the plugin has been deactivated. wp_safe_redirect( add_query_arg( 'deactivate', 'true', remove_query_arg( 'activate' ) ) ); exit; } } else { // Go on loading the plugin define( 'POLYLANG_VERSION', '3.6.1' ); define( 'PLL_MIN_WP_VERSION', '6.2' ); define( 'PLL_MIN_PHP_VERSION', '7.0' ); define( 'POLYLANG_FILE', __FILE__ ); define( 'POLYLANG_DIR', __DIR__ ); // Whether we are using Polylang or Polylang Pro, get the filename of the plugin in use. if ( ! defined( 'POLYLANG_ROOT_FILE' ) ) { define( 'POLYLANG_ROOT_FILE', __FILE__ ); } if ( ! defined( 'POLYLANG_BASENAME' ) ) { define( 'POLYLANG_BASENAME', plugin_basename( __FILE__ ) ); // Plugin name as known by WP. require __DIR__ . '/vendor/autoload.php'; } define( 'POLYLANG', ucwords( str_replace( '-', ' ', dirname( POLYLANG_BASENAME ) ) ) ); if ( empty( $_GET['deactivate-polylang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification new Polylang(); } } include/nav-menu.php 0000644 00000010665 15136114124 0010434 0 ustar 00 <?php /** * @package Polylang */ /** * Manages custom menus translations * Common to admin and frontend for the customizer * * @since 1.7.7 */ class PLL_Nav_Menu { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Theme name. * * @var string */ protected $theme; /** * Array of menu ids in a given language used when auto add pages to menus. * * @var int[] */ protected $auto_add_menus = array(); /** * Constructor: setups filters and actions. * * @since 1.7.7 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->model = &$polylang->model; $this->options = &$polylang->options; $this->theme = get_option( 'stylesheet' ); add_filter( 'wp_setup_nav_menu_item', array( $this, 'wp_setup_nav_menu_item' ) ); // Integration with WP customizer add_action( 'customize_register', array( $this, 'create_nav_menu_locations' ), 5 ); // Filter _wp_auto_add_pages_to_menu by language add_action( 'transition_post_status', array( $this, 'auto_add_pages_to_menu' ), 5, 3 ); // before _wp_auto_add_pages_to_menu } /** * Assigns the title and label to the language switcher menu items * * @since 2.6 * * @param stdClass $item Menu item. * @return stdClass */ public function wp_setup_nav_menu_item( $item ) { if ( isset( $item->url ) && '#pll_switcher' === $item->url ) { $item->post_title = __( 'Languages', 'polylang' ); $item->type_label = __( 'Language switcher', 'polylang' ); } return $item; } /** * Create temporary nav menu locations ( one per location and per language ) for all non-default language * to do only one time * * @since 1.2 * * @return void */ public function create_nav_menu_locations() { static $once; global $_wp_registered_nav_menus; $arr = array(); if ( isset( $_wp_registered_nav_menus ) && ! $once ) { foreach ( $_wp_registered_nav_menus as $loc => $name ) { foreach ( $this->model->get_languages_list() as $lang ) { $arr[ $this->combine_location( $loc, $lang ) ] = $name . ' ' . $lang->name; } } $_wp_registered_nav_menus = $arr; $once = true; } } /** * Creates a temporary nav menu location from a location and a language * * @since 1.8 * * @param string $loc Nav menu location. * @param PLL_Language $lang Language object. * @return string */ public function combine_location( $loc, $lang ) { return $loc . ( strpos( $loc, '___' ) || $lang->is_default ? '' : '___' . $lang->slug ); } /** * Get nav menu locations and language from a temporary location. * * @since 1.8 * * @param string $loc Temporary location. * @return string[] { * @type string $location Nav menu location. * @type string $lang Language code. * } */ public function explode_location( $loc ) { $infos = explode( '___', $loc ); if ( 1 == count( $infos ) ) { $infos[] = $this->options['default_lang']; } return array_combine( array( 'location', 'lang' ), $infos ); } /** * Filters the option nav_menu_options for auto added pages to menu. * * @since 0.9.4 * * @param array $options Options stored in the option nav_menu_options. * @return array Modified options. */ public function nav_menu_options( $options ) { $options['auto_add'] = array_intersect( $options['auto_add'], $this->auto_add_menus ); return $options; } /** * Filters _wp_auto_add_pages_to_menu by language. * * @since 0.9.4 * * @param string $new_status Transition to this post status. * @param string $old_status Previous post status. * @param WP_Post $post Post object. * @return void */ public function auto_add_pages_to_menu( $new_status, $old_status, $post ) { if ( 'publish' != $new_status || 'publish' == $old_status || 'page' != $post->post_type || ! empty( $post->post_parent ) ) { return; } if ( ! empty( $this->options['nav_menus'][ $this->theme ] ) ) { $lang = $this->model->post->get_language( $post->ID ); $lang = empty( $lang ) ? $this->options['default_lang'] : $lang->slug; // If the page has no language yet, the default language will be assigned // Get all the menus in the page language foreach ( $this->options['nav_menus'][ $this->theme ] as $loc ) { if ( ! empty( $loc[ $lang ] ) ) { $this->auto_add_menus[] = $loc[ $lang ]; } } add_filter( 'option_nav_menu_options', array( $this, 'nav_menu_options' ) ); } } } include/functions.php 0000644 00000016005 15136114124 0010710 0 ustar 00 <?php /** * @package Polylang */ /** * Define wordpress.com VIP equivalent of uncached functions * WordPress backward compatibility functions * and miscellaneous utility functions */ if ( ! function_exists( 'wpcom_vip_get_page_by_path' ) ) { /** * Retrieves a page given its path. * * @since 2.8.3 * * @param string $page_path Page path. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which correspond to * a WP_Post object, an associative array, or a numeric array, respectively. Default OBJECT. * @param string|array $post_type Optional. Post type or array of post types. Default 'page'. * @return WP_Post|array|null WP_Post (or array) on success, or null on failure. */ function wpcom_vip_get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { return get_page_by_path( $page_path, $output, $post_type ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_page_by_path_get_page_by_path } } if ( ! function_exists( 'sanitize_locale_name' ) ) { /** * Strips out all characters not allowed in a locale code. * Backward compatibility with WP < 6.2.1. * * @since 3.5 * * @param string $locale_name The locale name to be sanitized. * @return string The sanitized value. */ function sanitize_locale_name( $locale_name ) { // Limit to A-Z, a-z, 0-9, '_', '-'. $sanitized = (string) preg_replace( '/[^A-Za-z0-9_-]/', '', $locale_name ); /** * Filters a sanitized locale name string. * Backward compatibility with WP < 6.2.1. * * @since 3.5 * * @param string $sanitized The sanitized locale name. * @param string $locale_name The locale name before sanitization. */ return apply_filters( 'sanitize_locale_name', $sanitized, $locale_name ); } } /** * Determines whether we should load the cache compatibility * * @since 2.3.8 * * @return bool True if the cache compatibility must be loaded */ function pll_is_cache_active() { /** * Filters whether we should load the cache compatibility * * @since 2.3.8 * * @bool $is_cache True if a known cache plugin is active * incl. WP Fastest Cache which doesn't use WP_CACHE */ return apply_filters( 'pll_is_cache_active', ( defined( 'WP_CACHE' ) && WP_CACHE ) || defined( 'WPFC_MAIN_PATH' ) ); } /** * Get the the current requested url * * @since 2.6 * * @return string Requested url */ function pll_get_requested_url() { if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { return set_url_scheme( esc_url_raw( wp_unslash( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ) ); } /** @var string */ $home_url = get_option( 'home' ); /* * In WP CLI context, few developers define superglobals in wp-config.php * as proposed in https://make.wordpress.org/cli/handbook/common-issues/#php-notice-undefined-index-on-_server-superglobal * So let's return the unfiltered home url to avoid a bunch of notices. */ if ( defined( 'WP_CLI' ) && WP_CLI ) { return $home_url; } /* * When using system CRON instead of WP_CRON, the superglobals are likely undefined. */ if ( defined( 'DOING_CRON' ) && DOING_CRON ) { return $home_url; } if ( WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions trigger_error( '$_SERVER[\'HTTP_HOST\'] or $_SERVER[\'REQUEST_URI\'] are required but not set.' ); } return ''; } /** * Determines whether we should load the block editor plugin or the legacy languages metabox. * * @since 2.6.0 * * @return bool True to use the block editor plugin. */ function pll_use_block_editor_plugin() { /** * Filters whether we should load the block editor plugin or the legacy languages metabox. * * @since 2.6.0 * * @param bool $use_plugin True when loading the block editor plugin. */ return class_exists( 'PLL_Block_Editor_Plugin' ) && apply_filters( 'pll_use_block_editor_plugin', ! defined( 'PLL_USE_BLOCK_EDITOR_PLUGIN' ) || PLL_USE_BLOCK_EDITOR_PLUGIN ); } /** * Tells if a constant is defined. * * @since 3.5 * * @param string $constant_name Name of the constant. * @return bool True if the constant is defined, false otherwise. * * @phpstan-param non-falsy-string $constant_name */ function pll_has_constant( string $constant_name ): bool { return defined( $constant_name ); // phpcs:ignore WordPressVIPMinimum.Constants.ConstantString.NotCheckingConstantName } /** * Returns the value of a constant if it is defined. * * @since 3.5 * * @param string $constant_name Name of the constant. * @param mixed $default Optional. Value to return if the constant is not defined. Defaults to `null`. * @return mixed The value of the constant. * * @phpstan-param non-falsy-string $constant_name * @phpstan-param int|float|string|bool|array|null $default */ function pll_get_constant( string $constant_name, $default = null ) { if ( ! pll_has_constant( $constant_name ) ) { return $default; } return constant( $constant_name ); } /** * Defines a constant if it is not already defined. * * @since 3.5 * * @param string $constant_name Name of the constant. * @param mixed $value Value to set. * @return bool True on success, false on failure or already defined. * * @phpstan-param non-falsy-string $constant_name * @phpstan-param int|float|string|bool|array|null $value */ function pll_set_constant( string $constant_name, $value ): bool { if ( pll_has_constant( $constant_name ) ) { return false; } return define( $constant_name, $value ); // phpcs:ignore WordPressVIPMinimum.Constants.ConstantString.NotCheckingConstantName } /** * Determines whether a plugin is active. * * We define our own function because `is_plugin_active()` is available only in the backend. * * @since 3.5 * * @param string $plugin_name Plugin basename. * @return bool True if activated, false otherwise. */ function pll_is_plugin_active( string $plugin_name ) { $sitewide_plugins = get_site_option( 'active_sitewide_plugins' ); $sitewide_plugins = ! empty( $sitewide_plugins ) && is_array( $sitewide_plugins ) ? array_keys( $sitewide_plugins ) : array(); $current_site_plugins = (array) get_option( 'active_plugins', array() ); $plugins = array_merge( $sitewide_plugins, $current_site_plugins ); return in_array( $plugin_name, $plugins ); } /** * Prepares and registers notices. * * Wraps `add_settings_error()` to make its use more consistent. * * @since 3.6 * * @param WP_Error $error Error object. * @return void */ function pll_add_notice( WP_Error $error ) { if ( ! $error->has_errors() ) { return; } foreach ( $error->get_error_codes() as $error_code ) { // Extract the "error" type. $data = $error->get_error_data( $error_code ); $type = empty( $data ) || ! is_string( $data ) ? 'error' : $data; $message = wp_kses( implode( '<br>', $error->get_error_messages( $error_code ) ), array( 'a' => array( 'href' ), 'br' => array(), 'code' => array(), 'em' => array(), ) ); add_settings_error( 'polylang', $error_code, $message, $type ); } } include/query.php 0000644 00000014620 15136114124 0010046 0 ustar 00 <?php /** * @package Polylang */ /** * A class to manipulate the language query var in WP_Query * * @since 2.2 */ class PLL_Query { /** * @var PLL_Model */ public $model; /** * @var WP_Query */ public $query; /** * Constructor * * @since 2.2 * * @param WP_Query $query Reference to the WP_Query object. * @param PLL_Model $model Instance of PLL_Model. */ public function __construct( &$query, &$model ) { $this->query = &$query; $this->model = &$model; } /** * Checks if the query already includes a language taxonomy. * * @since 3.0 * * @param array $qvars WP_Query query vars. * @return bool */ protected function is_already_filtered( $qvars ) { if ( isset( $qvars['lang'] ) ) { return true; } if ( ! empty( $qvars['tax_query'] ) && is_array( $qvars['tax_query'] ) ) { foreach ( $qvars['tax_query'] as $tax_query ) { if ( isset( $tax_query['taxonomy'] ) && 'language' === $tax_query['taxonomy'] ) { return true; } } } return false; } /** * Check if translated taxonomy is queried * Compatible with nested queries introduced in WP 4.1 * * @see https://wordpress.org/support/topic/tax_query-bug * * @since 1.7 * * @param array $tax_queries An array of tax queries. * @return bool */ protected function have_translated_taxonomy( $tax_queries ) { if ( is_array( $tax_queries ) ) { foreach ( $tax_queries as $tax_query ) { if ( isset( $tax_query['taxonomy'] ) && $this->model->is_translated_taxonomy( $tax_query['taxonomy'] ) && ! ( isset( $tax_query['operator'] ) && 'NOT IN' === $tax_query['operator'] ) ) { return true; } // Nested queries elseif ( is_array( $tax_query ) && $this->have_translated_taxonomy( $tax_query ) ) { return true; } } } return false; } /** * Get queried taxonomies * * @since 2.2 * * @return array queried taxonomies */ public function get_queried_taxonomies() { return ! empty( $this->query->tax_query->queried_terms ) ? array_keys( wp_list_filter( $this->query->tax_query->queried_terms, array( 'operator' => 'NOT IN' ), 'NOT' ) ) : array(); } /** * Sets the language in query. * Optimized for (and requires) WP 3.5+. * * @since 2.2 * @since 3.3 Accepts now an array of languages. * * @param PLL_Language|PLL_Language[] $languages Language object(s). * @return void */ public function set_language( $languages ) { if ( ! is_array( $languages ) ) { $languages = array( $languages ); } $tt_ids = array(); foreach ( $languages as $language ) { $tt_ids[] = $language->get_tax_prop( 'language', 'term_taxonomy_id' ); } // Defining directly the tax_query (rather than setting 'lang' avoids transforming the query by WP). $lang_query = array( 'taxonomy' => 'language', 'field' => 'term_taxonomy_id', // Since WP 3.5 'terms' => $tt_ids, 'operator' => 'IN', ); $tax_query = &$this->query->query_vars['tax_query']; if ( isset( $tax_query['relation'] ) && 'OR' === $tax_query['relation'] ) { $tax_query = array( $lang_query, $tax_query, 'relation' => 'AND', ); } elseif ( is_array( $tax_query ) ) { // The tax query is expected to be *always* an array, but it seems that 3rd parties fill it with a string // Causing a fatal error if we don't check it. // See https://wordpress.org/support/topic/fatal-error-2947/ $tax_query[] = $lang_query; } elseif ( empty( $tax_query ) ) { // Supposing the tax query has been wrongly filled with an empty string $tax_query = array( $lang_query ); } } /** * Adds the language in the query after it has checked that it won't conflict with other query vars. * * @since 2.2 * * @param PLL_Language|false $lang Language. * @return void */ public function filter_query( $lang ) { $qvars = &$this->query->query_vars; if ( ! $this->is_already_filtered( $qvars ) ) { $taxonomies = array_intersect( $this->model->get_translated_taxonomies(), get_taxonomies( array( '_builtin' => false ) ) ); foreach ( $taxonomies as $tax ) { $tax_object = get_taxonomy( $tax ); if ( ! empty( $tax_object ) && ! empty( $qvars[ $tax_object->query_var ] ) ) { return; } } if ( ! empty( $qvars['tax_query'] ) && $this->have_translated_taxonomy( $qvars['tax_query'] ) ) { return; } // Filter queries according to the requested language if ( ! empty( $lang ) ) { $taxonomies = $this->get_queried_taxonomies(); if ( $taxonomies && ( empty( $qvars['post_type'] ) || 'any' === $qvars['post_type'] ) ) { foreach ( $taxonomies as $taxonomy ) { $tax_object = get_taxonomy( $taxonomy ); if ( ! empty( $tax_object ) && $this->model->is_translated_post_type( $tax_object->object_type ) ) { $this->set_language( $lang ); break; } } } elseif ( empty( $qvars['post_type'] ) || $this->model->is_translated_post_type( $qvars['post_type'] ) ) { $this->set_language( $lang ); } } } else { $this->maybe_set_language_for_or_relation(); // Do not filter untranslatable post types such as nav_menu_item if ( isset( $qvars['post_type'] ) && ! $this->model->is_translated_post_type( $qvars['post_type'] ) && ( empty( $qvars['tax_query'] ) || ! $this->have_translated_taxonomy( $qvars['tax_query'] ) ) ) { unset( $qvars['lang'] ); } // Unset 'all' query var (mainly for admin language filter). if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) { unset( $qvars['lang'] ); } } } /** * Sets the language correctly if the current query is a 'OR' relation, * since WordPress merges the language with the other query vars when the relation is OR. * * @since 3.3 * * @return void */ protected function maybe_set_language_for_or_relation() { if ( ! $this->query->tax_query instanceof WP_Tax_Query ) { return; } if ( 'OR' !== $this->query->tax_query->relation ) { return; } if ( ! isset( $this->query->tax_query->queried_terms['language'] ) ) { return; } $langs = $this->query->tax_query->queried_terms['language']['terms']; if ( is_string( $langs ) ) { $langs = explode( ',', $langs ); } $langs = array_map( array( $this->model, 'get_language' ), $langs ); $langs = array_filter( $langs ); if ( ! empty( $langs ) ) { $this->set_language( $langs ); unset( $this->query->query_vars['lang'] ); // Unset the language query var otherwise WordPress would add the language query by slug in WP_Query::parse_tax_query(). } } } include/translated-post.php 0000644 00000027602 15136114124 0012031 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Sets the posts languages and translations model up. * * @since 1.8 * * @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface */ class PLL_Translated_Post extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface { use PLL_Translatable_Object_With_Types_Trait; /** * Taxonomy name for the languages. * * @var string * * @phpstan-var non-empty-string */ protected $tax_language = 'language'; /** * Identifier that must be unique for each type of content. * Also used when checking capabilities. * * @var string * * @phpstan-var non-empty-string */ protected $type = 'post'; /** * Identifier for each type of content to used for cache type. * * @var string * * @phpstan-var non-empty-string */ protected $cache_type = 'posts'; /** * Taxonomy name for the translation groups. * * @var string * * @phpstan-var non-empty-string */ protected $tax_translations = 'post_translations'; /** * Constructor. * * @since 1.8 * * @param PLL_Model $model Instance of `PLL_Model`. */ public function __construct( PLL_Model &$model ) { parent::__construct( $model ); // Keep hooks in constructor for backward compatibility. $this->init(); } /** * Adds hooks. * * @since 3.4 * * @return static */ public function init() { // Registers completely the language taxonomy. add_action( 'setup_theme', array( $this, 'register_taxonomy' ), 1 ); // Setups post types to translate. add_action( 'registered_post_type', array( $this, 'registered_post_type' ) ); // Forces updating posts cache. add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); return parent::init(); } /** * Deletes a translation of a post. * * @since 0.5 * * @param int $id Post ID. * @return void */ public function delete_translation( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return; } parent::delete_translation( $id ); wp_set_object_terms( $id, array(), $this->tax_translations ); } /** * Returns object types (post types) that need to be translated. * The post types list is cached for better performance. * The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php. * * @since 3.4 * * @param bool $filter True if we should return only valid registered object types. * @return string[] Object type names for which Polylang manages languages. * * @phpstan-return array<non-empty-string, non-empty-string> */ public function get_translated_object_types( $filter = true ) { $post_types = $this->model->cache->get( 'post_types' ); if ( false === $post_types ) { $post_types = array( 'post' => 'post', 'page' => 'page', 'wp_block' => 'wp_block' ); if ( ! empty( $this->model->options['post_types'] ) && is_array( $this->model->options['post_types'] ) ) { $post_types = array_merge( $post_types, array_combine( $this->model->options['post_types'], $this->model->options['post_types'] ) ); } if ( empty( $this->model->options['media_support'] ) ) { // In case the post type attachment is stored in the option. unset( $post_types['attachment'] ); } else { $post_types['attachment'] = 'attachment'; } /** * Filters the list of post types available for translation. * The default are post types which have the parameter ‘public’ set to true. * The filter must be added soon in the WordPress loading process: * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes. * * @since 0.8 * * @param string[] $post_types List of post type names (as array keys and values). * @param bool $is_settings True when displaying the list of custom post types in Polylang settings. */ $post_types = (array) apply_filters( 'pll_get_post_types', $post_types, false ); if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) { $this->model->cache->set( 'post_types', $post_types ); } } /** @var array<non-empty-string, non-empty-string> $post_types */ return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types; } /** * Returns true if Polylang manages languages for this object type. * * @since 3.4 * * @param string|string[] $object_type Object type (post type) name or array of object type names. * @return bool * * @phpstan-param non-empty-string|non-empty-string[] $object_type */ public function is_translated_object_type( $object_type ) { $post_types = $this->get_translated_object_types( false ); return ( is_array( $object_type ) && array_intersect( $object_type, $post_types ) || in_array( $object_type, $post_types ) || 'any' === $object_type && ! empty( $post_types ) ); } /** * Registers the language taxonomy. * * @since 1.2 * * @return void */ public function register_taxonomy() { register_taxonomy( $this->tax_language, $this->model->get_translated_post_types(), array( 'labels' => array( 'name' => __( 'Languages', 'polylang' ), 'singular_name' => __( 'Language', 'polylang' ), 'all_items' => __( 'All languages', 'polylang' ), ), 'public' => false, 'show_ui' => false, // Hide the taxonomy on admin side, needed for WP 4.4.x. 'show_in_nav_menus' => false, // No metabox for nav menus, needed for WP 4.4.x. 'publicly_queryable' => true, // Since WP 4.5. 'query_var' => 'lang', 'rewrite' => false, // Rewrite rules are added through filters when needed. '_pll' => true, // Polylang taxonomy. ) ); } /** * Checks if registered post type must be translated. * * @since 1.2 * * @param string $post_type Post type name. * @return void * * @phpstan-param non-empty-string $post_type */ public function registered_post_type( $post_type ) { if ( $this->model->is_translated_post_type( $post_type ) ) { register_taxonomy_for_object_type( $this->tax_language, $post_type ); register_taxonomy_for_object_type( $this->tax_translations, $post_type ); } } /** * Forces calling 'update_object_term_cache' when querying posts or pages. * This is especially useful for nav menus with a lot of pages as, without doing this, * we would have one query per page in the menu to get the page language for the permalink. * * @since 1.8 * * @param WP_Query $query Reference to the query object. * @return void */ public function pre_get_posts( $query ) { if ( ! empty( $query->query['post_type'] ) && $this->model->is_translated_post_type( $query->query['post_type'] ) ) { $query->query_vars['update_post_term_cache'] = true; } } /** * Checks if the current user can read the post. * * @since 1.5 * @since 3.4 Renamed the parameter $post_id into $id. * * @param int $id Post ID * @param string $context Optional, 'edit' or 'view'. Defaults to 'view'. * @return bool * * @phpstan-param non-empty-string $context */ public function current_user_can_read( $id, $context = 'view' ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return false; } $post = get_post( $id ); if ( empty( $post ) ) { return false; } if ( 'inherit' === $post->post_status && $post->post_parent ) { $post = get_post( $post->post_parent ); if ( empty( $post ) ) { return false; } } if ( 'inherit' === $post->post_status || in_array( $post->post_status, get_post_stati( array( 'public' => true ) ) ) ) { return true; } // Follow WP practices, which shows links to private posts ( when readable ), but not for draft posts ( ex: get_adjacent_post_link() ) if ( in_array( $post->post_status, get_post_stati( array( 'private' => true ) ) ) ) { if ( ! is_user_logged_in() ) { return false; } $user = wp_get_current_user(); if ( (int) $user->ID === (int) $post->post_author ) { return true; } $post_type_object = get_post_type_object( $post->post_type ); return ! empty( $post_type_object ) && current_user_can( $post_type_object->cap->read_private_posts ); } // In edit context, show draft and future posts. if ( 'edit' === $context ) { $states = get_post_stati( array( 'protected' => true, 'show_in_admin_all_list' => true, ) ); if ( in_array( $post->post_status, $states ) ) { $user = wp_get_current_user(); return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || (int) $user->ID === (int) $post->post_author ); } } return false; } /** * Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in). * * @since 2.6 * * @param string $type Post type. * @param PLL_Language $untranslated_in The language the posts must not be translated in. * @param PLL_Language $lang Language of the searched posts. * @param string $search Limit the results to the posts matching this string. * @return WP_Post[] Array of posts. */ public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) { global $wpdb; $args = array( 'numberposts' => 20 ); // Limit to 20 posts by default. /** * Filters the query args when auto suggesting untranslated posts in the Languages metabox. * * @since 1.7 * @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts. * No `WP_Query` is made anymore, a custom one is used instead. * * @param array $args WP_Query arguments */ $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args ); $limit = $args['numberposts']; $search_like = '%' . $wpdb->esc_like( $search ) . '%'; $untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%'; $posts = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id) AND tr1.term_taxonomy_id IN (%d) AND (({$wpdb->posts}.post_title LIKE %s) OR ({$wpdb->posts}.post_excerpt LIKE %s) OR ({$wpdb->posts}.post_content LIKE %s) ) AND {$wpdb->posts}.post_type = %s AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft') WHERE {$wpdb->posts}.ID NOT IN ( SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id) INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id) AND (tt.taxonomy = 'post_translations') AND tt.description LIKE %s ) LIMIT 0, %d", $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ), $search_like, $search_like, $search_like, $type, $untranslated_like, $limit ) ); foreach ( $posts as $i => $post ) { if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) { unset( $posts[ $i ] ); continue; } $posts[ $i ] = new WP_Post( $post ); } return $posts; } /** * Returns database-related information that can be used in some of this class methods. * These are specific to the table containing the objects. * * @see PLL_Translatable_Object::join_clause() * @see PLL_Translatable_Object::get_objects_with_no_lang_sql() * * @since 3.4.3 * * @return string[] { * @type string $table Name of the table. * @type string $id_column Name of the column containing the object's ID. * @type string $type_column Name of the column containing the object's type. * @type string $default_alias Default alias corresponding to the object's table. * } * @phpstan-return DBInfoWithType */ protected function get_db_infos() { return array( 'table' => $GLOBALS['wpdb']->posts, 'id_column' => 'ID', 'type_column' => 'post_type', 'default_alias' => $GLOBALS['wpdb']->posts, ); } } include/links.php 0000644 00000002463 15136114124 0010023 0 ustar 00 <?php /** * @package Polylang */ /** * Manages links related functions * * @since 1.2 */ class PLL_Links { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Instance of a child class of PLL_Links_Model. * * @var PLL_Links_Model */ public $links_model; /** * Current language (used to filter the content). * * @var PLL_Language|null */ public $curlang; /** * Constructor * * @since 1.2 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->links_model = &$polylang->links_model; $this->model = &$polylang->model; $this->options = &$polylang->options; } /** * Returns the home url in the requested language. * * @since 1.3 * * @param PLL_Language|string $language The language. * @param bool $is_search Optional, whether we need the home url for a search form, defaults to false. * @return string */ public function get_home_url( $language, $is_search = false ) { if ( ! $language instanceof PLL_Language ) { $language = $this->model->get_language( $language ); } if ( empty( $language ) ) { return home_url( '/' ); } return $is_search ? $language->get_search_url() : $language->get_home_url(); } } include/filters-sanitization.php 0000644 00000006357 15136114124 0013073 0 ustar 00 <?php /** * @package Polylang */ /** * Setup specific filters useful for sanitization. * * Extract from PLL_Admin_Filters to be able to use in a REST API context. * * @since 2.9 */ class PLL_Filters_Sanitization { /** * Language used for the sanitization depending on the context. * * @var string $locale */ public $locale; /** * Constructor: setups filters and actions. * * @since 2.9 * * @param string $locale Locale code of the language. */ public function __construct( $locale ) { $this->locale = $locale; // We need specific filters for some languages like German and Danish $specific_locales = array( 'da_DK', 'de_DE', 'de_DE_formal', 'de_CH', 'de_CH_informal', 'ca', 'sr_RS', 'bs_BA' ); if ( in_array( $locale, $specific_locales ) ) { add_filter( 'sanitize_title', array( $this, 'sanitize_title' ), 10, 3 ); add_filter( 'sanitize_user', array( $this, 'sanitize_user' ), 10, 3 ); } } /** * Retrieve the locale code of the language. * * @since 2.0 * * @return string */ public function get_locale() { return $this->locale; } /** * Maybe fix the result of sanitize_title() in case the languages include German or Danish * Without this filter, if language of the title being sanitized is different from the language * used for the admin interface and if one this language is German or Danish, some specific * characters such as ä, ö, ü, ß are incorrectly sanitized. * * All the process is done by the remove_accents() WordPress function based on the locale value * * @link https://github.com/WordPress/WordPress/blob/5.5.1/wp-includes/formatting.php#L1920-L1944 * * @since 2.0 * * @param string $title Sanitized title. * @param string $raw_title The title prior to sanitization. * @param string $context The context for which the title is being sanitized. * @return string */ public function sanitize_title( $title, $raw_title, $context ) { static $once = false; if ( ! $once && 'save' == $context && ! empty( $title ) ) { $once = true; add_filter( 'locale', array( $this, 'get_locale' ), 20 ); // After the filter for the admin interface $title = sanitize_title( $raw_title, '', $context ); remove_filter( 'locale', array( $this, 'get_locale' ), 20 ); $once = false; } return $title; } /** * Maybe fix the result of sanitize_user() in case the languages include German or Danish * * All the process is done by the remove_accents() WordPress function based on the locale value * * @link https://github.com/WordPress/WordPress/blob/5.5-branch/wp-includes/formatting.php#L1920-L1944 * * @since 2.0 * * @param string $username Sanitized username. * @param string $raw_username The username prior to sanitization. * @param bool $strict Whether to limit the sanitization to specific characters. Default false. * @return string */ public function sanitize_user( $username, $raw_username, $strict ) { static $once = false; if ( ! $once ) { $once = true; add_filter( 'locale', array( $this, 'get_locale' ), 20 ); // After the filter for the admin interface $username = sanitize_user( $raw_username, $strict ); remove_filter( 'locale', array( $this, 'get_locale' ), 20 ); $once = false; } return $username; } } include/rest-request.php 0000644 00000006172 15136114124 0011347 0 ustar 00 <?php /** * @package Polylang */ /** * Main Polylang class for REST API requests, accessible from @see PLL(). * * @since 2.6 */ class PLL_REST_Request extends PLL_Base { /** * @var PLL_Language|false|null A `PLL_Language` when defined, `false` otherwise. `null` until the language * definition process runs. */ public $curlang; /** * @var PLL_Filters|null */ public $filters; /** * @var PLL_Filters_Links|null */ public $filters_links; /** * @var PLL_Admin_Links|null */ public $links; /** * @var PLL_Nav_Menu|null */ public $nav_menu; /** * @var PLL_Static_Pages|null */ public $static_pages; /** * @var PLL_Filters_Widgets_Options|null */ public $filters_widgets_options; /** * Constructor. * * @since 3.4 * * @param PLL_Links_Model $links_model Reference to the links model. */ public function __construct( &$links_model ) { parent::__construct( $links_model ); // Static front page and page for posts. // Early instantiated to be able to correctly initialize language properties. if ( 'page' === get_option( 'show_on_front' ) ) { $this->static_pages = new PLL_Static_Pages( $this ); } $this->model->set_languages_ready(); } /** * Setup filters. * * @since 2.6 * * @return void */ public function init() { parent::init(); if ( ! $this->model->has_languages() ) { return; } add_filter( 'rest_pre_dispatch', array( $this, 'set_language' ), 10, 3 ); $this->filters_links = new PLL_Filters_Links( $this ); $this->filters = new PLL_Filters( $this ); $this->filters_widgets_options = new PLL_Filters_Widgets_Options( $this ); $this->links = new PLL_Admin_Links( $this ); $this->nav_menu = new PLL_Frontend_Nav_Menu( $this ); // For auto added pages to menu. } /** * Sets the current language during a REST request if sent. * * @since 3.3 * * @param mixed $result Response to replace the requested version with. Remains untouched. * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. * @return mixed Untouched $result. * * @phpstan-param WP_REST_Request<array{lang?: string}> $request */ public function set_language( $result, $server, $request ) { $lang = $request->get_param( 'lang' ); if ( ! empty( $lang ) && is_string( $lang ) ) { $this->curlang = $this->model->get_language( sanitize_key( $lang ) ); if ( empty( $this->curlang ) && ! empty( $this->options['default_lang'] ) && is_string( $this->options['default_lang'] ) ) { // A lang has been requested but it is invalid, let's fall back to the default one. $this->curlang = $this->model->get_language( sanitize_key( $this->options['default_lang'] ) ); } } if ( ! empty( $this->curlang ) ) { /** This action is documented in frontend/choose-lang.php */ do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang ); } else { /** This action is documented in include/class-polylang.php */ do_action( 'pll_no_language_defined' ); // To load overridden textdomains. } return $result; } } include/crud-terms.php 0000644 00000023571 15136114124 0010773 0 ustar 00 <?php /** * @package Polylang */ /** * Adds actions and filters related to languages when creating, reading, updating or deleting posts * Acts both on frontend and backend * * @since 2.4 */ class PLL_CRUD_Terms { /** * @var PLL_Model */ public $model; /** * Current language (used to filter the content). * * @var PLL_Language|null */ public $curlang; /** * Language selected in the admin language filter. * * @var PLL_Language|null */ public $filter_lang; /** * Preferred language to assign to new contents. * * @var PLL_Language|null */ public $pref_lang; /** * Stores the 'lang' query var from WP_Query. * * @var string|null */ private $tax_query_lang; /** * Stores the term name before creating a slug if needed. * * @var string */ private $pre_term_name = ''; /** * Reference to the Polylang options array. * * @var array */ protected $options; /** * Constructor * * @since 2.4 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->options = &$polylang->options; $this->model = &$polylang->model; $this->curlang = &$polylang->curlang; $this->filter_lang = &$polylang->filter_lang; $this->pref_lang = &$polylang->pref_lang; // Saving terms add_action( 'create_term', array( $this, 'save_term' ), 999, 3 ); add_action( 'edit_term', array( $this, 'save_term' ), 999, 3 ); // After PLL_Admin_Filters_Term add_filter( 'pre_term_name', array( $this, 'set_pre_term_name' ) ); add_filter( 'pre_term_slug', array( $this, 'set_pre_term_slug' ), 10, 2 ); // Adds cache domain when querying terms add_filter( 'get_terms_args', array( $this, 'get_terms_args' ), 10, 2 ); // Filters terms by language add_filter( 'terms_clauses', array( $this, 'terms_clauses' ), 10, 3 ); add_action( 'pre_get_posts', array( $this, 'set_tax_query_lang' ), 999 ); add_action( 'posts_selection', array( $this, 'unset_tax_query_lang' ), 0 ); // Deleting terms add_action( 'pre_delete_term', array( $this, 'delete_term' ), 10, 2 ); } /** * Allows to set a language by default for terms if it has no language yet. * * @since 1.5.4 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @return void */ protected function set_default_language( $term_id, $taxonomy ) { if ( ! $this->model->term->get_language( $term_id ) ) { if ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification // Testing $this->pref_lang makes this test pass only on frontend. $this->model->term->set_language( $term_id, $lang ); } elseif ( ( $term = get_term( $term_id, $taxonomy ) ) && ! empty( $term->parent ) && $parent_lang = $this->model->term->get_language( $term->parent ) ) { // Sets language from term parent if exists thanks to Scott Kingsley Clark $this->model->term->set_language( $term_id, $parent_lang ); } elseif ( isset( $this->pref_lang ) ) { // Always defined on admin, never defined on frontend $this->model->term->set_language( $term_id, $this->pref_lang ); } elseif ( ! empty( $this->curlang ) ) { // Only on frontend due to the previous test always true on admin $this->model->term->set_language( $term_id, $this->curlang ); } else { // In all other cases set to default language. $this->model->term->set_language( $term_id, $this->options['default_lang'] ); } } } /** * Called when a category or post tag is created or edited. * Does nothing except on taxonomies which are filterable. * * @since 0.1 * * @param int $term_id Term id of the term being saved. * @param int $tt_id Term taxonomy id. * @param string $taxonomy Taxonomy name. * @return void */ public function save_term( $term_id, $tt_id, $taxonomy ) { if ( $this->model->is_translated_taxonomy( $taxonomy ) ) { $lang = $this->model->term->get_language( $term_id ); if ( empty( $lang ) ) { $this->set_default_language( $term_id, $taxonomy ); } /** * Fires after the term language and translations are saved. * * @since 1.2 * * @param int $term_id Term id. * @param string $taxonomy Taxonomy name. * @param int[] $translations The list of translations term ids. */ do_action( 'pll_save_term', $term_id, $taxonomy, $this->model->term->get_translations( $term_id ) ); } } /** * Get the language(s) to filter WP_Term_Query. * * @since 1.7.6 * * @param string[] $taxonomies Queried taxonomies. * @param array $args WP_Term_Query arguments. * @return PLL_Language|string|false The language(s) to use in the filter, false otherwise. */ protected function get_queried_language( $taxonomies, $args ) { global $pagenow; // Does nothing except on taxonomies which are filterable // Since WP 4.7, make sure not to filter wp_get_object_terms() if ( ! $this->model->is_translated_taxonomy( $taxonomies ) || ! empty( $args['object_ids'] ) ) { return false; } // If get_terms is queried with a 'lang' parameter if ( isset( $args['lang'] ) ) { return $args['lang']; } // On tags page, everything should be filtered according to the admin language filter except the parent dropdown if ( 'edit-tags.php' === $pagenow && empty( $args['class'] ) ) { return $this->filter_lang; } return $this->curlang; } /** * Adds language dependent cache domain when querying terms. * Useful as the 'lang' parameter is not included in cache key by WordPress. * * @since 1.3 * * @param array $args WP_Term_Query arguments. * @param string[] $taxonomies Queried taxonomies. * @return array Modified arguments. */ public function get_terms_args( $args, $taxonomies ) { // Don't break _get_term_hierarchy(). if ( 'all' === $args['get'] && 'id' === $args['orderby'] && 'id=>parent' === $args['fields'] ) { $args['lang'] = ''; } if ( isset( $this->tax_query_lang ) ) { $args['lang'] = empty( $this->tax_query_lang ) && ! empty( $this->curlang ) && ! empty( $args['slug'] ) ? $this->curlang->slug : $this->tax_query_lang; } if ( $lang = $this->get_queried_language( $taxonomies, $args ) ) { $lang = is_string( $lang ) && strpos( $lang, ',' ) ? explode( ',', $lang ) : $lang; $key = '_' . ( is_array( $lang ) ? implode( ',', $lang ) : $this->model->get_language( $lang )->slug ); $args['cache_domain'] = empty( $args['cache_domain'] ) ? 'pll' . $key : $args['cache_domain'] . $key; } return $args; } /** * Filters categories and post tags by language(s) when needed on admin side * * @since 0.2 * * @param string[] $clauses List of sql clauses. * @param string[] $taxonomies List of taxonomies. * @param array $args WP_Term_Query arguments. * @return string[] Modified sql clauses. */ public function terms_clauses( $clauses, $taxonomies, $args ) { $lang = $this->get_queried_language( $taxonomies, $args ); return $this->model->terms_clauses( $clauses, $lang ); } /** * Sets the WP_Term_Query language when doing a WP_Query. * Needed since WP 4.9. * * @since 2.3.2 * * @param WP_Query $query WP_Query object. * @return void */ public function set_tax_query_lang( $query ) { $this->tax_query_lang = isset( $query->query_vars['lang'] ) ? $query->query_vars['lang'] : ''; } /** * Removes the WP_Term_Query language filter for WP_Query. * Needed since WP 4.9. * * @since 2.3.2 * * @return void */ public function unset_tax_query_lang() { unset( $this->tax_query_lang ); } /** * Called when a category or post tag is deleted * Deletes language and translations * * @since 0.1 * * @param int $term_id Id of the term to delete. * @param string $taxonomy Name of the taxonomy. * @return void */ public function delete_term( $term_id, $taxonomy ) { if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) { return; } // Delete translation and relationships only if the term is translatable. $this->model->term->delete_translation( $term_id ); $this->model->term->delete_language( $term_id ); } /** * Stores the term name for use in pre_term_slug * * @since 0.9.5 * * @param string $name term name * @return string unmodified term name */ public function set_pre_term_name( $name ) { return $this->pre_term_name = $name; } /** * Appends language slug to the term slug if needed. * * @since 3.3 * * @param string $slug Term slug. * @param string $taxonomy Term taxonomy. * @return string Slug with a language suffix if found. */ public function set_pre_term_slug( $slug, $taxonomy ) { if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) { return $slug; } if ( ! $slug ) { $slug = sanitize_title( $this->pre_term_name ); } if ( ! term_exists( $slug, $taxonomy ) ) { return $slug; } /** * Filters the subsequently inserted term language. * * @since 3.3 * * @param PLL_Language|null $lang Found language object, null otherwise. * @param string $taxonomy Term taonomy. * @param string $slug Term slug */ $lang = apply_filters( 'pll_inserted_term_language', null, $taxonomy, $slug ); if ( ! $lang instanceof PLL_Language ) { return $slug; } $parent = 0; if ( is_taxonomy_hierarchical( $taxonomy ) ) { /** * Filters the subsequently inserted term parent. * * @since 3.3 * * @param int $parent Parent term ID, 0 if none. * @param string $taxonomy Term taxonomy. * @param string $slug Term slug */ $parent = apply_filters( 'pll_inserted_term_parent', 0, $taxonomy, $slug ); } $term_id = (int) $this->model->term_exists_by_slug( $slug, $lang, $taxonomy, $parent ); // If no term exist in the given language with that slug, it can be created. if ( ! $term_id ) { $slug .= '-' . $lang->slug; } return $slug; } } include/translatable-object-with-types-trait.php 0000644 00000003620 15136114124 0016053 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Trait to use for objects that can have one or more types. * This must be used with {@see PLL_Translatable_Object_With_Types_Interface}. * * @since 3.4 */ trait PLL_Translatable_Object_With_Types_Trait { /** * Returns SQL query that fetches the IDs of the objects without language. * * @since 3.4 * * @param int[] $language_ids List of language `term_taxonomy_id`. * @param int $limit Max number of objects to return. `-1` to return all of them. * @param array $args An array of translated object types. * @return string * * @phpstan-param array<positive-int> $language_ids * @phpstan-param -1|positive-int $limit * @phpstan-param array<string> $args */ protected function get_objects_with_no_lang_sql( array $language_ids, $limit, array $args = array() ) { if ( empty( $args ) ) { $args = $this->get_translated_object_types(); } $db = $this->get_db_infos(); return sprintf( "SELECT {$db['table']}.{$db['id_column']} FROM {$db['table']} WHERE {$db['table']}.{$db['id_column']} NOT IN ( SELECT object_id FROM {$GLOBALS['wpdb']->term_relationships} WHERE term_taxonomy_id IN (%s) ) AND {$db['type_column']} IN (%s) %s", PLL_Db_Tools::prepare_values_list( $language_ids ), PLL_Db_Tools::prepare_values_list( $args ), $limit >= 1 ? sprintf( 'LIMIT %d', $limit ) : '' ); } /** * Returns true if Polylang manages languages for this object type. * * @since 3.4 * * @param string|string[] $object_type Object type (taxonomy name) name or array of object type names. * @return bool * * @phpstan-param non-empty-string|non-empty-string[] $object_type */ public function is_translated_object_type( $object_type ) { $object_types = $this->get_translated_object_types( false ); return ! empty( array_intersect( (array) $object_type, $object_types ) ); } } include/translate-option.php 0000644 00000030727 15136114124 0012212 0 ustar 00 <?php /** * @package Polylang */ /** * Registers and translates strings in an option. * When a string is updated in an original option, the translations of the old string are assigned to the new original string. * * @since 2.9 */ class PLL_Translate_Option { /** * Array of option keys to translate. * * @var array */ private $keys; /** * Used to prevent filtering when retrieving the raw value of the option. * * @var bool */ private static $raw = false; /** * Array of updated strings. * * @var array */ private $updated_strings = array(); /** * @var PLL_MO[] */ private $translations; /** * Cache for the translated values. * * @var PLL_Cache<array|string> */ private $cache; /** * Constructor * * @since 2.9 * * @param string $name Option name. * @param array $keys Recursive array of option keys to translate in the form: * @example array( * 'option_key_to_translate_1' => 1, * 'option_key_to_translate_2' => 1, * 'my_group' => array( * 'sub_key_to_translate_1' => 1, * 'sub_key_to_translate_2' => 1, * ), * ) * * Note: only keys are interpreted. Any scalar can be used as values. * @param array $args { * Optional. Array of arguments for registering the option. * * @type string $context The group in which the strings will be registered. * @type string $sanitize_callback A callback function that sanitizes the option's value. * } */ public function __construct( $name, $keys = array(), $args = array() ) { $this->cache = new PLL_Cache(); // Registers the strings. $context = isset( $args['context'] ) ? $args['context'] : 'Polylang'; $this->register_string_recursive( $context, $name, get_option( $name ), $keys ); // Translates the strings. $this->keys = $keys; add_filter( 'option_' . $name, array( $this, 'translate' ) ); // Make sure to add this filter after options are registered. // Filters updated values. add_filter( 'pre_update_option_' . $name, array( $this, 'pre_update_option' ), 10, 3 ); add_action( 'update_option_' . $name, array( $this, 'update_option' ) ); // Sanitizes translated strings. if ( empty( $args['sanitize_callback'] ) ) { add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_option' ), 10, 2 ); } else { add_filter( 'pll_sanitize_string_translation', $args['sanitize_callback'], 10, 3 ); } } /** * Translates the strings registered for an option. * * @since 1.0 * * @param mixed $value Either a string to translate or a list of strings to translate. * @return mixed Translated string(s). */ public function translate( $value ) { if ( self::$raw ) { return $value; } if ( empty( $GLOBALS['l10n']['pll_string'] ) || ! $GLOBALS['l10n']['pll_string'] instanceof PLL_MO ) { return $value; } $lang = $GLOBALS['l10n']['pll_string']->get_header( 'Language' ); if ( ! is_string( $lang ) || '' === $lang ) { return $value; } $cache = $this->cache->get( $lang ); if ( false === $cache ) { $cache = $this->translate_string_recursive( $value, $this->keys ); $this->cache->set( $lang, $cache ); } return $cache; } /** * Recursively translates the strings registered for an option. * * @since 1.0 * * @param mixed $values Either a string to translate or a list of strings to translate. * @param array|bool $key Array of option keys to translate. * @return array|string Translated string(s) */ protected function translate_string_recursive( $values, $key ) { $children = is_array( $key ) ? $key : array(); if ( is_array( $values ) || is_object( $values ) ) { if ( count( $children ) ) { foreach ( $children as $name => $child ) { if ( is_array( $values ) && isset( $values[ $name ] ) ) { $values[ $name ] = $this->translate_string_recursive( $values[ $name ], $child ); continue; } if ( is_object( $values ) && isset( $values->$name ) ) { $values->$name = $this->translate_string_recursive( $values->$name, $child ); continue; } $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#'; foreach ( $values as $n => &$value ) { // The first case could be handled by the next one, but we avoid calls to preg_match here. if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) { $value = $this->translate_string_recursive( $value, $child ); } } } } else { // Parent key is a wildcard and no sub-key has been whitelisted. foreach ( $values as &$value ) { $value = $this->translate_string_recursive( $value, $key ); } } } else { $values = pll__( $values ); } return $values; } /** * Recursively registers strings for an option. * * @since 1.0 * @since 2.7 Signature modified * * @param string $context The group in which the strings will be registered. * @param string $option Option name. * @param mixed $values Option value. * @param array|bool $key Array of option keys to translate. * @return void */ protected function register_string_recursive( $context, $option, $values, $key ) { if ( is_object( $values ) ) { $values = (array) $values; } if ( is_array( $values ) ) { $children = is_array( $key ) ? $key : array(); if ( count( $children ) ) { foreach ( $children as $name => $child ) { if ( isset( $values[ $name ] ) ) { $this->register_string_recursive( $context, $name, $values[ $name ], $child ); continue; } $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#'; foreach ( $values as $n => $value ) { // The first case could be handled by the next one, but we avoid calls to preg_match here. if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) { $this->register_string_recursive( $context, $n, $value, $child ); } } } } else { foreach ( $values as $n => $value ) { // Parent key is a wildcard and no sub-key has been whitelisted. $this->register_string_recursive( $context, $n, $value, $key ); } } } else { PLL_Admin_Strings::register_string( $option, $values, $context, true ); } } /** * Returns the raw value of an option (without this class' filter). * * A static property is used to make sure that the option is not filtered * whatever the number of instances of this class filtering the option. * * @since 3.3 * * @param string $option_name Option name. * @return mixed */ protected function get_raw_option( $option_name ) { self::$raw = true; $option_value = get_option( $option_name ); self::$raw = false; return $option_value; } /** * Filters an option before it is updated. * * This is the step 1 in the update process, in which we prevent the update of * strings to their translations by filtering them out, and we store the updated strings * for the next step. * * @since 2.9 * * @param mixed $value The new, unserialized option value. * @param mixed $old_value The old (filtered) option value. * @param string $name Option name. * @return mixed */ public function pre_update_option( $value, $old_value, $name ) { // Stores the unfiltered old option value before it is updated in DB. $unfiltered_old_value = $this->get_raw_option( $name ); $languages = PLL()->model->get_languages_list(); if ( empty( $languages ) ) { return $value; } // Load translations in all languages. foreach ( $languages as $language ) { $this->translations[ $language->slug ] = new PLL_MO(); $this->translations[ $language->slug ]->import_from_db( $language ); } $lang = pll_current_language(); if ( empty( $lang ) ) { $lang = pll_default_language(); } if ( empty( $lang ) ) { return $value; // Something's wrong. } // Filters out the strings which would be updated to their translations and stores the updated strings. $value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys, $this->translations[ $lang ] ); return $value; } /** * Updates the string translations to keep the same translated value when updating the original option. * * This is the step 2 in the update process. Knowing all strings that have been updated, * we remove the old strings from the strings translations and replace them by * the new strings with the old translations. * * @since 2.9 * * @return void */ public function update_option() { $curlang = pll_current_language(); if ( ! empty( $this->updated_strings ) ) { foreach ( PLL()->model->get_languages_list() as $language ) { $mo = &$this->translations[ $language->slug ]; foreach ( $this->updated_strings as $old_string => $string ) { $translation = $mo->translate( $old_string ); if ( ( empty( $curlang ) && $translation === $old_string ) || $language->slug === $curlang ) { $translation = $string; } // Add new entry with new string and old translation. $mo->add_entry( $mo->make_entry( $string, $translation ) ); } $mo->export_to_db( $language ); } } $this->cache->clean(); } /** * Recursively compares the updated strings to the translation of the old string. * * This is the heart of the update process. If an updated string is found to be * the same as the translation of the old string, we restore the old string to * prevent the update in {@see PLL_Translate_Option::pre_update_option()}, otherwise * the updated string is stored in {@see PLL_Translate_Option::updated_strings} to be able to * later assign the translations to the new value in {@see PLL_Translate_Option::update_option()}. * * @since 2.9 * @since 3.5 Added $mo parameter. * * @param mixed $old_values The old option value. * @param mixed $values The new option value. * @param array|bool $key Array of option keys to translate. * @param PLL_MO $mo Translations used to compare the updated string to the translated old string. * @return mixed */ protected function check_value_recursive( $old_values, $values, $key, $mo ) { $children = is_array( $key ) ? $key : array(); if ( is_array( $values ) || is_object( $values ) ) { if ( count( $children ) ) { foreach ( $children as $name => $child ) { if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $name ], $values[ $name ] ) ) { $values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child, $mo ); continue; } if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$name, $values->$name ) ) { $values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child, $mo ); continue; } $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#'; foreach ( $values as $n => $value ) { // The first case could be handled by the next one, but we avoid calls to preg_match here. if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) { if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) { $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child, $mo ); } if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) { $values->$n = $this->check_value_recursive( $old_values->$n, $value, $child, $mo ); } } } } } else { // Parent key is a wildcard and no sub-key has been whitelisted. foreach ( $values as $n => $value ) { if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) { $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key, $mo ); } if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) { $values->$n = $this->check_value_recursive( $old_values->$n, $value, $key, $mo ); } } } } elseif ( $old_values !== $values ) { if ( $mo->translate( $old_values ) === $values ) { $values = $old_values; // Prevents updating the value to its translation. } else { $this->updated_strings[ $old_values ] = $values; // Stores the updated strings. } } return $values; } /** * Sanitizes the option value. * * @since 2.9 * * @param string $value The unsanitised value. * @param string $name The name of the option. * @return string Sanitized value. */ public function sanitize_option( $value, $name ) { return sanitize_option( $name, $value ); } } include/links-domain.php 0000644 00000006100 15136114124 0011260 0 ustar 00 <?php /** * @package Polylang */ /** * Links model for use when using one domain per language * for example mysite.com/something and mysite.fr/quelquechose. * * @since 1.2 */ class PLL_Links_Domain extends PLL_Links_Abstract_Domain { /** * An array with language code as keys and the host as values. * * @var string[] */ protected $hosts; /** * Constructor. * * @since 1.8 * * @param object $model PLL_Model instance. */ public function __construct( &$model ) { parent::__construct( $model ); $this->hosts = $this->get_hosts(); // Filters the site url (mainly to get the correct login form). add_filter( 'site_url', array( $this, 'site_url' ) ); } /** * Switches the primary domain to a secondary domain in the url. * * @since 1.2 * @since 3.4 Accepts now a language slug. * * @param string $url The url to modify. * @param PLL_Language|string|false $language Language object or slug. * @return string The modified url. */ public function add_language_to_link( $url, $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } if ( ! empty( $language ) && ! empty( $this->hosts[ $language ] ) ) { $url = preg_replace( '#://(' . wp_parse_url( $this->home, PHP_URL_HOST ) . ')($|/.*)#', '://' . $this->hosts[ $language ] . '$2', $url ); } return $url; } /** * Returns the url with the primary domain. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_language_from_link( $url ) { if ( ! empty( $this->hosts ) ) { $url = preg_replace( '#://(' . implode( '|', $this->hosts ) . ')($|/.*)#', '://' . wp_parse_url( $this->home, PHP_URL_HOST ) . '$2', $url ); } return $url; } /** * Returns the home url in a given language. * * @since 1.3.1 * @since 3.4 Accepts now a language slug. * * @param PLL_Language|string $language Language object or slug. * @return string */ public function home_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } return trailingslashit( empty( $this->options['domains'][ $language ] ) ? $this->home : $this->options['domains'][ $language ] ); } /** * Get the hosts managed on the website. * * @since 1.5 * * @return string[] List of hosts. */ public function get_hosts() { $hosts = array(); foreach ( $this->options['domains'] as $lang => $domain ) { $host = wp_parse_url( $domain, PHP_URL_HOST ); if ( ! is_string( $host ) ) { continue; } // The function idn_to_ascii() is much faster than the WordPress method. if ( function_exists( 'idn_to_ascii' ) && defined( 'INTL_IDNA_VARIANT_UTS46' ) ) { $hosts[ $lang ] = idn_to_ascii( $host, 0, INTL_IDNA_VARIANT_UTS46 ); } elseif ( class_exists( 'WpOrg\Requests\IdnaEncoder' ) ) { // Since WP 6.2. $hosts[ $lang ] = \WpOrg\Requests\IdnaEncoder::encode( $host ); } else { // Backward compatibility with WP < 6.2. $hosts[ $lang ] = Requests_IDNAEncoder::encode( $host ); } } return $hosts; } } include/language-deprecated.php 0000644 00000016273 15136114124 0012570 0 ustar 00 <?php /** * @package Polylang */ /** * Holds everything related to deprecated properties of `PLL_Language`. * * @since 3.4 */ abstract class PLL_Language_Deprecated { /** * List of deprecated term properties and related arguments to use with `get_tax_prop()`. * * @private * * @var string[][] */ const DEPRECATED_TERM_PROPERTIES = array( 'term_taxonomy_id' => array( 'language', 'term_taxonomy_id' ), 'count' => array( 'language', 'count' ), 'tl_term_id' => array( 'term_language', 'term_id' ), 'tl_term_taxonomy_id' => array( 'term_language', 'term_taxonomy_id' ), 'tl_count' => array( 'term_language', 'count' ), ); /** * List of deprecated URL properties and related getter to use. * * @private * * @var string[] */ const DEPRECATED_URL_PROPERTIES = array( 'home_url' => 'get_home_url', 'search_url' => 'get_search_url', ); /** * Returns a language term property value (term ID, term taxonomy ID, or count). * * @since 3.4 * * @param string $taxonomy_name Name of the taxonomy. * @param string $prop_name Name of the property: 'term_taxonomy_id', 'term_id', 'count'. * @return int * * @phpstan-param non-empty-string $taxonomy_name * @phpstan-param 'term_taxonomy_id'|'term_id'|'count' $prop_name * @phpstan-return int<0, max> */ abstract public function get_tax_prop( $taxonomy_name, $prop_name ); /** * Returns language's home URL. Takes care to render it dynamically if no cache is allowed. * * @since 3.4 * * @return string Language home URL. * * @phpstan-return non-empty-string */ abstract public function get_home_url(); /** * Returns language's search URL. Takes care to render it dynamically if no cache is allowed. * * @since 3.4 * * @return string Language search URL. * * @phpstan-return non-empty-string */ abstract public function get_search_url(); /** * Throws a depreciation notice if someone tries to get one of the following properties: * `term_taxonomy_id`, `count`, `tl_term_id`, `tl_term_taxonomy_id` or `tl_count`. * * Backward compatibility with Polylang < 3.4. * * @since 3.4 * * @param string $property Property to get. * @return mixed Required property value. */ public function __get( $property ) { // Deprecated property. if ( $this->is_deprecated_term_property( $property ) ) { $this->deprecated_property( $property, sprintf( "get_tax_prop( '%s', '%s' )", self::DEPRECATED_TERM_PROPERTIES[ $property ][0], self::DEPRECATED_TERM_PROPERTIES[ $property ][1] ) ); return $this->get_deprecated_term_property( $property ); } if ( $this->is_deprecated_url_property( $property ) ) { $this->deprecated_property( $property, "get_{$property}()" ); return $this->get_deprecated_url_property( $property ); } // Undefined property. if ( ! property_exists( $this, $property ) ) { return null; } // The property is defined. $ref = new ReflectionProperty( $this, $property ); // Public property. if ( $ref->isPublic() ) { return $this->{$property}; } // Protected or private property. $visibility = $ref->isPrivate() ? 'private' : 'protected'; $trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection, WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $file = isset( $trace[0]['file'] ) ? $trace[0]['file'] : ''; $line = isset( $trace[0]['line'] ) ? $trace[0]['line'] : 0; trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error esc_html( sprintf( "Cannot access %s property %s::$%s in %s on line %d.\nError handler", $visibility, get_class( $this ), $property, $file, $line ) ), E_USER_ERROR ); } /** * Checks for a deprecated property. * Is triggered by calling `isset()` or `empty()` on inaccessible (protected or private) or non-existing properties. * * Backward compatibility with Polylang < 3.4. * * @since 3.4 * * @param string $property A property name. * @return bool */ public function __isset( $property ) { return $this->is_deprecated_term_property( $property ) || $this->is_deprecated_url_property( $property ); } /** * Tells if the given term property is deprecated. * * @since 3.4 * @see PLL_Language::DEPRECATED_TERM_PROPERTIES for the list of deprecated properties. * * @param string $property A property name. * @return bool * * @phpstan-assert-if-true key-of<PLL_Language::DEPRECATED_TERM_PROPERTIES> $property */ protected function is_deprecated_term_property( $property ) { return array_key_exists( $property, self::DEPRECATED_TERM_PROPERTIES ); } /** * Returns a deprecated term property's value. * * @since 3.4 * @see PLL_Language::DEPRECATED_TERM_PROPERTIES for the list of deprecated properties. * * @param string $property A property name. * @return int * * @phpstan-param key-of<PLL_Language::DEPRECATED_TERM_PROPERTIES> $property * @phpstan-return int<0, max> */ protected function get_deprecated_term_property( $property ) { return $this->get_tax_prop( self::DEPRECATED_TERM_PROPERTIES[ $property ][0], self::DEPRECATED_TERM_PROPERTIES[ $property ][1] ); } /** * Tells if the given URL property is deprecated. * * @since 3.4 * @see PLL_Language::DEPRECATED_URL_PROPERTIES for the list of deprecated properties. * * @param string $property A property name. * @return bool * * @phpstan-assert-if-true key-of<PLL_Language::DEPRECATED_URL_PROPERTIES> $property */ protected function is_deprecated_url_property( $property ) { return array_key_exists( $property, self::DEPRECATED_URL_PROPERTIES ); } /** * Returns a deprecated URL property's value. * * @since 3.4 * @see PLL_Language::DEPRECATED_URL_PROPERTIES for the list of deprecated properties. * * @param string $property A property name. * @return string * * @phpstan-param key-of<PLL_Language::DEPRECATED_URL_PROPERTIES> $property * @phpstan-return non-empty-string */ protected function get_deprecated_url_property( $property ) { return $this->{self::DEPRECATED_URL_PROPERTIES[ $property ]}(); } /** * Triggers a deprecated an error for a deprecated property. * * @since 3.4 * * @param string $property Deprecated property name. * @param string $replacement Method or property name to use instead. * @return void */ private function deprecated_property( $property, $replacement ) { /** * Filters whether to trigger an error for deprecated properties. * * The filter name is intentionally not prefixed to use the same as WordPress * in case it is added in the future. * * @since 3.4 * * @param bool $trigger Whether to trigger the error for deprecated properties. Default true. */ if ( ! WP_DEBUG || ! apply_filters( 'deprecated_property_trigger_error', true ) ) { return; } trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error sprintf( "Class property %1\$s::\$%2\$s is deprecated, use %1\$s::%3\$s instead.\nError handler", esc_html( get_class( $this ) ), esc_html( $property ), esc_html( $replacement ) ), E_USER_DEPRECATED ); } } include/walker-list.php 0000644 00000004404 15136114124 0011136 0 ustar 00 <?php /** * @package Polylang */ /** * Displays a language list * * @since 1.2 * @since 3.4 Extends `PLL_Walker` now. */ class PLL_Walker_List extends PLL_Walker { /** * Database fields to use. * * @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields. * * @var string[] */ public $db_fields = array( 'parent' => 'parent', 'id' => 'id' ); /** * Outputs one element * * @since 1.2 * * @param string $output Passed by reference. Used to append additional content. * @param stdClass $element The data object. * @param int $depth Depth of the item. * @param array $args An array of additional arguments. * @param int $current_object_id ID of the current item. * @return void */ public function start_el( &$output, $element, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $output .= sprintf( '%6$s<li class="%1$s"><a %8$s lang="%2$s" hreflang="%2$s" href="%3$s">%4$s%5$s</a></li>%7$s', esc_attr( implode( ' ', $element->classes ) ), esc_attr( $element->locale ), esc_url( $element->url ), $element->flag, $args['show_flags'] && $args['show_names'] ? sprintf( '<span style="margin-%1$s:0.3em;">%2$s</span>', is_rtl() ? 'right' : 'left', esc_html( $element->name ) ) : esc_html( $element->name ), 'discard' === $args['item_spacing'] ? '' : "\t", 'discard' === $args['item_spacing'] ? '' : "\n", empty( $element->link_classes ) ? '' : 'class="' . esc_attr( implode( ' ', $element->link_classes ) ) . '"' ); } /** * Overrides Walker:walk to set depth argument * * @since 1.2 * @since 2.6.7 Use $max_depth and ...$args parameters to follow the move of WP 5.3 * * @param array $elements An array of elements. * @param int $max_depth The maximum hierarchical depth. * @param mixed ...$args Additional arguments. * @return string The hierarchical item output. */ public function walk( $elements, $max_depth, ...$args ) { // phpcs:ignore WordPressVIPMinimum.Classes.DeclarationCompatibility.DeclarationCompatibility $this->maybe_fix_walk_args( $max_depth, $args ); return parent::walk( $elements, $max_depth, $args ); } } include/filters-widgets-options.php 0000644 00000005112 15136114124 0013502 0 ustar 00 <?php /** * @package Polylang */ /** * Class PLL_Widgets_Filters * * @since 3.0 * * Add new options to {@see https://developer.wordpress.org/reference/classes/wp_widget/ WP_Widget} and saves them. */ class PLL_Filters_Widgets_Options { /** * @var PLL_Model */ public $model; /** * PLL_Widgets_Filters constructor. * * @since 3.0 Moved actions from PLL_Admin_Filters. * * @param PLL_Base $polylang The Polylang object. * @return void */ public function __construct( $polylang ) { $this->model = $polylang->model; add_action( 'in_widget_form', array( $this, 'in_widget_form' ), 10, 3 ); add_filter( 'widget_update_callback', array( $this, 'widget_update_callback' ), 10, 2 ); } /** * Add the language filter field to the widgets options form. * * @since 3.0 Moved PLL_Admin_Filters. * @since 3.1 Rename lang_choice field name and id to pll_lang as the widget setting. * * @param WP_Widget $widget The widget instance (passed by reference). * @param null $return Return null if new fields are added. * @param array $instance An array of the widget's settings. * @return void */ public function in_widget_form( $widget, $return, $instance ) { $dropdown = new PLL_Walker_Dropdown(); $dropdown_html = $dropdown->walk( array_merge( array( (object) array( 'slug' => 0, 'name' => __( 'All languages', 'polylang' ) ) ), $this->model->get_languages_list() ), -1, array( 'id' => $widget->get_field_id( 'pll_lang' ), 'name' => $widget->get_field_name( 'pll_lang' ), 'class' => 'tags-input pll-lang-choice', 'selected' => empty( $instance['pll_lang'] ) ? '' : $instance['pll_lang'], ) ); printf( '<p><label for="%1$s">%2$s %3$s</label></p>', esc_attr( $widget->get_field_id( 'pll_lang' ) ), esc_html__( 'The widget is displayed for:', 'polylang' ), $dropdown_html // phpcs:ignore WordPress.Security.EscapeOutput ); } /** * Called when widget options are saved. * Saves the language associated to the widget. * * @since 0.3 * @since 3.0 Moved from PLL_Admin_Filters. * @since 3.1 Remove unused $old_instance and $widget parameters. * * @param array $instance The current Widget's options. * @param array $new_instance The new Widget's options. * @return array Widget options. */ public function widget_update_callback( $instance, $new_instance ) { if ( ! empty( $new_instance['pll_lang'] ) && $lang = $this->model->get_language( $new_instance['pll_lang'] ) ) { $instance['pll_lang'] = $lang->slug; } else { unset( $instance['pll_lang'] ); } return $instance; } } include/filters.php 0000644 00000036610 15136114124 0010354 0 ustar 00 <?php /** * @package Polylang */ /** * Setup filters common to admin and frontend * * @since 1.4 */ class PLL_Filters { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Instance of a child class of PLL_Links_Model. * * @var PLL_Links_Model */ public $links_model; /** * Current language. * * @var PLL_Language|null */ public $curlang; /** * Constructor: setups filters * * @since 1.4 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { global $wp_version; $this->links_model = &$polylang->links_model; $this->model = &$polylang->model; $this->options = &$polylang->options; $this->curlang = &$polylang->curlang; // Deletes our cache for sticky posts when the list is updated. add_action( 'update_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) ); add_action( 'add_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) ); add_action( 'delete_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) ); // Filters the comments according to the current language add_action( 'parse_comment_query', array( $this, 'parse_comment_query' ) ); add_filter( 'comments_clauses', array( $this, 'comments_clauses' ), 10, 2 ); // Filters the get_pages function according to the current language if ( version_compare( $wp_version, '6.3-alpha', '<' ) ) { // Backward compatibility with WP < 6.3. add_filter( 'get_pages', array( $this, 'get_pages' ), 10, 2 ); } add_filter( 'get_pages_query_args', array( $this, 'get_pages_query_args' ), 10, 2 ); // Rewrites next and previous post links to filter them by language add_filter( 'get_previous_post_join', array( $this, 'posts_join' ), 10, 5 ); add_filter( 'get_next_post_join', array( $this, 'posts_join' ), 10, 5 ); add_filter( 'get_previous_post_where', array( $this, 'posts_where' ), 10, 5 ); add_filter( 'get_next_post_where', array( $this, 'posts_where' ), 10, 5 ); // Converts the locale to a valid W3C locale add_filter( 'language_attributes', array( $this, 'language_attributes' ) ); // Translate the site title in emails sent to users add_filter( 'password_change_email', array( $this, 'translate_user_email' ) ); add_filter( 'email_change_email', array( $this, 'translate_user_email' ) ); // Translates the privacy policy page add_filter( 'option_wp_page_for_privacy_policy', array( $this, 'translate_page_for_privacy_policy' ), 20 ); // Since WP 4.9.6 add_filter( 'map_meta_cap', array( $this, 'fix_privacy_policy_page_editing' ), 10, 4 ); // Personal data exporter add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ), 0 ); // Since WP 4.9.6 // Fix for `term_exists()`. add_filter( 'term_exists_default_query_args', array( $this, 'term_exists_default_query_args' ), 0, 3 ); // Since WP 6.0.0. } /** * Deletes the cache for multilingual sticky posts. * * @since 2.8.4 * * @return void */ public function delete_sticky_posts_cache() { wp_cache_delete( 'sticky_posts', 'options' ); } /** * Get the language to filter a comments query. * * @since 2.0 * @since 3.1 Always returns an array. Renamed from get_comments_queried_language(). * * @param WP_Comment_Query $query WP_Comment_Query object. * @return PLL_Language[] The languages to use in the filter. */ protected function get_comments_queried_languages( $query ) { // Don't filter comments if comment ids or post ids are specified. $plucked = wp_array_slice_assoc( $query->query_vars, array( 'comment__in', 'parent', 'post_id', 'post__in', 'post_parent' ) ); $fields = array_filter( $plucked ); if ( ! empty( $fields ) ) { return array(); } // Don't filter comments if a non translated post type is specified. if ( ! empty( $query->query_vars['post_type'] ) && ! $this->model->is_translated_post_type( $query->query_vars['post_type'] ) ) { return array(); } // If comments are queried with a 'lang' parameter, keeps only language codes. if ( isset( $query->query_vars['lang'] ) ) { $languages = is_string( $query->query_vars['lang'] ) ? explode( ',', $query->query_vars['lang'] ) : $query->query_vars['lang']; if ( is_array( $languages ) ) { $languages = array_map( array( $this->model, 'get_language' ), $languages ); return array_filter( $languages ); } } if ( ! empty( $this->curlang ) ) { return array( $this->curlang ); } return array(); } /** * Adds a language dependent cache domain when querying comments. * Useful as the 'lang' parameter is not included in cache key by WordPress. * Needed since WP 4.6 as comments have been added to persistent cache. See #36906, #37419. * * @since 2.0 * * @param WP_Comment_Query $query WP_Comment_Query object. * @return void */ public function parse_comment_query( $query ) { $lang = $this->get_comments_queried_languages( $query ); if ( ! empty( $lang ) ) { $lang = wp_list_pluck( $lang, 'slug' ); $key = '_' . implode( ',', $lang ); $query->query_vars['cache_domain'] = empty( $query->query_vars['cache_domain'] ) ? 'pll' . $key : $query->query_vars['cache_domain'] . $key; } } /** * Filters the comments according to the current language. * Used by the recent comments widget and admin language filter. * * @since 0.2 * * @param string[] $clauses SQL clauses. * @param WP_Comment_Query $query WP_Comment_Query object. * @return string[] Modified $clauses. */ public function comments_clauses( $clauses, $query ) { global $wpdb; $lang = $this->get_comments_queried_languages( $query ); if ( ! empty( $lang ) ) { $lang = wp_list_pluck( $lang, 'slug' ); // If this clause is not already added by WP. if ( ! strpos( $clauses['join'], '.ID' ) ) { $clauses['join'] .= " JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID"; } $clauses['join'] .= $this->model->post->join_clause(); $clauses['where'] .= $this->model->post->where_clause( $lang ); } return $clauses; } /** * Filters get_pages() per language. * * @since 1.4 * * @param WP_Post[] $pages An array of pages already queried. * @param array $args Array of get_pages() arguments. * @return WP_Post[] Modified list of pages. */ public function get_pages( $pages, $args ) { if ( isset( $args['lang'] ) && empty( $args['lang'] ) ) { return $pages; } $language = empty( $args['lang'] ) ? $this->curlang : $this->model->get_language( $args['lang'] ); if ( empty( $language ) || empty( $pages ) || ! $this->model->is_translated_post_type( $args['post_type'] ) ) { return $pages; } static $once = false; if ( ! empty( $args['number'] ) && ! $once ) { // We are obliged to redo the get_pages() query if we want to get the right number. $once = true; // Avoid infinite loop. // Take care that 'exclude' argument accepts integer or strings too. $args['exclude'] = array_merge( wp_parse_id_list( $args['exclude'] ), $this->get_related_page_ids( $language, 'NOT IN', $args ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude $numbered_pages = get_pages( $args ); $pages = ! $numbered_pages ? $pages : $numbered_pages; } $ids = wp_list_pluck( $pages, 'ID' ); if ( ! $once ) { // Filters the queried list of pages by language. $ids = array_intersect( $ids, $this->get_related_page_ids( $language, 'IN', $args ) ); foreach ( $pages as $key => $page ) { if ( ! in_array( $page->ID, $ids ) ) { unset( $pages[ $key ] ); } } $pages = array_values( $pages ); // In case 3rd parties suppose the existence of $pages[0]. } // Not done by WP but extremely useful for performance when manipulating taxonomies. update_object_term_cache( $ids, $args['post_type'] ); $once = false; // In case get_pages() is called another time. return $pages; } /** * Filters the WP_Query in get_pages() per language. * * @since 3.4.3 * * @param array $query_args Array of arguments passed to WP_Query. * @param array $parsed_args Array of get_pages() arguments. * @return array Array of arguments passed to WP_Query with the language. */ public function get_pages_query_args( $query_args, $parsed_args ) { if ( isset( $parsed_args['lang'] ) ) { $query_args['lang'] = $parsed_args['lang']; } return $query_args; } /** * Get page ids related to a get_pages() in or not in a given language. * * @since 3.2 * * @param PLL_Language $language The language to use in the relationship * @param string $relation 'IN' or 'NOT IN'. * @param array $args Array of get_pages() arguments. * @return int[] */ protected function get_related_page_ids( $language, $relation, $args ) { $r = array( 'lang' => '', // Ensure this query is not filtered. 'numberposts' => -1, 'nopaging' => true, 'post_type' => $args['post_type'], 'post_status' => $args['post_status'], 'fields' => 'ids', 'tax_query' => array( array( 'taxonomy' => 'language', 'field' => 'term_taxonomy_id', // Since WP 3.5. 'terms' => $language->get_tax_prop( 'language', 'term_taxonomy_id' ), 'operator' => $relation, ), ), ); return get_posts( $r ); } /** * Modifies the sql request for get_adjacent_post to filter by the current language. * * @since 0.1 * * @param string $sql The JOIN clause in the SQL. * @param bool $in_same_term Whether post should be in a same taxonomy term. * @param int[] $excluded_terms Array of excluded term IDs. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. * @return string Modified JOIN clause. */ public function posts_join( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) { return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->join_clause( 'p' ) : $sql; } /** * Modifies the sql request for wp_get_archives and get_adjacent_post to filter by the current language. * * @since 0.1 * * @param string $sql The WHERE clause in the SQL. * @param bool $in_same_term Whether post should be in a same taxonomy term. * @param int[] $excluded_terms Array of excluded term IDs. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. * @return string Modified WHERE clause. */ public function posts_where( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) { return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->where_clause( $this->curlang ) : $sql; } /** * Converts WordPress locale to valid W3 locale in html language attributes * * @since 1.8 * * @param string $output language attributes * @return string */ public function language_attributes( $output ) { if ( $language = $this->model->get_language( is_admin() ? get_user_locale() : get_locale() ) ) { $output = str_replace( '"' . get_bloginfo( 'language' ) . '"', '"' . $language->get_locale( 'display' ) . '"', $output ); } return $output; } /** * Translates the site title in emails sent to the user (change email, reset password) * It is necessary to filter the email because WP evaluates the site title before calling switch_to_locale() * * @since 2.1.3 * * @param string[] $email Email contents. * @return string[] Translated email contents. */ public function translate_user_email( $email ) { $blog_name = wp_specialchars_decode( pll__( get_option( 'blogname' ) ), ENT_QUOTES ); $email['subject'] = sprintf( $email['subject'], $blog_name ); $email['message'] = str_replace( '###SITENAME###', $blog_name, $email['message'] ); return $email; } /** * Translates the privacy policy page, on both frontend and admin * * @since 2.3.6 * * @param int $id Privacy policy page id * @return int */ public function translate_page_for_privacy_policy( $id ) { return empty( $this->curlang ) ? $id : $this->model->post->get( $id, $this->curlang ); } /** * Prevents edit and delete links for the translations of the privacy policy page for non admin * * @since 2.3.7 * * @param array $caps The user's actual capabilities. * @param string $cap Capability name. * @param int $user_id The user ID. * @param array $args Adds the context to the cap. The category id. * @return array */ public function fix_privacy_policy_page_editing( $caps, $cap, $user_id, $args ) { if ( in_array( $cap, array( 'edit_page', 'edit_post', 'delete_page', 'delete_post' ) ) ) { $privacy_page = get_option( 'wp_page_for_privacy_policy' ); if ( $privacy_page && array_intersect( $args, $this->model->post->get_translations( $privacy_page ) ) ) { $caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) ); } } return $caps; } /** * Register our personal data exporter * * @since 2.3.6 * * @param array $exporters Personal data exporters * @return array */ public function register_personal_data_exporter( $exporters ) { $exporters[] = array( 'exporter_friendly_name' => __( 'Translated user descriptions', 'polylang' ), 'callback' => array( $this, 'user_data_exporter' ), ); return $exporters; } /** * Export translated user description as WP exports only the description in the default language * * @since 2.3.6 * * @param string $email_address User email address * @return array Personal data */ public function user_data_exporter( $email_address ) { $email_address = trim( $email_address ); $data_to_export = array(); $user_data_to_export = array(); if ( $user = get_user_by( 'email', $email_address ) ) { foreach ( $this->model->get_languages_list() as $lang ) { if ( ! $lang->is_default && $value = get_user_meta( $user->ID, 'description_' . $lang->slug, true ) ) { $user_data_to_export[] = array( /* translators: %s is a language native name */ 'name' => sprintf( __( 'User description - %s', 'polylang' ), $lang->name ), 'value' => $value, ); } } if ( ! empty( $user_data_to_export ) ) { $data_to_export[] = array( 'group_id' => 'user', 'group_label' => __( 'User', 'polylang' ), 'item_id' => "user-{$user->ID}", 'data' => $user_data_to_export, ); } } return array( 'data' => $data_to_export, 'done' => true, ); } /** * Filters default query arguments for checking if a term exists. * In `term_exists()`, WP 6.0 uses `get_terms()`, which is filtered by language by Polylang. * This filter prevents `term_exists()` to be filtered by language. * * @since 3.2 * * @param array $defaults An array of arguments passed to get_terms(). * @param int|string $term The term to check. Accepts term ID, slug, or name. * @param string $taxonomy The taxonomy name to use. An empty string indicates the search is against all taxonomies. * @return array */ public function term_exists_default_query_args( $defaults, $term, $taxonomy ) { if ( ! empty( $taxonomy ) && ! $this->model->is_translated_taxonomy( $taxonomy ) ) { return $defaults; } if ( ! is_array( $defaults ) ) { $defaults = array(); } if ( ! isset( $defaults['lang'] ) ) { $defaults['lang'] = ''; } return $defaults; } } include/links-subdomain.php 0000644 00000004250 15136114124 0011776 0 ustar 00 <?php /** * @package Polylang */ /** * Links model for use when the language code is added in the url as a subdomain * for example en.mysite.com/something. * * @since 1.2 */ class PLL_Links_Subdomain extends PLL_Links_Abstract_Domain { /** * Stores whether the home url includes www. or not. * Either '://' or '://www.'. * * @var string */ protected $www; /** * Constructor. * * @since 1.7.4 * * @param PLL_Model $model Instance of PLL_Model. */ public function __construct( &$model ) { parent::__construct( $model ); $this->www = ( false === strpos( $this->home, '://www.' ) ) ? '://' : '://www.'; } /** * Adds the language code in a url. * * @since 1.2 * @since 3.4 Accepts now a language slug. * * @param string $url The url to modify. * @param PLL_Language|string|false $language Language object or slug. * @return string The modified url. */ public function add_language_to_link( $url, $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } if ( ! empty( $language ) && false === strpos( $url, '://' . $language . '.' ) ) { $url = $this->options['default_lang'] === $language && $this->options['hide_default'] ? $url : str_replace( $this->www, '://' . $language . '.', $url ); } return $url; } /** * Returns the url without the language code. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_language_from_link( $url ) { $languages = $this->model->get_languages_list( array( 'hide_default' => $this->options['hide_default'], 'fields' => 'slug', ) ); if ( ! empty( $languages ) ) { $url = preg_replace( '#://(' . implode( '|', $languages ) . ')\.#', $this->www, $url ); } return $url; } /** * Get the hosts managed on the website. * * @since 1.5 * * @return string[] The list of hosts. */ public function get_hosts() { $hosts = array(); foreach ( $this->model->get_languages_list() as $lang ) { $host = wp_parse_url( $this->home_url( $lang ), PHP_URL_HOST ); $hosts[ $lang->slug ] = $host ? $host : ''; } return $hosts; } } include/cache.php 0000644 00000004707 15136114124 0007751 0 ustar 00 <?php /** * @package Polylang */ /** * An extremely simple non persistent cache system. * * @since 1.7 * * @template TCacheData */ class PLL_Cache { /** * Current site id. * * @var int */ protected $blog_id; /** * The cache container. * * @var array * * @phpstan-var array<int, array<non-empty-string, TCacheData>> */ protected $cache = array(); /** * Constructor. * * @since 1.7 */ public function __construct() { $this->blog_id = get_current_blog_id(); add_action( 'switch_blog', array( $this, 'switch_blog' ) ); } /** * Called when switching blog. * * @since 1.7 * * @param int $new_blog_id New blog ID. * @return void */ public function switch_blog( $new_blog_id ) { $this->blog_id = $new_blog_id; } /** * Adds a value in cache. * * @since 1.7 * @since 3.6 Returns the cached value. * * @param string $key Cache key. * @param mixed $data The value to add to the cache. * @return mixed * * @phpstan-param non-empty-string $key * @phpstan-param TCacheData $data * @phpstan-return TCacheData */ public function set( $key, $data ) { $this->cache[ $this->blog_id ][ $key ] = $data; return $data; } /** * Returns value from cache. * * @since 1.7 * * @param string $key Cache key. * @return mixed * * @phpstan-param non-empty-string $key * @phpstan-return TCacheData|false */ public function get( $key ) { return isset( $this->cache[ $this->blog_id ][ $key ] ) ? $this->cache[ $this->blog_id ][ $key ] : false; } /** * Cleans the cache (for this blog only). * * @since 1.7 * * @param string $key Optional. Cache key. An empty string to clean the whole cache for the current blog. * Default is an empty string. * @return void */ public function clean( $key = '' ) { if ( '' === $key ) { unset( $this->cache[ $this->blog_id ] ); } else { unset( $this->cache[ $this->blog_id ][ $key ] ); } } /** * Generates and returns a "unique" cache key, depending on `$data` and prefixed by `$prefix`. * * @since 3.6 * * @param string $prefix String to prefix the cache key. * @param string|array|object $data Data. * @return string * * @phpstan-param non-empty-string $prefix * @phpstan-return non-empty-string */ public function get_unique_key( string $prefix, $data ): string { /** @var scalar */ $serialized = maybe_serialize( $data ); return $prefix . md5( (string) $serialized ); } } include/translated-term.php 0000644 00000023430 15136114124 0012006 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Sets the taxonomies languages and translations model up. * * @since 1.8 * * @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface */ class PLL_Translated_Term extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface { use PLL_Translatable_Object_With_Types_Trait; /** * Taxonomy name for the languages. * * @var string * * @phpstan-var non-empty-string */ protected $tax_language = 'term_language'; /** * Object type to use when registering the taxonomy. * * @var string * * @phpstan-var non-empty-string */ protected $object_type = 'term'; /** * Identifier that must be unique for each type of content. * Also used when checking capabilities. * * @var string * * @phpstan-var non-empty-string */ protected $type = 'term'; /** * Identifier for each type of content to used for cache type. * * @var string * * @phpstan-var non-empty-string */ protected $cache_type = 'terms'; /** * Taxonomy name for the translation groups. * * @var string * * @phpstan-var non-empty-string */ protected $tax_translations = 'term_translations'; /** * Constructor. * * @since 1.8 * * @param PLL_Model $model Instance of `PLL_Model`. */ public function __construct( PLL_Model &$model ) { parent::__construct( $model ); // Keep hooks in constructor for backward compatibility. $this->init(); } /** * Adds hooks. * * @since 3.4 * * @return static */ public function init() { add_filter( 'get_terms', array( $this, '_prime_terms_cache' ), 10, 2 ); add_action( 'clean_term_cache', array( $this, 'clean_term_cache' ) ); return parent::init(); } /** * Stores the term's language into the database. * * @since 0.6 * @since 3.4 Renamed the parameter $term_id into $id. * * @param int $id Term ID. * @param PLL_Language|string|int $lang Language (object, slug, or term ID). * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to * the object). */ public function set_language( $id, $lang ) { if ( ! parent::set_language( $id, $lang ) ) { return false; } $id = $this->sanitize_int_id( $id ); // Add translation group for correct WXR export. $translations = $this->get_translations( $id ); if ( ! empty( $translations ) ) { $translations = array_diff( $translations, array( $id ) ); } $this->save_translations( $id, $translations ); return true; } /** * Returns the language of a term. * * @since 0.1 * @since 3.4 Renamed the parameter $value into $id. * @since 3.4 Deprecated to retrieve the language by term slug + taxonomy anymore. * * @param int $id Term ID. * @return PLL_Language|false A `PLL_Language` object. `false` if no language is associated to that term or if the * ID is invalid. */ public function get_language( $id ) { if ( func_num_args() > 1 ) { // Backward compatibility. _deprecated_argument( __METHOD__ . '()', '3.4' ); $term = get_term_by( 'slug', $id, func_get_arg( 1 ) ); // @phpstan-ignore-line $id = $term instanceof WP_Term ? $term->term_id : 0; } return parent::get_language( $id ); } /** * Deletes a translation of a term. * * @since 0.5 * * @param int $id Term ID. * @return void */ public function delete_translation( $id ) { global $wpdb; $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return; } $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key. parent::delete_translation( $id ); wp_delete_object_term_relationships( $id, $this->tax_translations ); if ( doing_action( 'pre_delete_term' ) ) { return; } if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $wpdb->terms WHERE term_id = %d;", $id ) ) ) { return; } // Always keep a group for terms to allow relationships remap when importing from a WXR file. $group = uniqid( 'pll_' ); $translations = array( $slug => $id ); wp_insert_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) ); wp_set_object_terms( $id, $group, $this->tax_translations ); } /** * Returns object types (taxonomy names) that need to be translated. * The taxonomies list is cached for better performance. * The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php. * * @since 3.4 * * @param bool $filter True if we should return only valid registered object types. * @return string[] Object type names for which Polylang manages languages. * * @phpstan-return array<non-empty-string, non-empty-string> */ public function get_translated_object_types( $filter = true ) { $taxonomies = $this->model->cache->get( 'taxonomies' ); if ( false === $taxonomies ) { $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' ); if ( ! empty( $this->model->options['taxonomies'] ) && is_array( $this->model->options['taxonomies'] ) ) { $taxonomies = array_merge( $taxonomies, array_combine( $this->model->options['taxonomies'], $this->model->options['taxonomies'] ) ); } /** * Filters the list of taxonomies available for translation. * The default are taxonomies which have the parameter ‘public’ set to true. * The filter must be added soon in the WordPress loading process: * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes. * * @since 0.8 * * @param string[] $taxonomies List of taxonomy names (as array keys and values). * @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings. */ $taxonomies = (array) apply_filters( 'pll_get_taxonomies', $taxonomies, false ); if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) { $this->model->cache->set( 'taxonomies', $taxonomies ); } } /** @var array<non-empty-string, non-empty-string> $taxonomies */ return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies; } /** * Caches the language and translations when terms are queried by get_terms(). * * @since 1.2 * * @param WP_Term[]|int[] $terms Queried terms. * @param string[] $taxonomies Queried taxonomies. * @return WP_Term[]|int[] Unmodified $terms. * * @phpstan-param array<WP_Term|positive-int> $terms * @phpstan-param array<non-empty-string> $taxonomies * @phpstan-return array<WP_Term|positive-int> */ public function _prime_terms_cache( $terms, $taxonomies ) { $ids = array(); if ( is_array( $terms ) && $this->model->is_translated_taxonomy( $taxonomies ) ) { foreach ( $terms as $term ) { $ids[] = is_object( $term ) ? $term->term_id : (int) $term; } } if ( ! empty( $ids ) ) { update_object_term_cache( array_unique( $ids ), 'term' ); // Adds language and translation of terms to cache. } return $terms; } /** * When the term cache is cleaned, cleans the object term cache too. * * @since 2.0 * * @param int[] $ids An array of term IDs. * @return void * * @phpstan-param array<positive-int> $ids */ public function clean_term_cache( $ids ) { clean_object_term_cache( $this->sanitize_int_ids_list( $ids ), 'term' ); } /** * Tells whether a translation term must be updated. * * @since 2.3 * * @param int $id Term ID. * @param int[] $translations An associative array of translations with language code as key and translation ID as * value. Make sure to sanitize this. * @return bool * * @phpstan-param array<non-empty-string, positive-int> $translations */ protected function should_update_translation_group( $id, $translations ) { // Don't do anything if no translations have been added to the group. $old_translations = $this->get_translations( $id ); if ( count( $translations ) > 1 && ! empty( array_diff_assoc( $translations, $old_translations ) ) ) { return true; } // But we need a translation group for terms to allow relationships remap when importing from a WXR file $term = $this->get_object_term( $id, $this->tax_translations ); return empty( $term ) || ! empty( array_diff_assoc( $translations, $old_translations ) ); } /** * Assigns a language to terms in mass. * * @since 1.2 * @since 3.4 Moved from PLL_Admin_Model class. * * @param int[] $ids Array of post ids or term ids. * @param PLL_Language $lang Language to assign to the posts or terms. * @return void */ public function set_language_in_mass( $ids, $lang ) { parent::set_language_in_mass( $ids, $lang ); $translations = array(); foreach ( $ids as $id ) { $translations[] = array( $lang->slug => $id ); } if ( ! empty( $translations ) ) { $this->set_translation_in_mass( $translations ); } } /** * Returns database-related information that can be used in some of this class methods. * These are specific to the table containing the objects. * * @see PLL_Translatable_Object::join_clause() * @see PLL_Translatable_Object::get_objects_with_no_lang_sql() * * @since 3.4.3 * * @return string[] { * @type string $table Name of the table. * @type string $id_column Name of the column containing the object's ID. * @type string $type_column Name of the column containing the object's type. * @type string $default_alias Default alias corresponding to the object's table. * } * @phpstan-return DBInfoWithType */ protected function get_db_infos() { return array( 'table' => $GLOBALS['wpdb']->term_taxonomy, 'id_column' => 'term_id', 'type_column' => 'taxonomy', 'default_alias' => 't', ); } } include/translated-object.php 0000644 00000040622 15136114124 0012307 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Abstract class to use for object types that support translations. * * @since 1.8 */ abstract class PLL_Translated_Object extends PLL_Translatable_Object { /** * Taxonomy name for the translation groups. * * @var string * * @phpstan-var non-empty-string */ protected $tax_translations; /** * Constructor. * * @since 1.8 * * @param PLL_Model $model Instance of `PLL_Model`. */ public function __construct( PLL_Model &$model ) { parent::__construct( $model ); $this->tax_to_cache[] = $this->tax_translations; /* * Register our taxonomy as soon as possible. */ register_taxonomy( $this->tax_translations, (array) $this->object_type, array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true, 'update_count_callback' => '_update_generic_term_count', // Count *all* objects to correctly detect unused terms. ) ); } /** * Returns the translations group taxonomy name. * * @since 3.4 * * @return string * * @phpstan-return non-empty-string */ public function get_tax_translations() { return $this->tax_translations; } /** * Assigns a language to an object, taking care of the translations group. * * @since 3.4 * * @param int $id Object ID. * @param PLL_Language|string|int $lang Language to assign to the object. * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to * the object). */ public function set_language( $id, $lang ) { if ( ! parent::set_language( $id, $lang ) ) { return false; } $id = $this->sanitize_int_id( $id ); $translations = $this->get_translations( $id ); // Don't create translation groups with only 1 value. if ( ! empty( $translations ) ) { // Remove the object's former language from the new translations group before adding the new value. $translations = array_diff( $translations, array( $id ) ); $this->save_translations( $id, $translations ); } return true; } /** * Returns a list of object translations, given a `tax_translations` term ID. * * @since 3.2 * * @param int $term_id A `tax_translations` term ID. * @return int[] An associative array of translations with language code as key and translation ID as value. * * @phpstan-return array<non-empty-string, positive-int> */ public function get_translations_from_term_id( $term_id ) { $term_id = $this->sanitize_int_id( $term_id ); if ( empty( $term_id ) ) { return array(); } $translations_term = get_term( $term_id, $this->tax_translations ); if ( ! $translations_term instanceof WP_Term || empty( $translations_term->description ) ) { return array(); } // Lang slugs as array keys, translation IDs as array values. $translations = maybe_unserialize( $translations_term->description ); $translations = is_array( $translations ) ? $translations : array(); return $this->validate_translations( $translations, 0, 'display' ); } /** * Saves the object's translations. * * @since 0.5 * * @param int $id Object ID. * @param int[] $translations An associative array of translations with language code as key and translation ID as value. * @return int[] An associative array with language codes as key and object IDs as values. * * @phpstan-return array<non-empty-string, positive-int> */ public function save_translations( $id, array $translations = array() ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return array(); } $lang = $this->get_language( $id ); if ( empty( $lang ) ) { return array(); } // Sanitize and validate the translations array. $translations = $this->validate_translations( $translations, $id ); // Unlink removed translations. $old_translations = $this->get_translations( $id ); foreach ( array_diff_assoc( $old_translations, $translations ) as $id ) { $this->delete_translation( $id ); } // Check ID we need to create or update the translation group. if ( ! $this->should_update_translation_group( $id, $translations ) ) { return $translations; } $terms = wp_get_object_terms( $translations, $this->tax_translations ); $term = is_array( $terms ) && ! empty( $terms ) ? reset( $terms ) : false; if ( empty( $term ) ) { // Create a new term if necessary. $group = uniqid( 'pll_' ); wp_insert_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) ); } else { // Take care not to overwrite extra data stored in the description field, if any. $group = (int) $term->term_id; $descr = maybe_unserialize( $term->description ); $descr = is_array( $descr ) ? array_diff_key( $descr, $old_translations ) : array(); // Remove old translations. $descr = array_merge( $descr, $translations ); // Add new one. wp_update_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) ); } // Link all translations to the new term. foreach ( $translations as $p ) { wp_set_object_terms( $p, $group, $this->tax_translations ); } if ( ! is_array( $terms ) ) { return $translations; } // Clean now unused translation groups. foreach ( $terms as $term ) { // Get fresh count value. $term = get_term( $term->term_id, $this->tax_translations ); if ( $term instanceof WP_Term && empty( $term->count ) ) { wp_delete_term( $term->term_id, $this->tax_translations ); } } return $translations; } /** * Deletes a translation of an object. * * @since 0.5 * * @param int $id Object ID. * @return void */ public function delete_translation( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return; } $term = $this->get_object_term( $id, $this->tax_translations ); if ( empty( $term ) ) { return; } $descr = maybe_unserialize( $term->description ); if ( ! empty( $descr ) && is_array( $descr ) ) { $slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key. if ( false !== $slug ) { unset( $descr[ $slug ] ); } } if ( empty( $descr ) || ! is_array( $descr ) ) { wp_delete_term( (int) $term->term_id, $this->tax_translations ); } else { wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) ); } } /** * Returns an array of valid translations of an object. * * @since 0.5 * * @param int $id Object ID. * @return int[] An associative array of translations with language code as key and translation ID as value. * * @phpstan-return array<non-empty-string, positive-int> */ public function get_translations( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return array(); } $translations = $this->get_raw_translations( $id ); return $this->validate_translations( $translations, $id, 'display' ); } /** * Returns an unvalidated array of translations of an object. * It is generally preferable to use `get_translations()`. * * @since 3.4 * * @param int $id Object ID. * @return int[] An associative array of translations with language code as key and translation ID as value. * * @phpstan-return array<non-empty-string, positive-int> */ public function get_raw_translations( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return array(); } $term = $this->get_object_term( $id, $this->tax_translations ); if ( empty( $term->description ) ) { return array(); } $translations = maybe_unserialize( $term->description ); $translations = is_array( $translations ) ? $translations : array(); return $translations; } /** * Returns the ID of the translation of an object. * * @since 0.5 * * @param int $id Object ID. * @param PLL_Language|string $lang Language (slug or object). * @return int|false Object ID of the translation, `false` if there is none. * * @phpstan-return positive-int|false */ public function get_translation( $id, $lang ) { $lang = $this->model->get_language( $lang ); if ( empty( $lang ) ) { return false; } $translations = $this->get_translations( $id ); return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false; } /** * Among the object and its translations, returns the ID of the object which is in `$lang`. * * @since 0.1 * @since 3.4 Returns 0 instead of false. * * @param int $id Object ID. * @param PLL_Language|string|int $lang Language (object, slug, or term ID). * @return int The translation object ID if exists, otherwise the passed ID. `0` if the passed object has no language. * * @phpstan-return int<0, max> */ public function get( $id, $lang ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return 0; } $lang = $this->model->get_language( $lang ); if ( empty( $lang ) ) { return 0; } $obj_lang = $this->get_language( $id ); if ( empty( $obj_lang ) ) { return 0; } return $obj_lang->term_id === $lang->term_id ? $id : (int) $this->get_translation( $id, $lang ); } /** * Checks if a user can synchronize translations. * * @since 2.6 * * @param int $id Object ID. * @return bool */ public function current_user_can_synchronize( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return false; } /** * Filters whether a synchronization capability check should take place. * * @since 2.6 * * @param bool|null $check Null to enable the capability check, * true to always allow the synchronization, * false to always disallow the synchronization. * Defaults to true. * @param int $id The synchronization source object ID. */ $check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id ); if ( null !== $check ) { return (bool) $check; } if ( ! current_user_can( "edit_{$this->type}", $id ) ) { return false; } foreach ( $this->get_translations( $id ) as $tr_id ) { if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) { return false; } } return true; } /** * Tells whether a translation term must be updated. * * @since 2.3 * * @param int $id Object ID. * @param int[] $translations An associative array of translations with language code as key and translation ID as * value. Make sure to sanitize this. * @return bool * * @phpstan-param array<non-empty-string, positive-int> $translations */ protected function should_update_translation_group( $id, $translations ) { // Don't do anything if no translations have been added to the group. $old_translations = $this->get_translations( $id ); // Includes at least $id itself. return ! empty( array_diff_assoc( $translations, $old_translations ) ); } /** * Validates and sanitizes translations. * This will: * - Make sure to return only translations in existing languages (and only translations). * - Sanitize the values. * - Make sure the provided translation (`$id`) is in the list. * - Check that the translated objects are in the right language, if `$context` is set to 'save'. * * @since 3.1 * @since 3.2 Doesn't return `0` ID values. * @since 3.2 Added parameters `$id` and `$context`. * * @param int[] $translations An associative array of translations with language code as key and translation ID as * value. * @param int $id Optional. The object ID for which the translations are validated. When provided, the * process makes sure it is added to the list. Default 0. * @param string $context Optional. The operation for which the translations are validated. When set to * 'save', a check is done to verify that the IDs and langs correspond. * 'display' should be used otherwise. Default 'save'. * @return int[] * * @phpstan-param non-empty-string $context * @phpstan-return array<non-empty-string, positive-int> */ protected function validate_translations( $translations, $id = 0, $context = 'save' ) { if ( ! is_array( $translations ) ) { $translations = array(); } /** * Remove translations in non-existing languages, and non-translation data (we allow plugins to store other * information in the array). */ $translations = array_intersect_key( $translations, array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); // Make sure values are clean before working with them. /** @phpstan-var array<non-empty-string, positive-int> $translations */ $translations = $this->sanitize_int_ids_list( $translations ); if ( 'save' === $context ) { /** * Check that the translated objects are in the right language. * For better performance, this should be done only when saving the data into the database, not when * retrieving data from it. */ $valid_translations = array(); foreach ( $translations as $lang_slug => $tr_id ) { $tr_lang = $this->get_language( $tr_id ); if ( ! empty( $tr_lang ) && $tr_lang->slug === $lang_slug ) { $valid_translations[ $lang_slug ] = $tr_id; } } $translations = $valid_translations; } $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return $translations; } // Make sure to return at least the passed object in its translation array. $lang = $this->get_language( $id ); if ( empty( $lang ) ) { return $translations; } /** @phpstan-var array<non-empty-string, positive-int> $translations */ return array_merge( array( $lang->slug => $id ), $translations ); } /** * Creates translations groups in mass. * * @since 1.6.3 * @since 3.4 Moved from PLL_Admin_Model class. * * @param int[][] $translations Array of translations arrays. * @return void * * @phpstan-param array<array<string,int>> $translations */ public function set_translation_in_mass( $translations ) { global $wpdb; $terms = array(); $slugs = array(); $description = array(); $count = array(); foreach ( $translations as $t ) { $term = uniqid( 'pll_' ); // the term name $terms[] = $wpdb->prepare( '( %s, %s )', $term, $term ); $slugs[] = $wpdb->prepare( '%s', $term ); $description[ $term ] = maybe_serialize( $t ); $count[ $term ] = count( $t ); } // Insert terms if ( ! empty( $terms ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES " . implode( ',', array_unique( $terms ) ) ); } // Get all terms with their term_id // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $terms = $wpdb->get_results( "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN ( " . implode( ',', $slugs ) . ' )' ); $term_ids = array(); $tts = array(); // Prepare terms taxonomy relationship foreach ( $terms as $term ) { $term_ids[] = $term->term_id; $tts[] = $wpdb->prepare( '( %d, %s, %s, %d )', $term->term_id, $this->tax_translations, $description[ $term->slug ], $count[ $term->slug ] ); } // Insert term_taxonomy if ( ! empty( $tts ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES " . implode( ',', array_unique( $tts ) ) ); } // Get all terms with term_taxonomy_id $terms = get_terms( array( 'taxonomy' => $this->tax_translations, 'hide_empty' => false ) ); $trs = array(); // Prepare objects relationships. if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $t = maybe_unserialize( $term->description ); if ( is_array( $t ) && in_array( $t, $translations ) ) { foreach ( $t as $object_id ) { if ( ! empty( $object_id ) ) { $trs[] = $wpdb->prepare( '( %d, %d )', $object_id, $term->term_taxonomy_id ); } } } } } // Insert term_relationships if ( ! empty( $trs ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', $trs ) ); $trs = array_unique( $trs ); } clean_term_cache( $term_ids, $this->tax_translations ); } } include/class-polylang.php 0000644 00000021067 15136114124 0011634 0 ustar 00 <?php /** * @package Polylang */ if ( ! defined( 'ABSPATH' ) ) { exit; // Don't access directly } // Default directory to store user data such as custom flags if ( ! defined( 'PLL_LOCAL_DIR' ) ) { define( 'PLL_LOCAL_DIR', WP_CONTENT_DIR . '/polylang' ); } // Includes local config file if exists if ( is_readable( PLL_LOCAL_DIR . '/pll-config.php' ) ) { include_once PLL_LOCAL_DIR . '/pll-config.php'; } /** * Controls the plugin, as well as activation, and deactivation * * @since 0.1 * * @template TPLLClass of PLL_Base */ class Polylang { /** * Constructor * * @since 0.1 */ public function __construct() { require_once __DIR__ . '/functions.php'; // VIP functions // register an action when plugin is activating. register_activation_hook( POLYLANG_BASENAME, array( 'PLL_Wizard', 'start_wizard' ) ); $install = new PLL_Install( POLYLANG_BASENAME ); // Stopping here if we are going to deactivate the plugin ( avoids breaking rewrite rules ) if ( $install->is_deactivation() || ! $install->can_activate() ) { return; } // Plugin initialization // Take no action before all plugins are loaded add_action( 'plugins_loaded', array( $this, 'init' ), 1 ); // Override load text domain waiting for the language to be defined // Here for plugins which load text domain as soon as loaded :( if ( ! defined( 'PLL_OLT' ) || PLL_OLT ) { PLL_OLT_Manager::instance(); } /* * Loads the compatibility with some plugins and themes. * Loaded as soon as possible as we may need to act before other plugins are loaded. */ if ( ! defined( 'PLL_PLUGINS_COMPAT' ) || PLL_PLUGINS_COMPAT ) { PLL_Integrations::instance(); } } /** * Tells whether the current request is an ajax request on frontend or not * * @since 2.2 * * @return bool */ public static function is_ajax_on_front() { // Special test for plupload which does not use jquery ajax and thus does not pass our ajax prefilter // Special test for customize_save done in frontend but for which we want to load the admin $in = isset( $_REQUEST['action'] ) && in_array( sanitize_key( $_REQUEST['action'] ), array( 'upload-attachment', 'customize_save' ) ); // phpcs:ignore WordPress.Security.NonceVerification $is_ajax_on_front = wp_doing_ajax() && empty( $_REQUEST['pll_ajax_backend'] ) && ! $in; // phpcs:ignore WordPress.Security.NonceVerification /** * Filters whether the current request is an ajax request on front. * * @since 2.3 * * @param bool $is_ajax_on_front Whether the current request is an ajax request on front. */ return apply_filters( 'pll_is_ajax_on_front', $is_ajax_on_front ); } /** * Is the current request a REST API request? * Inspired by WP::parse_request() * Needed because at this point, the constant REST_REQUEST is not defined yet * * @since 2.4.1 * * @return bool */ public static function is_rest_request() { // Handle pretty permalinks. $home_path = trim( (string) wp_parse_url( home_url(), PHP_URL_PATH ), '/' ); $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) ); $req_uri = trim( (string) wp_parse_url( pll_get_requested_url(), PHP_URL_PATH ), '/' ); $req_uri = (string) preg_replace( $home_path_regex, '', $req_uri ); $req_uri = trim( $req_uri, '/' ); $req_uri = str_replace( 'index.php', '', $req_uri ); $req_uri = trim( $req_uri, '/' ); // And also test rest_route query string parameter is not empty for plain permalinks. $query_string = array(); wp_parse_str( (string) wp_parse_url( pll_get_requested_url(), PHP_URL_QUERY ), $query_string ); $rest_route = isset( $query_string['rest_route'] ) ? trim( $query_string['rest_route'], '/' ) : false; return 0 === strpos( $req_uri, rest_get_url_prefix() . '/' ) || ! empty( $rest_route ); } /** * Tells if we are in the wizard process. * * @since 2.7 * * @return bool */ public static function is_wizard() { return isset( $_GET['page'] ) && ! empty( $_GET['page'] ) && 'mlang_wizard' === sanitize_key( $_GET['page'] ); // phpcs:ignore WordPress.Security.NonceVerification } /** * Defines constants * May be overridden by a plugin if set before plugins_loaded, 1 * * @since 1.6 * * @return void */ public static function define_constants() { // Cookie name. no cookie will be used if set to false if ( ! defined( 'PLL_COOKIE' ) ) { define( 'PLL_COOKIE', 'pll_language' ); } // Backward compatibility with Polylang < 2.3 if ( ! defined( 'PLL_AJAX_ON_FRONT' ) ) { define( 'PLL_AJAX_ON_FRONT', self::is_ajax_on_front() ); } // Admin if ( ! defined( 'PLL_ADMIN' ) ) { define( 'PLL_ADMIN', wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) || ( is_admin() && ! PLL_AJAX_ON_FRONT ) ); } // Settings page whatever the tab except for the wizard which needs to be an admin process. if ( ! defined( 'PLL_SETTINGS' ) ) { define( 'PLL_SETTINGS', is_admin() && ( ( isset( $_GET['page'] ) && 0 === strpos( sanitize_key( $_GET['page'] ), 'mlang' ) && ! self::is_wizard() ) || ! empty( $_REQUEST['pll_ajax_settings'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification } } /** * Polylang initialization * setups models and separate admin and frontend * * @since 1.2 * * @return void */ public function init() { self::define_constants(); $options = get_option( 'polylang' ); // Plugin upgrade if ( $options && version_compare( $options['version'], POLYLANG_VERSION, '<' ) ) { $upgrade = new PLL_Upgrade( $options ); if ( ! $upgrade->upgrade() ) { // If the version is too old return; } } // In some edge cases, it's possible that no options were found in the database. Load default options as we need some. if ( ! $options ) { $options = PLL_Install::get_default_options(); } /** * Filter the model class to use * /!\ this filter is fired *before* the $polylang object is available * * @since 1.5 * * @param string $class either PLL_Model or PLL_Admin_Model */ $class = apply_filters( 'pll_model', PLL_SETTINGS || self::is_wizard() ? 'PLL_Admin_Model' : 'PLL_Model' ); /** @var PLL_Model $model */ $model = new $class( $options ); if ( ! $model->has_languages() ) { /** * Fires when no language has been defined yet * Used to load overridden textdomains * * @since 1.2 */ do_action( 'pll_no_language_defined' ); } $class = ''; if ( PLL_SETTINGS ) { $class = 'PLL_Settings'; } elseif ( PLL_ADMIN ) { $class = 'PLL_Admin'; } elseif ( self::is_rest_request() ) { $class = 'PLL_REST_Request'; } elseif ( $model->has_languages() ) { $class = 'PLL_Frontend'; } /** * Filters the class to use to instantiate the $polylang object * * @since 2.6 * * @param string $class A class name. */ $class = apply_filters( 'pll_context', $class ); if ( ! empty( $class ) ) { /** @phpstan-var class-string<TPLLClass> $class */ $this->init_context( $class, $model ); } } /** * Polylang initialization. * Setups the Polylang Context, loads the modules and init Polylang. * * @since 3.6 * * @param string $class The class name. * @param PLL_Model $model Instance of PLL_Model. * @return PLL_Base * * @phpstan-param class-string<TPLLClass> $class * @phpstan-return TPLLClass */ public function init_context( string $class, PLL_Model $model ): PLL_Base { global $polylang; $links_model = $model->get_links_model(); $polylang = new $class( $links_model ); /** * Fires after Polylang's model init. * This is the best place to register a custom table (see `PLL_Model`'s constructor). * /!\ This hook is fired *before* the $polylang object is available. * /!\ The languages are also not available yet. * * @since 3.4 * * @param PLL_Model $model Polylang model. */ do_action( 'pll_model_init', $model ); $model->maybe_create_language_terms(); /** * Fires after the $polylang object is created and before the API is loaded * * @since 2.0 * * @param object $polylang */ do_action_ref_array( 'pll_pre_init', array( &$polylang ) ); // Loads the API require_once POLYLANG_DIR . '/include/api.php'; // Loads the modules. $load_scripts = glob( POLYLANG_DIR . '/modules/*/load.php', GLOB_NOSORT ); if ( is_array( $load_scripts ) ) { foreach ( $load_scripts as $load_script ) { require_once $load_script; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } } $polylang->init(); /** * Fires after the $polylang object and the API is loaded * * @since 1.7 * * @param object $polylang */ do_action_ref_array( 'pll_init', array( &$polylang ) ); return $polylang; } } include/olt-manager.php 0000644 00000020567 15136114124 0011116 0 ustar 00 <?php /** * @package Polylang */ /** * It is best practice that plugins do nothing before plugins_loaded is fired * so it is what Polylang intends to do * but some plugins load their text domain as soon as loaded, thus before plugins_loaded is fired * this class differs text domain loading until the language is defined * either in a plugins_loaded action or in a wp action ( when the language is set from content on frontend ) * * @since 1.2 */ class PLL_OLT_Manager { /** * Singleton instance * * @var PLL_OLT_Manager|null */ protected static $instance; /** * Stores the default site locale before it is modified. * * @var string|null */ protected $default_locale; /** * Stores all loaded text domains and mo files. * * @var string[][] */ protected $list_textdomains = array(); /** * Stores post types an taxonomies labels to translate. * * @var string[][] */ public $labels = array(); /** * Constructor: setups relevant filters * * @since 1.2 */ public function __construct() { // Allows Polylang to be the first plugin loaded ;-) add_filter( 'pre_update_option_active_plugins', array( $this, 'make_polylang_first' ) ); add_filter( 'pre_update_option_active_sitewide_plugins', array( $this, 'make_polylang_first' ) ); // Overriding load text domain only on front since WP 4.7. if ( is_admin() && ! Polylang::is_ajax_on_front() ) { return; } // Saves the default locale before we start any language manipulation $this->default_locale = get_locale(); // Filters for text domain management add_filter( 'load_textdomain_mofile', array( $this, 'load_textdomain_mofile' ), 10, 2 ); add_filter( 'gettext', array( $this, 'gettext' ), 10, 3 ); add_filter( 'gettext_with_context', array( $this, 'gettext_with_context' ), 10, 4 ); // Loads text domains add_action( 'pll_language_defined', array( $this, 'load_textdomains' ), 2 ); // After PLL_Frontend::pll_language_defined add_action( 'pll_no_language_defined', array( $this, 'load_textdomains' ) ); } /** * Access to the single instance of the class * * @since 1.7 * * @return PLL_OLT_Manager */ public static function instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Loads text domains * * @since 0.1 * * @return void */ public function load_textdomains() { // Our load_textdomain_mofile filter has done its job. let's remove it before calling load_textdomain remove_filter( 'load_textdomain_mofile', array( $this, 'load_textdomain_mofile' ) ); remove_filter( 'gettext', array( $this, 'gettext' ) ); remove_filter( 'gettext_with_context', array( $this, 'gettext_with_context' ) ); $new_locale = get_locale(); // Don't try to save time for en_US as some users have theme written in another language // Now we can load all overridden text domains with the right language if ( ! empty( $this->list_textdomains ) ) { foreach ( $this->list_textdomains as $textdomain ) { // Since WP 4.6, plugins translations are first loaded from wp-content/languages if ( ! load_textdomain( $textdomain['domain'], str_replace( "{$this->default_locale}.mo", "$new_locale.mo", $textdomain['mo'] ) ) ) { // Since WP 3.5 themes may store languages files in /wp-content/languages/themes if ( ! load_textdomain( $textdomain['domain'], WP_LANG_DIR . "/themes/{$textdomain['domain']}-$new_locale.mo" ) ) { // Since WP 3.7 plugins may store languages files in /wp-content/languages/plugins load_textdomain( $textdomain['domain'], WP_LANG_DIR . "/plugins/{$textdomain['domain']}-$new_locale.mo" ); } } } } // First remove taxonomies and post_types labels that we don't need to translate $taxonomies = get_taxonomies( array( '_pll' => true ) ); $post_types = get_post_types( array( '_pll' => true ) ); // We don't need to translate core taxonomies and post types labels when setting the language from the url // As they will be translated when registered the second time if ( ! did_action( 'setup_theme' ) ) { $taxonomies = array_merge( get_taxonomies( array( '_builtin' => true ) ), $taxonomies ); $post_types = array_merge( get_post_types( array( '_builtin' => true ) ), $post_types ); } // Translate labels of post types and taxonomies foreach ( array_diff_key( $GLOBALS['wp_taxonomies'], array_flip( $taxonomies ) ) as $tax ) { $this->translate_labels( $tax ); } foreach ( array_diff_key( $GLOBALS['wp_post_types'], array_flip( $post_types ) ) as $pt ) { $this->translate_labels( $pt ); } // Act only if the language has not been set early ( before default textdomain loading and $wp_locale creation ) if ( did_action( 'after_setup_theme' ) ) { // Reinitializes wp_locale for weekdays and months unset( $GLOBALS['wp_locale'] ); $GLOBALS['wp_locale'] = new WP_Locale(); } /** * Fires after the post types and taxonomies labels have been translated * This allows plugins to translate text the same way we do for post types and taxonomies labels * * @since 1.2 * * @param array $labels list of strings to trnaslate */ do_action_ref_array( 'pll_translate_labels', array( &$this->labels ) ); // Free memory. $this->default_locale = null; $this->list_textdomains = array(); $this->labels = array(); } /** * Saves all text domains in a table for later usage. * It replaces the 'override_load_textdomain' filter previously used. * * @since 2.0.4 * * @param string $mofile The translation file name. * @param string $domain The text domain name. * @return string */ public function load_textdomain_mofile( $mofile, $domain ) { // On multisite, 2 files are sharing the same domain so we need to distinguish them. if ( 'default' === $domain && false !== strpos( $mofile, '/ms-' ) ) { $this->list_textdomains['ms-default'] = array( 'mo' => $mofile, 'domain' => $domain ); } else { $this->list_textdomains[ $domain ] = array( 'mo' => $mofile, 'domain' => $domain ); } return ''; // Hack to prevent WP loading text domains as we will load them all later. } /** * Saves post types and taxonomies labels for a later usage * * @since 0.9 * * @param string $translation not used * @param string $text string to translate * @param string $domain text domain * @return string unmodified $translation */ public function gettext( $translation, $text, $domain ) { if ( is_string( $text ) ) { // Avoid a warning with some buggy plugins which pass an array $this->labels[ $text ] = array( 'domain' => $domain ); } return $translation; } /** * Saves post types and taxonomies labels for a later usage * * @since 0.9 * * @param string $translation not used * @param string $text string to translate * @param string $context some comment to describe the context of string to translate * @param string $domain text domain * @return string unmodified $translation */ public function gettext_with_context( $translation, $text, $context, $domain ) { $this->labels[ $text ] = array( 'domain' => $domain, 'context' => $context ); return $translation; } /** * Translates post types and taxonomies labels once the language is known. * * @since 0.9 * * @param WP_Post_Type|WP_Taxonomy $type Either a post type or a taxonomy. * @return void */ public function translate_labels( $type ) { // Use static array to avoid translating several times the same ( default ) labels static $translated = array(); foreach ( (array) $type->labels as $key => $label ) { if ( is_string( $label ) && isset( $this->labels[ $label ] ) ) { if ( empty( $translated[ $label ] ) ) { // PHPCS:disable WordPress.WP.I18n $type->labels->$key = $translated[ $label ] = isset( $this->labels[ $label ]['context'] ) ? _x( $label, $this->labels[ $label ]['context'], $this->labels[ $label ]['domain'] ) : __( $label, $this->labels[ $label ]['domain'] ); // PHPCS:enable } else { $type->labels->$key = $translated[ $label ]; } } } } /** * Allows Polylang to be the first plugin loaded ;-). * * @since 1.2 * * @param string[] $plugins List of active plugins. * @return string[] List of active plugins. */ public function make_polylang_first( $plugins ) { if ( $key = array_search( POLYLANG_BASENAME, $plugins ) ) { unset( $plugins[ $key ] ); array_unshift( $plugins, POLYLANG_BASENAME ); } return $plugins; } } include/static-pages.php 0000644 00000015374 15136114124 0011274 0 ustar 00 <?php /** * @package Polylang */ /** * Base class to manage the static front page and the page for posts. * * @since 1.8 */ class PLL_Static_Pages { /** * Id of the page on front. * * @var int */ public $page_on_front = 0; /** * Id of the page for posts. * * @var int */ public $page_for_posts = 0; /** * @var PLL_Model */ protected $model; /** * Current language. * * @var PLL_Language|null */ protected $curlang; /** * Constructor: setups filters and actions. * * @since 1.8 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->model = &$polylang->model; $this->curlang = &$polylang->curlang; $this->init(); add_filter( 'pll_additional_language_data', array( $this, 'set_static_pages' ), 5, 2 ); // Before PLL_Links_Model. // Clean the languages cache when editing page of front, page for posts. add_action( 'update_option_show_on_front', array( $this, 'clean_cache' ) ); add_action( 'update_option_page_on_front', array( $this, 'clean_cache' ) ); add_action( 'update_option_page_for_posts', array( $this, 'clean_cache' ) ); // Refresh rewrite rules when the page on front is modified. add_action( 'update_option_page_on_front', 'flush_rewrite_rules' ); // Add option filters when the current language is defined add_action( 'pll_language_defined', array( $this, 'pll_language_defined' ) ); // Modifies the page link in case the front page is not in the default language. add_filter( 'page_link', array( $this, 'page_link' ), 20, 2 ); // OEmbed. add_filter( 'oembed_request_post_id', array( $this, 'oembed_request_post_id' ), 10, 2 ); } /** * Stores the page on front and page for posts ids. * * @since 1.8 * * @return void */ public function init() { $this->page_on_front = 0; $this->page_for_posts = 0; if ( 'page' !== get_option( 'show_on_front' ) ) { return; } $page_on_front = get_option( 'page_on_front' ); if ( is_numeric( $page_on_front ) ) { $this->page_on_front = (int) $page_on_front; } $page_for_posts = get_option( 'page_for_posts' ); if ( is_numeric( $page_for_posts ) ) { $this->page_for_posts = (int) $page_for_posts; } } /** * Returns the ID of the static page translation. * * @since 3.4 * * @param string $static_page Static page option name; `page_on_front` or `page_for_posts`. * @param array $language Language data. * @return int */ protected function get_translation( $static_page, $language ) { $translations = $this->model->post->get_raw_translations( $this->$static_page ); // When the current static page doesn't have any translation, we must return itself for its language. if ( empty( $translations ) ) { $page_lang = $this->model->post->get_object_term( $this->$static_page, $this->model->post->get_tax_language() ); if ( ! empty( $page_lang ) && $page_lang->slug === $language['slug'] ) { return $this->$static_page; } } if ( ! isset( $translations[ $language['slug'] ] ) ) { return 0; } return $translations[ $language['slug'] ]; } /** * Adds `page_on_front` and `page_for_posts` properties to language data before the object is created. * * @since 3.4 * * @param array $additional_data Array of language additional data. * @param array $language Language data. * @return array Language data with additional `page_on_front` and `page_for_posts` options added. */ public function set_static_pages( $additional_data, $language ) { $additional_data['page_on_front'] = $this->get_translation( 'page_on_front', $language ); $additional_data['page_for_posts'] = $this->get_translation( 'page_for_posts', $language ); return $additional_data; } /** * Cleans the language cache and resets the internal properties when options are updated. * * @since 3.4 * * @return void */ public function clean_cache() { $this->model->clean_languages_cache(); $this->init(); } /** * Init the hooks that filter the "page on front" and "page for posts" options. * * @since 3.3 * * @return void */ public function pll_language_defined() { // Translates page for posts and page on front. add_filter( 'option_page_on_front', array( $this, 'translate_page_on_front' ) ); add_filter( 'option_page_for_posts', array( $this, 'translate_page_for_posts' ) ); } /** * Translates the page on front option. * * @since 1.8 * @since 3.3 Was previously defined in PLL_Frontend_Static_Pages. * * @param int $page_id ID of the page on front. * @return int */ public function translate_page_on_front( $page_id ) { if ( empty( $this->curlang->page_on_front ) ) { return $page_id; } if ( doing_action( 'switch_blog' ) || doing_action( 'before_delete_post' ) || doing_action( 'wp_trash_post' ) ) { /* * Don't attempt to translate in a 'switch_blog' action as there is a risk to call this function while initializing the languages cache. * Don't translate while deleting a post or it will mess up `_reset_front_page_settings_for_post()`. */ return $page_id; } return $this->curlang->page_on_front; } /** * Translates the page for posts option. * * @since 1.8 * * @param int $page_id ID of the page for posts. * @return int */ public function translate_page_for_posts( $page_id ) { if ( empty( $this->curlang->page_for_posts ) ) { return $page_id; } if ( doing_action( 'switch_blog' ) || doing_action( 'before_delete_post' ) || doing_action( 'wp_trash_post' ) ) { /* * Don't attempt to translate in a 'switch_blog' action as there is a risk to call this function while initializing the languages cache. * Don't translate while deleting a post or it will mess up `_reset_front_page_settings_for_post()`. */ return $page_id; } return $this->curlang->page_for_posts; } /** * Modifies the page link in case the front page is not in the default language. * * @since 0.7.2 * * @param string $link The link to the page. * @param int $id The post ID of the page. * @return string Modified link. */ public function page_link( $link, $id ) { $lang = $this->model->post->get_language( $id ); if ( $lang && $id == $lang->page_on_front ) { return $lang->get_home_url(); } return $link; } /** * Fixes the oembed for the translated static front page * when the language page is redirected to the front page. * * @since 2.6 * * @param int $post_id The post ID. * @param string $url The requested URL. * @return int */ public function oembed_request_post_id( $post_id, $url ) { foreach ( $this->model->get_languages_list() as $lang ) { if ( is_string( $lang->get_home_url() ) && trailingslashit( $url ) === trailingslashit( $lang->get_home_url() ) ) { return (int) $lang->page_on_front; } } return $post_id; } } include/links-permalinks.php 0000644 00000013171 15136114124 0012164 0 ustar 00 <?php /** * @package Polylang */ /** * Links model base class when using pretty permalinks. * * @since 1.6 */ abstract class PLL_Links_Permalinks extends PLL_Links_Model { /** * Tells this child class of PLL_Links_Model is for pretty permalinks. * * @var bool */ public $using_permalinks = true; /** * The name of the index file which is the entry point to all requests. * We need this before the global $wp_rewrite is created. * Also hardcoded in WP_Rewrite. * * @var string */ protected $index = 'index.php'; /** * The prefix for all permalink structures. * * @var string */ protected $root; /** * Whether to add trailing slashes. * * @var bool */ protected $use_trailing_slashes; /** * The name of the rewrite rules to always modify. * * @var string[] */ protected $always_rewrite = array( 'date', 'root', 'comments', 'search', 'author' ); /** * Constructor. * * @since 1.8 * * @param PLL_Model $model PLL_Model instance. */ public function __construct( &$model ) { parent::__construct( $model ); // Inspired by WP_Rewrite. $permalink_structure = get_option( 'permalink_structure' ); $this->root = preg_match( '#^/*' . $this->index . '#', $permalink_structure ) ? $this->index . '/' : ''; $this->use_trailing_slashes = ( '/' == substr( $permalink_structure, -1, 1 ) ); } /** * Initializes permalinks. * * @since 3.5 * * @return void */ public function init() { parent::init(); if ( did_action( 'wp_loaded' ) ) { $this->do_prepare_rewrite_rules(); } else { add_action( 'wp_loaded', array( $this, 'do_prepare_rewrite_rules' ), 9 ); // Just before WordPress callback `WP_Rewrite::flush_rules()`. } } /** * Fires our own action telling Polylang plugins * and third parties are able to prepare rewrite rules. * * @since 3.5 * * @return void */ public function do_prepare_rewrite_rules() { self::$can_filter_rewrite_rules = true; /** * Tells when Polylang is able to prepare rewrite rules filters. * Action fired right after `wp_loaded` and just before WordPress `WP_Rewrite::flush_rules()` callback. * * @since 3.5 * * @param PLL_Links_Permalinks $links Current links object. */ do_action( 'pll_prepare_rewrite_rules', $this ); } /** * Returns the link to the first page when using pretty permalinks. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_paged_from_link( $url ) { /** * Filters an url after the paged part has been removed. * * @since 2.0.6 * * @param string $modified_url The link to the first page. * @param string $original_url The link to the original paged page. */ return apply_filters( 'pll_remove_paged_from_link', preg_replace( '#/page/[0-9]+/?#', $this->use_trailing_slashes ? '/' : '', $url ), $url ); } /** * Returns the link to the paged page when using pretty permalinks. * * @since 1.5 * * @param string $url The url to modify. * @param int $page The page number. * @return string The modified url. */ public function add_paged_to_link( $url, $page ) { /** * Filters an url after the paged part has been added. * * @since 2.0.6 * * @param string $modified_url The link to the paged page. * @param string $original_url The link to the original first page. * @param int $page The page number. */ return apply_filters( 'pll_add_paged_to_link', user_trailingslashit( trailingslashit( $url ) . 'page/' . $page, 'paged' ), $url, $page ); } /** * Returns the home url in a given language. * * @since 1.3.1 * @since 3.4 Accepts now a language slug. * * @param PLL_Language|string $language Language object or slug. * @return string */ public function home_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } return trailingslashit( parent::home_url( $language ) ); } /** * Returns the static front page url. * * @since 1.8 * @since 3.4 Accepts now an array of language properties. * * @param PLL_Language|array $language Language object or array of language properties. * @return string The static front page url. */ public function front_page_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->to_array(); } if ( $this->options['hide_default'] && $language['is_default'] ) { return trailingslashit( $this->home ); } $url = home_url( $this->root . get_page_uri( $language['page_on_front'] ) ); $url = $this->use_trailing_slashes ? trailingslashit( $url ) : untrailingslashit( $url ); return $this->options['force_lang'] ? $this->add_language_to_link( $url, $language['slug'] ) : $url; } /** * Prepares rewrite rules filters. * * @since 1.6 * * @return string[] */ public function get_rewrite_rules_filters() { // Make sure that we have the right post types and taxonomies. $types = array_values( array_merge( $this->model->get_translated_post_types(), $this->model->get_translated_taxonomies(), $this->model->get_filtered_taxonomies() ) ); $types = array_merge( $this->always_rewrite, $types ); /** * Filters the list of rewrite rules filters to be used by Polylang. * * @since 0.8.1 * * @param array $types The list of filters (without '_rewrite_rules' at the end). */ return apply_filters( 'pll_rewrite_rules', $types ); } /** * Removes hooks to filter rewrite rules, called when switching blog @see {PLL_Base::switch_blog()}. * * @since 3.5 * * @return void */ public function remove_filters() { parent::remove_filters(); remove_all_actions( 'pll_prepare_rewrite_rules' ); } } include/filters-links.php 0000644 00000013066 15136114124 0011472 0 ustar 00 <?php /** * @package Polylang */ /** * Manages links filters needed on both frontend and admin * * @since 1.8 */ class PLL_Filters_Links { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Instance of a child class of PLL_Links_Model. * * @var PLL_Links_Model */ public $links_model; /** * @var PLL_Links|null */ public $links; /** * Current language. * * @var PLL_Language|null */ public $curlang; /** * Constructor. * * @since 1.8 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->links = &$polylang->links; $this->links_model = &$polylang->links_model; $this->model = &$polylang->model; $this->options = &$polylang->options; $this->curlang = &$polylang->curlang; // Low priority on links filters to come after any other modifications. if ( $this->options['force_lang'] ) { add_filter( 'post_link', array( $this, 'post_type_link' ), 20, 2 ); add_filter( '_get_page_link', array( $this, '_get_page_link' ), 20, 2 ); } add_filter( 'post_type_link', array( $this, 'post_type_link' ), 20, 2 ); add_filter( 'term_link', array( $this, 'term_link' ), 20, 3 ); if ( $this->options['force_lang'] > 0 ) { add_filter( 'attachment_link', array( $this, 'attachment_link' ), 20, 2 ); } // Keeps the preview post link on default domain when using multiple domains and SSO is not available. if ( 3 === $this->options['force_lang'] && ! class_exists( 'PLL_Xdata_Domain' ) ) { add_filter( 'preview_post_link', array( $this, 'preview_post_link' ), 20 ); } // Rewrites post types archives links to filter them by language. add_filter( 'post_type_archive_link', array( $this, 'post_type_archive_link' ), 20, 2 ); } /** * Modifies page links * * @since 1.7 * * @param string $link post link * @param int $post_id post ID * @return string modified post link */ public function _get_page_link( $link, $post_id ) { // /!\ WP does not use pretty permalinks for preview return false !== strpos( $link, 'preview=true' ) && false !== strpos( $link, 'page_id=' ) ? $link : $this->links_model->switch_language_in_link( $link, $this->model->post->get_language( $post_id ) ); } /** * Modifies attachment links * * @since 1.6.2 * * @param string $link attachment link * @param int $post_id attachment link * @return string modified attachment link */ public function attachment_link( $link, $post_id ) { return wp_get_post_parent_id( $post_id ) ? $link : $this->links_model->switch_language_in_link( $link, $this->model->post->get_language( $post_id ) ); } /** * Modifies custom posts links. * * @since 1.6 * * @param string $link Post link. * @param WP_Post $post Post object. * @return string Modified post link. */ public function post_type_link( $link, $post ) { // /!\ WP does not use pretty permalinks for preview if ( ( false === strpos( $link, 'preview=true' ) || false === strpos( $link, 'p=' ) ) && $this->model->is_translated_post_type( $post->post_type ) ) { $lang = $this->model->post->get_language( $post->ID ); $link = $this->options['force_lang'] ? $this->links_model->switch_language_in_link( $link, $lang ) : $link; /** * Filters a post or custom post type link. * * @since 1.6 * * @param string $link The post link. * @param PLL_Language $lang The current language. * @param WP_Post $post The post object. */ $link = apply_filters( 'pll_post_type_link', $link, $lang, $post ); } return $link; } /** * Modifies term links. * * @since 0.7 * * @param string $link Term link. * @param WP_Term $term Term object. * @param string $tax Taxonomy name; * @return string Modified term link. */ public function term_link( $link, $term, $tax ) { if ( $this->model->is_translated_taxonomy( $tax ) ) { $lang = $this->model->term->get_language( $term->term_id ); $link = $this->options['force_lang'] ? $this->links_model->switch_language_in_link( $link, $lang ) : $link; /** * Filter a term link * * @since 1.6 * * @param string $link The term link. * @param PLL_Language $lang The current language. * @param WP_Term $term The term object. */ return apply_filters( 'pll_term_link', $link, $lang, $term ); } // In case someone calls get_term_link for the 'language' taxonomy. if ( 'language' === $tax ) { $lang = $this->model->get_language( $term->term_id ); if ( $lang ) { return $this->links_model->home_url( $lang ); } } return $link; } /** * Keeps the preview post link on default domain when using multiple domains. * * @since 1.6.1 * * @param string $url URL used for the post preview. * @return string The modified url. */ public function preview_post_link( $url ) { return $this->links_model->remove_language_from_link( $url ); } /** * Modifies the post type archive links to add the language parameter * only if the post type is translated. * * The filter was originally only on frontend but is needed on admin too for * compatibility with the archive link of the ACF link field since ACF 5.4.0. * * @since 1.7.6 * * @param string $link The post type archive permalink. * @param string $post_type Post type name. * @return string */ public function post_type_archive_link( $link, $post_type ) { return $this->model->is_translated_post_type( $post_type ) && 'post' !== $post_type ? $this->links_model->switch_language_in_link( $link, $this->curlang ) : $link; } } include/walker-dropdown.php 0000644 00000006722 15136114124 0012024 0 ustar 00 <?php /** * @package Polylang */ /** * Displays languages in a dropdown list * * @since 1.2 * @since 3.4 Extends `PLL_Walker` now. */ class PLL_Walker_Dropdown extends PLL_Walker { /** * Database fields to use. * * @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields. * * @var string[] */ public $db_fields = array( 'parent' => 'parent', 'id' => 'id' ); /** * Outputs one element. * * @since 1.2 * * @param string $output Passed by reference. Used to append additional content. * @param stdClass $element The data object. * @param int $depth Depth of the item. * @param array $args An array of additional arguments. * @param int $current_object_id ID of the current item. * @return void */ public function start_el( &$output, $element, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $value_type = $args['value']; $output .= sprintf( "\t" . '<option value="%1$s"%2$s%3$s>%4$s</option>' . "\n", 'url' === $value_type ? esc_url( $element->$value_type ) : esc_attr( $element->$value_type ), ! empty( $element->locale ) ? sprintf( ' lang="%s"', esc_attr( $element->locale ) ) : '', selected( isset( $args['selected'] ) && $args['selected'] === $element->$value_type, true, false ), esc_html( $element->name ) ); } /** * Starts the output of the dropdown list * * @since 1.2 * @since 2.6.7 Use $max_depth and ...$args parameters to follow the move of WP 5.3 * * List of parameters accepted in $args: * * flag => display the selected language flag in front of the dropdown if set to 1, defaults to 0 * value => the language field to use as value attribute, defaults to 'slug' * selected => the selected value, mandatory * name => the select name attribute, defaults to 'lang_choice' * id => the select id attribute, defaults to $args['name'] * class => the class attribute * disabled => disables the dropdown if set to 1 * * @param array $elements An array of `PLL_language` or `stdClass` elements. * @param int $max_depth The maximum hierarchical depth. * @param mixed ...$args Additional arguments. * @return string The hierarchical item output. * * @phpstan-param array<PLL_Language|stdClass> $elements */ public function walk( $elements, $max_depth, ...$args ) { // // phpcs:ignore WordPressVIPMinimum.Classes.DeclarationCompatibility.DeclarationCompatibility $output = ''; $this->maybe_fix_walk_args( $max_depth, $args ); $args = wp_parse_args( $args, array( 'value' => 'slug', 'name' => 'lang_choice' ) ); if ( ! empty( $args['flag'] ) ) { $current = wp_list_filter( $elements, array( $args['value'] => $args['selected'] ) ); $lang = reset( $current ); $output = sprintf( '<span class="pll-select-flag">%s</span>', empty( $lang->flag ) ? esc_html( $lang->slug ) : $lang->flag ); } $output .= sprintf( '<select name="%1$s"%2$s%3$s%4$s>' . "\n" . '%5$s' . "\n" . '</select>' . "\n", esc_attr( $args['name'] ), isset( $args['id'] ) && ! $args['id'] ? '' : ' id="' . ( empty( $args['id'] ) ? esc_attr( $args['name'] ) : esc_attr( $args['id'] ) ) . '"', empty( $args['class'] ) ? '' : ' class="' . esc_attr( $args['class'] ) . '"', disabled( empty( $args['disabled'] ), false, false ), parent::walk( $elements, $max_depth, $args ) ); return $output; } } include/language-factory.php 0000644 00000022477 15136114124 0012142 0 ustar 00 <?php /** * @package Polylang */ /** * PLL_Language factory. * * @since 3.4 * * @phpstan-import-type LanguageData from PLL_Language */ class PLL_Language_Factory { /** * Predefined languages. * * @var array[] * * @phpstan-var array<string, array<string, string>> */ private static $languages; /** * Polylang's options. * * @var array */ private $options; /** * Constructor. * * @since 3.4 * * @param array $options Array of Poylang's options passed by reference. * @return void */ public function __construct( &$options ) { $this->options = &$options; } /** * Returns a language object matching the given data, looking up in cached transient. * * @since 3.4 * * @param array $language_data Language object properties stored as an array. See `PLL_Language::__construct()` * for information on accepted properties. * * @return PLL_Language A language object if given data pass sanitization. * * @phpstan-param LanguageData $language_data */ public function get( $language_data ) { return new PLL_Language( $this->sanitize_data( $language_data ) ); } /** * Returns a language object based on terms. * * @since 3.4 * * @param WP_Term[] $terms List of language terms, with the language taxonomy names as array keys. * `language` is a mandatory key for the object to be created, * `term_language` should be too in a fully operational environment. * @return PLL_Language|null Language object on success, `null` on failure. * * @phpstan-param array{language?:WP_Term}&array<string, WP_Term> $terms */ public function get_from_terms( array $terms ) { if ( ! isset( $terms['language'] ) ) { return null; } $languages = $this->get_languages(); $data = array( 'name' => $terms['language']->name, 'slug' => $terms['language']->slug, 'term_group' => $terms['language']->term_group, 'term_props' => array(), 'is_default' => $this->options['default_lang'] === $terms['language']->slug, ); foreach ( $terms as $term ) { $data['term_props'][ $term->taxonomy ] = array( 'term_id' => $term->term_id, 'term_taxonomy_id' => $term->term_taxonomy_id, 'count' => $term->count, ); } // The description fields can contain any property. $description = maybe_unserialize( $terms['language']->description ); if ( is_array( $description ) ) { $description = array_intersect_key( $description, array( 'locale' => null, 'rtl' => null, 'flag_code' => null, 'active' => null, 'fallbacks' => null ) ); foreach ( $description as $prop => $value ) { if ( 'rtl' === $prop ) { $data['is_rtl'] = $value; } else { $data[ $prop ] = $value; } } } if ( ! empty( $data['locale'] ) ) { if ( isset( $languages[ $data['locale'] ]['w3c'] ) ) { $data['w3c'] = $languages[ $data['locale'] ]['w3c']; } else { $data['w3c'] = str_replace( '_', '-', $data['locale'] ); } if ( isset( $languages[ $data['locale'] ]['facebook'] ) ) { $data['facebook'] = $languages[ $data['locale'] ]['facebook']; } } $flag_props = $this->get_flag( $data['flag_code'], $data['name'], $data['slug'], $data['locale'] ); $data = array_merge( $data, $flag_props ); $additional_data = array(); /** * Filters additional data to add to the language before it is created. * * `home_url`, `search_url`, `page_on_front` and `page_for_posts` are only allowed. * * @since 3.4 * * @param array $additional_data. * @param array $data Language data. * * @phpstan-param array<non-empty-string, mixed> $additional_data * @phpstan-param non-empty-array<non-empty-string, mixed> $data */ $additional_data = apply_filters( 'pll_additional_language_data', $additional_data, $data ); $allowed_additional_data = array( 'home_url' => '', 'search_url' => '', 'page_on_front' => 0, 'page_for_posts' => 0, ); $data = array_merge( $data, array_intersect_key( $additional_data, $allowed_additional_data ) ); return new PLL_Language( $this->sanitize_data( $data ) ); } /** * Sanitizes data, to be ready to be used in the constructor. * This doesn't verify that the language terms exist. * * @since 3.4 * * @param array $data Data to process. See `PLL_Language::__construct()` for information on accepted data. * @return array Sanitized Data. * * @phpstan-return LanguageData */ private function sanitize_data( array $data ) { foreach ( $data['term_props'] as $tax => $props ) { $data['term_props'][ $tax ] = array_map( 'absint', $props ); } $data['is_rtl'] = ! empty( $data['is_rtl'] ) ? 1 : 0; $positive_fields = array( 'term_group', 'page_on_front', 'page_for_posts' ); foreach ( $positive_fields as $field ) { $data[ $field ] = ! empty( $data[ $field ] ) ? absint( $data[ $field ] ) : 0; } $data['active'] = isset( $data['active'] ) ? (bool) $data['active'] : true; if ( array_key_exists( 'fallbacks', $data ) && ! is_array( $data['fallbacks'] ) ) { unset( $data['fallbacks'] ); } /** * @var LanguageData */ return $data; } /** * Returns predefined languages data. * * @since 3.4 * * @return array[] * * @phpstan-return array<string, array<string, string>> */ private function get_languages() { if ( empty( self::$languages ) ) { self::$languages = include POLYLANG_DIR . '/settings/languages.php'; } return self::$languages; } /** * Creates flag_url and flag language properties. Also takes care of custom flag. * * @since 1.2 * @since 3.4 Moved from `PLL_Language`to `PLL_Language_Factory` and renamed * in favor of `get_flag()` (formerly `set_flag()`). * * @param string $flag_code Flag code. * @param string $name Language name. * @param string $slug Language slug. * @param string $locale Language locale. * @return array { * Array of the flag properties. * @type string $flag_url URL of the flag. * @type string $flag HTML markup of the flag. * @type string $custom_flag_url Optional. URL of the custom flag if it exists. * @type string $custom_flag Optional. HTML markup of the custom flag if it exists. * } * * @phpstan-return array{ * flag_url: string, * flag: string, * custom_flag_url?: non-empty-string, * custom_flag?: non-empty-string * } */ private function get_flag( $flag_code, $name, $slug, $locale ) { $flags = array( 'flag' => PLL_Language::get_flag_informations( $flag_code ), ); // Custom flags? $directories = array( PLL_LOCAL_DIR, get_stylesheet_directory() . '/polylang', get_template_directory() . '/polylang', ); foreach ( $directories as $dir ) { if ( is_readable( $file = "{$dir}/{$locale}.png" ) || is_readable( $file = "{$dir}/{$locale}.jpg" ) || is_readable( $file = "{$dir}/{$locale}.jpeg" ) || is_readable( $file = "{$dir}/{$locale}.svg" ) ) { $flags['custom_flag'] = array( 'url' => content_url( '/' . str_replace( WP_CONTENT_DIR, '', $file ) ), ); break; } } /** * Filters the custom flag information. * * @since 2.4 * * @param array|null $flag { * Information about the custom flag. * * @type string $url Flag url. * @type string $src Optional, src attribute value if different of the url, for example if base64 encoded. * @type int $width Optional, flag width in pixels. * @type int $height Optional, flag height in pixels. * } * @param string $code Flag code. */ $flags['custom_flag'] = apply_filters( 'pll_custom_flag', empty( $flags['custom_flag'] ) ? null : $flags['custom_flag'], $flag_code ); if ( ! empty( $flags['custom_flag']['url'] ) ) { if ( empty( $flags['custom_flag']['src'] ) ) { $flags['custom_flag']['src'] = esc_url( set_url_scheme( $flags['custom_flag']['url'], 'relative' ) ); } $flags['custom_flag']['url'] = esc_url_raw( $flags['custom_flag']['url'] ); } else { unset( $flags['custom_flag'] ); } /** * Filters the flag title attribute. * Defaults to the language name. * * @since 0.7 * * @param string $title The flag title attribute. * @param string $slug The language code. * @param string $locale The language locale. */ $title = apply_filters( 'pll_flag_title', $name, $slug, $locale ); $return = array(); /** * @var array{ * flag: array{ * url: string, * src: string, * width?: positive-int, * height?: positive-int * }, * custom_flag?: array{ * url: non-empty-string, * src: non-empty-string, * width?: positive-int, * height?: positive-int * } * } $flags */ foreach ( $flags as $key => $flag ) { $return[ "{$key}_url" ] = $flag['url']; /** * Filters the html markup of a flag. * * @since 1.0.2 * * @param string $flag Html markup of the flag or empty string. * @param string $slug Language code. */ $return[ $key ] = apply_filters( 'pll_get_flag', PLL_Language::get_flag_html( $flag, $title, $name ), $slug ); } /** * @var array{ * flag_url: string, * flag: string, * custom_flag_url?: non-empty-string, * custom_flag?: non-empty-string * } $return */ return $return; } } include/db-tools.php 0000644 00000002016 15136114124 0010420 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Small set of tools to work with the database. * * @since 3.2 */ class PLL_Db_Tools { /** * Changes an array of values into a comma separated list, ready to be used in a `IN ()` clause. * Only string and integers and supported for now. * * @since 3.2 * * @param (int|string)[] $values An array of values. * @return string A comma separated list of values. */ public static function prepare_values_list( $values ) { $values = array_map( array( __CLASS__, 'prepare_value' ), (array) $values ); return implode( ',', $values ); } /** * Wraps a value in escaped double quotes or casts as an integer. * Only string and integers and supported for now. * * @since 3.2 * * @global wpdb $wpdb * * @param int|string $value A value. * @return int|string */ public static function prepare_value( $value ) { if ( ! is_numeric( $value ) ) { return $GLOBALS['wpdb']->prepare( '%s', $value ); } return (int) $value; } } include/mo.php 0000644 00000003744 15136114124 0007321 0 ustar 00 <?php /** * @package Polylang */ /** * Manages strings translations storage * * @since 1.2 * @since 2.1 Stores the strings in a post meta instead of post content to avoid unserialize issues (See #63) * @since 3.4 Stores the strings into language taxonomy term meta instead of a post meta. */ class PLL_MO extends MO { /** * Writes the strings into a term meta. * * @since 1.2 * * @param PLL_Language $lang The language in which we want to export strings. * @return void */ public function export_to_db( $lang ) { /* * It would be convenient to store the whole object, but it would take a huge space in DB. * So let's keep only the strings in an array. * The strings are slashed to avoid breaking slashed strings in update_term_meta. * @see https://codex.wordpress.org/Function_Reference/update_post_meta#Character_Escaping. */ $strings = array(); foreach ( $this->entries as $entry ) { if ( '' !== $entry->singular ) { $strings[] = wp_slash( array( $entry->singular, $this->translate( $entry->singular ) ) ); } } update_term_meta( $lang->term_id, '_pll_strings_translations', $strings ); } /** * Reads a PLL_MO object from the term meta. * * @since 1.2 * @since 3.4 Reads a PLL_MO from the term meta. * * @param PLL_Language $lang The language in which we want to get strings. * @return void */ public function import_from_db( $lang ) { $this->set_header( 'Language', $lang->slug ); $strings = get_term_meta( $lang->term_id, '_pll_strings_translations', true ); if ( empty( $strings ) || ! is_array( $strings ) ) { return; } foreach ( $strings as $msg ) { $entry = $this->make_entry( $msg[0], $msg[1] ); if ( '' !== $entry->singular ) { $this->add_entry( $entry ); } } } /** * Deletes a string * * @since 2.9 * * @param string $string The source string to remove from the translations. * @return void */ public function delete_entry( $string ) { unset( $this->entries[ $string ] ); } } include/links-default.php 0000644 00000006034 15136114124 0011443 0 ustar 00 <?php /** * @package Polylang */ /** * Links model for the default permalinks * for example mysite.com/?somevar=something&lang=en. * * @since 1.2 */ class PLL_Links_Default extends PLL_Links_Model { /** * Tells this child class of PLL_Links_Model does not use pretty permalinks. * * @var bool */ public $using_permalinks = false; /** * Adds the language code in a url. * * @since 1.2 * @since 3.4 Accepts now a language slug. * * @param string $url The url to modify. * @param PLL_Language|string|false $language Language object or slug. * @return string The modified url. */ public function add_language_to_link( $url, $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } return empty( $language ) || ( $this->options['hide_default'] && $this->options['default_lang'] === $language ) ? $url : add_query_arg( 'lang', $language, $url ); } /** * Removes the language information from an url. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_language_from_link( $url ) { return remove_query_arg( 'lang', $url ); } /** * Returns the link to the first page. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_paged_from_link( $url ) { return remove_query_arg( 'paged', $url ); } /** * Returns the link to the paged page. * * @since 1.5 * * @param string $url The url to modify. * @param int $page The page number. * @return string The modified url. */ public function add_paged_to_link( $url, $page ) { return add_query_arg( array( 'paged' => $page ), $url ); } /** * Gets the language slug from the url if present. * * @since 1.2 * @since 2.0 Add the $url argument. * * @param string $url Optional, defaults to the current url. * @return string Language slug. */ public function get_language_from_url( $url = '' ) { if ( empty( $url ) ) { $url = pll_get_requested_url(); } $pattern = sprintf( '#[?&]lang=(?<lang>%s)(?:$|&)#', implode( '|', $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) ); return preg_match( $pattern, $url, $matches ) ? $matches['lang'] : ''; // $matches['lang'] is the slug of the requested language. } /** * Returns the static front page url in the given language. * * @since 1.8 * @since 3.4 Accepts now an array of language properties. * * @param PLL_Language|array $language Language object or array of language properties. * @return string The static front page url. */ public function front_page_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->to_array(); } if ( $this->options['hide_default'] && $language['is_default'] ) { return trailingslashit( $this->home ); } $url = home_url( '/?page_id=' . $language['page_on_front'] ); return $this->options['force_lang'] ? $this->add_language_to_link( $url, $language['slug'] ) : $url; } } include/language.php 0000644 00000043604 15136114124 0010470 0 ustar 00 <?php /** * @package Polylang */ /** * A language object is made of two terms in 'language' and 'term_language' taxonomies. * Manipulating only one object per language instead of two terms should make things easier. * * @since 1.2 * @immutable * * @phpstan-type LanguagePropData array{ * term_id: positive-int, * term_taxonomy_id: positive-int, * count: int<0, max> * } * @phpstan-type LanguageData array{ * term_props: array{ * language: LanguagePropData, * }&array<non-empty-string, LanguagePropData>, * name: non-empty-string, * slug: non-empty-string, * locale: non-empty-string, * w3c: non-empty-string, * flag_code: non-empty-string, * term_group: int, * is_rtl: int<0, 1>, * facebook?: string, * home_url: non-empty-string, * search_url: non-empty-string, * host: non-empty-string, * flag_url: non-empty-string, * flag: non-empty-string, * custom_flag_url?: string, * custom_flag?: string, * page_on_front: int<0, max>, * page_for_posts: int<0, max>, * active: bool, * fallbacks?: array<non-empty-string>, * is_default: bool * } */ class PLL_Language extends PLL_Language_Deprecated { /** * Language name. Ex: English. * * @var string * * @phpstan-var non-empty-string */ public $name; /** * Language code used in URL. Ex: en. * * @var string * * @phpstan-var non-empty-string */ public $slug; /** * Order of the language when displayed in a list of languages. * * @var int */ public $term_group; /** * ID of the term in 'language' taxonomy. * Duplicated from `$this->term_props['language']['term_id'], * but kept to facilitate the use of it. * * @var int * * @phpstan-var int<1, max> */ public $term_id; /** * WordPress language locale. Ex: en_US. * * @var string * * @phpstan-var non-empty-string */ public $locale; /** * 1 if the language is rtl, 0 otherwise. * * @var int * * @phpstan-var int<0, 1> */ public $is_rtl; /** * W3C locale. * * @var string * * @phpstan-var non-empty-string */ public $w3c; /** * Facebook locale. * * @var string */ public $facebook = ''; /** * Home URL in this language. * * @var string * * @phpstan-var non-empty-string */ private $home_url; /** * Home URL to use in search forms. * * @var string * * @phpstan-var non-empty-string */ private $search_url; /** * Host corresponding to this language. * * @var string * * @phpstan-var non-empty-string */ public $host; /** * ID of the page on front in this language (set from pll_additional_language_data filter). * * @var int * * @phpstan-var int<0, max> */ public $page_on_front = 0; /** * ID of the page for posts in this language (set from pll_additional_language_data filter). * * @var int * * @phpstan-var int<0, max> */ public $page_for_posts = 0; /** * Code of the flag. * * @var string * * @phpstan-var non-empty-string */ public $flag_code; /** * URL of the flag. Always set to the main domain. * * @var string * * @phpstan-var non-empty-string */ public $flag_url; /** * HTML markup of the flag. * * @var string * * @phpstan-var non-empty-string */ public $flag; /** * URL of the custom flag if it exists. Always set to the main domain. * * @var string */ public $custom_flag_url = ''; /** * HTML markup of the custom flag if it exists. * * @var string */ public $custom_flag = ''; /** * Whether or not the language is active. Default `true`. * * @var bool */ public $active = true; /** * List of WordPress language locales. Ex: array( 'en_GB' ). * * @var string[] * * @phpstan-var list<non-empty-string> */ public $fallbacks = array(); /** * Whether the language is the default one. * * @var bool */ public $is_default; /** * Stores language term properties (like term IDs and counts) for each language taxonomy (`language`, * `term_language`, etc). * This stores the values of the properties `$term_id` + `$term_taxonomy_id` + `$count` (`language`), `$tl_term_id` * + `$tl_term_taxonomy_id` + `$tl_count` (`term_language`), and the `term_id` + `term_taxonomy_id` + `count` for * other language taxonomies. * * @var array[] Array keys are language term names. * * @example array( * 'language' => array( * 'term_id' => 7, * 'term_taxonomy_id' => 8, * 'count' => 11, * ), * 'term_language' => array( * 'term_id' => 11, * 'term_taxonomy_id' => 12, * 'count' => 6, * ), * 'foo_language' => array( * 'term_id' => 33, * 'term_taxonomy_id' => 34, * 'count' => 0, * ), * ) * * @phpstan-var array{ * language: LanguagePropData, * } * &array<non-empty-string, LanguagePropData> */ protected $term_props; /** * Constructor: builds a language object given the corresponding data. * * @since 1.2 * @since 3.4 Only accepts one argument. * * @param array $language_data { * Language object properties stored as an array. * * @type array[] $term_props An array of language term properties. Array keys are language taxonomy names * (`language` and `term_language` are mandatory), array values are arrays of * language term properties (`term_id`, `term_taxonomy_id`, and `count`). * @type string $name Language name. Ex: English. * @type string $slug Language code used in URL. Ex: en. * @type string $locale WordPress language locale. Ex: en_US. * @type string $w3c W3C locale. * @type string $flag_code Code of the flag. * @type int $term_group Order of the language when displayed in a list of languages. * @type int $is_rtl `1` if the language is rtl, `0` otherwise. * @type string $facebook Optional. Facebook locale. * @type string $home_url Home URL in this language. * @type string $search_url Home URL to use in search forms. * @type string $host Host corresponding to this language. * @type string $flag_url URL of the flag. * @type string $flag HTML markup of the flag. * @type string $custom_flag_url Optional. URL of the custom flag if it exists. * @type string $custom_flag Optional. HTML markup of the custom flag if it exists. * @type int $page_on_front Optional. ID of the page on front in this language. * @type int $page_for_posts Optional. ID of the page for posts in this language. * @type bool $active Whether or not the language is active. Default `true`. * @type string[] $fallbacks List of WordPress language locales. Ex: array( 'en_GB' ). * @type bool $is_default Whether or not the language is the default one. * } * * @phpstan-param LanguageData $language_data */ public function __construct( array $language_data ) { foreach ( $language_data as $prop => $value ) { $this->$prop = $value; } $this->term_id = $this->term_props['language']['term_id']; } /** * Returns a language term property value (term ID, term taxonomy ID, or count). * * @since 3.4 * * @param string $taxonomy_name Name of the taxonomy. * @param string $prop_name Name of the property: 'term_taxonomy_id', 'term_id', 'count'. * @return int * * @phpstan-param non-empty-string $taxonomy_name * @phpstan-param 'term_taxonomy_id'|'term_id'|'count' $prop_name * @phpstan-return int<0, max> */ public function get_tax_prop( $taxonomy_name, $prop_name ) { return isset( $this->term_props[ $taxonomy_name ][ $prop_name ] ) ? $this->term_props[ $taxonomy_name ][ $prop_name ] : 0; } /** * Returns the language term props for all content types. * * @since 3.4 * * @param string $property Name of the field to return. An empty string to return them all. * @return (int[]|int)[] Array keys are taxonomy names, array values depend of `$property`. * * @phpstan-param 'term_taxonomy_id'|'term_id'|'count'|'' $property * @phpstan-return array<non-empty-string, ( * $property is non-empty-string ? * ( * $property is 'count' ? * int<0, max> : * positive-int * ) : * LanguagePropData * )> */ public function get_tax_props( $property = '' ) { if ( empty( $property ) ) { return $this->term_props; } $term_props = array(); foreach ( $this->term_props as $taxonomy_name => $props ) { $term_props[ $taxonomy_name ] = $props[ $property ]; } return $term_props; } /** * Returns the flag information. * * @since 2.6 * * @param string $code Flag code. * @return array { * Flag information. * * @type string $url Flag url. * @type string $src Optional, src attribute value if different of the url, for example if base64 encoded. * @type int $width Optional, flag width in pixels. * @type int $height Optional, flag height in pixels. * } * * @phpstan-return array{ * url: string, * src: string, * width?: positive-int, * height?: positive-int * } */ public static function get_flag_informations( $code ) { $flag = array( 'url' => '' ); // Polylang builtin flags. if ( ! empty( $code ) && is_readable( POLYLANG_DIR . ( $file = '/flags/' . $code . '.png' ) ) ) { $flag['url'] = plugins_url( $file, POLYLANG_FILE ); // If base64 encoded flags are preferred. if ( pll_get_constant( 'PLL_ENCODED_FLAGS', true ) ) { $imagesize = getimagesize( POLYLANG_DIR . $file ); if ( is_array( $imagesize ) ) { list( $flag['width'], $flag['height'] ) = $imagesize; } $file_contents = file_get_contents( POLYLANG_DIR . $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $flag['src'] = 'data:image/png;base64,' . base64_encode( $file_contents ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } } /** * Filters flag information: * * @since 2.4 * * @param array $flag { * Information about the flag. * * @type string $url Flag url. * @type string $src Optional, src attribute value if different of the url, for example if base64 encoded. * @type int $width Optional, flag width in pixels. * @type int $height Optional, flag height in pixels. * } * @param string $code Flag code. */ $flag = apply_filters( 'pll_flag', $flag, $code ); $flag['url'] = esc_url_raw( $flag['url'] ); if ( empty( $flag['src'] ) ) { $flag['src'] = esc_url( set_url_scheme( $flag['url'], 'relative' ) ); } return $flag; } /** * Returns HTML code for flag. * * @since 2.7 * * @param array $flag Flag properties: src, width and height. * @param string $title Optional title attribute. * @param string $alt Optional alt attribute. * @return string * * @phpstan-param array{ * src: string, * width?: int|numeric-string, * height?: int|numeric-string * } $flag */ public static function get_flag_html( $flag, $title = '', $alt = '' ) { if ( empty( $flag['src'] ) ) { return ''; } $alt_attr = empty( $alt ) ? '' : sprintf( ' alt="%s"', esc_attr( $alt ) ); $width_attr = empty( $flag['width'] ) ? '' : sprintf( ' width="%s"', (int) $flag['width'] ); $height_attr = empty( $flag['height'] ) ? '' : sprintf( ' height="%s"', (int) $flag['height'] ); $style = ''; $sizes = array_intersect_key( $flag, array_flip( array( 'width', 'height' ) ) ); if ( ! empty( $sizes ) ) { array_walk( $sizes, function ( &$value, $key ) { $value = sprintf( '%s: %dpx;', esc_attr( $key ), (int) $value ); } ); $style = sprintf( ' style="%s"', implode( ' ', $sizes ) ); } return sprintf( '<img src="%s"%s%s%s%s />', $flag['src'], $alt_attr, $width_attr, $height_attr, $style ); } /** * Returns the html of the custom flag if any, or the default flag otherwise. * * @since 2.8 * @since 3.5.3 Added the `$alt` parameter. * * @param string $alt Whether or not the alternative text should be set. Accepts 'alt' and 'no-alt'. * * @return string * * @phpstan-param 'alt'|'no-alt' $alt */ public function get_display_flag( $alt = 'alt' ) { $flag = empty( $this->custom_flag ) ? $this->flag : $this->custom_flag; if ( 'alt' === $alt ) { return $flag; } return (string) preg_replace( '/(?<=\salt=\")([^"]+)(?=\")/', '', $flag ); } /** * Returns the url of the custom flag if any, or the default flag otherwise. * * @since 2.8 * * @return string */ public function get_display_flag_url() { $flag_url = empty( $this->custom_flag_url ) ? $this->flag_url : $this->custom_flag_url; /** * Filters `flag_url` property. * * @since 3.4.4 * * @param string $flag_url Flag URL. * @param PLL_Language $language Current `PLL_language` instance. */ return apply_filters( 'pll_language_flag_url', $flag_url, $this ); } /** * Updates post and term count. * * @since 1.2 * * @return void */ public function update_count() { foreach ( $this->term_props as $taxonomy => $props ) { wp_update_term_count( $props['term_taxonomy_id'], $taxonomy ); } } /** * Returns the language locale. * Converts WP locales to W3C valid locales for display. * * @since 1.8 * * @param string $filter Either 'display' or 'raw', defaults to raw. * @return string * * @phpstan-param 'display'|'raw' $filter * @phpstan-return non-empty-string */ public function get_locale( $filter = 'raw' ) { return 'display' === $filter ? $this->w3c : $this->locale; } /** * Returns the values of this instance's properties, which can be filtered if required. * * @since 3.4 * * @param string $context Whether or not properties should be filtered. Accepts `db` or `display`. * Default to `display` which filters some properties. * * @return array Array of language object properties. * * @phpstan-return LanguageData */ public function to_array( $context = 'display' ) { $language = get_object_vars( $this ); if ( 'db' !== $context ) { $language['home_url'] = $this->get_home_url(); $language['search_url'] = $this->get_search_url(); } /** @phpstan-var LanguageData $language */ return $language; } /** * Converts current `PLL_language` into a `stdClass` object. Mostly used to allow dynamic properties. * * @since 3.4 * * @return stdClass Converted `PLL_Language` object. */ public function to_std_class() { return (object) $this->to_array(); } /** * Returns a predefined HTML flag. * * @since 3.4 * * @param string $flag_code Flag code to render. * @return string HTML code for the flag. */ public static function get_predefined_flag( $flag_code ) { $flag = self::get_flag_informations( $flag_code ); return self::get_flag_html( $flag ); } /** * Returns language's home URL. Takes care to render it dynamically if no cache is allowed. * * @since 3.4 * * @return string Language home URL. */ public function get_home_url() { if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) || ! pll_get_constant( 'PLL_CACHE_HOME_URL', true ) ) { /** * Filters current `PLL_Language` instance `home_url` property. * * @since 3.4.4 * * @param string $home_url The `home_url` prop. * @param array $language Current Array of `PLL_Language` properties. */ return apply_filters( 'pll_language_home_url', $this->home_url, $this->to_array( 'db' ) ); } return $this->home_url; } /** * Returns language's search URL. Takes care to render it dynamically if no cache is allowed. * * @since 3.4 * * @return string Language search URL. */ public function get_search_url() { if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) || ! pll_get_constant( 'PLL_CACHE_HOME_URL', true ) ) { /** * Filters current `PLL_Language` instance `search_url` property. * * @since 3.4.4 * * @param string $search_url The `search_url` prop. * @param array $language Current Array of `PLL_Language` properties. */ return apply_filters( 'pll_language_search_url', $this->search_url, $this->to_array( 'db' ) ); } return $this->search_url; } /** * Returns the value of a language property. * This is handy to get a property's value without worrying about triggering a deprecation warning or anything. * * @since 3.4 * * @param string $property A property name. A composite value can be used for language term property values, in the * form of `{language_taxonomy_name}:{property_name}` (see {@see PLL_Language::get_tax_prop()} * for the possible values). Ex: `term_language:term_taxonomy_id`. * @return string|int|bool|string[] The requested property for the language, `false` if the property doesn't exist. * * @phpstan-return ( * $property is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string> * ) */ public function get_prop( $property ) { // Deprecated property. if ( $this->is_deprecated_term_property( $property ) ) { return $this->get_deprecated_term_property( $property ); } if ( $this->is_deprecated_url_property( $property ) ) { return $this->get_deprecated_url_property( $property ); } // Composite property like 'term_language:term_taxonomy_id'. if ( preg_match( '/^(?<tax>.{1,32}):(?<field>term_id|term_taxonomy_id|count)$/', $property, $matches ) ) { /** @var array{tax:non-empty-string, field:'term_id'|'term_taxonomy_id'|'count'} $matches */ return $this->get_tax_prop( $matches['tax'], $matches['field'] ); } // Any other public property. if ( isset( $this->$property ) ) { return $this->$property; } return false; } } include/cookie.php 0000644 00000006551 15136114124 0010156 0 ustar 00 <?php /** * @package Polylang */ /** * A class to manage manage the language cookie * * @since 2.9 */ class PLL_Cookie { /** * Parses the cookie parameters. * * @since 2.9 * * @param array $args {@see PLL_Cookie::set()} * @return array */ protected static function parse_args( $args ) { /** * Filters the Polylang cookie duration. * * If a cookie duration of 0 is specified, a session cookie will be set. * If a negative cookie duration is specified, the cookie is removed. * /!\ This filter may be fired *before* the theme is loaded. * * @since 1.8 * * @param int $duration Cookie duration in seconds. */ $expiration = (int) apply_filters( 'pll_cookie_expiration', YEAR_IN_SECONDS ); $defaults = array( 'expires' => 0 !== $expiration ? time() + $expiration : 0, 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, // Cookie domain must be set to false for localhost (default value for `COOKIE_DOMAIN`) thanks to Stephen Harris. 'secure' => is_ssl(), 'httponly' => false, 'samesite' => 'Lax', ); $args = wp_parse_args( $args, $defaults ); /** * Filters the Polylang cookie arguments. * /!\ This filter may be fired *before* the theme is loaded. * * @since 3.6 * * @param array $args { * Optional. Array of arguments for setting the cookie. * * @type int $expires Cookie duration. * If a cookie duration of 0 is specified, a session cookie will be set. * If a negative cookie duration is specified, the cookie is removed. * @type string $path Cookie path. * @type string $domain Cookie domain. Must be set to false for localhost (default value for `COOKIE_DOMAIN`). * @type bool $secure Should the cookie be sent only over https? * @type bool $httponly Should the cookie be accessed only over http protocol?. * @type string $samesite Either 'Strict', 'Lax' or 'None'. * } */ return (array) apply_filters( 'pll_cookie_args', $args ); } /** * Sets the cookie. * * @since 2.9 * * @param string $lang Language cookie value. * @param array $args { * Optional. Array of arguments for setting the cookie. * * @type string $path Cookie path, defaults to COOKIEPATH. * @type string $domain Cookie domain, defaults to COOKIE_DOMAIN * @type bool $secure Should the cookie be sent only over https? * @type bool $httponly Should the cookie accessed only over http protocol? Defaults to false. * @type string $samesite Either 'Strict', 'Lax' or 'None', defaults to 'Lax'. * } * @return void */ public static function set( $lang, $args = array() ) { $args = self::parse_args( $args ); if ( ! headers_sent() && PLL_COOKIE !== false && self::get() !== $lang ) { if ( version_compare( PHP_VERSION, '7.3', '<' ) ) { $args['path'] .= '; SameSite=' . $args['samesite']; // Hack to set SameSite value in PHP < 7.3. Doesn't work with newer versions. setcookie( PLL_COOKIE, $lang, $args['expires'], $args['path'], $args['domain'], $args['secure'], $args['httponly'] ); } else { setcookie( PLL_COOKIE, $lang, $args ); } } } /** * Returns the language cookie value. * * @since 2.9 * * @return string */ public static function get() { return isset( $_COOKIE[ PLL_COOKIE ] ) ? sanitize_key( $_COOKIE[ PLL_COOKIE ] ) : ''; } } include/crud-posts.php 0000644 00000036227 15136114124 0011013 0 ustar 00 <?php /** * @package Polylang */ /** * Adds actions and filters related to languages when creating, updating or deleting posts. * Actions and filters triggered when reading posts are handled separately. * * @since 2.4 */ class PLL_CRUD_Posts { /** * @var PLL_Model */ protected $model; /** * Preferred language to assign to a new post. * * @var PLL_Language|null */ protected $pref_lang; /** * Current language. * * @var PLL_Language|null */ protected $curlang; /** * Reference to the Polylang options array. * * @var array */ protected $options; /** * Constructor * * @since 2.4 * * @param object $polylang The Polylang object. */ public function __construct( &$polylang ) { $this->options = &$polylang->options; $this->model = &$polylang->model; $this->pref_lang = &$polylang->pref_lang; $this->curlang = &$polylang->curlang; add_action( 'save_post', array( $this, 'save_post' ), 10, 2 ); add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 ); add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 2 ); add_action( 'before_delete_post', array( $this, 'delete_post' ) ); add_action( 'post_updated', array( $this, 'force_tags_translation' ), 10, 3 ); // Specific for media if ( $polylang->options['media_support'] ) { add_action( 'add_attachment', array( $this, 'set_default_language' ) ); add_action( 'delete_attachment', array( $this, 'delete_post' ) ); add_filter( 'wp_delete_file', array( $this, 'wp_delete_file' ) ); } } /** * Allows to set a language by default for posts if it has no language yet. * * @since 1.5 * * @param int $post_id Post ID. * @return void */ public function set_default_language( $post_id ) { if ( ! $this->model->post->get_language( $post_id ) ) { if ( ! empty( $_GET['new_lang'] ) && $lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification // Defined only on admin. $this->model->post->set_language( $post_id, $lang ); } elseif ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification // Testing $this->pref_lang makes this test pass only on admin. $this->model->post->set_language( $post_id, $lang ); } elseif ( ( $parent_id = wp_get_post_parent_id( $post_id ) ) && $parent_lang = $this->model->post->get_language( $parent_id ) ) { $this->model->post->set_language( $post_id, $parent_lang ); } elseif ( isset( $this->pref_lang ) ) { // Always defined on admin, never defined on frontend. $this->model->post->set_language( $post_id, $this->pref_lang ); } elseif ( ! empty( $this->curlang ) ) { // Only on frontend due to the previous test always true on admin. $this->model->post->set_language( $post_id, $this->curlang ); } else { // In all other cases set to default language. $this->model->post->set_language( $post_id, $this->options['default_lang'] ); } } } /** * Called when a post ( or page ) is saved, published or updated. * * @since 0.1 * @since 2.3 Does not save the language and translations anymore, unless the post has no language yet. * * @param int $post_id Post id of the post being saved. * @param WP_Post $post The post being saved. * @return void */ public function save_post( $post_id, $post ) { // Does nothing except on post types which are filterable. if ( $this->model->is_translated_post_type( $post->post_type ) ) { if ( $id = wp_is_post_revision( $post_id ) ) { $post_id = $id; } $lang = $this->model->post->get_language( $post_id ); if ( empty( $lang ) ) { $this->set_default_language( $post_id ); } /** * Fires after the post language and translations are saved. * * @since 1.2 * * @param int $post_id Post id. * @param WP_Post $post Post object. * @param int[] $translations The list of translations post ids. */ do_action( 'pll_save_post', $post_id, $post, $this->model->post->get_translations( $post_id ) ); } } /** * Makes sure that saved terms are in the right language. * * @since 2.3 * * @param int $object_id Object ID. * @param int[]|string[] $terms An array of object term IDs or slugs. * @param int[] $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @return void */ public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) { static $avoid_recursion; if ( $avoid_recursion || empty( $terms ) || ! is_array( $terms ) || ! $this->model->is_translated_taxonomy( $taxonomy ) ) { return; } $lang = $this->model->post->get_language( $object_id ); if ( empty( $lang ) ) { return; } // Use the term_taxonomy_ids to get all the requested terms in 1 query. $new_terms = get_terms( array( 'taxonomy' => $taxonomy, 'term_taxonomy_id' => array_map( 'intval', $tt_ids ), 'lang' => '', ) ); if ( empty( $new_terms ) || ! is_array( $new_terms ) ) { // Terms not found. return; } $new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang ); // Query the object's term. $orig_terms = get_terms( array( 'taxonomy' => $taxonomy, 'object_ids' => $object_id, 'lang' => '', ) ); if ( is_array( $orig_terms ) ) { $orig_term_ids = wp_list_pluck( $orig_terms, 'term_id' ); $orig_term_ids_translated = $this->translate_terms( $orig_terms, $taxonomy, $lang ); // Terms that are not in the translated list. $remove_term_ids = array_diff( $orig_term_ids, $orig_term_ids_translated ); if ( ! empty( $remove_term_ids ) ) { wp_remove_object_terms( $object_id, $remove_term_ids, $taxonomy ); } } else { $orig_term_ids = array(); $orig_term_ids_translated = array(); } // Terms to add. $add_term_ids = array_unique( array_merge( $orig_term_ids_translated, $new_term_ids_translated ) ); $add_term_ids = array_diff( $add_term_ids, $orig_term_ids ); if ( ! empty( $add_term_ids ) ) { $avoid_recursion = true; wp_set_object_terms( $object_id, $add_term_ids, $taxonomy, true ); // Append. $avoid_recursion = false; } } /** * Make sure that the post parent is in the correct language. * * @since 1.8 * * @param int $post_parent Post parent ID. * @param int $post_id Post ID. * @return int */ public function wp_insert_post_parent( $post_parent, $post_id ) { $lang = $this->model->post->get_language( $post_id ); $parent_post_type = $post_parent > 0 ? get_post_type( $post_parent ) : null; // Dont break the hierarchy in case the post has no language if ( ! empty( $lang ) && ! empty( $parent_post_type ) && $this->model->is_translated_post_type( $parent_post_type ) ) { $post_parent = $this->model->post->get_translation( $post_parent, $lang ); } return $post_parent; } /** * Called when a post, page or media is deleted * Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072 * * @since 0.1 * * @param int $post_id Post ID. * @return void */ public function delete_post( $post_id ) { if ( ! wp_is_post_revision( $post_id ) ) { $this->model->post->delete_translation( $post_id ); } } /** * Prevents WP deleting files when there are still media using them. * * @since 0.9 * * @param string $file Path to the file to delete. * @return string Empty or unmodified path. */ public function wp_delete_file( $file ) { global $wpdb; $uploadpath = wp_upload_dir(); // Get the main attached file. $attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) ); $attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file ); $ids = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s", $attached_file ) ); if ( ! empty( $ids ) ) { return ''; // Prevent deleting the file. } return $file; } /** * Creates a media translation * * @since 1.8 * * @param int $post_id Original attachment id. * @param string|object $lang New translation language. * @return int Attachment id of the translated media. */ public function create_media_translation( $post_id, $lang ) { if ( empty( $post_id ) ) { return 0; } $post = get_post( $post_id, ARRAY_A ); if ( empty( $post ) ) { return 0; } $lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug. if ( empty( $lang ) ) { return 0; } // Create a new attachment ( translate attachment parent if exists ). add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload. unset( $post['ID'] ); // Will force the creation. if ( ! empty( $post['post_parent'] ) ) { $post['post_parent'] = (int) $this->model->post->get_translation( $post['post_parent'], $lang->slug ); } $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language. $tr_id = wp_insert_attachment( wp_slash( $post ) ); remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload. // Copy metadata. $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered. if ( is_array( $data ) ) { wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed. } // Copy attached file. if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered. update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed. } // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it. if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) { add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) ); } $this->model->post->set_language( $tr_id, $lang ); $translations = $this->model->post->get_translations( $post_id ); $translations[ $lang->slug ] = $tr_id; $this->model->post->save_translations( $tr_id, $translations ); /** * Fires after a media translation is created * * @since 1.6.4 * * @param int $post_id Post id of the source media. * @param int $tr_id Post id of the new media translation. * @param string $slug Language code of the new translation. */ do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug ); return $tr_id; } /** * Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`. * * @since 3.4.5 * * @param int $post_id Post ID, unused. * @param WP_Post $post_after Post object following the update. * @param WP_Post $post_before Post object before the update. * @return void */ public function force_tags_translation( $post_id, $post_after, $post_before ) { if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) { return; } $terms = get_the_terms( $post_before, 'post_tag' ); if ( empty( $terms ) || ! is_array( $terms ) ) { return; } $term_ids = wp_list_pluck( $terms, 'term_id' ); // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job. wp_set_post_terms( $post_id, $term_ids, 'post_tag' ); } /** * Makes sure that all terms in the given list are in the given language. * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively). * * @since 3.5 * * @param WP_Term[] $terms List of terms to translate. * @param string $taxonomy The terms' taxonomy. * @param PLL_Language $language The language to translate the terms into. * @return int[] List of `term_id`s. * * @phpstan-return array<positive-int> */ private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array { $term_ids_translated = array(); foreach ( $terms as $term ) { $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language ); } return array_filter( $term_ids_translated ); } /** * Translates the given term into the given language. * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively). * * @since 3.5 * * @param WP_Term $term The term to translate. * @param string $taxonomy The term's taxonomy. * @param PLL_Language $language The language to translate the term into. * @return int A `term_id` on success, `0` on failure. * * @phpstan-return int<0, max> */ private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int { // Check if the term is in the correct language or if a translation exists. $tr_term_id = $this->model->term->get( $term->term_id, $language ); if ( ! empty( $tr_term_id ) ) { // Already in the correct language. return $tr_term_id; } // Or choose the correct language for tags (initially defined by name). $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language ); if ( ! empty( $tr_term_id ) ) { return $tr_term_id; } // Or create the term in the correct language. $tr_parent_term_id = 0; if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) { $parent = get_term( $term->parent, $taxonomy ); if ( $parent instanceof WP_Term ) { // Translate the parent recursively. $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language ); } } $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) { if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) { return $language; } return $lang; }; $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) { if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) { return $tr_parent_term_id; } return $parent_id; }; add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 ); add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 ); $new_term_info = wp_insert_term( $term->name, $taxonomy, array( 'parent' => $tr_parent_term_id, 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`. ) ); remove_filter( 'pll_inserted_term_language', $lang_callback ); remove_filter( 'pll_inserted_term_parent', $parent_callback ); if ( is_wp_error( $new_term_info ) ) { // Term creation failed. return 0; } $tr_term_id = max( 0, (int) $new_term_info['term_id'] ); if ( empty( $tr_term_id ) ) { return 0; } $this->model->term->set_language( $tr_term_id, $language ); $trs = $this->model->term->get_translations( $term->term_id ); $trs[ $language->slug ] = $tr_term_id; $this->model->term->save_translations( $term->term_id, $trs ); return $tr_term_id; } } include/widget-calendar.php 0000644 00000022703 15136114124 0011734 0 ustar 00 <?php /** * @package Polylang */ if ( ! class_exists( 'WP_Widget_Calendar' ) ) { require_once ABSPATH . '/wp-includes/default-widgets.php'; } /** * This classes rewrite the whole Calendar widget functionality as there is no filter on sql queries and only a filter on final output. * Code last checked: WP 5.5. * * A request to add filters on sql queries exists: http://core.trac.wordpress.org/ticket/15202. * Method used in 0.4.x: use of the get_calendar filter and overwrite the output of get_calendar function -> not very efficient (add 4 to 5 sql queries). * Method used since 0.5: remove the WP widget and replace it by our own -> our language filter will not work if get_calendar is called directly by a theme. * * @since 0.5 */ class PLL_Widget_Calendar extends WP_Widget_Calendar { protected static $pll_instance = 0; // Can't use $instance of WP_Widget_Calendar as it's private :/. /** * Outputs the content for the current Calendar widget instance. * Modified version of the parent function to call our own get_calendar() method. * * @since 0.5 * * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance The settings for the particular instance of the widget. */ public function widget( $args, $instance ) { $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo $args['before_widget']; if ( $title ) { echo $args['before_title'] . $title . $args['after_title']; } if ( 0 === self::$pll_instance ) { #modified# echo '<div id="calendar_wrap" class="calendar_wrap">'; } else { echo '<div class="calendar_wrap">'; } empty( PLL()->curlang ) ? get_calendar() : self::get_calendar(); #modified# echo '</div>'; echo $args['after_widget']; ++self::$pll_instance; #modified# } /** * Modified version of the WP get_calendar() function to filter the queries. * * @since 0.5 * * @param bool $initial Optional, default is true. Use initial calendar names. * @param bool $display Optional, default is true. Set to false for return. * @return void|string Void if `$display` argument is true, calendar HTML if `$display` is false. */ static public function get_calendar( $initial = true, $display = true ) { global $wpdb, $m, $monthnum, $year, $wp_locale, $posts; $join_clause = PLL()->model->post->join_clause(); #added# $where_clause = PLL()->model->post->where_clause( PLL()->curlang ); #added# $key = md5( PLL()->curlang->slug . $m . $monthnum . $year ); #modified# $cache = wp_cache_get( 'get_calendar', 'calendar' ); if ( $cache && is_array( $cache ) && isset( $cache[ $key ] ) ) { /** This filter is documented in wp-includes/general-template.php */ $output = apply_filters( 'get_calendar', $cache[ $key ] ); if ( $display ) { echo $output; return; } return $output; } if ( ! is_array( $cache ) ) { $cache = array(); } // Quick check. If we have no posts at all, abort! if ( ! $posts ) { $gotsome = $wpdb->get_var( "SELECT 1 as test FROM $wpdb->posts WHERE post_type = 'post' AND post_status = 'publish' LIMIT 1" ); if ( ! $gotsome ) { $cache[ $key ] = ''; wp_cache_set( 'get_calendar', $cache, 'calendar' ); return; } } if ( isset( $_GET['w'] ) ) { $w = (int) $_GET['w']; } // week_begins = 0 stands for Sunday. $week_begins = (int) get_option( 'start_of_week' ); // Let's figure out when we are. if ( ! empty( $monthnum ) && ! empty( $year ) ) { $thismonth = zeroise( (int) $monthnum, 2 ); $thisyear = (int) $year; } elseif ( ! empty( $w ) ) { // We need to get the month from MySQL. $thisyear = (int) substr( $m, 0, 4 ); // It seems MySQL's weeks disagree with PHP's. $d = ( ( $w - 1 ) * 7 ) + 6; $thismonth = $wpdb->get_var( "SELECT DATE_FORMAT((DATE_ADD('{$thisyear}0101', INTERVAL $d DAY) ), '%m')" ); } elseif ( ! empty( $m ) ) { $thisyear = (int) substr( $m, 0, 4 ); if ( strlen( $m ) < 6 ) { $thismonth = '01'; } else { $thismonth = zeroise( (int) substr( $m, 4, 2 ), 2 ); } } else { $thisyear = current_time( 'Y' ); $thismonth = current_time( 'm' ); } $unixmonth = mktime( 0, 0, 0, $thismonth, 1, $thisyear ); $last_day = gmdate( 't', $unixmonth ); // Get the next and previous month and year with at least one post. $previous = $wpdb->get_row( "SELECT MONTH(post_date) AS month, YEAR(post_date) AS year FROM $wpdb->posts $join_clause WHERE post_date < '$thisyear-$thismonth-01' AND post_type = 'post' AND post_status = 'publish' $where_clause ORDER BY post_date DESC LIMIT 1" ); #modified# $next = $wpdb->get_row( "SELECT MONTH(post_date) AS month, YEAR(post_date) AS year FROM $wpdb->posts $join_clause WHERE post_date > '$thisyear-$thismonth-{$last_day} 23:59:59' AND post_type = 'post' AND post_status = 'publish' $where_clause ORDER BY post_date ASC LIMIT 1" ); #modified# /* translators: Calendar caption: 1: Month name, 2: 4-digit year. */ $calendar_caption = _x( '%1$s %2$s', 'calendar caption' ); $calendar_output = '<table id="wp-calendar" class="wp-calendar-table"> <caption>' . sprintf( $calendar_caption, $wp_locale->get_month( $thismonth ), gmdate( 'Y', $unixmonth ) ) . '</caption> <thead> <tr>'; $myweek = array(); for ( $wdcount = 0; $wdcount <= 6; $wdcount++ ) { $myweek[] = $wp_locale->get_weekday( ( $wdcount + $week_begins ) % 7 ); } foreach ( $myweek as $wd ) { $day_name = $initial ? $wp_locale->get_weekday_initial( $wd ) : $wp_locale->get_weekday_abbrev( $wd ); $wd = esc_attr( $wd ); $calendar_output .= "\n\t\t<th scope=\"col\" title=\"$wd\">$day_name</th>"; } $calendar_output .= ' </tr> </thead> <tbody> <tr>'; $daywithpost = array(); // Get days with posts. $dayswithposts = $wpdb->get_results( "SELECT DISTINCT DAYOFMONTH(post_date) FROM $wpdb->posts $join_clause WHERE post_date >= '{$thisyear}-{$thismonth}-01 00:00:00' AND post_type = 'post' AND post_status = 'publish' AND post_date <= '{$thisyear}-{$thismonth}-{$last_day} 23:59:59' $where_clause", ARRAY_N ); #modified# if ( $dayswithposts ) { foreach ( (array) $dayswithposts as $daywith ) { $daywithpost[] = (int) $daywith[0]; } } // See how much we should pad in the beginning. $pad = calendar_week_mod( gmdate( 'w', $unixmonth ) - $week_begins ); if ( 0 != $pad ) { $calendar_output .= "\n\t\t" . '<td colspan="' . esc_attr( $pad ) . '" class="pad"> </td>'; } $newrow = false; $daysinmonth = (int) gmdate( 't', $unixmonth ); for ( $day = 1; $day <= $daysinmonth; ++$day ) { if ( isset( $newrow ) && $newrow ) { $calendar_output .= "\n\t</tr>\n\t<tr>\n\t\t"; } $newrow = false; if ( current_time( 'j' ) == $day && current_time( 'm' ) == $thismonth && current_time( 'Y' ) == $thisyear ) { $calendar_output .= '<td id="today">'; } else { $calendar_output .= '<td>'; } if ( in_array( $day, $daywithpost, true ) ) { // Any posts today? $date_format = gmdate( _x( 'F j, Y', 'daily archives date format' ), strtotime( "{$thisyear}-{$thismonth}-{$day}" ) ); /* translators: Post calendar label. %s: Date. */ $label = sprintf( __( 'Posts published on %s' ), $date_format ); $calendar_output .= sprintf( '<a href="%s" aria-label="%s">%s</a>', get_day_link( $thisyear, $thismonth, $day ), esc_attr( $label ), $day ); } else { $calendar_output .= $day; } $calendar_output .= '</td>'; if ( 6 == calendar_week_mod( gmdate( 'w', mktime( 0, 0, 0, $thismonth, $day, $thisyear ) ) - $week_begins ) ) { $newrow = true; } } $pad = 7 - calendar_week_mod( gmdate( 'w', mktime( 0, 0, 0, $thismonth, $day, $thisyear ) ) - $week_begins ); if ( 0 != $pad && 7 != $pad ) { $calendar_output .= "\n\t\t" . '<td class="pad" colspan="' . esc_attr( $pad ) . '"> </td>'; } $calendar_output .= "\n\t</tr>\n\t</tbody>"; $calendar_output .= "\n\t</table>"; $calendar_output .= '<nav aria-label="' . __( 'Previous and next months' ) . '" class="wp-calendar-nav">'; if ( $previous ) { $calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-prev"><a href="' . get_month_link( $previous->year, $previous->month ) . '">« ' . $wp_locale->get_month_abbrev( $wp_locale->get_month( $previous->month ) ) . '</a></span>'; } else { $calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-prev"> </span>'; } $calendar_output .= "\n\t\t" . '<span class="pad"> </span>'; if ( $next ) { $calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-next"><a href="' . get_month_link( $next->year, $next->month ) . '">' . $wp_locale->get_month_abbrev( $wp_locale->get_month( $next->month ) ) . ' »</a></span>'; } else { $calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-next"> </span>'; } $calendar_output .= ' </nav>'; $cache[ $key ] = $calendar_output; wp_cache_set( 'get_calendar', $cache, 'calendar' ); if ( $display ) { /** This filter is documented in wp-includes/general-template.php */ echo apply_filters( 'get_calendar', $calendar_output ); return; } /** This filter is documented in wp-includes/general-template.php */ return apply_filters( 'get_calendar', $calendar_output ); } } include/base.php 0000644 00000013454 15136114124 0007617 0 ustar 00 <?php /** * @package Polylang */ /** * Base class for both admin and frontend * * @since 1.2 */ #[AllowDynamicProperties] abstract class PLL_Base { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Instance of a child class of PLL_Links_Model. * * @var PLL_Links_Model */ public $links_model; /** * Registers hooks on insert / update post related actions and filters. * * @var PLL_CRUD_Posts|null */ public $posts; /** * Registers hooks on insert / update term related action and filters. * * @var PLL_CRUD_Terms|null */ public $terms; /** * Constructor. * * @since 1.2 * * @param PLL_Links_Model $links_model Links Model. */ public function __construct( &$links_model ) { $this->links_model = &$links_model; $this->model = &$links_model->model; $this->options = &$this->model->options; $GLOBALS['l10n_unloaded']['pll_string'] = true; // Short-circuit _load_textdomain_just_in_time() for 'pll_string' domain in WP 4.6+ add_action( 'widgets_init', array( $this, 'widgets_init' ) ); // User defined strings translations add_action( 'pll_language_defined', array( $this, 'load_strings_translations' ), 5 ); add_action( 'change_locale', array( $this, 'load_strings_translations' ) ); // Since WP 4.7 add_action( 'personal_options_update', array( $this, 'load_strings_translations' ), 1, 0 ); // Before WP, for confirmation request when changing the user email. add_action( 'lostpassword_post', array( $this, 'load_strings_translations' ), 10, 0 ); // Password reset email. // Switch_to_blog add_action( 'switch_blog', array( $this, 'switch_blog' ), 10, 2 ); } /** * Instantiates classes reacting to CRUD operations on posts and terms, * only when at least one language is defined. * * @since 2.6 * * @return void */ public function init() { if ( $this->model->has_languages() ) { $this->posts = new PLL_CRUD_Posts( $this ); $this->terms = new PLL_CRUD_Terms( $this ); // WordPress options. new PLL_Translate_Option( 'blogname', array(), array( 'context' => 'WordPress' ) ); new PLL_Translate_Option( 'blogdescription', array(), array( 'context' => 'WordPress' ) ); new PLL_Translate_Option( 'date_format', array(), array( 'context' => 'WordPress' ) ); new PLL_Translate_Option( 'time_format', array(), array( 'context' => 'WordPress' ) ); } } /** * Registers our widgets * * @since 0.1 * * @return void */ public function widgets_init() { register_widget( 'PLL_Widget_Languages' ); // Overwrites the calendar widget to filter posts by language if ( ! defined( 'PLL_WIDGET_CALENDAR' ) || PLL_WIDGET_CALENDAR ) { unregister_widget( 'WP_Widget_Calendar' ); register_widget( 'PLL_Widget_Calendar' ); } } /** * Loads user defined strings translations * * @since 1.2 * @since 2.1.3 $locale parameter added. * * @param string $locale Language locale or slug. Defaults to current locale. * @return void */ public function load_strings_translations( $locale = '' ) { if ( empty( $locale ) ) { $locale = ( is_admin() && ! Polylang::is_ajax_on_front() ) ? get_user_locale() : get_locale(); } $language = $this->model->get_language( $locale ); if ( ! empty( $language ) ) { $mo = new PLL_MO(); $mo->import_from_db( $language ); $GLOBALS['l10n']['pll_string'] = &$mo; } else { unset( $GLOBALS['l10n']['pll_string'] ); } } /** * Resets some variables when the blog is switched. * Applied only if Polylang is active on the new blog. * * @since 1.5.1 * * @param int $new_blog_id New blog ID. * @param int $prev_blog_id Previous blog ID. * @return void */ public function switch_blog( $new_blog_id, $prev_blog_id ) { if ( (int) $new_blog_id === (int) $prev_blog_id ) { // Do nothing if same blog. return; } $this->links_model->remove_filters(); if ( $this->is_active_on_current_site() ) { $this->options = get_option( 'polylang' ); // Needed for menus. $this->links_model = $this->model->get_links_model(); } } /** * Checks if Polylang is active on the current blog (useful when the blog is switched). * * @since 3.5.2 * * @return bool */ protected function is_active_on_current_site(): bool { return pll_is_plugin_active( POLYLANG_BASENAME ) && get_option( 'polylang' ); } /** * Check if the customize menu should be removed or not. * * @since 3.2 * * @return bool True if it should be removed, false otherwise. */ public function should_customize_menu_be_removed() { // Exit if a block theme isn't activated. if ( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) { return false; } return ! $this->is_customize_register_hooked(); } /** * Tells whether or not Polylang or third party callbacks are hooked to `customize_register`. * * @since 3.4.3 * * @global $wp_filter * * @return bool True if Polylang's callbacks are hooked, false otherwise. */ protected function is_customize_register_hooked() { global $wp_filter; if ( empty( $wp_filter['customize_register'] ) || ! $wp_filter['customize_register'] instanceof WP_Hook ) { return false; } /* * 'customize_register' is hooked by: * @see PLL_Nav_Menu::create_nav_menu_locations() * @see PLL_Frontend_Static_Pages::filter_customizer() */ $floor = 0; if ( ! empty( $this->nav_menu ) && (bool) $wp_filter['customize_register']->has_filter( 'customize_register', array( $this->nav_menu, 'create_nav_menu_locations' ) ) ) { ++$floor; } if ( ! empty( $this->static_pages ) && (bool) $wp_filter['customize_register']->has_filter( 'customize_register', array( $this->static_pages, 'filter_customizer' ) ) ) { ++$floor; } $count = array_sum( array_map( 'count', $wp_filter['customize_register']->callbacks ) ); return $count > $floor; } } include/widget-languages.php 0000644 00000011106 15136114124 0012124 0 ustar 00 <?php /** * @package Polylang */ /** * The language switcher widget * * @since 0.1 */ class PLL_Widget_Languages extends WP_Widget { /** * Constructor * * @since 0.1 */ public function __construct() { parent::__construct( 'polylang', __( 'Language switcher', 'polylang' ), array( 'description' => __( 'Displays a language switcher', 'polylang' ), 'customize_selective_refresh' => true, ) ); } /** * Displays the widget * * @since 0.1 * * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget. * @param array $instance The settings for the particular instance of the widget * @return void */ public function widget( $args, $instance ) { // Sets a unique id for dropdown. $instance['dropdown'] = empty( $instance['dropdown'] ) ? 0 : $this->id; $instance['echo'] = 0; $instance['raw'] = 0; $list = pll_the_languages( $instance ); if ( $list ) { $title = empty( $instance['title'] ) ? '' : $instance['title']; /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput if ( $title ) { echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput } // The title may be filtered: Strip out HTML and make sure the aria-label is never empty. $aria_label = trim( wp_strip_all_tags( $title ) ); if ( ! $aria_label ) { $aria_label = __( 'Choose a language', 'polylang' ); } if ( $instance['dropdown'] ) { echo '<label class="screen-reader-text" for="' . esc_attr( 'lang_choice_' . $instance['dropdown'] ) . '">' . esc_html( $aria_label ) . '</label>'; echo $list; // phpcs:ignore WordPress.Security.EscapeOutput } else { $format = current_theme_supports( 'html5', 'navigation-widgets' ) ? 'html5' : 'xhtml'; /** This filter is documented in wp-includes/widgets/class-wp-nav-menu-widget.php */ $format = apply_filters( 'navigation_widgets_format', $format ); if ( 'html5' === $format ) { echo '<nav aria-label="' . esc_attr( $aria_label ) . '">'; } echo "<ul>\n" . $list . "</ul>\n"; // phpcs:ignore WordPress.Security.EscapeOutput if ( 'html5' === $format ) { echo '</nav>'; } } echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput } } /** * Updates the widget options * * @since 0.4 * * @param array $new_instance New settings for this instance as input by the user via form() * @param array $old_instance Old settings for this instance * @return array Settings to save or bool false to cancel saving */ public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $instance = array( 'title' => sanitize_text_field( $new_instance['title'] ) ); foreach ( array_keys( PLL_Switcher::get_switcher_options( 'widget' ) ) as $key ) { $instance[ $key ] = ! empty( $new_instance[ $key ] ) ? 1 : 0; } return $instance; } /** * Displays the widget form. * * @since 0.4 * * @param array $instance Current settings. * @return string */ public function form( $instance ) { // Default values $instance = wp_parse_args( (array) $instance, array_merge( array( 'title' => '' ), PLL_Switcher::get_switcher_options( 'widget', 'default' ) ) ); // Title printf( '<p><label for="%1$s">%2$s</label><input class="widefat" id="%1$s" name="%3$s" type="text" value="%4$s" /></p>', esc_attr( $this->get_field_id( 'title' ) ), esc_html__( 'Title:', 'polylang' ), esc_attr( $this->get_field_name( 'title' ) ), esc_attr( $instance['title'] ) ); foreach ( PLL_Switcher::get_switcher_options( 'widget' ) as $key => $str ) { printf( '<div%5$s%6$s><input type="checkbox" class="checkbox %7$s" id="%1$s" name="%2$s"%3$s /><label for="%1$s">%4$s</label></div>', esc_attr( $this->get_field_id( $key ) ), esc_attr( $this->get_field_name( $key ) ), checked( $instance[ $key ], true, false ), esc_html( $str ), in_array( $key, array( 'show_names', 'show_flags', 'hide_current' ) ) ? sprintf( ' class="no-dropdown-%s"', esc_attr( $this->id ) ) : '', ( ! empty( $instance['dropdown'] ) && in_array( $key, array( 'show_names', 'show_flags', 'hide_current' ) ) ? ' style="display:none;"' : '' ), esc_attr( 'pll-' . $key ) ); } return ''; // Because the parent class returns a string, however not used. } } include/links-model.php 0000644 00000014757 15136114124 0011132 0 ustar 00 <?php /** * @package Polylang */ /** * Links model abstract class. * * @since 1.5 */ abstract class PLL_Links_Model { /** * True if the child class uses pretty permalinks, false otherwise. * * @var bool */ public $using_permalinks; /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * Stores the home url before it is filtered. * * @var string */ public $home; /** * Whether rewrite rules can be filtered or not. Default to `false`. * * @var boolean */ protected static $can_filter_rewrite_rules = false; /** * Constructor. * * @since 1.5 * * @param PLL_Model $model PLL_Model instance. */ public function __construct( &$model ) { $this->model = &$model; $this->options = &$model->options; $this->home = home_url(); // Hooked with normal priority because it needs to be run after static pages is set in language data. Must be done early (before languages objects are created). add_filter( 'pll_additional_language_data', array( $this, 'set_language_home_urls' ), 10, 2 ); // Adds our domains or subdomains to allowed hosts for safe redirection. add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) ); // Allows secondary domains for home and search URLs in `PLL_Language`. add_filter( 'pll_language_home_url', array( $this, 'set_language_home_url' ), 10, 2 ); add_filter( 'pll_language_search_url', array( $this, 'set_language_search_url' ), 10, 2 ); if ( did_action( 'pll_init' ) ) { $this->init(); } else { add_action( 'pll_init', array( $this, 'init' ) ); } } /** * Initializes the links model. * Does nothing by default. * * @since 3.5 * * @return void */ public function init() {} /** * Adds the language code in url. * * @since 1.2 * @since 3.4 Accepts now a language slug. * * @param string $url The url to modify. * @param PLL_Language|string|false $lang Language object or slug. * @return string The modified url. */ abstract public function add_language_to_link( $url, $lang ); /** * Returns the url without language code. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ abstract public function remove_language_from_link( $url ); /** * Returns the link to the first page. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ abstract public function remove_paged_from_link( $url ); /** * Returns the link to a paged page. * * @since 1.5 * * @param string $url The url to modify. * @param int $page The page number. * @return string The modified url. */ abstract public function add_paged_to_link( $url, $page ); /** * Returns the language based on the language code in the url. * * @since 1.2 * @since 2.0 Add the $url argument. * * @param string $url Optional, defaults to the current url. * @return string The language slug. */ abstract public function get_language_from_url( $url = '' ); /** * Returns the static front page url in a given language. * * @since 1.8 * @since 3.4 Accepts now an array of language properties. * * @param PLL_Language|array $language Language object or array of language properties. * @return string The static front page url. */ abstract public function front_page_url( $language ); /** * Changes the language code in url. * * @since 1.5 * * @param string $url The url to modify. * @param PLL_Language $lang The language object. * @return string The modified url. */ public function switch_language_in_link( $url, $lang ) { $url = $this->remove_language_from_link( $url ); return $this->add_language_to_link( $url, $lang ); } /** * Get the hosts managed on the website. * * @since 1.5 * * @return string[] The list of hosts. */ public function get_hosts() { return array( wp_parse_url( $this->home, PHP_URL_HOST ) ); } /** * Returns the home url in a given language. * * @since 1.3.1 * @since 3.4 Accepts now a language slug. * * @param PLL_Language|string $language Language object or slug. * @return string */ public function home_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } $url = trailingslashit( $this->home ); return $this->options['hide_default'] && $language === $this->options['default_lang'] ? $url : $this->add_language_to_link( $url, $language ); } /** * Adds home and search URLs to language data before the object is created. * * @since 3.4 * * @param array $additional_data Array of language additional data. * @param array $language Language data. * @return array Language data with home and search URLs added. */ public function set_language_home_urls( $additional_data, $language ) { $language = array_merge( $language, $additional_data ); $additional_data['search_url'] = $this->set_language_search_url( '', $language ); $additional_data['home_url'] = $this->set_language_home_url( '', $language ); return $additional_data; } /** * Adds our domains or subdomains to allowed hosts for safe redirect. * * @since 1.4.3 * * @param string[] $hosts Allowed hosts. * @return string[] Modified list of allowed hosts. */ public function allowed_redirect_hosts( $hosts ) { return array_unique( array_merge( $hosts, array_values( $this->get_hosts() ) ) ); } /** * Returns language home URL property according to the current domain. * * @since 3.4.4 * * @param string $url Home URL. * @param array $language Array of language props. * @return string Filtered home URL. */ public function set_language_home_url( $url, $language ) { if ( empty( $language['page_on_front'] ) || $this->options['redirect_lang'] ) { return $this->home_url( $language['slug'] ); } return $this->front_page_url( $language ); } /** * Returns language search URL property according to the current domain. * * @since 3.4.4 * * @param string $url Search URL. * @param array $language Array of language props. * @return string Filtered search URL. */ public function set_language_search_url( $url, $language ) { return $this->home_url( $language['slug'] ); } /** * Used to remove hooks in child classes, called when switching blog @see {PLL_Base::switch_blog()}. * Does nothing by default. * * @since 3.5 * * @return void */ public function remove_filters() { self::$can_filter_rewrite_rules = false; } } include/translatable-objects.php 0000644 00000006761 15136114124 0013013 0 ustar 00 <?php /** * @package Polylang */ /** * Registry for all translatable objects. * * @since 3.4 * * @phpstan-implements IteratorAggregate<non-empty-string, PLL_Translatable_Object> * @phpstan-type TranslatedObjectWithTypes PLL_Translated_Object&PLL_Translatable_Object_With_Types_Interface * @phpstan-type TranslatableObjectWithTypes PLL_Translatable_Object&PLL_Translatable_Object_With_Types_Interface */ class PLL_Translatable_Objects implements IteratorAggregate { /** * Type of the main translatable object. * * @var string */ private $main_type = ''; /** * List of registered objects. * * @var PLL_Translatable_Object[] Array keys are the type of translated content (post, term, etc). * * @phpstan-var array<non-empty-string, PLL_Translatable_Object> */ private $objects = array(); /** * Registers a translatable object. * * @since 3.4 * * @param PLL_Translatable_Object $object The translatable object to register. * @return PLL_Translatable_Object */ public function register( PLL_Translatable_Object $object ) { if ( empty( $this->main_type ) ) { $this->main_type = $object->get_type(); } if ( ! isset( $this->objects[ $object->get_type() ] ) ) { $this->objects[ $object->get_type() ] = $object; } return $this->objects[ $object->get_type() ]; } /** * Returns all registered translatable objects. * * @since 3.4 * * @return ArrayIterator Iterator on $objects array property. Keys are the type of translated content (post, term, etc). * * @phpstan-return ArrayIterator<string, PLL_Translatable_Object> */ #[\ReturnTypeWillChange] public function getIterator() { return new ArrayIterator( $this->objects ); } /** * Returns a translatable object, given an object type. * * @since 3.4 * * @param string $object_type The object type. * @return PLL_Translatable_Object|null * * @phpstan-return ( * $object_type is 'post' ? TranslatedObjectWithTypes : ( * $object_type is 'term' ? TranslatedObjectWithTypes : ( * TranslatedObjectWithTypes|TranslatableObjectWithTypes|PLL_Translated_Object|PLL_Translatable_Object|null * ) * ) * ) */ public function get( $object_type ) { if ( ! isset( $this->objects[ $object_type ] ) ) { return null; } return $this->objects[ $object_type ]; } /** * Returns all translatable objects except post one. * * @since 3.4 * * @return PLL_Translatable_Object[] An array of secondary translatable objects. Array keys are the type of translated content (post, term, etc). * * @phpstan-return array<non-empty-string, PLL_Translatable_Object> */ public function get_secondary_translatable_objects() { return array_diff_key( $this->objects, array( $this->main_type => null ) ); } /** * Returns taxonomy names to manage language and translations. * * @since 3.4 * * @param string[] $filter An array on value to filter taxonomy names to return. * @return string[] Taxonomy names. * * @phpstan-param array<'language'|'translations'> $filter * @phpstan-return list<non-empty-string> */ public function get_taxonomy_names( $filter = array( 'language', 'translations' ) ) { $taxonomies = array(); foreach ( $this->objects as $object ) { if ( in_array( 'language', $filter, true ) ) { $taxonomies[] = $object->get_tax_language(); } if ( in_array( 'translations', $filter, true ) && $object instanceof PLL_Translated_Object ) { $taxonomies[] = $object->get_tax_translations(); } } return $taxonomies; } } include/license.php 0000644 00000022307 15136114124 0010324 0 ustar 00 <?php /** * @package Polylang */ /** * A class to easily manage licenses for Polylang Pro and addons * * @since 1.9 */ class PLL_License { /** * Sanitized plugin name. * * @var string */ public $id; /** * Plugin name. * * @var string */ public $name; /** * License key. * * @var string */ public $license_key; /** * License data, obtained from the API request. * * @var stdClass|null */ public $license_data; /** * Main plugin file. * * @var string */ private $file; /** * Current plugin version. * * @var string */ private $version; /** * Plugin author. * * @var string */ private $author; /** * API url. * * @var string. */ private $api_url = 'https://polylang.pro'; /** * Constructor * * @since 1.9 * * @param string $file The plugin file. * @param string $item_name The plugin name. * @param string $version The plugin version. * @param string $author Author name. * @param string $api_url Optional url of the site managing the license. */ public function __construct( $file, $item_name, $version, $author, $api_url = null ) { $this->id = sanitize_title( $item_name ); $this->file = $file; $this->name = $item_name; $this->version = $version; $this->author = $author; $this->api_url = empty( $api_url ) ? $this->api_url : $api_url; $licenses = (array) get_option( 'polylang_licenses', array() ); $license = isset( $licenses[ $this->id ] ) && is_array( $licenses[ $this->id ] ) ? $licenses[ $this->id ] : array(); $this->license_key = ! empty( $license['key'] ) ? (string) $license['key'] : ''; if ( ! empty( $license['data'] ) ) { $this->license_data = (object) $license['data']; } // Updater $this->auto_updater(); // Register settings add_filter( 'pll_settings_licenses', array( $this, 'settings' ) ); // Weekly schedule if ( ! wp_next_scheduled( 'polylang_check_licenses' ) ) { wp_schedule_event( time(), 'weekly', 'polylang_check_licenses' ); } add_action( 'polylang_check_licenses', array( $this, 'check_license' ) ); } /** * Auto updater * * @since 1.9 * * @return void */ public function auto_updater() { $args = array( 'version' => $this->version, 'license' => $this->license_key, 'author' => $this->author, 'item_name' => $this->name, ); // Setup the updater new PLL_Plugin_Updater( $this->api_url, $this->file, $args ); } /** * Registers the licence in the Settings. * * @since 1.9 * * @param PLL_License[] $items Array of objects allowing to manage a license. * @return PLL_License[] */ public function settings( $items ) { $items[ $this->id ] = $this; return $items; } /** * Activates the license key. * * @since 1.9 * * @param string $license_key Activation key. * @return PLL_License Updated PLL_License object. */ public function activate_license( $license_key ) { $this->license_key = $license_key; $this->api_request( 'activate_license' ); // Tell WordPress to look for updates. delete_site_transient( 'update_plugins' ); return $this; } /** * Deactivates the license key. * * @since 1.9 * * @return PLL_License Updated PLL_License object. */ public function deactivate_license() { $this->api_request( 'deactivate_license' ); return $this; } /** * Checks if the license key is valid. * * @since 1.9 * * @return void */ public function check_license() { $this->api_request( 'check_license' ); } /** * Sends an api request to check, activate or deactivate the license * Updates the licenses option according to the status * * @since 1.9 * * @param string $request check_license | activate_license | deactivate_license * @return void */ private function api_request( $request ) { $licenses = get_option( 'polylang_licenses' ); if ( is_array( $licenses ) ) { unset( $licenses[ $this->id ] ); } else { $licenses = array(); } unset( $this->license_data ); if ( ! empty( $this->license_key ) ) { // Data to send in our API request $api_params = array( 'edd_action' => $request, 'license' => $this->license_key, 'item_name' => urlencode( $this->name ), 'url' => home_url(), ); // Call the API $response = wp_remote_post( $this->api_url, array( 'timeout' => 3, 'sslverify' => false, 'body' => $api_params, ) ); // Update the option only if we got a response if ( is_wp_error( $response ) ) { return; } // Save new license info $licenses[ $this->id ] = array( 'key' => $this->license_key ); $data = (object) json_decode( wp_remote_retrieve_body( $response ) ); if ( isset( $data->license ) && 'deactivated' !== $data->license ) { $licenses[ $this->id ]['data'] = $this->license_data = $data; } } update_option( 'polylang_licenses', $licenses ); // FIXME called multiple times when saving all licenses } /** * Get the html form field in a table row (one per license key) for display * * @since 2.7 * * @return string */ public function get_form_field() { if ( ! empty( $this->license_data ) ) { $license = $this->license_data; } $class = 'license-null'; $message = ''; $out = sprintf( '<td><label for="pll-licenses[%1$s]">%2$s</label></td>' . '<td><input name="licenses[%1$s]" id="pll-licenses[%1$s]" type="password" value="%3$s" class="regular-text code" />', esc_attr( $this->id ), esc_attr( $this->name ), esc_html( $this->license_key ) ); if ( ! empty( $license ) && is_object( $license ) ) { $now = time(); $expiration = isset( $license->expires ) ? strtotime( $license->expires ) : false; // Special case: the license expired after the last check if ( $license->success && $expiration && $expiration < $now ) { $license->success = false; $license->error = 'expired'; } if ( false === $license->success ) { $class = 'notice-error notice-alt'; switch ( $license->error ) { case 'expired': $message = sprintf( /* translators: %1$s is a date, %2$s is link start tag, %3$s is link end tag. */ esc_html__( 'Your license key expired on %1$s. Please %2$srenew your license key%3$s.', 'polylang' ), esc_html( date_i18n( get_option( 'date_format' ), $expiration ) ), sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ), '</a>' ); break; case 'disabled': case 'revoked': $message = esc_html__( 'Your license key has been disabled.', 'polylang' ); break; case 'missing': $message = sprintf( /* translators: %1$s is link start tag, %2$s is link end tag. */ esc_html__( 'Invalid license. Please %1$svisit your account page%2$s and verify it.', 'polylang' ), sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ), '</a>' ); break; case 'invalid': case 'site_inactive': $message = sprintf( /* translators: %1$s is a product name, %2$s is link start tag, %3$s is link end tag. */ esc_html__( 'Your %1$s license key is not active for this URL. Please %2$svisit your account page%3$s to manage your license key URLs.', 'polylang' ), esc_html( $this->name ), sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ), '</a>' ); break; case 'item_name_mismatch': /* translators: %s is a product name */ $message = sprintf( esc_html__( 'This is not a %s license key.', 'polylang' ), esc_html( $this->name ) ); break; case 'no_activations_left': $message = sprintf( /* translators: %1$s is link start tag, %2$s is link end tag */ esc_html__( 'Your license key has reached its activation limit. %1$sView possible upgrades%2$s now.', 'polylang' ), sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ), '</a>' ); break; } } else { $class = 'license-valid'; $out .= sprintf( '<button id="deactivate_%s" type="button" class="button button-secondary pll-deactivate-license">%s</button>', esc_attr( $this->id ), esc_html__( 'Deactivate', 'polylang' ) ); if ( 'lifetime' === $license->expires ) { $message = esc_html__( 'The license key never expires.', 'polylang' ); } elseif ( $expiration > $now && $expiration - $now < ( DAY_IN_SECONDS * 30 ) ) { $class = 'notice-warning notice-alt'; $message = sprintf( /* translators: %1$s is a date, %2$s is link start tag, %3$s is link end tag. */ esc_html__( 'Your license key will expire soon! Precisely, it will expire on %1$s. %2$sRenew your license key today!%3$s', 'polylang' ), esc_html( date_i18n( get_option( 'date_format' ), $expiration ) ), sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ), '</a>' ); } else { $message = sprintf( /* translators: %s is a date */ esc_html__( 'Your license key expires on %s.', 'polylang' ), esc_html( date_i18n( get_option( 'date_format' ), $expiration ) ) ); } } } if ( ! empty( $message ) ) { $out .= '<p>' . $message . '</p>'; } return sprintf( '<tr id="pll-license-%s" class="%s">%s</tr>', esc_attr( $this->id ), $class, $out ); } } include/walker.php 0000644 00000004440 15136114124 0010165 0 ustar 00 <?php /** * @package Polylang */ /** * A class for displaying various tree-like language structures. * * Extend the `PLL_Walker` class to use it, and implement some of the methods from `Walker`. * See: {https://developer.wordpress.org/reference/classes/walker/#methods}. * * @since 3.4 */ class PLL_Walker extends Walker { /** * Database fields to use. * * @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields. * * @var string[] */ public $db_fields = array( 'parent' => 'parent', 'id' => 'id' ); /** * Overrides Walker::display_element as it expects an object with a parent property. * * @since 1.2 * @since 3.4 Refactored and moved in `PLL_Walker`. * * @param PLL_Language|stdClass $element Data object. `PLL_language` in our case. * @param array $children_elements List of elements to continue traversing. * @param int $max_depth Max depth to traverse. * @param int $depth Depth of current element. * @param array $args An array of arguments. * @param string $output Passed by reference. Used to append additional content. * @return void */ public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { if ( $element instanceof PLL_Language ) { $element = $element->to_std_class(); } $element->parent = $element->id = 0; // Don't care about this. parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); } /** * Sets `PLL_Walker::walk()` arguments as it should * and triggers an error in case of misuse of them. * * @since 3.4 * * @param array|int $max_depth The maximum hierarchical depth. Passed by reference. * @param array $args Additional arguments. Passed by reference. * @return void */ protected function maybe_fix_walk_args( &$max_depth, &$args ) { if ( ! is_array( $max_depth ) ) { $args = isset( $args[0] ) ? $args[0] : array(); return; } // Backward compatibility with Polylang < 2.6.7 _doing_it_wrong( __CLASS__ . '::walk()', 'The method expects an integer as second parameter.', '2.6.7' ); $args = $max_depth; $max_depth = -1; } } include/translatable-object.php 0000644 00000032431 15136114124 0012621 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Abstract class to use for object types that support at least one language. * * @since 3.4 * * @phpstan-type DBInfo array{ * table: non-empty-string, * id_column: non-empty-string, * default_alias: non-empty-string * } */ abstract class PLL_Translatable_Object { /** * @var PLL_Model */ public $model; /** * List of taxonomies to cache. * * @var string[] * @see PLL_Translatable_Object::get_object_term() * * @phpstan-var list<non-empty-string> */ protected $tax_to_cache = array(); /** * Taxonomy name for the languages. * * @var string * * @phpstan-var non-empty-string */ protected $tax_language; /** * Identifier that must be unique for each type of content. * Also used when checking capabilities. * * @var string * * @phpstan-var non-empty-string */ protected $type; /** * Identifier for each type of content to used for cache type. * * @var string * * @phpstan-var non-empty-string */ protected $cache_type; /** * Object type to use when registering the taxonomy. * Left empty for posts. * * @var string|null * * @phpstan-var non-empty-string|null */ protected $object_type = null; /** * Constructor. * * @since 3.4 * * @param PLL_Model $model Instance of `PLL_Model`, passed by reference. */ public function __construct( PLL_Model &$model ) { $this->model = $model; $this->tax_to_cache[] = $this->tax_language; /* * Register our taxonomy as soon as possible. * This is early registration, not ready for rewrite rules as $wp_rewrite will be setup later. */ register_taxonomy( $this->tax_language, (array) $this->object_type, array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true, ) ); } /** * Returns the language taxonomy name. * * @since 3.4 * * @return string * * @phpstan-return non-empty-string */ public function get_tax_language() { return $this->tax_language; } /** * Returns the type of object. * * @since 3.4 * * @return string * * @phpstan-return non-empty-string */ public function get_type() { return $this->type; } /** * Adds hooks. * * @since 3.4 * * @return static */ public function init() { return $this; } /** * Stores the object's language into the database. * * @since 3.4 * * @param int $id Object ID. * @param PLL_Language|string|int $lang Language (object, slug, or term ID). * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to * the object). */ public function set_language( $id, $lang ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return false; } $old_lang = $this->get_language( $id ); $old_lang = $old_lang ? $old_lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0; $lang = $this->model->get_language( $lang ); $lang = $lang ? $lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0; if ( $old_lang === $lang ) { return false; } $term_taxonomy_ids = wp_set_object_terms( $id, $lang, $this->tax_language ); wp_cache_set( 'last_changed', microtime(), $this->cache_type ); return is_array( $term_taxonomy_ids ); } /** * Returns the language of an object. * * @since 0.1 * @since 3.4 Renamed the parameter $post_id into $id. * * @param int $id Object ID. * @return PLL_Language|false A `PLL_Language` object. `false` if no language is associated to that object or if the * ID is invalid. */ public function get_language( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return false; } // Get the language and make sure it is a PLL_Language object. $lang = $this->get_object_term( $id, $this->tax_language ); if ( empty( $lang ) ) { return false; } return $this->model->get_language( $lang->term_id ); } /** * Removes the term language from the database. * * @since 3.4 * * @param int $id Term ID. * @return void */ public function delete_language( $id ) { $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return; } wp_delete_object_term_relationships( $id, $this->tax_language ); } /** * Wraps `wp_get_object_terms()` to cache it and return only one object. * Inspired by the WordPress function `get_the_terms()`. * * @since 1.2 * * @param int $id Object ID. * @param string $taxonomy Polylang taxonomy depending if we are looking for a post (or term, or else) language. * @return WP_Term|false The term associated to the object in the requested taxonomy if it exists, `false` otherwise. */ public function get_object_term( $id, $taxonomy ) { global $wp_version; $id = $this->sanitize_int_id( $id ); if ( empty( $id ) ) { return false; } $term = get_object_term_cache( $id, $taxonomy ); if ( is_array( $term ) ) { return ! empty( $term ) ? reset( $term ) : false; } // Query terms. $terms = array(); $term = false; $object_terms = wp_get_object_terms( $id, $this->tax_to_cache, array( 'update_term_meta_cache' => false ) ); if ( is_array( $object_terms ) ) { foreach ( $object_terms as $t ) { $terms[ $t->taxonomy ] = $t; if ( $t->taxonomy === $taxonomy ) { $term = $t; } } } // Stores it the way WP expects it. Set an empty cache if no term was found in the taxonomy. $store_only_term_ids = version_compare( $wp_version, '6.0', '>=' ); foreach ( $this->tax_to_cache as $tax ) { if ( empty( $terms[ $tax ] ) ) { $to_cache = array(); } elseif ( $store_only_term_ids ) { $to_cache = array( $terms[ $tax ]->term_id ); } else { // Backward compatibility with WP < 6.0. $to_cache = array( $terms[ $tax ] ); } wp_cache_add( $id, $to_cache, "{$tax}_relationships" ); } return $term; } /** * A JOIN clause to add to sql queries when filtering by language is needed directly in query. * * @since 3.4 * * @param string $alias Optional alias for object table. * @return string The JOIN clause. * * @phpstan-return non-empty-string */ public function join_clause( $alias = '' ) { global $wpdb; $db = $this->get_db_infos(); if ( empty( $alias ) ) { $alias = $db['default_alias']; } return " INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = {$alias}.{$db['id_column']}"; } /** * A WHERE clause to add to sql queries when filtering by language is needed directly in query. * * @since 1.2 * * @param PLL_Language|PLL_Language[]|string|string[] $lang A `PLL_Language` object, or a comma separated list of language slugs, or an array of language slugs or objects. * @return string The WHERE clause. * * @phpstan-param PLL_Language|PLL_Language[]|non-empty-string|non-empty-string[] $lang */ public function where_clause( $lang ) { /* * $lang is an object. * This is generally the case if the query is coming from Polylang. */ if ( $lang instanceof PLL_Language ) { return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) ); } /* * $lang is an array of objects, an array of slugs, or a comma separated list of slugs. * The comma separated list of slugs can happen if the query is coming from outside with a 'lang' parameter. */ $languages = is_array( $lang ) ? $lang : explode( ',', $lang ); $languages_tt_ids = array(); foreach ( $languages as $language ) { $language = $this->model->get_language( $language ); if ( ! empty( $language ) ) { $languages_tt_ids[] = absint( $language->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) ); } } if ( empty( $languages_tt_ids ) ) { return ''; } return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages_tt_ids ) . ' )'; } /** * Returns the IDs of the objects without language. * * @since 3.4 * * @param int $limit Max number of objects to return. `-1` to return all of them. * @param array $args The object args. * @return int[] Array of object IDs. * * @phpstan-param -1|positive-int $limit * @phpstan-return list<positive-int> */ public function get_objects_with_no_lang( $limit, array $args = array() ) { $language_ids = array(); foreach ( $this->model->get_languages_list() as $language ) { $language_ids[] = $language->get_tax_prop( $this->get_tax_language(), 'term_taxonomy_id' ); } $language_ids = array_filter( $language_ids ); if ( empty( $language_ids ) ) { return array(); } $sql = $this->get_objects_with_no_lang_sql( $language_ids, $limit, $args ); $object_ids = $this->query_objects_with_no_lang( $sql ); return array_values( $this->sanitize_int_ids_list( $object_ids ) ); } /** * Returns object IDs without language given a specific SQL query. * Can be overridden by child classes in case queried object doesn't use * `wp_cache_set_last_changed()` or another cache system. * * @since 3.4 * * @param string $sql A prepared SQL query for object IDs with no language. * @return string[] An array of numeric object IDs. */ protected function query_objects_with_no_lang( $sql ) { $key = md5( $sql ); $last_changed = wp_cache_get_last_changed( $this->cache_type ); $cache_key = "{$this->cache_type}_no_lang:{$key}:{$last_changed}"; $object_ids = wp_cache_get( $cache_key, $this->cache_type ); if ( is_array( $object_ids ) ) { return $object_ids; } $object_ids = $GLOBALS['wpdb']->get_col( $sql ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared wp_cache_set( $cache_key, $object_ids, $this->cache_type ); return $object_ids; } /** * Sanitizes an ID as positive integer. * Kind of similar to `absint()`, but rejects negetive integers instead of making them positive. * * @since 3.2 * * @param mixed $id A supposedly numeric ID. * @return int A positive integer. `0` for non numeric values and negative integers. * * @phpstan-return int<0,max> */ public function sanitize_int_id( $id ) { return is_numeric( $id ) && $id >= 1 ? abs( (int) $id ) : 0; } /** * Sanitizes an array of IDs as positive integers. * `0` values are removed. * * @since 3.2 * * @param mixed $ids An array of numeric IDs. * @return int[] * * @phpstan-return array<positive-int> */ public function sanitize_int_ids_list( $ids ) { if ( empty( $ids ) || ! is_array( $ids ) ) { return array(); } $ids = array_map( array( $this, 'sanitize_int_id' ), $ids ); return array_filter( $ids ); } /** * Returns SQL query that fetches the IDs of the objects without language. * * @since 3.4 * * @param int[] $language_ids List of language `term_taxonomy_id`. * @param int $limit Max number of objects to return. `-1` to return all of them. * @param array $args The object args. * @return string * * @phpstan-param array<positive-int> $language_ids * @phpstan-param -1|positive-int $limit * @phpstan-param array<empty> $args */ protected function get_objects_with_no_lang_sql( array $language_ids, $limit, array $args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $db = $this->get_db_infos(); return sprintf( "SELECT {$db['table']}.{$db['id_column']} FROM {$db['table']} WHERE {$db['table']}.{$db['id_column']} NOT IN ( SELECT object_id FROM {$GLOBALS['wpdb']->term_relationships} WHERE term_taxonomy_id IN (%s) ) %s", PLL_Db_Tools::prepare_values_list( $language_ids ), $limit >= 1 ? sprintf( 'LIMIT %d', $limit ) : '' ); } /** * Assigns a language to object in mass. * * @since 1.2 * @since 3.4 Moved from PLL_Admin_Model class. * * @param int[] $ids Array of post ids or term ids. * @param PLL_Language $lang Language to assign to the posts or terms. * @return void */ public function set_language_in_mass( $ids, $lang ) { global $wpdb; $tt_id = $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ); if ( empty( $tt_id ) ) { return; } $ids = array_map( 'intval', $ids ); $ids = array_filter( $ids ); if ( empty( $ids ) ) { return; } $values = array(); foreach ( $ids as $id ) { $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id ); } // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) ); // Updating term count is mandatory (thanks to AndyDeGroo). $lang->update_count(); clean_term_cache( $ids, $this->tax_language ); // Invalidate our cache. wp_cache_set( 'last_changed', microtime(), $this->cache_type ); } /** * Returns database-related information that can be used in some of this class methods. * These are specific to the table containing the objects. * * @see PLL_Translatable_Object::join_clause() * @see PLL_Translatable_Object::get_objects_with_no_lang_sql() * * @since 3.4.3 * * @return string[] { * @type string $table Name of the table. * @type string $id_column Name of the column containing the object's ID. * @type string $default_alias Default alias corresponding to the object's table. * } * @phpstan-return DBInfo */ abstract protected function get_db_infos(); } include/switcher.php 0000644 00000025617 15136114124 0010541 0 ustar 00 <?php /** * @package Polylang */ /** * A class to display a language switcher on frontend * * @since 1.2 */ class PLL_Switcher { const DEFAULTS = array( 'dropdown' => 0, // Display as list and not as dropdown. 'echo' => 1, // Echoes the list. 'hide_if_empty' => 1, // Hides languages with no posts (or pages). 'show_flags' => 0, // Don't show flags. 'show_names' => 1, // Show language names. 'display_names_as' => 'name', // Display the language name. 'force_home' => 0, // Tries to find a translation. 'hide_if_no_translation' => 0, // Don't hide the link if there is no translation. 'hide_current' => 0, // Don't hide the current language. 'post_id' => null, // Link to the translations of the current page. 'raw' => 0, // Build the language switcher. 'item_spacing' => 'preserve', // Preserve whitespace between list items. 'admin_render' => 0, // Make the switcher in a frontend context. 'admin_current_lang' => null, // Use the global current language. ); /** * @var PLL_Links|null */ protected $links; /** * Returns options available for the language switcher - menu or widget * either strings to display the options or default values * * @since 0.7 * * @param string $type optional either 'menu', 'widget' or 'block', defaults to 'widget' * @param string $key optional either 'string' or 'default', defaults to 'string' * @return array list of switcher options strings or default values */ public static function get_switcher_options( $type = 'widget', $key = 'string' ) { $options = array( 'dropdown' => array( 'string' => __( 'Displays as a dropdown', 'polylang' ), 'default' => 0 ), 'show_names' => array( 'string' => __( 'Displays language names', 'polylang' ), 'default' => 1 ), 'show_flags' => array( 'string' => __( 'Displays flags', 'polylang' ), 'default' => 0 ), 'force_home' => array( 'string' => __( 'Forces link to front page', 'polylang' ), 'default' => 0 ), 'hide_current' => array( 'string' => __( 'Hides the current language', 'polylang' ), 'default' => 0 ), 'hide_if_no_translation' => array( 'string' => __( 'Hides languages with no translation', 'polylang' ), 'default' => 0 ), ); return wp_list_pluck( $options, $key ); } /** * Returns the current language code. * * @since 3.0 * * @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}. * @return string */ protected function get_current_language( $args ) { if ( $args['admin_current_lang'] ) { return $args['admin_current_lang']; } if ( isset( $this->links->curlang ) ) { return $this->links->curlang->slug; } return $this->links->options['default_lang']; } /** * Returns the link for a given language. * * @since 3.0 * * @param PLL_Language $language Language. * @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}. * @return string|null */ protected function get_link( $language, $args ) { global $post; // Priority to the post passed in parameters. if ( null !== $args['post_id'] ) { $tr_id = $this->links->model->post->get( $args['post_id'], $language ); if ( $tr_id && $this->links->model->post->current_user_can_read( $tr_id ) ) { return get_permalink( $tr_id ); } } // If we are on frontend. if ( $this->links instanceof PLL_Frontend_Links ) { return $this->links->get_translation_url( $language ); } // For blocks in posts in REST requests. if ( $post instanceof WP_Post ) { $tr_id = $this->links->model->post->get( $post->ID, $language ); if ( $tr_id && $this->links->model->post->current_user_can_read( $tr_id ) ) { return get_permalink( $tr_id ); } } return null; } /** * Get the language elements for use in a walker * * @since 1.2 * * @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}. * @return array Language switcher elements. */ protected function get_elements( $args ) { $first = true; $out = array(); foreach ( $this->links->model->get_languages_list( array( 'hide_empty' => $args['hide_if_empty'] ) ) as $language ) { $id = (int) $language->term_id; $order = (int) $language->term_group; $slug = $language->slug; $locale = $language->get_locale( 'display' ); $item_classes = array( 'lang-item', 'lang-item-' . $id, 'lang-item-' . esc_attr( $slug ) ); $classes = isset( $args['classes'] ) && is_array( $args['classes'] ) ? array_merge( $item_classes, $args['classes'] ) : $item_classes; $link_classes = isset( $args['link_classes'] ) ? $args['link_classes'] : array(); $current_lang = $this->get_current_language( $args ) === $slug; if ( $current_lang ) { if ( $args['hide_current'] && ! ( $args['dropdown'] && ! $args['raw'] ) ) { continue; // Hide current language except for dropdown } else { $classes[] = 'current-lang'; } } $url = $this->get_link( $language, $args ); if ( $no_translation = empty( $url ) ) { $classes[] = 'no-translation'; } /** * Filter the link in the language switcher * * @since 0.7 * * @param string|null $url The link, null if no translation was found. * @param string $slug The language code. * @param string $locale The language locale */ $url = apply_filters( 'pll_the_language_link', $url, $slug, $language->locale ); // Hide if no translation exists if ( empty( $url ) && $args['hide_if_no_translation'] ) { continue; } $url = empty( $url ) || $args['force_home'] ? $this->links->get_home_url( $language ) : $url; // If the page is not translated, link to the home page $name = $args['show_names'] || ! $args['show_flags'] || $args['raw'] ? ( 'slug' == $args['display_names_as'] ? $slug : $language->name ) : ''; if ( $args['raw'] && ! $args['show_flags'] ) { $flag = $language->get_display_flag_url(); } elseif ( $args['show_flags'] ) { $flag = $language->get_display_flag( empty( $args['show_names'] ) ? 'alt' : 'no-alt' ); } else { $flag = ''; } if ( $first ) { $classes[] = 'lang-item-first'; $first = false; } $out[ $slug ] = compact( 'id', 'order', 'slug', 'locale', 'name', 'url', 'flag', 'current_lang', 'no_translation', 'classes', 'link_classes' ); } return $out; } /** * Displays a language switcher * or returns the raw elements to build a custom language switcher. * * @since 0.1 * * @param PLL_Links $links Instance of PLL_Links. * @param array $args { * Optional array of arguments. * * @type int $dropdown The list is displayed as dropdown if set, defaults to 0. * @type int $echo Echoes the list if set to 1, defaults to 1. * @type int $hide_if_empty Hides languages with no posts ( or pages ) if set to 1, defaults to 1. * @type int $show_flags Displays flags if set to 1, defaults to 0. * @type int $show_names Shows language names if set to 1, defaults to 1. * @type string $display_names_as Whether to display the language name or its slug, valid options are 'slug' and 'name', defaults to name. * @type int $force_home Will always link to home in translated language if set to 1, defaults to 0. * @type int $hide_if_no_translation Hides the link if there is no translation if set to 1, defaults to 0. * @type int $hide_current Hides the current language if set to 1, defaults to 0. * @type int $post_id Returns links to the translations of the post defined by post_id if set, defaults not set. * @type int $raw Return a raw array instead of html markup if set to 1, defaults to 0. * @type string $item_spacing Whether to preserve or discard whitespace between list items, valid options are 'preserve' and 'discard', defaults to 'preserve'. * @type int $admin_render Allows to force the current language code in an admin context if set, default to 0. Need to set the admin_current_lang argument below. * @type string $admin_current_lang The current language code in an admin context. Need to set the admin_render to 1, defaults not set. * @type string[] $classes A list of CSS classes to set to each elements outputted. * @type string[] $link_classes A list of CSS classes to set to each link outputted. * } * @return string|array either the html markup of the switcher or the raw elements to build a custom language switcher */ public function the_languages( $links, $args = array() ) { $this->links = $links; $args = wp_parse_args( $args, self::DEFAULTS ); /** * Filter the arguments of the 'pll_the_languages' template tag * * @since 1.5 * * @param array $args */ $args = apply_filters( 'pll_the_languages_args', $args ); // Force not to hide the language for the widget preview even if the option is checked. if ( $this->links instanceof PLL_Admin_Links ) { $args['hide_if_no_translation'] = 0; } // Prevents showing empty options in `<select>`. if ( $args['dropdown'] && ! $args['raw'] ) { $args['show_names'] = 1; } $elements = $this->get_elements( $args ); if ( $args['raw'] ) { return $elements; } if ( $args['dropdown'] ) { $args['name'] = 'lang_choice_' . $args['dropdown']; $args['class'] = 'pll-switcher-select'; $args['value'] = 'url'; $args['selected'] = $this->get_link( $this->links->model->get_language( $this->get_current_language( $args ) ), $args ); $walker = new PLL_Walker_Dropdown(); } else { $walker = new PLL_Walker_List(); } // Cast each element to stdClass because $walker::walk() expects an array of objects. foreach ( $elements as $i => $element ) { $elements[ $i ] = (object) $element; } /** * Filter the whole html markup returned by the 'pll_the_languages' template tag * * @since 0.8 * * @param string $html html returned/outputted by the template tag * @param array $args arguments passed to the template tag */ $out = apply_filters( 'pll_the_languages', $walker->walk( $elements, -1, $args ), $args ); // Javascript to switch the language when using a dropdown list. if ( $args['dropdown'] && 0 === $args['admin_render'] ) { // Accept only few valid characters for the urls_x variable name (as the widget id includes '-' which is invalid). $out .= sprintf( '<script%1$s> document.getElementById( "%2$s" ).addEventListener( "change", function ( event ) { location.href = event.currentTarget.value; } ) </script>', current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"', esc_js( $args['name'] ) ); } if ( $args['echo'] ) { echo $out; // phpcs:ignore WordPress.Security.EscapeOutput } return $out; } } include/model.php 0000644 00000103533 15136114124 0010003 0 ustar 00 <?php /** * @package Polylang */ /** * Setups the language and translations model based on WordPress taxonomies * * @since 1.2 */ class PLL_Model { /** * Internal non persistent cache object. * * @var PLL_Cache<mixed> */ public $cache; /** * Stores the plugin options. * * @var array */ public $options; /** * Translatable objects registry. * * @since 3.4 * * @var PLL_Translatable_Objects */ public $translatable_objects; /** * Translated post model. * * @var PLL_Translated_Post */ public $post; /** * Translated term model. * * @var PLL_Translated_Term */ public $term; /** * Flag set to true during the language objects creation. * * @var bool */ private $is_creating_language_objects = false; /** * Tells if {@see PLL_Model::get_languages_list()} can be used. * * @var bool */ private $languages_ready = false; /** * Constructor. * Setups translated objects sub models. * Setups filters and actions. * * @since 1.2 * * @param array $options Polylang options. */ public function __construct( &$options ) { $this->options = &$options; $this->cache = new PLL_Cache(); $this->translatable_objects = new PLL_Translatable_Objects(); $this->post = $this->translatable_objects->register( new PLL_Translated_Post( $this ) ); // Translated post sub model. $this->term = $this->translatable_objects->register( new PLL_Translated_Term( $this ) ); // Translated term sub model. // We need to clean languages cache when editing a language and when modifying the permalink structure. add_action( 'edited_term_taxonomy', array( $this, 'clean_languages_cache' ), 10, 2 ); add_action( 'update_option_permalink_structure', array( $this, 'clean_languages_cache' ) ); add_action( 'update_option_siteurl', array( $this, 'clean_languages_cache' ) ); add_action( 'update_option_home', array( $this, 'clean_languages_cache' ) ); add_filter( 'get_terms_args', array( $this, 'get_terms_args' ) ); // Just in case someone would like to display the language description ;). add_filter( 'language_description', '__return_empty_string' ); } /** * Checks if there are languages or not. * * @since 3.3 * * @return bool True if there are, false otherwise. */ public function has_languages() { if ( ! empty( $this->cache->get( 'languages' ) ) ) { return true; } if ( ! empty( get_transient( 'pll_languages_list' ) ) ) { return true; } return ! empty( $this->get_language_terms() ); } /** * Returns the list of available languages. * - Stores the list in a db transient (except flags), unless `PLL_CACHE_LANGUAGES` is set to false. * - Caches the list (with flags) in a `PLL_Cache` object. * * @since 0.1 * * @param array $args { * @type bool $hide_empty Hides languages with no posts if set to `true` (defaults to `false`). * @type bool $hide_default Hides default language from the list (default to `false`). * @type string $fields Returns only that field if set; {@see PLL_Language} for a list of fields. * } * @return array List of PLL_Language objects or PLL_Language object properties. */ public function get_languages_list( $args = array() ) { if ( ! $this->are_languages_ready() ) { _doing_it_wrong( __METHOD__ . '()', "It must not be called before the hook 'pll_pre_init'.", '3.4' ); } $languages = $this->cache->get( 'languages' ); if ( ! is_array( $languages ) ) { // Bail out early if languages are currently created to avoid an infinite loop. if ( $this->is_creating_language_objects ) { return array(); } $this->is_creating_language_objects = true; if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) ) { // Create the languages from taxonomies. $languages = $this->get_languages_from_taxonomies(); } else { $languages = get_transient( 'pll_languages_list' ); if ( empty( $languages ) || ! is_array( $languages ) || empty( reset( $languages )['term_props'] ) ) { // Test `term_props` in case we got a transient older than 3.4. // Create the languages from taxonomies. $languages = $this->get_languages_from_taxonomies(); } else { // Create the languages directly from arrays stored in the transient. $languages = array_map( array( new PLL_Language_Factory( $this->options ), 'get' ), $languages ); // Remove potential empty language. $languages = array_filter( $languages ); // Re-index. $languages = array_values( $languages ); } } /** * Filters the list of languages *after* it is stored in the persistent cache. * /!\ This filter is fired *before* the $polylang object is available. * * @since 1.8 * @since 3.4 Deprecated. If you used this hook to filter URLs, you may hook `'site_url'` instead. * @deprecated * * @param PLL_Language[] $languages The list of language objects. */ $languages = apply_filters_deprecated( 'pll_after_languages_cache', array( $languages ), '3.4' ); if ( $this->are_languages_ready() ) { $this->cache->set( 'languages', $languages ); } $this->is_creating_language_objects = false; } $languages = array_filter( $languages, function ( $lang ) use ( $args ) { $keep_empty = empty( $args['hide_empty'] ) || $lang->get_tax_prop( 'language', 'count' ); $keep_default = empty( $args['hide_default'] ) || ! $lang->is_default; return $keep_empty && $keep_default; } ); $languages = array_values( $languages ); // Re-index. return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] ); } /** * Tells if {@see PLL_Model::get_languages_list()} can be used. * * @since 3.4 * * @return bool */ public function are_languages_ready() { return $this->languages_ready; } /** * Sets the internal property `$languages_ready` to `true`, telling that {@see PLL_Model::get_languages_list()} can be used. * * @since 3.4 * * @return void */ public function set_languages_ready() { $this->languages_ready = true; } /** * Cleans language cache * can be called directly with no parameter * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated * * @since 1.2 * * @param int $term not used * @param string $taxonomy taxonomy name * @return void */ public function clean_languages_cache( $term = 0, $taxonomy = null ) { if ( empty( $taxonomy ) || 'language' === $taxonomy ) { delete_transient( 'pll_languages_list' ); $this->cache->clean(); } } /** * Don't query term metas when only our taxonomies are queried * * @since 2.3 * * @param array $args WP_Term_Query arguments * @return array */ public function get_terms_args( $args ) { $taxonomies = $this->translatable_objects->get_taxonomy_names(); if ( isset( $args['taxonomy'] ) && ! array_diff( (array) $args['taxonomy'], $taxonomies ) ) { $args['update_term_meta_cache'] = false; } return $args; } /** * Returns the language by its term_id, tl_term_id, slug or locale. * * @since 0.1 * @since 3.4 Allow to get a language by `term_taxonomy_id`. * * @param mixed $value `term_id`, `term_taxonomy_id`, `slug`, `locale`, or `w3c` of the queried language. * `term_id` and `term_taxonomy_id` can be fetched for any language taxonomy. * /!\ For the `term_taxonomy_id`, prefix the ID by `tt:` (ex: `"tt:{$tt_id}"`), * this is to prevent confusion between `term_id` and `term_taxonomy_id`. * @return PLL_Language|false Language object, false if no language found. */ public function get_language( $value ) { if ( is_object( $value ) ) { return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // Will force cast to PLL_Language. } $return = $this->cache->get( 'language:' . $value ); if ( $return instanceof PLL_Language ) { return $return; } foreach ( $this->get_languages_list() as $lang ) { foreach ( $lang->get_tax_props() as $props ) { $this->cache->set( 'language:' . $props['term_id'], $lang ); $this->cache->set( 'language:tt:' . $props['term_taxonomy_id'], $lang ); } $this->cache->set( 'language:' . $lang->slug, $lang ); $this->cache->set( 'language:' . $lang->locale, $lang ); $this->cache->set( 'language:' . $lang->w3c, $lang ); } /** @var PLL_Language|false */ return $this->cache->get( 'language:' . $value ); } /** * Returns the default language. * * @since 3.4 * * @return PLL_Language|false Default language object, `false` if no language found. */ public function get_default_language() { if ( empty( $this->options['default_lang'] ) ) { return false; } return $this->get_language( $this->options['default_lang'] ); } /** * Adds terms clauses to the term query to filter them by languages. * * @since 1.2 * * @param string[] $clauses The list of sql clauses in terms query. * @param PLL_Language|false $lang PLL_Language object. * @return string[] Modified list of clauses. */ public function terms_clauses( $clauses, $lang ) { if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) { $clauses['join'] .= $this->term->join_clause(); $clauses['where'] .= $this->term->where_clause( $lang ); } return $clauses; } /** * Returns post types that need to be translated. * The post types list is cached for better better performance. * The method waits for 'after_setup_theme' to apply the cache * to allow themes adding the filter in functions.php. * * @since 1.2 * * @param bool $filter True if we should return only valid registered post types. * @return string[] Post type names for which Polylang manages languages and translations. */ public function get_translated_post_types( $filter = true ) { return $this->translatable_objects->get( 'post' )->get_translated_object_types( $filter ); } /** * Returns true if Polylang manages languages and translations for this post type. * * @since 1.2 * * @param string|string[] $post_type Post type name or array of post type names. * @return bool */ public function is_translated_post_type( $post_type ) { if ( empty( array_filter( (array) $post_type ) ) ) { return false; } /** @var non-empty-array<non-empty-string>|non-empty-string $post_type */ return $this->translatable_objects->get( 'post' )->is_translated_object_type( $post_type ); } /** * Returns taxonomies that need to be translated. * The taxonomies list is cached for better better performance. * The method waits for 'after_setup_theme' to apply the cache * to allow themes adding the filter in functions.php. * * @since 1.2 * * @param bool $filter True if we should return only valid registered taxonomies. * @return string[] Array of registered taxonomy names for which Polylang manages languages and translations. */ public function get_translated_taxonomies( $filter = true ) { return $this->translatable_objects->get( 'term' )->get_translated_object_types( $filter ); } /** * Returns true if Polylang manages languages and translations for this taxonomy. * * @since 1.2 * * @param string|string[] $tax Taxonomy name or array of taxonomy names. * @return bool */ public function is_translated_taxonomy( $tax ) { if ( empty( array_filter( (array) $tax ) ) ) { return false; } /** @var non-empty-array<non-empty-string>|non-empty-string $tax */ return $this->translatable_objects->get( 'term' )->is_translated_object_type( $tax ); } /** * Return taxonomies that need to be filtered (post_format like). * * @since 1.7 * * @param bool $filter True if we should return only valid registered taxonomies. * @return string[] Array of registered taxonomy names. */ public function get_filtered_taxonomies( $filter = true ) { if ( did_action( 'after_setup_theme' ) ) { static $taxonomies = null; } if ( empty( $taxonomies ) ) { $taxonomies = array( 'post_format' => 'post_format' ); /** * Filters the list of taxonomies not translatable but filtered by language. * Includes only the post format by default * The filter must be added soon in the WordPress loading process: * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes. * * @since 1.7 * * @param string[] $taxonomies List of taxonomy names. * @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings. */ $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false ); } return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies; } /** * Returns true if Polylang filters this taxonomy per language. * * @since 1.7 * * @param string|string[] $tax Taxonomy name or array of taxonomy names. * @return bool */ public function is_filtered_taxonomy( $tax ) { $taxonomies = $this->get_filtered_taxonomies( false ); return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) ); } /** * Returns the query vars of all filtered taxonomies. * * @since 1.7 * * @return string[] */ public function get_filtered_taxonomies_query_vars() { $query_vars = array(); foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) { $tax = get_taxonomy( $filtered_tax ); if ( ! empty( $tax ) && is_string( $tax->query_var ) ) { $query_vars[] = $tax->query_var; } } return $query_vars; } /** * It is possible to have several terms with the same name in the same taxonomy ( one per language ) * but the native term_exists() will return true even if only one exists. * So here the function adds the language parameter. * * @since 1.4 * * @param string $term_name The term name. * @param string $taxonomy Taxonomy name. * @param int $parent Parent term id. * @param string|PLL_Language $language The language slug or object. * @return int The `term_id` of the found term. 0 otherwise. * * @phpstan-return int<0, max> */ public function term_exists( $term_name, $taxonomy, $parent, $language ) { global $wpdb; $language = $this->get_language( $language ); if ( empty( $language ) ) { return 0; } $term_name = trim( wp_unslash( $term_name ) ); $term_name = _wp_specialchars( $term_name ); $select = "SELECT t.term_id FROM $wpdb->terms AS t"; $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id"; $join .= $this->term->join_clause(); $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name ); $where .= $this->term->where_clause( $language ); if ( $parent > 0 ) { $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent ); } // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $term_id = $wpdb->get_var( $select . $join . $where ); return max( 0, (int) $term_id ); } /** * Checks if a term slug exists in a given language, taxonomy, hierarchy. * * @since 1.9 * @since 2.8 Moved from PLL_Share_Term_Slug::term_exists() to PLL_Model::term_exists_by_slug(). * * @param string $slug The term slug to test. * @param string|PLL_Language $language The language slug or object. * @param string $taxonomy Optional taxonomy name. * @param int $parent Optional parent term id. * @return int The `term_id` of the found term. 0 otherwise. */ public function term_exists_by_slug( $slug, $language, $taxonomy = '', $parent = 0 ) { global $wpdb; $language = $this->get_language( $language ); if ( empty( $language ) ) { return 0; } $select = "SELECT t.term_id FROM {$wpdb->terms} AS t"; $join = " INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id"; $join .= $this->term->join_clause(); $where = $wpdb->prepare( ' WHERE t.slug = %s', $slug ); $where .= $this->term->where_clause( $language ); if ( ! empty( $taxonomy ) ) { $where .= $wpdb->prepare( ' AND tt.taxonomy = %s', $taxonomy ); } if ( $parent > 0 ) { $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent ); } // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_var( $select . $join . $where ); } /** * Returns the number of posts per language in a date, author or post type archive. * * @since 1.2 * * @param PLL_Language $lang PLL_Language instance. * @param array $q { * WP_Query arguments: * * @type string|string[] $post_type Post type or array of post types. * @type int $m Combination YearMonth. Accepts any four-digit year and month. * @type int $year Four-digit year. * @type int $monthnum Two-digit month. * @type int $day Day of the month. * @type int $author Author id. * @type string $author_name User 'user_nicename'. * @type string $post_format Post format. * @type string $post_status Post status. * } * @return int * * @phpstan-param array{ * post_type?: non-falsy-string|array<non-falsy-string>, * post_status?: non-falsy-string, * m?: numeric-string, * year?: positive-int, * monthnum?: int<1, 12>, * day?: int<1, 31>, * author?: int<1, max>, * author_name?: non-falsy-string, * post_format?: non-falsy-string * } $q * @phpstan-return int<0, max> */ public function count_posts( $lang, $q = array() ) { global $wpdb; $q = array_merge( array( 'post_type' => 'post', 'post_status' => 'publish' ), $q ); if ( ! is_array( $q['post_type'] ) ) { $q['post_type'] = array( $q['post_type'] ); } foreach ( $q['post_type'] as $key => $type ) { if ( ! post_type_exists( $type ) ) { unset( $q['post_type'][ $key ] ); } } if ( empty( $q['post_type'] ) ) { $q['post_type'] = array( 'post' ); // We *need* a post type. } $cache_key = $this->cache->get_unique_key( 'pll_count_posts_', $q ); $counts = wp_cache_get( $cache_key, 'counts' ); if ( ! is_array( $counts ) ) { $counts = array(); $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}"; $join = $this->post->join_clause(); $where = sprintf( " WHERE post_status = '%s'", esc_sql( $q['post_status'] ) ); $where .= sprintf( " AND {$wpdb->posts}.post_type IN ( '%s' )", implode( "', '", esc_sql( $q['post_type'] ) ) ); $where .= $this->post->where_clause( $this->get_languages_list() ); $groupby = ' GROUP BY pll_tr.term_taxonomy_id'; if ( ! empty( $q['m'] ) ) { $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] ); $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) ); if ( strlen( $q['m'] ) > 5 ) { $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) ); } if ( strlen( $q['m'] ) > 7 ) { $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) ); } } if ( ! empty( $q['year'] ) ) { $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] ); } if ( ! empty( $q['monthnum'] ) ) { $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] ); } if ( ! empty( $q['day'] ) ) { $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] ); } if ( ! empty( $q['author_name'] ) ) { $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) ); if ( $author ) { $q['author'] = $author->ID; } } if ( ! empty( $q['author'] ) ) { $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] ); } // Filtered taxonomies ( post_format ). foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) { if ( ! empty( $q[ $tax_qv ] ) ) { $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID"; $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id"; $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id"; $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] ); } } // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A ); foreach ( (array) $res as $row ) { $counts[ $row['term_taxonomy_id'] ] = $row['num_posts']; } wp_cache_set( $cache_key, $counts, 'counts' ); } $term_taxonomy_id = $lang->get_tax_prop( 'language', 'term_taxonomy_id' ); return empty( $counts[ $term_taxonomy_id ] ) ? 0 : $counts[ $term_taxonomy_id ]; } /** * Setup the links model based on options. * * @since 1.2 * * @return PLL_Links_Model */ public function get_links_model() { $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' ); $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default'; /** * Filters the links model class to use. * /!\ this filter is fired *before* the $polylang object is available. * * @since 2.1.1 * * @param string $class A class name: PLL_Links_Default, PLL_Links_Directory, PLL_Links_Subdomain, PLL_Links_Domain. */ $class = apply_filters( 'pll_links_model', $class ); return new $class( $this ); } /** * Returns a list of object IDs without language (used in settings and wizard). * * @since 0.9 * @since 2.2.6 Added the `$limit` parameter. * @since 3.4 Added the `$types` parameter. * * @param int $limit Optional. Max number of IDs to return. Defaults to -1 (no limit). * @param string[] $types Optional. Types to handle (@see PLL_Translatable_Object::get_type()). Defaults to * an empty array (all types). * @return int[][]|false { * IDs of objects without language. * * @type int[] $posts Array of post ids. * @type int[] $terms Array of term ids. * } * * @phpstan-param -1|positive-int $limit */ public function get_objects_with_no_lang( $limit = -1, array $types = array() ) { /** * Filters the max number of IDs to return when searching objects with no language. * This filter can be used to decrease the memory usage in case the number of objects * without language is too big. Using a negative value is equivalent to have no limit. * * @since 2.2.6 * @since 3.4 Added the `$types` parameter. * * @param int $limit Max number of IDs to retrieve from the database. * @param string[] $types Types to handle (@see PLL_Translatable_Object::get_type()). An empty array means all * types. */ $limit = apply_filters( 'get_objects_with_no_lang_limit', $limit, $types ); $limit = $limit < 1 ? -1 : max( (int) $limit, 1 ); $objects = array(); foreach ( $this->translatable_objects as $type => $object ) { if ( ! empty( $types ) && ! in_array( $type, $types, true ) ) { continue; } $ids = $object->get_objects_with_no_lang( $limit ); if ( empty( $ids ) ) { continue; } // The trailing 's' in the array key is for backward compatibility. $objects[ "{$type}s" ] = $ids; } $objects = ! empty( $objects ) ? $objects : false; /** * Filters the list of IDs of untranslated objects. * * @since 0.9 * @since 3.4 Added the `$limit` and `$types` parameters. * * @param int[][]|false $objects List of lists of object IDs, `false` if no IDs found. * @param int $limit Max number of IDs to retrieve from the database. * @param string[] $types Types to handle (@see PLL_Translatable_Object::get_type()). An empty array * means all types. */ return apply_filters( 'pll_get_objects_with_no_lang', $objects, $limit, $types ); } /** * Returns ids of post without language. * * @since 3.1 * * @param string|string[] $post_types A translated post type or an array of translated post types. * @param int $limit Max number of objects to return. `-1` to return all of them. * @return int[] * * @phpstan-param -1|positive-int $limit * @phpstan-return list<positive-int> */ public function get_posts_with_no_lang( $post_types, $limit ) { return $this->translatable_objects->get( 'post' )->get_objects_with_no_lang( $limit, (array) $post_types ); } /** * Returns ids of terms without language. * * @since 3.1 * * @param string|string[] $taxonomies A translated taxonomy or an array of taxonomies post types. * @param int $limit Max number of objects to return. `-1` to return all of them. * @return int[] * * @phpstan-param -1|positive-int $limit * @phpstan-return list<positive-int> */ public function get_terms_with_no_lang( $taxonomies, $limit ) { return $this->translatable_objects->get( 'term' )->get_objects_with_no_lang( $limit, (array) $taxonomies ); } /** * Assigns the default language to objects in mass. * * @since 1.2 * @since 3.4 Moved from PLL_Admin_Model class. * Removed `$limit` parameter, added `$lang` and `$types` parameters. * * @param PLL_Language|null $lang Optional. The language to assign to objects. Defaults to `null` (default language). * @param string[] $types Optional. Types to handle (@see PLL_Translatable_Object::get_type()). Defaults * to an empty array (all types). * @return void */ public function set_language_in_mass( $lang = null, array $types = array() ) { if ( ! $lang instanceof PLL_Language ) { $lang = $this->get_default_language(); if ( empty( $lang ) ) { return; } } // 1000 is an arbitrary value that will be filtered by `get_objects_with_no_lang_limit`. $nolang = $this->get_objects_with_no_lang( 1000, $types ); if ( empty( $nolang ) ) { return; } /** * Keep track of types where we set the language: * those are types where we may have more items to process if we have more than 1000 items in total. * This will prevent unnecessary SQL queries in the next recursion: if we have 0 items in this recursion for * a type, we'll still have 0 in the next one, no need for a new query. */ $types_with_objects = array(); foreach ( $this->translatable_objects as $type => $object ) { if ( empty( $nolang[ "{$type}s" ] ) ) { continue; } if ( ! empty( $types ) && ! in_array( $type, $types, true ) ) { continue; } $object->set_language_in_mass( $nolang[ "{$type}s" ], $lang ); $types_with_objects[] = $type; } if ( empty( $types_with_objects ) ) { return; } $this->set_language_in_mass( $lang, $types_with_objects ); } /** * Filters the ORDERBY clause of the languages query. * * This allows to order languages terms by `taxonomy` first then by `term_group` and `term_id`. * Ordering terms by taxonomy allows not to mix terms between all language taxomonomies. * Having the "language' taxonomy first is important for {@see PLL_Admin_Model:delete_language()}. * * @since 3.2.3 * * @param string $orderby `ORDERBY` clause of the terms query. * @param array $args An array of term query arguments. * @param string[] $taxonomies An array of taxonomy names. * @return string */ public function filter_language_terms_orderby( $orderby, $args, $taxonomies ) { $allowed_taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'language' ) ); if ( ! is_array( $taxonomies ) || ! empty( array_diff( $taxonomies, $allowed_taxonomies ) ) ) { return $orderby; } if ( empty( $orderby ) || ! is_string( $orderby ) ) { return $orderby; } if ( ! preg_match( '@^(?<alias>[^.]+)\.term_group$@', $orderby, $matches ) ) { return $orderby; } return sprintf( 'tt.taxonomy = \'language\' DESC, %1$s.term_group, %1$s.term_id', $matches['alias'] ); } /** * Maybe adds the missing language terms for 3rd party language taxonomies. * * @since 3.4 * * @return void */ public function maybe_create_language_terms() { $registered_taxonomies = array_diff( $this->translatable_objects->get_taxonomy_names( array( 'language' ) ), // Exclude the post and term language taxonomies from the list. array( $this->post->get_tax_language(), $this->term->get_tax_language() ) ); if ( empty( $registered_taxonomies ) ) { // No 3rd party language taxonomies. return; } // We have at least one 3rd party language taxonomy. $known_taxonomies = ! empty( $this->options['language_taxonomies'] ) && is_array( $this->options['language_taxonomies'] ) ? $this->options['language_taxonomies'] : array(); $new_taxonomies = array_diff( $registered_taxonomies, $known_taxonomies ); if ( empty( $new_taxonomies ) ) { // No new 3rd party language taxonomies. return; } // We have at least one unknown 3rd party language taxonomy. foreach ( $this->get_languages_list() as $language ) { $this->update_secondary_language_terms( $language->slug, $language->name, $language, $new_taxonomies ); } // Clear the cache, so the new `term_id` and `term_taxonomy_id` appear in the languages list. $this->clean_languages_cache(); // Keep the previous values, so this is triggered only once per taxonomy. $this->options['language_taxonomies'] = array_merge( $known_taxonomies, $new_taxonomies ); update_option( 'polylang', $this->options ); } /** * Updates or adds new terms for a secondary language taxonomy (aka not 'language'). * * @since 3.4 * * @param string $slug Language term slug (with or without the `pll_` prefix). * @param string $name Language name (label). * @param PLL_Language|null $language Optional. A language object. Required to update the existing terms. * @param string[] $taxonomies Optional. List of language taxonomies to deal with. An empty value means * all of them. Defaults to all taxonomies. * @return void * * @phpstan-param non-empty-string $slug * @phpstan-param non-empty-string $name * @phpstan-param array<non-empty-string> $taxonomies */ protected function update_secondary_language_terms( $slug, $name, PLL_Language $language = null, array $taxonomies = array() ) { $slug = 0 === strpos( $slug, 'pll_' ) ? $slug : "pll_$slug"; foreach ( $this->translatable_objects->get_secondary_translatable_objects() as $object ) { if ( ! empty( $taxonomies ) && ! in_array( $object->get_tax_language(), $taxonomies, true ) ) { // Not in the list. continue; } if ( ! empty( $language ) ) { $term_id = $language->get_tax_prop( $object->get_tax_language(), 'term_id' ); } else { $term_id = 0; } if ( empty( $term_id ) ) { // Attempt to repair the language if a term has been deleted by a database cleaning tool. wp_insert_term( $name, $object->get_tax_language(), array( 'slug' => $slug ) ); continue; } /** @var PLL_Language $language */ if ( "pll_{$language->slug}" !== $slug || $language->name !== $name ) { // Something has changed. wp_update_term( $term_id, $object->get_tax_language(), array( 'slug' => $slug, 'name' => $name ) ); } } } /** * Returns the list of available languages, based on the language taxonomy terms. * Stores the list in a db transient and in a `PLL_Cache` object. * * @since 3.4 * * @return PLL_Language[] An array of `PLL_Language` objects, array keys are the type. * * @phpstan-return list<PLL_Language> */ protected function get_languages_from_taxonomies() { $terms_by_slug = array(); foreach ( $this->get_language_terms() as $term ) { // Except for language taxonomy term slugs, remove 'pll_' prefix from the other language taxonomy term slugs. $key = 'language' === $term->taxonomy ? $term->slug : substr( $term->slug, 4 ); $terms_by_slug[ $key ][ $term->taxonomy ] = $term; } /** * @var ( * array{ * string: array{ * language: WP_Term, * }&array<non-empty-string, WP_Term> * } * ) $terms_by_slug */ $languages = array_filter( array_map( array( new PLL_Language_Factory( $this->options ), 'get_from_terms' ), array_values( $terms_by_slug ) ) ); /** * Filters the list of languages *before* it is stored in the persistent cache. * /!\ This filter is fired *before* the $polylang object is available. * * @since 1.7.5 * @since 3.4 Deprecated. * @deprecated * * @param PLL_Language[] $languages The list of language objects. * @param PLL_Model $model PLL_Model object. */ $languages = apply_filters_deprecated( 'pll_languages_list', array( $languages, $this ), '3.4', 'pll_additional_language_data' ); if ( ! $this->are_languages_ready() ) { // Do not cache an incomplete list. /** @var list<PLL_Language> $languages */ return $languages; } /** * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache. * Thanks to captin411 for catching this! * * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255 */ $languages_data = array_map( function ( $language ) { return $language->to_array( 'db' ); }, $languages ); set_transient( 'pll_languages_list', $languages_data ); /** @var list<PLL_Language> $languages */ return $languages; } /** * Returns the list of existing language terms. * - Returns all terms, that are or not assigned to posts. * - Terms are ordered by `term_group` and `term_id` (see `PLL_Model->filter_language_terms_orderby()`). * * @since 3.2.3 * * @return WP_Term[] */ protected function get_language_terms() { add_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ), 10, 3 ); $terms = get_terms( array( 'taxonomy' => $this->translatable_objects->get_taxonomy_names( array( 'language' ) ), 'orderby' => 'term_group', 'hide_empty' => false, ) ); remove_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ) ); return empty( $terms ) || is_wp_error( $terms ) ? array() : $terms; } } include/filter-rest-routes.php 0000644 00000011574 15136114124 0012465 0 ustar 00 <?php /** * @package Polylang */ /** * Class to manage REST routes filterable by language. * * @since 3.5 */ class PLL_Filter_REST_Routes { /** * REST routes filterable by language ordered by entity type. * * @var string[] * @phpstan-var array<string, string> */ private $filtered_entities = array(); /** * Other REST routes filterable by language. * * @var string[] * @phpstan-var array<string, string> */ private $filtered_routes = array(); /** * @var PLL_Model */ private $model; /** * Constructor. * * @since 3.5 * * @param PLL_Model $model Shared instance of the current PLL_Model. */ public function __construct( PLL_Model $model ) { $this->model = $model; // Adds search REST endpoint. $this->filtered_routes['search'] = 'wp/v2/search'; } /** * Adds query parameters to preload paths. * * @since 3.5 * * @param (string|string[])[] $preload_paths Array of paths to preload. * @param array $args Array of query strings to add paired by key/value. * @return (string|string[])[] */ public function add_query_parameters( array $preload_paths, array $args ): array { foreach ( $preload_paths as $k => $path ) { if ( empty( $path ) ) { continue; } $query_params = array(); // If the method request is OPTIONS, $path is an array and the first element is the path if ( is_array( $path ) ) { $temp_path = $path[0]; } else { $temp_path = $path; } $path_parts = wp_parse_url( $temp_path ); if ( ! isset( $path_parts['path'] ) || ! $this->is_filtered( $path_parts['path'] ) ) { continue; } if ( ! empty( $path_parts['query'] ) ) { parse_str( $path_parts['query'], $query_params ); } // Add params in query params foreach ( $args as $key => $value ) { $query_params[ $key ] = $value; } // Sort query params to put it in the same order as the preloading middleware does ksort( $query_params ); // Replace the key by the correct path with query params reordered $sorted_path = add_query_arg( urlencode_deep( $query_params ), $path_parts['path'] ); if ( is_array( $path ) ) { $preload_paths[ $k ][0] = $sorted_path; } else { $preload_paths[ $k ] = $sorted_path; } } return $preload_paths; } /** * Adds inline script to declare filtered REST route on client side. * * @since 3.5 * * @param string $script_handle Name of the script to add the inline script to. * @return void */ public function add_inline_script( string $script_handle ) { $script_var = 'let pllFilteredRoutes = ' . (string) wp_json_encode( $this->get() ); wp_add_inline_script( $script_handle, $script_var, 'before' ); } /** * Returns filtered REST routes by entity type (e.g. post type or taxonomy). * * @since 3.5 * * @return string[] REST routes. * @phpstan-return array<string, string> */ private function get(): array { if ( ! empty( $this->filtered_entities ) ) { return array_merge( $this->filtered_entities, $this->filtered_routes ); } $translatable_post_types = $this->model->get_translated_post_types(); $translatable_taxonomies = $this->model->get_translated_taxonomies(); $post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' ); $taxonomies = get_taxonomies( array( 'show_in_rest' => true ), 'objects' ); $this->extract_filtered_rest_entities( array_merge( $post_types, $taxonomies ), array_merge( $translatable_post_types, $translatable_taxonomies ) ); return array_merge( $this->filtered_entities, $this->filtered_routes ); } /** * Tells if a given route is fileterable by language. * * @since 3.5 * * @param string $rest_route Route to test. * @return bool Whether the route is filterable or not. */ private function is_filtered( string $rest_route ): bool { $rest_route = trim( $rest_route ); return ! preg_match( '/\d+$/', $rest_route ) && in_array( trim( $rest_route, '/' ), $this->get(), true ); } /** * Extracts filterable REST route from an array of entity objects * from a list of translatable entities (e.g. post types or taxonomies). * * @since 3.5 * * @param object[] $rest_entities Array of post type or taxonomy objects. * @param string[] $translatable_entities Array of translatable entity names. * @return void * @phpstan-param array<WP_Post_Type|WP_Taxonomy> $rest_entities */ private function extract_filtered_rest_entities( array $rest_entities, array $translatable_entities ) { $this->filtered_entities = array(); foreach ( $rest_entities as $rest_entity ) { if ( in_array( $rest_entity->name, $translatable_entities, true ) ) { $rest_base = empty( $rest_entity->rest_base ) ? $rest_entity->name : $rest_entity->rest_base; $rest_namespace = empty( $rest_entity->rest_namespace ) ? 'wp/v2' : $rest_entity->rest_namespace; $this->filtered_entities[ $rest_entity->name ] = "{$rest_namespace}/{$rest_base}"; } } } } include/links-abstract-domain.php 0000644 00000006250 15136114124 0013067 0 ustar 00 <?php /** * @package Polylang */ /** * Links model for use when using one domain or subdomain per language. * * @since 2.0 */ abstract class PLL_Links_Abstract_Domain extends PLL_Links_Permalinks { /** * Constructor. * * @since 2.0 * * @param PLL_Model $model Instance of PLL_Model. */ public function __construct( &$model ) { parent::__construct( $model ); // Avoid cross domain requests (mainly for custom fonts). add_filter( 'content_url', array( $this, 'site_url' ) ); add_filter( 'theme_root_uri', array( $this, 'site_url' ) ); // The above filter is not sufficient with WPMU Domain Mapping. add_filter( 'plugins_url', array( $this, 'site_url' ) ); add_filter( 'rest_url', array( $this, 'site_url' ) ); add_filter( 'upload_dir', array( $this, 'upload_dir' ) ); // Set the correct domain for each language. add_filter( 'pll_language_flag_url', array( $this, 'site_url' ) ); } /** * Returns the language based on the language code in url. * * @since 1.2 * @since 2.0 Add the $url argument. * * @param string $url Optional, defaults to the current url. * @return string Language slug. */ public function get_language_from_url( $url = '' ) { if ( empty( $url ) ) { $url = pll_get_requested_url(); } $host = wp_parse_url( $url, PHP_URL_HOST ); $lang = array_search( $host, $this->get_hosts() ); return is_string( $lang ) ? $lang : ''; } /** * Modifies an url to use the domain associated to the current language. * * @since 1.8 * * @param string $url The url to modify. * @return string The modified url. */ public function site_url( $url ) { $lang = $this->get_language_from_url(); $lang = $this->model->get_language( $lang ); return $this->add_language_to_link( $url, $lang ); } /** * Fixes the domain for the upload directory. * * @since 2.0.6 * * @param array $uploads Array of information about the upload directory. @see wp_upload_dir(). * @return array */ public function upload_dir( $uploads ) { $lang = $this->get_language_from_url(); $lang = $this->model->get_language( $lang ); $uploads['url'] = $this->add_language_to_link( $uploads['url'], $lang ); $uploads['baseurl'] = $this->add_language_to_link( $uploads['baseurl'], $lang ); return $uploads; } /** * Adds home and search URLs to language data before the object is created. * * @since 3.4.1 * * @param array $additional_data Array of language additional data. * @param array $language Language data. * @return array Language data with home and search URLs added. */ public function set_language_home_urls( $additional_data, $language ) { $language = array_merge( $language, $additional_data ); $additional_data['search_url'] = $this->home_url( $language['slug'] ); $additional_data['home_url'] = $additional_data['search_url']; return $additional_data; } /** * Returns language home URL property according to the current domain. * * @since 3.4.4 * * @param string $url Home URL. * @param array $language Array of language props. * @return string Filtered home URL. */ public function set_language_home_url( $url, $language ) { return $this->home_url( $language['slug'] ); } } include/links-directory.php 0000644 00000022771 15136114124 0012031 0 ustar 00 <?php /** * @package Polylang */ /** * Links model for use when the language code is added in the url as a directory * for example mysite.com/en/something. * * @since 1.2 */ class PLL_Links_Directory extends PLL_Links_Permalinks { /** * Relative path to the home url. * * @var string */ protected $home_relative; /** * Constructor. * * @since 1.2 * * @param PLL_Model $model PLL_Model instance. */ public function __construct( &$model ) { parent::__construct( $model ); $this->home_relative = home_url( '/', 'relative' ); } /** * Adds hooks for rewrite rules. * * @since 1.6 * * @return void */ public function init() { add_action( 'pll_prepare_rewrite_rules', array( $this, 'prepare_rewrite_rules' ) ); // Ensure it's hooked before `self::do_prepare_rewrite_rules()` is called. parent::init(); } /** * Adds the language code in a url. * * @since 1.2 * @since 3.4 Accepts now a language slug. * * @param string $url The url to modify. * @param PLL_Language|string|false $language Language object or slug. * @return string The modified url. */ public function add_language_to_link( $url, $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } if ( ! empty( $language ) ) { $base = $this->options['rewrite'] ? '' : 'language/'; $slug = $this->options['default_lang'] === $language && $this->options['hide_default'] ? '' : $base . $language . '/'; $root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : preg_replace( '#^https?://#', '://', $this->home . '/' . $this->root ); if ( false === strpos( $url, $new = $root . $slug ) ) { $pattern = preg_quote( $root, '#' ); $pattern = '#' . $pattern . '#'; return preg_replace( $pattern, $new, $url, 1 ); // Only once. } } return $url; } /** * Returns the url without the language code. * * @since 1.2 * * @param string $url The url to modify. * @return string The modified url. */ public function remove_language_from_link( $url ) { $languages = $this->model->get_languages_list( array( 'hide_default' => $this->options['hide_default'], 'fields' => 'slug', ) ); if ( ! empty( $languages ) ) { $root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : preg_replace( '#^https?://#', '://', $this->home . '/' . $this->root ); $pattern = preg_quote( $root, '@' ); $pattern = '@' . $pattern . ( $this->options['rewrite'] ? '' : 'language/' ) . '(' . implode( '|', $languages ) . ')(([?#])|(/|$))@'; $url = preg_replace( $pattern, $root . '$3', $url ); } return $url; } /** * Returns the language based on the language code in the url. * * @since 1.2 * @since 2.0 Add the $url argument. * * @param string $url Optional, defaults to the current url. * @return string The language slug. */ public function get_language_from_url( $url = '' ) { if ( empty( $url ) ) { $url = pll_get_requested_url(); } $path = (string) wp_parse_url( $url, PHP_URL_PATH ); $root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : $this->home . '/' . $this->root; $pattern = (string) wp_parse_url( $root . ( $this->options['rewrite'] ? '' : 'language/' ), PHP_URL_PATH ); $pattern = preg_quote( $pattern, '#' ); $pattern = '#^' . $pattern . '(' . implode( '|', $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) . ')(/|$)#'; return preg_match( $pattern, trailingslashit( $path ), $matches ) ? $matches[1] : ''; // $matches[1] is the slug of the requested language. } /** * Returns the home url in a given language. * * @since 1.3.1 * @since 3.4 Accepts now a language slug. * * @param PLL_Language|string $language Language object or slug. * @return string */ public function home_url( $language ) { if ( $language instanceof PLL_Language ) { $language = $language->slug; } $base = $this->options['rewrite'] ? '' : 'language/'; $slug = $this->options['default_lang'] === $language && $this->options['hide_default'] ? '' : '/' . $this->root . $base . $language; return trailingslashit( $this->home . $slug ); } /** * Prepares the rewrite rules filters. * * @since 0.8.1 * @since 3.5 Hooked to `pll_prepare_rewrite_rules` and remove `$pre` parameter. * * @return void */ public function prepare_rewrite_rules() { /* * Don't modify the rules if there is no languages created yet and make sure * to add the filters only once and if all custom post types and taxonomies * have been registered. */ if ( ! $this->model->has_languages() || ! did_action( 'wp_loaded' ) || ! self::$can_filter_rewrite_rules ) { return; } foreach ( $this->get_rewrite_rules_filters_with_callbacks() as $rule => $callback ) { add_filter( $rule, $callback ); } } /** * The rewrite rules ! * * Always make sure that the default language is at the end in case the language information is hidden for default language. * Thanks to brbrbr http://wordpress.org/support/topic/plugin-polylang-rewrite-rules-not-correct. * * @since 0.8.1 * * @param string[] $rules Rewrite rules. * @return string[] Modified rewrite rules. */ public function rewrite_rules( $rules ) { $filter = str_replace( '_rewrite_rules', '', current_filter() ); global $wp_rewrite; $newrules = array(); $languages = $this->model->get_languages_list( array( 'fields' => 'slug' ) ); if ( $this->options['hide_default'] ) { $languages = array_diff( $languages, array( $this->options['default_lang'] ) ); } if ( ! empty( $languages ) ) { $slug = $wp_rewrite->root . ( $this->options['rewrite'] ? '' : 'language/' ) . '(' . implode( '|', $languages ) . ')/'; } // For custom post type archives. $cpts = array_intersect( $this->model->get_translated_post_types(), get_post_types( array( '_builtin' => false ) ) ); $cpts = $cpts ? '#post_type=(' . implode( '|', $cpts ) . ')#' : ''; foreach ( $rules as $key => $rule ) { if ( ! is_string( $rule ) || ! is_string( $key ) ) { // Protection against a bug in Sendinblue for WooCommerce. See: https://wordpress.org/support/topic/bug-introduced-in-rewrite-rules/ continue; } // Special case for translated post types and taxonomies to allow canonical redirection. if ( $this->options['force_lang'] && in_array( $filter, array_merge( $this->model->get_translated_post_types(), $this->model->get_translated_taxonomies() ) ) ) { /** * Filters the rewrite rules to modify. * * @since 1.9.1 * * @param bool $modify Whether to modify or not the rule, defaults to true. * @param array $rule Original rewrite rule. * @param string $filter Current set of rules being modified. * @param string|bool $archive Custom post post type archive name or false if it is not a cpt archive. */ if ( isset( $slug ) && apply_filters( 'pll_modify_rewrite_rule', true, array( $key => $rule ), $filter, false ) ) { $newrules[ $slug . str_replace( $wp_rewrite->root, '', ltrim( $key, '^' ) ) ] = str_replace( array( '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]', '?' ), array( '[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '?lang=$matches[1]&' ), $rule ); // Should be enough! } $newrules[ $key ] = $rule; } // Rewrite rules filtered by language. elseif ( in_array( $filter, $this->always_rewrite ) || in_array( $filter, $this->model->get_filtered_taxonomies() ) || ( $cpts && preg_match( $cpts, $rule, $matches ) && ! strpos( $rule, 'name=' ) ) || ( 'rewrite_rules_array' != $filter && $this->options['force_lang'] ) ) { /** This filter is documented in include/links-directory.php */ if ( apply_filters( 'pll_modify_rewrite_rule', true, array( $key => $rule ), $filter, empty( $matches[1] ) ? false : $matches[1] ) ) { if ( isset( $slug ) ) { $newrules[ $slug . str_replace( $wp_rewrite->root, '', ltrim( $key, '^' ) ) ] = str_replace( array( '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]', '?' ), array( '[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '?lang=$matches[1]&' ), $rule ); // Should be enough! } if ( $this->options['hide_default'] ) { $newrules[ $key ] = str_replace( '?', '?lang=' . $this->options['default_lang'] . '&', $rule ); } } else { $newrules[ $key ] = $rule; } } // Unmodified rules. else { $newrules[ $key ] = $rule; } } // The home rewrite rule. if ( 'root' == $filter && isset( $slug ) ) { $newrules[ $slug . '?$' ] = $wp_rewrite->index . '?lang=$matches[1]'; } return $newrules; } /** * Removes hooks to filter rewrite rules, called when switching blog @see {PLL_Base::switch_blog()}. * See `self::prepare_rewrite_rules()` for added hooks. * * @since 3.5 * * @return void */ public function remove_filters() { parent::remove_filters(); foreach ( $this->get_rewrite_rules_filters_with_callbacks() as $rule => $callback ) { remove_filter( $rule, $callback ); } } /** * Returns *all* rewrite rules filters with their associated callbacks. * * @since 3.5 * * @return callable[] Array of hook names as key and callbacks as values. */ protected function get_rewrite_rules_filters_with_callbacks() { $filters = array( 'rewrite_rules_array' => array( $this, 'rewrite_rules' ), // Needed for post type archives. ); foreach ( $this->get_rewrite_rules_filters() as $type ) { $filters[ $type . '_rewrite_rules' ] = array( $this, 'rewrite_rules' ); } return $filters; } } include/api.php 0000644 00000042543 15136114124 0007457 0 ustar 00 <?php /** * The Polylang public API. * * @package Polylang */ /** * Template tag: displays the language switcher. * The function does nothing if used outside the frontend. * * @api * @since 0.5 * * @param array $args { * Optional array of arguments. * * @type int $dropdown The list is displayed as dropdown if set to 1, defaults to 0. * @type int $echo Echoes the list if set to 1, defaults to 1. * @type int $hide_if_empty Hides languages with no posts ( or pages ) if set to 1, defaults to 1. * @type int $show_flags Displays flags if set to 1, defaults to 0. * @type int $show_names Shows language names if set to 1, defaults to 1. * @type string $display_names_as Whether to display the language name or its slug, valid options are 'slug' and 'name', defaults to name. * @type int $force_home Will always link to the homepage in the translated language if set to 1, defaults to 0. * @type int $hide_if_no_translation Hides the link if there is no translation if set to 1, defaults to 0. * @type int $hide_current Hides the current language if set to 1, defaults to 0. * @type int $post_id Returns links to the translations of the post defined by post_id if set, defaults to not set. * @type int $raw Return a raw array instead of html markup if set to 1, defaults to 0. * @type string $item_spacing Whether to preserve or discard whitespace between list items, valid options are 'preserve' and 'discard', defaults to 'preserve'. * } * @return string|array Either the html markup of the switcher or the raw elements to build a custom language switcher. */ function pll_the_languages( $args = array() ) { if ( empty( PLL()->links ) ) { return empty( $args['raw'] ) ? '' : array(); } $switcher = new PLL_Switcher(); return $switcher->the_languages( PLL()->links, $args ); } /** * Returns the current language on frontend. * Returns the language set in admin language filter on backend (false if set to all languages). * * @api * @since 0.8.1 * @since 3.4 Accepts composite values. * * @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`. * Pass `\OBJECT` constant to get the language object. A composite value can be used for language * term property values, in the form of `{language_taxonomy_name}:{property_name}` (see * {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`. * @return string|int|bool|string[]|PLL_Language The requested field or object for the current language, `false` if the field isn't set or if current language doesn't exist yet. * * @phpstan-return ( * $field is \OBJECT ? PLL_Language : ( * $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string> * ) * )|false */ function pll_current_language( $field = 'slug' ) { if ( empty( PLL()->curlang ) ) { return false; } if ( \OBJECT === $field ) { return PLL()->curlang; } return PLL()->curlang->get_prop( $field ); } /** * Returns the default language. * * @api * @since 1.0 * @since 3.4 Accepts composite values. * * @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`. * Pass `\OBJECT` constant to get the language object. A composite value can be used for language * term property values, in the form of `{language_taxonomy_name}:{property_name}` (see * {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`. * @return string|int|bool|string[]|PLL_Language The requested field or object for the default language, `false` if the field isn't set or if default language doesn't exist yet. * * @phpstan-return ( * $field is \OBJECT ? PLL_Language : ( * $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string> * ) * )|false */ function pll_default_language( $field = 'slug' ) { $lang = PLL()->model->get_default_language(); if ( empty( $lang ) ) { return false; } if ( \OBJECT === $field ) { return $lang; } return $lang->get_prop( $field ); } /** * Among the post and its translations, returns the ID of the post which is in the language represented by $lang. * * @api * @since 0.5 * @since 3.4 Returns 0 instead of false. * @since 3.4 $lang accepts PLL_Language or string. * * @param int $post_id Post ID. * @param PLL_Language|string $lang Optional language (object or slug), defaults to the current language. * @return int|false The translation post ID if exists, otherwise the passed ID. False if the passed object has no language or if the language doesn't exist. * * @phpstan-return int<0, max>|false */ function pll_get_post( $post_id, $lang = '' ) { $lang = $lang ? $lang : pll_current_language(); if ( empty( $lang ) ) { return false; } return PLL()->model->post->get( $post_id, $lang ); } /** * Among the term and its translations, returns the ID of the term which is in the language represented by $lang. * * @api * @since 0.5 * @since 3.4 Returns 0 instead of false. * @since 3.4 $lang accepts PLL_Language or string. * * @param int $term_id Term ID. * @param PLL_Language|string $lang Optional language (object or slug), defaults to the current language. * @return int|false The translation term ID if exists, otherwise the passed ID. False if the passed object has no language or if the language doesn't exist. * * @phpstan-return int<0, max>|false */ function pll_get_term( $term_id, $lang = null ) { $lang = $lang ? $lang : pll_current_language(); if ( empty( $lang ) ) { return false; } return PLL()->model->term->get( $term_id, $lang ); } /** * Returns the home url in a language. * * @api * @since 0.8 * * @param string $lang Optional language code, defaults to the current language. * @return string */ function pll_home_url( $lang = '' ) { if ( empty( $lang ) ) { $lang = pll_current_language(); } if ( empty( $lang ) || empty( PLL()->links ) ) { return home_url( '/' ); } return PLL()->links->get_home_url( $lang ); } /** * Registers a string for translation in the "strings translation" panel. * * @api * @since 0.6 * * @param string $name A unique name for the string. * @param string $string The string to register. * @param string $context Optional, the group in which the string is registered, defaults to 'polylang'. * @param bool $multiline Optional, true if the string table should display a multiline textarea, * false if should display a single line input, defaults to false. * @return void */ function pll_register_string( $name, $string, $context = 'Polylang', $multiline = false ) { if ( PLL() instanceof PLL_Admin_Base ) { PLL_Admin_Strings::register_string( $name, $string, $context, $multiline ); } } /** * Translates a string ( previously registered with pll_register_string ). * * @api * @since 0.6 * * @param string $string The string to translate. * @return string The string translated in the current language. */ function pll__( $string ) { if ( ! is_scalar( $string ) || '' === $string ) { return $string; } return __( $string, 'pll_string' ); // PHPCS:ignore WordPress.WP.I18n } /** * Translates a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML output. * * @api * @since 2.1 * * @param string $string The string to translate. * @return string The string translated in the current language. */ function pll_esc_html__( $string ) { return esc_html( pll__( $string ) ); } /** * Translates a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML attributes. * * @api * @since 2.1 * * @param string $string The string to translate. * @return string The string translated in the current language. */ function pll_esc_attr__( $string ) { return esc_attr( pll__( $string ) ); } /** * Echoes a translated string ( previously registered with pll_register_string ) * It is an equivalent of _e() and is not escaped. * * @api * @since 0.6 * * @param string $string The string to translate. * @return void */ function pll_e( $string ) { echo pll__( $string ); // phpcs:ignore } /** * Echoes a translated string ( previously registered with pll_register_string ) and escapes it for safe use in HTML output. * * @api * @since 2.1 * * @param string $string The string to translate. * @return void */ function pll_esc_html_e( $string ) { echo pll_esc_html__( $string ); // phpcs:ignore WordPress.Security.EscapeOutput } /** * Echoes a translated a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML attributes. * * @api * @since 2.1 * * @param string $string The string to translate. * @return void */ function pll_esc_attr_e( $string ) { echo pll_esc_attr__( $string ); // phpcs:ignore WordPress.Security.EscapeOutput } /** * Translates a string ( previously registered with pll_register_string ). * * @api * @since 1.5.4 * * @param string $string The string to translate. * @param string $lang Language code. * @return string The string translated in the requested language. */ function pll_translate_string( $string, $lang ) { if ( PLL() instanceof PLL_Frontend && pll_current_language() === $lang ) { return pll__( $string ); } if ( ! is_scalar( $string ) || '' === $string ) { return $string; } $lang = PLL()->model->get_language( $lang ); if ( empty( $lang ) ) { return $string; } static $cache; // Cache object to avoid loading the same translations object several times. if ( empty( $cache ) ) { $cache = new PLL_Cache(); } $mo = $cache->get( $lang->slug ); if ( ! $mo instanceof PLL_MO ) { $mo = new PLL_MO(); $mo->import_from_db( $lang ); $cache->set( $lang->slug, $mo ); } return $mo->translate( $string ); } /** * Returns true if Polylang manages languages and translations for this post type. * * @api * @since 1.0.1 * * @param string $post_type Post type name. * @return bool */ function pll_is_translated_post_type( $post_type ) { return PLL()->model->is_translated_post_type( $post_type ); } /** * Returns true if Polylang manages languages and translations for this taxonomy. * * @api * @since 1.0.1 * * @param string $tax Taxonomy name. * @return bool */ function pll_is_translated_taxonomy( $tax ) { return PLL()->model->is_translated_taxonomy( $tax ); } /** * Returns the list of available languages. * * @api * @since 1.5 * * @param array $args { * Optional array of arguments. * * @type bool $hide_empty Hides languages with no posts if set to true ( defaults to false ). * @type string $fields Return only that field if set ( @see PLL_Language for a list of fields ), defaults to 'slug'. * } * @return string[] */ function pll_languages_list( $args = array() ) { $args = wp_parse_args( $args, array( 'fields' => 'slug' ) ); return PLL()->model->get_languages_list( $args ); } /** * Sets the post language. * * @api * @since 1.5 * @since 3.4 $lang accepts PLL_Language or string. * @since 3.4 Returns a boolean. * * @param int $id Post ID. * @param PLL_Language|string $lang Language (object or slug). * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to * the post). */ function pll_set_post_language( $id, $lang ) { return PLL()->model->post->set_language( $id, $lang ); } /** * Sets the term language. * * @api * @since 1.5 * @since 3.4 $lang accepts PLL_Language or string. * @since 3.4 Returns a boolean. * * @param int $id Term ID. * @param PLL_Language|string $lang Language (object or slug). * @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to * the term). */ function pll_set_term_language( $id, $lang ) { return PLL()->model->term->set_language( $id, $lang ); } /** * Save posts translations. * * @api * @since 1.5 * @since 3.4 Returns an associative array of translations. * * @param int[] $arr An associative array of translations with language code as key and post ID as value. * @return int[] An associative array with language codes as key and post IDs as values. * * @phpstan-return array<non-empty-string, positive-int> */ function pll_save_post_translations( $arr ) { $id = reset( $arr ); if ( $id ) { return PLL()->model->post->save_translations( $id, $arr ); } return array(); } /** * Save terms translations * * @api * @since 1.5 * @since 3.4 Returns an associative array of translations. * * @param int[] $arr An associative array of translations with language code as key and term ID as value. * @return int[] An associative array with language codes as key and term IDs as values. * * @phpstan-return array<non-empty-string, positive-int> */ function pll_save_term_translations( $arr ) { $id = reset( $arr ); if ( $id ) { return PLL()->model->term->save_translations( $id, $arr ); } return array(); } /** * Returns the post language. * * @api * @since 1.5.4 * @since 3.4 Accepts composite values for `$field`. * * @param int $post_id Post ID. * @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`. * Pass `\OBJECT` constant to get the language object. A composite value can be used for language * term property values, in the form of `{language_taxonomy_name}:{property_name}` (see * {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`. * @return string|int|bool|string[]|PLL_Language The requested field or object for the post language, `false` if no language is associated to that post. * * @phpstan-return ( * $field is \OBJECT ? PLL_Language : ( * $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string> * ) * )|false */ function pll_get_post_language( $post_id, $field = 'slug' ) { $lang = PLL()->model->post->get_language( $post_id ); if ( empty( $lang ) || \OBJECT === $field ) { return $lang; } return $lang->get_prop( $field ); } /** * Returns the term language. * * @api * @since 1.5.4 * @since 3.4 Accepts composite values for `$field`. * * @param int $term_id Term ID. * @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`. * Pass `\OBJECT` constant to get the language object. A composite value can be used for language * term property values, in the form of `{language_taxonomy_name}:{property_name}` (see * {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`. * @return string|int|bool|string[]|PLL_Language The requested field or object for the post language, `false` if no language is associated to that term. * * @phpstan-return ( * $field is \OBJECT ? PLL_Language : ( * $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string> * ) * )|false */ function pll_get_term_language( $term_id, $field = 'slug' ) { $lang = PLL()->model->term->get_language( $term_id ); if ( empty( $lang ) || \OBJECT === $field ) { return $lang; } return $lang->get_prop( $field ); } /** * Returns an array of translations of a post. * * @api * @since 1.8 * * @param int $post_id Post ID. * @return int[] An associative array of translations with language code as key and translation post ID as value. * * @phpstan-return array<non-empty-string, positive-int> */ function pll_get_post_translations( $post_id ) { return PLL()->model->post->get_translations( $post_id ); } /** * Returns an array of translations of a term. * * @api * @since 1.8 * * @param int $term_id Term ID. * @return int[] An associative array of translations with language code as key and translation term ID as value. * * @phpstan-return array<non-empty-string, positive-int> */ function pll_get_term_translations( $term_id ) { return PLL()->model->term->get_translations( $term_id ); } /** * Counts posts in a language. * * @api * @since 1.5 * * @param string $lang Language code. * @param array $args { * Optional array of arguments. * * @type string $post_type Post type. * @type int $m YearMonth ( ex: 201307 ). * @type int $year 4 digit year. * @type int $monthnum Month number (from 1 to 12). * @type int $day Day of the month (from 1 to 31). * @type int $author Author id. * @type string $author_name Author nicename. * @type string $post_format Post format. * @type string $post_status Post status. * } * @return int Posts count. */ function pll_count_posts( $lang, $args = array() ) { $lang = PLL()->model->get_language( $lang ); if ( empty( $lang ) ) { return 0; } return PLL()->model->count_posts( $lang, $args ); } /** * Allows to access the Polylang instance. * However, it is always preferable to use API functions * as internal methods may be changed without prior notice. * * @since 1.8 * * @return PLL_Frontend|PLL_Admin|PLL_Settings|PLL_REST_Request */ function PLL() { // PHPCS:ignore WordPress.NamingConventions.ValidFunctionName return $GLOBALS['polylang']; } include/translatable-object-with-types-interface.php 0000644 00000002147 15136114124 0016673 0 ustar 00 <?php /** * @package Polylang */ defined( 'ABSPATH' ) || exit; /** * Interface to use for objects that can have one or more types. * * @since 3.4 * * @phpstan-type DBInfoWithType array{ * table: non-empty-string, * id_column: non-empty-string, * type_column: non-empty-string, * default_alias: non-empty-string * } */ interface PLL_Translatable_Object_With_Types_Interface { /** * Returns object types that need to be translated. * * @since 3.4 * * @param bool $filter True if we should return only valid registered object types. * @return string[] Object type names for which Polylang manages languages. * * @phpstan-return array<non-empty-string, non-empty-string> */ public function get_translated_object_types( $filter = true ); /** * Returns true if Polylang manages languages for this object type. * * @since 3.4 * * @param string|string[] $object_type Object type name or array of object type names. * @return bool * * @phpstan-param non-empty-string|non-empty-string[] $object_type */ public function is_translated_object_type( $object_type ); } flags/cf.png 0000644 00000000543 15136114124 0006736 0 ustar 00 �PNG IHDR A<� �PLTE$�9鼼� � �����폘������jj���z����~�BBo��a���������������������44���H�H�㺤ۤ�ё�ʉ�Й l ] l�lM�MA�A4�4�** � �0�0 Y �##t� ��e��;��+����� <� �� ��^��P��7��,�� �� �� � ��l^ aIDATx�1V1 @���dU���c�p��x Τ6��bR�Jن<�c�eHl��V�q�\�y�`�"5?*�_5."�S`���s��5�g_�i�����? I3� IEND�B`� flags/ng.png 0000644 00000000352 15136114124 0006750 0 ustar 00 �PNG IHDR A<� QPLTE � � ������ a {�{x�x��������Y�YO�O L 1�1%�%E�E>�>