PK ! bփ modal-apis-empty.phpnu [
'.htmlentities($line,ENT_COMPAT,'UTF-8').'
';
// 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[] = ''.htmlentities($line,ENT_COMPAT,'UTF-8').'
';
}
}
// 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();
}
}
PK ! /# # ApisController.phpnu [ 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();
}
}
PK ! bWo o UploadController.phpnu [ 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(),
] );
}
}PK ! I& FsConnectController.phpnu [ 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 = '';
}
$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();
}
}PK ! t SyncController.phpnu [ 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();
}
}PK ! 1 DownloadController.phpnu [ 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;
}
}
PK ! bփ modal-apis-empty.phpnu [ PK ! | g modal-apis-batch.phpnu [ PK ! Y PingController.phpnu [ PK ! ~+
V SaveController.phpnu [ PK ! V4~p p 1 DiffController.phpnu [ PK ! t
# common/BundleController.phpnu [ PK ! h= = ( XgettextController.phpnu [ PK ! P V5 MsginitController.phpnu [ PK ! κl l :P DownloadConfController.phpnu [ PK ! Vq q T FsReferenceController.phpnu [ PK ! /# # q ApisController.phpnu [ PK ! bWo o UploadController.phpnu [ PK ! I& FsConnectController.phpnu [ PK ! t ԫ SyncController.phpnu [ PK ! 1 DownloadController.phpnu [ PK