Gestionnaire de fichiers - Editer - /home/wwgoat/public_html/blog/ajax.tar
Arrière
PingController.php 0000644 00000001207 14720715442 0010224 0 ustar 00 <?php /** * Ajax "ping" route, for testing Ajax responses are working. */ class Loco_ajax_PingController extends Loco_mvc_AjaxController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); // echo back bytes posted if( $post->has('echo') ){ $this->set( 'ping', $post['echo'] ); } // else just send pong else { $this->set( 'ping', 'pong' ); } // always send tick symbol to check json serializing of unicode $this->set( 'utf8', "\xE2\x9C\x93" ); return parent::render(); } } SaveController.php 0000644 00000006631 14720715442 0010233 0 ustar 00 <?php /** * Ajax "save" route, for saving editor contents to disk */ class Loco_ajax_SaveController extends Loco_ajax_common_BundleController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); // path parameter must not be empty $path = $post->path; if( ! $path ){ throw new InvalidArgumentException('Path parameter required'); } // locale must be posted to indicate whether PO or POT $locale = $post->locale; if( is_null($locale) ){ throw new InvalidArgumentException('Locale parameter required'); } $pofile = new Loco_fs_LocaleFile( $path ); $pofile->normalize( loco_constant('WP_CONTENT_DIR') ); // ensure we only deal with PO/POT source files. // posting of MO file paths is permitted when PO is missing, but we're about to fix that $ext = strtolower( $pofile->fullExtension() ); if( 'mo' === $ext ){ $pofile = $pofile->cloneExtension('po'); } else if( 'pot' === $ext ){ $locale = ''; } else if( 'po' !== $ext ){ throw new Loco_error_Exception('Disallowed file extension'); } // Prepare compiler for all save operations. PO/MO/JSON, or just POT $compiler = new Loco_gettext_Compiler($pofile); // data posted may be either 'multipart/form-data' (recommended for large files) if( isset($_FILES['po']) ){ $data = Loco_gettext_Data::fromSource( Loco_data_Upload::src('po') ); } // else 'application/x-www-form-urlencoded' by default else { $data = Loco_gettext_Data::fromSource( $post->data ); } // WordPress-ize some headers that differ from that sent from JavaScript if( $locale ){ $head = $data->getHeaders(); $head['Language'] = strtr( $locale, '-', '_' ); } // commit PO file directly to disk $bytes = $compiler->writePo($data); $mtime = $pofile->modified(); // start success data with bytes written and timestamp $this->set('locale', $locale ); $this->set('pobytes', $bytes ); $this->set('poname', $pofile->basename() ); $this->set('modified', $mtime); $this->set('datetime', Loco_mvc_ViewParams::date_i18n($mtime) ); // add bundle to recent items on file creation // editor permitted to save files not in a bundle, so catching failures try { $bundle = $this->getBundle(); Loco_data_RecentItems::get()->pushBundle($bundle)->persist(); } catch( Exception $e ){ $bundle = null; } // Compile MO and JSON files if PO is localised and not POT (template) if( $locale ){ $mobytes = $compiler->writeMo($data); $numjson = 0; // Project required for JSON writes if( $bundle ){ $project = $this->getProject($bundle); $jsons = $compiler->writeJson($project,$data); $numjson = $jsons->count(); } $this->set( 'mobytes', $mobytes ); $this->set( 'numjson', $numjson ); } // Final summary depending on whether MO and JSON compiled $compiler->getSummary(); return parent::render(); } } DiffController.php 0000644 00000003560 14720715442 0010203 0 ustar 00 <?php /** * Ajax "diff" route, for rendering PO/POT file diffs */ class Loco_ajax_DiffController extends Loco_mvc_AjaxController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); // require x2 valid files for diffing if( ! $post->lhs || ! $post->rhs ){ throw new InvalidArgumentException('Path parameters required'); } $dir = loco_constant('WP_CONTENT_DIR'); $lhs = new Loco_fs_File( $post->lhs ); $lhs->normalize($dir); $rhs = new Loco_fs_File( $post->rhs ); $rhs->normalize($dir); // avoid diffing non Gettext source files $exts = array_flip( [ 'pot', 'pot~', 'po', 'po~' ] ); /* @var $file Loco_fs_File */ foreach( [$lhs,$rhs] as $file ){ if( ! $file->exists() ){ throw new InvalidArgumentException('File paths must exist'); } if( ! $file->underContentDirectory() ){ throw new InvalidArgumentException('Files must be under '.basename($dir) ); } $ext = $file->extension(); if( ! isset($exts[$ext]) ){ throw new InvalidArgumentException('Disallowed file extension'); } } // OK to diff files as HTML table $renderer = new Loco_output_DiffRenderer; $emptysrc = $renderer->_startDiff().$renderer->_endDiff(); $tablesrc = $renderer->renderFiles( $rhs, $lhs ); if( $tablesrc === $emptysrc ){ // translators: Where %s is a file name $message = __('Revisions are identical, you can delete %s','loco-translate'); $this->set( 'error', sprintf( $message, $rhs->basename() ) ); } else { $this->set( 'html', $tablesrc ); } return parent::render(); } } common/BundleController.php 0000644 00000002245 14720715442 0012033 0 ustar 00 <?php /** * Common functions for all Ajax actions that operate on a bundle */ abstract class Loco_ajax_common_BundleController extends Loco_mvc_AjaxController { /** * @return Loco_package_Bundle */ protected function getBundle(){ if( $id = $this->get('bundle') ){ // type may be passed as separate argument if( $type = $this->get('type') ){ return Loco_package_Bundle::createType( $type, $id ); } // else embedded in standalone bundle identifier // TODO standardize this across all Ajax end points return Loco_package_Bundle::fromId($id); } // else may have type embedded in bundle throw new Loco_error_Exception('No bundle identifier posted'); } /** * @param Loco_package_Bundle $bundle * @return Loco_package_Project */ protected function getProject( Loco_package_Bundle $bundle ){ $project = $bundle->getProjectById( $this->get('domain') ); if( ! $project ){ throw new Loco_error_Exception('Failed to find translation project'); } return $project; } } XgettextController.php 0000644 00000006075 14720715442 0011153 0 ustar 00 <?php /** * Ajax "xgettext" route, for initializing new template file from source code */ class Loco_ajax_XgettextController extends Loco_ajax_common_BundleController { /** * {@inheritdoc} */ public function render(){ $this->validate(); $bundle = $this->getBundle(); $project = $this->getProject( $bundle ); // target location may not be next to POT file at all $base = loco_constant('WP_CONTENT_DIR'); $target = new Loco_fs_Directory( $this->get('path') ); $target->normalize( $base ); if( $target->exists() && ! $target->isDirectory() ){ throw new Loco_error_Exception('Target is not a directory'); } // basename should be posted from front end $name = $this->get('name'); if( ! $name ){ throw new Loco_error_Exception('Front end did not post $name'); } // POT file should be .pot but we'll allow .po $potfile = new Loco_fs_File( $target.'/'.$name ); $ext = strtolower( $potfile->fullExtension() ); if( 'pot' !== $ext && 'po' !== $ext ){ throw new Loco_error_Exception('Disallowed file extension'); } // File shouldn't exist currently $api = new Loco_api_WordPressFileSystem; $api->authorizeCreate($potfile); // Do extraction and grab only given domain's strings $ext = new Loco_gettext_Extraction( $bundle ); $domain = $project->getDomain()->getName(); $data = $ext->addProject($project)->includeMeta()->getTemplate( $domain ); // additional headers to set in new POT file $head = $data->getHeaders(); $head['Project-Id-Version'] = $project->getName(); // write POT file to disk returning byte length $potsize = $potfile->putContents( $data->msgcat(true) ); // set response data for debugging if( loco_debugging() ){ $this->set( 'debug', [ 'potname' => $potfile->basename(), 'potsize' => $potsize, 'total' => $ext->getTotal(), ] ); } // push recent items on file creation // TODO push project and locale file Loco_data_RecentItems::get()->pushBundle( $bundle )->persist(); // put flash message into session to be displayed on redirected page try { Loco_data_Session::get()->flash('success', __('Template file created','loco-translate') ); Loco_data_Session::close(); } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); } // redirect front end to bundle view. Discourages manual editing of template $type = strtolower( $bundle->getType() ); $href = Loco_mvc_AdminRouter::generate( sprintf('%s-view',$type), [ 'bundle' => $bundle->getHandle(), ] ); $hash = '#loco-'.$project->getId(); $this->set( 'redirect', $href.$hash ); return parent::render(); } } MsginitController.php 0000644 00000015237 14720715442 0010751 0 ustar 00 <?php /** * Ajax "msginit" route, for initializing new translation files */ class Loco_ajax_MsginitController extends Loco_ajax_common_BundleController { /** * @return Loco_Locale */ private function getLocale(){ if( $this->get('use-selector') ){ $tag = $this->get('select-locale'); } else { $tag = $this->get('custom-locale'); } $locale = Loco_Locale::parse($tag); if( ! $locale->isValid() ){ throw new Loco_error_LocaleException('Invalid locale'); } return $locale; } /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); $bundle = $this->getBundle(); $project = $this->getProject( $bundle ); $domain = (string) $project->getDomain(); $locale = $this->getLocale(); $suffix = (string) $locale; // The front end posts a template path, so we must replace the actual locale code $base = loco_constant('WP_CONTENT_DIR'); $path = $post->path[ $post['select-path'] ]; // The request_filesystem_credentials function will try to access the "path" field later $_POST['path'] = $path; $pofile = new Loco_fs_LocaleFile( $path ); if( 'po' !== $pofile->fullExtension() ){ throw new Loco_error_Exception('Disallowed file extension'); } if( $suffix !== $pofile->getSuffix() ){ $pofile = $pofile->cloneLocale( $locale ); if( $suffix !== $pofile->getSuffix() ){ throw new Loco_error_Exception('Failed to suffix file path with locale code'); } } // target PO should not exist yet $pofile->normalize( $base ); $api = new Loco_api_WordPressFileSystem; $api->authorizeCreate( $pofile ); // Target MO probably doesn't exist, but we don't want to overwrite it without asking $mofile = $pofile->cloneExtension('mo'); if( $mofile->exists() ){ throw new Loco_error_Exception( __('MO file exists for this language already. Delete it first','loco-translate') ); } // Permit forcing of any parsable file as strings template $source = (string) $post->source; $compile = false; $mergejson = false; if( '' !== $source ){ $translate = ! $post->strip; $compile = $translate; $potfile = new Loco_fs_LocaleFile( $source ); $potfile->normalize( $base ); $data = Loco_gettext_Data::load($potfile); // When copying a PO file we may need to augment with JSON strings if( $post->json ){ $mergejson = true; $siblings = new Loco_fs_Siblings($potfile); $jsons = $siblings->getJsons($domain); if( $jsons ){ $refs = clone $data; $merge = new Loco_gettext_Matcher($project); $merge->loadRefs($refs,$translate); $merge->loadJsons($jsons); // resolve faux merge into empty instance $data->clear(); $merge->mergeValid($refs,$data); $merge->mergeAdded($data); } } // Remove target strings when copying PO without msgstr fields if( ! $translate && 'pot' !== $potfile->extension() ){ $data->strip(); } } // else parse POT file if project defines one that exists else { $potfile = $project->getPot(); if( $potfile->exists() ){ $data = Loco_gettext_Data::load($potfile); } // else extract directly from source code, assuming domain passed though from front end else { $extr = new Loco_gettext_Extraction( $bundle ); $data = $extr->addProject($project)->includeMeta()->getTemplate($domain); $potfile = null; } } // Let template define Project-Id-Version, else set header to current project name $headers = []; $vers = $data->getHeaders()->{'Project-Id-Version'}; if( ! $vers || 'PACKAGE VERSION' === $vers ){ $headers['Project-Id-Version'] = $project->getName(); } // fallback header not actually used, but keeping for informational purposes if( $potfile instanceof Loco_fs_LocaleFile && $post->link ){ $fallback = $potfile->getLocale(); if( $fallback->isValid() ){ $headers['X-Loco-Fallback'] = (string) $fallback; } } // finalize PO data ready to write to new file $locale->ensureName( new Loco_api_WordPressTranslations ); $data->localize( $locale, $headers ); // save sync options in PO headers if linked to a custom template. if( $potfile && $post->link ){ $opts = new Loco_gettext_SyncOptions( $data->getHeaders() ); $opts->setTemplate( $potfile->getRelativePath( $bundle->getDirectoryPath() ) ); // legacy behaviour was to sync source AND target strings in the absence of the following $mode = $post->strip ? 'POT' : 'PO'; // even if no JSONs were merged we need to keep this option in case JSONs are added in future. if( $mergejson ){ $mode.= ',JSON'; } $opts->setSyncMode($mode); } // compile all files in this set when copying target translation $compiler = new Loco_gettext_Compiler($pofile); if( $compile ){ $compiler->writeAll($data,$project); } // empty translations don't require compiled files, but adding MO for completeness. else { $compiler->writePo($data); $data->clear(); $compiler->writeMo($data); } // return debugging information, used in tests. $this->set('debug',new Loco_mvc_ViewParams( [ 'poname' => $pofile->basename(), 'source' => $potfile ? $potfile->basename() : '', ] ) ); // push recent items on file creation Loco_data_RecentItems::get()->pushBundle($bundle)->persist(); // front end will redirect to the editor $type = strtolower( $this->get('type') ); $this->set( 'redirect', Loco_mvc_AdminRouter::generate( sprintf('%s-file-edit',$type), [ 'path' => $pofile->getRelativePath($base), 'bundle' => $bundle->getHandle(), 'domain' => $project->getId(), ] ) ); return parent::render(); } } DownloadConfController.php 0000644 00000002154 14720715442 0011706 0 ustar 00 <?php /** * Downloads a bundle configuration as XML or Json */ class Loco_ajax_DownloadConfController extends Loco_ajax_common_BundleController { /** * {@inheritdoc} */ public function render(){ $this->validate(); $bundle = $this->getBundle(); $file = new Loco_fs_File( $this->get('path') ); // Download actual loco.xml file if bundle is configured from it if( 'file' === $bundle->isConfigured() && 'xml' === $file->extension() ){ $file->normalize( $bundle->getDirectoryPath() ); if( $file->readable() ){ return $file->getContents(); } } // else render temporary config file $writer = new Loco_config_BundleWriter($bundle); switch( $file->extension() ){ case 'xml': return $writer->toXml(); case 'json': return json_encode( $writer->jsonSerialize() ); } // @codeCoverageIgnoreStart throw new Loco_error_Exception('Specify either XML or JSON file path'); } } FsReferenceController.php 0000644 00000016161 14720715442 0011523 0 ustar 00 <?php /** * Ajax service that returns source code for a given file system reference * Currently this is only PHP, but could theoretically be any file type. */ class Loco_ajax_FsReferenceController extends Loco_ajax_common_BundleController { /** * @param string $refpath * @return Loco_fs_File */ private function findSourceFile( $refpath ){ // reference may be resolvable via referencing PO file's location $pofile = new Loco_fs_File( $this->get('path') ); $pofile->normalize( loco_constant('WP_CONTENT_DIR') ); if( ! $pofile->exists() ){ throw new InvalidArgumentException('PO/POT file required to resolve reference'); } $search = new Loco_gettext_SearchPaths; $search->init($pofile); if( $srcfile = $search->match($refpath) ){ return $srcfile; } // check against PO file location when no search paths or search paths failed $srcfile = new Loco_fs_File($refpath); $srcfile->normalize( $pofile->dirname() ); if( $srcfile->exists() ){ return $srcfile; } // reference may be resolvable via known project roots try { $bundle = $this->getBundle(); // Loco extractions will always be relative to bundle root $srcfile = new Loco_fs_File( $refpath ); $srcfile->normalize( $bundle->getDirectoryPath() ); if( $srcfile->exists() ){ return $srcfile; } // check relative to parent theme root if( $bundle->isTheme() && ( $parent = $bundle->getParent() ) ){ $srcfile = new Loco_fs_File( $refpath ); $srcfile->normalize( $parent->getDirectoryPath() ); if( $srcfile->exists() ){ return $srcfile; } } // final attempt - search all project source roots // TODO is there too large a risk of false positives? especially with files like index.php /* @var $root Loco_fs_Directory */ /*foreach( $this->getProject($bundle)->getConfiguredSources() as $root ){ if( $root->isDirectory() ){ $srcfile = new Loco_fs_File( $refpath ); $srcfile->normalize( $root->getPath() ); if( $srcfile->exists() ){ return $srcfile; } } }*/ } catch( Loco_error_Exception $e ){ // permitted for there to be no bundle or project when viewing orphaned file } throw new Loco_error_Exception( sprintf('Failed to find source file matching "%s"',$refpath) ); } /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); // at the very least we need a reference to examine if( ! $post->has('ref') ){ throw new InvalidArgumentException('ref parameter required'); } // reference must parse as <path>:<line> $refpath = $post->ref; if( preg_match('/^(.+):(\\d+)$/', $refpath, $r ) ){ $refpath = $r[1]; $refline = (int) $r[2]; } else { $refline = 0; } // find file or fail $srcfile = $this->findSourceFile($refpath); // deny access to sensitive files if( 'wp-config.php' === $srcfile->basename() ){ throw new InvalidArgumentException('File access disallowed'); } // validate allowed source file types, including custom aliases $conf = Loco_data_Settings::get(); $ext = strtolower( $srcfile->extension() ); $type = $conf->ext2type($ext,'none'); if( 'none' === $type ){ throw new InvalidArgumentException('File extension disallowed, '.$ext ); } $this->set('type', $type ); $this->set('line', $refline ); $this->set('path', $srcfile->getRelativePath( loco_constant('WP_CONTENT_DIR') ) ); // source code will be HTML-tokenized into multiple lines $code = []; // observe the same size limits for source highlighting as for string extraction as tokenizing will use the same amount of juice $maxbytes = wp_convert_hr_to_bytes( $conf->max_php_size ); // tokenizers require gettext utilities, easiest just to ping the extraction library if( ! class_exists('Loco_gettext_Extraction',true) ){ throw new RuntimeException('Failed to load tokenizers'); // @codeCoverageIgnore } // PHP is the most likely format. if( 'php' === $type && ( $srcfile->size() <= $maxbytes ) && loco_check_extension('tokenizer') ) { $tokens = new LocoPHPTokens( token_get_all( $srcfile->getContents() ) ); } else if( 'js' === $type ){ $tokens = new LocoJsTokens( $srcfile->getContents() ); } else { $tokens = null; } // highlighting on back end because tokenizer provides more control than highlight.js if( $tokens instanceof LocoTokensInterface ){ $thisline = 1; while( $tok = $tokens->advance() ){ if( is_array($tok) ){ // line numbers added in PHP 5.2.2 - WordPress minimum is 5.2.4 list( $t, $str, $startline ) = $tok; $clss = token_name($t); // tokens can span multiple lines (whitespace/html/comments) $lines = preg_split('/\\R/', $str ); } else { // scalar symbol will always start on the line that the previous token ended on $clss = 'T_NONE'; $lines = [ $tok ]; $startline = $thisline; } // token can span multiple lines, so include only bytes on required line[s] foreach( $lines as $i => $line ){ $thisline = $startline + $i; $html = '<code class="'.$clss.'">'.htmlentities($line,ENT_COMPAT,'UTF-8').'</code>'; // append highlighted token to current line $j = $thisline - 1; if( isset($code[$j]) ){ $code[$j] .= $html; } else { $code[$j] = $html; } } } } // permit limited other file types, but without back end highlighting else { foreach( preg_split( '/\\R/u', $srcfile->getContents() ) as $line ){ $code[] = '<code>'.htmlentities($line,ENT_COMPAT,'UTF-8').'</code>'; } } // allow 0 line reference when line is unknown (e.g. block.json) else it must exist if( $refline && ! isset($code[$refline-1]) ){ throw new Loco_error_Exception( sprintf('Line %u not in source file', $refline) ); } $this->set( 'code', $code ); return parent::render(); } } ApisController.php 0000644 00000007443 14720715442 0010233 0 ustar 00 <?php /** * Ajax "apis" route, for handing off Ajax requests to hooked API integrations. */ class Loco_ajax_ApisController extends Loco_mvc_AjaxController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); // Fire an event so translation apis can register their hooks as lazily as possible do_action('loco_api_ajax'); // Get request renders API modal contents: if( 0 === $post->count() ){ $apis = Loco_api_Providers::configured(); $this->set('apis',$apis); // modal views for batch-translate and suggest feature $modal = new Loco_mvc_View; $modal->set('apis',$apis); // help buttons $locale = $this->get('locale'); $modal->set( 'help', new Loco_mvc_ViewParams( [ 'text' => __('Help','loco-translate'), 'href' => apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/providers'), ] ) ); $modal->set('prof', new Loco_mvc_ViewParams( [ 'text' => __('Need a human?','loco-translate'), 'href' => apply_filters('loco_external','https://localise.biz/wordpress/translation?l='.$locale), ] ) ); // render auto-translate modal or prompt for configuration if( $apis ){ $html = $modal->render('ajax/modal-apis-batch'); } else { $html = $modal->render('ajax/modal-apis-empty'); } $this->set('html',$html); return parent::render(); } // else API client id should be posted to perform operation $hook = (string) $post->hook; // API client must be hooked in using loco_api_providers filter $config = null; foreach( Loco_api_Providers::export() as $candidate ){ if( is_array($candidate) && array_key_exists('id',$candidate) && $candidate['id'] === $hook ){ $config = $candidate; break; } } if( is_null($config) ){ throw new Loco_error_Exception('API not registered: '.$hook ); } // Get input texts to translate via registered hook. shouldn't be posted if empty. $sources = $post->sources; if( ! is_array($sources) || ! $sources ){ throw new Loco_error_Exception('Empty sources posted to '.$hook.' hook'); } // The front end sends translations detected as HTML separately. This is to support common external apis. $config['type'] = $post->type; // We need a locale too, which should be valid as it's the same one loaded into the front end. $locale = Loco_Locale::parse( (string) $post->locale ); if( ! $locale->isValid() ){ throw new Loco_error_Exception('Invalid locale'); } // Check if hook is registered, else sources will be returned as-is $action = 'loco_api_translate_'.$hook; if( ! has_filter($action) ){ throw new Loco_error_Exception('API not hooked. Use `add_filter('.var_export($action,1).',...)`'); } // This is effectively a filter whereby the returned array should be a translation of the input array // TODO might be useful for translation hooks to know the PO file this comes from $targets = apply_filters( $action, $sources, $locale, $config ); if( count($targets) !== count($sources) ){ Loco_error_AdminNotices::warn('Number of translations does not match number of source strings'); } // Response data doesn't need anything except the translations $this->set('targets',$targets); return parent::render(); } } UploadController.php 0000644 00000007157 14720715442 0010565 0 ustar 00 <?php /** * Ajax "upload" route, for putting translation files to the server */ class Loco_ajax_UploadController extends Loco_ajax_common_BundleController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); $href = $this->process( $post ); // $this->set('redirect',$href); return parent::render(); } /** * Upload processor shared with standard postback controller * @param Loco_mvc_ViewParams $post script input * @return string redirect to file edit */ public function process( Loco_mvc_ViewParams $post ){ $bundle = $this->getBundle(); $project = $this->getProject( $bundle ); // Chosen folder location should be valid as a posted "dir" parameter if( ! $post->has('dir') ){ throw new Loco_error_Exception('No destination posted'); } $base = loco_constant('WP_CONTENT_DIR'); $parent = new Loco_fs_Directory($post->dir); $parent->normalize($base); // Loco_error_AdminNotices::debug('Destination set to '.$parent->getPath() ); // Ensure file uploaded ok if( ! isset($_FILES['f']) ){ throw new Loco_error_Exception('No file posted'); } $upload = new Loco_data_Upload($_FILES['f']); // Uploaded file will have a temporary name, so real name extension come from _FILES metadata $name = $upload->getOriginalName(); $ext = strtolower( pathinfo($name,PATHINFO_EXTENSION) ); // Loco_error_AdminNotices::debug('Have upload: '.$name.' @ '.$upload->getPath() ); switch( $ext ){ case 'po': case 'mo': $pomo = Loco_gettext_Data::load($upload,$ext); break; default: throw new Loco_error_Exception('Only PO/MO uploads supported'); } // PO/MO data is valid. // get real file name and establish if a locale can be extracted, otherwise get from headers $dummy = new Loco_fs_LocaleFile($name); $locale = $dummy->getLocale(); if( ! $locale->isValid() ){ $value = $pomo->getHeaders()->offsetGet('Language'); $locale = Loco_Locale::parse($value); if( ! $locale->isValid() ){ throw new Loco_error_Exception('Unable to detect language from '.$name ); } } // Fail if user presents a wrongly named file. This is to avoid mixing up text domains. $pofile = $project->initLocaleFile($parent,$locale); if( $pofile->filename() !== $dummy->filename() ){ throw new Loco_error_Exception( sprintf('File must be named %s', $pofile->filename().'.'.$ext ) ); } // Avoid processing if uploaded PO file is identical to existing one if( $pofile->exists() && $pofile->md5() === $upload->md5() ){ throw new Loco_error_Exception( __('Your file is identical to the existing one','loco-translate') ); } // recompile all files including uploaded one $compiler = new Loco_gettext_Compiler($pofile); $compiler->writeAll($pomo,$project); // push recent items on file creation Loco_data_RecentItems::get()->pushBundle($bundle)->persist(); // Redirect to edit this PO. Sync may be required and we're not doing automatically here. $type = strtolower( $this->get('type') ); return Loco_mvc_AdminRouter::generate( sprintf('%s-file-edit',$type), [ 'path' => $pofile->getRelativePath($base), 'bundle' => $bundle->getHandle(), 'domain' => $project->getId(), ] ); } } FsConnectController.php 0000644 00000015713 14720715442 0011220 0 ustar 00 <?php /** * Ajax service that provides remote server authentication for file system *write* operations */ class Loco_ajax_FsConnectController extends Loco_mvc_AjaxController { /** * @var Loco_api_WordPressFileSystem */ private $api; /** * @param Loco_fs_File existing file path (must exist) * @return bool */ private function authorizeDelete( Loco_fs_File $file ){ $files = new Loco_fs_Siblings($file); // require remote authentication if at least one dependant file is not deletable directly foreach( $files->expand() as $file ){ if( ! $this->api->authorizeDelete($file) ){ return false; } } // else no dependants failed deletable test return true; } /** * @param Loco_fs_File file being moved (must exist) * @param Loco_fs_File target path (should not exist) * @return bool */ private function authorizeMove( Loco_fs_File $source, Loco_fs_File $target = null ){ return $this->api->authorizeMove($source,$target); } /** * @param Loco_fs_File $file new file path (should not exist) * @return bool */ private function authorizeCreate( Loco_fs_File $file ){ return $this->api->authorizeCreate($file); } /** * @param Loco_fs_File $file path to update (should exist) * @return bool */ private function authorizeUpdate( Loco_fs_File $file ){ if( ! $this->api->authorizeUpdate($file) ){ return false; } // if backups are enabled, we need to be able to create new files too (i.e. update parent directory) if( Loco_data_Settings::get()->num_backups && ! $this->api->authorizeCopy($file) ){ return false; } // updating file will also recompile binary, which may or may not exist $files = new Loco_fs_Siblings($file); $mofile = $files->getBinary(); if( $mofile && ! $this->api->authorizeSave($mofile) ){ return false; } // else no dependants to update return true; } /** * @param Loco_fs_File $file path which may exist (update it) or may not (create it) * @return bool */ private function authorizeUpload( Loco_fs_File $file ){ if( $file->exists() ){ return $this->api->authorizeUpdate($file); } else { return $this->api->authorizeCreate($file); } } /** * {@inheritdoc} */ public function render(){ // establish operation being authorized (create,delete,etc..) $post = $this->validate(); $type = $post->auth; $func = 'authorize'.ucfirst($type); $auth = [ $this, $func ]; if( ! is_callable($auth) ){ throw new Loco_error_Exception('Unexpected file operation'); } // all auth methods require at least one file argument $file = new Loco_fs_File( $post->path ); $base = loco_constant('WP_CONTENT_DIR'); $file->normalize($base); $args = [$file]; // some auth methods also require a destination/target (move,copy,etc..) if( $dest = $post->dest ){ $file = new Loco_fs_File($dest); $file->normalize($base); $args[] = $file; } // call auth method and respond with status and prompt HTML if connect required try { $this->api = new Loco_api_WordPressFileSystem; if( call_user_func_array($auth,$args) ){ $this->set( 'authed', true ); $this->set( 'valid', $this->api->getOutputCredentials() ); $this->set( 'creds', $this->api->getInputCredentials() ); $this->set( 'method', $this->api->getFileSystem()->method ); $this->set( 'success', __('Connected to remote file system','loco-translate') ); // warning when writing to this location is risky (overwrites during wp update) if( Loco_data_Settings::get()->fs_protect && $file->getUpdateType() ){ if( 'create' === $type ){ $message = __('This file may be overwritten or deleted when you update WordPress','loco-translate'); } else if( 'delete' === $type ){ $message = __('This directory is managed by WordPress, be careful what you delete','loco-translate'); } else if( 'move' === $type ){ $message = __('This directory is managed by WordPress. Removed files may be restored during updates','loco-translate'); } else { $message = __('Changes to this file may be overwritten or deleted when you update WordPress','loco-translate'); } $this->set('warning',$message); } } else { $this->set( 'authed', false ); // HTML form should be set when authorization failed $html = $this->api->getForm(); if( '' === $html || ! is_string($html) ){ // this is the only non-error case where form will not be set. if( 'direct' === loco_constant('FS_METHOD') ){ $html = 'Remote connections are prevented by your WordPress configuration. Direct access only.'; } // else an unknown error occurred when fetching output from request_filesystem_credentials else { $html = 'Failed to get credentials form'; } // displaying error after clicking "connect" to avoid unnecessary warnings when operation may not be required $html = '<form><h2>Connection problem</h2><p>'.$html.'.</p></form>'; } $this->set( 'prompt', $html ); // supporting text based on file operation type explains why auth is required if( 'create' === $type ){ $message = __('Creating this file requires permission','loco-translate'); } else if( 'delete' === $type ){ $message = __('Deleting this file requires permission','loco-translate'); } else if( 'move' === $type ){ $message = __('This move operation requires permission','loco-translate'); } else { $message = __('Saving this file requires permission','loco-translate'); } // message is printed before default text, so needs delimiting. $this->set('message',$message.'.'); } } catch( Loco_error_WriteException $e ){ $this->set('authed', false ); $this->set('reason', $e->getMessage() ); } return parent::render(); } } SyncController.php 0000644 00000013727 14720715442 0010255 0 ustar 00 <?php /** * Ajax "sync" route. * Extracts strings from source (POT or code) and returns to the browser for in-editor merge. */ class Loco_ajax_SyncController extends Loco_mvc_AjaxController { /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); $bundle = Loco_package_Bundle::fromId( $post->bundle ); $project = $bundle->getProjectById( $post->domain ); if( ! $project instanceof Loco_package_Project ){ throw new Loco_error_Exception('No such project '.$post->domain); } // Merging on back end is only required if existing target file exists. // It always should do, and the editor is not permitted to contain unsaved changes when syncing. if( ! $post->has('path') ){ throw new Loco_error_Exception('path argument required'); } $file = new Loco_fs_File( $post->path ); $base = loco_constant('WP_CONTENT_DIR'); $file->normalize($base); $target = Loco_gettext_Data::load($file); // POT file always synced with source code $type = $post->type; if( 'pot' === $type ){ $potfile = null; } // allow front end to configure source file. (will have come from $target headers) else if( $post->sync ){ $potfile = new Loco_fs_File( $post->sync ); $potfile->normalize($base); } // else use project-configured template path (must return a file) else { $potfile = $project->getPot(); } // keep existing behaviour when template is missing, but add warning according to settings. if( $potfile && ! $potfile->exists() ){ $conf = Loco_data_Settings::get()->pot_expected; if( 2 === $conf ){ throw new Loco_error_Exception('Plugin settings disallow missing templates'); } if( 1 === $conf ){ // Translators: %s will be replaced with the name of a missing POT file Loco_error_AdminNotices::warn( sprintf( __('Falling back to source extraction because %s is missing','loco-translate'), $potfile->basename() ) ); } $potfile = null; } // defaults: no msgstr and no json $translate = false; $syncjsons = []; // Parse existing POT for source if( $potfile ){ $this->set('pot', $potfile->basename() ); try { $source = Loco_gettext_Data::load($potfile); } catch( Exception $e ){ // translators: Where %s is the name of the invalid POT file throw new Loco_error_ParseException( sprintf( __('Translation template is invalid (%s)','loco-translate'), $potfile->basename() ) ); } // Sync options are passed through from editor controller via JS $opts = new Loco_gettext_SyncOptions( new LocoPoHeaders ); $opts->setSyncMode( $post->mode ); // Only copy msgstr fields from source if it's a user-defined PO template and "copy translations" was selected. if( 'pot' !== $potfile->extension() ){ $translate = $opts->mergeMsgstr(); } // Only merge JSON translations if specified. This requires we know the localised path where they will be if( $opts->mergeJson() ){ $siblings = new Loco_fs_Siblings($potfile); $syncjsons = $siblings->getJsons( $project->getDomain()->getName() ); } } // else extract POT from source code else { $this->set('pot', '' ); $domain = (string) $project->getDomain(); $extr = new Loco_gettext_Extraction($bundle); $extr->addProject($project); // bail if any files were skipped if( $list = $extr->getSkipped() ){ $n = count($list); $maximum = Loco_mvc_FileParams::renderBytes( wp_convert_hr_to_bytes( Loco_data_Settings::get()->max_php_size ) ); $largest = Loco_mvc_FileParams::renderBytes( $extr->getMaxPhpSize() ); // Translators: (1) Number of files (2) Maximum size of file that will be included (3) Size of the largest encountered $text = _n('%1$s file has been skipped because it\'s %3$s. (Max is %2$s). Check all strings are present before saving.','%1$s files over %2$s have been skipped. (Largest is %3$s). Check all strings are present before saving.',$n,'loco-translate'); $text = sprintf( $text, number_format($n), $maximum, $largest ); // not failing, just warning. Nothing will be saved until user saves editor state Loco_error_AdminNotices::warn( $text ); } // Have source strings. These cannot contain any translations. $source = $extr->includeMeta()->getTemplate($domain); } // establish on back end what strings will be added, removed, and which could be fuzzy-matches $matcher = new Loco_gettext_Matcher($project); $matcher->loadRefs($source,$translate); // merging JSONs must be done before fuzzy matching as it may add source strings if( $syncjsons ) { $matcher->loadJsons($syncjsons); } // Fuzzy matching only applies to syncing PO files. POT files will always do hard sync (add/remove) if( 'po' === $type ){ $fuzziness = Loco_data_Settings::get()->fuzziness; $matcher->setFuzziness( (string) $fuzziness ); } else { $matcher->setFuzziness('0'); } // update matches sources, deferring unmatched for deferred fuzzy match $merged = clone $target; $merged->clear(); $this->set( 'done', $matcher->merge($target,$merged) ); $merged->sort(); $this->set( 'po', $merged->jsonSerialize() ); return parent::render(); } } DownloadController.php 0000644 00000007336 14720715442 0011107 0 ustar 00 <?php /** * Ajax "download" route, for outputting raw gettext file contents. */ class Loco_ajax_DownloadController extends Loco_ajax_common_BundleController { /** * @return string */ private function renderArchive( $path ){ $zipfile = new Loco_fs_File($path); $pofile = new Loco_fs_DummyFile( '/fake/'.$zipfile->filename().'.po'); // Resolving script refs requires configured project $bundle = $this->getBundle(); $project = $this->getProject($bundle); // Create a temporary file for zip, which must work on disk, not in memory $path = wp_tempnam(); if( ! $path || ! file_exists($path) ){ throw new Loco_error_Exception('Failed to create temporary file for zip archive'); } register_shutdown_function('unlink',$path); // initialize zip loco_check_extension('zip'); $z = new ZipArchive; $z->open( $path, ZipArchive::CREATE); $z->setArchiveComment( $bundle->getName() ); $post = Loco_mvc_PostParams::get(); $data = Loco_gettext_Data::fromSource($post->source); $compiler = new Loco_gettext_Compiler($pofile); /* @var Loco_fs_DummyFile $file */ foreach( $compiler->writeAll($data,$project) as $file ){ $z->addFromString( $file->basename(), $file->getContents() ); } $z->close(); return file_get_contents($path); } /** * {@inheritdoc} */ public function render(){ $post = $this->validate(); $path = $this->get('path'); // The UI now replaces .mo with .zip, but requires the ZipArchive extension is installed. if( '.zip' === substr($path,-4) ){ return $this->renderArchive($path); } // Below is for direct .po/pot downloads, plus legacy .mo/l10n.php // mo is only used when zip is not available. php works but not hooked into UI. $file = new Loco_fs_File($path); $file->normalize( loco_constant('WP_CONTENT_DIR') ); $ext = Loco_gettext_Data::ext($file); // posted source must be clean and must parse as whatever the file extension claims to be $raw = $post->source; if( is_string($raw) && '' !== $raw ){ // compile source if target is MO if( 'mo' === $ext ) { $raw = Loco_gettext_Data::fromSource($raw)->msgfmt(); } // supporting .l10n.php for WordPress >= 6.5 else if( 'php' === $ext && class_exists('WP_Translation_File_PHP',false) ){ $raw = Loco_gettext_PhpCache::render( Loco_gettext_Data::fromSource($raw) ); } } // else file can be output directly if it exists. // note that files on disk will not be parsed or manipulated. they will download strictly as-is else if( $file->exists() ){ $raw = $file->getContents(); } // else we can't do anything except bail else { throw new Loco_error_Exception('File not found and no source posted'); } // Observe UTF-8 BOM setting for PO and POT only if( 'po' === $ext || 'pot' === $ext ){ $has_bom = "\xEF\xBB\xBF" === substr($raw,0,3); $use_bom = (bool) Loco_data_Settings::get()->po_utf8_bom; // only alter file if valid UTF-8. Deferring detection overhead until required if( $has_bom !== $use_bom && preg_match('//u',$raw) ){ if( $use_bom ){ $raw = "\xEF\xBB\xBF".$raw; // prepend } else { $raw = substr($raw,3); // strip bom } } } return $raw; } } modal-apis-empty.php 0000644 00000001603 14720717223 0010444 0 ustar 00 <?php /* * Modal for when no APIs are configured. */ /* @var Loco_mvc_ViewParams $help */ /* @var Loco_mvc_ViewParams $prof */ ?><div id="loco-auto" class="loco-alert" title="<?php esc_html_e('No translation APIs configured','loco-translate');?>"> <p> <?php esc_html_e('Add automatic translation services in the plugin settings.','loco-translate')?> </p> <nav> <a href="<?php $this->route('config-apis')->e('href')?>" class="button button-link has-icon icon-cog"><?php esc_html_e('Settings','loco-translate'); ?></a> <a href="<?php $help->e('href')?>" class="button button-link has-icon icon-help" target="_blank"><?php $help->e('text'); ?></a> <a href="<?php $prof->e('href')?>" class="button button-link has-icon icon-group" target="_blank"><?php $prof->e('text'); ?></a> </nav> </div>