all-in-one-event-calendar/lib/theme/loader.php
2017-11-09 17:36:04 +01:00

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' );
}
}