656 lines
22 KiB
PHP
656 lines
22 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Loads files for admin and frontend.
|
|
*
|
|
* @author Time.ly Network Inc.
|
|
* @since 2.0
|
|
*
|
|
* @package AI1EC
|
|
* @subpackage AI1EC.Theme
|
|
*/
|
|
class Ai1ec_Theme_Loader {
|
|
|
|
/**
|
|
* @const string Name of option which forces theme clean-up if set to true.
|
|
*/
|
|
const OPTION_FORCE_CLEAN = 'ai1ec_clean_twig_cache';
|
|
|
|
/**
|
|
* @const string Prefix for theme arguments filter name.
|
|
*/
|
|
const ARGS_FILTER_PREFIX = 'ai1ec_theme_args_';
|
|
|
|
/**
|
|
* @var array contains the admin and theme paths.
|
|
*/
|
|
protected $_paths = array(
|
|
'admin' => array( AI1EC_ADMIN_PATH => AI1EC_ADMIN_URL ),
|
|
'theme' => array(),
|
|
);
|
|
|
|
/**
|
|
* @var Ai1ec_Registry_Object The registry Object.
|
|
*/
|
|
protected $_registry;
|
|
|
|
/**
|
|
* @var array Array of Twig environments.
|
|
*/
|
|
protected $_twig = array();
|
|
|
|
/**
|
|
* @var bool Whether this theme uses .php templates instead of .twig
|
|
*/
|
|
protected $_legacy_theme = false;
|
|
|
|
/**
|
|
* @var bool Whether this theme is a child of the default theme
|
|
*/
|
|
protected $_child_theme = false;
|
|
|
|
/**
|
|
* @var bool Whether this theme is a core theme
|
|
*/
|
|
protected $_core_theme = false;
|
|
|
|
/**
|
|
* @return boolean
|
|
*/
|
|
public function is_legacy_theme() {
|
|
return $this->_legacy_theme;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param $registry Ai1ec_Registry_Object
|
|
* The registry Object.
|
|
*/
|
|
public function __construct(
|
|
Ai1ec_Registry_Object $registry
|
|
) {
|
|
$this->_registry = $registry;
|
|
$option = $this->_registry->get( 'model.option' );
|
|
$theme = $option->get( 'ai1ec_current_theme' );
|
|
$this->_legacy_theme = (bool)$theme['legacy'];
|
|
|
|
// Find out if this is a core theme.
|
|
$core_themes = explode( ',', AI1EC_CORE_THEMES );
|
|
$this->_core_theme = in_array( $theme['stylesheet'], $core_themes );
|
|
|
|
// Default theme's path is always the last in the list of paths to check,
|
|
// so add it first (path list is a stack).
|
|
$this->add_path_theme(
|
|
AI1EC_DEFAULT_THEME_PATH . DIRECTORY_SEPARATOR,
|
|
AI1EC_THEMES_URL . '/' . AI1EC_DEFAULT_THEME_NAME . '/'
|
|
);
|
|
|
|
// If using a child theme, set flag and push its path to top of stack.
|
|
if ( AI1EC_DEFAULT_THEME_NAME !== $theme['stylesheet'] ) {
|
|
$this->_child_theme = true;
|
|
$this->add_path_theme(
|
|
$theme['theme_dir'] . DIRECTORY_SEPARATOR,
|
|
$theme['theme_url'] . '/'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs the filter for the specified filename just once
|
|
*
|
|
* @param array $args
|
|
* @param string $filename
|
|
* @param boole $is_admin
|
|
*
|
|
* @return array
|
|
*/
|
|
public function apply_filters_to_args( array $args, $filename, $is_admin ) {
|
|
return apply_filters(
|
|
self::ARGS_FILTER_PREFIX . $filename,
|
|
$args,
|
|
$is_admin
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Adds file search path to list. If an extension is adding this path, and
|
|
* this is a custom child theme, inserts its path at the second index of the
|
|
* list. Else pushes it onto the top of the stack.
|
|
*
|
|
* @param string $target Name of path purpose, i.e. 'admin' or 'theme'.
|
|
* @param string $path Absolute path to the directory to search.
|
|
* @param string $url URL to the directory represented by $path.
|
|
* @param string $is_extension Whether an extension is adding this page.
|
|
*
|
|
* @return bool Success.
|
|
*/
|
|
public function add_path( $target, $path, $url, $is_extension = false ) {
|
|
if ( ! isset( $this->_paths[$target] ) ) {
|
|
// Invalid target.
|
|
return false;
|
|
}
|
|
|
|
$path = apply_filters( 'ai1ec_theme_loader_add_path_file', $path, $url, $target, $is_extension );
|
|
$url = apply_filters( 'ai1ec_theme_loader_add_path_http', $url, $path, $target, $is_extension );
|
|
// New element to insert into associative array.
|
|
$new = array( $path => $url );
|
|
|
|
if (
|
|
true === $is_extension &&
|
|
true === $this->_child_theme &&
|
|
false === $this->_core_theme
|
|
) {
|
|
// Special case: extract first element into $head and insert $new after.
|
|
$head = array_splice( $this->_paths[$target], 0, 1 );
|
|
} else {
|
|
// Normal case: $new gets pushed to the top of the array.
|
|
$head = array();
|
|
}
|
|
|
|
$this->_paths[$target] = $head + $new + $this->_paths[$target];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add admin files search path.
|
|
*
|
|
* @param string $path Path to admin template files.
|
|
* @param string $url URL to the directory represented by $path.
|
|
*
|
|
* @return bool Success.
|
|
*/
|
|
public function add_path_admin( $path, $url ) {
|
|
return $this->add_path( 'admin', $path, $url );
|
|
}
|
|
|
|
/**
|
|
* Add theme files search path.
|
|
*
|
|
* @param string $path Path to theme template files.
|
|
* @param string $url URL to the directory represented by $path.
|
|
* @param string $is_extension Whether an extension is adding this path.
|
|
*
|
|
* @return bool Success.
|
|
*/
|
|
public function add_path_theme( $path, $url, $is_extension = false ) {
|
|
return $this->add_path( 'theme', $path, $url, $is_extension );
|
|
}
|
|
|
|
/**
|
|
* Extension registration hook to automatically add file paths.
|
|
*
|
|
* NOTICE: extensions are expected to exactly replicate Core directories
|
|
* structure. If different extension is to be developed at some point in
|
|
* time - this will have to be changed.
|
|
*
|
|
* @param string $path Absolute path to extension's directory.
|
|
* @param string $url URL to directory represented by $path.
|
|
*
|
|
* @return Ai1ec_Theme_Loader Instance of self for chaining.
|
|
*/
|
|
public function register_extension( $path, $url ) {
|
|
$D = DIRECTORY_SEPARATOR; // For readability.
|
|
|
|
// Add extension's admin path.
|
|
$this->add_path_admin(
|
|
$path . $D .'public' . $D . 'admin' . $D,
|
|
$url . '/public/admin/'
|
|
);
|
|
|
|
// Add extension's theme path(s).
|
|
$option = $this->_registry->get( 'model.option' );
|
|
$theme = $option->get( 'ai1ec_current_theme' );
|
|
|
|
// Default theme's path is always later in the list of paths to check,
|
|
// so add it first (path list is a stack).
|
|
$this->add_path_theme(
|
|
$path . $D . 'public' . $D . AI1EC_THEME_FOLDER . $D .
|
|
AI1EC_DEFAULT_THEME_NAME . $D,
|
|
$url . '/public/' . AI1EC_THEME_FOLDER . '/' . AI1EC_DEFAULT_THEME_NAME .
|
|
'/',
|
|
true
|
|
);
|
|
|
|
// If using a core child theme, set flag and push its path to top of stack.
|
|
if ( true === $this->_child_theme && true === $this->_core_theme ) {
|
|
$this->add_path_theme(
|
|
$path . $D . 'public' . $D . AI1EC_THEME_FOLDER . $D .
|
|
$theme['stylesheet'] . $D,
|
|
$url . '/public/' . AI1EC_THEME_FOLDER . '/' . $theme['stylesheet'] .
|
|
'/',
|
|
true
|
|
);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the requested file from the filesystem.
|
|
*
|
|
* Get the requested file from the filesystem. The file is already parsed.
|
|
*
|
|
* @param string $filename Name of file to load.
|
|
* @param array $args Map of variables to use in file.
|
|
* @param bool $is_admin Set to true for admin-side views.
|
|
* @param bool $throw_exception Set to true to throw exceptions on error.
|
|
* @param array $paths For PHP & Twig files only: list of paths to use instead of default.
|
|
*
|
|
* @throws Ai1ec_Exception If File is not found or not possible to handle.
|
|
*
|
|
* @return Ai1ec_File_Abstract An instance of a file object with content parsed.
|
|
*/
|
|
public function get_file(
|
|
$filename,
|
|
$args = array(),
|
|
$is_admin = false,
|
|
$throw_exception = true,
|
|
array $paths = null
|
|
) {
|
|
$dot_position = strrpos( $filename, '.' ) + 1;
|
|
$ext = substr( $filename, $dot_position );
|
|
$file = false;
|
|
|
|
switch ( $ext ) {
|
|
case 'less':
|
|
case 'css':
|
|
$filename_base = substr( $filename, 0, $dot_position - 1);
|
|
$file = $this->_registry->get(
|
|
'theme.file.less',
|
|
$filename_base,
|
|
array_keys( $this->_paths['theme'] ) // Values (URLs) not used for CSS
|
|
);
|
|
break;
|
|
|
|
case 'png':
|
|
case 'gif':
|
|
case 'jpg':
|
|
$paths = $is_admin ? $this->_paths['admin'] : $this->_paths['theme'];
|
|
$file = $this->_registry->get(
|
|
'theme.file.image',
|
|
$filename,
|
|
$paths // Paths => URLs needed for images
|
|
);
|
|
break;
|
|
|
|
case 'php':
|
|
$args = apply_filters(
|
|
self::ARGS_FILTER_PREFIX . $filename,
|
|
$args,
|
|
$is_admin
|
|
);
|
|
if ( null === $paths ) {
|
|
$paths = $is_admin ? $this->_paths['admin'] : $this->_paths['theme'];
|
|
$paths = array_keys( $paths ); // Values (URLs) not used for PHP
|
|
}
|
|
$args['is_legacy_theme'] = $this->_legacy_theme;
|
|
$file = $this->_registry->get(
|
|
'theme.file.php',
|
|
$filename,
|
|
$paths,
|
|
$args
|
|
);
|
|
break;
|
|
|
|
case 'twig':
|
|
$args = apply_filters(
|
|
self::ARGS_FILTER_PREFIX . $filename,
|
|
$args,
|
|
$is_admin
|
|
);
|
|
|
|
if ( null === $paths ) {
|
|
$paths = $is_admin ? $this->_paths['admin'] : $this->_paths['theme'];
|
|
$paths = array_keys( $paths ); // Values (URLs) not used for Twig
|
|
}
|
|
if ( true === $this->_legacy_theme && ! $is_admin ) {
|
|
$filename = substr( $filename, 0, $dot_position - 1);
|
|
$file = $this->_get_legacy_file(
|
|
$filename,
|
|
$args,
|
|
$paths
|
|
);
|
|
} else {
|
|
$file = $this->_registry->get(
|
|
'theme.file.twig',
|
|
$filename,
|
|
$args,
|
|
$this->_get_twig_instance( $paths, $is_admin )
|
|
);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Ai1ec_Exception(
|
|
sprintf(
|
|
Ai1ec_I18n::__( "We couldn't find a suitable loader for filename with extension '%s'" ),
|
|
$ext
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
|
|
// here file is a concrete class otherwise the exception is thrown
|
|
if ( ! $file->process_file() && true === $throw_exception ) {
|
|
throw new Ai1ec_Exception(
|
|
'The specified file "' . $filename . '" doesn\'t exist.'
|
|
);
|
|
}
|
|
return $file;
|
|
}
|
|
|
|
/**
|
|
* Reuturns loader paths.
|
|
*
|
|
* @return array Loader paths.
|
|
*/
|
|
public function get_paths() {
|
|
return $this->_paths;
|
|
}
|
|
|
|
/**
|
|
* Tries to load a PHP file from the theme. If not present, it falls back to
|
|
* Twig.
|
|
*
|
|
* @param string $filename Filename to locate
|
|
* @param array $args Args to pass to template
|
|
* @param array $paths Array of paths to search
|
|
*
|
|
* @return Ai1ec_File_Abstract
|
|
*/
|
|
protected function _get_legacy_file( $filename, array $args, array $paths ) {
|
|
$php_file = $filename . '.php';
|
|
$php_file = $this->get_file( $php_file, $args, false, false, $paths );
|
|
|
|
if ( false === $php_file->process_file() ) {
|
|
$twig_file = $this->_registry->get(
|
|
'theme.file.twig',
|
|
$filename . '.twig',
|
|
$args,
|
|
$this->_get_twig_instance( $paths, false )
|
|
);
|
|
|
|
// here file is a concrete class otherwise the exception is thrown
|
|
if ( ! $twig_file->process_file() ) {
|
|
throw new Ai1ec_Exception(
|
|
'The specified file "' . $filename . '" doesn\'t exist.'
|
|
);
|
|
}
|
|
return $twig_file;
|
|
}
|
|
return $php_file;
|
|
}
|
|
|
|
/**
|
|
* Get Twig instance.
|
|
*
|
|
* @param bool $is_admin Set to true for admin views.
|
|
* @param bool $refresh Set to true to get fresh instance.
|
|
*
|
|
* @return Twig_Environment Configured Twig instance.
|
|
*/
|
|
public function get_twig_instance( $is_admin = false, $refresh = false ) {
|
|
if ( $refresh ) {
|
|
unset( $this->_twig );
|
|
}
|
|
$paths = $is_admin ? $this->_paths['admin'] : $this->_paths['theme'];
|
|
$paths = array_keys( $paths ); // Values (URLs) not used for Twig
|
|
return $this->_get_twig_instance( $paths, $is_admin );
|
|
}
|
|
|
|
/**
|
|
* Get cache dir for Twig.
|
|
*
|
|
* @param bool $rescan Set to true to force rescan
|
|
*
|
|
* @return string|bool Cache directory or false
|
|
*/
|
|
public function get_cache_dir( $rescan = false ) {
|
|
$settings = $this->_registry->get( 'model.settings' );
|
|
$ai1ec_twig_cache = $settings->get( 'twig_cache' );
|
|
if (
|
|
! empty( $ai1ec_twig_cache ) &&
|
|
false === $rescan
|
|
) {
|
|
return ( AI1EC_CACHE_UNAVAILABLE === $ai1ec_twig_cache )
|
|
? false
|
|
: $ai1ec_twig_cache;
|
|
}
|
|
$path = false;
|
|
$scan_dirs = array( AI1EC_TWIG_CACHE_PATH );
|
|
if ( apply_filters( 'ai1ec_check_static_dir', true ) ) {
|
|
$filesystem = $this->_registry->get( 'filesystem.checker' );
|
|
$upload_folder = $filesystem->get_ai1ec_static_dir_if_available();
|
|
if ( '' !== $upload_folder ) {
|
|
$scan_dirs[] = $upload_folder;
|
|
}
|
|
}
|
|
foreach ( $scan_dirs as $dir ) {
|
|
if ( $this->_is_dir_writable( $dir ) ) {
|
|
$path = $dir;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$settings->set(
|
|
'twig_cache',
|
|
false === $path ? AI1EC_CACHE_UNAVAILABLE : $path
|
|
);
|
|
if ( false === $path ) {
|
|
/* @TODO: move this to Settings -> Advanced -> Cache and provide a nice message */
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* After upgrade clean cache if it's not default.
|
|
*
|
|
* @return void Method doesn't return
|
|
*/
|
|
public function clean_cache_on_upgrade() {
|
|
if ( ! apply_filters( 'ai1ec_clean_cache_on_upgrade', true ) ) {
|
|
return;
|
|
}
|
|
$model_option = $this->_registry->get( 'model.option' );
|
|
if ( $model_option->get( self::OPTION_FORCE_CLEAN, false ) ) {
|
|
$model_option->set( self::OPTION_FORCE_CLEAN, false );
|
|
$cache = realpath( $this->get_cache_dir() );
|
|
if ( 0 === strcmp( $cache, realpath( AI1EC_TWIG_CACHE_PATH ) ) ) {
|
|
return;
|
|
}
|
|
if (
|
|
! $this->_registry->get(
|
|
'theme.compiler'
|
|
)->clean_and_check_dir( $cache )
|
|
) {
|
|
$this->_registry->get( 'twig.cache' )->set_unavailable( $cache );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method whould be in a factory called by the object registry.
|
|
* I leave it here for reference.
|
|
*
|
|
* @param array $paths Array of paths to search
|
|
* @param bool $is_admin whether to use the admin or not admin Twig instance
|
|
*
|
|
* @return Twig_Environment
|
|
*/
|
|
protected function _get_twig_instance( array $paths, $is_admin ) {
|
|
$instance = $is_admin ? 'admin' : 'front';
|
|
if ( ! isset( $this->_twig[$instance] ) ) {
|
|
|
|
// Set up Twig environment.
|
|
$loader_path = array();
|
|
|
|
foreach ( $paths as $path ) {
|
|
if ( is_dir( $path . 'twig' . DIRECTORY_SEPARATOR ) ) {
|
|
$loader_path[] = $path . 'twig' . DIRECTORY_SEPARATOR;
|
|
}
|
|
}
|
|
|
|
$loader = new Ai1ec_Twig_Loader_Filesystem( $loader_path );
|
|
unset( $loader_path );
|
|
// TODO: Add cache support.
|
|
$environment = array(
|
|
'cache' => $this->get_cache_dir(),
|
|
'optimizations' => -1, // all
|
|
'auto_reload' => true,
|
|
);
|
|
if ( AI1EC_DEBUG ) {
|
|
$environment += array(
|
|
'debug' => true, // produce node structure
|
|
);
|
|
// auto_reload never worked well
|
|
$environment['cache'] = false;
|
|
unset( $environment['auto_reload'] );
|
|
}
|
|
$environment = apply_filters(
|
|
'ai1ec_twig_environment',
|
|
$environment
|
|
);
|
|
|
|
$ai1ec_twig_environment = new Ai1ec_Twig_Environment(
|
|
$loader,
|
|
$environment
|
|
);
|
|
$ai1ec_twig_environment->set_registry( $this->_registry );
|
|
|
|
$this->_twig[$instance] = $ai1ec_twig_environment;
|
|
if ( apply_filters( 'ai1ec_twig_add_debug', AI1EC_DEBUG ) ) {
|
|
$this->_twig[$instance]->addExtension( new Twig_Extension_Debug() );
|
|
}
|
|
|
|
$extension = $this->_registry->get( 'twig.ai1ec-extension' );
|
|
$extension->set_registry( $this->_registry );
|
|
$this->_twig[$instance]->addExtension( $extension );
|
|
}
|
|
return $this->_twig[$instance];
|
|
}
|
|
|
|
/**
|
|
* Called during 'after_setup_theme' action. Runs theme's special
|
|
* functions.php file, if present.
|
|
*/
|
|
public function execute_theme_functions() {
|
|
$option = $this->_registry->get( 'model.option' );
|
|
$theme = $option->get( 'ai1ec_current_theme' );
|
|
$functions = $theme['theme_dir'] . DIRECTORY_SEPARATOR . 'functions.php';
|
|
|
|
if ( file_exists( $functions ) ) {
|
|
include( $functions );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safe checking for directory writeability.
|
|
*
|
|
* @param string $dir Path of likely directory.
|
|
*
|
|
* @return bool Writeability.
|
|
*/
|
|
private function _is_dir_writable( $dir ) {
|
|
$stack = array(
|
|
dirname( dirname( $dir ) ),
|
|
dirname( $dir ),
|
|
$dir,
|
|
);
|
|
foreach ( $stack as $element ) {
|
|
if ( is_dir( $element ) ) {
|
|
continue;
|
|
}
|
|
if ( ! is_writable( dirname( $element ) ) ) {
|
|
return false;
|
|
}
|
|
if ( ! mkdir( $dir, 0755, true ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Switch to the given calendar theme.
|
|
*
|
|
* @param array $theme The theme's settings array
|
|
* @param bool $delete_variables If true, deletes user variables from DB.
|
|
* Else replaces them with config file.
|
|
*/
|
|
public function switch_theme( array $theme, $delete_variables = true ) {
|
|
/* @var $option Ai1ec_Option */
|
|
$option = $this->_registry->get( 'model.option' );
|
|
$option->set(
|
|
'ai1ec_current_theme',
|
|
$theme
|
|
);
|
|
$option->delete( 'ai1ec_fer_checked' );
|
|
$lessphp = $this->_registry->get( 'less.lessphp' );
|
|
// If requested, delete theme variables from DB.
|
|
if ( $delete_variables ) {
|
|
$option->delete( Ai1ec_Less_Lessphp::DB_KEY_FOR_LESS_VARIABLES );
|
|
}
|
|
// Else replace them with those loaded from config file.
|
|
else {
|
|
$option->set(
|
|
Ai1ec_Less_Lessphp::DB_KEY_FOR_LESS_VARIABLES,
|
|
$lessphp->get_less_variable_data_from_config_file()
|
|
);
|
|
}
|
|
// Recompile CSS for new theme.
|
|
$css_controller = $this->_registry->get( 'css.frontend' );
|
|
$css_controller->invalidate_cache( null, false );
|
|
}
|
|
|
|
/**
|
|
* Switches to default Vortex theme.
|
|
*
|
|
* @param bool $silent Whether notify admin or not.
|
|
*
|
|
* @return void Method does not return.
|
|
*/
|
|
public function switch_to_vortex( $silent = false ) {
|
|
$current_theme = $this->get_current_theme();
|
|
if (
|
|
isset( $current_theme['stylesheet'] ) &&
|
|
'vortex' === $current_theme['stylesheet']
|
|
) {
|
|
return $current_theme;
|
|
}
|
|
$root = AI1EC_PATH . DIRECTORY_SEPARATOR . 'public' .
|
|
DIRECTORY_SEPARATOR . AI1EC_THEME_FOLDER;
|
|
$theme = array(
|
|
'theme_root' => $root,
|
|
'theme_dir' => $root . DIRECTORY_SEPARATOR . 'vortex',
|
|
'theme_url' => AI1EC_URL . '/public/' . AI1EC_THEME_FOLDER . '/vortex',
|
|
'stylesheet' => 'vortex',
|
|
'legacy' => false
|
|
);
|
|
$this->switch_theme( $theme );
|
|
if ( ! $silent ) {
|
|
$this->_registry->get( 'notification.admin' )->store(
|
|
Ai1ec_I18n::__(
|
|
"Your calendar theme has been switched to Vortex due to a rendering problem. For more information, please enable debug mode by adding this line to your WordPress <code>wp-config.php</code> file:<pre>define( 'AI1EC_DEBUG', true );</pre>"
|
|
),
|
|
'error',
|
|
0,
|
|
array( Ai1ec_Notification_Admin::RCPT_ADMIN ),
|
|
true
|
|
);
|
|
}
|
|
return $theme;
|
|
}
|
|
|
|
/**
|
|
* Returns current calendar theme.
|
|
*
|
|
* @return mixed Theme array or null.
|
|
*
|
|
* @throws Ai1ec_Bootstrap_Exception
|
|
*/
|
|
public function get_current_theme() {
|
|
return $this->_registry->get(
|
|
'model.option'
|
|
)->get( 'ai1ec_current_theme' );
|
|
}
|
|
|
|
}
|