PK T\>N
N
js/Strings.phpnu W+A flush('loco');
}
}
PK T\G mvc/PostParams.phpnu W+A getArrayCopy(), false, '&' );
foreach( explode('&',$query) as $str ){
$serial[] = array_map( 'urldecode', explode( '=', $str, 2 ) );
}
return $serial;
}
}PK T\*MTk k mvc/AjaxRouter.phpnu W+A $route,
'action' => 'loco_ajax',
'loco-nonce' => wp_create_nonce($route),
);
return admin_url('admin-ajax.php','relative').'?'.http_build_query($args,null,'&');
}
/**
* Create a new ajax router and starts buffering output immediately
*/
public function __construct(){
$this->buffer = Loco_output_Buffer::start();
parent::__construct();
}
/**
* "init" action callback.
* early-ish hook that ensures controllers can initialize
*/
public function on_init(){
try {
$class = self::routeToClass( $_REQUEST['route'] );
// autoloader will throw error if controller class doesn't exist
$this->ctrl = new $class;
$this->ctrl->_init( $_REQUEST );
//
do_action('loco_controller_init', $this->ctrl );
}
catch( Loco_error_Exception $e ){
$this->ctrl = null;
// throw $e; // <- debug
}
}
/**
* @return string
*/
private static function routeToClass( $route ){
$route = explode( '-', $route );
// convert route to class name, e.g. "foo-bar" => "Loco_ajax_foo_BarController"
$key = count($route) - 1;
$route[$key] = ucfirst( $route[$key] );
return 'Loco_ajax_'.implode('_',$route).'Controller';
}
/**
* Common ajax hook for all Loco admin JSON requests
* @codeCoverageIgnore
*/
public function on_wp_ajax_loco_json(){
$json = $this->renderAjax();
// avoid outputing junk in JSON stream
Loco_output_Buffer::clear();
Loco_output_Buffer::check();
// output stream is clear, we can flush JSON
header('Content-Length: '.strlen($json), true );
header('Content-Type: application/json; charset=UTF-8', true );
// avoid hijacking of exit via wp_die_ajax_handler. Tests call renderAjax directly.
echo $json;
exit(0);
}
/**
* Additional ajax hook for download actions that won't be JSON
* @codeCoverageIgnore
*/
public function on_wp_ajax_loco_download(){
$data = $this->renderDownload();
if( is_string($data) ){
$path = ( $this->ctrl ? $this->ctrl->get('path') : '' ) or $path = 'error.json';
$file = new Loco_fs_File( $path );
$ext = $file->extension();
}
else if( $data instanceof Exception ){
$data = sprintf('%s in %s:%u', $data->getMessage(), basename($data->getFile()), $data->getLine() );
$ext = null;
}
else {
$data = (string) $data;
$ext = null;
}
// set content type header appropriate for supported file extensions
if( ! headers_sent() ){
$mimes = array(
'mo' => 'application/x-gettext-translation',
'po' => 'application/x-gettext',
'pot' => 'application/x-gettext',
'xml' => 'text/xml',
'json' => 'application/json',
);
if( $ext && isset($mimes[$ext]) ){
header('Content-Type: '.$mimes[$ext].'; charset=UTF-8', true );
header('Content-Disposition: attachment; filename='.$file->basename(), true );
}
else {
header('Content-Type: text/plain; charset=UTF-8', true );
}
header('Content-Length: '.strlen($data), true );
}
// avoid hijacking of exit via wp_die_ajax_handler. Tests call renderDownload directly.
echo $data;
exit(0);
}
/**
* Execute ajax controller to render JSON response body
* @return string
*/
public function renderAjax(){
try {
// respond with deferred failure from initAjax
if( ! $this->ctrl ){
$route = isset($_REQUEST['route']) ? $_REQUEST['route'] : '';
throw new Loco_error_Exception( sprintf( __('Ajax route not found: "%s"','loco'), $route ) );
}
// else execute controller to get json output
$json = $this->ctrl->render();
if( is_null($json) || ! isset($json{0}) ){
throw new Loco_error_Exception( __('Ajax controller returned empty JSON','loco') );
}
}
catch( Loco_error_Exception $e ){
$json = json_encode( array( 'error' => $e->jsonSerialize(), 'notices' => Loco_error_AdminNotices::destroyAjax() ) );
}
catch( Exception $e ){
$e = new Loco_error_Exception( $e->getMessage(), $e->getCode() );
$json = json_encode( array( 'error' => $e->jsonSerialize(), 'notices' => Loco_error_AdminNotices::destroyAjax() ) );
}
if( $this->buffer ){
$this->buffer->close();
$this->buffer = null;
}
return $json;
}
/**
* Execute ajax controller to render something other than JSON
* @return string|Exception
*/
public function renderDownload(){
try {
// respond with deferred failure from initAjax
if( ! $this->ctrl ){
throw new Loco_error_Exception( __('Download action not found','loco') );
}
// else execute controller to get raw output
$data = $this->ctrl->render();
if( is_null($data) || ! isset($data{0}) ){
throw new Loco_error_Exception( __('Download controller returned empty output','loco') );
}
}
catch( Exception $e ){
$data = $e;
}
if( $this->buffer ){
$this->buffer->close();
$this->buffer = null;
}
return $data;
}
}PK T\30& mvc/AdminController.phpnu W+A bench = microtime( true );
}
$this->view = new Loco_mvc_View( $args );
$this->auth();
// check essential extensions on all pages so admin notices are shown
foreach( array('json','mbstring') as $ext ){
loco_check_extension($ext);
}
// add contextual help tabs to current screen if there are any
if( $screen = get_current_screen() ){
$this->view->cd('/admin/help');
$tabs = $this->getHelpTabs();
// always append common help tabs
$tabs[ __('Help & support','loco') ] = $this->view->render('tab-support');
// set all tabs and common side bar
$i = 0;
foreach( $tabs as $title => $content ){
$id = sprintf('loco-help-%u', $i++ );
$screen->add_help_tab( compact('id','title','content') );
}
$screen->set_help_sidebar( $this->view->render('side-bar') );
$this->view->cd('/');
}
// helper properties for loading static resources
$this->baseurl = plugins_url( '', loco_plugin_self() );
// add common admin page resources
$this->enqueueStyle('admin', array('wp-jquery-ui-dialog') );
// load colour scheme is user has non-default
$skin = get_user_option('admin_color');
if( $skin && 'fresh' !== $skin ){
$this->enqueueStyle( 'skins/'.$skin );
}
// core minimized admin.js loaded on all pages before any other Loco scripts
$this->enqueueScript('min/admin', array('jquery-ui-dialog') );
$this->init();
return $this;
}
/**
* Post-construct initializer that may be overridden by child classes
* @return void
*/
public function init(){
}
/**
* "admin_title" filter, modifies HTML document title if we've set one
*/
public function filter_admin_title( $admin_title, $title ){
if( $view_title = $this->get('title') ){
$admin_title = $view_title.' ‹ '.$admin_title;
}
return $admin_title;
}
/**
* "admin_footer_text" filter, modifies admin footer only on Loco pages
*/
public function filter_admin_footer_text(){
$url = apply_filters('loco_external', 'https://localise.biz/');
return ''.sprintf( '%s Loco', esc_html(__('Loco Translate is powered by','loco')), esc_url($url) ).'';
}
/**
* "update_footer" filter, prints Loco version number in admin footer
*/
public function filter_update_footer( $text ){
$html = sprintf( 'v%s', loco_plugin_version() );
if( $this->bench ){
$info = $this->get('debug');
$html .= sprintf('%sms', number_format($info->time,2) );
}
return $html;
}
/**
* "loco_external" filter callback, campaignizes external links
*/
public function filter_loco_external( $url ){
static $query;
if( ! isset($query) ){
$query = http_build_query( array( 'utm_campaign' => 'wp', 'utm_source' => 'admin', 'utm_content' => $this->get('_route') ), null, '&' );
}
$u = parse_url( $url );
if( isset($u['host']) && 'localise.biz' === $u['host'] ){
$url = 'https://localise.biz'.$u['path'];
if( isset($u['query']) ){
$url .= '?'. $u['query'].'&'.$query;
}
else {
$url .= '?'.$query;
}
if( isset($u['fragment']) ){
$url .= '#'.$u['fragment'];
}
}
return $url;
}
/**
* All admin screens must define help tabs, eve if they return empty
* @return array
*/
public function getHelpTabs(){
return array();
}
/**
* {@inheritdoc}
*/
public function get( $prop ){
return $this->view->__get($prop);
}
/**
* {@inheritdoc}
*/
public function set( $prop, $value ){
$this->view->set( $prop, $value );
return $this;
}
/**
* Render template for echoing into admin screen
* @return string
*/
public function view( $tpl, array $args = array() ){
$view = $this->view;
foreach( $args as $prop => $value ){
$view->set( $prop, $value );
}
// ensure JavaScript config always present
if( $jsConf = $view->js ){
if( ! $jsConf instanceof Loco_mvc_ViewParams ){
throw new InvalidArgumentException('Bad "js" view parameter');
}
}
else {
$jsConf = new Loco_mvc_ViewParams;
$view->set( 'js', $jsConf );
}
// localize script if translations in memory
if( is_textdomain_loaded('loco') ){
$strings = new Loco_js_Strings;
$jsConf['wpl10n'] = $strings->compile();
$strings->unhook();
unset( $strings );
// add currently loaded locale for passing plural equation into js.
// note that plural rules come from our data, because MO is not trusted.
$tag = apply_filters( 'plugin_locale', get_locale(), 'loco' );
$jsConf['wplang'] = Loco_Locale::parse($tag);
}
// take benchmark for debugger to be rendered in footer
if( $this->bench ){
$this->set('debug', new Loco_mvc_ViewParams( array(
'time' => microtime(true) - $this->bench,
) ) );
}
return $view->render( $tpl );
}
/**
* Add CSS to head
* @return Loco_mvc_Controller
*/
public function enqueueStyle( $name, array $deps = array() ){
$href = $this->baseurl.'/pub/css/'.$name.'.css';
$vers = apply_filters( 'loco_static_version', loco_plugin_version(), $href );
wp_enqueue_style( 'loco-'.strtr($name,'/','-'), $href, $deps, $vers, 'all' );
return $this;
}
/**
* Add JavaScript to footer
* @return Loco_mvc_Controller
*/
public function enqueueScript( $name, array $deps = array() ){
$href = $this->baseurl.'/pub/js/'.$name.'.js';
$vers = apply_filters( 'loco_static_version', loco_plugin_version(), $href );
wp_enqueue_script( 'loco-js-'.strtr($name,'/','-'), $href, $deps, $vers, true );
return $this;
}
}PK T\ mvc/Controller.phpnu W+A exitForbidden();
}
/**
* Emulate permission denied screen as performed in wp-admin/admin.php
*/
protected function exitForbidden(){
do_action( 'admin_page_access_denied' );
wp_die( __( 'You do not have sufficient permissions to access this page.','default' ), 403 );
} // @codeCoverageIgnore
/**
* Set a nonce for the current page for when it submits a form
* @return Loco_mvc_ViewParams
*/
public function setNonce( $action ){
$name = 'loco-nonce';
$value = wp_create_nonce( $action );
$nonce = new Loco_mvc_ViewParams( compact('name','value','action') );
$this->set('nonce', $nonce );
return $nonce;
}
/**
* Check if a valid nonce has been sent in current request.
* Fails if nonce is invalid, but returns false if not sent so scripts can exit accordingly.
* @throws Loco_error_Exception
* @return bool true if data has been posted and nonce is valid
*/
public function checkNonce( $action ){
$posted = false;
$name = 'loco-nonce';
if( isset($_REQUEST[$name]) ){
$value = $_REQUEST[$name];
if( wp_verify_nonce( $value, $action ) ){
$posted = true;
}
else {
throw new Loco_error_Exception('Failed security check for '.$name);
}
}
return $posted;
}
}
PK T\!( ( mvc/AdminRouter.phpnu W+A ID );
// Ensure Loco permissions are set up for the first time, or nobody will have access at all
if( ! get_role('translator') || ( $super && ! is_multisite() && ! $user->has_cap($cap) ) ){
Loco_data_Permissions::init();
$user->get_role_caps(); // <- rebuild
}
// rendering hook for all menu items
$render = array( $this, 'renderPage' );
// main loco pages, hooking only if has permission
if( $user->has_cap($cap) ){
$label = __('Loco Translate','loco');
// translators: Page title for plugin home screen
$title = __('Loco, Translation Management','loco');
add_menu_page( $title, $label, $cap, 'loco', $render, 'dashicons-translation' );
// alternative label for first menu item which gets repeated from top level
add_submenu_page( 'loco', $title, __('Home','loco'), $cap, 'loco', $render );
$label = __('Themes','loco');
// translators: Page title for theme translations
$title = __('Theme translations ‹ Loco','loco');
add_submenu_page( 'loco', $title, $label, $cap, 'loco-theme', $render );
$label = __('Plugins', 'loco');
// translators: Page title for plugin translations
$title = __('Plugin translations ‹ Loco','loco');
add_submenu_page( 'loco', $title, $label, $cap, 'loco-plugin', $render );
$label = __('WordPress', 'loco');
// translators: Page title for core WordPress translations
$title = __('Core translations ‹ Loco', 'loco');
add_submenu_page( 'loco', $title, $label, $cap, 'loco-core', $render );
// settings page only for users with manage_options permission in addition to Loco access:
if( $user->has_cap('manage_options') ){
$title = __('Plugin settings','loco');
add_submenu_page( 'loco', $title, __('Settings','loco'), 'manage_options', 'loco-config', $render );
}
// but all users need access to user preferences which require standard Loco access permission
else {
$title = __('User options','loco');
add_submenu_page( 'loco', $title, __('Settings','loco'), $cap, 'loco-config-user', $render );
}
}
// legacy link redirect from previous v1.x slug
if( isset($_GET['page']) && 'loco-translate' === $_GET['page'] ){
if( wp_redirect( self::generate('') ) ){
exit(0); // <- required to avoid page permissions being checked
}
}
}
/**
* Early hook as soon as we know what screen will be rendered
*/
public function on_current_screen( WP_Screen $screen ){
$action = isset($_GET['action']) ? $_GET['action'] : null;
$this->initPage( $screen, $action );
}
/**
* Instantiate admin page controller from current screen.
* This is called early (before renderPage) so controller can listen on other hooks.
*
* @return Loco_mvc_AdminController
*/
public function initPage( WP_Screen $screen, $action = '' ){
$class = null;
$args = array ();
// suppress error display when establishing Loco page
$page = self::screenToPage($screen);
if( is_string($page) ){
$class = self::pageToClass( $page, $action, $args );
}
if( is_null($class) ){
$this->ctrl = null;
return;
}
// class should exist, so throw fatal if it doesn't
$this->ctrl = new $class;
if( ! $this->ctrl instanceof Loco_mvc_AdminController ){
throw new Exception( $class.' must inherit Loco_mvc_AdminController');
}
// transfer flash messages from session to admin notice buffer
try {
$session = Loco_data_Session::get();
while( $message = $session->flash('success') ){
Loco_error_AdminNotices::success( $message );
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// buffer errors during controller setup
try {
$this->ctrl->_init( $_GET + $args );
do_action('loco_admin_init', $this->ctrl );
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
return $this->ctrl;
}
/**
* Convert WordPress internal WPScreen $id into route prefix for an admin page controller
* @return array
*/
private static function screenToPage( WP_Screen $screen ){
// Hooked menu slug is either "toplevel_page_loco" or "{title}_page_loco-{page}"
// Sanitized {title} prefix is not reliable as it may be localized. instead just checking for "_page_loco"
// TODO is there a safer WordPress way to resolve this?
$id = $screen->id;
$start = strpos($id,'_page_loco');
// not one of our pages if token not found
if( is_int($start) ){
$page = substr( $id, $start+11 ) or $page = '';
return $page;
}
}
/**
* Get unvalidated controller class for given route parameters
* Abstracted from initPage so we can validate routes in self::generate
* @return string
*/
private static function pageToClass( $page, $action, array &$args ){
$routes = array (
'' => 'Root',
'debug' => 'Debug',
// site-wide plugin configurations
'config' => 'config_Settings',
'config-user' => 'config_Prefs',
'config-version' => 'config_Version',
// bundle type listings
'theme' => 'list_Themes',
'plugin' => 'list_Plugins',
'core' => 'list_Core',
// bundle level views
'{type}-view' => 'bundle_View',
'{type}-conf' => 'bundle_Conf',
'{type}-setup' => 'bundle_Setup',
'{type}-debug' => 'bundle_Debug',
// file initialization
'{type}-msginit' => 'init_InitPo',
'{type}-xgettext' => 'init_InitPot',
// file resource views
'{type}-file-view' => 'file_View',
'{type}-file-edit' => 'file_Edit',
'{type}-file-info' => 'file_Info',
'{type}-file-delete' => 'file_Delete',
);
if( ! $page ){
$page = $action;
}
else if( $action ){
$page .= '-'. $action;
}
$args['_route'] = $page;
// tokenize path arguments
if( preg_match('/^(plugin|theme|core)-/', $page, $r ) ){
$args['type'] = $r[1];
$page = substr_replace( $page, '{type}', 0, strlen($r[1]) );
}
if( isset($routes[$page]) ){
return 'Loco_admin_'.$routes[$page].'Controller';
}
// debug routing failures:
// throw new Exception( sprintf('Failed to get page class from $page=%s',$page) );
}
/**
* Main entry point for admin menu callback, establishes page and hands off to controller
*/
public function renderPage(){
try {
// show deferred failure from initPage
if( ! $this->ctrl ){
throw new Loco_error_Exception( __('Page not found','loco') );
}
// display loco admin page
echo $this->ctrl->render();
}
catch( Exception $e ){
$ctrl = new Loco_admin_ErrorController;
$ctrl->_init( array() );
echo $ctrl->renderError($e);
}
// ensure session always shutdown cleanly after render
Loco_data_Session::close();
}
/**
* Generate a routable link to Loco admin page
* @return string
*/
public static function generate( $route, array $args = array() ){
$url = null;
$page = null;
$action = null;
// empty action targets plugin root
if( ! $route ){
$route = 'loco';
}
// support direct usage of page hooks
if( $url = menu_page_url( $route, false ) ){
$page = $route;
}
// else split action into admin page (e.g. "loco-themes") and sub-action (e.g. "view-theme")
else {
$page = 'loco';
$path = explode( '-', $route );
if( $sub = array_shift($path) ){
$page .= '-'.$sub;
if( $path ){
$action = implode('-',$path);
}
}
}
// sanitize extended route in debug mode only. useful in tests
if( loco_debugging() ){
$tmp = array();
$class = self::pageToClass( substr($page,5), $action, $tmp );
if( ! $class || ! class_exists($class) ){
throw new InvalidArgumentException( sprintf('Invalid admin route: %s => %s', json_encode($route), json_encode($class) ) );
}
}
// if url found, it should contain the page
if( $url ){
unset( $args['page'] );
}
// else start with base URL
else {
$url = admin_url('admin.php');
$args['page'] = $page;
}
// add action if found
if( $action ){
$args['action'] = $action;
}
// else ensure not set in args, as it's reserved
else {
unset( $args['action'] );
}
// append all arguments to base URL
if( $query = http_build_query($args,null,'&') ){
$sep = false === strpos($url, '?') ? '?' : '&';
$url .= $sep.$query;
}
return $url;
}
}PK T\d)' mvc/ViewParams.phpnu W+A __get($p);
if( 1 < func_num_args() ){
$args = func_get_args();
$text = call_user_func_array( 'sprintf', $args );
}
echo $this->escape( $text );
return '';
}
/**
* Print property as string date, including time
*/
public function date( $p, $f = null ){
if( $u = $this->__get($p) ){
$s = self::date_i18n( $u, $f );
}
else {
$s = '';
}
echo $this->escape($s);
return '';
}
/**
* Print property as a string-formatted number
*/
public function n( $p, $dp = null ){
// number_format_i18n is pre-escaped for HTML
echo number_format_i18n( $this->__get($p), $dp );
return '';
}
/**
* Format property with passed formatting string
*/
public function f( $p, $f = '%s' ){
echo $this->escape( sprintf( $f, $this->__get($p) ) );
return '';
}
/**
* @return array
*/
public function jsonSerialize(){
return $this->getArrayCopy();
}
/**
* Fetch whole object as JSON
* @return string
*/
public function exportJson(){
return json_encode( $this->jsonSerialize() );
}
/**
* @return Loco_mvc_ViewParams
*/
public function concat( ArrayObject $more ){
foreach( $more as $name => $value ){
$this[$name] = $value;
}
return $this;
}
/**
* Debugging function
* @codeCoverageIgnore
*/
public function dump(){
echo '
',$this->escape( json_encode( $this->getArrayCopy(),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE ) ),'
';
}
// The following are all aliases for WordPress output functions in formatting.php
/*public function html( $p ){
return esc_html( $this->__get($p) );
}*/
/*public function attr( $p ){
return esc_attr( $this->__get($p) );
}*/
}PK T\a a mvc/FileParams.phpnu W+A = 1024 ){
$i++;
$dp++;
$n /= 1024;
}
$s = number_format( $n, $dp, '.', ',' );
// trim trailing zeros from decimal places
$a = explode('.',$s);
if( isset($a[1]) ){
$s = $a[0];
$d = trim($a[1],'0') and $s .= '.'.$d;
}
$units = array( ' bytes', ' KB', ' MB', ' GB', ' TB' );
$s .= $units[$i];
return $s;
}
/**
* @return Loco_mvc_FileParams
*/
public static function create( Loco_fs_File $file ) {
return new Loco_mvc_FileParams( array(), $file );
}
/**
* Override does lazy property initialization
*/
public function __construct( array $props = array(), Loco_fs_File $file ){
parent::__construct( array (
'name' => '',
'path' => '',
'relpath' => '',
'reltime' => '',
'bytes' => 0,
'size' => '',
'imode' => '',
'smode' => '',
'owner' => '',
'group' => '',
) + $props );
$this->file = $file;
}
/**
* {@inheritdoc}
* Override to get live information from file object
*/
public function offsetGet( $prop ){
$getter = array( $this, '_get_'.$prop );
if( is_callable($getter) ){
return call_user_func( $getter );
}
return parent::offsetGet($prop);
}
/**
* {@inheritdoc}
* Override to ensure all properties populated
*/
public function getArrayCopy(){
$a = array();
foreach( $this as $prop => $dflt ){
$a[$prop] = $this[$prop];
}
return $a;
}
/**
* @return string
*/
private function _get_name(){
return $this->file->basename();
}
/**
* @return string
*/
private function _get_path(){
return $this->file->getPath();
}
/**
* @return string
*/
private function _get_relpath(){
$base = loco_constant('WP_CONTENT_DIR');
return $this->file->getRelativePath($base);
}
/**
* Using slightly modified version of WordPress's Human time differencing
* + Added "Just now" when in the last 30 seconds
* TODO possibly replace with custom function that includes "Yesterday" etc..
*/
private function _get_reltime(){
$time = $this->file->modified();
$time_diff = time() - $time;
// use same time format as posts listing when in future or more than a day ago
if( $time_diff < 0 || $time_diff >= 86400 ){
return date_i18n( __('Y/m/d','default'), $time );
}
if( $time_diff < 30 ){
// translators: relative time when something happened in the last 30 seconds
return __('Just now','loco');
}
return sprintf( __('%s ago','default'), human_time_diff($time) );
}
/**
* @return int
*/
private function _get_bytes(){
return $this->file->size();
}
/**
* @return string
*/
private function _get_size(){
return self::renderBytes( $this->_get_bytes() );
}
/**
* Get octal file mode
* @return string
*/
private function _get_imode(){
$mode = new Loco_fs_FileMode( $this->file->mode() );
return (string) $mode;
}
/**
* Get rwx file mode
* @return string
*/
private function _get_smode(){
$mode = new Loco_fs_FileMode( $this->file->mode() );
return $mode->format();
}
/**
* Get file owner name
* @return string
*/
private function _get_owner(){
if( ( $uid = $this->file->uid() ) && function_exists('posix_getpwuid') && ( $a = posix_getpwuid($uid) ) ){
return $a['name'];
}
return sprintf('%u',$uid);
}
/**
* Get group owner name
* @return string
*/
private function _get_group(){
if( ( $gid = $this->file->gid() ) && function_exists('posix_getpwuid') && ( $a = posix_getgrgid($gid) ) ){
return $a['name'];
}
return sprintf('%u',$gid);
}
/**
* Print pseudo console line
*/
public function ls(){
$this->e('smode');
echo ' ';
$this->e('owner');
echo ':';
$this->e('group');
echo ' ';
$this->e('relpath');
return '';
}
}PK T\e` ` mvc/View.phpnu W+A scope = new Loco_mvc_ViewParams( $args );
$this->cwd = loco_plugin_root().'/tpl';
}
/**
* Change base path for template paths
* @param string path relative to current directory
* @return Loco_mvc_View
*/
public function cd( $path ){
if( $path && '/' === $path{0} ){
$this->cwd = rtrim( loco_plugin_root().'/tpl'.$path, '/' );
}
else {
$this->cwd = rtrim( $this->cwd.'/'.$path );
}
return $this;
}
/**
* @internal
* Clean up if something ubruptly stopped rendering before graceful end
*/
public function __destruct(){
if( $this->block ){
ob_end_clean();
}
}
/**
* Render error screen HTML
* @return string
*/
public static function renderError( Loco_error_Exception $e ){
$view = new Loco_mvc_View;
try {
$view->set( 'error', $e );
return $view->render( $e->getTemplate() );
}
catch( Exception $e ){
return ''.esc_html( $e->getMessage() ).'
';
}
}
/**
* @internal
* Make this view a child of another template. i.e. decorate this with that.
* Parent will have access to original argument scope, but separate from now on
* @return Loco_mvc_View the parent view
*/
private function extend( $tpl ){
$this->parent = new Loco_mvc_View;
$this->parent->cwd = $this->cwd;
$this->parent->setTemplate( $tpl );
return $this->parent;
}
/**
* @internal
* After start is called any captured output will be placed in the named variable
* @return void
*/
private function start( $name ){
$this->stop();
$this->scope[$name] = null;
$this->block = $name;
}
/**
* @internal
* When stop is called, buffered output is saved into current variable for output by parent template, or at end of script.
* @return void
*/
private function stop(){
$content = ob_get_contents();
ob_clean();
if( $b = $this->block ){
if( isset($this->scope[$b]) ){
$content = $this->scope[$b].$content;
}
$this->scope[$b] = new _LocoViewBuffer($content);
$this->block = null;
}
$this->block = '_trash';
}
/**
* implement IteratorAggregate::getIterator
*/
public function getIterator(){
return $this->scope;
}
/**
* @return mixed
*/
public function __get( $prop ){
return isset($this->scope[$prop]) ? $this->scope[$prop] : null;
}
/**
* @return bool
*/
public function has( $prop ){
return isset( $this->scope[$prop] );
}
/**
* Set a view argument
* @return Loco_mvc_View
*/
public function set( $prop, $value ){
$this->scope[$prop] = $value;
return $this;
}
/**
* Main entry to rendering complete template
* @param string template name excluding extension
* @param array extra arguments to set in view scope
* @return string
*/
public function render( $tpl, array $args = null, Loco_mvc_View $parent = null ){
if( $this->block ){
return $this->fork()->render( $tpl, $args, $this );
}
$this->setTemplate($tpl);
if( $parent && $this->template === $parent->template ){
throw new Loco_error_Exception('Avoiding infinite loop');
}
if( is_array($args) ){
foreach( $args as $prop => $value ){
$this->set($prop, $value);
}
}
ob_start();
$content = $this->buffer();
ob_end_clean();
return $content;
}
/**
* Do actual render of currently validated template path
* @return string content not captured in sub-blocks
*/
private function buffer(){
$this->start('_trash');
$this->execTemplate( $this->template );
$this->stop();
$this->block = null;
// decorate via parent view if there is one
if( $this->parent ){
$this->parent->scope = clone $this->scope;
$this->parent->set('_content', $this->_trash );
return $this->parent->buffer();
}
// else at the root of view chain
return (string) $this->_trash;
}
/**
* Set current template
* @param string path tro template, excluding file extension
*/
public function setTemplate( $tpl ){
$file = new Loco_fs_File( $tpl.'.php' );
$file->normalize( $this->cwd );
if( ! $file->exists() ){
$debug = str_replace( loco_plugin_root().'/', '', $file->getPath() );
throw new Loco_error_Exception( 'Template not found: '.$debug );
}
$this->cwd = $file->dirname();
$this->template = $file->getPath();
}
/**
* @return Loco_mvc_View
*/
private function fork(){
$view = new Loco_mvc_View;
$view->cwd = $this->cwd;
$view->scope = clone $this->scope;
return $view;
}
/**
* Do actual runtime template include
*/
private function execTemplate( $template ){
$params = $this->scope;
extract( $params->getArrayCopy() );
include $template;
}
/**
* Link generator
* return Loco_mvc_ViewParams
*/
public function route( $action, array $args = array() ){
return new Loco_mvc_ViewParams( array(
'href' => Loco_mvc_AdminRouter::generate( $action, $args ),
) );
}
/**
* Shorthand for `echo esc_html( sprintf( ...`
* @return void
*/
private function e( $text ){
if( 1 < func_num_args() ){
$args = func_get_args();
$text = call_user_func_array( 'sprintf', $args );
}
echo htmlspecialchars( $text, ENT_COMPAT, 'UTF-8' );
return '';
}
}
/**
* @internal
*/
class _LocoViewBuffer {
private $s;
public function __construct( $s ){
$this->s = $s;
}
public function __toString(){
return $this->s;
}
}
PK T\Ll l mvc/HiddenFields.phpnu W+A $value ){
echo '';
}
}
/**
* Add a nonce field
* @return Loco_mvc_HiddenFields
*/
public function setNonce( $action ){
$this['loco-nonce'] = wp_create_nonce( $action );
return $this;
}
/**
* Load postdata fields
* @return Loco_mvc_HiddenFields
*/
public function addPost( Loco_mvc_PostParams $post ){
foreach( $post->getSerial() as $pair ){
$this[ $pair[0] ] = isset($pair[1]) ? $pair[1] : '';
}
return $this;
}
}
PK T\ ӆL L mvc/AjaxController.phpnu W+A auth();
$this->output = new ArrayObject;
$this->input = new ArrayObject( $args );
// avoid fatal error if json extension is missing
loco_check_extension('json');
}
/**
* Get posted data and validate nonce in the process
* @return Loco_mvc_PostParams
*/
protected function validate(){
$route = $this->input['route'];
if( ! $this->checkNonce($route) ){
throw new Loco_error_Exception( sprintf('Ajax %s action requires postdata with nonce',$route) );
}
return Loco_mvc_PostParams::get();
}
/**
* {@inheritdoc}
*/
public function get( $prop ){
return isset($this->input[$prop]) ? $this->input[$prop] : null;
}
/**
* {@inheritdoc}
*/
public function set( $prop, $value ){
$this->output[$prop] = $value;
return $this;
}
/**
* @return string JSON
*/
public function render(){
$data = array (
'data' => $this->output->getArrayCopy(),
);
// non-fatal notices deliberately not in "error" key
if( $array = Loco_error_AdminNotices::destroyAjax() ){
$data['notices'] = $array;
}
return json_encode( $data );
}
/**
* Pretty json encode if PHP version allows
*
protected function json_encode( $data ){
$opts = 0;
if( defined('JSON_PRETTY_PRINT') ){
$opts |= JSON_PRETTY_PRINT;
}
if( defined('JSON_UNESCAPED_SLASHES') ){
$opts |= JSON_UNESCAPED_SLASHES;
}
return json_encode( $data, $opts );
}*/
}PK T\' ' data/CompiledData.phpnu W+A data = loco_include( $path );
$this->name = $name;
}
public function destroy(){
unset( self::$reg[$this->name], $this->data );
}
public function offsetGet( $k ){
return isset($this->data[$k]) ? $this->data[$k] : null;
}
public function offsetExists( $k ){
return isset($this->data[$k]);
}
public function offsetUnset( $k ){
throw new RuntimeException('Read only');
}
public function offsetSet( $k, $v ){
throw new RuntimeException('Read only');
}
}PK T\vLg data/Option.phpnu W+A getKey();
return update_option( $key, $this->getSerializable(), false );
}
/**
* Retrieve and unserialize this object from WordPress options table
* @return bool whether object existed in cache
*/
public function fetch(){
$key = 'loco_'.$this->getKey();
$data = get_option( $key );
try {
$this->setUnserialized($data);
}
catch( InvalidArgumentException $e ){
return false;
}
return true;
}
/**
* Delete option from WordPress
*/
public function remove(){
$key = 'loco_'.$this->getKey();
return delete_option( $key );
}
}PK T\̕ data/Serializable.phpnu W+A setFlags( ArrayObject::ARRAY_AS_PROPS );
parent::__construct( $data );
$this->dirty = (bool) $data;
}
/**
* Check if object's properties have change since last clean
* @return bool
*/
public function isDirty(){
return $this->dirty;
}
/**
* Make not dirty
* @return Loco_data_Serializable
*/
protected function clean(){
$this->dirty = false;
return $this;
}
/**
* Call persist method only if has changed since last clean
* @return Loco_data_Serializable
*/
public function persistIfDirty(){
if( $this->isDirty() ){
$params = func_get_args();
call_user_func_array( array($this,'persist'), $params );
}
return $this;
}
/**
* @override so we can set dirty flag
*/
public function offsetSet( $prop, $value ){
if( ! isset($this[$prop]) || $value !== $this[$prop] ){
parent::offsetSet( $prop, $value );
$this->dirty = true;
}
}
/**
* @override so we can set dirty flag
*/
public function offsetUnset( $prop ){
if( isset($this[$prop]) ){
parent::offsetUnset($prop);
$this->dirty = true;
}
}
/**
* @return Loco_data_Serializable
*/
public function setVersion( $version ){
if( $version !== $this->v ){
$this->v = $version;
$this->dirty = true;
}
return $this;
}
/**
* @return string|int|float
*/
public function getVersion(){
return $this->v;
}
/**
* Get serializable data for storage
* @return array
*/
protected function getSerializable(){
return array (
'c' => get_class($this),
'v' => $this->getVersion(),
'd' => $this->getArrayCopy(),
);
}
/**
* Restore object state from array as returned from getSerializable
* @return Loco_data_Serializable
*/
protected function setUnserialized( $data ){
if( ! is_array($data) || ! isset($data['d']) ) {
throw new InvalidArgumentException('Unexpected data');
}
if( get_class($this) !== $data['c'] ){
throw new InvalidArgumentException('Unexpected class name');
}
$this->setVersion( $data['v'] );
// ok to populate ArrayObject
$this->exchangeArray( $data['d'] );
// because object is being restored, probably from disk. this make it clean now
$this->dirty = false;
return $this;
}
}PK T\_? data/RecentItems.phpnu W+A fetch();
}
return self::$current;
}
/**
* Trash data and remove from memory
*/
public static function destroy(){
$tmp = new Loco_data_RecentItems;
$tmp->remove();
self::$current = null;
}
/**
* @internal
* @return Loco_data_RecentItems
*/
private function push( $object, array $indexes ){
foreach( $indexes as $key => $id ){
$stack = isset($this[$key]) ? $this[$key] : array();
// remove before add ensures latest item appended to hashmap
unset($stack[$id]);
$stack[$id] = time();
$this[$key] = $stack;
// TODO prune stack to maximum length
}
return $this;
}
/**
* @return array
*/
private function getItems( $key, $offset, $count ){
$stack = isset($this[$key]) ? $this[$key] : array();
// hash map should automatically be in "push" order, meaning most recent last
// sorting gives wrong order for same-second updates (only relevent in tests, but still..)
// asort( $stack, SORT_NUMERIC );
$stack = array_reverse( array_keys( $stack ) );
if( is_null($count) && 0 === $offset ){
return $stack;
}
return array_slice( $stack, $offset, $count, false );
}
/**
* @return int
*/
private function hasItem( $key, $id ){
if( isset($this[$key]) && ( $items = $this[$key] ) && isset($items[$id]) ){
return $items[$id];
}
return 0;
}
/**
* Push bundle to the front of recent bundles stack
* @return Loco_data_RecentItems
*/
public function pushBundle( Loco_package_Bundle $bundle ){
return $this->push( $bundle, array( 'bundle' => $bundle->getId() ) );
}
/**
* Get bundle IDs
* @return array
*/
public function getBundles( $offset = 0, $count = null ){
return $this->getItems('bundle', $offset, $count );
}
/**
* Check if a bundle has been recently used
* @return int timestamp item was added, 0 if absent
*/
public function hasBundle( $id ){
return $this->hasItem( 'bundle', $id );
}
/**
* TODO other types of item
* Push project to the front of recent bundles stack
* @return Loco_data_RecentItems
*
public function pushProject( Loco_package_Project $project ){
return $this;
}*/
}
PK T\:,MaG G data/Transient.phpnu W+A getKey();
$data = $this->getSerializable();
set_transient( $key, $data, $ttl );
$this->lazy = null;
$this->clean();
}
else {
$this->lazy = $ttl;
}
return $this;
}
/**
* Commit to transient cache on object destruction
*/
final public function __destruct(){
if( is_int($this->lazy) ){
$this->persistIfDirty( $this->lazy, true );
}
}
/**
* Retrieve and unserialize this object from WordPress transient cache
* @return bool whether object existed in cache
*/
public function fetch(){
$v = $this->getVersion();
$key = 'loco_'.$this->getKey();
$data = get_transient( $key );
try {
$this->setUnserialized($data);
return true;
}
catch( InvalidArgumentException $e ){
return false;
}
}
}PK T\{Ω data/Session.phpnu W+A clear();
}
catch( Exception $e ){
// probably no session to destroy
}
self::$current = null;
}
}
/**
* Commit current session data to WordPress storage and remove from memory
*/
public static function close(){
if( self::$current && self::$current->dirty ){
self::$current->persist();
self::$current = null;
}
}
/**
* @internal
*/
final public function __construct( array $raw = array() ){
$this->token = wp_get_session_token();
if( ! $this->token ){
throw new Loco_error_Exception('Failed to get session token');
}
parent::__construct( array() );
$this->manager = WP_Session_Tokens::get_instance( get_current_user_id() );
// populate object from stored session data
$data = $this->getRaw();
if( isset($data['loco']) ){
$this->setUnserialized( $data['loco'] );
}
// any initial arbitrary data can be merged on top
foreach( $raw as $prop => $value ){
$this[$prop] = $value;
}
// enforce single instance
self::$current = $this;
// ensure against unclean shutdown
if( loco_debugging() ){
register_shutdown_function( array($this,'_on_shutdown') );
}
}
/**
* @internal
* Ensure against unclean use of session storage
*/
public function _on_shutdown(){
if( $this->dirty ){
trigger_error('Unclean session shutdown: call either Loco_data_Session::destroy or Loco_data_Session::close');
}
}
/**
* Get raw session data held by WordPress
* @return array
*/
private function getRaw(){
$data = $this->manager->get( $this->token );
// session data will exist if WordPress login is valid
if( ! $data || ! is_array($data) ){
throw new Loco_error_Exception('Invalid session');
}
return $data;
}
/**
* Persist object in WordPress usermeta table
* @return Loco_data_Session
*/
public function persist(){
$data = $this->getRaw();
$data['loco'] = $this->getSerializable();
$this->manager->update( $this->token, $data );
$this->dirty = false;
return $this;
}
/**
* Clear object data and remove our key from WordPress usermeta record
* @return Loco_data_Session
*/
public function clear(){
$data = $this->getRaw();
if( isset($data['loco']) ){
unset( $data['loco'] );
$this->manager->update( $this->token, $data );
}
$this->exchangeArray( array() );
$this->dirty = false;
return $this;
}
/**
* @param string name of messages bag, e.g. "errors"
* @param mixed optionally put data in rather than getting data out
* @return mixed
*/
public function flash( $bag, $data = null ){
if( isset($data) ){
$this->dirty = true;
$this[$bag][] = $data;
return;
}
// else get first object in bag and remove before returning
if( isset($this[$bag]) ){
if( $data = array_shift($this[$bag]) ){
$this->dirty = true;
return $data;
}
}
}
/**
* @internal
*/
public function offsetSet( $index, $newval ){
if( ! isset($this[$index]) || $newval !== $this[$index] ){
$this->dirty = true;
parent::offsetSet( $index, $newval );
}
}
/**
* @internal
*/
public function offsetUnset( $index ){
if( isset($this[$index]) ){
$this->dirty = true;
parent::offsetUnset( $index );
}
}
}
PK T\$)T
T
data/Preferences.phpnu W+A '',
);
/**
* Get current user's preferences
* @return Loco_data_Preferences
*/
public static function get(){
$id = get_current_user_id();
if( ! $id ){
throw new Exception('No current user');
}
if( isset(self::$current[$id]) ){
return self::$current[$id];
}
$prefs = self::create($id);
self::$current[$id] = $prefs;
$prefs->fetch();
return $prefs;
}
/**
* Create default settings instance
* @return Loco_data_Preferences
*/
public static function create( $id ){
$prefs = new Loco_data_Preferences( self::$defaults );
$prefs->user_id = $id;
return $prefs;
}
/**
* Persist object in WordPress usermeta table
* @return bool
*/
public function persist(){
return update_user_meta( $this->user_id, 'loco_prefs', $this->getSerializable() ) ? true : false;
}
/**
* Retrieve and unserialize this object from WordPress usermeta table
* @return bool whether object existed in cache
*/
public function fetch(){
$data = get_user_meta( $this->user_id, 'loco_prefs', true );
try {
$this->setUnserialized($data);
}
catch( InvalidArgumentException $e ){
return false;
}
return true;
}
/**
* Delete usermeta entry from WordPress
* return bool
*/
public function remove(){
$id = $this->user_id;
self::$current[$id] = null;
return delete_user_meta( $id, 'loco_prefs' );
}
/**
* Populate all settings from raw postdata.
* @return Loco_data_Preferences
*/
public function populate( array $data ){
// set all keys present in array
foreach( $data as $prop => $value ){
try {
$this->offsetSet( $prop, $value );
}
catch( InvalidArgumentException $e ){
// skipping invalid key
}
}
return $this;
}
}PK T\]JV V data/Cookie.phpnu W+A setName( $name );
}
}
}
/**
* @internal
*/
public function __toString(){
$data = $this->getArrayCopy();
return http_build_query( $data, null, '&' );
}
/**
* @return Loco_data_Cookie
*/
public function setName( $name ){
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName(){
return $this->name;
}
/**
* Send cookie to the browser, unless filtered out.
* @return bool|null
*/
public function send(){
if( false !== apply_filters( 'loco_setcookie', $this ) ){
$value = (string) $this;
// @codeCoverageIgnoreStart
return setcookie( $this->name, $value, $this->expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
}
}
/**
* Empty values such that sending cookie would remove it from browser
* @return Loco_data_Cookie
*/
public function kill(){
$this->exchangeArray( array() );
$this->expires = time() - 86400;
return $this;
}
}PK T\t1 1 data/Permissions.phpnu W+A = 4.3
* @return WP_Roles
*/
private static function wp_roles(){
global $wp_roles;
if( ! isset($wp_roles) ){
get_role('ping');
}
return $wp_roles;
}
/**
* Set up default roles and capabilities
* @return WP_Roles
*/
public static function init(){
$roles = self::wp_roles();
$apply = array();
// absense of translator role indicates first run
// by default we'll initially allow full access to anyone that can manage_options
if( ! $roles->get_role('translator') ){
// lazy create "translator" role
$apply['translator'] = $roles->add_role( 'translator', 'Translator', array() );
/* @var $role WP_Role */
foreach( $roles->role_objects as $id => $role ){
if( $role->has_cap('manage_options') ){
$apply[$id] = $role;
}
}
}
// fix broken permissions whereby super admin cannot access Loco at all.
// this could happen if another plugin added the translator role before hand.
if( ! isset($apply['administrator']) && ! is_multisite() ){
$apply['administrator'] = $roles->get_role('administrator');
}
/* @var $role WP_Role */
foreach( $apply as $role ){
if( $role instanceof WP_Role ){
foreach( self::$caps as $cap ){
$role->has_cap($cap) || $role->add_cap($cap);
}
}
}
return $roles;
}
/**
* @return array
*/
public function getRoles(){
$roles = self::wp_roles();
return $roles->role_objects;
}
/**
* Check if role is protected such that user cannot lock themselves out when modifying settings
* @param WP_Role WordPress role object to check
* @return bool
*/
public function isProtectedRole( WP_Role $role ){
// if current user has this role and is not the super user, prevent lock-out
$user = wp_get_current_user();
if( $user instanceof WP_User && ! is_super_admin($user->ID) && $user->has_cap('manage_options') ){
return in_array( $role->name, $user->roles, true );
}
// admin users of single site install must never be denied access
// note that there is no such thing as a network admin role, but network admins have all permissions
return is_multisite() ? false : $role->has_cap('delete_users');
}
/**
* Completely remove all Loco permissions, as if uninstalling
* @return Loco_data_Permissions
*/
public function remove(){
/* @var $role WP_Role */
foreach( $this->getRoles() as $role ){
foreach( self::$caps as $cap ){
$role->has_cap($cap) && $role->remove_cap($cap);
}
}
// we'll only remove our custom role if it has no capabilities
// this avoids breaking other plugins that use it, or added it.
if( $role = get_role('translator') ){
if( ! $role->capabilities ){
remove_role('translator');
}
}
return $this;
}
/**
* Reset to default: roles include no Loco capabilities unless they have super admin privileges
* @param bool whether to prevent current user from locking themselves out of the plugin.
* @return array
*/
public function reset(){
$roles = $this->getRoles();
/* @var $role WP_Role */
foreach( $roles as $role ){
// always provide access to site admins on first run
$grant = $this->isProtectedRole($role);
foreach( self::$caps as $cap ){
if( $grant ){
$role->has_cap($cap) || $role->add_cap($cap);
}
else {
$role->has_cap($cap) && $role->remove_cap($cap);
}
}
}
return $roles;
}
/**
* Get translated WordPress role name
*/
public function getRoleName( $id ){
if( 'translator' === $id ){
$label = _x( 'Translator', 'User role', 'loco' );
}
else {
$names = self::wp_roles()->role_names;
$label = isset($names[$id]) ? translate_user_role( $names[$id] ) : $id;
}
return $label;
}
/**
* Populate permission settings from posted checkboxes
* @return Loco_data_Permissions
*/
public function populate( array $caps ){
// drop all permissions before adding (cos checkboxes)
$roles = $this->reset();
foreach( $caps as $id => $checked ){
if( isset($roles[$id]) ){
$role = $roles[$id];
/* @var $role WP_Role */
foreach( self::$caps as $cap ){
if( ! empty($checked[$cap]) ){
$role->has_cap($cap) || $role->add_cap($cap);
}
}
}
}
return $this;
}
} PK T\I?(B B data/Settings.phpnu W+A '',
// whether to compile hash table into MO files
'gen_hash' => false,
// whether to include Fuzzy strings in MO files
'use_fuzzy' => true,
// number of backups to keep of Gettext files
'num_backups' => 1,
// alternative names for POT files in priority order
'pot_alias' => array( 'default.po', 'en_US.po', 'en.po' ),
// alternative file extensions for PHP files
'php_alias' => array( 'php' ),
// whether to remember file system credentials in session
'fs_persist' => false,
// skip PHP source files this size or larger
'max_php_size' => '100K',
/*/ Legacy options from 1.x branch:
// whether to use external msgfmt command (1), or internal (default)
'use_msgfmt' => false,
// which external msgfmt command to use
'which_msgfmt' => '',
// whether to enable core package translation
'enable_core' => false,*/
);
/**
* Create default settings instance
* @return Loco_data_Settings
*/
public static function create(){
$args = self::$defaults;
$args['version'] = loco_plugin_version();
return new Loco_data_Settings( $args );
}
/**
* Get currently configured global settings
* @return Loco_data_Settings
*/
public static function get(){
$opts = self::$current;
if( ! $opts ){
$opts = self::create();
$opts->fetch();
self::$current = $opts;
}
return $opts;
}
/**
* Destroy current settings
* @return void
*/
public static function clear(){
delete_option('loco_settings');
self::$current = null;
}
/**
* Destroy current settings and return a fresh one
* @return Loco_data_Settings
*/
public static function reset(){
self::clear();
return self::$current = self::create();
}
/**
* @override
*/
public function offsetSet( $prop, $value ){
if( ! isset(self::$defaults[$prop]) ){
throw new InvalidArgumentException('Invalid option, '.$prop );
}
$default = self::$defaults[$prop];
// cast to same type as default
if( is_bool($default) ){
$value = (bool) $value;
}
else if( is_int($default) ){
$value = (int) $value;
}
else if( is_array($default) ){
if( ! is_array($value) ){
// TODO use a standard CSV split for array values?
$value = preg_split( '/[\s,]+/', trim($value), -1, PREG_SPLIT_NO_EMPTY );
}
}
else {
$value = (string) $value;
}
parent::offsetSet( $prop, $value );
}
/**
* Commit current settings to WordPress DB
* @return bool
*/
public function persist(){
$this->version = loco_plugin_version();
$this->clean();
return update_option('loco_settings', $this->getSerializable() );
}
/**
* Pull current settings from WordPress DB and merge into this object
* @return bool whether settings where previously saved
*/
public function fetch(){
if( $data = get_option('loco_settings') ){
$copy = new Loco_data_Settings;
$copy->setUnserialized($data);
// preserve any defaults not in previously saved data
// this will occur if we've added options since setting were saved
$data = $copy->getArrayCopy() + $this->getArrayCopy();
// could ensure redundant keys are removed, but no need currently
// $data = array_intersect_key( $data, self::$defaults );
$this->exchangeArray( $data );
$this->clean();
return true;
}
return false;
}
/**
* Run migration in case plugin has been upgraded since settings last saved
* @return bool whether upgrade has occured
*/
public function migrate(){
$existed = (bool) get_option('loco_settings');
// Populate new format from legacy 1.x options, but only on first run
if( ! $existed ){
$this->gen_hash = get_option('loco-translate-gen_hash','0');
$this->use_fuzzy = get_option('loco-translate-use_fuzzy', '1' );
$this->num_backups = get_option('loco-translate-num_backups','1');
$this->persist();
}
// currently the only upgrade could be 1.x => 2.0
// deliberately keeping the old options due to legacy switching feature
return ! $existed;
}
/**
* Populate all settings from raw postdata.
* @return Loco_data_Settings
*/
public function populate( array $data ){
// set all keys present in array
foreach( $data as $prop => $value ){
try {
$this->offsetSet( $prop, $value );
}
catch( InvalidArgumentException $e ){
// skipping invalid key
}
}
// set missing boolean keys as false, because checkboxes
if( $missing = array_diff_key(self::$defaults,$data) ){
foreach( $missing as $prop => $default ){
if( is_bool($default) ){
parent::offsetSet( $prop, false );
}
}
}
// enforce missing values that must have default
foreach( array('php_alias','max_php_size') as $prop ){
if( isset($data[$prop]) && '' === $data[$prop] ){
parent::offsetSet( $prop, self::$defaults[$prop] );
}
}
return $this;
}
}
PK T\?(͒ test/DummyFtpConnect.phpnu W+A getWriteContext()->connect( new WP_Filesystem_Debug($creds) )`
*/
class Loco_test_DummyFtpConnect extends Loco_hooks_Hookable {
public function filter_filesystem_method(){
return 'debug';
}
}
/**
* Dummy FTP file system.
* WARNING: this actually modifies files - it just does it while simulating a remote connection
* - All operations performed "direct" when authorized, else they fail.
*/
class WP_Filesystem_Debug extends WP_Filesystem_Base {
private $authed;
/**
* @var WP_Error
*/
public $errors;
public function __construct( array $opt ) {
$this->options = $opt;
$this->method = 'ftp';
}
/**
* Dummy FTP connect: requires username=foo password=xxx
*/
public function connect() {
$this->authed = false;
$this->errors = new WP_Error;
// @codeCoverageIgnoreStart
if( empty($this->options['hostname']) ){
$this->errors->add( 'bad_hostname', 'Debug: empty hostname');
return false;
}
if( empty($this->options['username']) ){
$this->errors->add( 'bad_username', 'Debug: empty username');
return false;
}
if( $this->options['username'] !== 'foo' ) {
$this->errors->add( 'bad_username', 'Debug: username expected to be "foo"');
return false;
}
if( empty($this->options['password']) ){
$this->errors->add( 'bad_username', 'Debug: empty password');
return false;
}
if( $this->options['password'] !== 'xxx' ) {
$this->errors->add( 'bad_password', 'Debug: password expected to be "xxx"' );
return false;
}
// @codeCoverageIgnoreEnd
$this->authed = true;
return true;
}
/**
* @return WP_Filesystem_Debug
*/
public function disconnect(){
$this->authed = false;
$this->options = array();
return $this;
}
/**
* {@inheritdoc}
* Dummy function allows exact path to be returned, subject to debugging filters
*/
public function find_folder( $path ){
if( WP_CONTENT_DIR === $path ){
return loco_constant('WP_CONTENT_DIR');
}
return false;
}
/**
* @internal
* Proxies supposed remote call to *real* direct call, as long as instance is authorized.
* Deliberately not extending WP_Filesystem_Direct for safety.
*/
private function _call( $method, array $args ){
if( $this->authed ){
$real = new WP_Filesystem_Direct( null );
return call_user_func_array( array($real,$method), $args );
}
return false;
}
/**
* {@inheritdoc}
*/
public function is_writable( $file ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function chmod( $file, $mode = false, $recursive = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function copy( $source, $destination, $overwrite = false, $mode = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function put_contents( $path, $data, $mode = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function delete( $file, $recursive = false, $type = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
}
PK T\P}# }# test/WordPressTestCase.phpnu W+A prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE '%s'", array('loco_%','_transient_loco_%','_transient_timeout_loco_%') );
if( $results = $wpdb->get_results($query,ARRAY_N) ){
foreach( $results as $row ){
list( $option_name ) = $row;
delete_option( $option_name );
}
}
}
public static function setUpBeforeClass(){
parent::setUpBeforeClass();
Loco_data_Settings::clear();
Loco_data_Session::destroy();
Loco_data_RecentItems::destroy();
self::dropOptions();
}
public static function tearDownAfterClass(){
parent::tearDownAfterClass();
Loco_data_Settings::clear();
Loco_data_Session::destroy();
Loco_data_RecentItems::destroy();
wp_cache_flush();
self::dropOptions();
}
public function setUp(){
parent::setUp();
Loco_mvc_PostParams::destroy();
Loco_error_AdminNotices::destroy();
Loco_package_Listener::destroy();
wp_cache_flush();
// text domains should be unloaded at start of all tests
$GLOBALS['l10n'] = array();
// ensure test themes are registered and WordPress's cache is valid
register_theme_directory( LOCO_TEST_DATA_ROOT.'/themes' );
$sniff = get_theme_roots();
if( ! isset($sniff['empty-theme']) ){
delete_site_transient( 'theme_roots' );
}
// avoid WordPress missing index notices
$GLOBALS['_SERVER'] += array (
'HTTP_HOST' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.0',
'HTTP_USER_AGENT' => 'Loco/'.get_class($this),
);
// tests should always dictate the file system method, which defaults to direct
add_filter('filesystem_method', array($this,'filter_fs_method') );
add_filter('loco_constant_DISALLOW_FILE_MODS', array($this,'filter_fs_allow') );
// capture cookies so we can test what is set
add_filter('loco_setcookie', array($this,'captureCookie'), 10, 1 );
$this->cookies_set = array();
}
/**
* {@inheritdoc}
*/
public function clean_up_global_scope(){
parent::clean_up_global_scope();
$_COOKIE = array();
$_REQUEST = array();
}
/**
* Capture cookie and prevent actual http sending
*/
public function captureCookie( Loco_data_Cookie $cookie ){
$this->cookies_set[ $cookie->getName() ] = $cookie;
return false;
}
/**
* @return Loco_data_Cookie
*/
public function assertCookieSet( $name, $message = '' ){
$this->assertArrayHasKey( $name, $this->cookies_set, $message );
$cookie = $this->cookies_set[ $name ];
$this->assertInstanceOf( 'Loco_data_Cookie', $cookie, $message );
return $cookie;
}
/**
* Invoke admin page controller without full hook set up
*/
public static function renderPage(){
$router = new Loco_mvc_AdminRouter;
$router->on_admin_menu();
$screen = get_current_screen();
$action = isset($_GET['action']) ? $_GET['action'] : null;
$router->initPage( $screen, $action );
return get_echo( array($router,'renderPage') );
}
/**
* Invoke Ajkax controller without full hook set up.
* @return string JSON
*/
protected function renderAjax(){
$router = new Loco_mvc_AjaxRouter;
$router->on_init();
return $router->renderAjax();
}
/**
* @internal
*/
public function filter_fs_method( $method = '' ){
return is_null($this->fs_method) ? $method : $this->fs_method;
}
/**
* @return Loco_test_WordPressTestCase
*/
public function set_fs_method( $method ){
$GLOBALS['wp_filesystem'] = null;
$this->fs_method = $method;
$ping = class_exists('Loco_test_DummyFtpConnect');
return $this;
}
/**
* @return Loco_test_WordPressTestCase
*/
public function disable_file_mods(){
$this->fs_allow = false;
return $this;
}
/**
* @internal
*/
public function filter_fs_allow(){
return ! $this->fs_allow;
}
/**
* Remove files created under tmp
*/
protected function clearTmp(){
$root = new Loco_fs_Directory( LOCO_TEST_DATA_ROOT.'/tmp' );
$dir = new Loco_fs_FileFinder( $root );
$dir->setRecursive( true );
$dirs = array();
/* @var $file Loco_fs_File */
foreach( $dir as $file ){
$dirs[ $file->dirname() ] = true;
$file->unlink();
}
// Be warned only directories found above will be removed
foreach( array_keys($dirs) as $path ){
$dir = new Loco_fs_Directory($path);
while( $dir->exists() && ! $dir->equal($root) ){
$dir->unlink();
$dir = $dir->getParent();
}
}
}
protected function login( $role = 'administrator' ){
$user = self::factory()->user->create( array( 'role' => $role ) );
if( $user instanceof WP_Error ){
foreach( $user->get_error_messages() as $message ){
trigger_error( $message );
}
throw new Exception('Failed to login');
}
// setting user required to have proper user object
$user = wp_set_current_user( $user );
// simulate default permissions used in admin menu hookage
if( $user->has_cap('manage_options') ){
$user->add_cap('loco_admin');
}
// simulate wp_set_auth_cookie. Can't actually set cookie cos headers
$_COOKIE[LOGGED_IN_COOKIE] = wp_generate_auth_cookie( $user->ID, time()+60, 'logged_in' );
$debug = array( 'name' => $this->getName(), 'token' => wp_get_session_token() ,'uid' => $user->ID );
// forcing new session instance
new Loco_data_Session;
}
protected function logout(){
Loco_data_Session::destroy();
wp_destroy_current_session();
unset( $_COOKIE[LOGGED_IN_COOKIE] );
wp_set_current_user( 0 );
$GLOBALS['current_user'] = null;
}
/**
* Switch loco_debugging on
*/
protected function enable_debug(){
add_filter('loco_debug', '__return_true' );
}
/**
* Switch loco_debugging off
*/
protected function disable_debug(){
add_filter('loco_debug', '__return_false' );
}
/**
* Temporarily enable the "en_GB_debug" test locale
*/
protected function enable_debug_locale(){
return $this->enable_locale('en_GB_debug');
}
/**
* Temporarily enable a specific locale
*/
protected function enable_locale( $locale ){
$this->locale = $locale;
add_filter('locale', array($this,'_filter_locale') );
}
/**
* @internal
*/
public function _filter_locale(){
return $this->locale;
}
/**
* Temporarily set test data root to content directory
*/
public function enable_test_content_dir(){
add_filter('loco_constant_WP_CONTENT_DIR', array($this,'_filter_wp_content_dir'), 10, 0 );
}
/**
* @internal
*/
public function _filter_wp_content_dir(){
return LOCO_TEST_DATA_ROOT;
}
public function capture_redirects(){
add_filter('wp_redirect', array($this,'filter_wp_redirect'), 10, 2 );
}
public function filter_wp_redirect( $location, $status ){
$this->redirect = func_get_args();
return false;
}
public function assertRedirected( $status = 302, $message = 'Failed to redirect' ){
$raw = $this->redirect;
$this->assertInternalType('array', $raw, $message );
$this->assertSame( $status, $raw[1], $message );
return $raw[0];
}
public function setPostArray( array $post ){
$_POST = $post;
$_REQUEST = array_merge( $_GET, $_POST, $_COOKIE );
$_SERVER['REQUEST_METHOD'] = 'POST';
Loco_mvc_PostParams::destroy();
}
public function addPostArray( array $post ){
$this->setPostArray( $post + $_POST );
}
public function setGetArray( array $get ){
$_GET = $get;
$_REQUEST = array_merge( $_GET, $_POST, $_COOKIE );
$_SERVER['REQUEST_METHOD'] = 'GET';
}
public function addGetArray( array $get ){
$this->setGetArray( $get + $_GET );
}
}PK T\4l test/TransientObject.phpnu W+A preserveWhitespace = false;
$dom->formatOutput = false;
$dom->loadXML( ''.$src.'' );
$dom->normalizeDocument();
$src = $dom->saveXML();
return trim( preg_replace( '/>\s+', '><', $src ) );
}
public function assertSameHtml( $expect, $actual, $message = null ){
return $this->assertSame( $this->normalizeHtml($expect), $this->normalizeHtml($actual), $message );
}
}PK T\
test/TestFilters.phpnu W+A formatOutput = true;
$dom->registerNodeClass('DOMElement','LocoConfig_DOMElement');
$this->xpath = new DOMXPath($dom);
return $dom;
}
/**
* {@inheritdoc}
* @return LocoConfigNodeListIterator
*/
public function query( $query, $context = null ){
$list = $this->xpath->query( $query, $context );
return new LocoConfigNodeListIterator( $list );
}
/**
* @return void
*/
public function loadXml( $source ){
if( ! $source ){
throw new Loco_error_XmlParseException( __('XML supplied is empty','loco') );
}
$dom = $this->getDom();
// parse with silent errors, clearing after
$used_errors = libxml_use_internal_errors(true);
$result = $dom->loadXML( $source, LIBXML_NONET );
unset( $source );
// fetch errors and ensure clean for next run.
$errors = libxml_get_errors();
$used_errors || libxml_use_internal_errors(false);
libxml_clear_errors();
// Throw exception if error level exceeds current tolerance
if( $errors ){
/* @var $error LibXMLError */
foreach( $errors as $error ){
if( $error->level >= LIBXML_ERR_FATAL ){
$e = new Loco_error_XmlParseException( trim($error->message) );
//$e->setContext( $error->line, $error->column, $source );
throw $e;
} // @codeCoverageIgnoreStart
}
}
// @codeCoverageIgnoreEnd
// Not currently validating against a DTD, but may as well pre-empt generic model loading errors
if( ! $dom->documentElement || 'bundle' !== $dom->documentElement->nodeName ){
throw new Loco_error_XmlParseException('Expected document element');
}
$this->xpath = new DOMXPath($dom);
}
/**
* {@inheritdoc}
* Overridden to avoid empty text nodes in XML files, preferring . to
*/
protected function setFileElementPath( $node, $path ){
if( ! $path && '0' !== $path ){
$path = '.';
}
return parent::setFileElementPath( $node, $path );
}
}
/**
* @internal
*/
class LocoConfig_DOMElement extends DOMElement implements IteratorAggregate, Countable {
public function getIterator(){
return new LocoConfigNodeListIterator( $this->childNodes );
}
public function count(){
return $this->childNodes->length;
}
}
/**
* @internal
* Cos NodeList doesn't iterate
*/
class LocoConfigNodeListIterator implements Iterator, Countable, ArrayAccess {
/**
* @var DOMNodeList
*/
private $nodes;
/**
* @var int
*/
private $i;
/**
* @var int
*/
private $n;
public function __construct( DOMNodeList $nodes ){
$this->nodes = $nodes;
$this->n = $nodes->length;
}
public function count(){
return $this->n;
}
public function rewind(){
$this->i = -1;
$this->next();
}
public function key(){
return $this->i;
}
public function current(){
return $this->nodes->item( $this->i );
}
public function valid(){
return is_int($this->i);
}
public function next(){
while( true ){
$this->i++;
if( $child = $this->nodes->item($this->i) ){
break;
}
$this->i = null;
break;
}
}
public function offsetExists( $i ){
return $i >= 0 && $i < $this->n;
}
public function offsetGet( $i ){
return $this->nodes->item($i);
}
/**
* @codeCoverageIgnore
*/
public function offsetSet( $i, $value ){
throw new Exception('Read only');
}
/**
* @codeCoverageIgnore
*/
public function offsetUnset( $i ){
throw new Exception('Read only');
}
}
PK T\=gq q config/CustomSaved.phpnu W+A bundle->getType() ).'_config__'.$this->bundle->getHandle();
}
/**
* {@inheritdoc}
*/
public function persist(){
$writer = new Loco_config_BundleWriter( $this->bundle );
$this->exchangeArray( $writer->toArray() );
return parent::persist();
}
/**
* @return Loco_config_CustomSaved
*/
public function setBundle( Loco_package_Bundle $bundle ){
$this->bundle = $bundle;
return $this;
}
/**
* Modify currently set bundle according to saved config data
* @return Loco_package_Bundle
*/
public function configure(){
$this->bundle->clear();
$reader = new Loco_config_BundleReader( $this->bundle );
$reader->loadArray( $this->getArrayCopy() );
return $this->bundle;
}
}PK T\_8 8 config/BundleWriter.phpnu W+A bundle = $bundle;
}
/**
* @return string XML source
*/
public function toXml(){
$model = new Loco_config_XMLModel;
$dom = $this->compile($model);
return $dom->saveXML();
}
/**
* @return array
*/
public function toArray(){
$model = new Loco_config_ArrayModel;
$dom = $this->compile($model);
return $dom->export();
}
/**
* @return Loco_mvc_PostParams
*/
public function toForm(){
$model = new Loco_config_FormModel;
$dom = $this->compile($model);
return $model->getPost();
}
/**
* Alias of toArray implementing JsonSerializable
* @return array
*/
public function jsonSerialize(){
return $this->toArray();
}
/**
* Agnostic compilation of any config data type
* @return LocoConfigDocumentInterface
*/
private function compile( Loco_config_Model $model ){
$bundle = $this->bundle;
$model->setDirectoryPath( $bundle->getDirectoryPath() );
$systemTargets = $bundle->getSystemTargets();
$dom = $model->getDom();
$root = $dom->appendChild( $dom->createElement('bundle') );
$root->setAttribute( 'name', $bundle->getName() );
/*/ additional headers for information only (not read back in)
if( $value = $bundle->getHeaderInfo()->getVendorHost() ){
$root->setAttribute( 'vendor', $value );
}*/
foreach( $bundle->exportGrouped() as $domainName => $projects ){
$domainElement = $root->appendChild( $dom->createElement('domain') );
$domainElement->setAttribute( 'name', $domainName );
/* @var $proj Loco_package_Project */
foreach( $projects as $proj ){
$projElement = $domainElement->appendChild( $dom->createElement('project') );
// add project name even if it's the same as the bundle name
// when loading however, missing name will default to bundle name
$value = $proj->getName() or $value = $bundle->getName();
$projElement->setAttribute( 'name', $value );
// add project slug even if it's the same as the domain name
$value = $proj->getSlug();
$projElement->setAttribute( 'slug', $value );
//
// zero or more source file locations
$sourcesElement = $dom->createElement('source');
/* @var $file Loco_fs_Directory */
foreach( $proj->getConfiguredSources() as $file ){
$sourcesElement->appendChild( $model->createFileElement($file) );
}
// zero or more excluded source paths
$excludeElement = $dom->createElement('exclude');
foreach( $proj->getConfiguredSourcesExcluded() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$sourcesElement->appendChild($excludeElement);
}
if( $sourcesElement->hasChildNodes() ){
$projElement->appendChild( $sourcesElement );
}
//
// add zero or more target locations
$targetsElement = $dom->createElement('target');
/* @var $file Loco_fs_Directory */
foreach( $proj->getConfiguredTargets() as $file ){
if( ! in_array( $file->getPath(), $systemTargets, true ) ){
$targetsElement->appendChild( $model->createFileElement($file) );
}
}
// zero or more excluded targets
$excludeElement = $dom->createElement('exclude');
foreach( $proj->getConfiguredTargetsExcluded() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$targetsElement->appendChild($excludeElement);
}
if( $targetsElement->hasChildNodes() ){
$projElement->appendChild( $targetsElement );
}
//
// add single POT template location
if( $file = $proj->getPot() ){
$templateElement = $projElement->appendChild( $dom->createElement('template') );
$templateElement->appendChild( $model->createFileElement($file) );
// template may be prortected from end-user tampering
if( $proj->isPotLocked() ){
$templateElement->setAttribute('locked','true');
}
}
}
}
// Write bundle-level path exclusions
$excludeElement = $dom->createElement('exclude');
foreach( $bundle->getExcludedLocations() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$root->appendChild( $excludeElement );
}
return $dom;
}
}PK T\_W, config/BundleReader.phpnu W+A bundle = $bundle;
}
/**
* @return Loco_package_Bundle
*/
public function loadXml( Loco_fs_File $file ){
$this->bundle->setDirectoryPath( $file->dirname() );
$model = new Loco_config_XMLModel;
$model->loadXml( $file->getContents() );
return $this->loadModel( $model );
}
/**
* @return Loco_package_Bundle
*/
public function loadJson( Loco_fs_File $file ){
$this->bundle->setDirectoryPath( $file->dirname() );
return $this->loadArray( json_decode( $file->getContents(), true ) );
}
/**
* @return Loco_package_Bundle
*/
public function loadArray( array $raw ){
$model = new Loco_config_ArrayModel;
$model->loadArray( $raw );
return $this->loadModel( $model );
}
/**
* Agnostic construction of Bundle from any configuration format
* @return Loco_package_Bundle
*/
public function loadModel( Loco_config_Model $model ){
// Base directory required to resolve relative paths
$bundle = $this->bundle;
$model->setDirectoryPath( $bundle->getDirectoryPath() );
$dom = $model->getDom();
$bundleElement = $dom->documentElement;
if( ! $bundleElement || 'bundle' !== $bundleElement->nodeName ){
throw new InvalidArgumentException('Expected root bundle element');
}
// Set bundle meta data if configured
// note that bundles have no inherent slug as it can change according to plugin/theme directory naming
if( $bundleElement->hasAttribute('name') ){
$bundle->setName( $bundleElement->getAttribute('name') );
}
// Bundle-level path exclusions
foreach( $model->query('exclude/*',$bundleElement) as $fileElement ){
$bundle->excludeLocation( $model->evaluateFileElement($fileElement) );
}
/* @var $domainElement LocoConfigElement */
foreach( $model->query('domain',$bundleElement) as $domainElement ){
$slug = $domainElement->getAttribute('name') or $slug = $bundle->getSlug();
// bundle may not have a handle set (most likely only in tests)
if( ! $bundle->getHandle() ){
$bundle->setHandle( $slug );
}
// Text Domain may also be declared by bundle author
$domain = new Loco_package_TextDomain( $slug );
$declared = $bundle->getHeaderInfo();
if( $declared && $declared->TextDomain === $slug ){
$domain->setCanonical( true );
}
/* @var $projectElement LocoConfigElement */
foreach( $model->query('project',$domainElement) as $projectElement ){
$name = $projectElement->getAttribute('name') or $name = $bundle->getName();
$project = new Loco_package_Project( $bundle, $domain, $name );
if( $projectElement->hasAttribute('slug') ){
$project->setSlug( $projectElement->getAttribute('slug') );
}
//
foreach( $model->query('source',$projectElement) as $sourceElement ){
// sources may be , or pass in special if it could be either
foreach( $model->query('file',$sourceElement) as $fileElement ){
$project->addSourceFile( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('directory',$sourceElement) as $fileElement ){
$project->addSourceDirectory( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('path',$sourceElement) as $fileElement ){
$project->addSourceLocation( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('exclude/*', $sourceElement) as $fileElement ){
$project->excludeSourcePath( $model->evaluateFileElement($fileElement) );
}
}
// Avoid having no source locations
if( ! $project->hasSourceFiles() ){
if( $bundle->isSingleFile() ){
$project->addSourceFile( $bundle->getBootstrapPath() );
}
else {
$project->addSourceDirectory( $bundle->getDirectoryPath() );
}
}
//
foreach( $model->query('target',$projectElement) as $targetElement ){
// targets support only directory paths:
foreach( $model->query('directory',$targetElement) as $fileElement ){
$project->addTargetDirectory( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('exclude/*', $targetElement) as $fileElement ){
$project->excludeTargetPath( $model->evaluateFileElement($fileElement) );
}
}
// Avoid having no target locations ..
if( 0 === count($project->getConfiguredTargets() ) ){
// .. unless the inherited root is a global location
if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
$project->addTargetDirectory( $bundle->getDirectoryPath() );
}
}
//
// configure POT file, should only be one
foreach( $model->query('template',$projectElement) as $templateElement ){
if( $model->evaulateBooleanAttribute( $templateElement, 'locked') ){
$project->setPotLock( true );
}
foreach( $model->query('file',$templateElement) as $fileElement ){
$project->setPot( $model->evaluateFileElement( $fileElement ) );
break 2;
}
}
// add project last for additional configs to be appended
$bundle->addProject( $project );
}
}
return $bundle;
}
}PK T\@jX! X! config/ArrayModel.phpnu W+A loadArray( $root );
}
/**
* Construct model from exported array
* @return void
*/
public function loadArray( array $root ){
$dom = $this->getDom();
$dom->load( array('#document', array(), array($root) ) );
}
/**
* {@inheritdoc}
* Emulates *very limited* XPath queries used by the XML DOM.
*/
public function query( $query, $context = null ){
$match = new LocoConfigNodeList;
$query = explode('/', $query );
// absolute path always starts in document
if( $absolute = empty($query[0]) ){
$match->append( $this->getDom() );
}
// else start with base for relative path
else if( $context instanceof LocoConfigNode ){
$match->append( $context );
}
while( $query ){
$name = array_shift($query);
// self references do nothing
if( ! $name || '.' === $name ){
continue;
}
// match all current branches to produce new set of parents
$next = new LocoConfigNodeList;
foreach( $match as $parent ){
foreach( $parent->childNodes as $child ){
if( $name === $child->nodeName || ( '*' === $name && $child instanceof LocoConfigElement ) || ( 'text()' === $name && $child instanceof LocoConfigText) ){
$next->append( $child );
}
}
}
$match = $next;
}
return $match;
}
}
// The following classes are "private" to this file:
// They partially implement the same interfaces as the core DOM classes and are used for code hints.
// Interfaces are deliberately not used as the real DOM classes would not be able to implement them.
/**
* Node
*/
abstract class LocoConfigNode implements IteratorAggregate {
/**
* Raw data of internal format
* @var array
*/
protected $data;
/**
* Child nodes once cast to node objects
* @var LocoConfigNodeList
*/
protected $children;
/**
* @return mixed
*/
abstract public function export();
final public function __construct( $data ){
$this->data = $data;
}
protected function get_nodeName(){
return $this->data[0];
}
/*protected function get_attributes(){
return $this->data[1];
}*/
protected function get_childNodes(){
return $this->getIterator();
}
public function __get( $prop ){
$method = array( $this, 'get_'.$prop );
if( is_callable($method) ){
return call_user_func( $method );
}
}
/** @return LocoConfigNode */
public function appendChild( LocoConfigNode $child ){
$children = $this->getIterator();
$children->append( $child );
return $child;
}
/** @return bool */
public function hasChildNodes(){
return (bool) count( $this->getIterator() );
}
/**
* @return LocoConfigNodeList
*/
public function getIterator(){
if( ! $this->children ){
$raw = isset($this->data[2]) ? $this->data[2] : array();
$this->children = new LocoConfigNodeList( $this->data[2] );
}
return $this->children;
}
public function get_textContent(){
$s = '';
foreach( $this as $child ){
$s .= $child->get_textContent();
}
return $s;
}
}
/**
* NodeList
*/
class LocoConfigNodeList implements Iterator, Countable, ArrayAccess {
private $nodes;
private $i;
private $n;
public function __construct( array $nodes = array() ){
$this->nodes = $nodes;
$this->n = count( $nodes );
}
public function count(){
return $this->n;
}
public function rewind(){
$this->i = -1;
$this->next();
}
public function key(){
return $this->i;
}
public function current(){
return $this[ $this->i ];
}
public function valid(){
return is_int($this->i);
}
public function next(){
if( ++$this->i === $this->n ){
$this->i = null;
}
}
public function offsetExists( $i ){
return $i >= 0 && $i < $this->n;
}
public function offsetGet( $i ){
$node = $this->nodes[$i];
if( ! $node instanceof LocoConfigNode ){
if( is_array($node) ){
$node = new LocoConfigElement( $node );
}
else {
$node = new LocoConfigText( $node );
}
$this->nodes[$i] = $node;
}
return $node;
}
/**
* @codeCoverageIgnore
*/
public function offsetSet( $i, $value ){
throw new Exception('Use append');
}
/**
* @codeCoverageIgnore
*/
public function offsetUnset( $i ){
throw new Exception('Read only');
}
public function append( LocoConfigNode $node ){
$this->nodes[] = $node;
$this->n++;
}
/**
* Revert nodes back to raw array form and return for exporting
* @return array
*/
public function normalize(){
foreach( $this->nodes as $i => $node ){
if( $node instanceof LocoConfigNode ){
$this->nodes[$i] = $node->export();
}
}
return $this->nodes;
}
}
/**
* Document
*/
class LocoConfigDocument extends LocoConfigNode {
/**
* Rapidly set new data for document
*/
public function load( $data ){
$this->data = $data;
$this->children = null;
}
/**
* @return LocoConfigElement
*/
public function createElement( $name ){
return new LocoConfigElement( array( $name, array(), array() ) );
}
/**
* @return LocoConfigText
*/
public function createTextNode( $text ){
return new LocoConfigText( $text );
}
/**
* @return LocoConfigElement
*/
public function get_documentElement(){
$child = null;
foreach( $this as $child ){
break;
}
return $child;
}
/**
* {@inheritdoc}
* Override to keep single element root
*/
public function export(){
if( $root = $this->get_documentElement() ){
return $root->export();
}
}
}
/**
* Element
*/
class LocoConfigElement extends LocoConfigNode {
public function setAttribute( $prop, $value ){
$this->data[1][$prop] = $value;
}
public function removeAttribute( $prop ){
unset( $this->data[1][$prop] );
}
public function getAttribute( $prop ){
if( isset($this->data[1][$prop]) ){
return $this->data[1][$prop];
}
return '';
}
public function hasAttribute( $prop ){
return isset($this->data[1][$prop]);
}
/**
* {@inheritdoc}
*/
public function export(){
$raw = $this->data;
// return any cast elements back to raw data
if( $this->children ){
$raw[2] = $this->children->normalize();
}
return $raw;
}
}
/**
* Text
*/
class LocoConfigText extends LocoConfigNode {
protected function get_nodeName(){
return '#text';
}
public function hasChildNodes(){
return false;
}
public function getIterator(){
return new ArrayIterator;
}
public function export(){
return (string) $this->data;
}
public function get_nodeValue(){
return (string) $this->data;
}
public function get_textContent(){
return (string) $this->data;
}
}
PK T\g config/FormModel.phpnu W+A getDom();
$root = $dom->documentElement;
$post = new Loco_mvc_PostParams( array (
'name' => $root->getAttribute('name'),
'exclude' => array (
'path' => '',
),
'conf' => array(),
) );
/* @var LocoConfigElement $domain */
foreach( $this->query('domain',$root) as $domain ){
$domainName = $domain->getAttribute('name');
/* @var LocoConfigElement $project */
foreach( $domain as $project ){
$tree = array (
'name' => $project->getAttribute('name'),
'slug' => $project->getAttribute('slug'),
'domain' => $domainName,
'source' => array (
'path' => '',
'exclude' => array( 'path' => '' ),
),
'target' => array (
'path' => '',
'exclude' => array( 'path' => '' ),
),
'template' => array( 'path' => '', 'locked' => false ),
);
$post['conf'][] = $this->collectPaths( $project, $tree );
}
}
/* @var LocoConfigElement $paths */
foreach( $this->query('exclude',$root) as $paths ){
$post['exclude'] = $this->collectPaths( $paths, $post['exclude'] );
}
return $post;
}
private function collectPaths( LocoConfigElement $parent, array $branch ){
$texts = array();
foreach( $parent as $child ){
$name = $child->nodeName;
// all file types as "path" in form model
if( 'file' === $name || 'directory' === $name ){
$name = 'path';
}
if( isset($branch[$name]) ){
// collect text if child is a node
if( 'path' === $name ){
$file = $this->evaluateFileElement($child);
$path = $file->getRelativePath( $this->getDirectoryPath() );
if( '' === $path ){
$path = '.';
}
$texts[] = $path;
}
// else could be simple key to next depth
else if( is_array($branch[$name]) ){
$branch[$name] = $this->collectPaths( $child, $branch[$name] );
}
}
// @codeCoverageIgnoreStart
else {
throw new Exception('Unexpected structure: '.$name.' not in '.json_encode($branch) );
}
// @codeCoverageIgnoreEnd
}
// parent may have attributes we can set in branch data
foreach( $branch as $name => $default ){
if( $parent->hasAttribute($name) ){
if( is_bool($default) ){
$branch[$name] = $this->evaulateBooleanAttribute($parent, $name);
}
else {
$branch[$name] = $parent->getAttribute($name);
}
}
}
// set compiled path values if any collected
if( $texts ){
$value = implode("\n", $texts );
// display single root path as empty, but not when additional paths defined
if( '.' === $value ){
$branch['path'] = '';
}
else {
$branch['path'] = $value;
}
}
return $branch;
}
/**
* Construct model from posted form data.
* @return void
*/
public function loadForm( Loco_mvc_PostParams $post ){
// basic validation unlikely to fail when posted from UI
$name = $post->name;
if( ! $name ){
throw new InvalidArgumentException('Bundle must have a name');
}
$confs = $post->conf;
if( ! $confs || ! is_array($confs) ){
throw new InvalidArgumentException('Bundle must have at least one definition');
}
// transform posted data into internal model:
// deliberately not configuring bundle object at this point. simply converting data for storage.
$dom = $this->getDom();
$root = $dom->appendChild( $dom->createElement('bundle') );
$root->setAttribute( 'name', $name );
// bundle level excluded paths
if( $nodes = array_intersect_key( $post->getArrayCopy(), array( 'exclude' => '' ) ) ) {
$this->loadStruct( $root, $nodes );
}
// collect all projects grouped by domain
$domains = array();
foreach( $confs as $i => $conf ){
if( ! empty($conf['removed']) ){
continue;
}
if( empty($conf['domain']) ){
throw new InvalidArgumentException( __('Text Domain cannot be empty','loco') );
}
$domains[ $conf['domain'] ][] = $project = $dom->createElement('project');
// project attributes
foreach( array('name','slug') as $attr ){
if( isset($conf[$attr]) ){
$project->setAttribute( $attr, $conf[$attr] );
}
}
// project children
if( $nodes = array_intersect_key( $conf, array( 'source' => '', 'target' => '', 'template' => '' ) ) ) {
$this->loadStruct( $project, $nodes );
}
}
// add all domains and their projects
foreach( $domains as $name => $projects ){
$parent = $root->appendChild( $dom->createElement('domain') );
$parent->setAttribute( 'name', $name );
/* @var $project LocoConfigElement */
foreach( $projects as $project ){
$parent->appendChild( $project );
}
}
}
/**
* Recursively add array structure into model.
* - Text nodes are split into one parent element per line.
* - Elements added here cannot have attributes, but are not expected to as they came from form fields
*/
private function loadStruct( LocoConfigElement $parent, array $nodes ){
$dom = $this->getDom();
foreach( $nodes as $name => $data ){
if( is_string($data) ){
// support common path containing elements
if( 'file' === $name || 'directory' === $name || 'path' === $name ){
// form model has multiline "path" nodes which we'll expand from non-empty lines
// resolving empty paths to "." must be done elsewhere. here empty means ignore.
foreach( preg_split('/\\R/', trim( $data,"\n\r"), -1, PREG_SPLIT_NO_EMPTY ) as $path ){
$ext = pathinfo( $path, PATHINFO_EXTENSION );
$child = $parent->appendChild( $dom->createElement( $ext ? 'file' : 'directory' ) );
$child->appendChild( $dom->createTextNode($path) );
}
}
// else assume valud is an attribute
else {
$parent->setAttribute( $name, $data );
}
}
else if( is_bool($data) ){
$data ? $parent->setAttribute($name,'true') : $parent->removeAttribute($name);
}
else if( ! is_array($data) ){
throw new InvalidArgumentException('Invalid datatype');
}
else {
$child = $parent->appendChild( $dom->createElement($name) );
$this->loadStruct( $child, $data );
}
}
}
}
PK T\{CU
config/Model.phpnu W+A dirs = array();
$this->dom = $this->createDom();
$this->setDirectoryPath( loco_constant('ABSPATH') );
}
/**
* @return void
*/
public function setDirectoryPath( $path, $key = null ){
$path = rtrim( $path, '/' );
if( is_null($key) ){
$this->base = $path;
}
else {
$this->dirs[$key] = $path;
}
}
/**
* @return LocoConfigDocument
*/
public function getDom(){
return $this->dom;
}
/**
* Evaluate a name constant pointing to a file location
* @param string one of 'LOCO_LANG_DIR', 'WP_LANG_DIR', 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', or 'ABSPATH'
*/
public function getDirectoryPath( $key = null ){
if( is_null($key) ){
$value = $this->base;
}
else if( isset($this->dirs[$key]) ){
$value = $this->dirs[$key];
}
else {
$value = rtrim( loco_constant($key), '/' );
}
return $value;
}
/**
* @return LocoConfigElement
*/
public function createFileElement( Loco_fs_File $file ){
$node = $this->dom->createElement( $file->isDirectory() ? 'directory' : 'file' );
if( $path = $file->getPath() ) {
// Calculate relative path to the config file itself
$relpath = $file->getRelativePath( $this->base );
// Map to a configured base path if target is not under our root. This makes XML more portable
// matching order is most specific first, resulting in shortest path
if( $relpath && ( Loco_fs_File::abs($relpath) || '..' === substr($relpath,0,2) || $this->base === $this->getDirectoryPath('ABSPATH') ) ){
$bases = array( 'LOCO_LANG_DIR', 'WP_LANG_DIR', 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH' );
foreach( $bases as $key ){
if( ( $base = $this->getDirectoryPath($key) ) && $base !== $this->base ){
$base .= '/';
$len = strlen($base);
if( substr($path,0,$len) === $base ){
$node->setAttribute('base',$key);
$relpath = substr( $path, $len );
break;
}
} // @codeCoverageIgnore
}
}
$path = $relpath;
}
$this->setFileElementPath( $node, $path );
return $node;
}
/**
* @param LocoConfigElement
* @param string
* @return LocoConfigText
*/
protected function setFileElementPath( $node, $path ){
return $node->appendChild( $this->dom->createTextNode($path) );
}
/**
* @param LocoConfigElement
* @return Loco_fs_File
*/
public function evaluateFileElement( $el ){
$path = $el->textContent;
switch( $el->nodeName ){
case 'directory':
$file = new Loco_fs_Directory($path);
break;
case 'file':
$file = new Loco_fs_File($path);
break;
case 'path':
$file = new Loco_fs_File($path);
if( $file->isDirectory() ){
$file = new Loco_fs_Directory($path);
}
break;
default:
throw new InvalidArgumentException('Cannot evaluate file element from <'.$el->nodeName.'>');
}
if( $el->hasAttribute('base') ){
$key = $el->getAttribute('base');
$base = $this->getDirectoryPath($key);
$file->normalize( $base );
}
else {
$file->normalize( $this->base );
}
return $file;
}
/**
* @param LocoConfigElement
* @return bool
*/
public function evaulateBooleanAttribute( $el, $attr ){
if( ! $el->hasAttribute($attr) ){
return false;
}
$value = (string) $el->getAttribute($attr);
return 'false' !== $value && 'no' !== $value && '' !== $value;
}
}
PK T\FT
Locale.phpnu W+A setSubtags( loco_parse_locale($tag) );
}
catch( Exception $e ){
// isValid should return false
}
return $locale;
}
public function __construct( $lang = '', $region = '', $variant = '' ){
$this->tag = compact('lang','region','variant');
}
/**
* @internal
* Allow read-only access to subtags
*/
public function __get( $t ){
return isset($this->tag[$t]) ? $this->tag[$t] : '';
}
/**
* Set subtags as produced from loco_parse_locale
* @return Loco_Locale
*/
public function setSubtags( array $tag ){
$default = array( 'lang' => '', 'region' => '', 'variant' => '' );
// disallow setting of unsupported tags
if( $bad = array_diff_key($tag, $default) ){
throw new Loco_error_LocaleException('Unsupported subtags: '.implode(',',$bad) );
}
$tag += $default;
// language tag is minimum requirement
if( ! $tag['lang'] ){
throw new Loco_error_LocaleException('Locale must have a language');
}
// no UN codes in Wordpress
if( is_numeric($tag['region']) ){
throw new Loco_error_LocaleException('Numeric regions not supported');
}
// single, scalar variant. Only using for Formal german currently.
if( is_array($tag['variant']) ){
$tag['variant'] = implode('_',$tag['variant']);
}
$this->tag = $tag;
return $this;
}
/**
* @return string
*/
public function __toString(){
return implode('_',array_filter($this->tag));
}
/**
* @return string
*/
public function getName(){
return (string) $this->name;
}
/**
* @return string
*/
public function getNativeName(){
return (string) $this->_name;
}
/**
* @return string
*/
public function getIcon(){
$tag = array();
if( ! $this->tag['lang'] ){
$tag[] = 'lang lang-zxx';
}
foreach( $this->tag as $class => $code ){
if( $code ){
$tag[] = $class.' '.$class.'-'.$code;
}
}
return strtolower( implode(' ',$tag) );
}
/**
* @return Loco_Locale
*/
public function setName( $english_name, $native_name = '' ){
$this->name = $english_name;
$this->_name = $native_name;
return $this;
}
/**
* Test whether locale is valid
*/
public function isValid(){
return (bool) $this->tag['lang']; // && 'zxx' !== $this->tag['lang'];
}
/**
* @return Loco_Locale
*/
public function normalize(){
$this->tag['lang'] = strtolower($this->tag['lang']);
$this->tag['region'] = strtoupper($this->tag['region']);
$this->tag['variant'] = strtolower($this->tag['variant']);
return $this;
}
/**
* Resolve this locale's "official" name from WordPress's translation api
* @return string English name currently set
*/
public function fetchName( Loco_api_WordPressTranslations $api ){
$tag = $this->normalize()->__toString();
if( $raw = $api->getLocaleData($tag) ){
$this->setName( $raw['english_name'], $raw['native_name'] );
}
return $this->name;
}
/**
* Resolve this locale's name from compiled Loco data
* @return string English name currently set
*/
public function buildName(){
$names = array();
// should at least have a language or not valid
if( $this->isValid() ){
$code = $this->tag['lang'];
$db = Loco_data_CompiledData::get('languages');
if( $name = $db[$code] ){
// if variant is present add only that in brackets (no lookup required)
if( $code = $this->tag['variant'] ){
$name .= ' ('.ucfirst($code).')';
}
// else add region in brackets if present
else if( $code = $this->tag['region'] ){
$db = Loco_data_CompiledData::get('regions');
if( $extra = $db[$code] ){
$name .= ' ('.$extra.')';
}
else {
$name .= ' ('.$code.')';
}
}
$this->setName( $name );
}
}
else {
$this->name = __('Invalid locale','loco');
}
return $this->name;
}
/**
* @return array
*/
public function jsonSerialize(){
$a = $this->tag;
$a['label'] = $this->name;
// plural data expected by editor
$p = $this->getPluralData();
$a['pluraleq'] = $p[0];
$a['plurals'] = $p[1];
$a['nplurals'] = count($p[1]);
return $a;
}
/**
* Get raw plural data
* @internal
* @return array
*/
public function getPluralData(){
if( ! $this->plurals ){
$db = Loco_data_CompiledData::get('plurals');
$lc = $this->lang;
$id = isset($db[$lc]) ? $db[$lc] : 0;
$this->plurals = $db[''][$id];
}
return $this->plurals;
}
/**
* Get PO style Plural-Forms header value comprising number of forms and integer equation for n
* @return string
*/
public function getPluralFormsHeader(){
list( $equation, $forms ) = $this->getPluralData();
return sprintf('nplurals=%u; plural=%s;', count($forms), $equation );
}
/**
* @return string
*/
public function exportJson(){
return json_encode( $this->jsonSerialize() );
}
}
// Depends on compiled library
if( ! function_exists('loco_parse_locale') ){
loco_include('lib/compiled/locales.php');
}
PK T\n{ { ajax/common/BundleController.phpnu W+A get('bundle') ){
// type may be passed as separate argument
if( $type = $this->get('type') ){
return Loco_package_Bundle::createType( $type, $id );
}
// else embedded in standalone bundle identifier
// TODO standardize this across all Ajax end points
return Loco_package_Bundle::fromId($id);
}
// else may have type embedded in bundle
throw new Loco_error_Exception('No bundle identifier posted');
}
/**
* @return Loco_package_Project
*/
protected function getProject( Loco_package_Bundle $bundle ){
$project = $bundle->getProjectById( $this->get('domain') );
if( ! $project ){
throw new Loco_error_Exception('Failed to find translation project');
}
return $project;
}
}PK T\.N N ajax/MsginitController.phpnu W+A get('use-selector') ){
$tag = $this->get('select-locale');
}
else {
$tag = $this->get('custom-locale');
}
$locale = Loco_Locale::parse($tag);
if( ! $locale->isValid() ){
throw new Loco_error_LocaleException('Invalid locale');
}
return $locale;
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
$locale = $this->getLocale();
$suffix = (string) $locale;
// The front end posts a template path, so we must replace the actual locale code
$base = loco_constant('WP_CONTENT_DIR');
$path = $post->path[ $post['select-path'] ];
$pofile = new Loco_fs_LocaleFile( $path );
if( $suffix !== $pofile->getSuffix() ){
$pofile = $pofile->cloneLocale( $locale );
if( $suffix !== $pofile->getSuffix() ){
throw new Loco_error_Exception('Failed to suffix file path with locale code');
}
}
// target PO should not exist yet
$pofile->normalize( $base );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate( $pofile );
// target MO shouldn't exist either, but we don't want to overwrite it without asking
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
throw new Loco_error_Exception( __('MO file exists for this language already. Delete it first','loco') );
}
// Permit forcing of any parsable file as strings template
if( $source = $post->source ){
$potfile = new Loco_fs_File( $source );
$potfile->normalize( $base );
$data = Loco_gettext_Data::load($potfile);
// Remove target strings when copying PO
if( $post->strip ){
$data->strip();
}
}
// else parse POT file if project defines one that exists
else if( ( $potfile = $project->getPot() ) && $potfile->exists() ){
$data = Loco_gettext_Data::load($potfile);
}
// else extract directly from source code, assuming domain passed though from front end
else {
$extr = new Loco_gettext_Extraction( $bundle );
$domain = (string) $project->getDomain();
$data = $extr->addProject($project)->includeMeta()->getTemplate($domain);
$potfile = null;
}
// Let template define Project-Id-Version, else set header to current project name
$headers = array();
$vers = $data->getHeaders()->{'Project-Id-Version'};
if( ! $vers || 'PACKAGE VERSION' === $vers ){
$headers['Project-Id-Version'] = $project->getName();
}
// relative path from bundle root to the template/source this file was created from
if( $potfile && $post->link ){
$headers['X-Loco-Template'] = $potfile->getRelativePath( $bundle->getDirectoryPath() );
}
$data->localize( $locale, $headers );
$posize = $pofile->putContents( (string) $data );
$mosize = $mofile->putContents( $data->msgfmt() );
// set debug response data
$this->set( 'debug', array (
'poname' => $pofile->basename(),
'posize' => $posize,
'mosize' => $mosize,
'source' => $potfile ? $potfile->basename() : '',
) );
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// front end will redirect to the editor
$type = strtolower( $this->get('type') );
$this->set( 'redirect', Loco_mvc_AdminRouter::generate( sprintf('%s-file-edit',$type), array (
'path' => $pofile->getRelativePath($base),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
) ) );
return parent::render();
}
}PK T\+v ajax/DownloadConfController.phpnu W+A validate();
$bundle = $this->getBundle();
$file = new Loco_fs_File( $this->get('path') );
// TODO should we download axtual loco.xml file if bundle is configured from it?
//$file->normalize( $bundle->getDirectoryPath() );
//if( $file->exists() ){}
$writer = new Loco_config_BundleWriter($bundle);
switch( $file->extension() ){
case 'xml':
return $writer->toXml();
case 'json':
return json_encode( $writer->jsonSerialize() );
}
// @codeCoverageIgnoreStart
throw new Loco_error_Exception('Specify either XML or JSON file path');
}
}PK T\K ajax/SyncController.phpnu W+A validate();
$bundle = Loco_package_Bundle::fromId( $post->bundle );
$project = $bundle->getProjectById( $post->domain );
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize( $base );
// POT file always synced with source code (even if a PO being used as POT)
if( 'pot' === $post->type ){
$potfile = null;
}
// allow post data to force a template file path
else if( $path = $post->sync ){
$potfile = new Loco_fs_File($path);
$potfile->normalize( $base );
}
// else use project-configured template if one is defined
else {
$potfile = $project->getPot();
}
// sync with POT if it exists
if( $potfile && $potfile->exists() ){
$this->set('pot', $potfile->basename() );
$data = Loco_gettext_Data::load($potfile);
}
// else sync with source code
else {
$this->set('pot', '' );
$domain = (string) $project->getDomain();
$extr = new Loco_gettext_Extraction($bundle);
$data = $extr->addProject($project)->includeMeta()->getTemplate($domain);
}
$this->set( 'po', $data->jsonSerialize() );
return parent::render();
}
}PK T\#F ajax/SaveController.phpnu W+A validate();
// path parameter must not be empty
$path = $post->path;
if( ! $path ){
throw new InvalidArgumentException('Path parameter required');
}
// locale must be posted to indicate whether PO or POT
$locale = $post->locale;
if( is_null($locale) ){
throw new InvalidArgumentException('Locale parameter required');
}
$pofile = new Loco_fs_LocaleFile( $path );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
// ensure we only deal with PO/POT source files.
// posting of MO file paths is permitted when PO is missing, but we're about to fix that
$ext = $pofile->extension();
if( 'mo' === $ext ){
$pofile = $pofile->cloneExtension('po');
}
else if( 'pot' === $ext ){
$locale = '';
}
else if( 'po' !== $ext ){
throw new Loco_error_Exception('Invalid file path');
}
// force the use of remote file system when configured from front end
if( $post->has('connection_type') ){
$api = new Loco_api_WordPressFileSystem;
$api->authorizeConnect( $pofile );
}
// data posted must be valid
$data = Loco_gettext_Data::fromSource( $post->data );
// backup existing file BEFORE overwriting
// file system write context will carry through when revisions clone pofile
if( $num_backups = Loco_data_Settings::get()->num_backups ){
try {
$backups = new Loco_fs_Revisions( $pofile );
$backups->create();
$backups->prune($num_backups);
}
catch( Exception $e ){
$message = __('Failed to create backup file in "%s". Check file permissions or disable backups','loco');
Loco_error_AdminNotices::info( sprintf( $message, $pofile->getParent()->basename() ) );
}
}
// commit file directly to disk
$bytes = $pofile->putContents( $data );
$mtime = $pofile->modified();
// push recent items on file creation
try {
$bundle = $this->getBundle();
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// TODO push project and locale file
}
catch( Exception $e ){
// editor permitted to save files not in a bundle, so catching failures
}
// start success data with bytes written and timestamp
$this->set('locale', $locale );
$this->set('pobytes', $bytes );
$this->set('poname', $pofile->basename() );
$this->set('modified', $mtime);
$this->set('datetime', Loco_mvc_ViewParams::date_i18n($mtime) );
// Intial message refers to PO/POT save success
$success = $locale ? __('PO file saved','loco') : __('POT file saved','loco');
// Compile MO file unless saving template
if( $locale ){
try {
$data = $data->msgfmt();
$mofile = $pofile->cloneExtension('mo');
$bytes = $mofile->putContents( $data );
$this->set( 'mobytes', $bytes );
Loco_error_AdminNotices::success( __('PO file saved and MO file compiled','loco') );
}
catch( Exception $e ){
Loco_error_AdminNotices::add( $e );
Loco_error_AdminNotices::info( __('PO file saved, but MO file compilation failed','loco') );
$this->set( 'mobytes', 0 );
}
}
else {
Loco_error_AdminNotices::success( __('POT file saved','loco') );
}
return parent::render();
}
}PK T\XϤ; ajax/FsConnectController.phpnu W+A validate();
$api = new Loco_api_WordPressFileSystem;
$func = 'authorize'.ucfirst($post->auth);
$auth = array( $api, $func );
if( ! is_callable($auth) ){
throw new Loco_error_Exception('Unexpected file operation');
}
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize($base);
try {
$file->getWriteContext()->authorize();
//
if( call_user_func( $auth, $file ) ){
$this->set( 'authed', true );
$this->set( 'valid', $api->getOutputCredentials() );
$this->set( 'creds', $api->getInputCredentials() );
$this->set( 'method', $api->getFileSystem()->method );
$this->set( 'success', __('Connected to remote file system','loco') );
}
else {
$this->set( 'authed', false );
$this->set( 'prompt', $api->getForm() );
}
}
catch( Loco_error_WriteException $e ){
$this->set('authed', false );
$this->set('reason', $e->getMessage() );
}
return parent::render();
}
}PK T\DҦ2 2 ajax/PingController.phpnu W+A set( 'ping', 'pong' );
return parent::render();
}
}PK T\!1Ů ajax/DownloadController.phpnu W+A validate();
// we need a path, but it may not need to exist
$file = new Loco_fs_File( $this->get('path') );
$file->normalize( loco_constant( 'WP_CONTENT_DIR') );
$is_binary = 'mo' === strtolower( $file->extension() );
// posted source must be clean and must parse as whatever the file extension claims to be
if( $raw = $post->source ){
// compile source if imaginary target is MO
if( $is_binary ) {
$po = new Loco_gettext_Data( loco_parse_po($raw) );
$raw = $po->msgfmt();
}
}
// else file can be output directly if it exists
else if( $file->exists() ){
$raw = $file->getContents();
$do_compile = false;
}
/*/ else if PO exists but MO doesn't, we can compile it on the fly
else if( ! $is_binary ){
}*/
else {
throw new Loco_error_Exception('File not found and no source posted');
}
return $raw;
}
}PK T\E ajax/XgettextController.phpnu W+A validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
// target location may not be next to POT file at all
$base = loco_constant('WP_CONTENT_DIR');
$target = new Loco_fs_Directory( $this->get('path') );
$target->normalize( $base );
if( $target->exists() && ! $target->isDirectory() ){
throw new Loco_error_Exception('Target is not a directory');
}
// basename should be posted from front end
$name = $this->get('name');
if( ! $name ){
throw new Loco_error_Exception('Front end did not post $name');
}
// POT file shouldn't exist currently
$potfile = new Loco_fs_File( $target.'/'.$name );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate($potfile);
// Do extraction and grab only given domain's strings
$ext = new Loco_gettext_Extraction( $bundle );
$domain = $project->getDomain()->getName();
$data = $ext->addProject($project)->includeMeta()->getTemplate( $domain );
// additional headers to set in new POT file
$headers = array (
'Project-Id-Version' => $project->getName(),
);
$potsize = $potfile->putContents( (string) $data );
// set response data for debugging
if( loco_debugging() ){
$this->set( 'debug', array (
'potname' => $potfile->basename(),
'potsize' => $potsize,
'total' => $ext->getTotal(),
) );
}
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// put flash message into session to be displayed on redirected page
try {
Loco_data_Session::get()->flash('success', __('Template file created','loco') );
Loco_data_Session::close();
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// redirect front end to bundle view. Discourages manual editing of template
$type = strtolower( $bundle->getType() );
$href = Loco_mvc_AdminRouter::generate( sprintf('%s-view',$type), array(
'bundle' => $bundle->getHandle(),
) );
$hash = '#loco-'.$project->getId();
$this->set( 'redirect', $href.$hash );
return parent::render();
}
}PK T\~;R R ajax/FsReferenceController.phpnu W+A exists() ){
return $srcfile;
}
}*/
// reference may be resolvable via referencing PO file's location
$pofile = new Loco_fs_File( $this->get('path') );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
if( ! $pofile->exists() ){
throw new InvalidArgumentException('PO/POT file required to resolve reference');
}
$search = new Loco_gettext_SearchPaths;
$search->init($pofile);
if( $srcfile = $search->match($refpath) ){
return $srcfile;
}
// reference may be resolvable via known project roots
try {
$bundle = $this->getBundle();
// Loco extractions will always be relative to bundle root
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $bundle->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
// check relative to parent theme root
if( $bundle->isTheme() && ( $parent = $bundle->getParentTheme() ) ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $parent->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
// final attempt - search all project source roots
// TODO is there too large a risk of false positives? especially with files like index.php
/* @var $root Loco_fs_Directory */
/*foreach( $this->getProject($bundle)->getConfiguredSources() as $root ){
if( $root->isDirectory() ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $root->getPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
}*/
}
catch( Loco_error_Exception $e ){
// permitted for there to be no bundle or project when viewing orphaned file
}
throw new Loco_error_Exception( sprintf('Failed to find source file matching "%s"',$refpath) );
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// at the very least we need a reference to examine
if( ! $post->has('ref') ){
throw new InvalidArgumentException('ref parameter required');
}
// reference must parse as :
$ref = $post->ref;
if( ! preg_match('/^(.+):(\d+)$/', $ref, $r ) ){
throw new InvalidArgumentException('Invalid file reference, '.$ref );
}
// find file or fail
list( , $refpath, $refline ) = $r;
$srcfile = $this->findSourceFile($refpath);
$type = strtolower( $srcfile->extension() );
$this->set('type', $type );
$this->set('line', (int) $refline );
$this->set('path', $srcfile->getRelativePath( loco_constant('WP_CONTENT_DIR') ) );
// source code will be HTML-tokenized into multiple lines
$code = array();
// PHP is the most likely format. highlighting on back end because tokenizer provides more control than highlight.js
if( 'php' === $type ) {
$thisline = 1;
foreach( token_get_all( $srcfile->getContents() ) as $tok ){
if( is_array($tok) ){
// line numbers added in PHP 5.2.2 - WordPress minimum is 5.2.4
list( $t, $str, $startline ) = $tok;
$clss = token_name($t);
// tokens can span multiple lines (whitespace/html/comments)
$lines = preg_split('/\\R/', $str );
}
else {
// scalar symbol will always start on the line that the previous token ended on
$clss = 'T_NONE';
$lines = array( $tok );
$startline = $thisline;
}
// token can span multiple lines, so include only bytes on required line[s]
foreach( $lines as $i => $line ){
$thisline = $startline + $i;
$html = ''.htmlentities($line,ENT_COMPAT,'UTF-8').'';
// append highlighted token to current line
$j = $thisline - 1;
if( isset($code[$j]) ){
$code[$j] .= $html;
}
else {
$code[$j] = $html;
}
}
}
}
/*/ TODO permit limited other file types, but without back end highlighting
else if( ){
foreach( preg_split( '/\\R/u', $srcfile->getContents() ) as $line ){
$code[] = ''.htmlentities($line,ENT_COMPAT,'UTF-8').'';
}
}*/
else {
throw new Loco_error_Exception( sprintf('%s source view not supported', $type) );
}
if( ! isset($code[$refline-1]) ){
throw new Loco_error_Exception( sprintf('Line %u not in source file', $refline) );
}
$this->set( 'code', $code );
return parent::render();
}
}
PK T\t5P P admin/bundle/SetupController.phpnu W+A getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Set up %s','loco'),$bundle->getName() ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Setup tab','loco') => $this->view('tab-bundle-setup'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle setup','loco') );
$bundle = $this->getBundle();
$action = 'setup:'.$bundle->getId();
// execute auto-configure if posted
$post = Loco_mvc_PostParams::get();
if( $post->has('auto-setup') && $this->checkNonce( 'auto-'.$action) ){
if( 0 === count($bundle) ){
$bundle->createDefault();
}
foreach( $bundle as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
}
// forcefully add every additional project into bundle
foreach( $bundle->invert() as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
$bundle[] = $project;
}
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('auto', null );
}
// execute XML-based config if posted
else if( $post->has('xml-setup') && $this->checkNonce( 'xml-'.$action) ){
$bundle->clear();
$model = new Loco_config_XMLModel;
$model->loadXml( trim( $post['xml-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('xml', null );
}
// execute JSON-based config if posted
else if( $post->has('json-setup') && $this->checkNonce( 'json-'.$action) ){
$bundle->clear();
$model = new Loco_config_ArrayModel;
$model->loadJson( trim( $post['json-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('json', null );
}
// execute reset if posted
else if( $post->has('reset-setup') && $this->checkNonce( 'reset-'.$action) ){
$this->resetBundle();
$bundle = $this->getBundle();
}
// bundle author links
$info = $bundle->getHeaderInfo();
$this->set( 'credit', $info->getAuthorCredit() );
// render according to current configuration method (save type)
$configured = $this->get('force') or $configured = $bundle->isConfigured();
$notices = new ArrayIterator;
$this->set('notices', $notices );
// collect configuration warnings
foreach( $bundle as $project ){
$potfile = $project->getPot();
if( ! $potfile ){
$notices[] = sprintf('No translation template for the "%s" text domain', $project->getSlug() );
}
}
// if extra files found consider incomplete
if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
$unknown = Loco_package_Inverter::export($bundle);
$n = 0;
foreach( $unknown as $ext => $files ){
$n += count($files);
}
if( $n ){
$notices[] = sprintf( _n("One file can't be matched to a known set of strings","%s files can't be matched to a known set of strings",$n,'loco'), number_format($n) );
}
}
// display setup options if at least one option specified
$doconf = false;
// enable form to invoke auto-configuration
if( $this->get('auto') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'auto-'.$action );
$this->set('autoFields', $fields );
$doconf = true;
}
// enable form to paste XML config
if( $this->get('xml') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'xml-'.$action );
$this->set('xmlFields', $fields );
$doconf = true;
}
// enable form to paste JSON config (via remote lookup)
if( $this->get('json') ){
$fields = new Loco_mvc_HiddenFields( array(
'json-content' => '',
'version' => $info->Version,
) );
$fields->setNonce( 'json-'.$action );
$this->set('jsonFields', $fields );
// other information for looking up bundle via api
$this->set('vendorSlug', $bundle->getSlug() );
// remote config is done via JavaScript
$this->enqueueScript('setup');
$apiBase = apply_filters( 'loco_api_url', 'https://localise.biz/api' );
$this->set('js', new Loco_mvc_ViewParams( array(
'apiUrl' => $apiBase.'/wp/'.strtolower( $bundle->getType() ),
) ) );
$doconf = true;
}
// display configurator if configurating
if( $doconf ){
return $this->view( 'admin/bundle/setup/conf' );
}
// else set configurator links back to self with required option
// ...
if( ! $configured || ! count($bundle) ){
return $this->view( 'admin/bundle/setup/none' );
}
if( 'db' === $configured ){
// form for resetting config
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'reset-'.$action );
$this->set( 'reset', $fields );
return $this->view('admin/bundle/setup/saved');
}
if( 'internal' === $configured ){
return $this->view('admin/bundle/setup/core');
}
if( 'file' === $configured ){
return $this->view('admin/bundle/setup/author');
}
if( count($notices) ){
return $this->view('admin/bundle/setup/partial');
}
return $this->view('admin/bundle/setup/meta');
}
}PK T\, admin/bundle/DebugController.phpnu W+A getBundle();
$this->set('title', 'Debug: '.$bundle );
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle diagnostics','loco') );
$bundle = $this->getBundle();
$debugger = new Loco_package_Debugger($bundle);
$this->set('notices', $notices = new Loco_mvc_ViewParams );
/* @var $notice Loco_error_Exception */
foreach( $debugger as $notice ){
$notices[] = new Loco_mvc_ViewParams( array(
'style' => 'notice inline notice-'.$notice->getType(),
'title' => $notice->getTitle(),
'body' => $notice->getMessage(),
) );
}
$meta = $bundle->getHeaderInfo();
$this->set('meta', new Loco_mvc_ViewParams( array(
'vendor' => $meta->getVendorHost(),
'author' => $meta->getAuthorCredit(),
) ) );
if( count($bundle) ){
$writer = new Loco_config_BundleWriter( $bundle );
$this->set( 'xml', $writer->toXml() );
}
return $this->view('admin/bundle/debug');
}
}PK T\QU. . admin/bundle/ViewController.phpnu W+A getBundle();
$this->set('title', $bundle->getName() );
$this->enqueueStyle('bundle');
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-bundle-view'),
);
}
/**
* Generate a link for a specific file resource within a project
* @return string
*/
private function getResourceLink( $page, Loco_package_Project $project, Loco_gettext_Metadata $meta, array $args = array() ){
$args['path'] = $meta->getPath(false);
return $this->getProjectLink( $page, $project, $args );
}
/**
* Generate a link for a project, but without being for a specific file
* @return string
*/
private function getProjectLink( $page, Loco_package_Project $project, array $args = array() ){
$args['bundle'] = $this->get('bundle');
$args['domain'] = $project->getId();
return $this->getLink( $page, $args );
}
/**
* Generate a link for the same type of bundle as this one
* @return string
*/
private function getLink( $page, array $args ){
$route = strtolower( $this->get('type') ).'-'.$page;
return Loco_mvc_AdminRouter::generate( $route, $args );
}
/**
* Initialize view parameters for a project
* @return Loco_mvc_ViewParams
*/
private function createProjectParams( Loco_package_Project $project ){
$name = $project->getName();
$domain = $project->getDomain()->getName();
$slug = $project->getSlug();
$p = new Loco_mvc_ViewParams( array (
'id' => $project->getId(),
'name' => $name,
'slug' => $slug,
'domain' => $domain,
'short' => ! $slug || $project->isDomainDefault() ? $domain : $domain.'→'.$slug,
) );
// POT template file
$file = $project->getPot();
if( $file && $file->exists() ){
$meta = Loco_gettext_Metadata::load($file)->persistIfDirty( 0, true );
$p['pot'] = new Loco_mvc_ViewParams( array(
// POT info
'name' => $file->basename(),
'time' => $file->modified(),
// POT links
'info' => $this->getResourceLink('file-info', $project, $meta ),
'edit' => $this->getResourceLink('file-edit', $project, $meta ),
) );
}
// PO/MO files
$po = $project->findLocaleFiles('po');
$mo = $project->findLocaleFiles('mo');
$p['po'] = $this->createProjectPairs( $project, $po, $mo );
// also pull invalid files so everything is available to the UI
$mo = $project->findNotLocaleFiles('mo');
$po = $project->findNotLocaleFiles('po')->augment( $project->findNotLocaleFiles('pot') );
$p['_po'] = $this->createProjectPairs( $project, $po, $mo );
// always offer msginit even if we find out later we can't extract any strings
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('msginit', $project ),
'name' => __('New language','loco'),
'icon' => 'add',
) );
// offer template editing if permitted
if( ! $project->isPotLocked() ){
$pot = $project->getPot();
if( $pot && $pot->exists() ){
$p['pot'] = $pot;
$meta = Loco_gettext_Metadata::load($pot)->persistIfDirty( 0, true );
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getResourceLink('file-edit', $project, $meta ),
'name' => __('Edit template','loco'),
'icon' => 'pencil',
) );
}
// else offer creation of new Template
else {
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('xgettext', $project ),
'name' => __('Create template','loco'),
'icon' => 'add',
) );
}
}
return $p;
}
/**
* Collect PO/MO pairings, ignoring any PO that is in use as a template
*/
private function createPairs( Loco_fs_FileList $po, Loco_fs_FileList $mo, Loco_fs_File $pot = null ){
$pairs = array();
/* @var $pofile Loco_fs_LocaleFile */
foreach( $po as $pofile ){
if( $pot && $pofile->equal($pot) ){
continue;
}
$pair = array( $pofile, null );
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
$pair[1] = $mofile;
}
$pairs[] = $pair;
}
/* @var $mofile Loco_fs_LocaleFile */
foreach( $mo as $mofile ){
$pofile = $mofile->cloneExtension('po');
if( $pot && $pofile->equal($pot) ){
continue;
}
if( ! $pofile->exists() ){
$pairs[] = array( null, $mofile );
}
}
return $pairs;
}
/**
* Initialize view parameters for each row representing a localized resource pair
* @return array collection of entries corresponding to available PO/MO pair.
*/
private function createProjectPairs( Loco_package_Project $project, Loco_fs_LocaleFileList $po, Loco_fs_LocaleFileList $mo ){
// populate official locale names for all found, or default to our own
if( $locales = $po->getLocales() + $mo->getLocales() ){
$api = new Loco_api_WordPressTranslations;
/* @var $locale Loco_Locale */
foreach( $locales as $tag => $locale ){
$locale->fetchName($api) or $locale->buildName() or $locale->setName($tag);
}
}
// collate as unique [PO,MO] pairs ensuring canonical template excluded
$pairs = $this->createPairs( $po, $mo, $project->getPot() );
$rows = array();
foreach( $pairs as $pair ){
// favour PO file if it exists
list( $pofile, $mofile ) = $pair;
$file = $pofile or $file = $mofile;
// establish locale, or assume invalid
$locale = null;
if( 'pot' !== $file->extension() ){
$tag = $file->getSuffix();
if( isset($locales[$tag]) ){
$locale = $locales[$tag];
}
}
$rows[] = $this->createFileParams( $project, $file, $locale );
}
return $rows;
}
/**
*
*/
private function createFileParams( Loco_package_Project $project, Loco_fs_File $file, Loco_Locale $locale = null ){
// Pull Gettext meta data from cache if possible
// TODO save write when cached version was used
$meta = Loco_gettext_Metadata::load($file)->persistIfDirty( 0, true );
// Establish whether translations are official or otherwise
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
// Retuen data required for PO table row
return new Loco_mvc_ViewParams( array (
// locale info
'lcode' => $locale ? (string) $locale : '',
'lname' => $locale ? $locale->getName() : '',
'lattr' => $locale ? 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"' : '',
// file info
'meta' => $meta,
'name' => $file->basename(),
'time' => $file->modified(),
'todo' => $meta->countIncomplete(),
'total' => $meta->getTotal(),
// author / contrib
'store' => $dir->getTypeLabel( $dir->getTypeId() ),
// links
'info' => $this->getResourceLink('file-info', $project, $meta ),
'edit' => $this->getResourceLink('file-edit', $project, $meta ),
'delete' => $this->getResourceLink('file-delete', $project, $meta ),
'copy' => $this->getProjectLink('msginit', $project, array( 'source' => $meta->getPath(false) ) ),
) );
}
/**
* Prepare view parameters for all projects in a bundle
* @return array
*/
private function createBundleListing( Loco_package_Bundle $bundle ){
$projects = array();
/* @var $project Loco_package_Project */
foreach( $bundle as $project ){
$projects[] = $this->createProjectParams($project);
}
return $projects;
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation();
$bundle = $this->getBundle();
$this->set('name', $bundle->getName() );
// bundle may not be fully configured
$configured = $bundle->isConfigured();
// Hello Dolly is an exception. don't show unless configured deliberately
if( ! $configured && 'hello.php' === $bundle->getHandle() && 'Hello Dolly' === $bundle->getName() ){
$this->set( 'redirect', Loco_mvc_AdminRouter::generate('core-view') );
return $this->view('admin/bundle/alias');
}
// Collect all configured projects
$projects = $this->createBundleListing( $bundle );
$unknown = array();
// sniff additional unknown files if bundle is a theme or directory-based plugin that's been auto-detected
if( 'file' === $configured || 'internal' === $configured ){
// presumed complete
}
else if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
// TODO This needs absracting into the Loco_package_Inverter class
$prefixes = array();
$po = new Loco_fs_LocaleFileList;
$mo = new Loco_fs_LocaleFileList;
foreach( Loco_package_Inverter::export($bundle) as $ext => $files ){
$list = 'mo' === $ext ? $mo : $po;
foreach( $files as $file ){
$file = new Loco_fs_LocaleFile($file);
$list->addLocalized( $file );
// Only look in system locations if locale is valid and domain/prefix available
$locale = $file->getLocale();
if( $locale->isValid() && ( $domain = $file->getPrefix() ) ){
$prefixes[$domain] = true;
}
}
}
// pick up given files in system locations only
foreach( $prefixes as $domain => $_bool ){
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain($domain), '' );
$bundle->addProject( $dummy ); // <- required to configure locations
$dummy->excludeTargetPath( $bundle->getDirectoryPath() );
$po->augment( $dummy->findLocaleFiles('po') );
$mo->augment( $dummy->findLocaleFiles('mo') );
}
// a fake project is required to disable functions that require a configured project
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain(''), '' );
$unknown = $this->createProjectPairs( $dummy, $po, $mo );
}
$this->set('projects', $projects );
$this->set('unknown', $unknown );
return $this->view( 'admin/bundle/view' );
}
}PK T\j0 admin/bundle/ConfController.phpnu W+A enqueueStyle('config');
$this->enqueueScript('config');
$bundle = $this->getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Configure %s','loco'),$bundle->getName() ) );
$post = Loco_mvc_PostParams::get();
// always set a nonce for current bundle
$nonce = $this->setNonce( $this->get('_route').'-'.$this->get('bundle') );
$this->set('nonce', $nonce );
try {
// Save configuration if posted
if( $post->has('conf') ){
if( ! $post->name ){
$post->name = $bundle->getName();
}
$this->checkNonce( $nonce->action );
$model = new Loco_config_FormModel;
$model->loadForm( $post );
// configure bundle from model in full
$bundle->clear();
$reader = new Loco_config_BundleReader( $bundle );
$reader->loadModel( $model );
$this->saveBundle();
}
// Delete configuration if posted
else if( $post->has('unconf') ){
$this->resetBundle();
}
}
catch( Exception $e ){
Loco_error_AdminNotices::warn( $e->getMessage() );
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Advanced tab','loco') => $this->view('tab-bundle-conf'),
);
}
/**
* {@inheritdoc}
*/
public function render() {
$bundle = $this->getBundle();
$base = $bundle->getDirectoryPath();
// parent themes are inherited into bundle, we don't want them in the child theme config
if( $bundle->isTheme() && ( $parent = $bundle->getParentTheme() ) ){
$this->set( 'parent', new Loco_mvc_ViewParams( array(
'name' => $parent->getName(),
'href' => Loco_mvc_AdminRouter::generate('theme-conf', array( 'bundle' => $parent->getSlug() ) + $_GET ),
) ) );
}
// render postdata straight back to form if sent
$data = Loco_mvc_PostParams::get();
// else build initial data from current bundle state
if( ! $data->has('conf') ){
if( 0 === count($bundle) ){
$bundle->createDefault('');
}
$writer = new Loco_config_BundleWriter($bundle);
$data = $writer->toForm();
// removed parent bundle configs, as they are inherited
/* @var Loco_package_Project $project */
foreach( $bundle as $i => $project ){
if( isset($parent) && $parent->hasProject($project) ){
$data['conf'][$i]['removed'] = true;
}
}
}
// build config blocks for form
$i = 0;
$conf = array();
foreach( $data['conf'] as $raw ){
if( empty($raw['removed']) ){
$slug = $raw['slug'];
$domain = $raw['domain'] or $domain = 'untitled';
$raw['prefix'] = sprintf('conf[%u]', $i++ );
$raw['short'] = ! $slug || ( $slug === $domain ) ? $domain : $domain.'→'.$slug;
$conf[] = new Loco_mvc_ViewParams( $raw );
}
}
// bundle level configs
$name = $bundle->getName();
$excl = $data['exclude'];
// access to type of configuration that's currently saved
$this->set('saved', $bundle->isConfigured() );
// link to author if there are config problems
$info = $bundle->getHeaderInfo();
$this->set('author', $info->getAuthorLink() );
// link for downloading current configuration XML file
$args = array (
'path' => 'loco.xml',
'action' => 'loco_download',
'bundle' => $bundle->getHandle(),
'type' => $bundle->getType()
);
$this->set( 'xmlUrl', Loco_mvc_AjaxRouter::generate( 'DownloadConf', $args ) );
$this->prepareNavigation()->add( __('Advanced configuration','loco') );
return $this->view('admin/bundle/conf', compact('conf','base','name','excl') );
}
} PK T\j6, , admin/bundle/BaseController.phpnu W+A bundle ){
$type = $this->get('type');
$handle = $this->get('bundle');
$this->bundle = Loco_package_Bundle::createType( $type, $handle );
}
return $this->bundle;
}
/**
* Commit bundle config to database
* @return Loco_admin_bundle_BaseController
*/
protected function saveBundle(){
$custom = new Loco_config_CustomSaved;
if( $custom->setBundle($this->bundle)->persist() ){
Loco_error_AdminNotices::success( __('Configuration saved','loco') );
}
// invalidate bundle in memory so next fetch is re-configured from DB
$this->bundle = null;
return $this;
}
/**
* Remove bundle config from database
* @return Loco_admin_bundle_BaseController
*/
protected function resetBundle(){
$option = $this->bundle->getCustomConfig();
if( $option && $option->remove() ){
Loco_error_AdminNotices::success( __('Configuration reset','loco') );
// invalidate bundle in memory so next fetch falls back to auto-config
$this->bundle = null;
}
return $this;
}
/**
* @return Loco_package_Project
*/
public function getProject(){
if( ! $this->project ){
$bundle = $this->getBundle();
$domain = $this->get('domain');
if( ! $domain ){
throw new Loco_error_Exception( sprintf('Translation set not known in %s', $bundle ) );
}
$this->project = $bundle->getProjectById($domain);
if( ! $this->project ){
throw new Loco_error_Exception( sprintf('Unknown translation set: %s not in %s', json_encode($domain), $bundle ) );
}
}
return $this->project;
}
/**
* @return Loco_admin_Navigation
*/
protected function prepareNavigation(){
$bundle = $this->getBundle();
// navigate up to bundle listing page
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between bundle view siblings
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'view' => __('Overview','loco'),
'setup' => __('Setup','loco'),
'conf' => __('Advanced','loco'),
);
if( loco_debugging() ){
$actions['debug'] = __('Debug','loco');
}
$suffix = $this->get('action');
$prefix = strtolower( $this->get('type') );
$getarg = array_intersect_key( $_GET, array('bundle'=>'') );
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $getarg );
$tabs->add( $name, $href, $action === $suffix );
}
return $breadcrumb;
}
/**
* Prepare file system connect
* @return Loco_mvc_HiddenFields
*/
protected function prepareFsConnect( $type, $path ){
$fields = new Loco_mvc_HiddenFields( array(
'auth' => $type,
'path' => $path,
'loco-nonce' => wp_create_nonce('fsConnect'),
) );
$this->set('fsFields', $fields );
// may have fs credentials saved in session
try {
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
$fields['connection_type'] = $session['loco-fs']['connection_type'];
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
return $fields;
}
}PK T\CAA A admin/RedirectController.phpnu W+A getLocation();
if( $location && wp_redirect($location) ){
// @codeCoverageIgnoreStart
exit;
}
}
/**
* @internal
*/
public function render(){
return 'Failed to redirect';
}
}PK T\^ۻ " admin/config/VersionController.phpnu W+A set( 'title', __('Version','loco') );
// handle legacy degradation
$nonce = $this->setNonce('downgrade');
try {
if( $this->checkNonce($nonce->action) ){
update_option('loco-branch', '1', true );
$legacy = add_query_arg( array('page'=>'loco-translate'), admin_url('admin.php') );
wp_redirect( $legacy );
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
// current plugin version
$version = loco_plugin_version();
// check for auto-update availabilty
if( $updates = get_site_transient('update_plugins') ){
$key = loco_plugin_self();
if( isset($updates->checked[$key]) && isset($updates->response[$key]) ){
$old = $updates->checked[$key];
$new = $updates->response[$key]->new_version;
$diff = version_compare( $new, $old );
if( 1 === $diff ){
// current version is lower than latest
$this->setUpdate( $new );
}
/*else {
// current version is a future release (dev branch probably)
}*/
}
}
// $this->setUpdate('2.0.1-debug');
return $this->view('admin/config/version', compact('breadcrumb','version') );
}
/**
* @internal
*/
private function setUpdate( $version ){
$action = 'upgrade-plugin_'.loco_plugin_self();
$link = admin_url( 'update.php?action=upgrade-plugin&plugin='.rawurlencode(loco_plugin_self()) );
$this->set('update', $version );
$this->set('update_href', wp_nonce_url( $link, $action ) );
}
}PK T\ admin/config/PrefsController.phpnu W+A set( 'title', __('User options','loco') );
// user preference options
$opts = Loco_data_Preferences::get();
$this->set( 'opts', $opts );
// default value for Last-Translator credit
$user = wp_get_current_user();
$name = $user->get('display_name') or $name = 'nobody';
$email = $user->get('user_email') or $email = 'nobody@localhost';
$this->set('credit', sprintf('%s <%s>', $name, $email ) );
// handle save action
$nonce = $this->setNonce('save-prefs');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
Loco_error_AdminNotices::success( __('Settings saved','loco') );
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/prefs', compact('breadcrumb') );
}
}
PK T\y~ ~ admin/config/BaseController.phpnu W+A set( 'tabs', $tabs );
$actions = array (
'' => __('Site options','loco'),
'user' => __('User options','loco'),
'version' => __('Version','loco'),
);
$suffix = (string) $this->get('action');
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( 'config-'.$action, $_GET );
$tabs->add( $name, $href, $action === $suffix );
}
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-settings'),
);
}
}PK T\W
# admin/config/SettingsController.phpnu W+A set( 'opts', $opts );
$this->set( 'dflt', Loco_data_Settings::create() );
// roles and capabilities
$perms = new Loco_data_Permissions;
// handle save action
$nonce = $this->setNonce('save-config');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
$perms->populate( $post->has('caps') ? $post->caps : array() );
// done update
Loco_error_AdminNotices::success( __('Settings saved','loco') );
// remove saved params if persistant options unset
if( ! $opts['fs_persist'] ){
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
unset( $session['loco-fs'] );
$session->persist();
}
}
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
$this->set('caps', $caps = new Loco_mvc_ViewParams );
// there is no distinct role for network admin, so we'll fake it for UI
if( is_multisite() ){
$caps[''] = new Loco_mvc_ViewParams( array(
'label' => __('Super Admin','default'),
'name' => 'dummy-admin-cap',
'attrs' => 'checked disabled'
) );
}
/* @var $role WP_Role */
foreach( $perms->getRoles() as $id => $role ){
$caps[$id] = new Loco_mvc_ViewParams( array(
'value' => '1',
'label' => $perms->getRoleName($id),
'name' => 'caps['.$id.'][loco_admin]',
'attrs' => $perms->isProtectedRole($role) ? 'checked disabled ' : ( $role->has_cap('loco_admin') ? 'checked ' : '' ),
) );
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/settings', compact('breadcrumb') );
}
}
PK T\hd2I admin/ErrorController.phpnu W+A set('error', Loco_error_Exception::convert($e) );
return $this->render();
}
public function render(){
$e = $this->get('error') or $e = new Loco_error_Exception('No error set');
return $this->view( $e->getTemplate() );
}
}
PK T\2l<& & admin/Navigation.phpnu W+A add( $name, $href );
}*/
/**
* Create a breadcrumb trail for a given view below a bundle
* @return Loco_admin_Navigation
*/
public static function createBreadcrumb( Loco_package_Bundle $bundle ){
$nav = new Loco_admin_Navigation;
// root link depends on bundle type
$type = strtolower( $bundle->getType() );
if( 'core' !== $type ){
$link = new Loco_mvc_ViewParams( array(
'href' => Loco_mvc_AdminRouter::generate($type),
) );
if( 'theme' === $type ){
$link['name'] = __('Themes','loco');
}
else {
$link['name'] = __('Plugins','loco');
}
$nav[] = $link;
}
// Add actual bundle page, href may be unset to show as current page if needed
$nav->add (
$bundle->getName(),
Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $bundle->getHandle() ) )
);
// client code will add current page
return $nav;
}
/**
* @return Loco_mvc_ViewParams
*
public function getSecondLast(){
$i = count($this);
if( $i > 1 ){
return $this[ $i-2 ];
}
}*/
}PK T\\]$s s admin/init/InitPotController.phpnu W+A enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New template','loco').' ‹ '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-init-pot'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confising when no project-scope navigation
// $this->get('tabs')->add( __('New POT','loco'), '', true );
$bundle = $this->getBundle();
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$this->set('domain', $domain );
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
// Establish default POT path whether it exists or not
$pot = $project->getPot();
while( ! $pot ){
$name = ( $slug ? $slug : $domain ).'.pot';
/* @var $dir Loco_fs_Directory */
foreach( $project->getConfiguredTargets() as $dir ){
$pot = new Loco_fs_File( $dir->getPath().'/'.$name );
break 2;
}
// unlikely to have no configured targets, but possible ... so default to standard
$pot = new Loco_fs_File( $bundle->getDirectoryPath().'/languages/'.$name );
break;
}
// POT should actually not exist at this stage. It should be edited instead.
if( $pot->exists() ){
throw new Loco_error_Exception( __('Template file already exists','loco') );
}
// Bundle may deliberately lock template to avoid end-user tampering
// it makes little sense to do so when template doesn't exist, but we will honour the setting anyway.
if( $project->isPotLocked() ){
throw new Loco_error_Exception('Template is protected from updates by the bundle configuration');
}
// Just warn if POT writing will fail when saved, but still show screen
$dir = $pot->getParent();
// Avoiding full source scan until actioned, but calculate size to manage expectations
$bytes = 0;
$nfiles = 0;
$nskip = 0;
$largest = 0;
$sources = $project->findSourceFiles();
// skip files larger than configured maximum
$opts = Loco_data_Settings::get();
$max = wp_convert_hr_to_bytes( $opts->max_php_size );
/* @var $sourceFile Loco_fs_File */
foreach( $sources as $sourceFile ){
$nfiles++;
$fsize = $sourceFile->size();
$largest = max( $largest, $fsize );
if( $fsize > $max ){
$nskip += 1;
}
else {
$bytes += $fsize;
}
}
$this->set( 'scan', new Loco_mvc_ViewParams( array (
'bytes' => $bytes,
'count' => $nfiles,
'skip' => $nskip,
'size' => Loco_mvc_FileParams::renderBytes($bytes),
'large' => Loco_mvc_FileParams::renderBytes($max),
'largest' => Loco_mvc_FileParams::renderBytes($largest),
) ) );
// file metadata
$this->set('pot', Loco_mvc_FileParams::create( $pot ) );
$this->set('dir', Loco_mvc_FileParams::create( $dir ) );
$title = __('New template file','loco');
$subhead = sprintf( __('New translations template for "%s"','loco'), $project );
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// ajax service takes the target directory path
$content_dir = loco_constant('WP_CONTENT_DIR');
$target_path = $pot->getParent()->getRelativePath($content_dir);
// hidden fields to pass through to Ajax endpoint
$this->set( 'hidden', new Loco_mvc_ViewParams( array(
'action' => 'loco_json',
'route' => 'xgettext',
'loco-nonce' => $this->setNonce('xgettext')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
'path' => $target_path,
'name' => $pot->basename(),
) ) );
// File system connect required if location not writable
if( ! $pot->creatable() ){
$path = $pot->getRelativePath($content_dir);
$this->prepareFsConnect('create', $path );
}
$this->enqueueScript('potinit');
return $this->view( 'admin/init/init-pot' );
}
}PK T\ admin/init/BaseController.phpnu W+A PK T\@% % admin/init/InitPoController.phpnu W+A enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New language','loco').' ‹ '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-init-po'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confising when no project-scope navigation
// $this->get('tabs')->add( __('New PO','loco'), '', true );
// bundle mandatory, but project optional
$bundle = $this->getBundle();
try {
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$subhead = sprintf( __('Initializing new translations in "%s"','loco'), $slug?$slug:$domain );
}
catch( Loco_error_Exception $e ){
$project = null;
$subhead = __('Initializing new translations in unknown set','loco');
}
$title = __('New language','loco');
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// default locale is a placeholder
$locale = new Loco_Locale('zxx');
$content_dir = rtrim( loco_constant('WP_CONTENT_DIR'), '/' );
$copying = false;
// Permit using any provided file a template instead of POT
if( $potpath = $this->get('source') ){
$potfile = new Loco_fs_LocaleFile($potpath);
$potfile->normalize( $content_dir );
if( ! $potfile->exists() ){
throw new Loco_error_Exception('Forced template argument must exist');
}
$copying = true;
// forced source could be a POT (although UI would normally prevent it)
if( $potfile->getSuffix() ){
$locale = $potfile->getLocale();
$this->set('sourceLocale', $locale );
}
}
// else project not configured. UI should prevent this by not offering msginit
else if( ! $project ){
throw new Loco_error_Exception('Cannot add new language to unconfigured set');
}
// else POT file may or may not be known, and may or may not exist
else {
$potfile = $project->getPot();
}
// start dropdown list with installed languages
$default = new Loco_Locale('en','US');
$installed = array(
new Loco_mvc_ViewParams( array(
'icon' => $default->getIcon(),
'value' => (string) $default,
'label' => $default->buildName(),
) )
);
// pull the same list of "available" languages as used in WordPress settings
$api = new Loco_api_WordPressTranslations;
$locales = array();
foreach( $api->getAvailableCore() as $tag => $raw ){
$locale = Loco_Locale::parse($tag);
$vparam = new Loco_mvc_ViewParams( array(
'icon' => $locale->getIcon(),
'value' => (string) $locale,
'label' => $locale->fetchName($api),
) );
if( $api->isInstalled($tag) ){
$installed[] = $vparam;
}
else {
$locales[] = $vparam;
}
}
$this->set( 'locales', $locales );
$this->set( 'installed', $installed );
// Critical that user selects the correct save location:
if( $project ){
$filechoice = $project->initLocaleFiles( $locale );
}
// without configured project we will only allow save to same location
else {
$filechoice = new Loco_fs_FileList;
}
// show information about POT file if we are initialializing from template
if( $potfile && $potfile->exists() ){
$meta = Loco_gettext_Metadata::load($potfile);
$total = $meta->getTotal();
$summary = sprintf( _n('One string found in %2$s','%s strings found in %s',$total,'loco'), number_format($total), $potfile->basename() );
$this->set( 'pot', new Loco_mvc_ViewParams( array(
'name' => $potfile->basename(),
'path' => $meta->getPath(false),
) ) );
// if copying an existing PO file, we can fairly safely establish the correct prefixing
if( $copying ){
$poname = ( $prefix = $potfile->getPrefix() ) ? sprintf('%s-%s.po',$prefix,$locale) : sprintf('%s.po',$locale);
$pofile = new Loco_fs_LocaleFile( $poname );
$pofile->normalize( $potfile->dirname() );
$filechoice->add( $pofile );
}
/// else if POT is in a folder we don't know about, we may as well add to the choices
// TODO this means another utilty function in project for prefixing rules on individual location
}
// else no template exists, so we prompt to extract from source
else {
$this->set( 'ext', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('type').'-xgettext', $_GET ),
'text' => __('Create template','loco'),
) ) );
// if forcing source extraction show brief description of source files
if( $this->get('extract') ){
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
$nfiles = count( $project->findSourceFiles() );
$summary = sprintf( _n('1 source file will be scanned for translatable strings','%s source files will be scanned for translatable strings',$nfiles,'loco'), number_format_i18n($nfiles) );
}
// else prompt for template creation before continuing
else {
$this->set( 'skip', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('_route'), $_GET + array( 'extract' => '1' ) ),
'text' => __('Skip template','loco'),
) ) );
// POT could still be defined, it might just not exist yet
if( $potfile ){
$this->set('pot', Loco_mvc_FileParams::create($potfile) );
}
return $this->view('admin/init/init-prompt');
}
}
$this->set( 'summary', $summary );
// group established locations into types (offical, etc..)
// there is no point checking whether any of these file exist, because we don't know what language will be chosen yet.
$locations = array();
$preferred = null;
/* @var $pofile Loco_fs_File */
foreach( $filechoice as $pofile ){
$parent = new Loco_fs_LocaleDirectory( $pofile->dirname() );
$typeId = $parent->getTypeId();
if( ! isset($locations[$typeId]) ){
$locations[$typeId] = new Loco_mvc_ViewParams( array(
'label' => $parent->getTypeLabel( $typeId ),
'paths' => array(),
) );
}
// lazy build of directory path, suppressing errors
if( ! $parent->exists() ){
try {
$parent->mkdir();
}
catch( Exception $e ){
// Loco_error_AdminNotices::warn( $e->getMessage() );
}
}
$params = new Loco_mvc_ViewParams( array (
'locked' => ! $parent->writable(),
'parent' => Loco_mvc_FileParams::create( $parent ),
'hidden' => $pofile->getRelativePath($content_dir),
'holder' => str_replace( (string) $locale, '<locale>', $pofile->basename() ),
) );
// use first writable (or createable) location as default option
if( is_null($preferred) && ! $params['locked'] ){
$preferred = $pofile;
$params['checked'] = 'checked';
}
$locations[$typeId]['paths'][] = $params;
}
$this->set( 'locations', $locations );
// hidden fields to pass through to Ajax endpoint
$this->set('hidden', new Loco_mvc_ViewParams( array(
'action' => 'loco_json',
'route' => 'msginit',
'loco-nonce' => $this->setNonce('msginit')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project ? $project->getId() : '',
'source' => $this->get('source'),
) ) );
// file system prompts will be handled when paths are selected (i.e. we don't have one yet)
$this->prepareFsConnect( 'create', '' );
$this->enqueueScript('poinit');
return $this->view( 'admin/init/init-po', array() );
}
}PK T\o) admin/RootController.phpnu W+A $this->view('tab-home'),
);
}
/**
* Render main entry home screen
*/
public function render(){
// translators: home screen title where %s is the version number
$this->set('title', sprintf( __('Loco Translate %s','loco'), loco_plugin_version() ) );
// Show currently active theme on home page
$theme = Loco_package_Theme::create(null);
$this->set('theme', $this->bundleParam($theme) );
// Show plugins that have currently loaded translations
$bundles = array();
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
try {
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// bundle should exist if we heard it. reduce to debug notice
Loco_error_AdminNotices::debug( $e->getMessage() );
}
}
$this->set('plugins', $bundles );
// Show recently "used' bundles
$bundles = array();
$recent = Loco_data_RecentItems::get();
// filter in lieu of plugin setting
$maxlen = apply_filters('loco_num_recent_bundles', 10 );
foreach( $recent->getBundles(0,$maxlen) as $id ){
try {
$bundle = Loco_package_Bundle::fromId($id);
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// possible that bundle ID changed since being saved in recent items list
}
}
$this->set('recent', $bundles );
// TODO favourites/starred
// current locale notice
$tag = get_locale();
if( 'en_' !== substr($tag,0,3) ){
$locale = Loco_Locale::parse($tag);
$this->set( 'locale', $locale );
}
// roll back link
$this->set( 'rollback', Loco_mvc_AdminRouter::generate('config-version') );
return $this->view('admin/root');
}
}PK T\ admin/DebugController.phpnu W+A set('title','DEBUG');
}
/**
* {@inheritdoc}
*/
public function render(){
// debug package listener
$themes = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getThemes() as $bundle ){
$themes[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
$this->set('themes', $themes );
$plugins = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
$plugins[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
// $this->set( 'plugins', Loco_package_Plugin::get_plugins() );
// $this->set('installed', wp_get_installed_translations('plugins') );
// $this->set('active', get_option( 'active_plugins', array() ) );
// $this->set('langs',get_available_languages());
/*$plugins = get_plugins();
$plugin_info = get_site_transient( 'update_plugins' );
foreach( $plugins as $plugin_file => $plugin_data ){
if ( isset( $plugin_info->response[$plugin_file] ) ) {
$plugins[$plugin_file]['____'] = $plugin_info->response[$plugin_file];
}
}*/
/*/ inspect session and test flash messages
$session = Loco_data_Session::get();
$session->flash( 'success', microtime() );
$this->set('session', $session->getArrayCopy() );
Loco_data_Session::close();*/
/*/ try some notices
Loco_error_AdminNotices::add( new Loco_error_Success('This is a sample success message') );
Loco_error_AdminNotices::add( new Loco_error_Warning('This is a sample warning') );
Loco_error_AdminNotices::add( new Loco_error_Exception('This is a sample error') );
//*/
return $this->view('admin/debug');
}
}
PK T\x/| | admin/list/CoreController.phpnu W+A loco-core-view
*/
class Loco_admin_list_CoreController extends Loco_admin_RedirectController {
/**
* {@inheritdoc}
*/
public function getLocation(){
return Loco_mvc_AdminRouter::generate('core-view');
}
}PK T\؞. . admin/list/ThemesController.phpnu W+A set('type', 'theme' );
$this->set('title', __( 'Translate themes', 'loco' ) );
/* @var $theme WP_Theme */
foreach( wp_get_themes() as $theme ){
$bundle = Loco_package_Theme::create( $theme->get_stylesheet() );
$this->addBundle( $bundle );
}
return parent::render();
}
}PK T\fs> > admin/list/PluginsController.phpnu W+A set( 'type', 'plugin' );
$this->set( 'title', __( 'Translate plugins', 'loco' ) );
foreach( Loco_package_Plugin::get_plugins() as $handle => $data ){
try {
$bundle = Loco_package_Plugin::create( $handle );
$this->addBundle($bundle);
}
// @codeCoverageIgnoreStart
catch( Exception $e ){
$bundle = new Loco_package_Plugin( $handle, $handle );
$this->addBundle( $bundle );
}
// @codeCoverageIgnoreEnd
}
return parent::render();
}
}PK T\rݠ admin/list/BaseController.phpnu W+A getHandle();
// compatibility will be 'ok', 'warn' or 'error' depending on severity
if( $default = $bundle->getDefaultProject() ){
$compat = $default->getPot() instanceof Loco_fs_File;
}
else {
$compat = false;
}
//$info = $bundle->getHeaderInfo();
return new Loco_mvc_ViewParams( array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'dflt' => $default ? $default->getDomain() : '--',
'size' => count( $bundle ),
'save' => $bundle->isConfigured(),
'type' => $type = strtolower( $bundle->getType() ),
'view' => Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $handle ) ),
'time' => $bundle->getLastUpdated(),
) );
}
/**
* Add bundle to enabled or disabled list, depending on whether it is configured
*/
protected function addBundle( Loco_package_Bundle $bundle ){
$this->bundles[] = $this->bundleParam($bundle);
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-list-bundles'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// breadcrumb is just the root
$here = new Loco_admin_Navigation( array (
new Loco_mvc_ViewParams( array( 'name' => $this->get('title') ) ),
) );
/*/ tab between the types of bundles
$types = array (
'' => __('Home','loco'),
'theme' => __('Themes','loco'),
'plugin' => __('Plugins','loco'),
);
$current = $this->get('_route');
$tabs = new Loco_admin_Navigation;
foreach( $types as $type => $name ){
$href = Loco_mvc_AdminRouter::generate($type);
$tabs->add( $name, $href, $type === $current );
}
*/
return $this->view( 'admin/list/bundles', array (
'bundles' => $this->bundles,
'breadcrumb' => $here,
) );
}
}PK T\Қ6 admin/file/BaseController.phpnu W+A List > Bundle > Resource
*/
abstract class Loco_admin_file_BaseController extends Loco_admin_bundle_BaseController {
/**
* 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', array() );
}
if( $file->isDirectory() ){
$this->set('info', Loco_mvc_FileParams::create($file) );
return $this->view( 'admin/errors/file-isdir', array() );
}
/*/ just warn if file isn't writeable
if( ! $file->writable() ){
$message = __("This file isn't writeable. Click the 'File info' tab for help setting the right permissions",'loco');
Loco_error_AdminNotices::add( new Loco_error_Warning($message) ); // <- TODO add contextual help link
}*/
return '';
}
/**
* {@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') );
// POT file has no locale
$ext = $file->extension();
if( 'pot' === $ext ){
$locale = null;
}
// else file may have a locale suffix (unless invalid, such as "default.po")
else {
$locale = $file->getLocale();
if( $locale->isValid() ){
$locale->fetchName( new Loco_api_WordPressTranslations ) or $locale->buildName();
}
else {
$locale = null;
}
}
$this->set('file', $file );
$this->set('filetype', strtoupper($ext) );
$this->set('title', $file->basename() );
$this->set('locale', $locale );
// 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 = array (
'file-edit' => __('Editor','loco'),
'file-view' => __('Source','loco'),
'file-info' => __('File info','loco'),
'file-delete' => __('Delete','loco'),
);
$suffix = $this->get('action');
$prefix = $this->get('type');
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $_GET );
$tabs->add( $name, $href, $action === $suffix );
}
// Provide common language creation link if project scope is is valid
try {
$project = $this->getProject();
$args = array( 'bundle' => $bundle->getHandle(), 'domain' => $project->getId() );
$this->set( 'msginit', new Loco_mvc_ViewParams( array (
'href' => Loco_mvc_AdminRouter::generate( $prefix.'-msginit', $args ),
'text' => __('New language','loco'),
) ) );
}
catch( Exception $e ){
}
}
/**
* {@inheritdoc}
*/
public function view( $tpl, array $args = array() ){
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
$title = $this->get('title') or $title = 'Untitled';
$breadcrumb->add( $title );
}
return parent::view( $tpl, $args );
}
}PK T\