Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)始,故人唐宰相鲁公,🆚开府南服,余以布衣从戎。明年,别公漳水湄。后明年,公以事过张睢阳庙及颜杲卿所尝往来处,悲歌慷慨,卒不负其言而从之游。今其诗具在,可考也。😭 HEX
HEX
Server: Apache
System: Linux webm006.cluster120.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
User: studionolh (122383)
PHP: 7.3.33
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/s/t/u/studionolh/www/wp-includes/Requests/Cookie/reusable-blocks/file.tar
block.json000064400000002127152331531560006537 0ustar00{
	"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.css000064400000004043152331531560006423 0ustar00/**
 * 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.css000064400000001172152331531560007333 0ustar00.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.css000064400000001221152331531560007200 0ustar00.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.css000064400000004010152331531560006543 0ustar00/**
 * 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.css000064400000004023152331531560007220 0ustar00/**
 * 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.css000064400000004011152331531560007343 0ustar00/**
 * 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.css000064400000001222152331531560010000 0ustar00.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.css000064400000001173152331531560010133 0ustar00.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.php000064400000021603152336322140010217 0ustar00<?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().' &lsaquo; '.$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.php000064400000022075152336322140010236 0ustar00<?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() ).' &lsaquo; '.$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.php000064400000010721152336322140010525 0ustar00<?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() ).' &lsaquo; '.$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.php000064400000015323152336322140010167 0ustar00<?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().' &lsaquo; '.$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.php000064400000007225152336322140010242 0ustar00<?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().' &lsaquo; '.$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.php000064400000013367152336322140010204 0ustar00<?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() ).' &lsaquo; '.$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.php000064400000026416152336322140010220 0ustar00<?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.php000064400000013247152336322140010203 0ustar00<?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 );
    }


}