Providers.php000064400000004764147207147450007260 0ustar00 'deepl', 'name' => 'DeepL Translator', 'key' => $settings->offsetGet('deepl_api_key'), 'url' => 'https://www.deepl.com/translator', ],[ 'id' => 'google', 'name' => 'Google Translate', 'key' => $settings->offsetGet('google_api_key'), 'url' => 'https://translate.google.com/', ],[ 'id' => 'microsoft', 'name' => 'Microsoft Translator', 'key' => $settings->offsetGet('microsoft_api_key'), 'region' => $settings->offsetGet('microsoft_api_region'), 'url' => 'https://aka.ms/MicrosoftTranslatorAttribution', ],[ 'id' => 'lecto', 'name' => 'Lecto AI', 'key' => $settings->offsetGet('lecto_api_key'), 'url' => 'https://lecto.ai/?ref=loco', ], ]; } /** * Get only configured APIs, and sort them fairly * @return array[] */ public static function configured(){ return self::sort( array_filter( self::export(), [__CLASS__,'filterConfigured'] ) ); } /** * @internal * @param $api string[] * @return bool */ private static function filterConfigured( array $api ){ return array_key_exists('key',$api) && is_string($api['key']) && '' !== $api['key']; } /** * @internal * @param string[] $a * @param string[] $b * @return int */ private static function compareNames( array $a, array $b ){ return strcasecmp($a['name'],$b['name']); } /** * Sort providers alphabetically * @param array[] $apis * @return array */ public static function sort( array $apis ){ usort( $apis, [__CLASS__,'compareNames'] ); return $apis; } } WordPressFileSystem.php000064400000036721147207147450011236 0ustar00form; } /** * Pre-auth checks for superficial file system denials and disconnects any active remotes * @param Loco_fs_File $file the file you wish to modify * @throws Loco_error_WriteException * @return void */ public function preAuthorize( Loco_fs_File $file ){ if( ! $this->fs_allowed ){ $file->getWriteContext()->authorize(); $this->fs_allowed = true; } // Disconnecting remote file system ensures the auth functions always start with direct file access $file->getWriteContext()->disconnect(); } /** * Authorize for the creation of a file that does not exist * @param Loco_fs_File $file * @return bool whether file system is authorized NOT necessarily whether file is creatable */ public function authorizeCreate( Loco_fs_File $file ){ $this->preAuthorize($file); if( $file->exists() ){ // translators: %s refers to the name of a new file to be created, but which already existed throw new Loco_error_WriteException( sprintf( __('%s already exists in this folder','loco-translate'), $file->basename() ) ); } return $file->creatable() || $this->authorize($file); } /** * Authorize for the update of a file that does exist * @param Loco_fs_File $file * @return bool whether file system is authorized NOT necessarily whether file is updatable */ public function authorizeUpdate( Loco_fs_File $file ){ $this->preAuthorize($file); if( ! $file->exists() ){ throw new Loco_error_WriteException("File doesn't exist, try authorizeCreate"); } return $file->writable() || $this->authorize($file); } /** * Authorize for update or creation, depending on whether file exists * @param Loco_fs_File $file * @return bool */ public function authorizeSave( Loco_fs_File $file ){ $this->preAuthorize($file); return ( $file->exists() ? $file->writable() : $file->creatable() ) || $this->authorize($file); } /** * Authorize for copy (to same directory), meaning source file must exist and directory be writable * @param Loco_fs_File $file * @return bool */ public function authorizeCopy( Loco_fs_File $file ){ $this->preAuthorize($file); if( ! $file->exists() ){ throw new Loco_error_WriteException("Can't copy a file that doesn't exist"); } return $file->creatable() || $this->authorize($file); } /** * Authorize for move (to another path if given). * @param Loco_fs_File $source file being moved (must exist) * @param Loco_fs_File|null $target target path (should not exist) * @return bool */ public function authorizeMove( Loco_fs_File $source, Loco_fs_File $target = null ){ // source is in charge of its own deletion $result = $this->authorizeDelete($source); // target is in charge of copying original which it must also be able to read. if( $target && ! $this->authorizeCreate($target) ){ $result = false; } // value returned will be false if at least one file requires we add credentials return $result; } /** * Authorize for the removal of an existing file * @param Loco_fs_File $file * @return bool whether file system is authorized NOT necessarily whether file is removable */ public function authorizeDelete( Loco_fs_File $file ){ $this->preAuthorize($file); if( ! $file->exists() ){ throw new Loco_error_WriteException("Can't delete a file that doesn't exist"); } return $file->deletable() || $this->authorize($file); } /** * Connect file to credentials in posted data. Used when established in advance what connection is needed * @param Loco_fs_File $file * @return bool whether file system is authorized */ public function authorizeConnect( Loco_fs_File $file ){ $this->preAuthorize($file); // front end may have posted that "direct" connection will work $post = Loco_mvc_PostParams::get(); if( 'direct' === $post->connection_type ){ return true; } return $this->authorize($file); } /** * Wraps `request_filesystem_credentials` negotiation to obtain a remote connection and buffer WordPress form output * Call before output started, because buffers. * @param Loco_fs_File $file * @return bool */ private function authorize( Loco_fs_File $file ){ // may already have authorized successfully if( $this->fs instanceof WP_Filesystem_Base ){ $file->getWriteContext()->connect( $this->fs, false ); return true; } // may have already failed authorization if( $this->form ){ return false; } // network access may be disabled if( ! apply_filters('loco_allow_remote', true ) ){ throw new Loco_error_WriteException('Remote connection required, but network access is disabled'); } // else begin new auth $this->fs = null; $this->form = ''; $this->creds_out = []; // observe settings held temporarily in session try { $session = Loco_data_Session::get(); if( isset($session['loco-fs']) ){ $creds = $session['loco-fs']; if( is_array($creds) && $this->tryCredentials($creds,$file) ){ $this->creds_in = []; return true; } } } catch( Exception $e ){ // tolerate session failure } $post = Loco_mvc_PostParams::get(); $dflt = [ 'hostname' => '', 'username' => '', 'password' => '', 'public_key' => '', 'private_key' => '', 'connection_type' => '', '_fs_nonce' => '' ]; $this->creds_in = array_intersect_key( $post->getArrayCopy(), $dflt ); // deliberately circumventing call to `get_filesystem_method` // risk of WordPress version compatibility issues, but only sane way to force a remote connection // @codeCoverageIgnoreStart if( defined('FS_METHOD') && FS_METHOD ){ $type = FS_METHOD; // forcing direct access means request_filesystem_credentials will never give us a form :( if( 'direct' === $type ){ Loco_error_AdminNotices::debug('Cannot connect remotely when FS_METHOD is "direct"'); return false; } } // direct filesystem is OK if the front end already posted it else if( 'direct' === $post->connection_type ){ return true; } // else perform same logic as request_filesystem_credentials does to establish type else if( 'ssh' === $post->connection_type && extension_loaded('ssh2') && function_exists('stream_get_contents') ){ $type = 'ssh2'; } else if( extension_loaded('ftp') ){ $type = 'ftpext'; } else if( extension_loaded('sockets') || function_exists('fsockopen') ){ $type = 'ftpsockets'; } // @codeCoverageIgnoreEnd else { $type = ''; } // context is nonsense here as the system doesn't know what operation we're performing // testing directory write-permission when we're updating a file, for example. $context = '/ignore/this'; $type = apply_filters( 'filesystem_method', $type, $post->getArrayCopy(), $context, true ); // the only params we'll pass into form will be those used by the ajax fsConnect end point $extra = [ 'loco-nonce', 'path', 'auth', 'dest' ]; // capture WordPress output during negotiation. $buffer = Loco_output_Buffer::start(); $creds = request_filesystem_credentials( '', $type, false, $context, $extra ); if( is_array($creds) ){ // credentials passed through, should allow to connect if they are correct if( $this->tryCredentials($creds,$file) ){ $this->persistCredentials(); return true; } // else there must be an error with the credentials $error = true; // pull more useful connection error for display in form if( isset($GLOBALS['wp_filesystem']) ){ $fs = $GLOBALS['wp_filesystem']; $GLOBALS['wp_filesystem'] = null; if( $fs && $fs->errors && $fs->errors->get_error_code() ){ $error = $fs->errors; } } // annoyingly WordPress moves the error notice above the navigation tabs :-/ request_filesystem_credentials( '', $type, $error, $context, $extra ); } // should now have unauthorized remote connection form $this->form = (string) $buffer->close(); if( '' === $this->form ){ Loco_error_AdminNotices::debug('Unknown error capturing output from request_filesystem_credentials'); } return false; } /** * @param array $creds credentials returned from request_filesystem_credentials * @param Loco_fs_File $file file to authorize write context * @return bool when credentials connected ok */ private function tryCredentials( array $creds, Loco_fs_File $file ){ // lazy construct the file system from current credentials if possible // in typical WordPress style, after success the object will be held in a global. if( WP_Filesystem( $creds, '/ignore/this/' ) ){ $this->fs = $GLOBALS['wp_filesystem']; // hook new file system into write context (specifying that connect has already been performed) $file->getWriteContext()->connect( $this->fs, false ); $this->creds_out = $creds; return true; } return false; } /** * Set current credentials in session if settings allow * @return bool whether credentials persisted */ private function persistCredentials(){ try { $settings = Loco_data_Settings::get(); if( $settings['fs_persist'] ){ $session = Loco_data_Session::get(); $session['loco-fs'] = $this->creds_out; $session->persist(); return true; } } catch( Exception $e ){ // tolerate session failure Loco_error_AdminNotices::debug( $e->getMessage() ); } return false; } /** * Get working credentials that resulted in connection * @return array */ public function getOutputCredentials(){ return $this->creds_out; } /** * Get input credentials from original post. * this is not the same as getCredentials. It is designed for replay only, regardless of success * Note that input to request_filesystem_credentials is not the same as the output (specifically how hostname:port is handled) */ public function getInputCredentials(){ return $this->creds_in; } /** * Get currently configured filesystem API * @return WP_Filesystem_Direct */ public function getFileSystem(){ if( ! $this->fs ){ return self::direct(); } return $this->fs; } /** * Check if a file is subject to WordPress automatic updates * @param Loco_fs_File $file * @return bool */ public function isAutoUpdatable( Loco_fs_File $file ){ // all paths safe from auto-updates if auto-updates are completely disabled if( $this->isAutoUpdateDenied() ){ return false; } // Auto-updates aren't denied, so ascertain location "type" and run through the same filters as WP_Automatic_Updater::should_update() $type = $file->getUpdateType(); if( '' !== $type ){ // Since 5.5.0: "{type}_s_auto_update_enabled" filters auto-update status for themes and plugins // admins must also enable auto-updates on plugins and themes individually, but not checking that here. if( function_exists('wp_is_auto_update_enabled_for_type') && ('plugin'===$type||'theme'===$type) ){ $enabled = (bool) apply_filters( "{$type}s_auto_update_enabled", true ); if( $enabled ){ // resolve given file to plugin/theme handle, so we can check if it's been enabled $bundle = Loco_package_Bundle::fromFile($file); if( $bundle instanceof Loco_package_Bundle ){ $handle = $bundle->getHandle(); $option = (array) get_site_option( "auto_update_{$type}s", [] ); // var_dump( compact('handle','option') ); if( ! in_array($handle,$option,true) ){ $enabled = false; } } } return $enabled; } // WordPress updater will have {item} from remote API data which we don't have here. $item = new stdClass; $item->new_files = false; $item->autoupdate = true; $item->disable_autoupdate = false; return apply_filters( 'auto_update_'.$type, true, $item ); } // else safe (not auto-updatable) return false; } /** * Check if system is configured to deny auto-updates * @return bool */ public function isAutoUpdateDenied(){ // WordPress >= 4.8 can disable auto updates completely with "automatic_updater" context if( function_exists('wp_is_file_mod_allowed') && ! wp_is_file_mod_allowed('automatic_updater') ){ return true; } // else simply observe AUTOMATIC_UPDATER_DISABLED constant if( apply_filters( 'automatic_updater_disabled', loco_constant('AUTOMATIC_UPDATER_DISABLED') ) ) { return true; } // else nothing explicitly denying updates return false; } }WordPressTranslations.php000064400000011712147207147450011624 0ustar00locales; if( is_null($locales) ){ $locales = []; // get official locales from API if we have network $cached = $this->wp_get_available_translations(); if( is_array($cached) && $cached ){ $english_name = 'english_name'; $native_name = 'native_name'; } // else fall back to bundled data cached else { $english_name = 0; $native_name = 1; $cached = Loco_data_CompiledData::get('locales'); // debug so we can see on front end that data was offline // $locales['en-debug'] = ( new Loco_Locale('en','','debug') )->setName('OFFLINE DATA'); } /* @var string $tag */ foreach( $cached as $tag => $raw ){ $locale = Loco_Locale::parse($tag); if( $locale->isValid() ){ $locale->setName( $raw[$english_name], $raw[$native_name] ); $locales[ (string) $locale ] = $locale; } /* Skip invalid language tags, e.g. "pt_PT_ao90" should be "pt_PT_ao1990" * No point fixing invalid tags, because core translation files won't match. else { Loco_error_AdminNotices::debug( sprintf('Invalid locale: %s', $tag) ); }*/ } $this->locales = $locales; } return $locales; } /** * Wrap get_available_languages * @return string[] */ public function getInstalledCore(){ // wp-includes/l10n.php should always be included at runtime if( ! is_array($this->installed) ){ $this->installed = get_available_languages(); // en_US is implicitly installed if( ! in_array('en_US',$this->installed) ){ array_unshift( $this->installed, 'en_US' ); } } return $this->installed; } /** * @return array */ private function getInstalledHash(){ if( ! is_array($this->installed_hash) ){ $this->installed_hash = array_flip( $this->getInstalledCore() ); } return $this->installed_hash; } /** * Check if a given locale is installed * @param string|Loco_Locale * @return bool */ public function isInstalled( $locale ){ return array_key_exists( (string) $locale, $this->getInstalledHash() ); } /** * Get WordPress locale data by strictly well-formed language tag * @param string $tag * @return Loco_Locale */ public function getLocale( $tag ){ $all = $this->getAvailableCore(); return isset($all[$tag]) ? $all[$tag] : null; } /** * Check whether remote API may be disabled for whatever reason, usually debugging. * @return bool */ public function hasNetwork(){ if( is_null($this->enabled) ){ $this->enabled = (bool) apply_filters('loco_allow_remote', true ); } return $this->enabled; } /** * Wrapper for translations_api * @param string * @param array * @return array[] */ public function apiGet( $type, array $args ){ if( ! function_exists('translations_api') ){ require_once ABSPATH.'wp-admin/includes/translation-install.php'; } $response = translations_api($type,$args); if( $response instanceof WP_Error ){ $message = 'Unknown error from translations_api'; foreach( $response->get_error_messages() as $message ){ Loco_error_AdminNotices::debug('translations_api error: '.$message); } throw new Loco_error_Exception($message); } return (array) $response; } }