Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)始,故人唐宰相鲁公,🆚开府南服,余以布衣从戎。明年,别公漳水湄。后明年,公以事过张睢阳庙及颜杲卿所尝往来处,悲歌慷慨,卒不负其言而从之游。今其诗具在,可考也。😭
block.json 0000644 00000002127 15233153156 0006537 0 ustar 00 {
"apiVersion": 2,
"name": "core/file",
"title": "File",
"category": "media",
"description": "Add a link to a downloadable file.",
"keywords": [ "document", "pdf", "download" ],
"textdomain": "default",
"attributes": {
"id": {
"type": "number"
},
"href": {
"type": "string"
},
"fileName": {
"type": "string",
"source": "html",
"selector": "a:not([download])"
},
"textLinkHref": {
"type": "string",
"source": "attribute",
"selector": "a:not([download])",
"attribute": "href"
},
"textLinkTarget": {
"type": "string",
"source": "attribute",
"selector": "a:not([download])",
"attribute": "target"
},
"showDownloadButton": {
"type": "boolean",
"default": true
},
"downloadButtonText": {
"type": "string",
"source": "html",
"selector": "a[download]"
},
"displayPreview": {
"type": "boolean"
},
"previewHeight": {
"type": "number",
"default": 600
}
},
"supports": {
"anchor": true,
"align": true
},
"viewScript": "file:./view.min.js",
"editorStyle": "wp-block-file-editor",
"style": "wp-block-file"
}
style.css 0000644 00000004043 15233153156 0006423 0 ustar 00 /**
* Colors
*/
/**
* Breakpoints & Media Queries
*/
/**
* SCSS Variables.
*
* Please use variables from this sheet to ensure consistency across the UI.
* Don't add to this sheet unless you're pretty sure the value will be reused in many places.
* For example, don't add rules to this sheet that affect block visuals. It's purely for UI.
*/
/**
* Colors
*/
/**
* Fonts & basic variables.
*/
/**
* Grid System.
* https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/
*/
/**
* Dimensions.
*/
/**
* Shadows.
*/
/**
* Editor widths.
*/
/**
* Block & Editor UI.
*/
/**
* Block paddings.
*/
/**
* React Native specific.
* These variables do not appear to be used anywhere else.
*/
/**
* Breakpoint mixins
*/
/**
* Long content fade mixin
*
* Creates a fading overlay to signify that the content is longer
* than the space allows.
*/
/**
* Focus styles.
*/
/**
* Applies editor left position to the selector passed as argument
*/
/**
* Styles that are reused verbatim in a few places
*/
/**
* Allows users to opt-out of animations via OS-level preferences.
*/
/**
* Reset default styles for JavaScript UI based pages.
* This is a WP-admin agnostic reset
*/
/**
* Reset the WP Admin page styles for Gutenberg-like pages.
*/
.wp-block-file {
margin-bottom: 1.5em;
}
.wp-block-file.aligncenter {
text-align: center;
}
.wp-block-file.alignright {
/*rtl:ignore*/
text-align: right;
}
.wp-block-file .wp-block-file__embed {
margin-bottom: 1em;
}
.wp-block-file .wp-block-file__button {
background: #32373c;
border-radius: 2em;
color: #fff;
font-size: 0.8em;
padding: 0.5em 1em;
}
.wp-block-file a.wp-block-file__button {
text-decoration: none;
}
.wp-block-file a.wp-block-file__button:hover, .wp-block-file a.wp-block-file__button:visited, .wp-block-file a.wp-block-file__button:focus, .wp-block-file a.wp-block-file__button:active {
box-shadow: none;
color: #fff;
opacity: 0.85;
text-decoration: none;
}
.wp-block-file * + .wp-block-file__button {
margin-left: 0.75em;
} editor.min.css 0000644 00000001172 15233153156 0007333 0 ustar 00 .wp-block-file{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:0}.wp-block[data-align=left]>.wp-block-file,.wp-block[data-align=right]>.wp-block-file{height:auto}.wp-block-file .components-resizable-box__container{margin-bottom:1em}.wp-block-file .wp-block-file__preview{margin-bottom:1em;width:100%;height:100%}.wp-block-file .wp-block-file__preview-overlay{position:absolute;top:0;right:0;bottom:0;left:0}.wp-block-file .wp-block-file__content-wrapper{flex-grow:1}.wp-block-file a{min-width:1em}.wp-block-file .wp-block-file__button-richtext-wrapper{display:inline-block;margin-left:.75em} style.min.css 0000644 00000001221 15233153156 0007200 0 ustar 00 .wp-block-file{margin-bottom:1.5em}.wp-block-file.aligncenter{text-align:center}.wp-block-file.alignright{text-align:right}.wp-block-file .wp-block-file__embed{margin-bottom:1em}.wp-block-file .wp-block-file__button{background:#32373c;border-radius:2em;color:#fff;font-size:.8em;padding:.5em 1em}.wp-block-file a.wp-block-file__button{text-decoration:none}.wp-block-file a.wp-block-file__button:active,.wp-block-file a.wp-block-file__button:focus,.wp-block-file a.wp-block-file__button:hover,.wp-block-file a.wp-block-file__button:visited{box-shadow:none;color:#fff;opacity:.85;text-decoration:none}.wp-block-file *+.wp-block-file__button{margin-left:.75em} editor.css 0000644 00000004010 15233153156 0006543 0 ustar 00 /**
* Colors
*/
/**
* Breakpoints & Media Queries
*/
/**
* SCSS Variables.
*
* Please use variables from this sheet to ensure consistency across the UI.
* Don't add to this sheet unless you're pretty sure the value will be reused in many places.
* For example, don't add rules to this sheet that affect block visuals. It's purely for UI.
*/
/**
* Colors
*/
/**
* Fonts & basic variables.
*/
/**
* Grid System.
* https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/
*/
/**
* Dimensions.
*/
/**
* Shadows.
*/
/**
* Editor widths.
*/
/**
* Block & Editor UI.
*/
/**
* Block paddings.
*/
/**
* React Native specific.
* These variables do not appear to be used anywhere else.
*/
/**
* Breakpoint mixins
*/
/**
* Long content fade mixin
*
* Creates a fading overlay to signify that the content is longer
* than the space allows.
*/
/**
* Focus styles.
*/
/**
* Applies editor left position to the selector passed as argument
*/
/**
* Styles that are reused verbatim in a few places
*/
/**
* Allows users to opt-out of animations via OS-level preferences.
*/
/**
* Reset default styles for JavaScript UI based pages.
* This is a WP-admin agnostic reset
*/
/**
* Reset the WP Admin page styles for Gutenberg-like pages.
*/
.wp-block-file {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
}
.wp-block[data-align=left] > .wp-block-file, .wp-block[data-align=right] > .wp-block-file {
height: auto;
}
.wp-block-file .components-resizable-box__container {
margin-bottom: 1em;
}
.wp-block-file .wp-block-file__preview {
margin-bottom: 1em;
width: 100%;
height: 100%;
}
.wp-block-file .wp-block-file__preview-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.wp-block-file .wp-block-file__content-wrapper {
flex-grow: 1;
}
.wp-block-file a {
min-width: 1em;
}
.wp-block-file .wp-block-file__button-richtext-wrapper {
display: inline-block;
margin-left: 0.75em;
} style-rtl.css 0000644 00000004023 15233153156 0007220 0 ustar 00 /**
* Colors
*/
/**
* Breakpoints & Media Queries
*/
/**
* SCSS Variables.
*
* Please use variables from this sheet to ensure consistency across the UI.
* Don't add to this sheet unless you're pretty sure the value will be reused in many places.
* For example, don't add rules to this sheet that affect block visuals. It's purely for UI.
*/
/**
* Colors
*/
/**
* Fonts & basic variables.
*/
/**
* Grid System.
* https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/
*/
/**
* Dimensions.
*/
/**
* Shadows.
*/
/**
* Editor widths.
*/
/**
* Block & Editor UI.
*/
/**
* Block paddings.
*/
/**
* React Native specific.
* These variables do not appear to be used anywhere else.
*/
/**
* Breakpoint mixins
*/
/**
* Long content fade mixin
*
* Creates a fading overlay to signify that the content is longer
* than the space allows.
*/
/**
* Focus styles.
*/
/**
* Applies editor left position to the selector passed as argument
*/
/**
* Styles that are reused verbatim in a few places
*/
/**
* Allows users to opt-out of animations via OS-level preferences.
*/
/**
* Reset default styles for JavaScript UI based pages.
* This is a WP-admin agnostic reset
*/
/**
* Reset the WP Admin page styles for Gutenberg-like pages.
*/
.wp-block-file {
margin-bottom: 1.5em;
}
.wp-block-file.aligncenter {
text-align: center;
}
.wp-block-file.alignright {
text-align: right;
}
.wp-block-file .wp-block-file__embed {
margin-bottom: 1em;
}
.wp-block-file .wp-block-file__button {
background: #32373c;
border-radius: 2em;
color: #fff;
font-size: 0.8em;
padding: 0.5em 1em;
}
.wp-block-file a.wp-block-file__button {
text-decoration: none;
}
.wp-block-file a.wp-block-file__button:hover, .wp-block-file a.wp-block-file__button:visited, .wp-block-file a.wp-block-file__button:focus, .wp-block-file a.wp-block-file__button:active {
box-shadow: none;
color: #fff;
opacity: 0.85;
text-decoration: none;
}
.wp-block-file * + .wp-block-file__button {
margin-right: 0.75em;
} editor-rtl.css 0000644 00000004011 15233153156 0007343 0 ustar 00 /**
* Colors
*/
/**
* Breakpoints & Media Queries
*/
/**
* SCSS Variables.
*
* Please use variables from this sheet to ensure consistency across the UI.
* Don't add to this sheet unless you're pretty sure the value will be reused in many places.
* For example, don't add rules to this sheet that affect block visuals. It's purely for UI.
*/
/**
* Colors
*/
/**
* Fonts & basic variables.
*/
/**
* Grid System.
* https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/
*/
/**
* Dimensions.
*/
/**
* Shadows.
*/
/**
* Editor widths.
*/
/**
* Block & Editor UI.
*/
/**
* Block paddings.
*/
/**
* React Native specific.
* These variables do not appear to be used anywhere else.
*/
/**
* Breakpoint mixins
*/
/**
* Long content fade mixin
*
* Creates a fading overlay to signify that the content is longer
* than the space allows.
*/
/**
* Focus styles.
*/
/**
* Applies editor left position to the selector passed as argument
*/
/**
* Styles that are reused verbatim in a few places
*/
/**
* Allows users to opt-out of animations via OS-level preferences.
*/
/**
* Reset default styles for JavaScript UI based pages.
* This is a WP-admin agnostic reset
*/
/**
* Reset the WP Admin page styles for Gutenberg-like pages.
*/
.wp-block-file {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
}
.wp-block[data-align=left] > .wp-block-file, .wp-block[data-align=right] > .wp-block-file {
height: auto;
}
.wp-block-file .components-resizable-box__container {
margin-bottom: 1em;
}
.wp-block-file .wp-block-file__preview {
margin-bottom: 1em;
width: 100%;
height: 100%;
}
.wp-block-file .wp-block-file__preview-overlay {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.wp-block-file .wp-block-file__content-wrapper {
flex-grow: 1;
}
.wp-block-file a {
min-width: 1em;
}
.wp-block-file .wp-block-file__button-richtext-wrapper {
display: inline-block;
margin-right: 0.75em;
} style-rtl.min.css 0000644 00000001222 15233153156 0010000 0 ustar 00 .wp-block-file{margin-bottom:1.5em}.wp-block-file.aligncenter{text-align:center}.wp-block-file.alignright{text-align:right}.wp-block-file .wp-block-file__embed{margin-bottom:1em}.wp-block-file .wp-block-file__button{background:#32373c;border-radius:2em;color:#fff;font-size:.8em;padding:.5em 1em}.wp-block-file a.wp-block-file__button{text-decoration:none}.wp-block-file a.wp-block-file__button:active,.wp-block-file a.wp-block-file__button:focus,.wp-block-file a.wp-block-file__button:hover,.wp-block-file a.wp-block-file__button:visited{box-shadow:none;color:#fff;opacity:.85;text-decoration:none}.wp-block-file *+.wp-block-file__button{margin-right:.75em} editor-rtl.min.css 0000644 00000001173 15233153156 0010133 0 ustar 00 .wp-block-file{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:0}.wp-block[data-align=left]>.wp-block-file,.wp-block[data-align=right]>.wp-block-file{height:auto}.wp-block-file .components-resizable-box__container{margin-bottom:1em}.wp-block-file .wp-block-file__preview{margin-bottom:1em;width:100%;height:100%}.wp-block-file .wp-block-file__preview-overlay{position:absolute;top:0;left:0;bottom:0;right:0}.wp-block-file .wp-block-file__content-wrapper{flex-grow:1}.wp-block-file a{min-width:1em}.wp-block-file .wp-block-file__button-richtext-wrapper{display:inline-block;margin-right:.75em} InfoController.php 0000644 00000021603 15233632214 0010217 0 ustar 00 <?php
/**
* File info / management view.
*/
class Loco_admin_file_InfoController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('fileinfo');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set('title', $file->basename().' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return [
__('Overview','loco-translate') => $this->viewSnippet('tab-file-info'),
];
}
/**
* {@inheritdoc}
*/
public function render(){
/* @var Loco_fs_LocaleFile $file */
$file = $this->get('file');
$this->setFileTitle($file);
if( $fail = $this->getFileError($file) ){
return $fail;
}
// file info
$ext = strtolower( $file->extension() );
$finfo = Loco_mvc_FileParams::create( $file );
$this->set('file', $finfo );
$finfo['type'] = strtoupper($ext);
if( $file->exists() ){
$finfo['existent'] = true;
$finfo['writable'] = $file->writable();
$finfo['deletable'] = $file->deletable();
$finfo['mtime'] = $file->modified();
// Notify if file is managed by WordPress
$api = new Loco_api_WordPressFileSystem;
if( $api->isAutoUpdatable($file) ){
$finfo['autoupdate'] = true;
}
}
// location info
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
$dinfo = Loco_mvc_FileParams::create( $dir );
$this->set('dir', $dinfo );
$dinfo['type'] = $dir->getTypeId();
if( $dir->exists() && $dir->isDirectory() ){
$dinfo['existent'] = true;
$dinfo['writable'] = $dir->writable();
}
// secure download link
$args = new Loco_mvc_HiddenFields( [
'route' => 'download',
'action' => 'loco_download',
'path' => $file->getRelativePath(loco_constant('WP_CONTENT_DIR')),
] );
$args->setNonce('download');
$finfo['download'] = $args->getHref( admin_url('admin-ajax.php','relative') );
// allow link to modify headers/settings
$finfo['configure'] = str_replace('file-info','file-head',$_SERVER['REQUEST_URI']);
// collect note worthy problems with file headers
$debugging = loco_debugging();
$debug = [];
// get the name of the web server for information purposes
$this->set('httpd', Loco_compat_PosixExtension::getHttpdUser() );
// unknown file template if required
$locale = null;
$project = null;
$tpl = 'admin/file/info-other';
// we should know the project the file belongs to, but permitting orphans for debugging
try {
$project = $this->getProject();
$template = $project->getPot();
$isTemplate = $template && $file->equal($template);
$this->set('isTemplate', $isTemplate );
$this->set('project', $project );
}
catch( Loco_error_Exception $e ){
$debug[] = $e->getMessage();
$isTemplate = false;
$template = null;
}
// file will be Gettext most likely
if( 'pot' === $ext || 'po' === $ext || 'mo' === $ext ){
// treat as template until locale verified
$tpl = 'admin/file/info-pot';
// don't attempt to pull locale of template file
if( 'pot' !== $ext && ! $isTemplate ){
$locale = $file->getLocale();
if( $locale->isValid() ){
// find PO/MO counter parts
if( 'po' === $ext ){
$tpl = 'admin/file/info-po';
$sibling = $file->cloneExtension('mo');
}
else {
$tpl = 'admin/file/info-mo';
$sibling = $file->cloneExtension('po');
}
$info = Loco_mvc_FileParams::create($sibling);
$this->set( 'sibling', $info );
if( $sibling->exists() ){
$info['existent'] = true;
$info['writable'] = $sibling->writable();
}
}
}
// Do full parse to get stats and headers
try {
$data = Loco_gettext_Data::load($file);
$head = $data->getHeaders();
$author = $head->trimmed('Last-Translator') or $author = __('Unknown author','loco-translate');
$this->set( 'author', $author );
// date headers may not be same as file modification time (files copied to server etc..)
$podate = $head->trimmed( $locale ? 'PO-Revision-Date' : 'POT-Creation-Date' );
$potime = Loco_gettext_Data::parseDate($podate) or $potime = $file->modified();
$this->set('potime', $potime );
// access to meta stats, normally cached on listing pages
$meta = Loco_gettext_Metadata::create($file,$data);
$this->set( 'meta', $meta );
// allow PO header to specify alternative template for sync
$opts = new Loco_gettext_SyncOptions($head);
if( $opts->hasTemplate() ){
$altpot = $opts->getTemplate();
$altpot->normalize( $this->getBundle()->getDirectoryPath() );
if( $altpot->exists() && ( ! $template || ! $template->equal($altpot) ) ){
$template = $altpot;
}
}
// establish whether PO is in sync with POT
if( $template && ! $isTemplate && 'po' === $ext && $template->exists() ){
try {
$this->set('potfile', new Loco_mvc_FileParams( [
'synced' => Loco_gettext_Data::load($template)->equalSource($data),
], $template ) );
}
catch( Exception $e ){
// ignore invalid template in this context
}
}
if( $debugging ){
// missing or invalid headers are tolerated but developers should be notified
if( ! count($head) ){
$debug[] = __('File does not have a valid header','loco-translate');
}
// Language header sanity checks, raising developer (debug) warnings
if( $locale ){
$value = $head->trimmed('Language');
if( '' !== $value ){
if( Loco_Locale::parse($value)->__toString() !== $locale->__toString() ){
// translators: Warning that the language in the file header (1) does not match the file name (2)
$debug[]= sprintf( __('Language header is "%1$s" but file name contains "%2$s"','loco-translate'), $value, $locale );
}
}
$value = $head->trimmed('Plural-Forms');
if( '' !== $value ){
try {
$locale->setPluralFormsHeader($value);
}
catch( InvalidArgumentException $e ){
$debug[] = sprintf('Plural-Forms header is invalid, "%s"',$value);
}
}
}
// Other sanity checks
if( $project && $head->has('Project-Id-Version') ){
$inProj = $project->getName();
$inHead = $head->trimmed('Project-Id-Version');
if( false === strpos($inProj,$inHead) && false === strpos($inHead,$inProj) ) {
$debug[] = sprintf( 'Project-Id-Version header is "%s" but project is "%s"', $inHead, $inProj );
}
}
}
// Count source text for templates only (assumed English)
if( 'admin/file/info-pot' === $tpl ){
$counter = new Loco_gettext_WordCount($data);
$this->set('words', $counter->count() );
}
}
catch( Loco_error_Exception $e ){
$this->set('error', $e->getMessage() );
$tpl = 'admin/file/info-other';
}
}
if( $debugging && $debug ){
$this->set( 'debug', new Loco_mvc_ViewParams($debug) );
}
return $this->view( $tpl );
}
}
MoveController.php 0000644 00000022075 15233632214 0010236 0 ustar 00 <?php
/**
* Translation set relocation tool.
* Moves PO/MO pair and all related files to a new location
*/
class Loco_admin_file_MoveController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
/* @var Loco_fs_File $file */
if( $file->exists() && ! $file->isDirectory() ){
$files = new Loco_fs_Siblings($file);
$files->setDomain( $this->getDomain() );
// nonce action will be specific to file for extra security
$path = $file->getPath();
$action = 'move:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( [] );
$fields->setNonce( $action );
$fields['auth'] = 'move';
$fields['path'] = $this->get('path');
$this->set('hidden',$fields );
// attempt move if valid nonce posted back
while( $this->checkNonce($action) ){
$post = Loco_mvc_PostParams::get();
// Chosen location should be valid as a posted "dest" parameter
if( ! $post->has('dest') ){
Loco_error_AdminNotices::err('No destination posted');
break;
}
$target = new Loco_fs_LocaleFile( $post->dest );
$ext = $target->extension();
// could be a directory when we wanted the full path to the file
if( $target->isDirectory() ){
Loco_error_AdminNotices::err('Enter the full path to the .'.$file->extension().' file, not the directory');
break;
}
// primary file extension should only be permitted to change between po and pot
if( $ext !== $file->extension() && 'po' !== $ext && 'pot' !== $ext ){
Loco_error_AdminNotices::err('Invalid file extension, .po or .pot only');
break;
}
$target->normalize( loco_constant('WP_CONTENT_DIR') );
$target_dir = $target->getParent()->getPath();
// Primary file gives template remapping, so all files are renamed with same stub.
// this can only be one of three things: (en -> en) or (foo-en -> en) or (en -> foo-en)
// suffix will then consist of file extension, plus any other stuff like backup file date.
$target_base = $target->filename();
$source_snip = strlen( $file->filename() );
// buffer all files to move to preempt write failures
$movable = [];
$api = new Loco_api_WordPressFileSystem;
foreach( $files->expand() as $source ){
$suffix = substr( $source->basename(), $source_snip ); // <- e.g. "-backup.po~"
$target = new Loco_fs_File( $target_dir.'/'.$target_base.$suffix );
// permit valid change of file extension on primary source file (po/pot)
if( $source === $files->getSource() && $target->extension() !== $ext ){
$target = $target->cloneExtension($ext);
}
if( ! $api->authorizeMove($source,$target) ) {
Loco_error_AdminNotices::err('Failed to authorize relocation of '.$source->basename() );
break 2;
}
$movable[] = [$source,$target];
}
// commit moves. If any fail we'll have separated the files, which is bad
$count = 0;
$total = count($movable);
foreach( $movable as $pair ){
try {
$pair[0]->move( $pair[1] );
$count++;
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
// flash messages for display after redirect
try {
if( $count ) {
// translators: %s is the quantity of files which were successfully moved
Loco_data_Session::get()->flash( 'success', sprintf( _n( '%s file moved', '%s files moved', $total, 'loco-translate' ), $total ) );
}
if( $total > $count ){
$diff = $total - $count;
// translators: %s is the quantity of files which failed to be moved
Loco_data_Session::get()->flash( 'error', sprintf( _n( '%s file could not be moved', '%s files could not be moved', $diff, 'loco-translate' ), $diff ) );
}
Loco_data_Session::close();
}
catch( Exception $e ){
// tolerate session failure
}
// redirect to bundle overview
$href = Loco_mvc_AdminRouter::generate( $this->get('type').'-view', [ 'bundle' => $this->get('bundle') ] );
if( wp_redirect($href) ){
exit;
}
// end pseudo loop
break;
}
}
// set page title before render sets inline title
$bundle = $this->getBundle();
// translators: Page title where %s is the name of a file to be moved
$this->set('title', sprintf( __('Move %s','loco-translate'), $file->basename() ).' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
// relocation requires knowing text domain and locale
$files = new Loco_fs_Siblings($file);
try {
$project = $this->getProject();
$files->setDomain( $project->getDomain()->getName() );
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::warn($e->getMessage());
$project = null;
}
$file = new Loco_fs_LocaleFile( $files->getSource() );
$locale = $file->getLocale();
// switch between canonical move and custom file path mode
$custom = is_null($project) || $this->get('custom') || 'po' !== $file->extension() || ! $locale->isValid();
// common page elements:
$this->set('files',$files->expand() );
// phpcs:ignore -- duplicate string
$this->setFileTitle($file,__('Move %s','loco-translate'));
$this->enqueueScript('move');
// set info for existing file location
$content_dir = loco_constant('WP_CONTENT_DIR');
$current = $file->getRelativePath($content_dir);
$parent = new Loco_fs_LocaleDirectory( $file->dirname() );
$typeId = $parent->getTypeId();
$this->set('current', new Loco_mvc_ViewParams([
'path' => $parent->getRelativePath($content_dir),
'type' => $parent->getTypeLabel($typeId),
]) );
// moving files will require deletion permission on current file location
// plus write permission on target location, but we don't know what that is yet.
$fields = $this->prepareFsConnect('move',$current);
$fields['path'] = '';
$fields['dest'] = '';
// custom file move template (POT mode)
if( $custom ){
$this->get('hidden')->offsetSet('custom','1');
$this->set('file', Loco_mvc_FileParams::create($file) );
return $this->view('admin/file/move-pot');
}
// establish valid locations for translation set, which may include current:
$filechoice = $project->initLocaleFiles($locale);
// start with current location so always first in list
$locations = [];
$locations[$typeId] = new Loco_mvc_ViewParams( [
'label' => $parent->getTypeLabel($typeId),
'paths' => [ new Loco_mvc_ViewParams( [
'path' => $current,
'active' => true,
] ) ]
] );
/* @var Loco_fs_File $pofile */
foreach( $filechoice as $pofile ){
$relpath = $pofile->getRelativePath($content_dir);
if( $current === $relpath ){
continue;
}
// initialize location type (system, etc..)
$parent = new Loco_fs_LocaleDirectory( $pofile->dirname() );
$typeId = $parent->getTypeId();
if( ! isset($locations[$typeId]) ){
$locations[$typeId] = new Loco_mvc_ViewParams( [
'label' => $parent->getTypeLabel($typeId),
'paths' => [],
] );
}
$choice = new Loco_mvc_ViewParams( [
'path' => $relpath,
] );
$locations[$typeId]['paths'][] = $choice;
}
$this->set('locations', $locations );
$this->set('advanced', $_SERVER['REQUEST_URI'].'&custom=1' );
return $this->view('admin/file/move-po');
}
} DeleteController.php 0000644 00000010721 15233632214 0010525 0 ustar 00 <?php
/**
* File delete function
*/
class Loco_admin_file_DeleteController extends Loco_admin_file_BaseController {
/**
* Expand single path to all files that will be deleted
* @param Loco_fs_File $file primary file being deleted, probably the PO
* @return array
*/
private function expandFiles( Loco_fs_File $file ){
try {
$siblings = new Loco_fs_Siblings( $file );
}
catch( InvalidArgumentException $e ){
$ext = $file->extension();
throw new Loco_error_Exception( sprintf('Refusing to delete a %s file', strtoupper($ext) ) );
}
$siblings->setDomain( $this->getDomain() );
return $siblings->expand();
}
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
// set up form for delete confirmation
if( $file->exists() && ! $file->isDirectory() ){
// nonce action will be specific to file for extra security
// TODO could also add file MD5 to avoid deletion after changes made.
$path = $file->getPath();
$action = 'delete:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( [] );
$fields->setNonce( $action );
$this->set( 'hidden', $fields );
// attempt delete if valid nonce posted back
if( $this->checkNonce($action) ){
$api = new Loco_api_WordPressFileSystem;
// delete dependant files first, so master still exists if others fail
$files = array_reverse( $this->expandFiles($file) );
try {
/* @var $trash Loco_fs_File */
foreach( $files as $trash ){
$api->authorizeDelete($trash);
$trash->unlink();
}
// flash message for display after redirect
try {
$n = count( $files );
// translators: %u is a number of files which were successfully deleted
Loco_data_Session::get()->flash('success', sprintf( _n('%u file deleted','%u files deleted',$n,'loco-translate'),$n) );
Loco_data_Session::close();
}
catch( Exception $e ){
// tolerate session failure
}
// redirect to bundle overview
$href = Loco_mvc_AdminRouter::generate( $this->get('type').'-view', [ 'bundle' => $this->get('bundle') ] );
if( wp_redirect($href) ){
exit;
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
}
}
// set page title before render sets inline title
$bundle = $this->getBundle();
// translators: Page title where %s is the name of a file to be deleted
$this->set('title', sprintf( __('Delete %s','loco-translate'), $file->basename() ).' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$files = $this->expandFiles( $file );
$info = Loco_mvc_FileParams::create($file);
$this->set( 'info', $info );
// phpcs:ignore -- duplicate string
$this->setFileTitle( $file, __('Delete %s','loco-translate') );
// warn about additional files that will be deleted along with this
if( $deps = array_slice($files,1) ){
$count = count($deps);
// translators: Warning that deleting a file will also delete others. %s indicates that quantity.
$this->set('warn', sprintf( _n( '%s dependent file will also be deleted', '%s dependent files will also be deleted', $count, 'loco-translate' ), $count ) );
$infos = [];
foreach( $deps as $depfile ){
$infos[] = Loco_mvc_FileParams::create( $depfile );
}
$this->set('deps', $infos );
}
$this->prepareFsConnect( 'delete', $this->get('path') );
$this->enqueueScript('delete');
return $this->view('admin/file/delete');
}
} HeadController.php 0000644 00000015323 15233632214 0010167 0 ustar 00 <?php
/**
* Controller to edit PO file header values and sync settings
*/
class Loco_admin_file_HeadController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
/* @var Loco_fs_File $file */
if( $file->exists() && ! $file->isDirectory() ){
// nonce action will be specific to file for extra security
$path = $file->getPath();
$action = 'head:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( [] );
$fields->setNonce( $action );
$fields['auth'] = 'update';
$fields['path'] = $this->get('path');
$this->set('hidden',$fields );
// attempt update if valid nonce posted back
while( $this->checkNonce($action) ) {
$data = Loco_gettext_Data::load($file);
// check some headers prior ro updating
$head = $data->getHeaders();
$plurals = $head['Plural-Forms'];
// in advanced mode we will set all headers from form as-is.
$post = Loco_mvc_PostParams::get();
if( $post->has('headers') ){
$raw = (array) $post->headers;
$head = new LocoPoHeaders($raw);
$data->setHeaders($head);
// modifying character encoding is not currently supported
if( 'UTF-8' !== $head->getCharset() ){
Loco_error_AdminNotices::warn('Loco Translate only supports UTF-8 encoded files');
$head['Content-Type'] = 'text/plain; charset=UTF-8';
}
}
// in basic mode we save PO sync settings only
else if( 'po' !== $file->extension() ){
throw new Exception( 'Sync settings apply to PO files only' );
}
else if( ! $post->has('conf') ){
throw new Exception( 'Unexpected post data' );
}
else {
$conf = new Loco_gettext_SyncOptions($head);
$raw = (array) $post->conf;
$conf->setTemplate( $raw['template'] );
$mode = isset($raw['mode']) ? $raw['mode'] : 'pot';
if( isset($raw['json']) ){
$mode .= ',json';
}
$conf->setSyncMode($mode);
}
// Validate and remove redundant headers
$conf = new Loco_gettext_SyncOptions($head);
$head = $conf->getHeaders();
// Render PO without modifying sort order
if( $file instanceof Loco_fs_LocaleFile && $file->getLocale()->isValid() ){
$compiler = new Loco_gettext_Compiler($file);
$compiler->writePo($data);
// If we save the PO we should recompile MO, but only actually required if plural forms have changed
if( $head['Plural-Forms'] !== $plurals ){
$compiler->writeMo($data);
}
}
// else save just the actual file. Probably .pot, or wrongly named .po file.
else {
$api = new Loco_api_WordPressFileSystem;
$api->authorizeUpdate($file);
$file->putContents( $data->msgcat() );
}
// flash message for display after redirect
try {
// translators: Success notice where %s is a file extension, e.g. "PO"
Loco_data_Session::get()->flash('success',sprintf( __('%s file saved','loco-translate'), strtoupper($file->extension()) ));
Loco_data_Session::close();
}
catch( Exception $e ){
// tolerate session failure
}
if( wp_redirect($_SERVER['REQUEST_URI']) ){
exit;
}
break;
}
}
$bundle = $this->getBundle();
$this->set('title', 'Configure '.$file->basename().' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
$fail = $this->getFileError($file);
if( is_string($fail) && '' !== $fail ){
return $fail;
}
// parse PO header
$head = Loco_gettext_Data::head($file);
$this->set('head',$head);
// Remote file system required if file is not directly writable
$this->prepareFsConnect( 'update', $this->get('path') );
$this->enqueueScript('head');
// localized files only can have sync settings
$localized = $file instanceof Loco_fs_LocaleFile && $file->getLocale()->isValid();
// Advanced mode shows all headers in one form
$this->setFileTitle($file);
if( $this->get('advanced') || ! $localized ){
return $this->view('admin/file/head');
}
// link to advanced mode and display sync settings form (PO only)
$this->set('advanced', $_SERVER['REQUEST_URI'].'&advanced=1' );
$conf = new Loco_gettext_SyncOptions($head);
$this->set('conf', $conf );
// perform some basic validation of sync mode
if( $conf->hasTemplate() ){
$potfile = $conf->getTemplate();
}
else {
$potfile = new Loco_fs_LocaleFile( (string) $this->getProject()->getPot() );
}
if( $conf->mergeMsgstr() && 'po' !== $potfile->extension() ){
Loco_error_AdminNotices::warn('Copying translations requires template is a PO file');
}
if( $conf->mergeJson() && ! $potfile->getLocale()->isValid() ){
Loco_error_AdminNotices::warn('Merging JSON files requires template has a localized file path');
}
// may or may not already have a custom template with a known locale
$this->set('potName','--');
if( $conf->hasTemplate() ){
$file = $conf->getTemplate();
$file->normalize( $this->getBundle()->getDirectoryPath() );
if( ! $file->exists() ){
Loco_error_AdminNotices::warn('Configured template does not currently exist');
}
$this->set('potName', $file->basename() );
}
// force basic POT mode for unconfigured templates
else if( '' === $conf->getSyncMode() ){
$conf->setSyncMode('pot');
}
return $this->view('admin/file/conf');
}
}
ViewController.php 0000644 00000007225 15233632214 0010242 0 ustar 00 <?php
/**
* File view / source formatted view.
*/
class Loco_admin_file_ViewController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('poview');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set( 'title', 'Source of '.$file->basename().' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return [
__('Overview') => $this->viewSnippet('tab-file-view'),
];
}
private function getUtf8Source( Loco_fs_File $file, LocoPoHeaders $head ){
$src = $file->getContents();
try {
$src = loco_remove_bom( $src, $cs );
if( '' === $cs ){
$cs = $head->getCharset();
}
if( '' !== $cs ){
$src = loco_convert_utf8($src,$cs,false);
}
}
catch ( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
return $src;
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
/* @var Loco_fs_File $file */
$file = $this->get('file');
$this->setFileTitle($file);
$type = strtolower( $file->extension() );
if( $fail = $this->getFileError($file) ){
return $fail;
}
// Establish if file belongs to a configured project
$bundle = null;
$project = null;
try {
$bundle = $this->getBundle();
$project = $this->getProject();
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// Parse data before rendering, so we know it's a valid Gettext format
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load( $file );
}
catch( Loco_error_ParseException $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
$data = Loco_gettext_Data::dummy();
}
$this->set( 'meta', Loco_gettext_Metadata::create($file, $data) );
// binary MO will be hex-formatted in template
if( 'mo' === $type ){
$this->set('bin', $file->getContents() );
return $this->view('admin/file/view-mo' );
}
// l10n.php files are unlikely to be encountered without a po or mo, but still..
if( 'php'=== $type ){
return $this->view('admin/file/view-php', ['phps'=>$file->getContents()] );
}
// else is a PO or POT file
$this->enqueueScript('poview');//->enqueueScript('min/highlight');
$lines = preg_split('/\\n|\\r\\n?/', $this->getUtf8Source( $file, $data->getHeaders() ) );
$this->set( 'lines', $lines );
// ajax parameters required for pulling reference sources
$this->set('js', new Loco_mvc_ViewParams( [
'popath' => $this->get('path'),
'nonces' => [
'fsReference' => wp_create_nonce('fsReference'),
],
'project' => $bundle ? [
'bundle' => $bundle->getId(),
'domain' => $project ? $project->getId() : '',
] : null,
] ) );
// treat as PO if file name has locale
if( $this->getLocale() ){
return $this->view('admin/file/view-po' );
}
// else view as POT
return $this->view('admin/file/view-pot' );
}
} DiffController.php 0000644 00000013367 15233632214 0010204 0 ustar 00 <?php
/**
* File revisions and rollback
*/
class Loco_admin_file_DiffController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('podiff');
$pofile = $this->get('file');
if( $pofile->exists() && ! $pofile->isDirectory() ){
$path = $pofile->getPath();
$action = 'restore:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( [] );
$fields->setNonce($action);
$this->set( 'hidden', $fields );
// attempt rollback if valid nonce posted back with backup path
if( $this->checkNonce($action) ){
try {
$post = Loco_mvc_PostParams::get();
// Restore
if( $post->has('backup') ){
$path = $post->backup;
$target = new Loco_fs_File( $path );
$target->normalize( loco_constant('WP_CONTENT_DIR') );
// Recompile back to current version. Note that restoring a backup also backs up current file
$data = Loco_gettext_Data::fromSource( $target->getContents() );
$compiler = new Loco_gettext_Compiler($pofile);
$compiler->writeAll( $data, $this->getOptionalProject() );
Loco_error_AdminNotices::success( __('File restored','loco-translate') );
}
// Delete an old backup from revision list
else if( $post->has('delete') ){
$path = $post->delete;
$target = new Loco_fs_File( $path );
$target->normalize( loco_constant('WP_CONTENT_DIR') );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeDelete( $target );
$target->unlink();
Loco_error_AdminNotices::success( __('File deleted','loco-translate') );
}
else {
throw new Loco_error_Exception('Nothing selected');
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
}
}
$bundle = $this->getBundle();
// translators: %s is a file name to be rolled back to a previous version.
$this->set('title', sprintf( __('Restore %s','loco-translate'), $pofile->basename() ).' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$info = Loco_mvc_FileParams::create($file);
$info['mtime'] = $file->modified();
$this->set( 'master', $info );
// translators: Page title where %s refers to a file name
$this->setFileTitle( $file, __('Restore %s','loco-translate') );
$enabled = Loco_data_Settings::get()->num_backups;
$this->set( 'enabled', $enabled );
$files = [];
$wp_content = loco_constant('WP_CONTENT_DIR');
$paths = [ $file->getRelativePath($wp_content) ];
$podate = 'pot' === $file->extension() ? 'POT-Creation-Date' : 'PO-Revision-Date';
$backups = new Loco_fs_Revisions($file);
foreach( $backups->getPaths() as $path ){
$tmp = new Loco_fs_File( $path );
$info = Loco_mvc_FileParams::create($tmp);
// time file was snapshotted is actually the time the next version was updated
// $info['mtime'] = $backups->getTimestamp($path);
// pull "real" update time, meaning when the revision was last updated as current version
try {
$head = Loco_gettext_Data::head($tmp);
$value = $head->trimmed($podate);
if( '' !== $value ){
$info['potime'] = Loco_gettext_Data::parseDate($value);
}
else {
throw new Loco_error_Exception('Backup has no '.$podate.' field');
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
continue;
}
$paths[] = $tmp->getRelativePath($wp_content);
$files[] = $info;
}
// no backups = no restore
if( ! $files ){
return $this->view('admin/errors/no-backups');
}
/*/ warn if current backup settings aren't enough to restore without losing older revisions
$min = count($files) + 1;
if( $enabled < $min ){
$notice = Loco_error_AdminNotices::info('We recommend enabling more backups before restoring');
$notice->addLink( apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/settings#po'), __('Documentation','loco-translate') )
->addLink( Loco_mvc_AdminRouter::generate('config').'#loco--num-backups', __('Settings') );
}*/
// restore permissions required are create and delete on current location
$this->prepareFsConnect( 'update', $this->get('path') );
// prepare revision arguments for JavaScript
$this->set( 'js', new Loco_mvc_ViewParams( [
'paths' => $paths,
'nonces' => [
'diff' => wp_create_nonce('diff'),
]
] ) );
$this->enqueueScript('podiff');
return $this->view('admin/file/diff', compact('files','backups') );
}
}
EditController.php 0000644 00000026416 15233632214 0010220 0 ustar 00 <?php
/**
* PO editor view
*/
class Loco_admin_file_EditController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('editor');
//
$file = $this->get('file');
$bundle = $this->getBundle();
// translators: %1$s is the file name, %2$s is the bundle name
$this->set('title', sprintf( __('Editing %1$s in %2$s','loco-translate'), $file->basename(), $bundle ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return [
__('Overview') => $this->viewSnippet('tab-file-edit'),
];
}
/**
* @param bool $readonly whether po files is in read-only mode
* @return array
*/
private function getNonces( $readonly ){
$nonces = [];
foreach( $readonly ? ['fsReference'] : ['sync','save','fsReference','apis'] as $name ){
$nonces[$name] = wp_create_nonce($name);
}
return $nonces;
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
/* @var Loco_fs_File $file */
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
// editor will be rendered
$this->enqueueScript('editor');
// Parse file data into JavaScript for editor
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load($file);
}
catch( Exception $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
$data = Loco_gettext_Data::dummy();
}
$head = $data->getHeaders();
// default is to permit editing of any file
$readonly = false;
// All files must belong to a bundle.
$bundle = $this->getBundle();
// Establish if file belongs to a configured project
try {
$project = $this->getProject();
}
// Fine if not, this just means sync isn't possible.
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
Loco_error_AdminNotices::debug("Sync is disabled because this file doesn't relate to a known set of translations");
$project = null;
}
// Establish PO/POT edit mode
$potfile = null;
$syncmode = null;
$locale = $this->getLocale();
if( $locale instanceof Loco_Locale ){
// alternative POT file may be forced by sync options
$sync = new Loco_gettext_SyncOptions($head);
$syncmode = $sync->getSyncMode();
if( $sync->hasTemplate() ){
$potfile = $sync->getTemplate();
$potfile->normalize( $bundle->getDirectoryPath() );
}
// else use project-configured template, assuming there is one
// no way to get configured POT if invalid project
else if( $project ){
$potfile = $project->getPot();
// Handle situation where project defines a localised file as the official template
if( $potfile && $potfile->equal($file) ){
$locale = null;
$potfile = null;
}
}
if( $potfile ){
// Validate template file as long as it exists
if( $potfile->exists() ){
try {
$potdata = Loco_gettext_Data::load($potfile);
// If template is pulling JSON files, we must merge them in before msgid comparison
if( $project && $sync->mergeJson() ){
$siblings = new Loco_fs_Siblings($potfile);
$jsons = $siblings->getJsons( $project->getDomain()->getName() );
if( $jsons ){
// using matcher because regular iterator isn't indexed, and additions must be unique
$merged = new Loco_gettext_Matcher($project);
$merged->loadRefs($potdata);
$merged->loadJsons($jsons);
$potdata = $merged->exportPo();
unset($matcher);
}
}
if( ! $potdata->equalSource($data) ){
// translators: %s refers to the name of a POT file
Loco_error_AdminNotices::info( sprintf( __("Translations don't match template. Run sync to update from %s",'loco-translate'), $potfile->basename() ) )
->addLink( apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/sync'), __('Documentation','loco-translate') );
}
unset($potdata);
}
catch( Exception $e ){
// translators: Where %s is the name of the invalid POT file
Loco_error_AdminNotices::warn( sprintf( __('Translation template is invalid (%s)','loco-translate'), $potfile->basename() ) );
$potfile = null;
}
}
// else template doesn't exist, so sync will be done to source code
else {
// Loco_error_AdminNotices::debug( sprintf( __('Template file not found (%s)','loco-translate'), $potfile->basename() ) );
$potfile = null;
}
}
if( $locale ){
// allow PO file to dictate its own Plural-Forms
try {
$locale->setPluralFormsHeader( $head['Plural-Forms'] );
}
catch( InvalidArgumentException $e ){
// ignore invalid Plural-Forms
}
// fill in missing PO headers now locale is fully resolved
$data->localize($locale);
// If MO file will be compiled, check for library/config problems
if ( 2 !== strlen( "\xC2\xA3" ) ) {
Loco_error_AdminNotices::warn('Your mbstring configuration will result in corrupt MO files. Please ensure mbstring.func_overload is disabled');
}
}
}
// WordPress source locale is always en_US, but filter allows override for purpose of sending to translation APIs.
$tag = apply_filters('loco_api_provider_source', 'en', $file->getPath() );
$source = Loco_Locale::parse($tag);
$settings = Loco_data_Settings::get();
if( is_null($locale) ){
// notify if template is locked (save and sync will be disabled)
if( $project && $project->isPotLocked() ){
$this->set('fsDenied', true );
$readonly = true;
}
// translators: Warning when POT file is opened in the file editor. It can be disabled in settings.
else if( 1 === $settings->pot_protect ){
$e = new Loco_error_Warning( __("This is NOT a translation file. Manual editing of source strings is not recommended.",'loco-translate') );
$e->addLink( Loco_mvc_AdminRouter::generate('config').'#loco--pot-protect', __('Settings','loco-translate') )
->addLink( apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/templates'), __('Documentation','loco-translate') )
->noLog();
Loco_error_AdminNotices::add($e);
}
}
// back end expects paths relative to wp-content
$wp_content = loco_constant('WP_CONTENT_DIR');
$this->set( 'js', new Loco_mvc_ViewParams( [
'podata' => $data->jsonSerialize(),
'powrap' => (int) $settings->po_width,
'multipart' => (bool) $settings->ajax_files,
'locale' => $locale ? $locale->jsonSerialize() : null,
'source' => $source->jsonSerialize(),
'potpath' => $locale && $potfile ? $potfile->getRelativePath($wp_content) : null,
'syncmode' => $syncmode,
'popath' => $this->get('path'),
'readonly' => $readonly,
'project' => $project ? [
'bundle' => $bundle->getId(),
'domain' => $project->getId(),
] : null,
'nonces' => $this->getNonces($readonly),
'adminUrl' => Loco_mvc_AdminRouter::generate('loco'),
] ) );
$this->set( 'ui', new Loco_mvc_ViewParams( [
// Translators: button for adding a new string when manually editing a POT file
'add' => _x('Add','Editor','loco-translate'),
// Translators: button for removing a string when manually editing a POT file
'del' => _x('Remove','Editor','loco-translate'),
'help' => __('Help','loco-translate'),
// Translators: Button that saves translations to disk
'save' => _x('Save','Editor','loco-translate'),
// Translators: Button that runs in-editor sync/operation
'sync' => _x('Sync','Editor','loco-translate'),
// Translators: Button that reloads current screen
'revert' => _x('Revert','Editor','loco-translate'),
// Translators: Button that opens window for auto-translating
'auto' => _x('Auto','Editor','loco-translate'),
// Translators: Button that validates current translation formatting
'lint' => _x('Check','Editor','loco-translate'),
// Translators: Button for downloading a PO, MO or POT file
'download' => _x('Download','Editor','loco-translate'),
// Translators: Placeholder text for text filter above editor
'filter' => __('Filter translations','loco-translate'),
// Translators: Button that toggles invisible characters
'invs' => _x('Toggle invisibles','Editor','loco-translate'),
// Translators: Button that toggles between "code" and regular text editing modes
'code' => _x('Toggle code view','Editor','loco-translate'),
] ) );
// Download form params
$hidden = new Loco_mvc_HiddenFields( [
'route' => 'download',
'action' => 'loco_download',
'path' => '',
'source' => '',
] );
// zip archive will on;y be available if bundle is configured
if( $bundle && $project ){
$hidden['bundle'] = $bundle->getId();
$hidden['domain'] = $project->getId();
}
$this->set( 'dlFields', $hidden->setNonce('download') );
$this->set( 'dlAction', admin_url('admin-ajax.php','relative') );
// Remote file system required if file is not directly writable
$this->prepareFsConnect( 'update', $this->get('path') );
// ok to render editor as either po or pot
$tpl = $locale ? 'po' : 'pot';
$this->setFileTitle($file);
return $this->view( 'admin/file/edit-'.$tpl, [] );
}
} BaseController.php 0000644 00000013247 15233632214 0010203 0 ustar 00 <?php
/**
* Base class for a file resource belonging to a bundle
* Root > List > Bundle > Resource
*/
abstract class Loco_admin_file_BaseController extends Loco_admin_bundle_BaseController {
/**
* @var Loco_Locale
*/
private $locale;
/**
* @return Loco_Locale
*/
protected function getLocale(){
return $this->locale;
}
/**
* Check file is valid or return error
* @return string rendered error
*/
protected function getFileError( Loco_fs_File $file = null ){
// file must exist for editing
if( is_null($file) || ! $file->exists() ){
return $this->view( 'admin/errors/file-missing', [] );
}
if( $file->isDirectory() ){
$this->set('info', Loco_mvc_FileParams::create($file) );
return $this->view( 'admin/errors/file-isdir', [] );
}
// security validations
try {
Loco_gettext_Data::ext( $file );
}
catch( Exception $e ){
return $this->view( 'admin/errors/file-sec', [ 'reason' => $e->getMessage() ] );
}
return '';
}
/**
* Set template title argument for a file
* @return void
*/
protected function setFileTitle( Loco_fs_File $file, $format = '%s' ){
$name = Loco_mvc_ViewParams::format($format,[$file->basename()]);
// append folder location for translation files
if( in_array( $file->extension(), ['po','mo'] ) ){
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
$type = $dir->getTypeLabel( $dir->getTypeId() );
$name .= ' ('.mb_strtolower($type,'UTF-8').')';
}
$this->set('title', $name );
}
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// views at this level are always related to a file
// file is permitted to be missing during this execution.
$path = $this->get('path');
if( ! $path ){
throw new Loco_error_Exception('path argument required');
}
$file = new Loco_fs_LocaleFile( $path );
$file->normalize( loco_constant('WP_CONTENT_DIR') );
$ext = strtolower( $file->extension() );
// POT file has no locale
if( 'pot' === $ext ){
$locale = null;
$localised = false;
}
// else file may have a locale suffix (unless invalid, such as "default.po")
else {
$locale = $file->getLocale();
$localised = $locale->isValid();
}
if( $localised ){
$this->locale = $locale;
$code = (string) $locale;
$this->set( 'locale', new Loco_mvc_ViewParams( [
'code' => $code,
'lang' => $locale->lang,
'icon' => $locale->getIcon(),
'name' => $locale->ensureName( new Loco_api_WordPressTranslations ),
'href' => Loco_mvc_AdminRouter::generate('lang-view', ['locale'=>$code] ),
] ) );
}
else {
$this->set( 'locale', null );
}
$this->set('file', $file );
$this->set('filetype', strtoupper($ext) );
$this->set('title', $file->basename() );
// navigate up to root from this bundle sub view
$bundle = $this->getBundle();
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between sub view siblings for this resource
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = [
'file-edit' => __('Editor','loco-translate'),
'file-view' => __('Source','loco-translate'),
'file-info' => __('File info','loco-translate'),
'file-diff' => __('Restore','loco-translate'),
'file-move' => $localised ? __('Relocate','loco-translate') : null,
'file-delete' => __('Delete','loco-translate'),
];
$suffix = $this->get('action');
$prefix = $this->get('type');
$args = array_intersect_key($_GET,['path'=>1,'bundle'=>1,'domain'=>1]);
foreach( $actions as $action => $name ){
if( is_string($name) ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $args );
$tabs->add( $name, $href, $action === $suffix );
}
}
// Provide common language creation link if project scope is valid
$project = $this->getOptionalProject();
if( $project ){
$args = [ 'bundle' => $bundle->getHandle(), 'domain' => $project->getId() ];
$this->set( 'msginit', new Loco_mvc_ViewParams( [
'href' => Loco_mvc_AdminRouter::generate( $prefix.'-msginit', $args ),
'text' => __('New language','loco-translate'),
] ) );
}
}
/**
* {@inheritdoc}
*/
public function view( $tpl, array $args = [] ){
if( $breadcrumb = $this->get('breadcrumb') ){
// Add project name into breadcrumb if not the same as bundle name
try {
$project = $this->getProject();
if( $project->getName() !== $this->getBundle()->getName() ){
$breadcrumb->add( $project->getName() );
}
}
catch( Loco_error_Exception $e ){
// ignore missing project in breadcrumb
}
// Always add page title as final breadcrumb element
$breadcrumb->add( $this->get('title')?:'Untitled' );
}
return parent::view( $tpl, $args );
}
}