dvadf
File manager - Edit - /home/theblueo/tv/wp-includes/pomo/lib/package.tar
Back
Core.php 0000666 00000010506 15214156543 0006161 0 ustar 00 <?php /** * The "WordPress Core" translations bundle */ class Loco_package_Core extends Loco_package_Bundle { /** * {@inheritdoc} */ public function getSystemTargets(){ return array ( rtrim( loco_constant('LOCO_LANG_DIR'), '/' ), rtrim( loco_constant('WP_LANG_DIR'), '/' ) ); } /** * {@inheritdoc} */ public function getHeaderInfo(){ return new Loco_package_Header( array ( 'TextDomain' => 'default', 'DomainPath' => '/wp-content/languages/', // dummy author info for core components 'Name' => __('WordPress core','loco'), 'Version' => $GLOBALS['wp_version'], 'Author' => __('The WordPress Team','default'), 'AuthorURI' => __('https://wordpress.org/','default'), ) ); } /** * {@inheritdoc} */ public function getMetaTranslatable(){ return array(); } /** * {@inheritdoc} */ public function getType(){ return 'Core'; } /** * {@inheritdoc} * Core bundle doesn't need a handle, there is only one. */ public function getId(){ return 'core'; } /** * {@inheritdoc} * Core bundle is always configured */ public function isConfigured(){ $saved = parent::isConfigured() or $saved = 'internal'; return $saved; } /** * Manually define the core WordPress translations as a single bundle * Projects are those included in standard WordPress downloads: [default], "admin", "admin-network" and "continents-cities" * @return Loco_package_Core */ public static function create(){ $rootDir = loco_constant('ABSPATH'); $langDir = loco_constant('WP_LANG_DIR'); $bundle = new Loco_package_Core('core', __('WordPress Core','loco') ); $bundle->setDirectoryPath( $rootDir ); // Core config may be saved in DB, but not supporting bundled XML if( $bundle->configureDb() ){ return $bundle; } // front end, admin and network admin packages are all part of the "default" domain $domain = new Loco_package_TextDomain('default'); $domain->setCanonical( true ); // front end subset, has empty name in WP $project = $domain->createProject( $bundle, 'Development'); $project->setSlug('') ->setPot( new Loco_fs_File($langDir.'/wordpress.pot') ) ->addSourceDirectory( $rootDir) ->excludeSourcePath( $rootDir.'/wp-admin') ->excludeSourcePath( $rootDir.'/wp-content') ->excludeSourcePath( $rootDir.'/wp-includes/class-pop3.php') ; // "Administration" project (admin subset) $project = $domain->createProject( $bundle, 'Administration'); $project->setSlug('admin') ->setPot( new Loco_fs_File($langDir.'/admin.pot') ) ->addSourceDirectory( $rootDir.'/wp-admin' ) ->excludeSourcePath( $rootDir.'/wp-admin/js') ->excludeSourcePath( $rootDir.'/wp-admin/css') ->excludeSourcePath( $rootDir.'/wp-admin/network') ->excludeSourcePath( $rootDir.'/wp-admin/network.php') ->excludeSourcePath( $rootDir.'/wp-admin/includes/continents-cities.php') ; // "Network Admin" package (admin-network subset) $project = $domain->createProject($bundle, 'Network Admin'); $project->setSlug('admin-network') ->setPot( new Loco_fs_File($langDir.'/admin-network.pot') ) ->addSourceDirectory( $rootDir.'/wp-admin/network' ) ->addSourceFile( $rootDir.'/wp-admin/network.php' ) ; // end of "default" domain projects $bundle->addDomain( $domain ); // Continents & Cities is its own text domain) $domain = new Loco_package_TextDomain('continents-cities'); $project = $domain->createProject( $bundle, 'Continents & Cities'); $project->setPot( new Loco_fs_File( $langDir.'/continents-cities.pot') ) ->addSourceFile( $rootDir.'/wp-admin/includes/continents-cities.php' ) ; $bundle->addDomain( $domain ); return $bundle; } } Theme.php 0000666 00000007046 15214156543 0006340 0 ustar 00 <?php /** * Represents a bundle of type "theme" */ class Loco_package_Theme extends Loco_package_Bundle { /** * @var Loco_package_Theme */ private $parent; /** * {@inheritdoc} */ public function getSystemTargets(){ return array ( rtrim( loco_constant('LOCO_LANG_DIR'), '/' ).'/themes', rtrim( loco_constant('WP_LANG_DIR'), '/' ).'/themes', ); } /** * {@inheritdoc} */ public function isTheme(){ return true; } /** * {@inheritdoc} */ public function getType(){ return 'Theme'; } /** * {@inheritdoc} */ public function getHeaderInfo(){ $root = dirname( $this->getDirectoryPath() ); $theme = new WP_Theme( $this->getSlug(), $root ); return new Loco_package_Header( $theme ); } /** * {@inheritdoc} */ public function getMetaTranslatable(){ return array ( 'Name' => 'Name of the theme', 'Description' => 'Description of the theme', 'ThemeURI' => 'URI of the theme', 'Author' => 'Author of the theme', 'AuthorURI' => 'Author URI of the theme', // 'Tags' => 'Tags of the theme', ); } /** * Get parent bundle if theme is a child * @return Loco_package_Theme */ public function getParentTheme(){ return $this->parent; } /** * Create theme bundle definition from WordPress theme handle * * @param string short name of theme, e.g. "twentyfifteen" * @return Loco_package_Plugin */ public static function create( $slug, $root = null ){ return self::createFromTheme( wp_get_theme( $slug, $root ) ); } /** * Create theme bundle definition from WordPress theme data */ public static function createFromTheme( WP_Theme $theme ){ $slug = $theme->get_stylesheet(); $base = $theme->get_stylesheet_directory(); $name = $theme->get('Name') or $name = $slug; if( ! $theme->exists() ){ throw new Loco_error_Exception('Theme not found: '.$name ); } $bundle = new Loco_package_Theme( $slug, $name ); // ideally theme has declared its TextDomain $domain = $theme->get('TextDomain') or // if not, we can see if the Domain listener has picked it up $domain = Loco_package_Listener::singleton()->getDomain($slug); // otherwise we won't try to guess as it results in silent problems when guess is wrong // ideally theme has declared its DomainPath $target = $theme->get('DomainPath') or // if not, we can see if the Domain listener has picked it up $target = Loco_package_Listener::singleton()->getDomainPath($domain); // otherwise project will use theme root by default $bundle->configure( $base, array ( 'Name' => $name, 'TextDomain' => $domain, 'DomainPath' => $target, ) ); // parent theme inheritance: if( $parent = $theme->parent() ){ try { $bundle->parent = self::createFromTheme($parent); $bundle->inherit( $bundle->parent ); } catch( Loco_error_Exception $e ){ Loco_error_AdminNotices::add($e); } } // TODO provide hook to modify bundle? // do_action( 'loco_bundle_configured', $bundle ); return $bundle; } } Project.php 0000666 00000047545 15214156543 0006714 0 ustar 00 <?php /** * A project is a set of translations within a Text Domain. * Often a text domain will have just one set, but this allows domains to be split into multiple POT files. */ class Loco_package_Project { /** * Text Domain in which project lives * @var Loco_package_TextDomain */ private $domain; /** * Bundle in which project lives * @var Loco_package_Bundle */ private $bundle; /** * Friendly project name, e.g. "Network Admin" * @var string */ private $name; /** * Short name used for naming files, e.g "admin" * @var string */ private $slug; /** * Configured domain path[s] not including global search paths * @var Loco_fs_FileList */ private $dpaths; /** * Additional system domain path[s] added separately from bindle config * @var Loco_fs_FileList */ private $gpaths; /** * Directory paths to exclude during target scanning * @var Loco_fs_FileList */ private $xdpaths; /** * Locations where POT, PO and MO files may be saved, including standard global paths * @var Loco_fs_FileFinder */ private $target; /** * Configured source path[s] not including global search paths * @var Loco_fs_FileList */ private $spaths; /** * File and directory paths to exclude from source file extraction * @var Loco_fs_FileList */ private $xspaths; /** * Locations where extractable source files may be found * @var Loco_fs_FileFinder */ private $source; /** * Explicitly added individual PHP source files * @var Loco_fs_FileList */ private $sfiles; /** * Paths globally excluded by bundle-level configuration * @var Loco_fs_FileList */ private $xgpaths; /** * POT template file, ideally named "<name>.pot" * @var Loco_fs_File */ private $pot; /** * Whether POT file is protected from end-user update and sync operations. * @var bool */ private $potlock; /** * Construct project from its domain and a descriptive name */ public function __construct( Loco_package_Bundle $bundle, Loco_package_TextDomain $domain, $name ){ $this->name = $name; $this->bundle = $bundle; $this->domain = $domain; // take default slug from domain, avoiding wildcard $slug = $domain->getName(); if( '*' === $slug ){ $slug = ''; } $this->slug = $slug; // sources $this->sfiles = new Loco_fs_FileList; $this->spaths = new Loco_fs_FileList; $this->xspaths = new Loco_fs_FileList; // targets $this->dpaths = new Loco_fs_FileList; $this->gpaths = new Loco_fs_FileList; $this->xdpaths = new Loco_fs_FileList; // global $this->xgpaths = new Loco_fs_FileList; } /** * Get ID identifying project uniquely within a bundle * @return string */ public function getId(){ $slug = $this->getSlug(); $domain = (string) $this->getDomain(); if( $slug === $domain ){ return $slug; } return addcslashes($domain,'.').'.'.addcslashes($slug,'.'); } /** * @return string */ public function __toString(){ return (string) $this->name; } /** * Set friendly name of project * @return Loco_package_Project */ public function setName( $name ){ $this->name = $name; return $this; } /** * Set short name of project * @return Loco_package_Project */ public function setSlug( $slug ){ $this->slug = $slug; return $this; } /** * Get friendly name of project, e.g. "Network Admin" * @return string */ public function getName(){ return $this->name; } /** * Get short name of project, e.g. "admin" * @return string */ public function getSlug(){ return $this->slug; } /** * @var Loco_package_TextDomain */ public function getDomain(){ return $this->domain; } /** * Whether project is the default for its domain. * @return bool */ public function isDomainDefault(){ $slug = $this->getSlug(); $name = $this->getDomain()->getName(); // default if slug matches text domain. // else special case for Core "default" domain which has empty slug return $slug === $name || ( 'default' === $name && '' === $slug ) || 1 === count($this->bundle); } /** * Add a root path where translation files may live * @return Loco_package_Project */ public function addTargetDirectory( $location ){ $this->target = null; $this->dpaths->add( new Loco_fs_Directory($location) ); return $this; } /** * Add a global search path where translation files may live * @return Loco_package_Project */ public function addSystemTargetDirectory( $location ){ $this->target = null; $this->gpaths->add( new Loco_fs_Directory($location) ); return $this; } /** * Get domain paths configured in project * @return Loco_fs_FileList */ public function getConfiguredTargets(){ return $this->dpaths; } /** * Get system paths added to project after configuration * @return Loco_fs_FileList */ public function getSystemTargets(){ return $this->gpaths; } /** * Get all target directory roots including global search paths * @return Loco_fs_FileList */ public function getDomainTargets(){ return $this->getTargetFinder()->getRootDirectories(); } /** * Lazy create all searchable domain paths including global directories * @return Loco_fs_FileFinder */ private function getTargetFinder(){ if( ! $this->target ){ $target = new Loco_fs_FileFinder; $target->setRecursive(false)->group('pot','po','mo'); foreach( $this->dpaths as $path ){ // TODO search need not be recursive if it was the configured DomainPath // currenly no way to know at this point, so recursing by default. $target->addRoot( (string) $path, true ); } foreach( $this->gpaths as $path ){ $target->addRoot( (string) $path, false ); } $this->excludeTargets( $target ); $this->target = $target; } return $this->target; } /** * utility excludes current exclude paths from target finder * @return Loco_fs_FileFinder */ private function excludeTargets( Loco_fs_FileFinder $finder ){ foreach( $this->xdpaths as $file ){ if( $path = realpath( (string) $file ) ){ $finder->exclude( $path ); } } foreach( $this->xgpaths as $file ){ if( $path = realpath( (string) $file ) ){ $finder->exclude( $path ); } } return $finder; } /** * Add a path for excluding in a recursive target file search * @return Loco_package_Project */ public function excludeTargetPath( $path ){ $this->target = null; $this->xdpaths->add( new Loco_fs_File($path) ); return $this; } /** * Get all paths excluded when searching for targets * @return Loco_fs_FileList */ public function getConfiguredTargetsExcluded(){ return $this->xdpaths; } /** * Lazy create all searchable source paths * @return Loco_fs_FileFinder */ private function getSourceFinder(){ if( ! $this->source ){ $source = new Loco_fs_FileFinder; // .php extensions configured in plugin options $conf = Loco_data_Settings::get(); $exts = $conf->php_alias or $exts = array('php'); $source->setRecursive(true)->groupBy( $exts ); /* @var $file Loco_fs_File */ foreach( $this->spaths as $file ){ $path = realpath( (string) $file ); if( $path && is_dir($path) ){ $source->addRoot( $path, true ); } } $this->excludeSources( $source ); $this->source = $source; } return $this->source; } /** * utility excludes current exclude paths from target finder * @return Loco_fs_FileFinder */ private function excludeSources( Loco_fs_FileFinder $finder ){ foreach( $this->xspaths as $file ){ if( $path = realpath( (string) $file ) ){ $finder->exclude( $path ); } } foreach( $this->xgpaths as $file ){ if( $path = realpath( (string) $file ) ){ $finder->exclude( $path ); } } return $finder; } /** * Add a root path where source files may live under for this project * @return Loco_package_Project */ public function addSourceDirectory( $location ){ $this->source = null; $this->spaths->add( new Loco_fs_File($location) ); return $this; } /** * Add Explicit source file to project config * @return Loco_package_Project */ public function addSourceFile( $path ){ $this->source = null; $this->sfiles->add( new Loco_fs_File($path) ); return $this; } /** * Add a file or directory as a source location * @return Loco_package_Project */ public function addSourceLocation( $path ){ $file = new Loco_fs_File( $path ); if( $file->isDirectory() ){ $this->addSourceDirectory( $file ); } else { $this->addSourceFile( $file ); } return $this; } /** * Get all source directories and files defined in project * @return Loco_fs_FileList */ public function getConfiguredSources(){ $dynamic = $this->spaths->getArrayCopy(); $statics = $this->sfiles->getArrayCopy(); return new Loco_fs_FileList( array_merge( $dynamic, $statics ) ); } /** * Test if bundle has configured source files (even if they're excluded by other rules) * @return bool */ public function hasSourceFiles(){ return count( $this->sfiles ) || count( $this->spaths ); } /** * Add a path for excluding in source file search * @return Loco_package_Project */ public function excludeSourcePath( $path ){ $this->source = null; $this->xspaths->add( new Loco_fs_File($path) ); return $this; } /** * Get all paths excluded when searching for sources * @return Loco_fs_FileList */ public function getConfiguredSourcesExcluded(){ return $this->xspaths; } /** * Add a globally excluded location affecting sources and targets * @return Loco_package_Project */ public function excludeLocation( $path ){ $this->source = null; $this->target = null; $this->xgpaths->add( new Loco_fs_File($path) ); return $this; } /** * Check whether POT file is protected from end-user update and sync operations. * @return bool */ public function isPotLocked(){ return (bool) $this->potlock; } /** * Lock POT file to prevent end-user updates * @return Loco_package_Project */ public function setPotLock( $locked ){ $this->potlock = (bool) $locked; return $this; } /** * Get full path to template POT (file) * @return Loco_fs_File */ public function getPot(){ if( ! $this->pot ){ // attempt to match POT exactly under configured domain paths $name = $this->getSlug().'.pot'; $targets = $this->getConfiguredTargets()->export(); // permit POT file in the bundle root (i.e. outside domain path) if( $this->isDomainDefault() && $this->bundle->hasDirectoryPath() ){ $targets[] = $this->bundle->getDirectoryPath(); } foreach( $targets as $dir ){ $file = new Loco_fs_File( $name ); $file->normalize( $dir ); if( $file->exists() ){ $this->pot = $file; break; } } } return $this->pot; } /** * Force the use of a known POT file. This could be a PO file if necessary * @return Loco_package_Project */ public function setPot( Loco_fs_File $pot ){ $this->pot = $pot; return $this; } /** * Take a guess at most likely POT file under target locations * @return Loco_fs_File */ public function guessPot(){ $slug = $this->getSlug(); if( ! is_string($slug) || '' === $slug ){ $slug = (string) $this->getDomain(); if( '' === $slug ){ $slug = 'default'; } } // search only inside bundle for template $finder = new Loco_fs_FileFinder; foreach( $this->dpaths as $path ){ $finder->addRoot( (string) $path, true ); } $this->excludeTargets($finder); $files = $finder->group('pot','po','mo')->exportGroups(); foreach( array('pot','po') as $ext ){ /* @var $pot Loco_fs_File */ foreach( $files[$ext] as $pot ){ $name = $pot->filename(); // use exact match on project slug if found if( $slug === $name ){ return $pot; } // support unconventional <slug>-en_US.<ext> foreach( array('-en_US'=>6, '-en'=>3 ) as $tail => $len ){ if( '-en_US' === substr($name,-$len) && $slug === substr($name,0,-$len) ){ return $pot; } } } } // Failed to find correctly named POT file, // but if a single POT file is found we'll use it. if( 1 === count($files['pot']) ){ return $files['pot'][0]; } // Either no POT files are found, or multiple are found. // if the project is the default in its domain, we can try aliases which may be PO if( $this->isDomainDefault() ){ $options = Loco_data_Settings::get(); if( $aliases = $options->pot_alias ){ $found = array(); /* @var $pot Loco_fs_File */ foreach( $finder as $pot ){ $priority = array_search( $pot->basename(), $aliases, true ); if( false !== $priority ){ $found[$priority] = $pot; } } if( $found ){ ksort( $found ); return current($found); } } } // failed to guess POT file } /** * Get all extractable PHP source files found under all source paths * @return Loco_fs_FileList */ public function findSourceFiles(){ $source = $this->getSourceFinder(); // augment file list from directories unless already done so if( ! $source->isCached() ){ $crawled = $source->exportGroups(); foreach( $crawled as $ext => $files ){ foreach( $files as $file ){ $this->sfiles->add($file); } } } return $this->sfiles; } /** * Get all translation files matching project prefix across target directories * @return Loco_fs_LocaleFileList */ public function findLocaleFiles( $ext ){ $list = new Loco_fs_LocaleFileList; $files = $this->getTargetFinder()->exportGroups(); $prefix = $this->getSlug(); $domain = $this->domain->getName(); $default = $this->isDomainDefault(); /* @var $file Loco_fs_File */ foreach( $files[$ext] as $file ){ $file = new Loco_fs_LocaleFile( $file ); // add file if prefix matches and has a suffix. locale will be validated later if( $file->getPrefix() === $prefix && $file->getSuffix() ){ $list->addLocalized( $file ); } // else in some cases a suffix-only file like "el.po" can match else if( $default && $file->hasSuffixOnly() ){ // 1. theme files under their own directory if( $file->underThemeDirectory() ){ $list->addLocalized( $file ); } // 2. WordPress core "default" domain, default project else if( 'default' === $domain ){ $list->addLocalized( $file ); } } } return $list; } /** * @return Loco_fs_FileList */ public function findNotLocaleFiles( $ext ){ $list = new Loco_fs_LocaleFileList; $files = $this->getTargetFinder()->exportGroups(); /* @var $file Loco_fs_LocaleFile */ foreach( $files[$ext] as $file ){ $file = new Loco_fs_LocaleFile( $file ); // add file if it has no locale suffix and is inside the bundle if( $file->hasPrefixOnly() && ! $file->underGlobalDirectory() ){ $list->add( $file ); } } return $list; } /** * Intialize choice of PO file paths for a given locale * @return Loco_fs_FileList */ public function initLocaleFiles( Loco_Locale $locale ){ $slug = $this->getSlug(); $domain = $this->domain->getName(); $default = $this->isDomainDefault(); $suffix = sprintf( '%s.po', $locale ); $prefix = $slug ? sprintf('%s-',$slug) : ''; $choice = new Loco_fs_FileList; /* @var $dir Loco_fs_Directory */ foreach( $this->getConfiguredTargets() as $dir ){ // theme files under their own directory normally have no file prefix if( $default && $dir->underThemeDirectory() ){ $path = $dir->getPath().'/'.$suffix; } // plugin files are prefixed even in their own directory, so empty prefix here implies incorrect bundle configuration //else if( $default && ! $prefix && $dir->underPluginDirectory() ){ // $path = $dir->getPath().'/'.$domain.'-'.$suffix; //} // all other paths use configured prefix, which may be empty else { $path = $dir->getPath().'/'.$prefix.$suffix; } $choice->add( new Loco_fs_LocaleFile($path) ); } /* @var $dir Loco_fs_Directory */ foreach( $this->getSystemTargets() as $dir ){ $path = $dir->getPath(); // themes and plugins under global locations will be loaded by domain, regardless of prefix if( '/themes' === substr($path,-7) || '/plugins' === substr($path,-8) ){ $path .= '/'.$domain.'-'.$suffix; } // all other paths (probably core) use configured prefix, which may be empty else { $path .= '/'.$prefix.$suffix; } $choice->add( new Loco_fs_LocaleFile($path) ); } return $choice; } /** * Get newest timestamp of all translation files (includes template, but exclude source files) * @return int */ public function getLastUpdated(){ $t = 0; $file = $this->getPot(); if( $file && $file->exists() ){ $t = $file->modified(); } /* @var $file Loco_fs_File */ foreach( $this->findLocaleFiles('po') as $file ){ $t = max( $t, $file->modified() ); } return $t; } } Plugin.php 0000666 00000017537 15214156543 0006542 0 ustar 00 <?php /** * Represents a bundle of type "plugin" */ class Loco_package_Plugin extends Loco_package_Bundle { /** * {@inheritdoc} */ public function getSystemTargets(){ return array ( rtrim( loco_constant('LOCO_LANG_DIR'), '/' ).'/plugins', rtrim( loco_constant('WP_LANG_DIR'),'/' ).'/plugins', ); } /** * {@inheritdoc} */ public function isPlugin(){ return true; } /** * {@inheritdoc} */ public function getType(){ return 'Plugin'; } /** * {@inheritdoc} */ public function getSlug(){ // TODO establish "official" slug somehow // Fallback to first handle component $slug = explode( '/', parent::getSlug(), 2 ); return current( $slug ); } /** * Maintaining our own cache of full paths to available plugins, because get_mu_plugins doesn't get cached by WP * @return array */ public static function get_plugins(){ $cached = wp_cache_get('plugins','loco'); if( ! is_array($cached) ){ $cached = array(); // regular plugins + mu plugins: $search = array ( 'WP_PLUGIN_DIR' => 'get_plugins', 'WPMU_PLUGIN_DIR' => 'get_mu_plugins', ); foreach( $search as $const => $getter ){ if( $list = call_user_func($getter) ){ $base = loco_constant($const); foreach( $list as $handle => $data ){ if( isset($cached[$handle]) ){ Loco_error_AdminNotices::debug( sprintf('Plugin conflict on %s', $handle) ); continue; } // WordPress 4.6 introduced TextDomain header fallback @37562 see https://core.trac.wordpress.org/changeset/37562/ // if we don't force the original text domain header we can't know if a bundle is misconfigured. This leads to silent errors. // this has a performance overhead, and also results in "unconfigured" messages that users may not have had in previous releases. /*/ TODO perhaps implement a plugin setting that forces original headers $file = new Loco_fs_File($base.'/'.$handle); if( $file->exists() ){ $map = array( 'TextDomain' => 'Text Domain' ); $raw = get_file_data( $file->getPath(), $map, 'plugin' ); $data['TextDomain'] = $raw['TextDomain']; }*/ // set resolved base directory before caching our copy of plugin data $data['basedir'] = $base; $cached[$handle] = $data; } } } $cached = apply_filters('loco_plugins_data', $cached ); uasort( $cached, '_sort_uname_callback' ); wp_cache_set('plugins', $cached, 'loco'); } return $cached; } /** * Get raw plugin data from WordPress registry, plus additional "basedir" field for resolving handle to actual file. * @return array */ public static function get_plugin( $handle ){ $search = self::get_plugins(); // plugin must be registered with WordPress if( isset($search[$handle]) ){ $data = $search[$handle]; } // else plugin is not known to WordPress else { $data = apply_filters( 'loco_missing_plugin', array(), $handle ); } // plugin not valid if name absent from raw data if( empty($data['Name']) ){ return null; } // basedir is added by our get_plugins function, but filtered arrays could be broken if( ! array_key_exists('basedir',$data) ){ Loco_error_AdminNotices::debug( sprintf('"basedir" property required to resolve %s',$handle) ); return null; } return $data; } /** * {@inheritdoc} */ public function getHeaderInfo(){ $handle = $this->getHandle(); $data = self::get_plugin($handle); if( ! is_array($data) ){ // permitting direct file access if file exists (tests) $path = $this->getBootstrapPath(); if( $path && file_exists($path) ){ $data = get_plugin_data( $path, false, false ); } else { $data = array(); } } return new Loco_package_Header( $data ); } /** * {@inheritdoc} */ public function getMetaTranslatable(){ return array ( 'Name' => 'Name of the plugin', 'Description' => 'Description of the plugin', 'PluginURI' => 'URI of the plugin', 'Author' => 'Author of the plugin', 'AuthorURI' => 'Author URI of the plugin', // 'Tags' => 'Tags of the plugin', ); } /** * {@inheritdoc} */ public function setHandle( $slug ){ // plugin handles are relative paths from plugin directory to bootstrap file // so plugin is single file if its handle has no directory prefix if( basename($slug) === $slug ){ $this->solo = true; } else { $this->solo = false; } return parent::setHandle( $slug ); } /** * {@inheritdoc} */ public function setDirectoryPath( $path ){ parent::setDirectoryPath($path); // plugin bootstrap file can be inferred from base directory + handle if( ! $this->getBootstrapPath() ){ $file = new Loco_fs_File( basename( $this->getHandle() ) ); $file->normalize( $path ); $this->setBootstrapPath( $file->getPath() ); } return $this; } /** * Create plugin bundle definition from WordPress plugin data * * @param string plugin handle relative to plugin directory * @return Loco_package_Plugin */ public static function create( $handle ){ // plugin must be registered with at least a name and "basedir" $data = self::get_plugin($handle); if( ! $data ){ throw new Loco_error_Exception( sprintf( __('Plugin not found: %s','loco'),$handle) ); } // lazy resolve of base directory from "basedir" property that we added $file = new Loco_fs_File( $handle ); $file->normalize( $data['basedir'] ); $base = $file->dirname(); // handle and name is enough data to construct empty bundle $bundle = new Loco_package_Plugin( $handle, $data['Name'] ); // check if listener heard the real text domain, but only use when none declared // This will not longer happen since WP 4.6 header fallback, but we could warn about it $listener = Loco_package_Listener::singleton(); if( $domain = $listener->getDomain($handle) ){ if( empty($data['TextDomain']) ){ $data['TextDomain'] = $domain; if( empty($data['DomainPath']) ){ $data['DomainPath'] = $listener->getDomainPath($domain); } } // ideally would only warn on certain pages, but unsure where to place this logic other than here // TODO possibly allow bundle to hold errors/warnings as part of its config. else if( $data['TextDomain'] !== $domain ){ Loco_error_AdminNotices::debug( sprintf("Plugin loaded text domain '%s' but WordPress knows it as '%s'",$domain, $data['TextDomain']) ); } } // do initial configuration of bundle from metadata $bundle->configure( $base, $data ); return $bundle; } } Inverter.php 0000666 00000015242 15214156543 0007071 0 ustar 00 <?php /** * Bundle inverter utility class. */ abstract class Loco_package_Inverter { /** * Get all Gettext files that are not configured and valid in the given bundle * @return array */ public static function export( Loco_package_Bundle $bundle ){ // search paths for inverted bundle will exclude global ignore paths, // plus anything known to the current configuration which we'll add now. $finder = $bundle->getFileFinder(); /* @var $project Loco_package_Project */ foreach( $bundle as $project ){ if( $file = $project->getPot() ){ // excluding all extensions in case POT is actually a PO/MO pair foreach( array('pot','po','mo') as $ext ){ $file = $file->cloneExtension($ext); if( $path = realpath( $file->getPath() ) ){ $finder->exclude( $path ); } } } foreach( $project->findLocaleFiles('po') as $file ){ if( $path = realpath( $file->getPath() ) ){ $finder->exclude( $path ); } } foreach( $project->findLocaleFiles('mo') as $file ){ if( $path = realpath( $file->getPath() ) ){ $finder->exclude( $path ); } } } // Do a deep scan of all files that haven't been seen, or been excluded: // This will include files in global directories and inside the bundle. return $finder->setRecursive(true)->followLinks(false)->group('po','mo','pot')->exportGroups(); } /** * Compile anything found under bundle root that isn't configured in $known * @return Loco_package_Bundle */ public static function compile( Loco_package_Bundle $bundle ){ $found = self::export($bundle); // done with original bundle now $bundle = clone $bundle; $bundle->clear(); // first iteration groups found files into common locations that should hopefully indicate translation sets $groups = array(); $templates = array(); $localised = array(); $root = $bundle->getDirectoryPath(); /* @var $list Loco_fs_FileList */ foreach( $found as $ext => $list ){ /* @var $file Loco_fs_LocaleFile */ foreach( $list as $file ){ // printf("Found: %s <br />\n", $file ); // This file is NOT known to be part of a configured project $dir = $file->getParent(); $key = $dir->getRelativePath( $root ); // if( ! isset($groups[$key]) ){ $groups[$key] = $dir; $templates[$key] = array(); $localised[$key] = array(); } // template should define single set of translations unique by directory and file prefix if( 'pot' === $ext ){ $slug = $file->filename(); $templates[$key][$slug] = true; } // else ideally PO/MO files will correspond to a template by common prefix else { $file = new Loco_fs_LocaleFile( $file ); $slug = $file->getPrefix(); if( $file->getLocale()->isValid() ){ $localised[$key][$slug] = true; } // else could be some kind of non-standard template else { $slug = $file->filename(); $templates[$key][$slug] = true; } } } } unset($found); // next iteration matches collected files together into likely project sets $unique = array(); /* @var $list Loco_fs_Directory */ foreach( $groups as $key => $dir ){ // pair up all projects that match templates neatly to prefixed files foreach( $templates[$key] as $slug => $bool ){ if( isset($localised[$key][$slug]) ){ //printf("Perfect match on domain '%s' in %s <br />\n", $slug, $key ); $unique[$key][$slug] = $dir; // done with this prefectly matched set $templates[$key][$slug] = null; $localised[$key][$slug] = null; } } // pair up any unprefixed localised files if( isset($localised[$key]['']) ){ $slug = 'unknown'; // Match to first (hopefully only) template to establish a slug foreach( $templates[$key] as $_slug => $bool ){ if( $bool ){ $slug = $_slug; $templates[$key][$slug] = null; break; // <- not possible to know how multiple POTs might be paired up } } //printf("Pairing unprefixed files in %s to '%s' <br />\n", $key, $slug ); $unique[$key][$slug] = $dir; // done with unprefixed localised files in this directory $localised[$key][''] = null; } // add any orphaned translations (those with no template matched) foreach( $localised[$key] as $slug => $bool ){ if( $bool ){ // printf("Picked up orphoned locales in %s as '%s' <br />\n", $key, $slug ); $unique[$key][$slug] = $dir; } } // add any orphaned templates (those with no localised files matched) foreach( $templates[$key] as $slug => $bool ){ if( $bool ){ //printf("Picked up orphoned template in %s as '%s' <br />\n", $key, $slug ); $unique[$key][$slug] = $dir; } } } unset( $groups, $localised, $templates ); // final iteration adds unique projects to bundle foreach( $unique as $key => $sets ){ foreach( $sets as $slug => $dir ){ $name = ucfirst( strtr( $slug, '-_', ' ' ) ); $domain = new Loco_package_TextDomain( $slug ); $project = $domain->createProject( $bundle, $name ); $project->addTargetDirectory($dir); $bundle->addProject($project); } // TODO how to prevent overlapping sets by adding each other's files to exclude lists } return $bundle; } } Listener.php 0000666 00000027506 15214156544 0007067 0 ustar 00 <?php /** * Captures text domains being loaded at runtime and establishes what bundles they belong to. * */ class Loco_package_Listener extends Loco_hooks_Hookable { /** * Global availability of a single listener * @var Loco_package_Listener */ private static $singleton; /** * Buffer of captured text domain loads before they're resolved * @var array */ private $buffer; /** * Whether buffer can be flushed. i.e. whether there is anything new to resolve * @var bool */ private $buffered; /** * Resolved theme bundles, indexed by slug (stylesheet dir) * @var array */ private $themes; /** * Resolved plugin bundles, indexed by slug (relative file path) * @var array */ private $plugins; /** * Map of all established bundle's and their *primary* text domain * @var array { slug: domain } */ private $domains; /** * Map of all text domains and their official directory location * @var array { slug: domain } */ private $domainPaths; /** * Map of all known plugin handles indexed by their relative containing directory * @var array { slug: domain } */ private $pluginHandles; /** * List of common directories that don't indicate ownership to a bundle, e.g. WP_LANG_DIR * @var array */ private $globalPaths; /** * Get singleton listener or create new if not already exists * @return Loco_package_Listener */ public static function singleton(){ $active = self::$singleton or $active = self::create(); return $active; } /** * @internal */ public static function destroy(){ if( $active = self::$singleton ){ $active->unhook(); self::$singleton = null; } } /** * Create a singleton listener that we can query from anywhere * @return Loco_package_Listener */ public static function create(){ self::destroy(); self::$singleton = new Loco_package_Listener; return self::$singleton->clear(); } /** * @return Loco_package_Listener */ public function clear(){ $this->buffer = array(); $this->themes = array(); $this->plugins = array(); $this->domains = array(); $this->domainPaths = array(); $this->pluginHandles = null; $this->buffered = false; $this->globalPaths = array(); foreach( array('WP_LANG_DIR') as $name ){ if( $value = loco_constant($name) ){ $this->globalPaths[$value] = strlen($value); } } return $this; } /** * Early hook listening for active bundles loading their own text domains. */ public function on_load_textdomain( $domain, $mofile ){ // echo '<pre>Debug:',esc_html( json_encode(compact('domain','mofile'),JSON_UNESCAPED_SLASHES)),'</pre>'; $this->buffered = true; $this->buffer[$domain][] = $mofile; } /** * Get primary Text Domain that's uniquely assigned to a bundle * @param string theme or plugin relative path */ public function getDomain( $handle ){ $this->flush(); return isset($this->domains[$handle]) ? $this->domains[$handle] : ''; } /** * Get the default directory path where captured files of a given domain are held * @param string TextDomain * @return string relative path */ public function getDomainPath( $domain ){ $this->flush(); return isset($this->domainPaths[$domain]) ? $this->domainPaths[$domain] : ''; } /** * Utility: checks if a file path is under a given root * @return string subpath relative to given root */ private static function relative( $path, $root ){ $root = trailingslashit($root); $snip = strlen($root); // attempt unaltered path if( substr($path,0,$snip) === $root ){ return substr( $path, $snip ); } // attempt resolved in case symlinks along path $real = realpath($path); if( $real && $real !== $path && substr($real,0,$snip) === $root ){ return substr( $real, $snip ); } // path not under root return null; } /** * Check if given relative directory path the root of a known plugin * @param string relative plugin directory name, e.g. "foo/bar" * @return string relative plugin file handle, e.g. "foo/bar/baz.php" */ private function isPlugin( $check ){ if( ! $this->pluginHandles ){ $this->pluginHandles = array(); foreach( Loco_package_Plugin::get_plugins() as $handle => $data ){ $this->pluginHandles[ dirname($handle) ] = $handle; // set default text domain because additional domains could be discovered before the canonical one if( isset($data['TextDomain']) && ( $domain = $data['TextDomain'] ) ){ $this->domains[$handle] = $domain; } } } if( ! array_key_exists($check, $this->pluginHandles) ){ return null; } return $this->pluginHandles[$check]; } /** * Convert a file path to a theme or plugin bundle * @return Loco_package_Bundle */ private function resolve( $path, $domain ){ $file = new Loco_fs_LocaleFile( $path ); // ignore suffix-only files when locale is invalid as locale code would be taken wrongly as slug, e.g. if you tried to load "english.po" if( $file->hasPrefixOnly() ){ return; } // no point looking at files in global directory as they tell us only the domain which we already know foreach( $this->globalPaths as $prefix => $length ){ if( substr($path,0,$length) === $prefix ){ return; } } // avoid infinite loops during bundle resolution $wasBuffered = $this->buffered; $this->buffered = false; // file prefix is *probably* the Text Domain, but can differ if load_textdomain called directly from bundle code $slug = $file->getPrefix() or $slug = $domain; $path = dirname($path); $bundle = null; while( true ){ // check if MO file lives inside a theme foreach( $GLOBALS['wp_theme_directories'] as $root ){ $relative = self::relative($path, $root); if( is_null($relative) ){ continue; } // theme's "stylesheet directory" must be immediately under this root // passed path could root of theme, or any directory below it, but we only need the top level $chunks = explode( '/', $relative, 2 ); $handle = current( $chunks ); if( ! $handle ){ continue; } $theme = new WP_Theme( $handle, $root ); if( ! $theme->exists() ){ continue; } $abspath = $root.'/'.$handle; // theme may have officially declared text domain if( $default = $theme->get('TextDomain') ){ $this->domains[$handle] = $default; } // else set current domain as default if not already set else if ( ! isset($this->domains[$handle]) ){ $this->domains[$handle] = $domain; } if( ! isset($this->domainPaths[$domain]) ){ $this->domainPaths[$domain] = self::relative( $path, $abspath ); } // theme bundle may already exist if( isset($this->themes[$handle]) ){ $bundle = $this->themes[$handle]; } // create default project for theme bundle else { $bundle = Loco_package_Theme::createFromTheme($theme); $this->themes[$handle] = $bundle; } // possibility that additional text domains are being added $project = $bundle->getProject($slug); if( ! $project ){ $project = new Loco_package_Project( $bundle, new Loco_package_TextDomain($domain), $slug ); $bundle->addProject( $project ); } // bundle was a theme, even if we couldn't configure it, so no point checking plugins break 2; } // check if MO file lives inside a plugin foreach( array( 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR' ) as $const ){ $root = loco_constant( $const ); $relative = self::relative($path, $root); if( is_null($relative) ){ continue; } // plugin *might* live directly under root $stack = array(); foreach( explode( '/', dirname($relative) ) as $next ){ $stack[] = $next; $relbase = implode('/', $stack ); if( $handle = $this->isPlugin($relbase) ){ $abspath = $root.'/'.$handle; // set this as default domain if not already cached if( ! isset($this->domains[$handle]) ){ $this->domains[$handle] = $domain; } if( ! isset($this->domainPaths[$domain]) ){ $target = self::relative( $path, dirname($abspath) ); $this->domainPaths[$domain] = $target; } // plugin bundle may already exist if( isset($this->plugins[$handle]) ){ $bundle = $this->plugins[$handle]; } // create default project for plugin bundle (not necessarily the current text domain) else { $bundle = Loco_package_Plugin::create($handle); $this->plugins[$handle] = $bundle; } // add current domain as translation project if not already set // this avoids extra domains getting set before the default one if( ! $bundle->getProject($slug) ){ $project = new Loco_package_Project( $bundle, new Loco_package_TextDomain($domain), $slug ); $bundle->addProject( $project ); } break; } } } // failed to establish a bundle break; } $this->buffered = $wasBuffered; return $bundle; } /** * @internal * Resolve all currently buffered text domain paths */ private function flush(){ if( $this->buffered ){ foreach( $this->buffer as $domain => $paths ){ foreach( $paths as $path ){ try { if( $bundle = $this->resolve($path,$domain) ){ continue 2; } } catch( Loco_error_Exception $e ){ // silent errors for non-critical function } } } $this->buffer = array(); $this->buffered = false; } } /** * @return array */ public function getThemes(){ $this->flush(); return $this->themes; } /** * @return array */ public function getPlugins(){ $this->flush(); return $this->plugins; } } Bundle.php 0000666 00000044221 15214156544 0006504 0 ustar 00 <?php /** * A bundle may use one or more text domains, and may or may not physically house them. * Essentially a bundle "uses" a text domain. * Types are "theme", "plugin" and "core" */ abstract class Loco_package_Bundle extends ArrayObject implements JsonSerializable { /** * Internal handle for targeting in WordPress, e.g. "twentyfifteen" or "loco-translate/loco.php" * @var string */ private $handle; /** * Short name, e.g. "twentyfifteen" or "loco-translate" * @var string */ private $slug; /** * Friendly name, e.g. "Twenty Fifteen * @var string */ private $name; /** * Full path to root directory of bundle * @var string */ private $root; /** * Directory paths to exclude from all projects * @var Loco_fs_FileList */ private $xpaths; /** * Full path to PHP bootsrap file * @var string */ private $boot; /** * Whether bundle is a single file, as opposed to in its own directory * @var bool */ protected $solo; /** * Method with which bundle has been configured * @var string|false (file|db|meta|internal) */ private $saved = false; /** * Get system (i.e. "global") target locations for all projects of this type. * These are aways append to configs, and always excluded from serialization * @return array<string> absolute directory paths */ abstract public function getSystemTargets(); /** * Get canonical info registered with WordPress, i.e. plugin or theme headers * @return Loco_package_Header */ abstract public function getHeaderInfo(); /** * Get built-in translatable values mapped to annotation for translators * @return array */ abstract public function getMetaTranslatable(); /** * Get type of Bundle (title case) * @return string */ abstract public function getType(); /** * Construct bundle from unique ID containing type and handle * @return Loco_package_Bundle */ public static function fromId( $id ){ $r = explode( '.', $id, 2 ); return self::createType( $r[0], isset($r[1]) ? $r[1] : '' ); } /** * @return Loco_package_Bundle * @throws Loco_error_Exception */ public static function createType( $type, $handle ){ $func = array( 'Loco_package_'.ucfirst($type), 'create' ); if( is_callable($func) ){ $bundle = call_user_func( $func, $handle ); } else { throw new Loco_error_Exception('Unexpected bundle type: '.$type ); } return $bundle; } /** * Construct from WordPress handle and friendly name */ public function __construct( $handle, $name ){ $this->setHandle($handle)->setName($name); $this->xpaths = new Loco_fs_FileList; } /** * Refetch this bundle from its currently saved location * @return Loco_package_Bundle */ public function reload(){ return call_user_func( array( get_class($this), 'create' ), $this->getSlug() ); } /** * Get ID that uniquely identifies bundle by its type and handle * @return string */ public function getId(){ $type = strtolower( $this->getType() ); return $type.'.'.$this->getHandle(); } /** * @return string */ public function __toString(){ return (string) $this->name; } /** * @return bool */ public function isTheme(){ return false; } /** * @return bool */ public function isPlugin(){ return false; } /** * Get handle of bundle unique for its type, e.g. "twentyfifteen" or "loco-translate/loco.php" * @return string */ public function getHandle(){ return $this->handle; } /** * Attempt to get the vendor-specific slug, which may or may not be the same as the internal handle * @return string */ public function getSlug(){ if( $slug = $this->slug ){ return $slug; } // fall back to runtime handle return $this->getHandle(); } /** * Set friendly name of bundle * @return Loco_package_Bundle */ public function setName( $name ){ $this->name = $name; return $this; } /** * Set short name of bundle which may or may not match unique handle * @return Loco_package_Bundle */ public function setSlug( $slug ){ $this->slug = $slug; return $this; } /** * Set internal handle registered with WordPress for this bundle type * @return Loco_package_Bundle */ public function setHandle( $handle ){ $this->handle = $handle; return $this; } /** * Get friendly name of bundle, e.g. "Twenty Fifteen" or "Loco Translate" * @return string */ public function getName(){ return $this->name; } /** * Whether bundle root is currently known * @return bool */ public function hasDirectoryPath(){ return (bool) $this->root; } /** * Set root directory for bundle. e.g. theme or plugin directory * @return Loco_package_Bundle */ public function setDirectoryPath( $path ){ $this->root = new Loco_fs_Directory( $path ); $this->root->normalize(); return $this; } /** * Get absolute path to root directory for bundle. e.g. theme or plugin directory * @return string */ public function getDirectoryPath(){ if( $this->root ){ return $this->root->getPath(); } // without a root directory return WordPress root return rtrim(ABSPATH,'/'); } /** * Get file locations to exclude from all projects in bundle. These are effectively "hidden" * @return Loco_fs_FileList */ public function getExcludedLocations(){ return $this->xpaths; } /** * Add a path for excluding from all projects * @return Loco_package_Bundle */ public function excludeLocation( $path ){ $this->xpaths->add( new Loco_fs_File($path) ); return $this; } /** * Create a file searcher from root location, excluding that which is excluded * @return Loco_fs_file_Finder */ public function getFileFinder(){ $root = $this->getDirectoryPath(); /*/ if bundle is symlinked it's resource files won't be matched properly if( is_link($root) && ( $real = realpath($root) ) ){ $root = $real; }*/ $finder = new Loco_fs_FileFinder( $root ); foreach( $this->xpaths as $path ){ $finder->exclude( (string) $path ); } return $finder; } /** * Get primary PHP source file containing bundle bootstrap code, if applicable * @return string */ public function getBootstrapPath(){ return $this->boot; } /** * Set primary PHP source file containing bundle bootstrap code, if applicable. * @return Loco_package_Bundle */ public function setBootstrapPath( $path ){ $this->boot = (string) $path; // base directory can be inferred from bootstrap path if( ! $this->hasDirectoryPath() ){ $this->setDirectoryPath( dirname($this->boot) ); } return $this; } /** * Test whether bundle consists of a single file */ public function isSingleFile(){ return (bool) $this->solo; } /** * Add all projects defined in a TextDomain * @return Loco_package_Bundle */ public function addDomain( Loco_package_TextDomain $domain ){ /* @var $proj Loco_package_Project */ foreach( $domain as $proj ){ $this->addProject($proj); } return $this; } /** * Add a translation project to bundle. * Note that this always adds without checking uniqueness. Call hasProject first if it could be a duplicate * @return Loco_package_Bundle */ public function addProject( Loco_package_Project $project ){ // add global targets foreach( $this->getSystemTargets() as $path ){ $project->addSystemTargetDirectory( $path ); } // add global exclusions affecting source and target locations foreach( $this->xpaths as $path ){ $project->excludeLocation( $path ); } // projects must be unique by Text Domain and "slug" (used to prefix files) // however, I am not indexing them here on purpose so domain and slug may be added at any time. $this[] = $project; return $this; } /** * Export projects grouped by domain * @return array indexed by Text Domain name */ public function exportGrouped(){ $domains = array(); /* @var $proj Loco_package_Project */ foreach( $this as $proj ){ $domain = $proj->getDomain(); $key = $domain->getName(); $domains[$key][] = $proj; } return $domains; } /** * Create a suitable Text Domain from bundle's name. * Note that internal handle may be a directory name differing entirely from the author's intention, hence the configured bundle name is slugged instead * @return Loco_package_TextDomain */ public function createDomain(){ $slug = sanitize_title( $this->name, $this->slug ); return new Loco_package_TextDomain( $slug ); } /** * Generate default configuration. * Adds a simple one domain, one project config * @param string optional Text Domain to use * @return Loco_package_Project */ public function createDefault( $domainName = null ){ if( is_null($domainName) ){ $domain = $this->createDomain(); } else { $domain = new Loco_package_TextDomain($domainName); } $project = $domain->createProject( $this, $this->name ); if( $this->solo ){ $project->addSourceFile( $this->getBootstrapPath() ); } else { $project->addSourceDirectory( $this->getDirectoryPath() ); } $this->addProject( $project ); return $project; } /** * Configure from custom saved option * @return bool whether configured */ public function configureDb(){ if( $option = $this->getCustomConfig() ){ $option->configure(); $this->saved = 'db'; return true; } return false; } /** * Configure from XML config * @return bool whether configured */ public function configureXml(){ if( $xmlfile = $this->getConfigFile() ){ $reader = new Loco_config_BundleReader($this); $reader->loadXml( $xmlfile ); $this->saved = 'file'; return true; } return false; } /** * Get XML configuration file used to define this bundle * TODO will we also support JSON for when dom extension is loaded? * TODO support custom location for user-saved XML? * @return Loco_fs_File */ public function getConfigFile(){ $base = $this->getDirectoryPath(); $file = new Loco_fs_File( $base.'/loco.xml' ); if( ! $file->exists() || ! loco_check_extension('dom') ){ return null; } return $file; } /** * Check whether bundle is manually configured, as opposed to guessed * @return string (file|db|meta|internal) */ public function isConfigured(){ return $this->saved; } /** * Do basic configuration from bundle meta data (file headers) * @return bool whether configured */ public function configureMeta( array $header ){ if( isset($header['Name']) ){ $this->setName( $header['Name'] ); } if( isset($header['TextDomain']) && ( $slug = $header['TextDomain'] ) ){ $domain = new Loco_package_TextDomain($slug); $domain->setCanonical( true ); // use domain as bundle handle and slug if not set when constructed if( ! $this->handle ){ $this->handle = $slug; } if( ! $this->getSlug() ){ $this->setSlug( $slug ); } $project = $domain->createProject( $this, $this->name ); // May have declared DomainPath $base = $this->getDirectoryPath(); if( isset($header['DomainPath']) && ( $path = trim($header['DomainPath'],'/') ) ){ $project->addTargetDirectory( $base.'/'.$path ); } else if( $this->solo ){ // skip } // else use standard language path if it exists else if( is_dir($base.'/languages') ) { $project->addTargetDirectory($base.'/languages'); } // else add bundle root by default else { $project->addTargetDirectory( $base ); } // single file bundles can have only one source file if( $this->solo ){ $project->addSourceFile( $this->getBootstrapPath() ); } // else add bundle root as default source file location else { $project->addSourceDirectory( $base ); } // default domain added $this->addProject($project); $this->saved = 'meta'; return true; } return false; } /** * Configure bundle from canonical sources. * Source order is "db","file","meta" where meta is the auto-config fallback. * No deep scanning is performed at this point * @return Loco_package_Bundle */ public function configure( $base, array $header ){ $this->setDirectoryPath( $base ); $this->configureDb() || $this->configureXml() || $this->configureMeta($header); return $this; } /** * Get the custom config saved in WordPress DB for this bundle * @return Loco_config_CustomSaved */ public function getCustomConfig(){ $custom = new Loco_config_CustomSaved; if( $custom->setBundle($this)->fetch() ){ return $custom; } } /** * Inherit another bundle. Used for child themes to display parent translations * @return Loco_package_Bundle */ public function inherit( Loco_package_Bundle $parent ){ foreach( $parent as $project ){ if( ! $this->hasProject($project) ){ $this->addProject( $project ); } } return $this; } /** * Get unique translation project by text domain (and optionally slug) * TODO would prefer to avoid iteration, but slug can be changed at any time * @return Loco_package_Project */ public function getProject( $domain, $slug = null ){ if( is_null($slug) ){ $slug = $domain; } /* @var $project Loco_package_Project */ foreach( $this as $project ){ if( $project->getSlug() === $slug && $project->getDomain()->getName() === $domain ){ return $project; } } return null; } /** * @return Loco_package_Project */ public function getDefaultProject(){ $i = 0; /* @var $project Loco_package_Project */ foreach( $this as $project ){ if( $project->isDomainDefault() ){ return $project; } $i++; } // nothing is domain default, but if we only have one, then duh if( 1 === $i ){ return $project; } } /** * Test if project already exists in bundle * @return bool */ public function hasProject( Loco_package_Project $project ){ return (bool) $this->getProject( $project->getDomain()->getName(), $project->getSlug() ); } /** * @return array<Loco_package_TextDomain> */ public function getDomains(){ $domains = array(); /* @var $project Loco_package_Project */ foreach( $this as $project ){ if( $domain = $project->getDomain() ){ $d = (string) $domain; if( ! isset($domains[$d]) ){ $domains[$d] = $domain; } } } return $domains; } /** * Get newest timestamp of all translation files (includes template, but exclude source files) * @return int */ public function getLastUpdated(){ // recent items is a convenient cache for checking last modified times $t = Loco_data_RecentItems::get()->hasBundle( $this->getId() ); // else have to scan targets across all projects if( 0 === $t ){ /* @var $project Loco_package_Project */ foreach( $this as $project ){ $t = max( $t, $project->getLastUpdated() ); } } return $t; } /** * Get project by ID * @param string <domain>[.<slug>] * @return Loco_package_Project */ public function getProjectById( $id ){ $r = preg_split('/(?<!\\\\)\\./', $id, 2 ); $domain = stripcslashes($r[0]); $slug = isset($r[1]) ? stripcslashes($r[1]) : null; return $this->getProject( $domain, $slug ); } /** * Reset bundle configuration, but keep metadata like name and slug. * Call this before applying a saved config, otherwise values will just be added on top. * @return Loco_package_Bundle */ public function clear(){ $this->exchangeArray( array() ); $this->xpaths = new Loco_fs_FileList; $this->saved = false; return $this; } /** * @return array */ public function jsonSerialize(){ $writer = new Loco_config_BundleWriter( $this ); return $writer->toArray(); } /** * Create a copy of this bundle containg any files found that aren't currently configured * @return Loco_package_Bundle */ public function invert(){ return Loco_package_Inverter::compile( $this ); } } Debugger.php 0000666 00000031046 15214156544 0007020 0 ustar 00 <?php /** * Bundle diagnostics. */ class Loco_package_Debugger implements IteratorAggregate { /** * @var array */ private $messages; /** * @var array */ private $counts; /** * Run immediately on construct */ public function __construct( Loco_package_Bundle $bundle ){ $this->messages = array(); $this->counts = array( 'success' => 0, 'warning' => 0, 'debug' => 0, 'info' => 0, ); // config storage type switch( $bundle->isConfigured() ){ case 'db': $this->info("Custom configuration saved in database"); break; case 'meta': $this->good("Configuration auto-detected from file headers"); break; case 'file': $this->good("Official configuration provided by author"); break; case 'internal': $this->info("Configuration built-in to Loco"); break; case '': $this->warn("Cannot auto-detect configuration"); break; default: throw new Exception('Unexpected isConfigured() return value'); } $base = $bundle->getDirectoryPath(); // $this->devel('Bundle root is %s',$base); // self-declarations provided by author in file headers $native = $bundle->getHeaderInfo(); if( $value = $native->TextDomain ){ $this->info('WordPress says primary text domain is "%s"', $value); // WordPress 4.6 changes mean this header could be a fallback and not actually declared by author if( $bundle->isPlugin() ){ $map = array ( 'TextDomain' => 'Text Domain' ); $raw = get_file_data( $bundle->getBootstrapPath(), $map, 'plugin' ); if( empty($raw['TextDomain']) ){ $this->warn('Author doesn\'t define the TextDomain header, WordPress guessed it'); } } // Warn if WordPress-assumed text domain is not configured. plugin/theme headers won't be translated $domains = $bundle->getDomains(); if( ! isset($domains[$value]) ){ $this->warn('Expected text domain "%s" is not configured', $value ); } } else { $this->warn("Author doesn't define the TextDomain header"); } if( $value = $native->DomainPath ){ $this->good('Primary domain path declared by author as "%s"', $value ); } else if( is_dir($base.'/languages') ){ $this->info('Standard "languages" folder found, although DomainPath not declared'); } else { $this->warn("Author doesn't define the DomainPath header"); } // check validity of single-file plugins if( $bundle->isSingleFile() && ! $bundle->getBootstrapPath() ){ $this->warn('Plugin is a single file, but bootstrap file is unknown'); } // collecting only configured domains to match against source code $domains = array(); $templates = array(); // show each known subset if( $count = count($bundle) ){ /* @var $project Loco_package_Project */ foreach( $bundle as $project ){ $id = $project->getId(); $domain = (string) $project->getDomain(); $domains[$domain] = true; // Domain path[s] within bundle directory $targets = array(); /* @var $dir Loco_fs_Directory */ foreach( $project->getConfiguredTargets() as $dir ){ $targets[] = $dir->getRelativePath($base); } if( $targets ){ $this->info('%u domain path[s] configured for "%s" -> %s', count($targets), $id, json_encode($targets,JSON_UNESCAPED_SLASHES) ); } else { $this->warn('No domain paths configured for "%s"', $id ); } // POT template file if( $potfile = $project->getPot() ){ if( $potfile->exists() ){ $this->good('Template file for "%s" exists at "%s"', $id, $potfile->getRelativePath($base) ); try { $data = Loco_gettext_Data::load($potfile); $templates[$domain][] = $data; } catch( Exception $e ){ $this->warn('Template file for "%s" is invalid format', $id ); } } else { $this->warn('Template file for "%s" does not exist (%s)', $id, $potfile->getRelativePath($base) ); } } else { $this->warn('No template file configured for "%s"', $domain ); if( $potfile = $project->guessPot() ){ $this->devel('Possible non-standard name for "%s" template at "%s"', $id, $potfile->getRelativePath($base) ); $project->setPot( $potfile ); // <- adding so that invert ignores it } } } $default = $bundle->getDefaultProject(); if( ! $default ){ $this->warn('%u subsets configured, but failed to establish the default/primary', $count ); } } else { $default = $bundle->createDefault(); $domain = (string) $default->getDomain(); $this->devel( 'Suggested text domain: "%s"', $domain ); } // files picked up with no context as to what they're for if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){ $unknown = $bundle->invert(); if( $n = count($unknown) ){ /* @var $project Loco_package_Project */ foreach( $unknown as $project ){ $domain = (string) $project->getDomain(); // should only have one target due the way the inverter groups results /* @var $dir Loco_fs_Directory */ foreach( $project->getConfiguredTargets() as $dir ){ $reldir = $dir->getRelativePath($base) or $stub = '.'; $this->warn('Unconfigured files found in "%s", possible domain name: "%s"', $reldir, $domain ); } } } } // source code extraction across entire bundle $tmp = clone $bundle; $tmp->exchangeArray( array() ); $project = $tmp->createDefault( (string) $default->getDomain() ); $extr = new Loco_gettext_Extraction( $tmp ); $extr->addProject( $project ); if( $total = $extr->getTotal() ){ // real count excludes additional metadata $realCounts = $extr->getDomainCounts(); $counts = $extr->includeMeta()->getDomainCounts(); // $this->good("%u string[s] can be extracted from source code for %s", $total, $this->implodeKeys($counts) ); foreach( array_intersect_key($counts, $domains) as $domain => $count ){ if( isset($realCounts[$domain]) ){ $realCount = $realCounts[$domain]; $str = _n( 'One string extracted from source code for "%2$s"', '%s strings extracted from source code for "%s"', $realCount, 'loco' ); $this->good( $str, number_format($realCount), $domain ); } else { $this->warn('No strings extracted from source code for "%s"', $domain ); } // check POT agrees with extracted count, but only if domain has single POT (i.e. not split across files on purpose) if( isset($templates[$domain]) && 1 === count($templates[$domain]) ){ $data = current( $templates[$domain] ); if( ! $extr->getTemplate($domain)->equalSource($data) ){ $meta = Loco_gettext_Metadata::create( new Loco_fs_DummyFile(''), $data ); $this->devel('Template is not in sync with source code (%s in file)', $meta->getTotalSummary() ); } } } // with extracted strings we can check for domain mismatches if( $missing = array_diff_key($domains, $realCounts) ){ $num = count($missing); $str = _n( 'Configured domain has no extractable strings', '%u configured domains have no extractable strings', $num, 'loco' ); $this->warn( $str.': %2$s', $num, $this->implodeKeys($missing) ); } if( $extra = array_diff_key($realCounts,$domains) ){ $this->info('%u unconfigured domain[s] found in source code: %s', count($extra), $this->implodeKeys($extra) ); /*/ debug other domains extracted foreach( $extra as $name => $count ){ $this->devel(' > %s (%u)', $name, $count ); }*/ // extracted domains could prove that declared domain is wrong if( $missing ){ foreach( array_keys($extra) as $name ){ $flat = preg_replace('/[^a-z0-9]/','', strtolower($name) ); foreach( array_keys($missing) as $decl ){ if( preg_replace('/[^a-z0-9]/','', strtolower($decl) ) === $flat ){ $this->devel('"%s" might be a mistake. Should it be "%s"?', $decl, $name ); } } } } } } else { $this->warn("No strings can be extracted from source code"); } } /** * @internal * Implements IteratorAggregate for looping over messages * @return ArrayIterator */ public function getIterator(){ return new ArrayIterator( $this->messages ); } /** * Add a success notice * @return Loco_package_Debugger */ private function good( $text ){ $args = func_get_args(); $text = call_user_func_array('sprintf', $args ); return $this->add( new Loco_error_Success($text) ); } /** * Add a warning notice * @return Loco_package_Debugger */ private function warn( $text ){ $args = func_get_args(); $text = call_user_func_array('sprintf', $args ); return $this->add( new Loco_error_Warning($text) ); } /** * Add an information notice (not good, or bad) * @return Loco_package_Debugger */ private function info( $text ){ $args = func_get_args(); $text = call_user_func_array('sprintf', $args ); return $this->add( new Loco_error_Notice($text) ); } /** * Add a developer notice. probably something helpful for fixing a problem * @return Loco_package_Debugger */ private function devel( $text ){ $args = func_get_args(); $text = call_user_func_array('sprintf', $args ); return $this->add( new Loco_error_Debug($text) ); } /** * @return Loco_package_Debugger */ private function add( Loco_error_Exception $error ){ $this->counts[ $error->getType() ]++; $this->messages[] = $error; return $this; } /** * Print all diagnostic messages suitable for CLI * @codeCoverageIgnore */ public function dump( $prefix = '' ){ /* @var $notice Loco_error_Exception */ foreach( $this as $notice ){ printf("%s[%s] %s\n", $prefix, $notice->getType(), $notice->getMessage() ); } } /** * Get number of bad things discovered * @return int */ public function countWarnings(){ return $this->counts['warning']; } /** * Utility for printing "x", "y" & "z" * @return string */ private function implodeNames( array $names ){ $last = array_pop($names); if( $names ){ return '"'.implode('", "',$names).'" & "'.$last.'"'; } if( is_string($last) ){ return '"'.$last.'"'; } return ''; } /** * @internal * @return string */ private function implodeKeys( array $assoc ){ return $this->implodeNames( array_keys($assoc) ); } } TextDomain.php 0000666 00000002744 15214156544 0007353 0 ustar 00 <?php /** * Object represents a Text Domain within a bundle. * TODO implement a conflict watcher to warn when domains are shared by multiple bundles? */ class Loco_package_TextDomain extends ArrayIterator { /** * Actual Gettext-like name of Text Domain, e.g. "twentyfifteen" * @var string */ private $name; /** * Whether this is the officially declared domain for a theme or plugin * @var bool */ private $canonical = false; /** * Create new Text Domain from its name */ public function __construct( $name ){ $this->name = $name; } /** * @internal */ public function __toString(){ return (string) $this->name; } /** * Get name of Text Domain, e.g. "twentyfifteen" * @return string */ public function getName(){ return $this->name; } /** * Create a named project in a given bundle for this Text Domain * @return Loco_package_Project */ public function createProject( Loco_package_Bundle $bundle, $name ){ $proj = new Loco_package_Project( $bundle, $this, $name ); $this[] = $proj; return $proj; } /** * @return Loco_package_TextDomain */ public function setCanonical( $bool ){ $this->canonical = (bool) $bool; return $this; } /** * @return bool */ public function isCanonical(){ return $this->canonical; } } Header.php 0000666 00000005576 15214156544 0006475 0 ustar 00 <?php /** * Common access to bundle headers. * Because access to theme and plugin header data via WordPress is a total mess. * * @property-read string $Name * @property-read string $Author * @property-read string $AuthorURI * @property-read string $TextDomain * @property-read string $DomainPath */ class Loco_package_Header { /** * WordPress's internal data * @var array|ArrayAccess */ private $wp; public function __construct( $header ){ $this->wp = $header; } /** * @return string */ public function __get( $prop ){ $wp = $this->wp; // prefer require "get" method to access raw properties (WP_Theme) if( method_exists($wp, 'get') && ( $value = $wp->get($prop) ) ){ return $value; } // may have key directly, e.g. TextDomain in plugin array if( isset($wp[$prop]) ){ return $wp[$prop]; } // else header not defined, which is probably fine return ''; } /** * @codeCoverageIgnore */ public function __set( $prop, $value ){ throw new RuntimeException('Read only'); } /** * Get bundle author as linked text, just like the WordPress plugin list does * @return string escaped HTML */ public function getAuthorLink(){ if( ( $link = $this->AuthorURI ) || ( $link = $this->PluginURI ) || ( $link = $this->ThemeURI ) ){ $author = $this->Author or $author = $link; return '<a href="'.esc_url($link).'" target="_blank">'.esc_html($author).'</a>'; } return ''; } /** * Get "name" by <author> credit * @return string escaped HTML */ public function getAuthorCredit(){ if( $author = $this->Author ){ $author = esc_html( strip_tags($author) ); if( $link = $this->AuthorURI ){ $author = '<a href="'.esc_url($link).'" target="_blank">'.$author.'</a>'; } } else { $author = __('Unknown author','loco'); } $html = sprintf( __('"%s" %s by %s','default'), esc_html($this->Name), $this->Version, $author ); if( ( $link = $this->PluginURI ) || ( $link = $this->ThemeURI ) ){ $html .= sprintf( ' — <a href="%s" target="_blank">%s</a>', esc_url($link), __('Visit official site','loco') ); } return $html; } /** * Get hostname of vendor that hosts theme/plugin * @return string e.g. "wordpress.org" */ public function getVendorHost(){ $host = ''; if( ( $url = $this->PluginURI ) || ( $url = $this->ThemeURI ) ){ if( $host = parse_url($url,PHP_URL_HOST) ){ $bits = explode( '.', $host ); $host = implode( '.', array_slice($bits,-2) ); } } return $host; } }
dvadf
dvadf
| ver. 1.4 |
Github
|
.
| PHP 7.0.33 | Generation time: 0 |
proxy
|
phpinfo
|
Settings