2017-03-16 16:59:53 +01:00
< ? php
/**
* Loads files for admin and frontend .
*
* @ author Time . ly Network Inc .
* @ since 2.0
*
* @ package AI1EC
* @ subpackage AI1EC . Theme
*/
class Ai1ec_Theme_Loader {
2017-11-09 17:36:04 +01:00
/**
* @ const string Name of option which forces theme clean - up if set to true .
*/
2017-03-16 16:59:53 +01:00
const OPTION_FORCE_CLEAN = 'ai1ec_clean_twig_cache' ;
2017-11-09 17:36:04 +01:00
/**
* @ 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' );
}
2017-03-16 16:59:53 +01:00
}