PKT\>N N js/Strings.phpnuW+Aflush('loco'); } } PKT\Gmvc/PostParams.phpnuW+AgetArrayCopy(), false, '&' ); foreach( explode('&',$query) as $str ){ $serial[] = array_map( 'urldecode', explode( '=', $str, 2 ) ); } return $serial; } }PKT\*MTkkmvc/AjaxRouter.phpnuW+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; } }PKT\30&mvc/AdminController.phpnuW+Abench = 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; } }PKT\mvc/Controller.phpnuW+AexitForbidden(); } /** * 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; } } PKT\!((mvc/AdminRouter.phpnuW+AID ); // 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; } }PKT\d)'mvc/ViewParams.phpnuW+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) ); }*/ }PKT\ aamvc/FileParams.phpnuW+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 ''; } }PKT\e`` mvc/View.phpnuW+Ascope = 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; } } PKT\Lllmvc/HiddenFields.phpnuW+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; } } PKT\ ӆLLmvc/AjaxController.phpnuW+Aauth(); $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 ); }*/ }PKT\''data/CompiledData.phpnuW+Adata = 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'); } }PKT\vLgdata/Option.phpnuW+AgetKey(); 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 ); } }PKT\̕ data/Serializable.phpnuW+AsetFlags( 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; } }PKT\_? data/RecentItems.phpnuW+Afetch(); } 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; }*/ } PKT\:,MaGGdata/Transient.phpnuW+AgetKey(); $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; } } }PKT\{Ωdata/Session.phpnuW+Aclear(); } 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 ); } } } PKT\$)T T data/Preferences.phpnuW+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; } }PKT\]JVVdata/Cookie.phpnuW+AsetName( $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; } }PKT\t11data/Permissions.phpnuW+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; } } PKT\I?(BBdata/Settings.phpnuW+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; } } PKT\?(͒test/DummyFtpConnect.phpnuW+AgetWriteContext()->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() ); } } PKT\P׺}#}#test/WordPressTestCase.phpnuW+Aprepare( "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 ); } }PKT\4ltest/TransientObject.phpnuW+ApreserveWhitespace = 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 ); } }PKT\  test/TestFilters.phpnuW+AformatOutput = 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'); } } PKT\=gqqconfig/CustomSaved.phpnuW+Abundle->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; } }PKT\_88config/BundleWriter.phpnuW+Abundle = $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 ); } //