PK }\N 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 * @param string property name * @param string date format * @return string empty string */ 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 * @param string property name * @param int optional decimal places * @return string empty string */ public function n( $p, $dp = null ){ // number_format_i18n is pre-escaped for HTML echo number_format_i18n( $this->__get($p), $dp ); return ''; } /** * Print property with passed formatting string * e.g. $params->f('name', 'My name is %s' ); * @param string property name * @param string formatting string * @return string empty 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() ); } /** * Merge parameters into ours * @param ArrayObject * @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->__debugInfo(),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE ) ),''; } /** * @codeCoverageIgnore */ public function __debugInfo() { return $this->getArrayCopy(); } /** * @param callable * @return Loco_mvc_ViewParams */ public function sort( $callback ){ $raw = $this->getArrayCopy(); uasort( $raw, $callback ); $this->exchangeArray( $raw ); return $this; } }PK }\5 5 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; } /** * @param Loco_fs_File * @return Loco_mvc_FileParams */ public static function create( Loco_fs_File $file ) { return new Loco_mvc_FileParams( array(), $file ); } /** * Override does lazy property initialization * @param array initial extra properties * @param Loco_fs_File */ public function __construct( array $props, 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; } /** * @internal * @return string */ private function _get_name(){ return $this->file->basename(); } /** * @internal * @return string */ private function _get_path(){ return $this->file->getPath(); } /** * @internal * @return string */ private function _get_relpath(){ return $this->file->getRelativePath( loco_constant('WP_CONTENT_DIR') ); } /** * 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.. * @internal * @return string */ private function _get_reltime(){ $time = $this->has('mtime') ? $this['mtime'] : $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-translate'); } return sprintf( __('%s ago','default'), human_time_diff($time) ); } /** * @internal * @return int */ private function _get_bytes(){ return $this->file->size(); } /** * @internal * @return string */ private function _get_size(){ return self::renderBytes( $this->_get_bytes() ); } /** * Get octal file mode * @internal * @return string */ private function _get_imode(){ $mode = new Loco_fs_FileMode( $this->file->mode() ); return (string) $mode; } /** * Get rwx file mode * @internal * @return string */ private function _get_smode(){ $mode = new Loco_fs_FileMode( $this->file->mode() ); return $mode->format(); } /** * Get file owner name * @internal * @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 * @internal * @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 * @return string; */ public function ls(){ $this->e('smode'); echo ' '; $this->e('owner'); echo ':'; $this->e('group'); echo ' '; $this->e('relpath'); return ''; } }PK }\NN+ + 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 ); // hook name compatible with AdminRouter do_action('loco_admin_init', $this->ctrl ); // previous hook name is deprecated if( has_action('loco_controller_init') ){ Loco_error_AdminNotices::debug('`loco_controller_init` is deprecated, use `loco_admin_init`'); 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 * Note that tests call renderAjax directly. * @codeCoverageIgnore */ public function on_wp_ajax_loco_json(){ $json = $this->renderAjax(); $this->exitScript( $json, array ( 'Content-Type' => 'application/json; charset=UTF-8', ) ); } /** * Additional ajax hook for download actions that won't be JSON * Note that tests call renderDownload directly. * @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; } $mimes = array ( 'mo' => 'application/x-gettext-translation', 'po' => 'application/x-gettext', 'pot' => 'application/x-gettext', 'xml' => 'text/xml', 'json' => 'application/json', ); $headers = array(); if( $ext && isset($mimes[$ext]) ){ $headers['Content-Type'] = $mimes[$ext].'; charset=UTF-8'; $headers['Content-Disposition'] = 'attachment; filename='.$file->basename(); } else { $headers['Content-Type'] = 'text/plain; charset=UTF-8'; } $this->exitScript( $data, $headers ); } /** * Exit script before WordPress shutdown, avoids hijacking of exit via wp_die_ajax_handler. * Also gives us a final chance to check for output buffering problems. * @codeCoverageIgnore * @param string * @param array */ private function exitScript( $str, array $headers ){ try { do_action('loco_admin_shutdown'); Loco_output_Buffer::clear(); $this->buffer = null; Loco_output_Buffer::check(); $headers['Content-Length'] = strlen($str); foreach( $headers as $name => $value ){ header( $name.': '.$value, true ); } } catch( Exception $e ){ Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) ); $str = $e->getMessage(); } echo $str; 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-translate'), $route ) ); } // else execute controller to get json output $json = $this->ctrl->render(); if( is_null($json) || '' === $json ){ throw new Loco_error_Exception( __('Ajax controller returned empty JSON','loco-translate') ); } } catch( Loco_error_Exception $e ){ $json = json_encode( array( 'error' => $e->jsonSerialize(), 'notices' => Loco_error_AdminNotices::destroyAjax() ) ); } catch( Exception $e ){ $e = Loco_error_Exception::convert($e); $json = json_encode( array( 'error' => $e->jsonSerialize(), 'notices' => Loco_error_AdminNotices::destroyAjax() ) ); } $this->buffer->discard(); 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-translate') ); } // else execute controller to get raw output $data = $this->ctrl->render(); if( is_null($data) || '' === $data ){ throw new Loco_error_Exception( __('Download controller returned empty output','loco-translate') ); } } catch( Exception $e ){ $data = $e; } $this->buffer->discard(); return $data; } }PK }\U U PostParams.phpnu W+A getArrayCopy(), false, '&' ); foreach( explode('&',$query) as $str ){ $serial[] = array_map( 'urldecode', explode( '=', $str, 2 ) ); } return $serial; } }PK }\H H 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() ){ try { $this->view->cd('/admin/help'); $tabs = $this->getHelpTabs(); // always append common help tabs $tabs[ __('Help & support','loco-translate') ] = $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('/'); } // avoid critical errors rendering non-critical part of page catch( Loco_error_Exception $e ){ $this->view->cd('/'); Loco_error_AdminNotices::add( $e ); } } // 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-translate')), 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('%ss', number_format_i18n($info['time'],2) ); } return $html; } /** * "loco_external" filter callback, campaignizes external links */ public function filter_loco_external( $url ){ $u = parse_url( $url ); if( isset($u['host']) && 'localise.biz' === $u['host'] ){ $query = http_build_query( array( 'utm_medium' => 'plugin', 'utm_campaign' => 'wp', 'utm_source' => 'admin', 'utm_content' => $this->get('_route') ), null, '&' ); $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, even 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() ){ /*if( ! $this->baseurl ){ throw new Loco_error_Debug('Did you mean to call $this->viewSnippet('.json_encode($tpl,JSON_UNESCAPED_SLASHES).') in '.get_class($this).'?'); }*/ $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-translate') ){ $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-translate' ); $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, ) ) ); // additional debugging info when enabled $jsConf['WP_DEBUG'] = true; } return $view->render( $tpl ); } /** * Shortcut to render template without full page arguments as per view * @return string */ public function viewSnippet( $tpl ){ return $this->view->render( $tpl ); } /** * Add CSS to head * @return Loco_mvc_Controller */ public function enqueueStyle( $name, array $deps = array() ){ if( $base = $this->baseurl ){ $href = $base.'/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; } throw new Loco_error_Exception('Too early to enqueueStyle('.json_encode($name,JSON_UNESCAPED_SLASHES).')'); } /** * Add JavaScript to footer * @return Loco_mvc_Controller */ public function enqueueScript( $name, array $deps = array() ){ if( $base = $this->baseurl ){ $href = $base.'/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; } throw new Loco_error_Exception('Too early to enqueueScript('.json_encode($name,JSON_UNESCAPED_SLASHES).')'); } }PK }\ܮH 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 * @param string action for passing to wp_verify_nonce * @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; } /** * Filter callback for `translations_api' * Ensures silent failure of translations_api when network disabled, see $this->getAvailableCore */ public function filter_translations_api( $value = false ){ if( apply_filters('loco_allow_remote', true ) ){ return $value; } // returning error here has the safe effect as returning empty translations list return new WP_Error( -1, 'Translations API blocked by loco_allow_remote filter' ); } /** * Filter callback for `pre_http_request` * Ensures fatal error if we failed to handle offline mode earlier. */ public function filter_pre_http_request( $value = false ){ if( apply_filters('loco_allow_remote', true ) ){ return $value; } // little point returning WP_Error error because WordPress will just show "unexpected error" throw new Loco_error_Exception('HTTP request blocked by loco_allow_remote filter' ); } } PK }\K`]p 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 && '/' === substr($path,0,1) ){ $this->cwd = untrailingslashit( loco_plugin_root().'/tpl'.$path ); } else { $this->cwd = untrailingslashit( $this->cwd.'/'.$path ); } return $this; } /** * @internal * Clean up if something abruptly stopped rendering before graceful end */ public function __destruct(){ if( $this->block ){ ob_end_clean(); } } /** * Render error screen HTML * @param Loco_error_Exception * @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 '