',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp.png' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp@2x.png' ),
esc_attr__( 'WPForms ♥ WP Mail SMTP', 'wpforms-lite' ),
esc_html__( 'Making Email Deliverability Easy for WordPress', 'wpforms-lite' ),
esc_html__( 'WP Mail SMTP allows you to easily set up WordPress to use a trusted provider to reliably send emails, including form notifications. Built by the same folks behind WPForms.', 'wpforms-lite' )
);
}
/**
* Generate and output screenshot section HTML.
*
* @since 1.5.7
*/
protected function output_section_screenshot() {
// Screenshot section.
printf(
'
';
}
/**
* Generate and output heading section HTML.
*
* @since 1.5.7
*/
public function output_section_heading() {
// Heading section.
printf(
'
%4$s
%5$s
',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/analytics/wpforms-monsterinsights.png' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/analytics/wpforms-monsterinsights@2x.png' ),
esc_attr__( 'WPForms ♥ MonsterInsights', 'wpforms-lite' ),
esc_html__( 'The Best Google Analytics Plugin for WordPress', 'wpforms-lite' ),
esc_html__( 'MonsterInsights connects WPForms to Google Analytics, providing a powerful integration with their Forms addon. MonsterInsights is a sister company of WPForms.', 'wpforms-lite' )
);
}
/**
* Generate and output heading section HTML.
*
* @since 1.5.7
*/
protected function output_section_screenshot() {
// Screenshot section.
printf(
'
%4$s
%5$s
%6$s
%7$s
',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/analytics/screenshot-tnail.jpg' ),
esc_attr__( 'Analytics screenshot', 'wpforms-lite' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/analytics/screenshot-full.jpg' ),
esc_html__( 'Track form impressions and conversions.', 'wpforms-lite' ),
esc_html__( 'View form conversion rates from WordPress.', 'wpforms-lite' ),
esc_html__( 'Complete UTM tracking with form entries.', 'wpforms-lite' ),
esc_html__( 'Automatic integration with WPForms.', 'wpforms-lite' )
);
}
/**
* Generate and output step 'Install' section HTML.
*
* @since 1.5.7
*/
protected function output_section_step_install() {
$step = $this->get_data_step_install();
if ( empty( $step ) ) {
return;
}
printf(
'
WPForms Challenge and get up and running within %1$d %2$s.', 'wpforms-lite' ),
\absint( $this->minutes ),
\_n( 'minute', 'minutes', \absint( $this->minutes ), 'wpforms-lite' )
),
array( 'b' => array() )
);
?>
' . \absint( $this->minutes ) .':00'
);
?>
%1$s %2$s %3$s %4$s. Share your success story with other WPForms users and help us spread the word by giving WPForms a 5-star rating (%5$s) on WordPress.org. Thanks for your support and we look forward to bringing more awesome features.', 'wpforms-lite' ),
'',
\_n( 'minute', 'minutes', \absint( $this->minutes ), 'wpforms-lite' ),
'',
\_n( 'second', 'seconds', \absint( $this->minutes ), 'wpforms-lite' ),
''
),
array(
'span' => array(
'id' => array(),
'class' => array(),
),
'b' => array(),
'i' => array(
'class' => array(),
),
)
);
?>
';
}
/**
* Widget content HTML if a user has no forms.
*
* @since 1.5.0
*/
public function widget_content_no_forms_html() {
$create_form_url = \add_query_arg( 'page', 'wpforms-builder', \admin_url( 'admin.php' ) );
$learn_more_url = 'https://wpforms.com/docs/creating-first-form/?utm_source=WordPress&utm_medium=link&utm_campaign=liteplugin&utm_content=dashboardwidget';
?>
forms_list_block(); ?>
get_entries_count_by_form();
if ( empty( $forms ) ) {
$this->forms_list_block_empty_html();
} else {
$this->forms_list_block_html( $forms );
}
}
/**
* Empty forms list block HTML.
*
* @since 1.5.0
*/
public function forms_list_block_empty_html() {
?>
' . esc_html__( 'We\'re sorry, the %name% is not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ) . '
',
'bonus' => '
' .
wp_kses(
__( 'Bonus: WPForms Lite users get 50% off regular price, automatically applied at checkout.', 'wpforms-lite' ),
[
'strong' => [],
'span' => [],
]
) .
'
' . esc_html__( 'We\'re sorry, the %name% is not available on your plan. Please upgrade to the Elite plan to unlock all these awesome features.', 'wpforms-lite' ) . '
',
'bonus' => '
' .
wp_kses(
__( 'Bonus: WPForms Lite users get 50% off regular price, automatically applied at checkout.', 'wpforms-lite' ),
[
'strong' => [],
'span' => [],
]
) .
'
',
'doc' => '' . esc_html__( 'Already purchased?', 'wpforms-lite' ) . '',
'button' => esc_html__( 'Upgrade to Elite', 'wpforms-lite' ),
'url' => wpforms_admin_upgrade_link( 'builder-modal', 'upgrade-elite' ),
'modal' => wpforms_get_upgrade_modal_text( 'elite' ),
],
];
return $strings;
}
/**
* Load enqueues.
*
* @since 1.5.1
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-builder-education',
WPFORMS_PLUGIN_URL . "lite/assets/js/admin/builder-education{$min}.js",
array( 'jquery', 'jquery-confirm' ),
WPFORMS_VERSION,
false
);
}
/**
* Display templates.
*
* @since 1.5.1
*/
public function templates() {
$templates = array(
array(
'name' => esc_html__( 'Request A Quote Form', 'wpforms-lite' ),
'slug' => 'request-quote',
'description' => esc_html__( 'Start collecting leads with this pre-made Request a quote form. You can add and remove fields as needed.', 'wpforms-lite' ),
),
array(
'name' => esc_html__( 'Donation Form', 'wpforms-lite' ),
'slug' => 'donation',
'description' => esc_html__( 'Start collecting donation payments on your website with this ready-made Donation form. You can add and remove fields as needed.', 'wpforms-lite' ),
),
array(
'name' => esc_html__( 'Billing / Order Form', 'wpforms-lite' ),
'slug' => 'order',
'description' => esc_html__( 'Collect payments for product and service orders with this ready-made form template. You can add and remove fields as needed.', 'wpforms-lite' ),
),
);
?>
' . esc_html__( 'We\'re sorry, the %name% is not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ) . '
',
'bonus' => '
' .
wp_kses(
__( 'Bonus: WPForms Lite users get 50% off regular price, automatically applied at checkout.', 'wpforms-lite' ),
[
'strong' => [],
'span' => [],
]
) .
'
' . esc_html__( 'We\'re sorry, the %name% is not available on your plan. Please upgrade to the Elite plan to unlock all these awesome features.', 'wpforms-lite' ) . '
',
'bonus' => '
' .
wp_kses(
__( 'Bonus: WPForms Lite users get 50% off regular price, automatically applied at checkout.', 'wpforms-lite' ),
[
'strong' => [],
'span' => [],
]
) .
'
',
esc_html__( 'Access Controls', 'wpforms-lite' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/lite-settings-access/pro-plus.svg' ),
esc_attr__( 'Pro+', 'wpforms-lite' ),
esc_html__( 'Access controls allows you to manage and customize access to WPForms functionality.', 'wpforms-lite' ),
esc_html__( 'You can easily grant or restrict access using the simple built-in controls, or use our official integrations with Members and User Role Editor plugins.', 'wpforms-lite' )
);
}
/**
* Generate and output section "Screenshots" row HTML.
*
* @since 1.5.8
*/
public function output_section_row_screenshots() {
$format = '
';
}
// 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) );
}*/
} mvc/FileParams.php 0000666 00000011541 15214173534 0010100 0 ustar 00 = 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 '';
}
} mvc/View.php 0000666 00000015540 15214173534 0006772 0 ustar 00 scope = new Loco_mvc_ViewParams( $args );
$this->cwd = loco_plugin_root().'/tpl';
}
/**
* Change base path for template paths
* @param string path relative to current directory
* @return Loco_mvc_View
*/
public function cd( $path ){
if( $path && '/' === $path{0} ){
$this->cwd = rtrim( loco_plugin_root().'/tpl'.$path, '/' );
}
else {
$this->cwd = rtrim( $this->cwd.'/'.$path );
}
return $this;
}
/**
* @internal
* Clean up if something ubruptly stopped rendering before graceful end
*/
public function __destruct(){
if( $this->block ){
ob_end_clean();
}
}
/**
* Render error screen HTML
* @return string
*/
public static function renderError( Loco_error_Exception $e ){
$view = new Loco_mvc_View;
try {
$view->set( 'error', $e );
return $view->render( $e->getTemplate() );
}
catch( Exception $e ){
return '
'.esc_html( $e->getMessage() ).'
';
}
}
/**
* @internal
* Make this view a child of another template. i.e. decorate this with that.
* Parent will have access to original argument scope, but separate from now on
* @return Loco_mvc_View the parent view
*/
private function extend( $tpl ){
$this->parent = new Loco_mvc_View;
$this->parent->cwd = $this->cwd;
$this->parent->setTemplate( $tpl );
return $this->parent;
}
/**
* @internal
* After start is called any captured output will be placed in the named variable
* @return void
*/
private function start( $name ){
$this->stop();
$this->scope[$name] = null;
$this->block = $name;
}
/**
* @internal
* When stop is called, buffered output is saved into current variable for output by parent template, or at end of script.
* @return void
*/
private function stop(){
$content = ob_get_contents();
ob_clean();
if( $b = $this->block ){
if( isset($this->scope[$b]) ){
$content = $this->scope[$b].$content;
}
$this->scope[$b] = new _LocoViewBuffer($content);
$this->block = null;
}
$this->block = '_trash';
}
/**
* implement IteratorAggregate::getIterator
*/
public function getIterator(){
return $this->scope;
}
/**
* @return mixed
*/
public function __get( $prop ){
return isset($this->scope[$prop]) ? $this->scope[$prop] : null;
}
/**
* @return bool
*/
public function has( $prop ){
return isset( $this->scope[$prop] );
}
/**
* Set a view argument
* @return Loco_mvc_View
*/
public function set( $prop, $value ){
$this->scope[$prop] = $value;
return $this;
}
/**
* Main entry to rendering complete template
* @param string template name excluding extension
* @param array extra arguments to set in view scope
* @return string
*/
public function render( $tpl, array $args = null, Loco_mvc_View $parent = null ){
if( $this->block ){
return $this->fork()->render( $tpl, $args, $this );
}
$this->setTemplate($tpl);
if( $parent && $this->template === $parent->template ){
throw new Loco_error_Exception('Avoiding infinite loop');
}
if( is_array($args) ){
foreach( $args as $prop => $value ){
$this->set($prop, $value);
}
}
ob_start();
$content = $this->buffer();
ob_end_clean();
return $content;
}
/**
* Do actual render of currently validated template path
* @return string content not captured in sub-blocks
*/
private function buffer(){
$this->start('_trash');
$this->execTemplate( $this->template );
$this->stop();
$this->block = null;
// decorate via parent view if there is one
if( $this->parent ){
$this->parent->scope = clone $this->scope;
$this->parent->set('_content', $this->_trash );
return $this->parent->buffer();
}
// else at the root of view chain
return (string) $this->_trash;
}
/**
* Set current template
* @param string path tro template, excluding file extension
*/
public function setTemplate( $tpl ){
$file = new Loco_fs_File( $tpl.'.php' );
$file->normalize( $this->cwd );
if( ! $file->exists() ){
$debug = str_replace( loco_plugin_root().'/', '', $file->getPath() );
throw new Loco_error_Exception( 'Template not found: '.$debug );
}
$this->cwd = $file->dirname();
$this->template = $file->getPath();
}
/**
* @return Loco_mvc_View
*/
private function fork(){
$view = new Loco_mvc_View;
$view->cwd = $this->cwd;
$view->scope = clone $this->scope;
return $view;
}
/**
* Do actual runtime template include
*/
private function execTemplate( $template ){
$params = $this->scope;
extract( $params->getArrayCopy() );
include $template;
}
/**
* Link generator
* return Loco_mvc_ViewParams
*/
public function route( $action, array $args = array() ){
return new Loco_mvc_ViewParams( array(
'href' => Loco_mvc_AdminRouter::generate( $action, $args ),
) );
}
/**
* Shorthand for `echo esc_html( sprintf( ...`
* @return void
*/
private function e( $text ){
if( 1 < func_num_args() ){
$args = func_get_args();
$text = call_user_func_array( 'sprintf', $args );
}
echo htmlspecialchars( $text, ENT_COMPAT, 'UTF-8' );
return '';
}
}
/**
* @internal
*/
class _LocoViewBuffer {
private $s;
public function __construct( $s ){
$this->s = $s;
}
public function __toString(){
return $this->s;
}
}
mvc/HiddenFields.php 0000666 00000001554 15214173534 0010402 0 ustar 00 $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;
}
}
mvc/AjaxController.php 0000666 00000004114 15214173534 0011002 0 ustar 00 auth();
$this->output = new ArrayObject;
$this->input = new ArrayObject( $args );
// avoid fatal error if json extension is missing
loco_check_extension('json');
}
/**
* Get posted data and validate nonce in the process
* @return Loco_mvc_PostParams
*/
protected function validate(){
$route = $this->input['route'];
if( ! $this->checkNonce($route) ){
throw new Loco_error_Exception( sprintf('Ajax %s action requires postdata with nonce',$route) );
}
return Loco_mvc_PostParams::get();
}
/**
* {@inheritdoc}
*/
public function get( $prop ){
return isset($this->input[$prop]) ? $this->input[$prop] : null;
}
/**
* {@inheritdoc}
*/
public function set( $prop, $value ){
$this->output[$prop] = $value;
return $this;
}
/**
* @return string JSON
*/
public function render(){
$data = array (
'data' => $this->output->getArrayCopy(),
);
// non-fatal notices deliberately not in "error" key
if( $array = Loco_error_AdminNotices::destroyAjax() ){
$data['notices'] = $array;
}
return json_encode( $data );
}
/**
* Pretty json encode if PHP version allows
*
protected function json_encode( $data ){
$opts = 0;
if( defined('JSON_PRETTY_PRINT') ){
$opts |= JSON_PRETTY_PRINT;
}
if( defined('JSON_UNESCAPED_SLASHES') ){
$opts |= JSON_UNESCAPED_SLASHES;
}
return json_encode( $data, $opts );
}*/
} data/CompiledData.php 0000666 00000002447 15214173534 0010534 0 ustar 00 data = loco_include( $path );
$this->name = $name;
}
public function destroy(){
unset( self::$reg[$this->name], $this->data );
}
public function offsetGet( $k ){
return isset($this->data[$k]) ? $this->data[$k] : null;
}
public function offsetExists( $k ){
return isset($this->data[$k]);
}
public function offsetUnset( $k ){
throw new RuntimeException('Read only');
}
public function offsetSet( $k, $v ){
throw new RuntimeException('Read only');
}
} data/Option.php 0000666 00000002247 15214173534 0007454 0 ustar 00 getKey();
return update_option( $key, $this->getSerializable(), false );
}
/**
* Retrieve and unserialize this object from WordPress options table
* @return bool whether object existed in cache
*/
public function fetch(){
$key = 'loco_'.$this->getKey();
$data = get_option( $key );
try {
$this->setUnserialized($data);
}
catch( InvalidArgumentException $e ){
return false;
}
return true;
}
/**
* Delete option from WordPress
*/
public function remove(){
$key = 'loco_'.$this->getKey();
return delete_option( $key );
}
} data/Serializable.php 0000666 00000006335 15214173534 0010614 0 ustar 00 setFlags( ArrayObject::ARRAY_AS_PROPS );
parent::__construct( $data );
$this->dirty = (bool) $data;
}
/**
* Check if object's properties have change since last clean
* @return bool
*/
public function isDirty(){
return $this->dirty;
}
/**
* Make not dirty
* @return Loco_data_Serializable
*/
protected function clean(){
$this->dirty = false;
return $this;
}
/**
* Call persist method only if has changed since last clean
* @return Loco_data_Serializable
*/
public function persistIfDirty(){
if( $this->isDirty() ){
$params = func_get_args();
call_user_func_array( array($this,'persist'), $params );
}
return $this;
}
/**
* @override so we can set dirty flag
*/
public function offsetSet( $prop, $value ){
if( ! isset($this[$prop]) || $value !== $this[$prop] ){
parent::offsetSet( $prop, $value );
$this->dirty = true;
}
}
/**
* @override so we can set dirty flag
*/
public function offsetUnset( $prop ){
if( isset($this[$prop]) ){
parent::offsetUnset($prop);
$this->dirty = true;
}
}
/**
* @return Loco_data_Serializable
*/
public function setVersion( $version ){
if( $version !== $this->v ){
$this->v = $version;
$this->dirty = true;
}
return $this;
}
/**
* @return string|int|float
*/
public function getVersion(){
return $this->v;
}
/**
* Get serializable data for storage
* @return array
*/
protected function getSerializable(){
return array (
'c' => get_class($this),
'v' => $this->getVersion(),
'd' => $this->getArrayCopy(),
);
}
/**
* Restore object state from array as returned from getSerializable
* @return Loco_data_Serializable
*/
protected function setUnserialized( $data ){
if( ! is_array($data) || ! isset($data['d']) ) {
throw new InvalidArgumentException('Unexpected data');
}
if( get_class($this) !== $data['c'] ){
throw new InvalidArgumentException('Unexpected class name');
}
$this->setVersion( $data['v'] );
// ok to populate ArrayObject
$this->exchangeArray( $data['d'] );
// because object is being restored, probably from disk. this make it clean now
$this->dirty = false;
return $this;
}
} data/RecentItems.php 0000666 00000005724 15214173534 0010431 0 ustar 00 fetch();
}
return self::$current;
}
/**
* Trash data and remove from memory
*/
public static function destroy(){
$tmp = new Loco_data_RecentItems;
$tmp->remove();
self::$current = null;
}
/**
* @internal
* @return Loco_data_RecentItems
*/
private function push( $object, array $indexes ){
foreach( $indexes as $key => $id ){
$stack = isset($this[$key]) ? $this[$key] : array();
// remove before add ensures latest item appended to hashmap
unset($stack[$id]);
$stack[$id] = time();
$this[$key] = $stack;
// TODO prune stack to maximum length
}
return $this;
}
/**
* @return array
*/
private function getItems( $key, $offset, $count ){
$stack = isset($this[$key]) ? $this[$key] : array();
// hash map should automatically be in "push" order, meaning most recent last
// sorting gives wrong order for same-second updates (only relevent in tests, but still..)
// asort( $stack, SORT_NUMERIC );
$stack = array_reverse( array_keys( $stack ) );
if( is_null($count) && 0 === $offset ){
return $stack;
}
return array_slice( $stack, $offset, $count, false );
}
/**
* @return int
*/
private function hasItem( $key, $id ){
if( isset($this[$key]) && ( $items = $this[$key] ) && isset($items[$id]) ){
return $items[$id];
}
return 0;
}
/**
* Push bundle to the front of recent bundles stack
* @return Loco_data_RecentItems
*/
public function pushBundle( Loco_package_Bundle $bundle ){
return $this->push( $bundle, array( 'bundle' => $bundle->getId() ) );
}
/**
* Get bundle IDs
* @return array
*/
public function getBundles( $offset = 0, $count = null ){
return $this->getItems('bundle', $offset, $count );
}
/**
* Check if a bundle has been recently used
* @return int timestamp item was added, 0 if absent
*/
public function hasBundle( $id ){
return $this->hasItem( 'bundle', $id );
}
/**
* TODO other types of item
* Push project to the front of recent bundles stack
* @return Loco_data_RecentItems
*
public function pushProject( Loco_package_Project $project ){
return $this;
}*/
}
data/Transient.php 0000666 00000003107 15214173534 0010147 0 ustar 00 getKey();
$data = $this->getSerializable();
set_transient( $key, $data, $ttl );
$this->lazy = null;
$this->clean();
}
else {
$this->lazy = $ttl;
}
return $this;
}
/**
* Commit to transient cache on object destruction
*/
final public function __destruct(){
if( is_int($this->lazy) ){
$this->persistIfDirty( $this->lazy, true );
}
}
/**
* Retrieve and unserialize this object from WordPress transient cache
* @return bool whether object existed in cache
*/
public function fetch(){
$v = $this->getVersion();
$key = 'loco_'.$this->getKey();
$data = get_transient( $key );
try {
$this->setUnserialized($data);
return true;
}
catch( InvalidArgumentException $e ){
return false;
}
}
} data/Session.php 0000666 00000011251 15214173534 0007622 0 ustar 00 clear();
}
catch( Exception $e ){
// probably no session to destroy
}
self::$current = null;
}
}
/**
* Commit current session data to WordPress storage and remove from memory
*/
public static function close(){
if( self::$current && self::$current->dirty ){
self::$current->persist();
self::$current = null;
}
}
/**
* @internal
*/
final public function __construct( array $raw = array() ){
$this->token = wp_get_session_token();
if( ! $this->token ){
throw new Loco_error_Exception('Failed to get session token');
}
parent::__construct( array() );
$this->manager = WP_Session_Tokens::get_instance( get_current_user_id() );
// populate object from stored session data
$data = $this->getRaw();
if( isset($data['loco']) ){
$this->setUnserialized( $data['loco'] );
}
// any initial arbitrary data can be merged on top
foreach( $raw as $prop => $value ){
$this[$prop] = $value;
}
// enforce single instance
self::$current = $this;
// ensure against unclean shutdown
if( loco_debugging() ){
register_shutdown_function( array($this,'_on_shutdown') );
}
}
/**
* @internal
* Ensure against unclean use of session storage
*/
public function _on_shutdown(){
if( $this->dirty ){
trigger_error('Unclean session shutdown: call either Loco_data_Session::destroy or Loco_data_Session::close');
}
}
/**
* Get raw session data held by WordPress
* @return array
*/
private function getRaw(){
$data = $this->manager->get( $this->token );
// session data will exist if WordPress login is valid
if( ! $data || ! is_array($data) ){
throw new Loco_error_Exception('Invalid session');
}
return $data;
}
/**
* Persist object in WordPress usermeta table
* @return Loco_data_Session
*/
public function persist(){
$data = $this->getRaw();
$data['loco'] = $this->getSerializable();
$this->manager->update( $this->token, $data );
$this->dirty = false;
return $this;
}
/**
* Clear object data and remove our key from WordPress usermeta record
* @return Loco_data_Session
*/
public function clear(){
$data = $this->getRaw();
if( isset($data['loco']) ){
unset( $data['loco'] );
$this->manager->update( $this->token, $data );
}
$this->exchangeArray( array() );
$this->dirty = false;
return $this;
}
/**
* @param string name of messages bag, e.g. "errors"
* @param mixed optionally put data in rather than getting data out
* @return mixed
*/
public function flash( $bag, $data = null ){
if( isset($data) ){
$this->dirty = true;
$this[$bag][] = $data;
return;
}
// else get first object in bag and remove before returning
if( isset($this[$bag]) ){
if( $data = array_shift($this[$bag]) ){
$this->dirty = true;
return $data;
}
}
}
/**
* @internal
*/
public function offsetSet( $index, $newval ){
if( ! isset($this[$index]) || $newval !== $this[$index] ){
$this->dirty = true;
parent::offsetSet( $index, $newval );
}
}
/**
* @internal
*/
public function offsetUnset( $index ){
if( isset($this[$index]) ){
$this->dirty = true;
parent::offsetUnset( $index );
}
}
}
data/Preferences.php 0000666 00000005124 15214173534 0010442 0 ustar 00 '',
);
/**
* 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;
}
} data/Cookie.php 0000666 00000003526 15214173534 0007416 0 ustar 00 setName( $name );
}
}
}
/**
* @internal
*/
public function __toString(){
$data = $this->getArrayCopy();
return http_build_query( $data, null, '&' );
}
/**
* @return Loco_data_Cookie
*/
public function setName( $name ){
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName(){
return $this->name;
}
/**
* Send cookie to the browser, unless filtered out.
* @return bool|null
*/
public function send(){
if( false !== apply_filters( 'loco_setcookie', $this ) ){
$value = (string) $this;
// @codeCoverageIgnoreStart
return setcookie( $this->name, $value, $this->expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
}
}
/**
* Empty values such that sending cookie would remove it from browser
* @return Loco_data_Cookie
*/
public function kill(){
$this->exchangeArray( array() );
$this->expires = time() - 86400;
return $this;
}
} data/Permissions.php 0000666 00000013061 15214173534 0010513 0 ustar 00 = 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;
}
} data/Settings.php 0000666 00000014102 15214173534 0007775 0 ustar 00 '',
// 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;
}
}
test/DummyFtpConnect.php 0000666 00000007622 15214173534 0011333 0 ustar 00 getWriteContext()->connect( new WP_Filesystem_Debug($creds) )`
*/
class Loco_test_DummyFtpConnect extends Loco_hooks_Hookable {
public function filter_filesystem_method(){
return 'debug';
}
}
/**
* Dummy FTP file system.
* WARNING: this actually modifies files - it just does it while simulating a remote connection
* - All operations performed "direct" when authorized, else they fail.
*/
class WP_Filesystem_Debug extends WP_Filesystem_Base {
private $authed;
/**
* @var WP_Error
*/
public $errors;
public function __construct( array $opt ) {
$this->options = $opt;
$this->method = 'ftp';
}
/**
* Dummy FTP connect: requires username=foo password=xxx
*/
public function connect() {
$this->authed = false;
$this->errors = new WP_Error;
// @codeCoverageIgnoreStart
if( empty($this->options['hostname']) ){
$this->errors->add( 'bad_hostname', 'Debug: empty hostname');
return false;
}
if( empty($this->options['username']) ){
$this->errors->add( 'bad_username', 'Debug: empty username');
return false;
}
if( $this->options['username'] !== 'foo' ) {
$this->errors->add( 'bad_username', 'Debug: username expected to be "foo"');
return false;
}
if( empty($this->options['password']) ){
$this->errors->add( 'bad_username', 'Debug: empty password');
return false;
}
if( $this->options['password'] !== 'xxx' ) {
$this->errors->add( 'bad_password', 'Debug: password expected to be "xxx"' );
return false;
}
// @codeCoverageIgnoreEnd
$this->authed = true;
return true;
}
/**
* @return WP_Filesystem_Debug
*/
public function disconnect(){
$this->authed = false;
$this->options = array();
return $this;
}
/**
* {@inheritdoc}
* Dummy function allows exact path to be returned, subject to debugging filters
*/
public function find_folder( $path ){
if( WP_CONTENT_DIR === $path ){
return loco_constant('WP_CONTENT_DIR');
}
return false;
}
/**
* @internal
* Proxies supposed remote call to *real* direct call, as long as instance is authorized.
* Deliberately not extending WP_Filesystem_Direct for safety.
*/
private function _call( $method, array $args ){
if( $this->authed ){
$real = new WP_Filesystem_Direct( null );
return call_user_func_array( array($real,$method), $args );
}
return false;
}
/**
* {@inheritdoc}
*/
public function is_writable( $file ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function chmod( $file, $mode = false, $recursive = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function copy( $source, $destination, $overwrite = false, $mode = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function put_contents( $path, $data, $mode = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function delete( $file, $recursive = false, $type = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
/**
* {@inheritdoc}
*/
public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ){
return $this->_call( __FUNCTION__, func_get_args() );
}
}
test/WordPressTestCase.php 0000666 00000021575 15214173534 0011643 0 ustar 00 prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE '%s'", array('loco_%','_transient_loco_%','_transient_timeout_loco_%') );
if( $results = $wpdb->get_results($query,ARRAY_N) ){
foreach( $results as $row ){
list( $option_name ) = $row;
delete_option( $option_name );
}
}
}
public static function setUpBeforeClass(){
parent::setUpBeforeClass();
Loco_data_Settings::clear();
Loco_data_Session::destroy();
Loco_data_RecentItems::destroy();
self::dropOptions();
}
public static function tearDownAfterClass(){
parent::tearDownAfterClass();
Loco_data_Settings::clear();
Loco_data_Session::destroy();
Loco_data_RecentItems::destroy();
wp_cache_flush();
self::dropOptions();
}
public function setUp(){
parent::setUp();
Loco_mvc_PostParams::destroy();
Loco_error_AdminNotices::destroy();
Loco_package_Listener::destroy();
wp_cache_flush();
// text domains should be unloaded at start of all tests
$GLOBALS['l10n'] = array();
// ensure test themes are registered and WordPress's cache is valid
register_theme_directory( LOCO_TEST_DATA_ROOT.'/themes' );
$sniff = get_theme_roots();
if( ! isset($sniff['empty-theme']) ){
delete_site_transient( 'theme_roots' );
}
// avoid WordPress missing index notices
$GLOBALS['_SERVER'] += array (
'HTTP_HOST' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.0',
'HTTP_USER_AGENT' => 'Loco/'.get_class($this),
);
// tests should always dictate the file system method, which defaults to direct
add_filter('filesystem_method', array($this,'filter_fs_method') );
add_filter('loco_constant_DISALLOW_FILE_MODS', array($this,'filter_fs_allow') );
// capture cookies so we can test what is set
add_filter('loco_setcookie', array($this,'captureCookie'), 10, 1 );
$this->cookies_set = array();
}
/**
* {@inheritdoc}
*/
public function clean_up_global_scope(){
parent::clean_up_global_scope();
$_COOKIE = array();
$_REQUEST = array();
}
/**
* Capture cookie and prevent actual http sending
*/
public function captureCookie( Loco_data_Cookie $cookie ){
$this->cookies_set[ $cookie->getName() ] = $cookie;
return false;
}
/**
* @return Loco_data_Cookie
*/
public function assertCookieSet( $name, $message = '' ){
$this->assertArrayHasKey( $name, $this->cookies_set, $message );
$cookie = $this->cookies_set[ $name ];
$this->assertInstanceOf( 'Loco_data_Cookie', $cookie, $message );
return $cookie;
}
/**
* Invoke admin page controller without full hook set up
*/
public static function renderPage(){
$router = new Loco_mvc_AdminRouter;
$router->on_admin_menu();
$screen = get_current_screen();
$action = isset($_GET['action']) ? $_GET['action'] : null;
$router->initPage( $screen, $action );
return get_echo( array($router,'renderPage') );
}
/**
* Invoke Ajkax controller without full hook set up.
* @return string JSON
*/
protected function renderAjax(){
$router = new Loco_mvc_AjaxRouter;
$router->on_init();
return $router->renderAjax();
}
/**
* @internal
*/
public function filter_fs_method( $method = '' ){
return is_null($this->fs_method) ? $method : $this->fs_method;
}
/**
* @return Loco_test_WordPressTestCase
*/
public function set_fs_method( $method ){
$GLOBALS['wp_filesystem'] = null;
$this->fs_method = $method;
$ping = class_exists('Loco_test_DummyFtpConnect');
return $this;
}
/**
* @return Loco_test_WordPressTestCase
*/
public function disable_file_mods(){
$this->fs_allow = false;
return $this;
}
/**
* @internal
*/
public function filter_fs_allow(){
return ! $this->fs_allow;
}
/**
* Remove files created under tmp
*/
protected function clearTmp(){
$root = new Loco_fs_Directory( LOCO_TEST_DATA_ROOT.'/tmp' );
$dir = new Loco_fs_FileFinder( $root );
$dir->setRecursive( true );
$dirs = array();
/* @var $file Loco_fs_File */
foreach( $dir as $file ){
$dirs[ $file->dirname() ] = true;
$file->unlink();
}
// Be warned only directories found above will be removed
foreach( array_keys($dirs) as $path ){
$dir = new Loco_fs_Directory($path);
while( $dir->exists() && ! $dir->equal($root) ){
$dir->unlink();
$dir = $dir->getParent();
}
}
}
protected function login( $role = 'administrator' ){
$user = self::factory()->user->create( array( 'role' => $role ) );
if( $user instanceof WP_Error ){
foreach( $user->get_error_messages() as $message ){
trigger_error( $message );
}
throw new Exception('Failed to login');
}
// setting user required to have proper user object
$user = wp_set_current_user( $user );
// simulate default permissions used in admin menu hookage
if( $user->has_cap('manage_options') ){
$user->add_cap('loco_admin');
}
// simulate wp_set_auth_cookie. Can't actually set cookie cos headers
$_COOKIE[LOGGED_IN_COOKIE] = wp_generate_auth_cookie( $user->ID, time()+60, 'logged_in' );
$debug = array( 'name' => $this->getName(), 'token' => wp_get_session_token() ,'uid' => $user->ID );
// forcing new session instance
new Loco_data_Session;
}
protected function logout(){
Loco_data_Session::destroy();
wp_destroy_current_session();
unset( $_COOKIE[LOGGED_IN_COOKIE] );
wp_set_current_user( 0 );
$GLOBALS['current_user'] = null;
}
/**
* Switch loco_debugging on
*/
protected function enable_debug(){
add_filter('loco_debug', '__return_true' );
}
/**
* Switch loco_debugging off
*/
protected function disable_debug(){
add_filter('loco_debug', '__return_false' );
}
/**
* Temporarily enable the "en_GB_debug" test locale
*/
protected function enable_debug_locale(){
return $this->enable_locale('en_GB_debug');
}
/**
* Temporarily enable a specific locale
*/
protected function enable_locale( $locale ){
$this->locale = $locale;
add_filter('locale', array($this,'_filter_locale') );
}
/**
* @internal
*/
public function _filter_locale(){
return $this->locale;
}
/**
* Temporarily set test data root to content directory
*/
public function enable_test_content_dir(){
add_filter('loco_constant_WP_CONTENT_DIR', array($this,'_filter_wp_content_dir'), 10, 0 );
}
/**
* @internal
*/
public function _filter_wp_content_dir(){
return LOCO_TEST_DATA_ROOT;
}
public function capture_redirects(){
add_filter('wp_redirect', array($this,'filter_wp_redirect'), 10, 2 );
}
public function filter_wp_redirect( $location, $status ){
$this->redirect = func_get_args();
return false;
}
public function assertRedirected( $status = 302, $message = 'Failed to redirect' ){
$raw = $this->redirect;
$this->assertInternalType('array', $raw, $message );
$this->assertSame( $status, $raw[1], $message );
return $raw[0];
}
public function setPostArray( array $post ){
$_POST = $post;
$_REQUEST = array_merge( $_GET, $_POST, $_COOKIE );
$_SERVER['REQUEST_METHOD'] = 'POST';
Loco_mvc_PostParams::destroy();
}
public function addPostArray( array $post ){
$this->setPostArray( $post + $_POST );
}
public function setGetArray( array $get ){
$_GET = $get;
$_REQUEST = array_merge( $_GET, $_POST, $_COOKIE );
$_SERVER['REQUEST_METHOD'] = 'GET';
}
public function addGetArray( array $get ){
$this->setGetArray( $get + $_GET );
}
} test/TransientObject.php 0000666 00000000247 15214173534 0011346 0 ustar 00 preserveWhitespace = false;
$dom->formatOutput = false;
$dom->loadXML( ''.$src.'' );
$dom->normalizeDocument();
$src = $dom->saveXML();
return trim( preg_replace( '/>\s+', '><', $src ) );
}
public function assertSameHtml( $expect, $actual, $message = null ){
return $this->assertSame( $this->normalizeHtml($expect), $this->normalizeHtml($actual), $message );
}
} test/TestFilters.php 0000666 00000001015 15214173534 0010512 0 ustar 00 formatOutput = true;
$dom->registerNodeClass('DOMElement','LocoConfig_DOMElement');
$this->xpath = new DOMXPath($dom);
return $dom;
}
/**
* {@inheritdoc}
* @return LocoConfigNodeListIterator
*/
public function query( $query, $context = null ){
$list = $this->xpath->query( $query, $context );
return new LocoConfigNodeListIterator( $list );
}
/**
* @return void
*/
public function loadXml( $source ){
if( ! $source ){
throw new Loco_error_XmlParseException( __('XML supplied is empty','loco') );
}
$dom = $this->getDom();
// parse with silent errors, clearing after
$used_errors = libxml_use_internal_errors(true);
$result = $dom->loadXML( $source, LIBXML_NONET );
unset( $source );
// fetch errors and ensure clean for next run.
$errors = libxml_get_errors();
$used_errors || libxml_use_internal_errors(false);
libxml_clear_errors();
// Throw exception if error level exceeds current tolerance
if( $errors ){
/* @var $error LibXMLError */
foreach( $errors as $error ){
if( $error->level >= LIBXML_ERR_FATAL ){
$e = new Loco_error_XmlParseException( trim($error->message) );
//$e->setContext( $error->line, $error->column, $source );
throw $e;
} // @codeCoverageIgnoreStart
}
}
// @codeCoverageIgnoreEnd
// Not currently validating against a DTD, but may as well pre-empt generic model loading errors
if( ! $dom->documentElement || 'bundle' !== $dom->documentElement->nodeName ){
throw new Loco_error_XmlParseException('Expected document element');
}
$this->xpath = new DOMXPath($dom);
}
/**
* {@inheritdoc}
* Overridden to avoid empty text nodes in XML files, preferring . to
*/
protected function setFileElementPath( $node, $path ){
if( ! $path && '0' !== $path ){
$path = '.';
}
return parent::setFileElementPath( $node, $path );
}
}
/**
* @internal
*/
class LocoConfig_DOMElement extends DOMElement implements IteratorAggregate, Countable {
public function getIterator(){
return new LocoConfigNodeListIterator( $this->childNodes );
}
public function count(){
return $this->childNodes->length;
}
}
/**
* @internal
* Cos NodeList doesn't iterate
*/
class LocoConfigNodeListIterator implements Iterator, Countable, ArrayAccess {
/**
* @var DOMNodeList
*/
private $nodes;
/**
* @var int
*/
private $i;
/**
* @var int
*/
private $n;
public function __construct( DOMNodeList $nodes ){
$this->nodes = $nodes;
$this->n = $nodes->length;
}
public function count(){
return $this->n;
}
public function rewind(){
$this->i = -1;
$this->next();
}
public function key(){
return $this->i;
}
public function current(){
return $this->nodes->item( $this->i );
}
public function valid(){
return is_int($this->i);
}
public function next(){
while( true ){
$this->i++;
if( $child = $this->nodes->item($this->i) ){
break;
}
$this->i = null;
break;
}
}
public function offsetExists( $i ){
return $i >= 0 && $i < $this->n;
}
public function offsetGet( $i ){
return $this->nodes->item($i);
}
/**
* @codeCoverageIgnore
*/
public function offsetSet( $i, $value ){
throw new Exception('Read only');
}
/**
* @codeCoverageIgnore
*/
public function offsetUnset( $i ){
throw new Exception('Read only');
}
}
config/CustomSaved.php 0000666 00000002161 15214173534 0010770 0 ustar 00 bundle->getType() ).'_config__'.$this->bundle->getHandle();
}
/**
* {@inheritdoc}
*/
public function persist(){
$writer = new Loco_config_BundleWriter( $this->bundle );
$this->exchangeArray( $writer->toArray() );
return parent::persist();
}
/**
* @return Loco_config_CustomSaved
*/
public function setBundle( Loco_package_Bundle $bundle ){
$this->bundle = $bundle;
return $this;
}
/**
* Modify currently set bundle according to saved config data
* @return Loco_package_Bundle
*/
public function configure(){
$this->bundle->clear();
$reader = new Loco_config_BundleReader( $this->bundle );
$reader->loadArray( $this->getArrayCopy() );
return $this->bundle;
}
} config/BundleWriter.php 0000666 00000013070 15214173534 0011142 0 ustar 00 bundle = $bundle;
}
/**
* @return string XML source
*/
public function toXml(){
$model = new Loco_config_XMLModel;
$dom = $this->compile($model);
return $dom->saveXML();
}
/**
* @return array
*/
public function toArray(){
$model = new Loco_config_ArrayModel;
$dom = $this->compile($model);
return $dom->export();
}
/**
* @return Loco_mvc_PostParams
*/
public function toForm(){
$model = new Loco_config_FormModel;
$dom = $this->compile($model);
return $model->getPost();
}
/**
* Alias of toArray implementing JsonSerializable
* @return array
*/
public function jsonSerialize(){
return $this->toArray();
}
/**
* Agnostic compilation of any config data type
* @return LocoConfigDocumentInterface
*/
private function compile( Loco_config_Model $model ){
$bundle = $this->bundle;
$model->setDirectoryPath( $bundle->getDirectoryPath() );
$systemTargets = $bundle->getSystemTargets();
$dom = $model->getDom();
$root = $dom->appendChild( $dom->createElement('bundle') );
$root->setAttribute( 'name', $bundle->getName() );
/*/ additional headers for information only (not read back in)
if( $value = $bundle->getHeaderInfo()->getVendorHost() ){
$root->setAttribute( 'vendor', $value );
}*/
foreach( $bundle->exportGrouped() as $domainName => $projects ){
$domainElement = $root->appendChild( $dom->createElement('domain') );
$domainElement->setAttribute( 'name', $domainName );
/* @var $proj Loco_package_Project */
foreach( $projects as $proj ){
$projElement = $domainElement->appendChild( $dom->createElement('project') );
// add project name even if it's the same as the bundle name
// when loading however, missing name will default to bundle name
$value = $proj->getName() or $value = $bundle->getName();
$projElement->setAttribute( 'name', $value );
// add project slug even if it's the same as the domain name
$value = $proj->getSlug();
$projElement->setAttribute( 'slug', $value );
//
// zero or more source file locations
$sourcesElement = $dom->createElement('source');
/* @var $file Loco_fs_Directory */
foreach( $proj->getConfiguredSources() as $file ){
$sourcesElement->appendChild( $model->createFileElement($file) );
}
// zero or more excluded source paths
$excludeElement = $dom->createElement('exclude');
foreach( $proj->getConfiguredSourcesExcluded() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$sourcesElement->appendChild($excludeElement);
}
if( $sourcesElement->hasChildNodes() ){
$projElement->appendChild( $sourcesElement );
}
//
// add zero or more target locations
$targetsElement = $dom->createElement('target');
/* @var $file Loco_fs_Directory */
foreach( $proj->getConfiguredTargets() as $file ){
if( ! in_array( $file->getPath(), $systemTargets, true ) ){
$targetsElement->appendChild( $model->createFileElement($file) );
}
}
// zero or more excluded targets
$excludeElement = $dom->createElement('exclude');
foreach( $proj->getConfiguredTargetsExcluded() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$targetsElement->appendChild($excludeElement);
}
if( $targetsElement->hasChildNodes() ){
$projElement->appendChild( $targetsElement );
}
//
// add single POT template location
if( $file = $proj->getPot() ){
$templateElement = $projElement->appendChild( $dom->createElement('template') );
$templateElement->appendChild( $model->createFileElement($file) );
// template may be prortected from end-user tampering
if( $proj->isPotLocked() ){
$templateElement->setAttribute('locked','true');
}
}
}
}
// Write bundle-level path exclusions
$excludeElement = $dom->createElement('exclude');
foreach( $bundle->getExcludedLocations() as $file ){
$excludeElement->appendChild( $model->createFileElement($file) );
}
if( $excludeElement->hasChildNodes() ){
$root->appendChild( $excludeElement );
}
return $dom;
}
} config/BundleReader.php 0000666 00000015222 15214173534 0011071 0 ustar 00 bundle = $bundle;
}
/**
* @return Loco_package_Bundle
*/
public function loadXml( Loco_fs_File $file ){
$this->bundle->setDirectoryPath( $file->dirname() );
$model = new Loco_config_XMLModel;
$model->loadXml( $file->getContents() );
return $this->loadModel( $model );
}
/**
* @return Loco_package_Bundle
*/
public function loadJson( Loco_fs_File $file ){
$this->bundle->setDirectoryPath( $file->dirname() );
return $this->loadArray( json_decode( $file->getContents(), true ) );
}
/**
* @return Loco_package_Bundle
*/
public function loadArray( array $raw ){
$model = new Loco_config_ArrayModel;
$model->loadArray( $raw );
return $this->loadModel( $model );
}
/**
* Agnostic construction of Bundle from any configuration format
* @return Loco_package_Bundle
*/
public function loadModel( Loco_config_Model $model ){
// Base directory required to resolve relative paths
$bundle = $this->bundle;
$model->setDirectoryPath( $bundle->getDirectoryPath() );
$dom = $model->getDom();
$bundleElement = $dom->documentElement;
if( ! $bundleElement || 'bundle' !== $bundleElement->nodeName ){
throw new InvalidArgumentException('Expected root bundle element');
}
// Set bundle meta data if configured
// note that bundles have no inherent slug as it can change according to plugin/theme directory naming
if( $bundleElement->hasAttribute('name') ){
$bundle->setName( $bundleElement->getAttribute('name') );
}
// Bundle-level path exclusions
foreach( $model->query('exclude/*',$bundleElement) as $fileElement ){
$bundle->excludeLocation( $model->evaluateFileElement($fileElement) );
}
/* @var $domainElement LocoConfigElement */
foreach( $model->query('domain',$bundleElement) as $domainElement ){
$slug = $domainElement->getAttribute('name') or $slug = $bundle->getSlug();
// bundle may not have a handle set (most likely only in tests)
if( ! $bundle->getHandle() ){
$bundle->setHandle( $slug );
}
// Text Domain may also be declared by bundle author
$domain = new Loco_package_TextDomain( $slug );
$declared = $bundle->getHeaderInfo();
if( $declared && $declared->TextDomain === $slug ){
$domain->setCanonical( true );
}
/* @var $projectElement LocoConfigElement */
foreach( $model->query('project',$domainElement) as $projectElement ){
$name = $projectElement->getAttribute('name') or $name = $bundle->getName();
$project = new Loco_package_Project( $bundle, $domain, $name );
if( $projectElement->hasAttribute('slug') ){
$project->setSlug( $projectElement->getAttribute('slug') );
}
//
foreach( $model->query('source',$projectElement) as $sourceElement ){
// sources may be , or pass in special if it could be either
foreach( $model->query('file',$sourceElement) as $fileElement ){
$project->addSourceFile( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('directory',$sourceElement) as $fileElement ){
$project->addSourceDirectory( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('path',$sourceElement) as $fileElement ){
$project->addSourceLocation( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('exclude/*', $sourceElement) as $fileElement ){
$project->excludeSourcePath( $model->evaluateFileElement($fileElement) );
}
}
// Avoid having no source locations
if( ! $project->hasSourceFiles() ){
if( $bundle->isSingleFile() ){
$project->addSourceFile( $bundle->getBootstrapPath() );
}
else {
$project->addSourceDirectory( $bundle->getDirectoryPath() );
}
}
//
foreach( $model->query('target',$projectElement) as $targetElement ){
// targets support only directory paths:
foreach( $model->query('directory',$targetElement) as $fileElement ){
$project->addTargetDirectory( $model->evaluateFileElement($fileElement) );
}
foreach( $model->query('exclude/*', $targetElement) as $fileElement ){
$project->excludeTargetPath( $model->evaluateFileElement($fileElement) );
}
}
// Avoid having no target locations ..
if( 0 === count($project->getConfiguredTargets() ) ){
// .. unless the inherited root is a global location
if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
$project->addTargetDirectory( $bundle->getDirectoryPath() );
}
}
//
// configure POT file, should only be one
foreach( $model->query('template',$projectElement) as $templateElement ){
if( $model->evaulateBooleanAttribute( $templateElement, 'locked') ){
$project->setPotLock( true );
}
foreach( $model->query('file',$templateElement) as $fileElement ){
$project->setPot( $model->evaluateFileElement( $fileElement ) );
break 2;
}
}
// add project last for additional configs to be appended
$bundle->addProject( $project );
}
}
return $bundle;
}
} config/ArrayModel.php 0000666 00000020530 15214173534 0010572 0 ustar 00 loadArray( $root );
}
/**
* Construct model from exported array
* @return void
*/
public function loadArray( array $root ){
$dom = $this->getDom();
$dom->load( array('#document', array(), array($root) ) );
}
/**
* {@inheritdoc}
* Emulates *very limited* XPath queries used by the XML DOM.
*/
public function query( $query, $context = null ){
$match = new LocoConfigNodeList;
$query = explode('/', $query );
// absolute path always starts in document
if( $absolute = empty($query[0]) ){
$match->append( $this->getDom() );
}
// else start with base for relative path
else if( $context instanceof LocoConfigNode ){
$match->append( $context );
}
while( $query ){
$name = array_shift($query);
// self references do nothing
if( ! $name || '.' === $name ){
continue;
}
// match all current branches to produce new set of parents
$next = new LocoConfigNodeList;
foreach( $match as $parent ){
foreach( $parent->childNodes as $child ){
if( $name === $child->nodeName || ( '*' === $name && $child instanceof LocoConfigElement ) || ( 'text()' === $name && $child instanceof LocoConfigText) ){
$next->append( $child );
}
}
}
$match = $next;
}
return $match;
}
}
// The following classes are "private" to this file:
// They partially implement the same interfaces as the core DOM classes and are used for code hints.
// Interfaces are deliberately not used as the real DOM classes would not be able to implement them.
/**
* Node
*/
abstract class LocoConfigNode implements IteratorAggregate {
/**
* Raw data of internal format
* @var array
*/
protected $data;
/**
* Child nodes once cast to node objects
* @var LocoConfigNodeList
*/
protected $children;
/**
* @return mixed
*/
abstract public function export();
final public function __construct( $data ){
$this->data = $data;
}
protected function get_nodeName(){
return $this->data[0];
}
/*protected function get_attributes(){
return $this->data[1];
}*/
protected function get_childNodes(){
return $this->getIterator();
}
public function __get( $prop ){
$method = array( $this, 'get_'.$prop );
if( is_callable($method) ){
return call_user_func( $method );
}
}
/** @return LocoConfigNode */
public function appendChild( LocoConfigNode $child ){
$children = $this->getIterator();
$children->append( $child );
return $child;
}
/** @return bool */
public function hasChildNodes(){
return (bool) count( $this->getIterator() );
}
/**
* @return LocoConfigNodeList
*/
public function getIterator(){
if( ! $this->children ){
$raw = isset($this->data[2]) ? $this->data[2] : array();
$this->children = new LocoConfigNodeList( $this->data[2] );
}
return $this->children;
}
public function get_textContent(){
$s = '';
foreach( $this as $child ){
$s .= $child->get_textContent();
}
return $s;
}
}
/**
* NodeList
*/
class LocoConfigNodeList implements Iterator, Countable, ArrayAccess {
private $nodes;
private $i;
private $n;
public function __construct( array $nodes = array() ){
$this->nodes = $nodes;
$this->n = count( $nodes );
}
public function count(){
return $this->n;
}
public function rewind(){
$this->i = -1;
$this->next();
}
public function key(){
return $this->i;
}
public function current(){
return $this[ $this->i ];
}
public function valid(){
return is_int($this->i);
}
public function next(){
if( ++$this->i === $this->n ){
$this->i = null;
}
}
public function offsetExists( $i ){
return $i >= 0 && $i < $this->n;
}
public function offsetGet( $i ){
$node = $this->nodes[$i];
if( ! $node instanceof LocoConfigNode ){
if( is_array($node) ){
$node = new LocoConfigElement( $node );
}
else {
$node = new LocoConfigText( $node );
}
$this->nodes[$i] = $node;
}
return $node;
}
/**
* @codeCoverageIgnore
*/
public function offsetSet( $i, $value ){
throw new Exception('Use append');
}
/**
* @codeCoverageIgnore
*/
public function offsetUnset( $i ){
throw new Exception('Read only');
}
public function append( LocoConfigNode $node ){
$this->nodes[] = $node;
$this->n++;
}
/**
* Revert nodes back to raw array form and return for exporting
* @return array
*/
public function normalize(){
foreach( $this->nodes as $i => $node ){
if( $node instanceof LocoConfigNode ){
$this->nodes[$i] = $node->export();
}
}
return $this->nodes;
}
}
/**
* Document
*/
class LocoConfigDocument extends LocoConfigNode {
/**
* Rapidly set new data for document
*/
public function load( $data ){
$this->data = $data;
$this->children = null;
}
/**
* @return LocoConfigElement
*/
public function createElement( $name ){
return new LocoConfigElement( array( $name, array(), array() ) );
}
/**
* @return LocoConfigText
*/
public function createTextNode( $text ){
return new LocoConfigText( $text );
}
/**
* @return LocoConfigElement
*/
public function get_documentElement(){
$child = null;
foreach( $this as $child ){
break;
}
return $child;
}
/**
* {@inheritdoc}
* Override to keep single element root
*/
public function export(){
if( $root = $this->get_documentElement() ){
return $root->export();
}
}
}
/**
* Element
*/
class LocoConfigElement extends LocoConfigNode {
public function setAttribute( $prop, $value ){
$this->data[1][$prop] = $value;
}
public function removeAttribute( $prop ){
unset( $this->data[1][$prop] );
}
public function getAttribute( $prop ){
if( isset($this->data[1][$prop]) ){
return $this->data[1][$prop];
}
return '';
}
public function hasAttribute( $prop ){
return isset($this->data[1][$prop]);
}
/**
* {@inheritdoc}
*/
public function export(){
$raw = $this->data;
// return any cast elements back to raw data
if( $this->children ){
$raw[2] = $this->children->normalize();
}
return $raw;
}
}
/**
* Text
*/
class LocoConfigText extends LocoConfigNode {
protected function get_nodeName(){
return '#text';
}
public function hasChildNodes(){
return false;
}
public function getIterator(){
return new ArrayIterator;
}
public function export(){
return (string) $this->data;
}
public function get_nodeValue(){
return (string) $this->data;
}
public function get_textContent(){
return (string) $this->data;
}
}
config/FormModel.php 0000666 00000020023 15214173534 0010414 0 ustar 00 getDom();
$root = $dom->documentElement;
$post = new Loco_mvc_PostParams( array (
'name' => $root->getAttribute('name'),
'exclude' => array (
'path' => '',
),
'conf' => array(),
) );
/* @var LocoConfigElement $domain */
foreach( $this->query('domain',$root) as $domain ){
$domainName = $domain->getAttribute('name');
/* @var LocoConfigElement $project */
foreach( $domain as $project ){
$tree = array (
'name' => $project->getAttribute('name'),
'slug' => $project->getAttribute('slug'),
'domain' => $domainName,
'source' => array (
'path' => '',
'exclude' => array( 'path' => '' ),
),
'target' => array (
'path' => '',
'exclude' => array( 'path' => '' ),
),
'template' => array( 'path' => '', 'locked' => false ),
);
$post['conf'][] = $this->collectPaths( $project, $tree );
}
}
/* @var LocoConfigElement $paths */
foreach( $this->query('exclude',$root) as $paths ){
$post['exclude'] = $this->collectPaths( $paths, $post['exclude'] );
}
return $post;
}
private function collectPaths( LocoConfigElement $parent, array $branch ){
$texts = array();
foreach( $parent as $child ){
$name = $child->nodeName;
// all file types as "path" in form model
if( 'file' === $name || 'directory' === $name ){
$name = 'path';
}
if( isset($branch[$name]) ){
// collect text if child is a node
if( 'path' === $name ){
$file = $this->evaluateFileElement($child);
$path = $file->getRelativePath( $this->getDirectoryPath() );
if( '' === $path ){
$path = '.';
}
$texts[] = $path;
}
// else could be simple key to next depth
else if( is_array($branch[$name]) ){
$branch[$name] = $this->collectPaths( $child, $branch[$name] );
}
}
// @codeCoverageIgnoreStart
else {
throw new Exception('Unexpected structure: '.$name.' not in '.json_encode($branch) );
}
// @codeCoverageIgnoreEnd
}
// parent may have attributes we can set in branch data
foreach( $branch as $name => $default ){
if( $parent->hasAttribute($name) ){
if( is_bool($default) ){
$branch[$name] = $this->evaulateBooleanAttribute($parent, $name);
}
else {
$branch[$name] = $parent->getAttribute($name);
}
}
}
// set compiled path values if any collected
if( $texts ){
$value = implode("\n", $texts );
// display single root path as empty, but not when additional paths defined
if( '.' === $value ){
$branch['path'] = '';
}
else {
$branch['path'] = $value;
}
}
return $branch;
}
/**
* Construct model from posted form data.
* @return void
*/
public function loadForm( Loco_mvc_PostParams $post ){
// basic validation unlikely to fail when posted from UI
$name = $post->name;
if( ! $name ){
throw new InvalidArgumentException('Bundle must have a name');
}
$confs = $post->conf;
if( ! $confs || ! is_array($confs) ){
throw new InvalidArgumentException('Bundle must have at least one definition');
}
// transform posted data into internal model:
// deliberately not configuring bundle object at this point. simply converting data for storage.
$dom = $this->getDom();
$root = $dom->appendChild( $dom->createElement('bundle') );
$root->setAttribute( 'name', $name );
// bundle level excluded paths
if( $nodes = array_intersect_key( $post->getArrayCopy(), array( 'exclude' => '' ) ) ) {
$this->loadStruct( $root, $nodes );
}
// collect all projects grouped by domain
$domains = array();
foreach( $confs as $i => $conf ){
if( ! empty($conf['removed']) ){
continue;
}
if( empty($conf['domain']) ){
throw new InvalidArgumentException( __('Text Domain cannot be empty','loco') );
}
$domains[ $conf['domain'] ][] = $project = $dom->createElement('project');
// project attributes
foreach( array('name','slug') as $attr ){
if( isset($conf[$attr]) ){
$project->setAttribute( $attr, $conf[$attr] );
}
}
// project children
if( $nodes = array_intersect_key( $conf, array( 'source' => '', 'target' => '', 'template' => '' ) ) ) {
$this->loadStruct( $project, $nodes );
}
}
// add all domains and their projects
foreach( $domains as $name => $projects ){
$parent = $root->appendChild( $dom->createElement('domain') );
$parent->setAttribute( 'name', $name );
/* @var $project LocoConfigElement */
foreach( $projects as $project ){
$parent->appendChild( $project );
}
}
}
/**
* Recursively add array structure into model.
* - Text nodes are split into one parent element per line.
* - Elements added here cannot have attributes, but are not expected to as they came from form fields
*/
private function loadStruct( LocoConfigElement $parent, array $nodes ){
$dom = $this->getDom();
foreach( $nodes as $name => $data ){
if( is_string($data) ){
// support common path containing elements
if( 'file' === $name || 'directory' === $name || 'path' === $name ){
// form model has multiline "path" nodes which we'll expand from non-empty lines
// resolving empty paths to "." must be done elsewhere. here empty means ignore.
foreach( preg_split('/\\R/', trim( $data,"\n\r"), -1, PREG_SPLIT_NO_EMPTY ) as $path ){
$ext = pathinfo( $path, PATHINFO_EXTENSION );
$child = $parent->appendChild( $dom->createElement( $ext ? 'file' : 'directory' ) );
$child->appendChild( $dom->createTextNode($path) );
}
}
// else assume valud is an attribute
else {
$parent->setAttribute( $name, $data );
}
}
else if( is_bool($data) ){
$data ? $parent->setAttribute($name,'true') : $parent->removeAttribute($name);
}
else if( ! is_array($data) ){
throw new InvalidArgumentException('Invalid datatype');
}
else {
$child = $parent->appendChild( $dom->createElement($name) );
$this->loadStruct( $child, $data );
}
}
}
}
config/Model.php 0000666 00000011415 15214173534 0007575 0 ustar 00 dirs = array();
$this->dom = $this->createDom();
$this->setDirectoryPath( loco_constant('ABSPATH') );
}
/**
* @return void
*/
public function setDirectoryPath( $path, $key = null ){
$path = rtrim( $path, '/' );
if( is_null($key) ){
$this->base = $path;
}
else {
$this->dirs[$key] = $path;
}
}
/**
* @return LocoConfigDocument
*/
public function getDom(){
return $this->dom;
}
/**
* Evaluate a name constant pointing to a file location
* @param string one of 'LOCO_LANG_DIR', 'WP_LANG_DIR', 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', or 'ABSPATH'
*/
public function getDirectoryPath( $key = null ){
if( is_null($key) ){
$value = $this->base;
}
else if( isset($this->dirs[$key]) ){
$value = $this->dirs[$key];
}
else {
$value = rtrim( loco_constant($key), '/' );
}
return $value;
}
/**
* @return LocoConfigElement
*/
public function createFileElement( Loco_fs_File $file ){
$node = $this->dom->createElement( $file->isDirectory() ? 'directory' : 'file' );
if( $path = $file->getPath() ) {
// Calculate relative path to the config file itself
$relpath = $file->getRelativePath( $this->base );
// Map to a configured base path if target is not under our root. This makes XML more portable
// matching order is most specific first, resulting in shortest path
if( $relpath && ( Loco_fs_File::abs($relpath) || '..' === substr($relpath,0,2) || $this->base === $this->getDirectoryPath('ABSPATH') ) ){
$bases = array( 'LOCO_LANG_DIR', 'WP_LANG_DIR', 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH' );
foreach( $bases as $key ){
if( ( $base = $this->getDirectoryPath($key) ) && $base !== $this->base ){
$base .= '/';
$len = strlen($base);
if( substr($path,0,$len) === $base ){
$node->setAttribute('base',$key);
$relpath = substr( $path, $len );
break;
}
} // @codeCoverageIgnore
}
}
$path = $relpath;
}
$this->setFileElementPath( $node, $path );
return $node;
}
/**
* @param LocoConfigElement
* @param string
* @return LocoConfigText
*/
protected function setFileElementPath( $node, $path ){
return $node->appendChild( $this->dom->createTextNode($path) );
}
/**
* @param LocoConfigElement
* @return Loco_fs_File
*/
public function evaluateFileElement( $el ){
$path = $el->textContent;
switch( $el->nodeName ){
case 'directory':
$file = new Loco_fs_Directory($path);
break;
case 'file':
$file = new Loco_fs_File($path);
break;
case 'path':
$file = new Loco_fs_File($path);
if( $file->isDirectory() ){
$file = new Loco_fs_Directory($path);
}
break;
default:
throw new InvalidArgumentException('Cannot evaluate file element from <'.$el->nodeName.'>');
}
if( $el->hasAttribute('base') ){
$key = $el->getAttribute('base');
$base = $this->getDirectoryPath($key);
$file->normalize( $base );
}
else {
$file->normalize( $this->base );
}
return $file;
}
/**
* @param LocoConfigElement
* @return bool
*/
public function evaulateBooleanAttribute( $el, $attr ){
if( ! $el->hasAttribute($attr) ){
return false;
}
$value = (string) $el->getAttribute($attr);
return 'false' !== $value && 'no' !== $value && '' !== $value;
}
}
Locale.php 0000666 00000014633 15214173534 0006474 0 ustar 00 setSubtags( loco_parse_locale($tag) );
}
catch( Exception $e ){
// isValid should return false
}
return $locale;
}
public function __construct( $lang = '', $region = '', $variant = '' ){
$this->tag = compact('lang','region','variant');
}
/**
* @internal
* Allow read-only access to subtags
*/
public function __get( $t ){
return isset($this->tag[$t]) ? $this->tag[$t] : '';
}
/**
* Set subtags as produced from loco_parse_locale
* @return Loco_Locale
*/
public function setSubtags( array $tag ){
$default = array( 'lang' => '', 'region' => '', 'variant' => '' );
// disallow setting of unsupported tags
if( $bad = array_diff_key($tag, $default) ){
throw new Loco_error_LocaleException('Unsupported subtags: '.implode(',',$bad) );
}
$tag += $default;
// language tag is minimum requirement
if( ! $tag['lang'] ){
throw new Loco_error_LocaleException('Locale must have a language');
}
// no UN codes in Wordpress
if( is_numeric($tag['region']) ){
throw new Loco_error_LocaleException('Numeric regions not supported');
}
// single, scalar variant. Only using for Formal german currently.
if( is_array($tag['variant']) ){
$tag['variant'] = implode('_',$tag['variant']);
}
$this->tag = $tag;
return $this;
}
/**
* @return string
*/
public function __toString(){
return implode('_',array_filter($this->tag));
}
/**
* @return string
*/
public function getName(){
return (string) $this->name;
}
/**
* @return string
*/
public function getNativeName(){
return (string) $this->_name;
}
/**
* @return string
*/
public function getIcon(){
$tag = array();
if( ! $this->tag['lang'] ){
$tag[] = 'lang lang-zxx';
}
foreach( $this->tag as $class => $code ){
if( $code ){
$tag[] = $class.' '.$class.'-'.$code;
}
}
return strtolower( implode(' ',$tag) );
}
/**
* @return Loco_Locale
*/
public function setName( $english_name, $native_name = '' ){
$this->name = $english_name;
$this->_name = $native_name;
return $this;
}
/**
* Test whether locale is valid
*/
public function isValid(){
return (bool) $this->tag['lang']; // && 'zxx' !== $this->tag['lang'];
}
/**
* @return Loco_Locale
*/
public function normalize(){
$this->tag['lang'] = strtolower($this->tag['lang']);
$this->tag['region'] = strtoupper($this->tag['region']);
$this->tag['variant'] = strtolower($this->tag['variant']);
return $this;
}
/**
* Resolve this locale's "official" name from WordPress's translation api
* @return string English name currently set
*/
public function fetchName( Loco_api_WordPressTranslations $api ){
$tag = $this->normalize()->__toString();
if( $raw = $api->getLocaleData($tag) ){
$this->setName( $raw['english_name'], $raw['native_name'] );
}
return $this->name;
}
/**
* Resolve this locale's name from compiled Loco data
* @return string English name currently set
*/
public function buildName(){
$names = array();
// should at least have a language or not valid
if( $this->isValid() ){
$code = $this->tag['lang'];
$db = Loco_data_CompiledData::get('languages');
if( $name = $db[$code] ){
// if variant is present add only that in brackets (no lookup required)
if( $code = $this->tag['variant'] ){
$name .= ' ('.ucfirst($code).')';
}
// else add region in brackets if present
else if( $code = $this->tag['region'] ){
$db = Loco_data_CompiledData::get('regions');
if( $extra = $db[$code] ){
$name .= ' ('.$extra.')';
}
else {
$name .= ' ('.$code.')';
}
}
$this->setName( $name );
}
}
else {
$this->name = __('Invalid locale','loco');
}
return $this->name;
}
/**
* @return array
*/
public function jsonSerialize(){
$a = $this->tag;
$a['label'] = $this->name;
// plural data expected by editor
$p = $this->getPluralData();
$a['pluraleq'] = $p[0];
$a['plurals'] = $p[1];
$a['nplurals'] = count($p[1]);
return $a;
}
/**
* Get raw plural data
* @internal
* @return array
*/
public function getPluralData(){
if( ! $this->plurals ){
$db = Loco_data_CompiledData::get('plurals');
$lc = $this->lang;
$id = isset($db[$lc]) ? $db[$lc] : 0;
$this->plurals = $db[''][$id];
}
return $this->plurals;
}
/**
* Get PO style Plural-Forms header value comprising number of forms and integer equation for n
* @return string
*/
public function getPluralFormsHeader(){
list( $equation, $forms ) = $this->getPluralData();
return sprintf('nplurals=%u; plural=%s;', count($forms), $equation );
}
/**
* @return string
*/
public function exportJson(){
return json_encode( $this->jsonSerialize() );
}
}
// Depends on compiled library
if( ! function_exists('loco_parse_locale') ){
loco_include('lib/compiled/locales.php');
}
ajax/common/BundleController.php 0000666 00000002173 15214173534 0012761 0 ustar 00 get('bundle') ){
// type may be passed as separate argument
if( $type = $this->get('type') ){
return Loco_package_Bundle::createType( $type, $id );
}
// else embedded in standalone bundle identifier
// TODO standardize this across all Ajax end points
return Loco_package_Bundle::fromId($id);
}
// else may have type embedded in bundle
throw new Loco_error_Exception('No bundle identifier posted');
}
/**
* @return Loco_package_Project
*/
protected function getProject( Loco_package_Bundle $bundle ){
$project = $bundle->getProjectById( $this->get('domain') );
if( ! $project ){
throw new Loco_error_Exception('Failed to find translation project');
}
return $project;
}
} ajax/MsginitController.php 0000666 00000010516 15214173534 0011672 0 ustar 00 get('use-selector') ){
$tag = $this->get('select-locale');
}
else {
$tag = $this->get('custom-locale');
}
$locale = Loco_Locale::parse($tag);
if( ! $locale->isValid() ){
throw new Loco_error_LocaleException('Invalid locale');
}
return $locale;
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
$locale = $this->getLocale();
$suffix = (string) $locale;
// The front end posts a template path, so we must replace the actual locale code
$base = loco_constant('WP_CONTENT_DIR');
$path = $post->path[ $post['select-path'] ];
$pofile = new Loco_fs_LocaleFile( $path );
if( $suffix !== $pofile->getSuffix() ){
$pofile = $pofile->cloneLocale( $locale );
if( $suffix !== $pofile->getSuffix() ){
throw new Loco_error_Exception('Failed to suffix file path with locale code');
}
}
// target PO should not exist yet
$pofile->normalize( $base );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate( $pofile );
// target MO shouldn't exist either, but we don't want to overwrite it without asking
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
throw new Loco_error_Exception( __('MO file exists for this language already. Delete it first','loco') );
}
// Permit forcing of any parsable file as strings template
if( $source = $post->source ){
$potfile = new Loco_fs_File( $source );
$potfile->normalize( $base );
$data = Loco_gettext_Data::load($potfile);
// Remove target strings when copying PO
if( $post->strip ){
$data->strip();
}
}
// else parse POT file if project defines one that exists
else if( ( $potfile = $project->getPot() ) && $potfile->exists() ){
$data = Loco_gettext_Data::load($potfile);
}
// else extract directly from source code, assuming domain passed though from front end
else {
$extr = new Loco_gettext_Extraction( $bundle );
$domain = (string) $project->getDomain();
$data = $extr->addProject($project)->includeMeta()->getTemplate($domain);
$potfile = null;
}
// Let template define Project-Id-Version, else set header to current project name
$headers = array();
$vers = $data->getHeaders()->{'Project-Id-Version'};
if( ! $vers || 'PACKAGE VERSION' === $vers ){
$headers['Project-Id-Version'] = $project->getName();
}
// relative path from bundle root to the template/source this file was created from
if( $potfile && $post->link ){
$headers['X-Loco-Template'] = $potfile->getRelativePath( $bundle->getDirectoryPath() );
}
$data->localize( $locale, $headers );
$posize = $pofile->putContents( (string) $data );
$mosize = $mofile->putContents( $data->msgfmt() );
// set debug response data
$this->set( 'debug', array (
'poname' => $pofile->basename(),
'posize' => $posize,
'mosize' => $mosize,
'source' => $potfile ? $potfile->basename() : '',
) );
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// front end will redirect to the editor
$type = strtolower( $this->get('type') );
$this->set( 'redirect', Loco_mvc_AdminRouter::generate( sprintf('%s-file-edit',$type), array (
'path' => $pofile->getRelativePath($base),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
) ) );
return parent::render();
}
} ajax/DownloadConfController.php 0000666 00000001661 15214173534 0012636 0 ustar 00 validate();
$bundle = $this->getBundle();
$file = new Loco_fs_File( $this->get('path') );
// TODO should we download axtual loco.xml file if bundle is configured from it?
//$file->normalize( $bundle->getDirectoryPath() );
//if( $file->exists() ){}
$writer = new Loco_config_BundleWriter($bundle);
switch( $file->extension() ){
case 'xml':
return $writer->toXml();
case 'json':
return json_encode( $writer->jsonSerialize() );
}
// @codeCoverageIgnoreStart
throw new Loco_error_Exception('Specify either XML or JSON file path');
}
} ajax/SyncController.php 0000666 00000003245 15214173534 0011175 0 ustar 00 validate();
$bundle = Loco_package_Bundle::fromId( $post->bundle );
$project = $bundle->getProjectById( $post->domain );
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize( $base );
// POT file always synced with source code (even if a PO being used as POT)
if( 'pot' === $post->type ){
$potfile = null;
}
// allow post data to force a template file path
else if( $path = $post->sync ){
$potfile = new Loco_fs_File($path);
$potfile->normalize( $base );
}
// else use project-configured template if one is defined
else {
$potfile = $project->getPot();
}
// sync with POT if it exists
if( $potfile && $potfile->exists() ){
$this->set('pot', $potfile->basename() );
$data = Loco_gettext_Data::load($potfile);
}
// else sync with source code
else {
$this->set('pot', '' );
$domain = (string) $project->getDomain();
$extr = new Loco_gettext_Extraction($bundle);
$data = $extr->addProject($project)->includeMeta()->getTemplate($domain);
}
$this->set( 'po', $data->jsonSerialize() );
return parent::render();
}
} ajax/SaveController.php 0000666 00000010005 15214173534 0011147 0 ustar 00 validate();
// path parameter must not be empty
$path = $post->path;
if( ! $path ){
throw new InvalidArgumentException('Path parameter required');
}
// locale must be posted to indicate whether PO or POT
$locale = $post->locale;
if( is_null($locale) ){
throw new InvalidArgumentException('Locale parameter required');
}
$pofile = new Loco_fs_LocaleFile( $path );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
// ensure we only deal with PO/POT source files.
// posting of MO file paths is permitted when PO is missing, but we're about to fix that
$ext = $pofile->extension();
if( 'mo' === $ext ){
$pofile = $pofile->cloneExtension('po');
}
else if( 'pot' === $ext ){
$locale = '';
}
else if( 'po' !== $ext ){
throw new Loco_error_Exception('Invalid file path');
}
// force the use of remote file system when configured from front end
if( $post->has('connection_type') ){
$api = new Loco_api_WordPressFileSystem;
$api->authorizeConnect( $pofile );
}
// data posted must be valid
$data = Loco_gettext_Data::fromSource( $post->data );
// backup existing file BEFORE overwriting
// file system write context will carry through when revisions clone pofile
if( $num_backups = Loco_data_Settings::get()->num_backups ){
try {
$backups = new Loco_fs_Revisions( $pofile );
$backups->create();
$backups->prune($num_backups);
}
catch( Exception $e ){
$message = __('Failed to create backup file in "%s". Check file permissions or disable backups','loco');
Loco_error_AdminNotices::info( sprintf( $message, $pofile->getParent()->basename() ) );
}
}
// commit file directly to disk
$bytes = $pofile->putContents( $data );
$mtime = $pofile->modified();
// push recent items on file creation
try {
$bundle = $this->getBundle();
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// TODO push project and locale file
}
catch( Exception $e ){
// editor permitted to save files not in a bundle, so catching failures
}
// start success data with bytes written and timestamp
$this->set('locale', $locale );
$this->set('pobytes', $bytes );
$this->set('poname', $pofile->basename() );
$this->set('modified', $mtime);
$this->set('datetime', Loco_mvc_ViewParams::date_i18n($mtime) );
// Intial message refers to PO/POT save success
$success = $locale ? __('PO file saved','loco') : __('POT file saved','loco');
// Compile MO file unless saving template
if( $locale ){
try {
$data = $data->msgfmt();
$mofile = $pofile->cloneExtension('mo');
$bytes = $mofile->putContents( $data );
$this->set( 'mobytes', $bytes );
Loco_error_AdminNotices::success( __('PO file saved and MO file compiled','loco') );
}
catch( Exception $e ){
Loco_error_AdminNotices::add( $e );
Loco_error_AdminNotices::info( __('PO file saved, but MO file compilation failed','loco') );
$this->set( 'mobytes', 0 );
}
}
else {
Loco_error_AdminNotices::success( __('POT file saved','loco') );
}
return parent::render();
}
} ajax/FsConnectController.php 0000666 00000003011 15214173534 0012132 0 ustar 00 validate();
$api = new Loco_api_WordPressFileSystem;
$func = 'authorize'.ucfirst($post->auth);
$auth = array( $api, $func );
if( ! is_callable($auth) ){
throw new Loco_error_Exception('Unexpected file operation');
}
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize($base);
try {
$file->getWriteContext()->authorize();
//
if( call_user_func( $auth, $file ) ){
$this->set( 'authed', true );
$this->set( 'valid', $api->getOutputCredentials() );
$this->set( 'creds', $api->getInputCredentials() );
$this->set( 'method', $api->getFileSystem()->method );
$this->set( 'success', __('Connected to remote file system','loco') );
}
else {
$this->set( 'authed', false );
$this->set( 'prompt', $api->getForm() );
}
}
catch( Loco_error_WriteException $e ){
$this->set('authed', false );
$this->set('reason', $e->getMessage() );
}
return parent::render();
}
} ajax/PingController.php 0000666 00000000462 15214173534 0011154 0 ustar 00 set( 'ping', 'pong' );
return parent::render();
}
} ajax/DownloadController.php 0000666 00000002422 15214173534 0012024 0 ustar 00 validate();
// we need a path, but it may not need to exist
$file = new Loco_fs_File( $this->get('path') );
$file->normalize( loco_constant( 'WP_CONTENT_DIR') );
$is_binary = 'mo' === strtolower( $file->extension() );
// posted source must be clean and must parse as whatever the file extension claims to be
if( $raw = $post->source ){
// compile source if imaginary target is MO
if( $is_binary ) {
$po = new Loco_gettext_Data( loco_parse_po($raw) );
$raw = $po->msgfmt();
}
}
// else file can be output directly if it exists
else if( $file->exists() ){
$raw = $file->getContents();
$do_compile = false;
}
/*/ else if PO exists but MO doesn't, we can compile it on the fly
else if( ! $is_binary ){
}*/
else {
throw new Loco_error_Exception('File not found and no source posted');
}
return $raw;
}
} ajax/XgettextController.php 0000666 00000005433 15214173534 0012076 0 ustar 00 validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
// target location may not be next to POT file at all
$base = loco_constant('WP_CONTENT_DIR');
$target = new Loco_fs_Directory( $this->get('path') );
$target->normalize( $base );
if( $target->exists() && ! $target->isDirectory() ){
throw new Loco_error_Exception('Target is not a directory');
}
// basename should be posted from front end
$name = $this->get('name');
if( ! $name ){
throw new Loco_error_Exception('Front end did not post $name');
}
// POT file shouldn't exist currently
$potfile = new Loco_fs_File( $target.'/'.$name );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate($potfile);
// Do extraction and grab only given domain's strings
$ext = new Loco_gettext_Extraction( $bundle );
$domain = $project->getDomain()->getName();
$data = $ext->addProject($project)->includeMeta()->getTemplate( $domain );
// additional headers to set in new POT file
$headers = array (
'Project-Id-Version' => $project->getName(),
);
$potsize = $potfile->putContents( (string) $data );
// set response data for debugging
if( loco_debugging() ){
$this->set( 'debug', array (
'potname' => $potfile->basename(),
'potsize' => $potsize,
'total' => $ext->getTotal(),
) );
}
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// put flash message into session to be displayed on redirected page
try {
Loco_data_Session::get()->flash('success', __('Template file created','loco') );
Loco_data_Session::close();
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// redirect front end to bundle view. Discourages manual editing of template
$type = strtolower( $bundle->getType() );
$href = Loco_mvc_AdminRouter::generate( sprintf('%s-view',$type), array(
'bundle' => $bundle->getHandle(),
) );
$hash = '#loco-'.$project->getId();
$this->set( 'redirect', $href.$hash );
return parent::render();
}
} ajax/FsReferenceController.php 0000666 00000013522 15214173534 0012447 0 ustar 00 exists() ){
return $srcfile;
}
}*/
// reference may be resolvable via referencing PO file's location
$pofile = new Loco_fs_File( $this->get('path') );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
if( ! $pofile->exists() ){
throw new InvalidArgumentException('PO/POT file required to resolve reference');
}
$search = new Loco_gettext_SearchPaths;
$search->init($pofile);
if( $srcfile = $search->match($refpath) ){
return $srcfile;
}
// reference may be resolvable via known project roots
try {
$bundle = $this->getBundle();
// Loco extractions will always be relative to bundle root
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $bundle->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
// check relative to parent theme root
if( $bundle->isTheme() && ( $parent = $bundle->getParentTheme() ) ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $parent->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
// final attempt - search all project source roots
// TODO is there too large a risk of false positives? especially with files like index.php
/* @var $root Loco_fs_Directory */
/*foreach( $this->getProject($bundle)->getConfiguredSources() as $root ){
if( $root->isDirectory() ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $root->getPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
}*/
}
catch( Loco_error_Exception $e ){
// permitted for there to be no bundle or project when viewing orphaned file
}
throw new Loco_error_Exception( sprintf('Failed to find source file matching "%s"',$refpath) );
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// at the very least we need a reference to examine
if( ! $post->has('ref') ){
throw new InvalidArgumentException('ref parameter required');
}
// reference must parse as :
$ref = $post->ref;
if( ! preg_match('/^(.+):(\d+)$/', $ref, $r ) ){
throw new InvalidArgumentException('Invalid file reference, '.$ref );
}
// find file or fail
list( , $refpath, $refline ) = $r;
$srcfile = $this->findSourceFile($refpath);
$type = strtolower( $srcfile->extension() );
$this->set('type', $type );
$this->set('line', (int) $refline );
$this->set('path', $srcfile->getRelativePath( loco_constant('WP_CONTENT_DIR') ) );
// source code will be HTML-tokenized into multiple lines
$code = array();
// PHP is the most likely format. highlighting on back end because tokenizer provides more control than highlight.js
if( 'php' === $type ) {
$thisline = 1;
foreach( token_get_all( $srcfile->getContents() ) as $tok ){
if( is_array($tok) ){
// line numbers added in PHP 5.2.2 - WordPress minimum is 5.2.4
list( $t, $str, $startline ) = $tok;
$clss = token_name($t);
// tokens can span multiple lines (whitespace/html/comments)
$lines = preg_split('/\\R/', $str );
}
else {
// scalar symbol will always start on the line that the previous token ended on
$clss = 'T_NONE';
$lines = array( $tok );
$startline = $thisline;
}
// token can span multiple lines, so include only bytes on required line[s]
foreach( $lines as $i => $line ){
$thisline = $startline + $i;
$html = ''.htmlentities($line,ENT_COMPAT,'UTF-8').'';
// append highlighted token to current line
$j = $thisline - 1;
if( isset($code[$j]) ){
$code[$j] .= $html;
}
else {
$code[$j] = $html;
}
}
}
}
/*/ TODO permit limited other file types, but without back end highlighting
else if( ){
foreach( preg_split( '/\\R/u', $srcfile->getContents() ) as $line ){
$code[] = ''.htmlentities($line,ENT_COMPAT,'UTF-8').'';
}
}*/
else {
throw new Loco_error_Exception( sprintf('%s source view not supported', $type) );
}
if( ! isset($code[$refline-1]) ){
throw new Loco_error_Exception( sprintf('Line %u not in source file', $refline) );
}
$this->set( 'code', $code );
return parent::render();
}
}
admin/bundle/SetupController.php 0000666 00000015120 15214173534 0012772 0 ustar 00 getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Set up %s','loco'),$bundle->getName() ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Setup tab','loco') => $this->view('tab-bundle-setup'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle setup','loco') );
$bundle = $this->getBundle();
$action = 'setup:'.$bundle->getId();
// execute auto-configure if posted
$post = Loco_mvc_PostParams::get();
if( $post->has('auto-setup') && $this->checkNonce( 'auto-'.$action) ){
if( 0 === count($bundle) ){
$bundle->createDefault();
}
foreach( $bundle as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
}
// forcefully add every additional project into bundle
foreach( $bundle->invert() as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
$bundle[] = $project;
}
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('auto', null );
}
// execute XML-based config if posted
else if( $post->has('xml-setup') && $this->checkNonce( 'xml-'.$action) ){
$bundle->clear();
$model = new Loco_config_XMLModel;
$model->loadXml( trim( $post['xml-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('xml', null );
}
// execute JSON-based config if posted
else if( $post->has('json-setup') && $this->checkNonce( 'json-'.$action) ){
$bundle->clear();
$model = new Loco_config_ArrayModel;
$model->loadJson( trim( $post['json-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('json', null );
}
// execute reset if posted
else if( $post->has('reset-setup') && $this->checkNonce( 'reset-'.$action) ){
$this->resetBundle();
$bundle = $this->getBundle();
}
// bundle author links
$info = $bundle->getHeaderInfo();
$this->set( 'credit', $info->getAuthorCredit() );
// render according to current configuration method (save type)
$configured = $this->get('force') or $configured = $bundle->isConfigured();
$notices = new ArrayIterator;
$this->set('notices', $notices );
// collect configuration warnings
foreach( $bundle as $project ){
$potfile = $project->getPot();
if( ! $potfile ){
$notices[] = sprintf('No translation template for the "%s" text domain', $project->getSlug() );
}
}
// if extra files found consider incomplete
if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
$unknown = Loco_package_Inverter::export($bundle);
$n = 0;
foreach( $unknown as $ext => $files ){
$n += count($files);
}
if( $n ){
$notices[] = sprintf( _n("One file can't be matched to a known set of strings","%s files can't be matched to a known set of strings",$n,'loco'), number_format($n) );
}
}
// display setup options if at least one option specified
$doconf = false;
// enable form to invoke auto-configuration
if( $this->get('auto') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'auto-'.$action );
$this->set('autoFields', $fields );
$doconf = true;
}
// enable form to paste XML config
if( $this->get('xml') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'xml-'.$action );
$this->set('xmlFields', $fields );
$doconf = true;
}
// enable form to paste JSON config (via remote lookup)
if( $this->get('json') ){
$fields = new Loco_mvc_HiddenFields( array(
'json-content' => '',
'version' => $info->Version,
) );
$fields->setNonce( 'json-'.$action );
$this->set('jsonFields', $fields );
// other information for looking up bundle via api
$this->set('vendorSlug', $bundle->getSlug() );
// remote config is done via JavaScript
$this->enqueueScript('setup');
$apiBase = apply_filters( 'loco_api_url', 'https://localise.biz/api' );
$this->set('js', new Loco_mvc_ViewParams( array(
'apiUrl' => $apiBase.'/wp/'.strtolower( $bundle->getType() ),
) ) );
$doconf = true;
}
// display configurator if configurating
if( $doconf ){
return $this->view( 'admin/bundle/setup/conf' );
}
// else set configurator links back to self with required option
// ...
if( ! $configured || ! count($bundle) ){
return $this->view( 'admin/bundle/setup/none' );
}
if( 'db' === $configured ){
// form for resetting config
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'reset-'.$action );
$this->set( 'reset', $fields );
return $this->view('admin/bundle/setup/saved');
}
if( 'internal' === $configured ){
return $this->view('admin/bundle/setup/core');
}
if( 'file' === $configured ){
return $this->view('admin/bundle/setup/author');
}
if( count($notices) ){
return $this->view('admin/bundle/setup/partial');
}
return $this->view('admin/bundle/setup/meta');
}
} admin/bundle/DebugController.php 0000666 00000002664 15214173534 0012731 0 ustar 00 getBundle();
$this->set('title', 'Debug: '.$bundle );
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle diagnostics','loco') );
$bundle = $this->getBundle();
$debugger = new Loco_package_Debugger($bundle);
$this->set('notices', $notices = new Loco_mvc_ViewParams );
/* @var $notice Loco_error_Exception */
foreach( $debugger as $notice ){
$notices[] = new Loco_mvc_ViewParams( array(
'style' => 'notice inline notice-'.$notice->getType(),
'title' => $notice->getTitle(),
'body' => $notice->getMessage(),
) );
}
$meta = $bundle->getHeaderInfo();
$this->set('meta', new Loco_mvc_ViewParams( array(
'vendor' => $meta->getVendorHost(),
'author' => $meta->getAuthorCredit(),
) ) );
if( count($bundle) ){
$writer = new Loco_config_BundleWriter( $bundle );
$this->set( 'xml', $writer->toXml() );
}
return $this->view('admin/bundle/debug');
}
} admin/bundle/ViewController.php 0000666 00000027032 15214173534 0012611 0 ustar 00 getBundle();
$this->set('title', $bundle->getName() );
$this->enqueueStyle('bundle');
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-bundle-view'),
);
}
/**
* Generate a link for a specific file resource within a project
* @return string
*/
private function getResourceLink( $page, Loco_package_Project $project, Loco_gettext_Metadata $meta, array $args = array() ){
$args['path'] = $meta->getPath(false);
return $this->getProjectLink( $page, $project, $args );
}
/**
* Generate a link for a project, but without being for a specific file
* @return string
*/
private function getProjectLink( $page, Loco_package_Project $project, array $args = array() ){
$args['bundle'] = $this->get('bundle');
$args['domain'] = $project->getId();
return $this->getLink( $page, $args );
}
/**
* Generate a link for the same type of bundle as this one
* @return string
*/
private function getLink( $page, array $args ){
$route = strtolower( $this->get('type') ).'-'.$page;
return Loco_mvc_AdminRouter::generate( $route, $args );
}
/**
* Initialize view parameters for a project
* @return Loco_mvc_ViewParams
*/
private function createProjectParams( Loco_package_Project $project ){
$name = $project->getName();
$domain = $project->getDomain()->getName();
$slug = $project->getSlug();
$p = new Loco_mvc_ViewParams( array (
'id' => $project->getId(),
'name' => $name,
'slug' => $slug,
'domain' => $domain,
'short' => ! $slug || $project->isDomainDefault() ? $domain : $domain.'→'.$slug,
) );
// POT template file
$file = $project->getPot();
if( $file && $file->exists() ){
$meta = Loco_gettext_Metadata::load($file)->persistIfDirty( 0, true );
$p['pot'] = new Loco_mvc_ViewParams( array(
// POT info
'name' => $file->basename(),
'time' => $file->modified(),
// POT links
'info' => $this->getResourceLink('file-info', $project, $meta ),
'edit' => $this->getResourceLink('file-edit', $project, $meta ),
) );
}
// PO/MO files
$po = $project->findLocaleFiles('po');
$mo = $project->findLocaleFiles('mo');
$p['po'] = $this->createProjectPairs( $project, $po, $mo );
// also pull invalid files so everything is available to the UI
$mo = $project->findNotLocaleFiles('mo');
$po = $project->findNotLocaleFiles('po')->augment( $project->findNotLocaleFiles('pot') );
$p['_po'] = $this->createProjectPairs( $project, $po, $mo );
// always offer msginit even if we find out later we can't extract any strings
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('msginit', $project ),
'name' => __('New language','loco'),
'icon' => 'add',
) );
// offer template editing if permitted
if( ! $project->isPotLocked() ){
$pot = $project->getPot();
if( $pot && $pot->exists() ){
$p['pot'] = $pot;
$meta = Loco_gettext_Metadata::load($pot)->persistIfDirty( 0, true );
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getResourceLink('file-edit', $project, $meta ),
'name' => __('Edit template','loco'),
'icon' => 'pencil',
) );
}
// else offer creation of new Template
else {
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('xgettext', $project ),
'name' => __('Create template','loco'),
'icon' => 'add',
) );
}
}
return $p;
}
/**
* Collect PO/MO pairings, ignoring any PO that is in use as a template
*/
private function createPairs( Loco_fs_FileList $po, Loco_fs_FileList $mo, Loco_fs_File $pot = null ){
$pairs = array();
/* @var $pofile Loco_fs_LocaleFile */
foreach( $po as $pofile ){
if( $pot && $pofile->equal($pot) ){
continue;
}
$pair = array( $pofile, null );
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
$pair[1] = $mofile;
}
$pairs[] = $pair;
}
/* @var $mofile Loco_fs_LocaleFile */
foreach( $mo as $mofile ){
$pofile = $mofile->cloneExtension('po');
if( $pot && $pofile->equal($pot) ){
continue;
}
if( ! $pofile->exists() ){
$pairs[] = array( null, $mofile );
}
}
return $pairs;
}
/**
* Initialize view parameters for each row representing a localized resource pair
* @return array collection of entries corresponding to available PO/MO pair.
*/
private function createProjectPairs( Loco_package_Project $project, Loco_fs_LocaleFileList $po, Loco_fs_LocaleFileList $mo ){
// populate official locale names for all found, or default to our own
if( $locales = $po->getLocales() + $mo->getLocales() ){
$api = new Loco_api_WordPressTranslations;
/* @var $locale Loco_Locale */
foreach( $locales as $tag => $locale ){
$locale->fetchName($api) or $locale->buildName() or $locale->setName($tag);
}
}
// collate as unique [PO,MO] pairs ensuring canonical template excluded
$pairs = $this->createPairs( $po, $mo, $project->getPot() );
$rows = array();
foreach( $pairs as $pair ){
// favour PO file if it exists
list( $pofile, $mofile ) = $pair;
$file = $pofile or $file = $mofile;
// establish locale, or assume invalid
$locale = null;
if( 'pot' !== $file->extension() ){
$tag = $file->getSuffix();
if( isset($locales[$tag]) ){
$locale = $locales[$tag];
}
}
$rows[] = $this->createFileParams( $project, $file, $locale );
}
return $rows;
}
/**
*
*/
private function createFileParams( Loco_package_Project $project, Loco_fs_File $file, Loco_Locale $locale = null ){
// Pull Gettext meta data from cache if possible
// TODO save write when cached version was used
$meta = Loco_gettext_Metadata::load($file)->persistIfDirty( 0, true );
// Establish whether translations are official or otherwise
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
// Retuen data required for PO table row
return new Loco_mvc_ViewParams( array (
// locale info
'lcode' => $locale ? (string) $locale : '',
'lname' => $locale ? $locale->getName() : '',
'lattr' => $locale ? 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"' : '',
// file info
'meta' => $meta,
'name' => $file->basename(),
'time' => $file->modified(),
'todo' => $meta->countIncomplete(),
'total' => $meta->getTotal(),
// author / contrib
'store' => $dir->getTypeLabel( $dir->getTypeId() ),
// links
'info' => $this->getResourceLink('file-info', $project, $meta ),
'edit' => $this->getResourceLink('file-edit', $project, $meta ),
'delete' => $this->getResourceLink('file-delete', $project, $meta ),
'copy' => $this->getProjectLink('msginit', $project, array( 'source' => $meta->getPath(false) ) ),
) );
}
/**
* Prepare view parameters for all projects in a bundle
* @return array
*/
private function createBundleListing( Loco_package_Bundle $bundle ){
$projects = array();
/* @var $project Loco_package_Project */
foreach( $bundle as $project ){
$projects[] = $this->createProjectParams($project);
}
return $projects;
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation();
$bundle = $this->getBundle();
$this->set('name', $bundle->getName() );
// bundle may not be fully configured
$configured = $bundle->isConfigured();
// Hello Dolly is an exception. don't show unless configured deliberately
if( ! $configured && 'hello.php' === $bundle->getHandle() && 'Hello Dolly' === $bundle->getName() ){
$this->set( 'redirect', Loco_mvc_AdminRouter::generate('core-view') );
return $this->view('admin/bundle/alias');
}
// Collect all configured projects
$projects = $this->createBundleListing( $bundle );
$unknown = array();
// sniff additional unknown files if bundle is a theme or directory-based plugin that's been auto-detected
if( 'file' === $configured || 'internal' === $configured ){
// presumed complete
}
else if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
// TODO This needs absracting into the Loco_package_Inverter class
$prefixes = array();
$po = new Loco_fs_LocaleFileList;
$mo = new Loco_fs_LocaleFileList;
foreach( Loco_package_Inverter::export($bundle) as $ext => $files ){
$list = 'mo' === $ext ? $mo : $po;
foreach( $files as $file ){
$file = new Loco_fs_LocaleFile($file);
$list->addLocalized( $file );
// Only look in system locations if locale is valid and domain/prefix available
$locale = $file->getLocale();
if( $locale->isValid() && ( $domain = $file->getPrefix() ) ){
$prefixes[$domain] = true;
}
}
}
// pick up given files in system locations only
foreach( $prefixes as $domain => $_bool ){
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain($domain), '' );
$bundle->addProject( $dummy ); // <- required to configure locations
$dummy->excludeTargetPath( $bundle->getDirectoryPath() );
$po->augment( $dummy->findLocaleFiles('po') );
$mo->augment( $dummy->findLocaleFiles('mo') );
}
// a fake project is required to disable functions that require a configured project
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain(''), '' );
$unknown = $this->createProjectPairs( $dummy, $po, $mo );
}
$this->set('projects', $projects );
$this->set('unknown', $unknown );
return $this->view( 'admin/bundle/view' );
}
} admin/bundle/ConfController.php 0000666 00000010737 15214173534 0012570 0 ustar 00 enqueueStyle('config');
$this->enqueueScript('config');
$bundle = $this->getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Configure %s','loco'),$bundle->getName() ) );
$post = Loco_mvc_PostParams::get();
// always set a nonce for current bundle
$nonce = $this->setNonce( $this->get('_route').'-'.$this->get('bundle') );
$this->set('nonce', $nonce );
try {
// Save configuration if posted
if( $post->has('conf') ){
if( ! $post->name ){
$post->name = $bundle->getName();
}
$this->checkNonce( $nonce->action );
$model = new Loco_config_FormModel;
$model->loadForm( $post );
// configure bundle from model in full
$bundle->clear();
$reader = new Loco_config_BundleReader( $bundle );
$reader->loadModel( $model );
$this->saveBundle();
}
// Delete configuration if posted
else if( $post->has('unconf') ){
$this->resetBundle();
}
}
catch( Exception $e ){
Loco_error_AdminNotices::warn( $e->getMessage() );
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Advanced tab','loco') => $this->view('tab-bundle-conf'),
);
}
/**
* {@inheritdoc}
*/
public function render() {
$bundle = $this->getBundle();
$base = $bundle->getDirectoryPath();
// parent themes are inherited into bundle, we don't want them in the child theme config
if( $bundle->isTheme() && ( $parent = $bundle->getParentTheme() ) ){
$this->set( 'parent', new Loco_mvc_ViewParams( array(
'name' => $parent->getName(),
'href' => Loco_mvc_AdminRouter::generate('theme-conf', array( 'bundle' => $parent->getSlug() ) + $_GET ),
) ) );
}
// render postdata straight back to form if sent
$data = Loco_mvc_PostParams::get();
// else build initial data from current bundle state
if( ! $data->has('conf') ){
if( 0 === count($bundle) ){
$bundle->createDefault('');
}
$writer = new Loco_config_BundleWriter($bundle);
$data = $writer->toForm();
// removed parent bundle configs, as they are inherited
/* @var Loco_package_Project $project */
foreach( $bundle as $i => $project ){
if( isset($parent) && $parent->hasProject($project) ){
$data['conf'][$i]['removed'] = true;
}
}
}
// build config blocks for form
$i = 0;
$conf = array();
foreach( $data['conf'] as $raw ){
if( empty($raw['removed']) ){
$slug = $raw['slug'];
$domain = $raw['domain'] or $domain = 'untitled';
$raw['prefix'] = sprintf('conf[%u]', $i++ );
$raw['short'] = ! $slug || ( $slug === $domain ) ? $domain : $domain.'→'.$slug;
$conf[] = new Loco_mvc_ViewParams( $raw );
}
}
// bundle level configs
$name = $bundle->getName();
$excl = $data['exclude'];
// access to type of configuration that's currently saved
$this->set('saved', $bundle->isConfigured() );
// link to author if there are config problems
$info = $bundle->getHeaderInfo();
$this->set('author', $info->getAuthorLink() );
// link for downloading current configuration XML file
$args = array (
'path' => 'loco.xml',
'action' => 'loco_download',
'bundle' => $bundle->getHandle(),
'type' => $bundle->getType()
);
$this->set( 'xmlUrl', Loco_mvc_AjaxRouter::generate( 'DownloadConf', $args ) );
$this->prepareNavigation()->add( __('Advanced configuration','loco') );
return $this->view('admin/bundle/conf', compact('conf','base','name','excl') );
}
} admin/bundle/BaseController.php 0000666 00000010054 15214173534 0012545 0 ustar 00 bundle ){
$type = $this->get('type');
$handle = $this->get('bundle');
$this->bundle = Loco_package_Bundle::createType( $type, $handle );
}
return $this->bundle;
}
/**
* Commit bundle config to database
* @return Loco_admin_bundle_BaseController
*/
protected function saveBundle(){
$custom = new Loco_config_CustomSaved;
if( $custom->setBundle($this->bundle)->persist() ){
Loco_error_AdminNotices::success( __('Configuration saved','loco') );
}
// invalidate bundle in memory so next fetch is re-configured from DB
$this->bundle = null;
return $this;
}
/**
* Remove bundle config from database
* @return Loco_admin_bundle_BaseController
*/
protected function resetBundle(){
$option = $this->bundle->getCustomConfig();
if( $option && $option->remove() ){
Loco_error_AdminNotices::success( __('Configuration reset','loco') );
// invalidate bundle in memory so next fetch falls back to auto-config
$this->bundle = null;
}
return $this;
}
/**
* @return Loco_package_Project
*/
public function getProject(){
if( ! $this->project ){
$bundle = $this->getBundle();
$domain = $this->get('domain');
if( ! $domain ){
throw new Loco_error_Exception( sprintf('Translation set not known in %s', $bundle ) );
}
$this->project = $bundle->getProjectById($domain);
if( ! $this->project ){
throw new Loco_error_Exception( sprintf('Unknown translation set: %s not in %s', json_encode($domain), $bundle ) );
}
}
return $this->project;
}
/**
* @return Loco_admin_Navigation
*/
protected function prepareNavigation(){
$bundle = $this->getBundle();
// navigate up to bundle listing page
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between bundle view siblings
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'view' => __('Overview','loco'),
'setup' => __('Setup','loco'),
'conf' => __('Advanced','loco'),
);
if( loco_debugging() ){
$actions['debug'] = __('Debug','loco');
}
$suffix = $this->get('action');
$prefix = strtolower( $this->get('type') );
$getarg = array_intersect_key( $_GET, array('bundle'=>'') );
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $getarg );
$tabs->add( $name, $href, $action === $suffix );
}
return $breadcrumb;
}
/**
* Prepare file system connect
* @return Loco_mvc_HiddenFields
*/
protected function prepareFsConnect( $type, $path ){
$fields = new Loco_mvc_HiddenFields( array(
'auth' => $type,
'path' => $path,
'loco-nonce' => wp_create_nonce('fsConnect'),
) );
$this->set('fsFields', $fields );
// may have fs credentials saved in session
try {
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
$fields['connection_type'] = $session['loco-fs']['connection_type'];
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
return $fields;
}
} admin/RedirectController.php 0000666 00000001101 15214173534 0012154 0 ustar 00 getLocation();
if( $location && wp_redirect($location) ){
// @codeCoverageIgnoreStart
exit;
}
}
/**
* @internal
*/
public function render(){
return 'Failed to redirect';
}
} admin/config/VersionController.php 0000666 00000004346 15214173534 0013323 0 ustar 00 set( 'title', __('Version','loco') );
// handle legacy degradation
$nonce = $this->setNonce('downgrade');
try {
if( $this->checkNonce($nonce->action) ){
update_option('loco-branch', '1', true );
$legacy = add_query_arg( array('page'=>'loco-translate'), admin_url('admin.php') );
wp_redirect( $legacy );
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
// current plugin version
$version = loco_plugin_version();
// check for auto-update availabilty
if( $updates = get_site_transient('update_plugins') ){
$key = loco_plugin_self();
if( isset($updates->checked[$key]) && isset($updates->response[$key]) ){
$old = $updates->checked[$key];
$new = $updates->response[$key]->new_version;
$diff = version_compare( $new, $old );
if( 1 === $diff ){
// current version is lower than latest
$this->setUpdate( $new );
}
/*else {
// current version is a future release (dev branch probably)
}*/
}
}
// $this->setUpdate('2.0.1-debug');
return $this->view('admin/config/version', compact('breadcrumb','version') );
}
/**
* @internal
*/
private function setUpdate( $version ){
$action = 'upgrade-plugin_'.loco_plugin_self();
$link = admin_url( 'update.php?action=upgrade-plugin&plugin='.rawurlencode(loco_plugin_self()) );
$this->set('update', $version );
$this->set('update_href', wp_nonce_url( $link, $action ) );
}
} admin/config/PrefsController.php 0000666 00000003027 15214173534 0012750 0 ustar 00 set( 'title', __('User options','loco') );
// user preference options
$opts = Loco_data_Preferences::get();
$this->set( 'opts', $opts );
// default value for Last-Translator credit
$user = wp_get_current_user();
$name = $user->get('display_name') or $name = 'nobody';
$email = $user->get('user_email') or $email = 'nobody@localhost';
$this->set('credit', sprintf('%s <%s>', $name, $email ) );
// handle save action
$nonce = $this->setNonce('save-prefs');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
Loco_error_AdminNotices::success( __('Settings saved','loco') );
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/prefs', compact('breadcrumb') );
}
}
admin/config/BaseController.php 0000666 00000002176 15214173534 0012547 0 ustar 00 set( 'tabs', $tabs );
$actions = array (
'' => __('Site options','loco'),
'user' => __('User options','loco'),
'version' => __('Version','loco'),
);
$suffix = (string) $this->get('action');
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( 'config-'.$action, $_GET );
$tabs->add( $name, $href, $action === $suffix );
}
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-settings'),
);
}
} admin/config/SettingsController.php 0000666 00000005330 15214173534 0013470 0 ustar 00 set( 'opts', $opts );
$this->set( 'dflt', Loco_data_Settings::create() );
// roles and capabilities
$perms = new Loco_data_Permissions;
// handle save action
$nonce = $this->setNonce('save-config');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
$perms->populate( $post->has('caps') ? $post->caps : array() );
// done update
Loco_error_AdminNotices::success( __('Settings saved','loco') );
// remove saved params if persistant options unset
if( ! $opts['fs_persist'] ){
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
unset( $session['loco-fs'] );
$session->persist();
}
}
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
$this->set('caps', $caps = new Loco_mvc_ViewParams );
// there is no distinct role for network admin, so we'll fake it for UI
if( is_multisite() ){
$caps[''] = new Loco_mvc_ViewParams( array(
'label' => __('Super Admin','default'),
'name' => 'dummy-admin-cap',
'attrs' => 'checked disabled'
) );
}
/* @var $role WP_Role */
foreach( $perms->getRoles() as $id => $role ){
$caps[$id] = new Loco_mvc_ViewParams( array(
'value' => '1',
'label' => $perms->getRoleName($id),
'name' => 'caps['.$id.'][loco_admin]',
'attrs' => $perms->isProtectedRole($role) ? 'checked disabled ' : ( $role->has_cap('loco_admin') ? 'checked ' : '' ),
) );
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/settings', compact('breadcrumb') );
}
}
admin/ErrorController.php 0000666 00000000653 15214173534 0011517 0 ustar 00 set('error', Loco_error_Exception::convert($e) );
return $this->render();
}
public function render(){
$e = $this->get('error') or $e = new Loco_error_Exception('No error set');
return $this->view( $e->getTemplate() );
}
}
admin/Navigation.php 0000666 00000003446 15214173534 0010464 0 ustar 00 add( $name, $href );
}*/
/**
* Create a breadcrumb trail for a given view below a bundle
* @return Loco_admin_Navigation
*/
public static function createBreadcrumb( Loco_package_Bundle $bundle ){
$nav = new Loco_admin_Navigation;
// root link depends on bundle type
$type = strtolower( $bundle->getType() );
if( 'core' !== $type ){
$link = new Loco_mvc_ViewParams( array(
'href' => Loco_mvc_AdminRouter::generate($type),
) );
if( 'theme' === $type ){
$link['name'] = __('Themes','loco');
}
else {
$link['name'] = __('Plugins','loco');
}
$nav[] = $link;
}
// Add actual bundle page, href may be unset to show as current page if needed
$nav->add (
$bundle->getName(),
Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $bundle->getHandle() ) )
);
// client code will add current page
return $nav;
}
/**
* @return Loco_mvc_ViewParams
*
public function getSecondLast(){
$i = count($this);
if( $i > 1 ){
return $this[ $i-2 ];
}
}*/
} admin/init/InitPotController.php 0000666 00000012163 15214173534 0012756 0 ustar 00 enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New template','loco').' ‹ '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-init-pot'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confising when no project-scope navigation
// $this->get('tabs')->add( __('New POT','loco'), '', true );
$bundle = $this->getBundle();
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$this->set('domain', $domain );
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
// Establish default POT path whether it exists or not
$pot = $project->getPot();
while( ! $pot ){
$name = ( $slug ? $slug : $domain ).'.pot';
/* @var $dir Loco_fs_Directory */
foreach( $project->getConfiguredTargets() as $dir ){
$pot = new Loco_fs_File( $dir->getPath().'/'.$name );
break 2;
}
// unlikely to have no configured targets, but possible ... so default to standard
$pot = new Loco_fs_File( $bundle->getDirectoryPath().'/languages/'.$name );
break;
}
// POT should actually not exist at this stage. It should be edited instead.
if( $pot->exists() ){
throw new Loco_error_Exception( __('Template file already exists','loco') );
}
// Bundle may deliberately lock template to avoid end-user tampering
// it makes little sense to do so when template doesn't exist, but we will honour the setting anyway.
if( $project->isPotLocked() ){
throw new Loco_error_Exception('Template is protected from updates by the bundle configuration');
}
// Just warn if POT writing will fail when saved, but still show screen
$dir = $pot->getParent();
// Avoiding full source scan until actioned, but calculate size to manage expectations
$bytes = 0;
$nfiles = 0;
$nskip = 0;
$largest = 0;
$sources = $project->findSourceFiles();
// skip files larger than configured maximum
$opts = Loco_data_Settings::get();
$max = wp_convert_hr_to_bytes( $opts->max_php_size );
/* @var $sourceFile Loco_fs_File */
foreach( $sources as $sourceFile ){
$nfiles++;
$fsize = $sourceFile->size();
$largest = max( $largest, $fsize );
if( $fsize > $max ){
$nskip += 1;
}
else {
$bytes += $fsize;
}
}
$this->set( 'scan', new Loco_mvc_ViewParams( array (
'bytes' => $bytes,
'count' => $nfiles,
'skip' => $nskip,
'size' => Loco_mvc_FileParams::renderBytes($bytes),
'large' => Loco_mvc_FileParams::renderBytes($max),
'largest' => Loco_mvc_FileParams::renderBytes($largest),
) ) );
// file metadata
$this->set('pot', Loco_mvc_FileParams::create( $pot ) );
$this->set('dir', Loco_mvc_FileParams::create( $dir ) );
$title = __('New template file','loco');
$subhead = sprintf( __('New translations template for "%s"','loco'), $project );
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// ajax service takes the target directory path
$content_dir = loco_constant('WP_CONTENT_DIR');
$target_path = $pot->getParent()->getRelativePath($content_dir);
// hidden fields to pass through to Ajax endpoint
$this->set( 'hidden', new Loco_mvc_ViewParams( array(
'action' => 'loco_json',
'route' => 'xgettext',
'loco-nonce' => $this->setNonce('xgettext')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
'path' => $target_path,
'name' => $pot->basename(),
) ) );
// File system connect required if location not writable
if( ! $pot->creatable() ){
$path = $pot->getRelativePath($content_dir);
$this->prepareFsConnect('create', $path );
}
$this->enqueueScript('potinit');
return $this->view( 'admin/init/init-pot' );
}
} admin/init/BaseController.php 0000666 00000000000 15214173534 0012225 0 ustar 00 admin/init/InitPoController.php 0000666 00000022653 15214173534 0012577 0 ustar 00 enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New language','loco').' ‹ '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-init-po'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confising when no project-scope navigation
// $this->get('tabs')->add( __('New PO','loco'), '', true );
// bundle mandatory, but project optional
$bundle = $this->getBundle();
try {
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$subhead = sprintf( __('Initializing new translations in "%s"','loco'), $slug?$slug:$domain );
}
catch( Loco_error_Exception $e ){
$project = null;
$subhead = __('Initializing new translations in unknown set','loco');
}
$title = __('New language','loco');
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// default locale is a placeholder
$locale = new Loco_Locale('zxx');
$content_dir = rtrim( loco_constant('WP_CONTENT_DIR'), '/' );
$copying = false;
// Permit using any provided file a template instead of POT
if( $potpath = $this->get('source') ){
$potfile = new Loco_fs_LocaleFile($potpath);
$potfile->normalize( $content_dir );
if( ! $potfile->exists() ){
throw new Loco_error_Exception('Forced template argument must exist');
}
$copying = true;
// forced source could be a POT (although UI would normally prevent it)
if( $potfile->getSuffix() ){
$locale = $potfile->getLocale();
$this->set('sourceLocale', $locale );
}
}
// else project not configured. UI should prevent this by not offering msginit
else if( ! $project ){
throw new Loco_error_Exception('Cannot add new language to unconfigured set');
}
// else POT file may or may not be known, and may or may not exist
else {
$potfile = $project->getPot();
}
// start dropdown list with installed languages
$default = new Loco_Locale('en','US');
$installed = array(
new Loco_mvc_ViewParams( array(
'icon' => $default->getIcon(),
'value' => (string) $default,
'label' => $default->buildName(),
) )
);
// pull the same list of "available" languages as used in WordPress settings
$api = new Loco_api_WordPressTranslations;
$locales = array();
foreach( $api->getAvailableCore() as $tag => $raw ){
$locale = Loco_Locale::parse($tag);
$vparam = new Loco_mvc_ViewParams( array(
'icon' => $locale->getIcon(),
'value' => (string) $locale,
'label' => $locale->fetchName($api),
) );
if( $api->isInstalled($tag) ){
$installed[] = $vparam;
}
else {
$locales[] = $vparam;
}
}
$this->set( 'locales', $locales );
$this->set( 'installed', $installed );
// Critical that user selects the correct save location:
if( $project ){
$filechoice = $project->initLocaleFiles( $locale );
}
// without configured project we will only allow save to same location
else {
$filechoice = new Loco_fs_FileList;
}
// show information about POT file if we are initialializing from template
if( $potfile && $potfile->exists() ){
$meta = Loco_gettext_Metadata::load($potfile);
$total = $meta->getTotal();
$summary = sprintf( _n('One string found in %2$s','%s strings found in %s',$total,'loco'), number_format($total), $potfile->basename() );
$this->set( 'pot', new Loco_mvc_ViewParams( array(
'name' => $potfile->basename(),
'path' => $meta->getPath(false),
) ) );
// if copying an existing PO file, we can fairly safely establish the correct prefixing
if( $copying ){
$poname = ( $prefix = $potfile->getPrefix() ) ? sprintf('%s-%s.po',$prefix,$locale) : sprintf('%s.po',$locale);
$pofile = new Loco_fs_LocaleFile( $poname );
$pofile->normalize( $potfile->dirname() );
$filechoice->add( $pofile );
}
/// else if POT is in a folder we don't know about, we may as well add to the choices
// TODO this means another utilty function in project for prefixing rules on individual location
}
// else no template exists, so we prompt to extract from source
else {
$this->set( 'ext', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('type').'-xgettext', $_GET ),
'text' => __('Create template','loco'),
) ) );
// if forcing source extraction show brief description of source files
if( $this->get('extract') ){
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
$nfiles = count( $project->findSourceFiles() );
$summary = sprintf( _n('1 source file will be scanned for translatable strings','%s source files will be scanned for translatable strings',$nfiles,'loco'), number_format_i18n($nfiles) );
}
// else prompt for template creation before continuing
else {
$this->set( 'skip', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('_route'), $_GET + array( 'extract' => '1' ) ),
'text' => __('Skip template','loco'),
) ) );
// POT could still be defined, it might just not exist yet
if( $potfile ){
$this->set('pot', Loco_mvc_FileParams::create($potfile) );
}
return $this->view('admin/init/init-prompt');
}
}
$this->set( 'summary', $summary );
// group established locations into types (offical, etc..)
// there is no point checking whether any of these file exist, because we don't know what language will be chosen yet.
$locations = array();
$preferred = null;
/* @var $pofile Loco_fs_File */
foreach( $filechoice as $pofile ){
$parent = new Loco_fs_LocaleDirectory( $pofile->dirname() );
$typeId = $parent->getTypeId();
if( ! isset($locations[$typeId]) ){
$locations[$typeId] = new Loco_mvc_ViewParams( array(
'label' => $parent->getTypeLabel( $typeId ),
'paths' => array(),
) );
}
// lazy build of directory path, suppressing errors
if( ! $parent->exists() ){
try {
$parent->mkdir();
}
catch( Exception $e ){
// Loco_error_AdminNotices::warn( $e->getMessage() );
}
}
$params = new Loco_mvc_ViewParams( array (
'locked' => ! $parent->writable(),
'parent' => Loco_mvc_FileParams::create( $parent ),
'hidden' => $pofile->getRelativePath($content_dir),
'holder' => str_replace( (string) $locale, '<locale>', $pofile->basename() ),
) );
// use first writable (or createable) location as default option
if( is_null($preferred) && ! $params['locked'] ){
$preferred = $pofile;
$params['checked'] = 'checked';
}
$locations[$typeId]['paths'][] = $params;
}
$this->set( 'locations', $locations );
// hidden fields to pass through to Ajax endpoint
$this->set('hidden', new Loco_mvc_ViewParams( array(
'action' => 'loco_json',
'route' => 'msginit',
'loco-nonce' => $this->setNonce('msginit')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project ? $project->getId() : '',
'source' => $this->get('source'),
) ) );
// file system prompts will be handled when paths are selected (i.e. we don't have one yet)
$this->prepareFsConnect( 'create', '' );
$this->enqueueScript('poinit');
return $this->view( 'admin/init/init-po', array() );
}
} admin/RootController.php 0000666 00000004422 15214173534 0011347 0 ustar 00 $this->view('tab-home'),
);
}
/**
* Render main entry home screen
*/
public function render(){
// translators: home screen title where %s is the version number
$this->set('title', sprintf( __('Loco Translate %s','loco'), loco_plugin_version() ) );
// Show currently active theme on home page
$theme = Loco_package_Theme::create(null);
$this->set('theme', $this->bundleParam($theme) );
// Show plugins that have currently loaded translations
$bundles = array();
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
try {
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// bundle should exist if we heard it. reduce to debug notice
Loco_error_AdminNotices::debug( $e->getMessage() );
}
}
$this->set('plugins', $bundles );
// Show recently "used' bundles
$bundles = array();
$recent = Loco_data_RecentItems::get();
// filter in lieu of plugin setting
$maxlen = apply_filters('loco_num_recent_bundles', 10 );
foreach( $recent->getBundles(0,$maxlen) as $id ){
try {
$bundle = Loco_package_Bundle::fromId($id);
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// possible that bundle ID changed since being saved in recent items list
}
}
$this->set('recent', $bundles );
// TODO favourites/starred
// current locale notice
$tag = get_locale();
if( 'en_' !== substr($tag,0,3) ){
$locale = Loco_Locale::parse($tag);
$this->set( 'locale', $locale );
}
// roll back link
$this->set( 'rollback', Loco_mvc_AdminRouter::generate('config-version') );
return $this->view('admin/root');
}
} admin/DebugController.php 0000666 00000004743 15214173534 0011460 0 ustar 00 set('title','DEBUG');
}
/**
* {@inheritdoc}
*/
public function render(){
// debug package listener
$themes = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getThemes() as $bundle ){
$themes[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
$this->set('themes', $themes );
$plugins = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
$plugins[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
// $this->set( 'plugins', Loco_package_Plugin::get_plugins() );
// $this->set('installed', wp_get_installed_translations('plugins') );
// $this->set('active', get_option( 'active_plugins', array() ) );
// $this->set('langs',get_available_languages());
/*$plugins = get_plugins();
$plugin_info = get_site_transient( 'update_plugins' );
foreach( $plugins as $plugin_file => $plugin_data ){
if ( isset( $plugin_info->response[$plugin_file] ) ) {
$plugins[$plugin_file]['____'] = $plugin_info->response[$plugin_file];
}
}*/
/*/ inspect session and test flash messages
$session = Loco_data_Session::get();
$session->flash( 'success', microtime() );
$this->set('session', $session->getArrayCopy() );
Loco_data_Session::close();*/
/*/ try some notices
Loco_error_AdminNotices::add( new Loco_error_Success('This is a sample success message') );
Loco_error_AdminNotices::add( new Loco_error_Warning('This is a sample warning') );
Loco_error_AdminNotices::add( new Loco_error_Exception('This is a sample error') );
//*/
return $this->view('admin/debug');
}
}
admin/list/CoreController.php 0000666 00000000574 15214173534 0012273 0 ustar 00 loco-core-view
*/
class Loco_admin_list_CoreController extends Loco_admin_RedirectController {
/**
* {@inheritdoc}
*/
public function getLocation(){
return Loco_mvc_AdminRouter::generate('core-view');
}
} admin/list/ThemesController.php 0000666 00000001056 15214173534 0012624 0 ustar 00 set('type', 'theme' );
$this->set('title', __( 'Translate themes', 'loco' ) );
/* @var $theme WP_Theme */
foreach( wp_get_themes() as $theme ){
$bundle = Loco_package_Theme::create( $theme->get_stylesheet() );
$this->addBundle( $bundle );
}
return parent::render();
}
} admin/list/PluginsController.php 0000666 00000001476 15214173534 0013026 0 ustar 00 set( 'type', 'plugin' );
$this->set( 'title', __( 'Translate plugins', 'loco' ) );
foreach( Loco_package_Plugin::get_plugins() as $handle => $data ){
try {
$bundle = Loco_package_Plugin::create( $handle );
$this->addBundle($bundle);
}
// @codeCoverageIgnoreStart
catch( Exception $e ){
$bundle = new Loco_package_Plugin( $handle, $handle );
$this->addBundle( $bundle );
}
// @codeCoverageIgnoreEnd
}
return parent::render();
}
} admin/list/BaseController.php 0000666 00000004754 15214173534 0012261 0 ustar 00 getHandle();
// compatibility will be 'ok', 'warn' or 'error' depending on severity
if( $default = $bundle->getDefaultProject() ){
$compat = $default->getPot() instanceof Loco_fs_File;
}
else {
$compat = false;
}
//$info = $bundle->getHeaderInfo();
return new Loco_mvc_ViewParams( array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'dflt' => $default ? $default->getDomain() : '--',
'size' => count( $bundle ),
'save' => $bundle->isConfigured(),
'type' => $type = strtolower( $bundle->getType() ),
'view' => Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $handle ) ),
'time' => $bundle->getLastUpdated(),
) );
}
/**
* Add bundle to enabled or disabled list, depending on whether it is configured
*/
protected function addBundle( Loco_package_Bundle $bundle ){
$this->bundles[] = $this->bundleParam($bundle);
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-list-bundles'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// breadcrumb is just the root
$here = new Loco_admin_Navigation( array (
new Loco_mvc_ViewParams( array( 'name' => $this->get('title') ) ),
) );
/*/ tab between the types of bundles
$types = array (
'' => __('Home','loco'),
'theme' => __('Themes','loco'),
'plugin' => __('Plugins','loco'),
);
$current = $this->get('_route');
$tabs = new Loco_admin_Navigation;
foreach( $types as $type => $name ){
$href = Loco_mvc_AdminRouter::generate($type);
$tabs->add( $name, $href, $type === $current );
}
*/
return $this->view( 'admin/list/bundles', array (
'bundles' => $this->bundles,
'breadcrumb' => $here,
) );
}
} admin/file/BaseController.php 0000666 00000010714 15214173534 0012216 0 ustar 00 List > Bundle > Resource
*/
abstract class Loco_admin_file_BaseController extends Loco_admin_bundle_BaseController {
/**
* Check file is valid or return error
* @return string rendered error
*/
protected function getFileError( Loco_fs_File $file = null ){
// file must exist for editing
if( is_null($file) || ! $file->exists() ){
return $this->view( 'admin/errors/file-missing', array() );
}
if( $file->isDirectory() ){
$this->set('info', Loco_mvc_FileParams::create($file) );
return $this->view( 'admin/errors/file-isdir', array() );
}
/*/ just warn if file isn't writeable
if( ! $file->writable() ){
$message = __("This file isn't writeable. Click the 'File info' tab for help setting the right permissions",'loco');
Loco_error_AdminNotices::add( new Loco_error_Warning($message) ); // <- TODO add contextual help link
}*/
return '';
}
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// views at this level are always related to a file
// file is permitted to be missing during this execution.
$path = $this->get('path');
if( ! $path ){
throw new Loco_error_Exception('path argument required');
}
$file = new Loco_fs_LocaleFile( $path );
$file->normalize( loco_constant('WP_CONTENT_DIR') );
// POT file has no locale
$ext = $file->extension();
if( 'pot' === $ext ){
$locale = null;
}
// else file may have a locale suffix (unless invalid, such as "default.po")
else {
$locale = $file->getLocale();
if( $locale->isValid() ){
$locale->fetchName( new Loco_api_WordPressTranslations ) or $locale->buildName();
}
else {
$locale = null;
}
}
$this->set('file', $file );
$this->set('filetype', strtoupper($ext) );
$this->set('title', $file->basename() );
$this->set('locale', $locale );
// navigate up to root from this bundle sub view
$bundle = $this->getBundle();
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between sub view siblings for this resource
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'file-edit' => __('Editor','loco'),
'file-view' => __('Source','loco'),
'file-info' => __('File info','loco'),
'file-delete' => __('Delete','loco'),
);
$suffix = $this->get('action');
$prefix = $this->get('type');
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $_GET );
$tabs->add( $name, $href, $action === $suffix );
}
// Provide common language creation link if project scope is is valid
try {
$project = $this->getProject();
$args = array( 'bundle' => $bundle->getHandle(), 'domain' => $project->getId() );
$this->set( 'msginit', new Loco_mvc_ViewParams( array (
'href' => Loco_mvc_AdminRouter::generate( $prefix.'-msginit', $args ),
'text' => __('New language','loco'),
) ) );
}
catch( Exception $e ){
}
}
/**
* {@inheritdoc}
*/
public function view( $tpl, array $args = array() ){
if( $breadcrumb = $this->get('breadcrumb') ){
// Add project name into breadcrumb if not the same as bundle name
try {
$project = $this->getProject();
if( $project->getName() !== $this->getBundle()->getName() ){
$breadcrumb->add( $project->getName() );
}
}
catch( Loco_error_Exception $e ){
// ignore missing project in breadcrumb
}
// Always add page title as final breadcrumb element
$title = $this->get('title') or $title = 'Untitled';
$breadcrumb->add( $title );
}
return parent::view( $tpl, $args );
}
} admin/file/DeleteController.php 0000666 00000011751 15214173534 0012550 0 ustar 00 extension();
//
if( 'po' === $ext ){
$sibling = $file->cloneExtension('mo');
if( $sibling->exists() ){
$files[] = $sibling;
}
}
else if( 'mo' === $ext ){
$sibling = $file->cloneExtension('po');
if( $sibling->exists() ){
$files[] = $sibling;
}
}
else if( 'pot' !== $ext ){
throw new Loco_error_Exception( sprintf('Refusing to delete a %s file', strtoupper($ext) ) );
}
// add backups of all files (although there should be none for MO files)
foreach( array_values($files) as $file ){
$backups = new Loco_fs_Revisions($file);
foreach( $backups->getPaths() as $path ){
$files[] = new Loco_fs_File($path);
}
}
return $files;
}
/**
* {@inheritdoc}
*
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-file-delete'),
);
}*/
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
// set up form for delete confirmation
if( $file->exists() && ! $file->isDirectory() ){
// nonce action will be specific to file for extra security
// TODO could also add file MD5 to avoid deletion after changes made.
$path = $file->getPath();
$action = 'delete:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( array() );
$fields->setNonce( $action );
$this->set( 'hidden', $fields );
// attempt delete if valid nonce posted back
if( $this->checkNonce($action) ){
$api = new Loco_api_WordPressFileSystem;
// delete dependant files, so that master always exists if any others fail
$files = array_reverse( $this->expandFiles($file) );
try {
/* @var $trash Loco_fs_File */
foreach( $files as $trash ){
$api->authorizeDelete($trash);
$trash->unlink();
}
// flash message for display after redirect
try {
$n = count( $files );
Loco_data_Session::get()->flash('success', sprintf( _n('File deleted','%u files deleted',$n,'loco'),$n) );
Loco_data_Session::close();
}
catch( Exception $e ){
// tollerate session failure
}
// redirect to bundle overview
$href = Loco_mvc_AdminRouter::generate( $this->get('type').'-view', array( 'bundle' => $this->get('bundle') ) );
if( wp_redirect($href) ){
exit;
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
}
}
$bundle = $this->getBundle();
$this->set('title', sprintf( __('Delete %s','loco'), $file->basename() ).' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$files = $this->expandFiles( $file );
$info = Loco_mvc_FileParams::create($file);
$this->set( 'info', $info );
$this->set( 'title', sprintf( __('Delete %s','loco'), $info->name ) );
$locked = $file->deletable() ? 0 : 1;
// warn about additional files that will be deleted along with this
if( $deps = array_slice($files,1) ){
$count = count($deps);
$this->set('warn', sprintf( _n( 'One dependant file will also be deleted', '%u dependant files will also be deleted', $count, 'loco' ), $count ) );
$infos = array();
foreach( $deps as $depfile ){
$infos[] = Loco_mvc_FileParams::create( $depfile );
if( is_int($locked) && ! $depfile->deletable() ){
++$locked;
}
}
$this->set('deps', $infos );
}
$this->set( 'locked', $locked );
if( $locked ){
$this->prepareFsConnect( 'delete', $this->get('path') );
}
$this->enqueueScript('delete');
return $this->view('admin/file/delete');
}
} admin/file/EditController.php 0000666 00000020023 15214173534 0012223 0 ustar 00 enqueueStyle('editor');
//
$file = $this->get('file');
$bundle = $this->getBundle();
// translators: %1$s is the file name, %2$s is the bundle name
$this->set('title', sprintf( __('Editing %1$s in %2$s','loco'), $file->basename(), $bundle ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-file-edit'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
// editor will be rendered
$this->enqueueScript('editor');
// Parse file data into JavaScript for editor
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load( $file );
}
catch( Exception $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
$data = Loco_gettext_Data::dummy();
}
// Pre-populate PO headers with data that JavaScript doesn't have access to
if( $locale = $this->get('locale') ){
$data->localize( $locale );
$lname = $locale->getName() or $lname = (string) $locale;
$this->set( 'localeName', $lname );
}
// default is to permit editing of any file
$readonly = false;
// Establish if file belongs to a configured project
try {
$bundle = $this->getBundle();
$project = $this->getProject();
}
// Fine if not, this just means sync isn't possible.
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::debug( sprintf("Sync is disabled because this file doesn't relate to a known set of translations", $bundle ) );
$project = null;
}
// Establish PO/POT edit mode
if( $locale ){
// alternative POT file may be forced by PO headers
$head = $data->getHeaders();
if( $head->has('X-Loco-Template') ){
$potfile = new Loco_fs_File($head['X-Loco-Template']);
$potfile->normalize( $bundle->getDirectoryPath() );
}
// no way to get configured POT if invalid project
else if( is_null($project) ){
$potfile = null;
}
// else use project-configured template, assuming there is one
else if( $potfile = $project->getPot() ){
// Handle situation where project defines a localised file as the official template
if( $potfile->equal($file) ){
$locale = null;
$potfile = null;
}
}
if( $potfile ){
// Validate template file as long as it exists
if( $potfile->exists() ){
$potdata = Loco_gettext_Data::load( $potfile );
if( ! $potdata->equalSource($data) ){
Loco_error_AdminNotices::debug( sprintf( __("Translations don't match template. Run sync to update from %s",'loco'), $potfile->basename() ) );
}
}
// else template doesn't exist, so sync will be done to source code
else {
// Loco_error_AdminNotices::debug( sprintf( __('Template file not found (%s)','loco'), $potfile->basename() ) );
$potfile = null;
}
}
}
// notify if template is locked (save and sync will be disabled)
if( is_null($locale) && $project && $project->isPotLocked() ){
Loco_error_AdminNotices::warn('Template is protected from updates by the bundle configuration');
$readonly = true;
}
// back end expects paths relative to wp-content
$wp_content = loco_constant('WP_CONTENT_DIR');
$this->set( 'js', new Loco_mvc_ViewParams( array(
'podata' => $data->jsonSerialize(),
'locale' => $locale ? $locale->jsonSerialize() : null,
'potpath' => $locale && $potfile ? $potfile->getRelativePath($wp_content) : null,
'popath' => $this->get('path'),
'readonly' => $readonly,
'project' => $project ? array (
'bundle' => $bundle->getId(),
'domain' => $project->getId(),
) : null,
'nonces' => $readonly ? null : array (
'save' => wp_create_nonce('save'),
'sync' => wp_create_nonce('sync'),
'fsConnect' => wp_create_nonce('fsConnect'),
),
) ) );
$this->set( 'ui', new Loco_mvc_ViewParams( array(
// Translators: button for adding a new string when manually editing a POT file
'add' => _x('Add','Editor','loco'),
// Translators: button for removing a string when manually editing a POT file
'del' => _x('Remove','Editor','loco'),
'help' => __('Help','loco'),
// Translators: Button that saves translations to disk
'save' => _x('Save','Editor','loco'),
// Translators: Button that runs in-editor sync/operation
'sync' => _x('Sync','Editor','loco'),
// Translators: Button that reloads current screen
'revert' => _x('Revert','Editor','loco'),
// Translators: Button that toggles a translation's Fuzzy flag
'fuzzy' => _x('Fuzzy','Editor','loco'),
// Translators: Button for downloading a PO, MO or POT file
'download' => _x('Download','Editor','loco'),
// Translators: Placeholder text for text filter above editor
'filter' => __('Filter translations','loco'),
// Translators: Button that toggles invisible characters
'invs' => _x('Toggle invisibles','Editor','loco'),
// Translators: Button that toggles between "code" and regular text editing modes
'code' => _x('Toggle code view','Editor','loco'),
) ) );
// Download form params
$hidden = new Loco_mvc_HiddenFields( array(
'path' => '',
'source' => '',
'route' => 'download',
'action' => 'loco_download',
) );
$this->set( 'dlFields', $hidden->setNonce('download') );
$this->set( 'dlAction', admin_url('admin-ajax.php','relative') );
// validate file system writableness for all operations involved in save
$writable = $file->writable();
// Check in advance if MO file can be compiled in this directory
if( $writable ){
$dummy = $file->cloneExtension('mo');
if( ! ( $dummy->exists() ? $dummy->writable() : $dummy->creatable() ) ){
$writable = false;
}
// Check in advance if backups will work in this directory
else if( Loco_data_Settings::get()->num_backups ){
$dummy = new Loco_fs_File( $file->dirname().'/does-not-exist.po~' );
if( ! $dummy->creatable() ){
$writable = false;
}
}
}
// File system connect if any operations likely to fail
if( ! $writable ){
$this->prepareFsConnect( 'connect', $this->get('path') );
}
// set simpler title for breadcrumb
$this->set('title', $file->basename() );
// ok to render editor as either po or pot
$tpl = $locale ? 'po' : 'pot';
return $this->view( 'admin/file/edit-'.$tpl, array() );
}
} admin/file/ViewController.php 0000666 00000005622 15214173534 0012260 0 ustar 00 enqueueStyle('poview');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set( 'title', 'Source of '.$file->basename().' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-file-view'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
$file = $this->get('file');
$name = $file->basename();
$type = strtolower( $file->extension() );
$this->set('title', $name );
if( $fail = $this->getFileError($file) ){
return $fail;
}
// Establish if file belongs to a configured project
try {
$bundle = $this->getBundle();
$project = $this->getProject();
}
catch( Exception $e ){
$project = null;
}
// Parse data before rendering, so we know it's a valid Gettext format
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load( $file );
$this->set( 'meta', Loco_gettext_Metadata::create($file, $data) );
}
catch( Exception $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
}
// binary MO will be hex-formated in template
if( 'mo' === $type ){
$this->set('bin', $file->getContents() );
return $this->view('admin/file/view-mo' );
}
// else is a PO or POT file
$this->enqueueScript('poview');//->enqueueScript('min/highlight');
$lines = preg_split('/\\R/u', loco_ensure_utf8( $file->getContents() ) );
$this->set( 'lines', $lines );
// ajax parameters required for pulling reference sources
$this->set('js', new Loco_mvc_ViewParams( array (
'popath' => $this->get('path'),
'nonces' => array(
'fsReference' => wp_create_nonce('fsReference'),
),
'project' => $bundle ? array (
'bundle' => $bundle->getId(),
'domain' => $project ? $project->getId() : '',
) : null,
) ) );
// treat as PO if file name has locale
if( $locale = $this->get('locale') ){
$lname = $locale->getName() or $lname = (string) $locale;
$this->set( 'localeName', $lname );
return $this->view('admin/file/view-po' );
}
// else view as POT
return $this->view('admin/file/view-pot' );
}
} admin/file/InfoController.php 0000666 00000015457 15214173534 0012250 0 ustar 00 enqueueStyle('fileinfo');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set('title', $file->basename().' ‹ '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->view('tab-file-info'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
$name = $file->basename();
$this->set('title', $name );
if( $fail = $this->getFileError($file) ){
return $fail;
}
$ext = strtolower( $file->extension() );
$path = $file->getPath();
// file info
$info = Loco_mvc_FileParams::create( $file );
$this->set('file', $info );
$info['type'] = strtoupper($ext);
if( $file->exists() ){
$info['existant'] = true;
$info['writable'] = $file->writable();
$info['deletable'] = $file->deletable();
$info['mtime'] = $file->modified();
}
// location info
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
$info = Loco_mvc_FileParams::create( $dir );
$this->set('dir', $info );
$info['type'] = $dir->getTypeId();
if( $dir->exists() && $dir->isDirectory() ){
$info['existant'] = true;
$info['writable'] = $dir->writable();
}
// get the name of the webserver for information purposes
$this->set('httpd', Loco_compat_PosixExtension::getHttpdUser() );
// unknown file template if required
$locale = null;
$tpl = 'admin/file/info-other';
// we should know the project the file belongs to, but permitting orphans for debugging
try {
$project = $this->getProject();
$template = $project->getPot();
$isTemplate = $template && $file->equal($template);
$this->set('isTemplate', $isTemplate );
}
catch( Loco_error_Exception $e ){
$isTemplate = false;
$template = null;
}
// file will be Gettext most likely
if( 'pot' === $ext || 'po' === $ext || 'mo' === $ext ){
// treat as templte until locale verified
$tpl = 'admin/file/info-pot';
// don't attempt to pull locale of template file
if( 'pot' !== $ext && ! $isTemplate ){
$locale = $file->getLocale();
$code = (string) $locale;
if( $locale->isValid() ){
$api = new Loco_api_WordPressTranslations;
$locale->fetchName( $api );
$this->set( 'locale', new Loco_mvc_ViewParams( array(
'code' => $code,
'name' => $locale->getName(),
'icon' => $locale->getIcon(),
'lang' => $locale->lang,
) ) );
// find PO/MO counter parts
if( 'po' === $ext ){
$tpl = 'admin/file/info-po';
$sibling = $file->cloneExtension('mo');
}
else {
$tpl = 'admin/file/info-mo';
$sibling = $file->cloneExtension('po');
}
$info = Loco_mvc_FileParams::create($sibling);
$this->set( 'sibling', $info );
if( $sibling->exists() ){
$info['existant'] = true;
$info['writable'] = $sibling->writable();
}
}
}
// Do full parse to get stats and headers
try {
$data = Loco_gettext_Data::load($file);
$head = $data->getHeaders();
$author = $head->trimmed('Last-Translator') or $author = __('Unknown author','loco');
$this->set( 'author', $author );
// date headers may not be same as file modification time (files copied to server etc..)
$podate = $head->trimmed( $locale ? 'PO-Revision-Date' : 'POT-Creation-Date' );
$potime = Loco_gettext_Data::parseDate($podate) or $potime = $file->modified();
$this->set('potime', $potime );
// access to meta stats, normally cached on listing pages
$meta = Loco_gettext_Metadata::create($file,$data);
$this->set( 'meta', $meta );
// allow PO header to specify alternative template for sync
if( $head->has('X-Loco-Template') ){
$altpot = new Loco_fs_File($head['X-Loco-Template']);
$altpot->normalize( $this->getBundle()->getDirectoryPath() );
if( $altpot->exists() && ( ! $template || ! $template->equal($altpot) ) ){
$this->set('altpot', true );
$template = $altpot;
}
}
// missing or invalid headers are tollerated but developers should be notified
if( ! count($head) ){
Loco_error_AdminNotices::debug(__('File does not have a valid header','loco'));
}
// establish whether PO is in sync with POT
if( $template && ! $isTemplate && 'po' === $ext && $template->exists() ){
try {
$this->set('potfile', new Loco_mvc_FileParams( array(
'synced' => Loco_gettext_Data::load($template)->equalSource($data),
), $template ) );
}
catch( Exception $e ){
// ignore invalid template in this context
}
}
// language sanity check. Developer warning if file name disagrees with PO header
if( $locale && ( $value = $head['Language'] ) ){
$check = (string) Loco_Locale::parse($value);
if( $check !== $code ){
Loco_error_AdminNotices::debug( sprintf( __('Language header is "%s" but file name contains "%s"','loco'), $value, $code ) );
}
}
}
catch( Exception $e ){
$this->set('error', $e->getMessage() );
$tpl = 'admin/file/info-other';
}
}
return $this->view( $tpl );
}
}
api/WordPressTranslations.php 0000666 00000002716 15214173534 0012377 0 ustar 00 installed) ){
// wp-includes/l10n.php should always be included at runtime
$this->installed = array( 'en_US' => 1 ) + array_flip( get_available_languages() );
}
return array_key_exists( (string) $locale, $this->installed );
}
/**
* Get WordPress locale data by strictly well-formed language tag
* @return array
*/
public function getLocaleData( $tag ){
$all = $this->getAvailableCore();
return isset($all[$tag]) ? $all[$tag] : null;
}
/**
* Get and populate a Locale object from a valid language tag
* @return Loco_Locale
*/
public function getLocale( $tag ){
$obj = Loco_Locale::parse($tag);
if( $obj->isValid() ){
$obj->fetchName( $this );
}
return $obj;
}
}
api/WordPressFileSystem.php 0000666 00000021252 15214173534 0011776 0 ustar 00 form;
}
/**
* Authorize for the creation of a file that does not exist
* @return bool whether file system is authorized NOT necessarily whether file is creatable
*/
public function authorizeCreate( Loco_fs_File $file ){
if( $file->exists() ){
throw new Loco_error_WriteException( sprintf( __('%s already exists in this folder','loco'), $file->basename() ) );
}
return $file->creatable() || $this->authorize($file);
}
/**
* Authorize for the update of a file that does exist
* @return bool whether file system is authorized NOT necessarily whether file is updatable
*/
public function authorizeUpdate( Loco_fs_File $file ){
if( ! $file->exists() ){
throw new Loco_error_WriteException("File doesn't exist, try authorizeCreate");
}
return $file->writable() || $this->authorize($file);
}
/**
* Authorize for the removal of an existing file
* @return bool whether file system is authorized NOT necessarily whether file is removable
*/
public function authorizeDelete( Loco_fs_File $file ){
if( ! $file->exists() ){
throw new Loco_error_WriteException("Can't delete a file that doesn't exist");
}
return $file->deletable() || $this->authorize($file);
}
/**
* Authorizes update or create, depending on whether file exists
* @return bool whether file system is authorized
*/
public function authorizeWrite( Loco_fs_File $file ){
return ( $file->exists() ? $file->writable() : $file->creatable() ) || $this->authorize($file);
}
/**
* Authorize connection for any operations, regardless of whether direct file system is available.
* @return bool whether file system is authorized
*/
public function authorizeConnect( Loco_fs_File $file ){
return $this->authorize($file);
}
/**
* Wraps `request_filesystem_credentials` negotiation to obtain a remote connection and buffer WordPress form outout
* Call before output started, because buffers.
*/
private function authorize( Loco_fs_File $file ){
$this->fs = null;
$this->form = '';
$this->creds_out = array();
// observe settings held temporarily in session
try {
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
$creds = $session['loco-fs'];
if( $this->tryCredentials($creds,$file) ){
$this->creds_in = array();
return true;
}
}
}
catch( Exception $e ){
// tollerate session failure
}
$post = Loco_mvc_PostParams::get();
$dflt = array( 'hostname' => '', 'username' => '', 'password' => '', 'public_key' => '', 'private_key' => '', 'connection_type' => '');
$this->creds_in = array_intersect_key( $post->getArrayCopy(), $dflt );
// deliberately circumventing call to `get_filesystem_method`
// risk of WordPress version compatibility issues, but only sane way to force a remote connection
// @codeCoverageIgnoreStart
if( defined('FS_METHOD') && FS_METHOD ){
$type = FS_METHOD;
// forcing direct access means request_filesystem_credentials will never give us a form :(
if( 'direct' === $type ){
return false;
}
}
else if( 'ssh' === $post->connection_type && extension_loaded('ssh2') && function_exists('stream_get_contents') ){
$type = 'ssh2';
}
else if( extension_loaded('ftp') ){
$type = 'ftpext';
}
else if( extension_loaded('sockets') || function_exists('fsockopen') ){
$type = 'ftpsockets';
}
// @codeCoverageIgnoreEnd
else {
$type = '';
}
// context is nonsense here as the system doesn't know what operation we're performing
// testing directory write-permission when we're updating a file, for example.
$context = '/ignore/this';
$type = apply_filters( 'filesystem_method', $type, $post->getArrayCopy(), $context, true );
// the only params we'll pass into form will be those used by the ajax fsConnect end point
$extra = array( 'loco-nonce', 'path', 'auth' );
// capture WordPress output during negotiation.
$buffer = Loco_output_Buffer::start();
if( $creds = request_filesystem_credentials( '', $type, false, $context, $extra ) ){
// credentials passed through, should allow connect if they are correct
if( $this->tryCredentials( $creds?$creds:array(), $file ) ){
$this->persistCredentials();
return true;
}
// else there must be an error with the credentials
$error = true;
// pull more useful connection error for display in form
if( isset($GLOBALS['wp_filesystem']) ){
$fs = $GLOBALS['wp_filesystem'];
$GLOBALS['wp_filesystem'] = null;
if( $fs && $fs->errors && $fs->errors->get_error_code() ){
$error = $fs->errors;
}
}
// annoyingly WordPress moves the error notice above the navigation tabs :-/
request_filesystem_credentials( '', $type, $error, $context, $extra );
}
// now have unauthorized remote connection
$this->form = (string) $buffer->close();
return false;
}
/**
* @internal
* @return bool when credentials connected ok
*/
private function tryCredentials( array $creds, Loco_fs_File $file ){
// lazy construct the file system from current credentials if possible
// in typical WordPress style, after success the object will be held in a global.
if( WP_Filesystem( $creds, '/ignore/this/' ) ){
$this->fs = $GLOBALS['wp_filesystem'];
// hook new file system into write context (specifying that connect has already been performed)
$file->getWriteContext()->connect( $this->fs, false );
//
$this->creds_out = $creds;
return true;
}
return false;
}
/**
* Set current credentials in session if settings allow
* @return bool whether creds persisted
*/
private function persistCredentials(){
try {
$settings = Loco_data_Settings::get();
if( $settings['fs_persist'] ){
$session = Loco_data_Session::get();
$session['loco-fs'] = $this->creds_out;
$session->persist();
return true;
}
}
catch( Exception $e ){
// tollerate session failure
Loco_error_AdminNotices::debug( $e->getMessage() );
}
return false;
}
/**
* Get working credentials that resulted in connection
* @return array
*/
public function getOutputCredentials(){
return $this->creds_out;
}
/**
* Get input credentials from original post.
* this is not the same as getCredentials. It is designed for replay only, regardless of success
* Note that input to request_filesystem_credentials is not the same as the output (specifically how hostname:port is handled)
*/
public function getInputCredentials(){
return $this->creds_in;
}
/**
* Get currently configured filesystem API
* @return WP_Filesystem_Direct
*/
public function getFileSystem(){
if( ! $this->fs ){
$this->fs = new WP_Filesystem_Direct( null );
}
return $this->fs;
}
/**
* Check if filesystem access is direct
* @return bool
*/
public function isDirect(){
return 'direct' === $this->getFileSystem()->method;
}
} fs/Link.php 0000666 00000000671 15214173534 0006577 0 ustar 00 getPath();
if( $real = realpath($path) ){
if( is_dir($path) ){
return new Loco_fs_Directory($real);
}
return new Loco_fs_File($real);
}
// else fail
return null;
}
} fs/Revisions.php 0000666 00000010234 15214173534 0007657 0 ustar 00 -backup-.~"
*/
class Loco_fs_Revisions implements Countable/*, IteratorAggregate*/ {
/**
* @var loco_fs_File
*/
private $master;
/**
* Sortable list of backed up file paths (not including master)
* @var array
*/
private $paths;
/**
* Cached count of backups + 1
* @var int
*/
private $length;
/**
* Paths to delete when object removed from memory
* @var array
*/
private $trash = array();
/**
* Construct from master file (current version)
*/
public function __construct( Loco_fs_File $file ){
$this->master = $file;
}
/**
* @internal
* Executes deferred deletions with silent errors
*/
public function __destruct(){
if( $trash = $this->trash ){
$writer = $this->master->getWriteContext();
foreach( $trash as $file ){
if( $file->exists() ){
try {
$writer->setFile($file);
$writer->delete(false);
}
catch( Loco_error_WriteException $e ){
// avoiding fatals as pruning is non-critical operation
}
}
}
}
}
/**
* Check that file permissions allow a new backup to be created
* @return bool
*/
public function writable(){
return $this->master->getParent()->writable();
}
/**
* Create a new backup of current version
* @return Loco_fs_File
*/
public function create(){
$vers = 0;
$date = date('YmdHis');
$ext = $this->master->extension();
$base = $this->master->dirname().'/'.$this->master->filename();
do {
$path = sprintf( '%s-backup-%s%u.%s~', $base, $date, $vers++, $ext);
}
while (
file_exists($path)
);
$copy = $this->master->copy( $path );
// invalidate cache so next access reads disk
$this->paths = null;
$this->length = null;
return $copy;
}
/**
* Delete oldest backups until we have maximuim of $num_backups remaining
* @return Loco_fs_Revisions
*/
public function prune( $num_backups ){
$paths = $this->getPaths();
if( isset($paths[$num_backups]) ){
foreach( array_slice( $paths, $num_backups ) as $path ){
$this->unlinkLater($path);
}
$this->paths = array_slice( $paths, 0, $num_backups );
$this->length = null;
}
return $this;
}
/**
* @return array
*/
public function getPaths(){
if( is_null($this->paths) ){
// build regex for matching backed up revisions of master
$regex = preg_quote( $this->master->filename(), '/' ).'-backup-(\\d{14,})?';
if( $ext = $this->master->extension() ){
$regex .= preg_quote('.'.$ext,'/');
}
$regex = '/'.$regex.'~/';
//
$this->paths = array();
$finder = new Loco_fs_FileFinder( $this->master->dirname() );
/** @var $file Loco_fs_File */
foreach( $finder as $file ){
if( preg_match( $regex, $file->basename(), $r ) ){
$this->paths[] = $file->getPath();
}
}
// time sort order descending
rsort( $this->paths );
}
return $this->paths;
}
/**
* Get number of backups plus master
* @return int
*/
public function count(){
if( ! $this->length ){
$this->length = 1 + count( $this->getPaths() );
}
return $this->length;
}
/**
* Delete file when object removed from memory.
* Previously unlinked on shutdown, but doesn't work with WordPress file system abstraction
* @return void
*/
public function unlinkLater($path){
$this->trash[] = new Loco_fs_File($path);
}
} fs/FileList.php 0000666 00000004772 15214173534 0007423 0 ustar 00 add( $file );
}
}
/**
* Like getArrayCopy, but exports string paths
* @return array
*/
public function export(){
$a = array();
foreach( $this as $file ){
$a[] = (string) $file;
}
return $a;
}
/**
* @internal
*/
public function __toString(){
return implode( "\n", $this->getArrayCopy() );
}
/**
* Generate a unique key for file.
*/
private function hash( Loco_fs_File $file ){
$path = $file->normalize();
// if file is real, we must resolve its real path
if( $file->exists() && ( $real = realpath($path) ) ){
$path = $real;
}
return $path;
}
/**
* @codeCoverageIgnore
*/
public function offsetSet( $index, $value ){
throw new Exception('Use Loco_fs_FileList::add');
}
/**
* Add a file uniquely collection
* @return Loco_fs_FileList
*/
public function add( Loco_fs_File $file ){
$hash = $this->hash( $file );
if( ! isset($this->unique[$hash]) ){
$this->unique[$hash] = true;
parent::offsetSet( null, $file );
}
return $this;
}
/**
* Check if given file is already in list
* @return bool
*/
public function has( Loco_fs_File $file ){
$hash = $this->hash( $file );
return isset($this->unique[$hash]);
}
/**
* Get a copy of list with only files not contained in passed list
* @return Loco_fs_FileList
*/
public function diff( Loco_fs_FileList $not_in ){
$list = new Loco_fs_FileList;
foreach( $this as $file ){
$not_in->has($file) || $list->add( $file );
}
return $list;
}
/**
* Merge another list of the SAME TYPE uniquely on top of current one
* @return Loco_fs_FileList
*/
public function augment( loco_fs_FileList $list ){
foreach( $list as $file ){
$this->add( $file );
}
return $this;
}
}
fs/FileMode.php 0000666 00000003223 15214173534 0007362 0 ustar 00 i = (int) $mode;
}
/**
* @return string
*/
public function __toString(){
return sprintf('%03o', $this->i & 07777 );
}
/**
* rwx style friendly formatting
* @return string
*/
public function format(){
$mode = $this->i;
$setuid = $mode & 04000;
$setgid = $mode & 02000;
$sticky = $mode & 01000;
return
$this->type().
( $mode & 0400 ? 'r' : '-' ).
( $mode & 0200 ? 'w' : '-' ).
( $mode & 0100 ? ($setuid?'s':'x') : ($setuid?'S':'-') ).
( $mode & 0040 ? 'r' : '-' ).
( $mode & 0020 ? 'w' : '-' ).
( $mode & 0010 ? ($setgid?'s':'x') : ($setgid?'S':'-') ).
( $mode & 0004 ? 'r' : '-' ).
( $mode & 0002 ? 'w' : '-' ).
( $mode & 0001 ? ($sticky?'t':'x') : ($sticky?'T':'-') );
}
/**
* File type bit field:
* http://man7.org/linux/man-pages/man2/stat.2.html
*/
public function type(){
$mode = $this->i & 0170000;
switch( $mode ){
case 0010000:
return '-';
case 0040000:
return 'd';
case 0120000:
return 'l';
case 0140000:
return 's';
case 0060000:
return 'c';
default:
return '-';
}
}
} fs/LocaleFile.php 0000666 00000005450 15214173534 0007701 0 ustar 00 suffix) ){
$parts = explode( '-', $this->filename() );
$this->suffix = array_pop( $parts );
$this->prefix = implode( '-', $parts );
// handle situations where suffixless name is wrongly taken as the prefix
// e.g. "de.po" is valid but "hello.po" is not.
// There are still some ambigous situations, e.g. "foo-bar.po" is valid, but nonsense
if( ! $this->prefix && ! $this->getLocale()->isValid() ){
$this->prefix = $this->suffix;
$this->suffix = '';
$this->locale = null;
}
}
return array( $this->prefix, $this->suffix );
}
/**
* @var Loco_Locale
*/
public function getLocale(){
if( ! $this->locale ){
if( $tag = $this->getSuffix() ){
$this->locale = Loco_Locale::parse($tag);
}
else {
$this->locale = new Loco_Locale('');
}
}
return $this->locale;
}
/**
* @return Loco_fs_LocaleFile
*/
public function cloneLocale( Loco_locale $locale ){
$this->split();
$path = (string) $locale;
if( $str = $this->prefix ){
$path = $str.'-'.$path;
}
if( $str = $this->extension() ){
$path .= '.'.$str;
}
if( $dir = $this->getParent() ){
$path = $dir->getPath().'/'.$path;
}
return new Loco_fs_LocaleFile($path);
}
/**
* Get prefix (or stem) from name that comes before locale suffix
* @return string
*/
public function getPrefix(){
$info = $this->split();
return $info[0];
}
/**
* Get suffix (or locale code) from name that comes after "-" separator
* @return string
*/
public function getSuffix(){
$info = $this->split();
return $info[1];
}
/**
* Test if file is suffix only, e.g. "en_US.po"
* @return bool
*/
public function hasSuffixOnly(){
$info = $this->split();
return $info[1] && ! $info[0];
}
/**
* Test if file is prefix only, e.g. "incorrect.po"
* @return bool
*/
public function hasPrefixOnly(){
$info = $this->split();
return $info[0] && ! $info[1];
}
}
fs/LocaleFileList.php 0000666 00000003173 15214173534 0010535 0 ustar 00 add( $file );
if( count($this) !== $i ){
if( $key = $file->getSuffix() ){
$this->index[$key][] = $i;
}
}
return $this;
}
/**
* Get a new list containing just files for a given locale (exactly)
* @return Loco_fs_LocaleFileList
*/
public function filter( $tag ){
$list = new Loco_fs_LocaleFileList;
if( isset($this->index[$tag]) ){
foreach( $this->index[$tag] as $i ){
$list->addLocalized( $this[$i] );
}
}
return $list;
}
/**
* Get a unique list of valid locales for which there are files
* @return array
*/
public function getLocales(){
$list = array();
foreach( array_keys($this->index) as $tag ){
$locale = Loco_Locale::parse($tag);
if( $locale->isValid() ){
$list[$tag] = $locale;
}
}
return $list;
}
/**
* {@inheritdoc}
* @return Loco_fs_LocaleFileList
*/
public function augment( Loco_fs_FileList $list ){
foreach( $list as $file ){
$this->addLocalized( $file );
}
return $this;
}
} fs/FileFinder.php 0000666 00000025572 15214173534 0007720 0 ustar 00 roots = new Loco_fs_FileList;
$this->excluded = array();
if( $root ){
$this->addRoot( $root );
}
}
/**
* Set recursive state of all defined roots
* @return Loco_fs_FileFinder
*/
public function setRecursive( $bool ){
$this->invalidate();
$this->recursive = $bool;
/* @var $dir Loco_fs_Directory */
foreach( $this->roots as $dir ){
$dir->setRecursive( $bool );
}
return $this;
}
/**
* @return Loco_fs_FileFinder
*/
public function followLinks( $bool ){
$this->invalidate();
$this->symlinks = (bool) $bool;
return $this;
}
/**
*
*/
private function invalidate(){
$this->cached = false;
$this->cache = null;
$this->subdir = null;
}
/**
* @return Loco_fs_FileList
*/
public function export(){
if( ! $this->cached ){
$this->rewind();
while( $this->valid() ){
$this->next();
}
}
return $this->cache;
}
/**
* @return array
*/
public function exportGroups(){
$this->cached || $this->export();
return $this->exts;
}
/**
* Add a directory root to search.
* @return Loco_fs_FileFinder
*/
public function addRoot( $root, $recursive = null ){
$this->invalidate();
$dir = new Loco_fs_Directory($root);
$this->roots->add( $dir );
// new directory inherits current global setting unless set explicitly
$dir->setRecursive( is_bool($recursive) ? $recursive : $this->recursive );
return $this;
}
/**
* Get all root directories to be searched
* @return Loco_fs_FileList
*/
public function getRootDirectories(){
return $this->roots;
}
/**
* Group results by file extension
* @return Loco_fs_FileFinder
*/
public function group(){
return $this->groupBy( func_get_args() );
}
/**
* Group results by file extensions given in array
* @return Loco_fs_FileFinder
*/
public function groupBy( array $exts ){
$this->invalidate();
$this->exts = array();
foreach( $exts as $ext ){
$this->exts[ trim($ext,'*.') ] = new Loco_fs_FileList;
}
return $this;
}
/**
* Add one or more paths to exclude from listing
* @param string e.g "node_modules"
* @return Loco_fs_FileFinder
*/
public function exclude(){
$this->invalidate();
foreach( func_get_args() as $path ){
$file = new Loco_fs_File($path);
// if path is absolute, add straight onto list
if( $file->isAbsolute() ){
$file->normalize();
$this->excluded[] = $file;
}
// else append to all defined roots
else {
foreach( $this->roots as $dir ) {
$file = new Loco_fs_File( $dir.'/'.$path );
$file->normalize();
$this->excluded[] = $file;
}
}
}
return $this;
}
/**
* Export excluded paths as file objects
* @return array
*/
public function getExcluded(){
return $this->excluded;
}
/**
* @return resource
*/
private function open( Loco_fs_Directory $dir ){
$path = $dir->getPath();
$recursive = $dir->isRecursive();
if( is_link($path) ){
$link = new Loco_fs_Link($path);
if( $dir = $link->resolve() ){
$dir->setRecursive( $recursive );
return $this->open( $dir );
}
}// @codeCoverageIgnore
/*if( ! is_dir($path) ){
throw new InvalidArgumentException('Path is not a readable directory, '.$path );
}*/
$this->cwd = $path;
$this->recursing = $recursive;
return $this->dir = opendir( $path );
}
private function close(){
closedir( $this->dir );
$this->dir = null;
$this->recursing = null;
}
/**
* Test if given path is matched by one of our exclude rules
* TODO would prefer a method that didn't require iteration
* @param string
* @return bool
*/
public function isExcluded( $path ){
/* @var $excl Loco_fs_File */
foreach( $this->excluded as $excl ){
if( $excl->equal($path) ){
return true;
}
}
return false;
}
/**
* Read next valid file path from root directories
* @return Loco_fs_File
*/
private function read(){
$path = null;
if( is_resource($this->dir) ){
while( $f = readdir($this->dir) ){
if( '.' === $f{0} ){
continue;
}
$path = $this->cwd.'/'.$f;
// follow symlinks (subdir hash ensures against loops)
if( is_link($path) ){
if( ! $this->symlinks ){
continue;
}
$link = new Loco_fs_Link($path);
if( $file = $link->resolve() ){
$path = $file->getPath();
}
else {
continue;
}
}
// add subdirectory to recursion list
// this will result in breadth-first listing
if( is_dir($path) ){
if( $this->recursing && ! $this->isExcluded($path) ){
$subdir = new Loco_fs_Directory($path);
$subdir->setRecursive(true);
$this->subdir->add( $subdir );
}
continue;
}
else if( $this->isExcluded($path) ){
continue;
}
// file represented as object containing original path
$file = new Loco_fs_File( $path );
$this->add( $file );
$this->i++;
return $file;
}
$this->close();
}
// try next dir if nothing matched in this one
$d = $this->d + 1;
if( isset($this->subdir[$d]) ){
$this->d = $d;
$this->open( $this->subdir[$d] );
return $this->read();
}
// else at end of all available files
$this->cached = true;
}
/**
* Implement FileListInterface::add
*/
public function add( Loco_fs_File $file ){
if( $this->exts ){
$ext = $file->extension();
if( ! isset($this->exts[$ext]) ){
return;
}
$this->exts[$ext]->add( $file );
}
$this->cache->add( $file );
}
/**
* @return int
*/
public function count(){
return count( $this->export() );
}
/**
* @return Loco_fs_File|null
*/
public function current(){
$i = $this->i;
if( is_int($i) && isset($this->cache[$i]) ){
return $this->cache[$i];
}
}
/**
* @return Loco_fs_File|null
*/
public function next(){
if( $this->cached ){
$i = $this->i + 1;
if( isset($this->cache[$i]) ){
$this->i = $i;
return $this->cache[$i];
}
}
else if( $path = $this->read() ){
return $path;
}
// else at end of all directory listings
$this->i = null;
return null;
}
public function key(){
return $this->i;
}
public function valid(){
// may be in lazy state after rewind
// must do initial read now in case list is empty
return is_int($this->i);
}
/**
* @return void
*/
public function rewind(){
if( $this->cached ){
reset( $this->cache );
$this->i = key($this->cache);
}
else {
$this->d = 0;
$this->dir = null;
$this->cache = new Loco_fs_FileList;
// add only root directories that exist
$this->subdir = new Loco_fs_FileList;
/* @var Loco_fs_Directory */
foreach( $this->roots as $root ){
if( $root->exists() && ! $this->isExcluded( $root->getPath() ) ){
$this->subdir->add( $root );
}
}
if( $root = reset($this->subdir) ){
$this->i = -1;
$this->open( $root );
$this->next();
}
else {
$this->i = null;
$this->subdir = null;
$this->cached = true;
}
}
}
/**
* test whether internal list has been fully cached in memory
*/
public function isCached(){
return $this->cached;
}
}
fs/File.php 0000666 00000031415 15214173534 0006561 0 ustar 00 setPath( $path );
}
/**
* Internally set path value and flag whether relative or absolute
*/
private function setPath( $path ){
$path = (string) $path;
if( $fixed = self::abs($path) ){
$path = $fixed;
$this->rel = false;
}
else {
$this->rel = true;
}
if( $path !== $this->path ){
$this->path = $path;
$this->info = null;
}
return $path;
}
/**
* @return array
*/
public function isAbsolute(){
return ! $this->rel;
}
/**
* @internal
*/
public function __clone(){
$this->cloneWriteContext( $this->w );
}
/**
* Copy write context with oursel
* @return
*/
private function cloneWriteContext( Loco_fs_FileWriter $context = null ){
if( $context ){
$context = clone $context;
$this->w = $context->setFile($this);
}
return $this;
}
/**
* Get file system context for operations that *modify* the file system.
* Read operations and operations that stat the file will always do so directly.
* @return Loco_fs_FileWriter
*/
public function getWriteContext(){
if( ! $this->w ){
$this->w = new Loco_fs_FileWriter( $this );
}
return $this->w;
}
/**
* @internal
*/
private function pathinfo(){
return is_array($this->info) ? $this->info : ( $this->info = pathinfo($this->path) );
}
/**
* @return bool
*/
public function exists(){
return file_exists( $this->path );
}
/**
* @return bool
*/
public function writable(){
return $this->getWriteContext()->writable();
}
/**
* @return bool
*/
public function deletable(){
$parent = $this->getParent();
if( $parent && $parent->writable() ){
// sticky directory requires that either the file its parent is owned by effective user
if( $parent->mode() & 01000 ){
$writer = $this->getWriteContext();
if( $writer->isDirect() && ( $uid = Loco_compat_PosixExtension::getuid() ) ){
return $uid === $this->uid() || $uid === $parent->uid();
}
// else delete operation won't be done directly, so can't pre-empt sticky problems
// TODO is it worth comparing FTP username etc.. for ownership?
}
// defaulting to "deletable" based on fact that parent is writable.
return true;
}
return false;
}
/**
* Get owner uid
* @return int
*/
public function uid(){
return fileowner($this->path);
}
/**
* Get group gid
* @return int
*/
public function gid(){
return filegroup($this->path);
}
/**
* Check if file can't be overwitten when existant, nor created when non-existant
* This does not check permissions recursively as directory trees are not built implicitly
* @return bool
*/
public function locked(){
if( $this->exists() ){
return ! $this->writable();
}
if( $dir = $this->getParent() ){
return ! $dir->writable();
}
return true;
}
/**
* Check if full path can be built to non-existant file.
* @return bool
*/
public function creatable(){
$file = $this;
while( $file = $file->getParent() ){
if( $file->exists() ){
return $file->writable();
}
}
return false;
}
/**
* @return string
*/
public function dirname(){
$info = $this->pathinfo();
return $info['dirname'];
}
/**
* @return string
*/
public function basename(){
$info = $this->pathinfo();
return $info['basename'];
}
/**
* @return string
*/
public function filename(){
$info = $this->pathinfo();
return $info['filename'];
}
/**
* @return string
*/
public function extension(){
$info = $this->pathinfo();
return isset($info['extension']) ? $info['extension'] : '';
}
/**
* @return string
*/
public function getPath(){
return $this->path;
}
/**
* @return int
*/
public function modified(){
return filemtime( $this->path );
}
/**
* @return int
*/
public function size(){
return filesize( $this->path );
}
/**
* @return int
*/
public function mode(){
if( is_link($this->path) ){
$stat = lstat( $this->path );
$mode = $stat[2];
}
else {
$mode = fileperms($this->path);
}
return $mode;
}
/**
* Set file mode
* @return Loco_fs_File
*/
public function chmod( $mode, $recursive = false ){
$this->getWriteContext()->chmod( $mode, $recursive );
return $this->clearStat();
}
/**
* Clear stat cache if any file data has changed
* @return Loco_fs_File
*/
public function clearStat(){
$this->info = null;
// PHP 5.3.0 Added optional clear_realpath_cache and filename parameters.
if( version_compare( PHP_VERSION, '5.3.0', '>=' ) ){
clearstatcache( true, $this->path );
}
// else no choice but to drop entire stat cache
else {
clearstatcache();
}
return $this;
}
/**
* @return string
*/
public function __toString(){
return $this->getPath();
}
/**
* Check if passed path is equal to ours
* @param string
* @return bool
*/
public function equal( $path ){
return $this->path === (string) $path;
}
/**
* Normalize path for string comparison, resolves redundant dots and slashes.
* @param string path to prefix
* @return string
*/
public function normalize( $base = '' ){
if( $path = self::abs($base) ){
$base = $path;
}
if( $base !== $this->base ){
$path = $this->path;
if( '' === $path ){
$this->setPath($base);
}
else {
if( ! $this->rel || ! $base ){
$b = array();
}
else {
$b = self::explode( $base, array() );
}
$b = self::explode( $path, $b );
$this->setPath( implode('/',$b) );
}
$this->base = $base;
}
return $this->path;
}
/**
*
*/
private static function explode( $path, array $b ){
$a = explode( '/', $path );
foreach( $a as $i => $s ){
if( '' === $s ){
if( 0 !== $i ){
continue;
}
}
if( '.' === $s ){
continue;
}
if( '..' === $s ){
if( array_pop($b) ){
continue;
}
}
$b[] = $s;
}
return $b;
}
/**
* Get path relative to given location, unless path is already relative
* @return string
*/
public function getRelativePath( $base ){
$path = $this->normalize();
if( $abspath = self::abs($path) ){
// base may needs require normalizing
$file = new Loco_fs_File($base);
$base = $file->normalize();
$length = strlen($base);
// if we are below given base path, return ./relative
if( substr($path,0,$length) === $base ){
++$length;
if( isset($path{$length}) ){
return substr( $path, $length );
}
// else paths were idenitcal
return '';
}
// else attempt to find nearest common root
$i = 0;
$source = explode('/',$base);
$target = explode('/',$path);
while( isset($source[$i]) && isset($target[$i]) && $source[$i] === $target[$i] ){
$i++;
}
if( $i > 1 ){
$depth = count($source) - $i;
$build = array_merge( array_fill( 0, $depth, '..' ), array_slice( $target, $i ) );
$path = implode( '/', $build );
}
}
// else return unmodified
return $path;
}
/**
* @return bool
*/
public function isDirectory(){
if( file_exists($this->path) ){
return is_dir($this->path);
}
return ! $this->extension();
}
/**
* Load contents of file into a string
* @return string
*/
public function getContents(){
return file_get_contents( $this->path );
}
/**
* Check if path is under a theme directory
* @return bool
*/
public function underThemeDirectory(){
return Loco_fs_Locations::getThemes()->check( $this->path );
}
/**
* Check if path is under a plugin directory
* @return bool
*/
public function underPluginDirectory(){
return Loco_fs_Locations::getPlugins()->check( $this->path );
}
/**
* Check if path is under a global system directory
* @return bool
*/
public function underGlobalDirectory(){
return Loco_fs_Locations::getGlobal()->check( $this->path );
}
/**
* @return Loco_fs_Directory
*/
public function getParent(){
$path = $this->dirname();
if( '.' !== $path && $this->path !== $path ){
$dir = new Loco_fs_Directory( $path );
$dir->cloneWriteContext( $this->w );
return $dir;
}
}
/**
* Copy this file for real
* @throws Loco_error_Exception
* @return Loco_fs_File new file
*/
public function copy( $dest ){
$copy = clone $this;
$copy->path = $dest;
$copy->clearStat();
$this->getWriteContext()->copy( $copy );
return $copy;
}
/**
* Delete this file for real
* @throws Loco_error_Exception
* @return Loco_fs_File
*/
public function unlink(){
$recursive = $this->isDirectory();
$this->getWriteContext()->delete( $recursive );
return $this->clearStat();
}
/**
* Copy this object with an alternative file extension
* @return Loco_fs_File
*/
public function cloneExtension( $ext ){
$snip = strlen( $this->extension() );
$file = clone $this;
if( $snip ){
$file->path = substr_replace( $this->path, $ext, - $snip );
}
else {
$file->path .= '.'.$ext;
}
$file->info = null;
return $file;
}
/**
* Ensure full parent directory tree exists
* @return Loco_fs_Directory
*/
public function createParent(){
if( $dir = $this->getParent() ){
if( ! $dir->exists() ){
$dir->mkdir();
}
}
return $dir;
}
/**
* @return int bytes written to file
*/
public function putContents( $data ){
$this->getWriteContext()->putContents($data);
$this->clearStat();
return $this->size();
}
} fs/Directory.php 0000666 00000001762 15214173534 0007650 0 ustar 00 r = (bool) $bool;
return $this;
}
/**
* @return bool
*/
public function isRecursive(){
if( func_num_args() ){
throw new InvalidArgumentException('Did you mean to use setRecursive?');
}
return $this->r;
}
/**
* Create this directory for real.
*
* @throws Loco_error_WriteException
* @return Loco_fs_Directory
*/
public function mkdir(){
if( ! $this->exists() ){
$this->getWriteContext()->mkdir();
}
return $this;
}
}
fs/DummyFile.php 0000666 00000005512 15214173534 0007574 0 ustar 00 mtime = time();
}
/**
* {@inheritdoc}
*/
public function exists(){
return false;
}
/**
* {@inheritdoc}
*/
public function getContents(){
return $this->contents;
}
/**
* {@inheritdoc}
*/
public function size(){
return strlen($this->contents);
}
/**
* {@inheritdoc}
*/
public function putContents( $contents ){
$this->contents = (string) $contents;
return $this;
}
/**
* {@inheritdoc}
*/
public function modified(){
return $this->mtime;
}
/**
* Allow forcing of modified stamp for testing purposes
* @return Loco_fs_File
*/
public function touch( $modified ){
$this->mtime = (int) $modified;
return $this;
}
/**
* {@inheritdoc}
*/
public function mode(){
return $this->fmode;
}
/**
* {@inheritdoc}
*/
public function chmod( $mode, $recursive = false ){
$this->fmode = (int) $mode;
return $this;
}
/**
* TODO implement in parent
*/
public function chown( $uid = null, $gid = null ){
if( is_int($uid) ){
$this->uid = $uid;
}
if( is_int($gid) ){
$this->gid = $gid;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function copy( $dest ){
$copy = clone $this;
$copy->path = $dest;
return $copy;
}
/**
* {@inheritdoc}
*/
public function uid(){
return $this->uid;
}
/**
* {@inheritdoc}
*/
public function gid(){
return $this->gid;
}
/**
* {@inheritdoc}
* @codeCoverageIgnore
*/
public function writable(){
$mode = $this->mode();
// world writable
if( $mode & 02 ){
return true;
}
// group writable
if( ( $mode & 020 ) && $this->gid() === Loco_compat_PosixExtension::getgid() ){
return true;
}
// owner writable
if( ( $mode & 0200 ) && $this->uid() === Loco_compat_PosixExtension::getuid() ){
return true;
}
// else locked:
return false;
}
} fs/LocaleDirectory.php 0000666 00000005547 15214173534 0010775 0 ustar 00 normalize() );
// anything under Loco's protected directory is our location for custom overrides
$prefix = trailingslashit( loco_constant('LOCO_LANG_DIR') );
if( substr($path,0,strlen($prefix) ) === $prefix ){
return 'custom';
}
// standard subdirectories of WP_LANG_DIR are under WordPress auto-update control
$prefix = trailingslashit( loco_constant('WP_LANG_DIR') );
if( substr($path,0,strlen($prefix) ) === $prefix ){
if( $path === $prefix || $path === $prefix.'plugins/' || $path === $prefix.'themes/' ){
return 'wplang';
}
}
else {
// anything under a registered theme directory is bundled
$dirs = Loco_fs_Locations::getThemes();
if( $dirs->check($path) ){
return 'theme';
}
// anything under a registered plugin directory is bundled
$dirs = Loco_fs_Locations::getPlugins();
if( $dirs->check($path) ){
return 'plugin';
}
}
// anything else, which includes subdirectories of WP_LANG_DIR etc..
return 'other';
}
/**
* Get translated version of getTypeId
* @return string
*/
public function getTypeLabel( $id ){
switch( $id ){
case 'theme':
case 'plugin':
// Translators: Refers to bundled plugin or theme translation files - i.e. those supplied by the author
return _x('Author','File location','loco');
case 'wplang':
// Translators: Refers to system-installed translation files - i.e. those under WP_LANG_DIR
return _x('System','File location','loco');
case 'custom':
// Translators: Refers to translation files in Loco's custom/protected directory
return _x('Custom','File location','loco');
case 'other':
// Translators: Refers to translation files in an alternative location that isn't Author, System or Custom.
return _x('Other','File location','loco');
}
throw new InvalidArgumentException('Invalid location type: '.$id );
}
} fs/FileWriter.php 0000666 00000017333 15214173534 0007761 0 ustar 00 file = $file;
$this->connect( new WP_Filesystem_Direct(null) );
}
/**
* @return Loco_fs_FileWriter
*/
public function setFile( Loco_fs_File $file ){
$this->file = $file;
return $this;
}
/**
* Connect to alternative file system context
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function connect( WP_Filesystem_Base $fs, $disconnected = true ){
if( $disconnected && ! $fs->connect() ){
$errors = $fs->errors;
if( is_wp_error($errors) ){
foreach( $errors->get_error_messages() as $reason ){
Loco_error_AdminNotices::warn($reason);
}
}
throw new Loco_error_WriteException( __('Failed to connect to remote server','loco') );
}
$this->fs = $fs;
return $this;
}
/**
* Get mapped path for use in indirect file system manipulation
* @return string
*/
public function getPath(){
return $this->mapPath( $this->file->getPath() );
}
/**
* @internal
*/
private function mapPath( $path ){
/*/ restrict file extensions to Gettext files for additional layer of security
// disabled until configurable, too annoying when using safely (e.g. zip files)
if( ( $ext = $this->file->extension() ) && ! preg_match('/^(po|mo|pot)~?$/',$ext) ){
throw new Loco_error_WriteException('Unwriteable file extension: *.'.$ext.' disallowed');
}*/
// sanitize writeable locations
$remote = ! $this->isDirect();
$base = rtrim( loco_constant('WP_CONTENT_DIR'), '/' );
$snip = strlen($base);
if( substr( $path, 0, $snip ) !== $base ){
if( $remote ){
throw new Loco_error_WriteException('Remote path must be under WP_CONTENT_DIR');
}
/*/ Allowing direct file system access, because symlinks and also get_temp_dir()
else {
throw new Loco_error_WriteException('Direct path must be under WP_CONTENT_DIR');
}*/
}
// map virtual path for remote file system
if( $remote ){
$virt = $this->fs->wp_content_dir();
if( false === $virt ){
throw new Loco_error_WriteException('Failed to find WP_CONTENT_DIR via remote connection');
}
$virt = rtrim( $virt, '/' );
$path = substr_replace( $path, $virt, 0, $snip );
}
return $path;
}
/**
* Test if a direct (not remote) file system
* @return bool
*/
public function isDirect(){
return $this->fs instanceof WP_Filesystem_Direct;
}
/**
* @return bool
*/
public function writable(){
return ! $this->disabled() && $this->fs->is_writable( $this->getPath() );
}
/**
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function chmod( $mode, $recursive = false ){
$this->authorize();
if( ! $this->fs->chmod( $this->getPath(), $mode, $recursive ) ){
throw new Loco_error_WriteException( sprintf( __('Failed to chmod %s','loco'), $this->file->basename() ) );
}
return $this;
}
/**
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function copy( Loco_fs_File $copy ){
$this->authorize();
$source = $this->getPath();
$target = $this->mapPath( $copy->getPath() );
// bugs in WP file system "exists" methods mean forcing $overwrite, but checking reliably first
if( $copy->exists() ){
throw new Loco_error_WriteException( __('Refusing to copy over an existing file','loco') );
}
if( ! $this->fs->copy( $source, $target, true ) ){
throw new Loco_error_WriteException( sprintf( __('Failed to copy %s to %s','loco'), basename($source), basename($target) ) );
}
return $this;
}
/**
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function delete( $recursive = false ){
$this->authorize();
if( ! $this->fs->delete( $this->getPath(), $recursive ) ){
throw new Loco_error_WriteException( sprintf( __('Failed to delete %s','loco'), $this->file->basename() ) );
}
return $this;
}
/**
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function putContents( $data ){
$this->authorize();
$file = $this->file;
if( $file->isDirectory() ){
throw new Loco_error_WriteException( sprintf( __('"%s" is a directory, not a file','loco'), $file->basename() ) );
}
// avoid chmod of existing file
if( $file->exists() ){
$mode = $file->mode();
}
// may have bypassed definition of FS_CHMOD_FILE
else {
$mode = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644;
}
$path = $this->getPath();
if( ! $this->fs->put_contents( $path, $data, $mode ) ){
// provide useful reason for failure if possible
if( $file->exists() && ! $this->fs->is_writable($path) ){
throw new Loco_error_WriteException( __("Permission denied to update file",'loco') );
}
// else check directory exists in which to create a new file
else if( ( $dir = $file->getParent() ) && ! $dir->exists() ){
throw new Loco_error_WriteException( __("Parent directory doesn't exist",'loco') );
}
// else reason for failure is not established
throw new Loco_error_WriteException( __('Failed to save file','loco') );
}
return $this;
}
/**
* @return Loco_fs_FileWriter
* @throws Loco_error_WriteException
*/
public function mkdir(){
$this->authorize();
$fs = $this->fs;
// may have bypassed definition of FS_CHMOD_DIR
$mode = defined('FS_CHMOD_DIR') ? FS_CHMOD_DIR : 0755;
// find first ancestor that exists while building tree
$stack = array();
$here = $this->file;
/* @var $parent Loco_fs_Directory */
while( $parent = $here->getParent() ){
array_unshift( $stack, $this->mapPath( $here->getPath() ) );
if( $parent->exists() ){
// have existant directory, now build full path
foreach( $stack as $path ){
if( ! $fs->mkdir( $path, $mode ) ){
throw new Loco_error_WriteException( __('Failed to create directory','loco') );
}
}
return true;
}
$here = $parent;
}
throw new Loco_error_WriteException( __('Failed to build directory path','loco') );
}
/**
* Check whether write operations are permitted, or throw
* @throws Loco_error_WriteException
* @return Loco_fs_FileWriter
*/
public function authorize(){
if( $this->disabled() ){
throw new Loco_error_WriteException( __('File modification is disallowed by your WordPress config','loco') );
}
return $this;
}
/**
* Check if file system modification is banned
* @return bool
*/
public function disabled(){
return loco_constant('DISALLOW_FILE_MODS');
}
}
fs/Locations.php 0000666 00000005415 15214173534 0007636 0 ustar 00 $path ){
// path should be normalized absolute path and be compared only with others of the same
$path = Loco_fs_File::abs($path);
if( ! $path ){
throw new InvalidArgumentException('Location must be absolute path');
}
// path must have trailing slash, otherwise "/plugins/foobar" would match "/plugins/foo/"
$path = trailingslashit($path);
$cache[$path] = strlen($path);
}
parent::__construct( $cache );
}
/**
* Check if a given path begins with any of the registered ones
* @param string absolute path
* @return bool whether path matched
*/
public function check( $path ){
$path = Loco_fs_File::abs($path);
foreach( $this as $prefix => $length ){
if( substr($path,0,$length) === $prefix ){
return true;
}
}
return false;
}
} fs/FileListInterface.php 0000666 00000000203 15214173534 0011225 0 ustar 00 buffer[$domain][$msgid] = null;
return $msgstr;
}
/**
* `gettext_with_context` filter callback
*/
public function filter_gettext_with_context( $msgstr, $msgid, $msgctxt, $domain ){
$this->buffer[$domain][$msgctxt."\x04".$msgid] = null;
return $msgstr;
}
/**
* `ngettext` filter callback
*/
public function filter_ngettext( $msgstr, $msgid, $msgid_plural, $number, $domain ){
$this->buffer[$domain][$msgid] = null;
return $msgstr;
}
/**
* `ngettext_with_context` filter callback
*/
function filter_ngettext_with_context( $msgstr, $msgid, $msgid_plural, $number, $msgctxt, $domain ){
$this->buffer[$domain][$msgctxt."\x04".$msgid] = null;
return $msgstr;
}
/**
* Export all captured translations in a raw form and reset buffer
* @param string the specific domain listened for
* @return array
*/
public function flush( $domain ){
$export = array();
if( isset($this->buffer[$domain]) ){
// what we captures was just a unique namespace
$captured = $this->buffer[$domain];
unset($this->buffer[$domain]);
// process raw data for all that actually exist
// this survives on WordPress internals not changing :-/
$loaded = get_translations_for_domain($domain);
if( $loaded instanceof Translations && is_array($loaded->entries) ){
$entries = array_intersect_key( $loaded->entries, $captured );
/* @var $entry Translation_Entry */
foreach( $entries as $key => $entry ){
$export[$key] = $entry->translations;
}
}
}
return $export;
}
}
hooks/LoadHelper.php 0000666 00000007017 15214173534 0010435 0 ustar 00 context = array( 'themes', $domain, $locale );
unset( $this->lock[$domain] );
return $locale;
}
/**
* `plugin_locale` filter callback.
* Signals the beginning of a "load_plugin_textdomain" process
*/
public function filter_plugin_locale( $locale, $domain = '' ){
$this->context = array( 'plugins', $domain, $locale );
unset( $this->lock[$domain] );
return $locale;
}
/**
* `unload_textdomain` action callback.
* Lets us release lock so that custom file may be loaded again (hopefully for another locale)
*/
public function on_unload_textdomain( $domain ){
unset( $this->lock[$domain] );
}
/**
* `load_textdomain` action callback.
* Lets us load our custom translations before WordPress loads what it was going to anyway.
* We're deliberately not stopping WordPress loading $mopath, if it exists it will be merged on top of our custom strings.
* @return void
*/
public function on_load_textdomain( $domain, $mopath ){
$key = '';
// domains may be split into multiple files
$name = pathinfo( $mopath, PATHINFO_FILENAME );
if( $lpos = strrpos( $name, '-') ){
$slug = substr( $name, 0, $lpos );
if( $slug !== $domain ){
$key = $slug;
}
}
// avoid recursion when we've already handled this domain/slug
if( isset($this->lock[$domain][$key]) ){
return;
}
// language roots
$wp_lang_dir = trailingslashit( loco_constant('WP_LANG_DIR') );
$lc_lang_dir = trailingslashit( loco_constant('LOCO_LANG_DIR') );
// if context is set, then a theme or plugin initialized the loading process properly
if( is_array($this->context) ){
list( $subdir, $_domain, $locale ) = $this->context;
$this->context = null;
// It shouldn't be possible to catch a different domain after setting context, but we'd better bail just in case
if( $_domain !== $domain ){
return;
}
$mopath = $lc_lang_dir.$subdir.'/'.$domain.'-'.$locale.'.mo';
}
// else load_textdomain must have been called directly to bypass locale filters
else {
$snip = strlen($wp_lang_dir);
// direct file loads must be under WP_LANG_DIR if we are to map them
if( substr( dirname($mopath).'/', 0, $snip ) === $wp_lang_dir ){
$mopath = substr_replace( $mopath, $lc_lang_dir, 0, $snip );
}
// else no way to map files from WP_LANG_DIR to LOCO_LANG_DIR
else {
return;
}
}
// Load our custom translations avoiding recursion back into this hook
$this->lock[$domain][$key] = true;
load_textdomain( $domain, $mopath );
}
} hooks/Hookable.php 0000666 00000004541 15214173534 0010141 0 ustar 00 getMethods( ReflectionMethod::IS_PUBLIC ) as $method ){
$func = $method->name;
// support filter_{filter_hook} methods
if( 0 === strpos($func,'filter_' ) ) {
$hook = substr( $func, 7 );
}
// support on_{action_hook} methods
else if( 0 === strpos($func,'on_' ) ){
$hook = substr( $func, 3 );
}
else {
continue;
}
// this goes to 11 so we run after system defaults
$priority = 11;
// support @priority tag in comment block (uncomment if needed)
/*if( ( $docblock = $method->getDocComment() ) && ( $offset = strpos($docblock,'@priority ') ) ){
preg_match( '/^\d+/', substr($docblock,$offset+10), $r ) and
$priority = (int) $r[0];
}*/
// call add_action or add_filter with required arguments and hook is registered
// add_action actually calls add_filter, although unsure how long that's been the case.
$num_args = $method->getNumberOfParameters();
add_filter( $hook, array( $this, $func ), $priority, $num_args );
// register hook for destruction so object can be removed from memory
$reg[] = array( $hook, $func, $priority );
}
$this->hooks = $reg;
}
/**
* Deregister active hooks.
* We can't use __destruct because instances persist in WordPress hook registry
*/
public function unhook(){
if( is_array($this->hooks) ){
foreach( $this->hooks as $r ){
remove_filter( $r[0], array($this,$r[1]), $r[2] );
}
}
$this->hooks = null;
}
} hooks/AdminHooks.php 0000666 00000007000 15214173534 0010442 0 ustar 00 migrate() ){
// would trigger upgrade handlers here in future releases
}
}
// we'll need our own translations on all admin pages not just our own, for menu items etc..
self::init_l10n();
}
}
/**
* "admin_init" callback.
* Unhooks failure notice that would fire if this hook was not successful
*/
public function on_admin_init(){
remove_action( 'admin_notices', array('Loco_compat_Failure','print_hook_failure') );
}
/**
* plugin_action_links action callback
*/
public function on_plugin_action_links( $links, $plugin = '' ){
try {
if( $plugin && current_user_can('loco_admin') && Loco_package_Plugin::get_plugin($plugin) ){
// ok to add "translate" link into meta row
$href = Loco_mvc_AdminRouter::generate('plugin-view', array( 'bundle' => $plugin) );
$links[] = ''.esc_html__('Translate','loco').'';
}
}
catch( Exception $e ){
// $links[] = esc_html( 'Debug: '.$e->getMessage() );
}
return $links;
}
/**
* deactivate_plugin action callback
*
public function on_deactivate_plugin( $plugin, $network = false ){
if( loco_plugin_self() === $plugin ){
// TODO flush all our transient cache entries
// "DELETE FROM ___ WHERE `option_name` LIKE '_transient_loco_%' OR `option_name` LIKE '_transient_timeout_loco_%'";
}
}*/
/*public function filter_all( $hook ){
error_log( $hook, 0 );
}*/
}
compat/JsonSerializable.php 0000666 00000000766 15214173534 0012022 0 ustar 00 jsonSerialize() )
*/
if( ! interface_exists('JsonSerializable') ){
interface JsonSerializable {
public function jsonSerialize();
}
}
// @codeCoverageIgnoreEnd
/**
* Redundant interface so this file will autoload when JsonSerializable is referenced
*/
interface Loco_compat_JsonSerializable extends JsonSerializable {
} compat/MbstringExtension.php 0000666 00000003020 15214173534 0012226 0 ustar 00
Error: '.implode('. ',$texts).'
';
}
}
compat/PosixExtension.php 0000666 00000004557 15214173535 0011564 0 ustar 00 '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;
}
} package/Theme.php 0000666 00000007046 15214173535 0007733 0 ustar 00 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;
}
} package/Project.php 0000666 00000047545 15214173535 0010307 0 ustar 00 .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 -en_US.
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;
}
} package/Plugin.php 0000666 00000017537 15214173535 0010135 0 ustar 00 '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;
}
} package/Inverter.php 0000666 00000015242 15214173535 0010464 0 ustar 00 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 \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 \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' \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' \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' \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;
}
} package/Listener.php 0000666 00000027506 15214173535 0010461 0 ustar 00 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 '