diff --git a/api/RestfulService.php b/api/RestfulService.php index dea9de7da..e3f19217d 100644 --- a/api/RestfulService.php +++ b/api/RestfulService.php @@ -121,68 +121,9 @@ class RestfulService extends ViewableData { $response = unserialize($store); } else { - $ch = curl_init(); - $timeout = 5; - $useragent = "SilverStripe/" . SapphireInfo::Version(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_USERAGENT, $useragent); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - - // Add headers - if($this->customHeaders) { - $headers = array_merge((array)$this->customHeaders, (array)$headers); - } - - if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - // Add authentication - if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword"); - - // Add fields to POST and PUT requests - if($method == 'POST') { - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - } - else if($method == 'PUT') { - $put = fopen("php://temp", 'r+'); - fwrite($put, $data); - fseek($put, 0); - - curl_setopt($ch, CURLOPT_PUT, 1); - curl_setopt($ch, CURLOPT_INFILE, $put); - curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data)); - } + $response = $this->curlRequest($url, $method, $data, $headers, $curlOptions); - // Apply proxy settings - if(is_array($this->proxy)) { - curl_setopt_array($ch, $this->proxy); - } - - // Set any custom options passed to the request() function - curl_setopt_array($ch, $curlOptions); - - // Run request - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $responseBody = curl_exec($ch); - $curlError = curl_error($ch); - - // Problem verifying the server SSL certificate; just ignore it as it's not mandatory - if(strpos($curlError,'14090086') !== false) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $responseBody = curl_exec($ch); - $curlError = curl_error($ch); - } - - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if($curlError !== '' || $statusCode == 0) $statusCode = 500; - - $response = new RestfulService_Response($responseBody, $statusCode); - curl_close($ch); - - if($curlError === '' && !$response->isError()) { + if(!$response->isError()) { // Serialise response object and write to cache $store = serialize($response); file_put_contents($cache_path, $store); @@ -204,7 +145,83 @@ class RestfulService extends ViewableData { return $response; } - + + /** + * Actually performs a remote service request using curl. This is used by + * {@link RestfulService::request()}. + * + * @param string $url + * @param string $method + * @param array $data + * @param array $headers + * @param array $curlOptions + * @return RestfulService_Response + */ + public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) { + $ch = curl_init(); + $timeout = 5; + $useragent = 'SilverStripe/' . SapphireInfo::Version(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); + if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + + // Add headers + if($this->customHeaders) { + $headers = array_merge((array)$this->customHeaders, (array)$headers); + } + + if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Add authentication + if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword"); + + // Add fields to POST and PUT requests + if($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } elseif($method == 'PUT') { + $put = fopen("php://temp", 'r+'); + fwrite($put, $data); + fseek($put, 0); + + curl_setopt($ch, CURLOPT_PUT, 1); + curl_setopt($ch, CURLOPT_INFILE, $put); + curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data)); + } + + // Apply proxy settings + if(is_array($this->proxy)) { + curl_setopt_array($ch, $this->proxy); + } + + // Set any custom options passed to the request() function + curl_setopt_array($ch, $curlOptions); + + // Run request + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $responseBody = curl_exec($ch); + $curlError = curl_error($ch); + + // Problem verifying the server SSL certificate; just ignore it as it's not mandatory + if(strpos($curlError,'14090086') !== false) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $responseBody = curl_exec($ch); + $curlError = curl_error($ch); + } + + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if($curlError !== '' || $statusCode == 0) $statusCode = 500; + + $response = new RestfulService_Response($responseBody, $statusCode); + curl_close($ch); + + return $response; + } + /** * Returns a full request url * @param string diff --git a/core/ClassInfo.php b/core/ClassInfo.php old mode 100755 new mode 100644 index 09370c25a..3e23cf524 --- a/core/ClassInfo.php +++ b/core/ClassInfo.php @@ -12,16 +12,14 @@ class ClassInfo { * @todo Improve documentation */ static function allClasses() { - global $_ALL_CLASSES; - return $_ALL_CLASSES['exists']; + return SS_ClassLoader::instance()->allClasses(); } /** * @todo Improve documentation */ static function exists($class) { - global $_ALL_CLASSES; - return isset($_ALL_CLASSES['exists'][$class]) ? $_ALL_CLASSES['exists'][$class] : null; + return SS_ClassLoader::instance()->classExists($class); } /** @@ -59,54 +57,54 @@ class ClassInfo { } /** - * Return the database tables linked to this class. - * Gets an array of the current class, it subclasses and its ancestors. It then filters that list - * to those with DB tables + * Returns an array of the current class and all its ancestors and children + * which have a DB table. * - * @param mixed $class string of the classname or instance of the class + * @param string|object $class * @todo Move this into data object * @return array */ - static function dataClassesFor($class) { - global $_ALL_CLASSES; - if (is_object($class)) $class = get_class($class); - - $dataClasses = array(); - - if(!$_ALL_CLASSES['parents'][$class]) user_error("ClassInfo::dataClassesFor() no parents for $class", E_USER_WARNING); - foreach($_ALL_CLASSES['parents'][$class] as $subclass) { - if(self::hasTable($subclass)) $dataClasses[] = $subclass; - } - - if(self::hasTable($class)) $dataClasses[] = $class; + public static function dataClassesFor($class) { + $result = array(); - if(isset($_ALL_CLASSES['children'][$class])) - foreach($_ALL_CLASSES['children'][$class] as $subclass) - { - if(self::hasTable($subclass)) $dataClasses[] = $subclass; + if (is_object($class)) { + $class = get_class($class); } - - return $dataClasses; + + $classes = array_merge( + self::ancestry($class), + self::subclassesFor($class)); + + foreach ($classes as $class) { + if (self::hasTable($class)) $result[$class] = $class; + } + + return $result; } - + /** - * Return the root data class for that class. - * This root table has a lot of special use in the DataObject system. - * - * @param mixed $class string of the classname or instance of the class - * @return array + * Returns the root class (the first to extend from DataObject) for the + * passed class. + * + * @param string|object $class + * @return string */ - static function baseDataClass($class) { - global $_ALL_CLASSES; + public static function baseDataClass($class) { if (is_object($class)) $class = get_class($class); - reset($_ALL_CLASSES['parents'][$class]); - while($val = next($_ALL_CLASSES['parents'][$class])) { - if($val == 'DataObject') break; + + if (!self::is_subclass_of($class, 'DataObject')) { + throw new Exception("$class is not a subclass of DataObject"); + } + + while ($next = get_parent_class($class)) { + if ($next == 'DataObject') { + return $class; + } + + $class = $next; } - $baseDataClass = next($_ALL_CLASSES['parents'][$class]); - return $baseDataClass ? $baseDataClass : $class; } - + /** * Returns a list of classes that inherit from the given class. * The resulting array includes the base class passed @@ -125,38 +123,43 @@ class ClassInfo { * @param mixed $class string of the classname or instance of the class * @return array Names of all subclasses as an associative array. */ - static function subclassesFor($class){ - global $_ALL_CLASSES; - if (is_object($class)) $class = get_class($class); - - // get all classes from the manifest - $subclasses = isset($_ALL_CLASSES['children'][$class]) ? $_ALL_CLASSES['children'][$class] : null; + public static function subclassesFor($class) { + $descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($class); + $result = array($class => $class); - // add the base class to the array - if(isset($subclasses)) { - array_unshift($subclasses, $class); + if ($descendants) { + return $result + ArrayLib::valuekey($descendants); } else { - $subclasses[$class] = $class; + return $result; } - - return $subclasses; } - + /** - * @todo Improve documentation + * Returns the passed class name along with all its parent class names in an + * array, sorted with the root class first. + * + * @param string $class + * @param bool $tablesOnly Only return classes that have a table in the db. + * @return array */ - static function ancestry($class, $onlyWithTables = false) { - global $_ALL_CLASSES; + public static function ancestry($class, $tablesOnly = false) { + $ancestry = array(); - if(is_object($class)) $class = $class->class; - else if(!is_string($class)) user_error("Bad class value " . var_export($class, true) . " passed to ClassInfo::ancestry()", E_USER_WARNING); - - $items = $_ALL_CLASSES['parents'][$class]; - $items[$class] = $class; - if($onlyWithTables) foreach($items as $item) { - if(!DataObject::has_own_table($item)) unset($items[$item]); + if (is_object($class)) { + $class = get_class($class); + } elseif (!is_string($class)) { + throw new Exception(sprintf( + 'Invalid class value %s, must be an object or string', var_export($class, true) + )); } - return $items; + + do { + if (!$tablesOnly || DataObject::has_own_table($class)) { + $ancestry[$class] = $class; + } + } while ($class = get_parent_class($class)); + + return array_reverse($ancestry); } /** @@ -164,27 +167,23 @@ class ClassInfo { * classes and not built-in PHP classes. */ static function implementorsOf($interfaceName) { - global $_ALL_CLASSES; - return (isset($_ALL_CLASSES['implementors'][$interfaceName])) ? $_ALL_CLASSES['implementors'][$interfaceName] : false; + return SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName); } /** * Returns true if the given class implements the given interface */ static function classImplements($className, $interfaceName) { - global $_ALL_CLASSES; - return isset($_ALL_CLASSES['implementors'][$interfaceName][$className]); + return in_array($className, SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName)); } /** - * Returns true if $subclass is a subclass of $parentClass. - * Identical to the PHP built-in function, but faster. + * @deprecated 3.0 Please use is_subclass_of. */ - static function is_subclass_of($subclass, $parentClass) { - global $_ALL_CLASSES; - return isset($_ALL_CLASSES['parents'][$subclass][$parentClass]); + public static function is_subclass_of($class, $parent) { + return is_subclass_of($class, $parent); } - + /** * Get all classes contained in a file. * @uses ManifestBuilder @@ -196,11 +195,11 @@ class ClassInfo { * @return array */ static function classes_for_file($filePath) { - $absFilePath = Director::getAbsFile($filePath); - global $_CLASS_MANIFEST; - + $absFilePath = Director::getAbsFile($filePath); $matchedClasses = array(); - foreach($_CLASS_MANIFEST as $class => $compareFilePath) { + $manifest = SS_ClassLoader::instance()->getManifest()->getClasses(); + + foreach($manifest as $class => $compareFilePath) { if($absFilePath == $compareFilePath) $matchedClasses[] = $class; } @@ -217,11 +216,11 @@ class ClassInfo { * @return array Array of class names */ static function classes_for_folder($folderPath) { - $absFolderPath = Director::getAbsFile($folderPath); - global $_CLASS_MANIFEST; - + $absFolderPath = Director::getAbsFile($folderPath); $matchedClasses = array(); - foreach($_CLASS_MANIFEST as $class => $compareFilePath) { + $manifest = SS_ClassLoader::instance()->getManifest()->getClasses(); + + foreach($manifest as $class => $compareFilePath) { if(stripos($compareFilePath, $absFolderPath) === 0) $matchedClasses[] = $class; } diff --git a/core/Core.php b/core/Core.php old mode 100755 new mode 100644 index 997003280..a9a523547 --- a/core/Core.php +++ b/core/Core.php @@ -199,44 +199,39 @@ set_include_path(BASE_PATH . '/sapphire' . PATH_SEPARATOR . BASE_PATH . '/sapphire/thirdparty' . PATH_SEPARATOR . get_include_path()); -/** - * Sapphire class autoloader. Requires the ManifestBuilder to work. - * $_CLASS_MANIFEST must have been loaded up by ManifestBuilder for this to successfully load - * classes. Classes will be loaded from any PHP file within the application. - * If your class contains an underscore, for example, Page_Controller, then the filename is - * expected to be the stuff before the underscore. In this case, Page.php. - * - * Class names are converted to lowercase for lookup to adhere to PHP's case-insensitive - * way of dealing with them. - */ -function sapphire_autoload($className) { - global $_CLASS_MANIFEST; - $lClassName = strtolower($className); - if(isset($_CLASS_MANIFEST[$lClassName])) include_once($_CLASS_MANIFEST[$lClassName]); - else if(isset($_CLASS_MANIFEST[$className])) include_once($_CLASS_MANIFEST[$className]); -} - -spl_autoload_register('sapphire_autoload'); - -require_once("core/ManifestBuilder.php"); -require_once("core/ClassInfo.php"); -require_once('core/Object.php'); -require_once('core/control/Director.php'); -require_once('filesystem/Filesystem.php'); -require_once("core/Session.php"); +// Include the files needed the initial manifest building, as well as any files +// that are needed for the boostrap process on every request. +require_once 'cache/Cache.php'; +require_once 'core/Object.php'; +require_once 'core/ClassInfo.php'; +require_once 'core/control/Director.php'; +require_once 'dev/Debug.php'; +require_once 'filesystem/FileFinder.php'; +require_once 'manifest/ClassLoader.php'; +require_once 'manifest/ClassManifest.php'; +require_once 'manifest/ManifestFileFinder.php'; +require_once 'manifest/TemplateLoader.php'; +require_once 'manifest/TemplateManifest.php'; +require_once 'manifest/TokenisedRegularExpression.php'; /////////////////////////////////////////////////////////////////////////////// // MANIFEST -/** - * Include the manifest - */ -ManifestBuilder::include_manifest(); +// Regenerate the manifest if ?flush is set, or if the database is being built. +// The coupling is a hack, but it removes an annoying bug where new classes +// referenced in _config.php files can be referenced during the build process. +$flush = (isset($_GET['flush']) || isset($_REQUEST['url']) && ( + $_REQUEST['url'] == 'dev/build' || $_REQUEST['url'] == BASE_URL . '/dev/build' +)); +$manifest = new SS_ClassManifest(BASE_PATH, false, $flush); -/** - * ?debugmanifest=1 hook - */ -if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE)); +$loader = SS_ClassLoader::instance(); +$loader->registerAutoloader(); +$loader->pushManifest($manifest); + +SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( + BASE_PATH, false, isset($_GET['flush']) +)); // If this is a dev site, enable php error reporting // This is necessary to force developers to acknowledge and fix @@ -316,16 +311,10 @@ function getTempFolder($base = null) { } /** - * Return the file where that class is stored. - * - * @param String $className Case-insensitive lookup. - * @return String + * @deprecated 3.0 Please use {@link SS_ClassManifest::getItemPath()}. */ function getClassFile($className) { - global $_CLASS_MANIFEST; - $lClassName = strtolower($className); - if(isset($_CLASS_MANIFEST[$lClassName])) return $_CLASS_MANIFEST[$lClassName]; - else if(isset($_CLASS_MANIFEST[$className])) return $_CLASS_MANIFEST[$className]; + return SS_ClassLoader::instance()->getManifest()->getItemPath($className); } /** diff --git a/core/ManifestBuilder.php b/core/ManifestBuilder.php deleted file mode 100644 index 7d1fd7c11..000000000 --- a/core/ManifestBuilder.php +++ /dev/null @@ -1,630 +0,0 @@ -Autoloading - * - * Sapphire class autoloader. Requires the ManifestBuilder to work. - * $_CLASS_MANIFEST must have been loaded up by ManifestBuilder for this to successfully load classes. - * Classes will be loaded from any PHP file within the application. If your class contains an underscore, - * for example, Page_Controller, then the filename is expected to be the stuff before the underscore. - * In this case, Page.php. - * - * @see main.php, __autoload(), SSViewer, Requirements::themedCSS() - * @package sapphire - * @subpackage core - */ -class ManifestBuilder { - - static $restrict_to_modules = array(); - static $extendsArray = array(); - static $classArray = array(); - static $implementsArray = array(); - - /** - * @var array $ignore_files Full filenames (without directory-path) which - * should be ignored by the manifest. - */ - public static $ignore_files = array( - 'main.php', - 'cli-script.php', - 'install.php', - 'index.php', - ); - - /** - * @var array $ignore_folders Foldernames (without path) which - * should be ignored by the manifest. - */ - public static $ignore_folders = array( - 'mysql', - 'assets', - 'shortstat', - 'HTML', - ); - - /** - * Include the manifest, regenerating it if necessary - */ - static function include_manifest() { - if(isset($_REQUEST['usetestmanifest'])) { - self::load_test_manifest(); - } else { - // The dev/build reference is some coupling but it solves an annoying bug - if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH)) - || isset($_GET['flush']) || (isset($_REQUEST['url']) && ($_REQUEST['url'] == 'dev/build' - || $_REQUEST['url'] == BASE_URL . '/dev/build'))) { - self::create_manifest_file(); - } - require_once(MANIFEST_FILE); - } - } - - /** - * Load a copy of the manifest with tests/ folders included. - * Only loads the ClassInfo and __autoload() globals; this assumes that _config.php files are already included. - */ - static function load_test_manifest() { - $testManifestFile = MANIFEST_FILE . '-test'; - - // The dev/build reference is some coupling but it solves an annoying bug - if(!file_exists($testManifestFile) - || (filemtime($testManifestFile) < filemtime(BASE_PATH)) - || isset($_GET['flush'])) { - - // Build the manifest, including the tests/ folders - $manifestInfo = self::get_manifest_info(BASE_PATH); - $manifest = self::generate_php_file($manifestInfo); - if($fh = fopen($testManifestFile, 'wb')) { - fwrite($fh, $manifest); - fclose($fh); - } else { - user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR); - } - } - - require($testManifestFile); - } - - /** - * Loads all PHP class files - actually opening them and executing them. - */ - static function load_all_classes() { - global $_CLASS_MANIFEST; - foreach($_CLASS_MANIFEST as $classFile) require_once($classFile); - } - - /** - * Generates a new manifest file and saves it to {@link MANIFEST_FILE}. - */ - static function create_manifest_file() { - // Build the manifest, ignoring the tests/ folders - $manifestInfo = self::get_manifest_info(BASE_PATH, array("tests")); - - $manifest = self::generate_php_file($manifestInfo); - if($fh = fopen(MANIFEST_FILE, 'wb')) { - fwrite($fh, $manifest); - fclose($fh); - } else { - user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR); - } - } - - /** - * Turn an array produced by get_manifest_info() into the content of the manifest PHP include - */ - static function generate_php_file($manifestInfo) { - $output = " $globalVal) { - $output .= "global \$$globalName;\n\$$globalName = " . var_export($globalVal, true) . ";\n\n"; - } - foreach($manifestInfo['require_once'] as $requireItem) { - $output .= 'require_once("' . addslashes($requireItem) . "\");\n"; - } - - return $output; - } - - - /** - * Parse the $manifestInfo array, updating the appropriate globals and loading the appropriate _config files. - */ - static function process_manifest($manifestInfo) { - foreach($manifestInfo['globals'] as $globalName => $globalVal) { - global $$globalName; - $$globalName = $globalVal; - } - foreach($manifestInfo['require_once'] as $requireItem) { - require_once("$requireItem"); - } - } - - /** - * Get themes from a particular directory. - * - * @param string $baseDir Optional: Absolute path to theme directory for testing e.g. "/Users/sharvey/Sites/test24/themes" - * @param boolean $includeSubThemes If set to TRUE, sub-themes such as "blackcandy_blog" are included too - * @return array Listing of theme directories - */ - public static function get_themes($baseDir = null, $includeSubThemes = false) { - // If no base directory specified, the default is the project root - if(!$baseDir) $baseDir = BASE_PATH . DIRECTORY_SEPARATOR . THEMES_DIR; - $themes = array(); - if(!file_exists($baseDir)) return $themes; - - $handle = opendir($baseDir); - if($handle) { - while(false !== ($file = readdir($handle))) { - $fullPath = $baseDir . DIRECTORY_SEPARATOR . $file; - if(strpos($file, '.') === false && is_dir($fullPath)) { - $include = $includeSubThemes ? true : false; - if(strpos($file, '_') === false) { - $include = true; - } - if($include) $themes[$file] = $file; - } - } - closedir($handle); - } - return $themes; - } - - /** - * Return an array containing information for the manifest - * @param $baseDir The root directory to analyse - * @param $excludedFolders An array folder names to exclude. These don't care about where the - * folder appears in the hierarchy, so be careful - */ - static function get_manifest_info($baseDir, $excludedFolders = array()) { - // locate and include the exclude files - $topLevel = scandir($baseDir); - foreach($topLevel as $file) { - if($file[0] == '.') continue - - $fullPath = ''; - $fullPath = $baseDir . '/' . $file; - - if(@is_dir($fullPath . '/') && file_exists($fullPath . '/_exclude.php')) { - require_once($fullPath . '/_exclude.php'); - } - } - - // Project - used to give precedence to template files - $project = null; - - // Class, CSS, template manifest - $allPhpFiles = array(); - $templateManifest = array(); - $cssManifest = array(); - - - if(is_array(self::$restrict_to_modules) && count(self::$restrict_to_modules)) { - // $restrict_to_modules is set, so we include only those specified - // modules - foreach(self::$restrict_to_modules as $module) - ManifestBuilder::get_all_php_files($baseDir . '/' . $module, $excludedFolders, $allPhpFiles); - } else { - // Include all directories which have an _config.php file but don't - // have an _manifest_exclude file - $topLevel = scandir($baseDir); - foreach($topLevel as $filename) { - - // Skip certain directories - if($filename[0] == '.') continue; - if($filename == THEMES_DIR) continue; - if($filename == ASSETS_DIR) continue; - if(in_array($filename, $excludedFolders)) continue; - - if(@is_dir("$baseDir/$filename") && - file_exists("$baseDir/$filename/_config.php") && - !file_exists("$baseDir/$filename/_manifest_exclude")) { - - // Get classes, templates, and CSS files - ManifestBuilder::get_all_php_files("$baseDir/$filename", $excludedFolders, $allPhpFiles); - ManifestBuilder::getTemplateManifest($baseDir, $filename, $excludedFolders, $templateManifest, $cssManifest); - - // List the _config.php files - $manifestInfo["require_once"][] = "$baseDir/$filename/_config.php"; - // Find the $project variable in the relevant config file without having to execute the config file - if(preg_match("/\\\$project\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("$baseDir/$filename/_config.php"), $parts)) { - eval($parts[0]); - } - } - } - } - - // Get themes - if(file_exists("$baseDir/themes")) { - $themeDirs = self::get_themes("$baseDir/themes", true); - foreach($themeDirs as $themeDir) { - $themeName = strtok($themeDir, '_'); - ManifestBuilder::getTemplateManifest($baseDir, THEMES_DIR . "/$themeDir", $excludedFolders, $templateManifest, $cssManifest, $themeName); - } - } - - // Build class-info array from class manifest - $allClasses = ManifestBuilder::allClasses($allPhpFiles); - - // Pull the class filenames out - $classManifest = $allClasses['file']; - unset($allClasses['file']); - - // Ensure that any custom templates get favoured - if(!$project) user_error("\$project isn't set", E_USER_WARNING); - else if(!file_exists("$baseDir/$project")) user_error("\$project is set to '$project' but no such folder exists.", E_USER_WARNING); - else ManifestBuilder::getTemplateManifest($baseDir, $project, $excludedFolders, $templateManifest, $cssManifest); - - $manifestInfo["globals"]["_CLASS_MANIFEST"] = $classManifest; - $manifestInfo["globals"]["_ALL_CLASSES"] = $allClasses; - $manifestInfo["globals"]["_TEMPLATE_MANIFEST"] = $templateManifest; - $manifestInfo["globals"]["_CSS_MANIFEST"] = $cssManifest; - - return $manifestInfo; - } - - - /** - * Generates a list of all the PHP files that should be analysed by the manifest builder. - * - * @param string $folder The folder to traverse (recursively) - * @param array $classMap The already built class map - */ - private static function get_all_php_files($folder, $excludedFolders, &$allPhpFiles) { - $items = scandir($folder); - if($items) foreach($items as $item) { - // Skip some specific PHP files - if(in_array($item, self::$ignore_files)) continue; - - // ignore hidden files and folders - if(substr($item,0,1) == '.') continue; - - // ignore files without php-extension - if(substr($item,-4) != '.php' && !@is_dir("$folder/$item")) continue; - - // ignore files and folders with underscore-prefix - if(substr($item,0,1) == '_') continue; - - // ignore certain directories - if(@is_dir("$folder/$item") && in_array($item, self::$ignore_folders)) continue; - - // ignore directories with _manifest_exlude file - if(@is_dir("$folder/$item") && file_exists("$folder/$item/_manifest_exclude")) continue; - - // i18n: ignore language files (loaded on demand) - if($item == 'lang' && @is_dir("$folder/$item") && ereg_replace("/[^/]+/\\.\\.","",$folder.'/..') == Director::baseFolder()) continue; - - if(@is_dir("$folder/$item")) { - // Folder exclusion - used to skip over tests/ folders - if(in_array($item, $excludedFolders)) continue; - - // recurse into directories (if not in $ignore_folders) - ManifestBuilder::get_all_php_files("$folder/$item", $excludedFolders, $allPhpFiles); - } else { - $allPhpFiles[] = "$folder/$item"; - } - - } - } - - - /** - * Generates the template manifest - a list of all the .ss files in the - * application. - * - * See {@link SSViewer} for an overview on the array structure this class creates. - * - * @param String $baseDir - * @param String $folder - */ - private static function getTemplateManifest($baseDir, $folder, $excludedFolders, &$templateManifest, &$cssManifest, $themeName = null) { - $items = scandir("$baseDir/$folder"); - if($items) foreach($items as $item) { - // Skip hidden files/folders - if(substr($item,0,1) == '.') continue; - - // Parse *.ss files - if(substr($item,-3) == '.ss') { - // Remove extension from template name - $templateName = substr($item, 0, -3); - - // The "type" is effectively a subfolder underneath $folder, - // mostly "Includes" or "Layout". - $templateType = substr($folder,strrpos($folder,'/')+1); - // The parent folder counts as type "main" - if($templateType == "templates") $templateType = "main"; - - // Write either to theme or to non-themed array - if($themeName) { - $templateManifest[$templateName]['themes'][$themeName][$templateType] = "$baseDir/$folder/$item"; - } else { - $templateManifest[$templateName][$templateType] = "$baseDir/$folder/$item"; - } - - } else if(substr($item,-4) == '.css') { - $cssName = substr($item, 0, -4); - // Debug::message($item); - - if($themeName) { - $cssManifest[$cssName]['themes'][$themeName] = "$folder/$item"; - } else { - $cssManifest[$cssName]['unthemed'] = "$folder/$item"; - } - - - } else if(@is_dir("$baseDir/$folder/$item")) { - // Folder exclusion - used to skip over tests/ folders - if(in_array($item, $excludedFolders)) continue; - - ManifestBuilder::getTemplateManifest($baseDir, "$folder/$item", $excludedFolders, $templateManifest, $cssManifest, $themeName); - } - } - } - - - /** - * Include everything, so that actually *all* classes are available and - * build a map of classes and their subclasses - * - * @param $classManifest An array of all Sapphire classes; keys are class names and values are filenames - * - * @return array Returns an array that holds all class relevant - * information. - */ - private static function allClasses($classManifest) { - self::$classArray = array(); - self::$extendsArray = array(); - self::$implementsArray = array(); - - // Include everything, so we actually have *all* classes - foreach($classManifest as $file) { - $b = basename($file); - if($b != 'cli-script.php' && $b != 'main.php') - self::parse_file($file); - } - - $allClasses["parents"] = self::find_parents(); - $allClasses["children"] = self::find_children(); - $allClasses["implementors"] = self::$implementsArray; - - foreach(self::$classArray as $class => $info) { - $allClasses['exists'][$class] = $class; - // Class names are converted to lowercase for lookup to adhere to PHP's case-insensitive - // way of dealing with them. - $allClasses['file'][strtolower($class)] = $info['file']; - } - - // Build a map of classes and their subclasses - $_classes = get_declared_classes(); - - foreach($_classes as $class) { - $allClasses['exists'][$class] = $class; - foreach($_classes as $subclass) { - if(is_subclass_of($class, $subclass)) $allClasses['parents'][$class][$subclass] = $subclass; - if(is_subclass_of($subclass, $class)) $allClasses['children'][$class][$subclass] = $subclass; - } - } - - return $allClasses; - } - - /** - * Parses a php file and adds any class or interface information into self::$classArray - * - * @param string $filename - */ - private static function parse_file($filename) { - $file = file_get_contents($filename); - - $implements = ""; - $extends = ""; - $class=""; - - if($file === null) user_error("ManifestBuilder::parse_file(): Couldn't open $filename", E_USER_ERROR); - if(!$file) return; - - // We cache the parse results of each file, since only a few files will have changed between flushings - // And, although it's accurate, TokenisedRegularExpression isn't particularly fast. - // We use an MD5 of the file as a part of the cache key because using datetime caused problems when users - // were upgrading their sites - $fileMD5 = md5($file); - $parseCacheFile = TEMP_FOLDER . "/manifestClassParse-" . str_replace(array("/", ":", "\\", "."), "_", basename($filename)) . "-$fileMD5"; - if(file_exists($parseCacheFile)) { - include($parseCacheFile); - // Check for a bad cache file - if(!isset($classes) || !isset($interfaces) || !is_array($classes) || !is_array($interfaces)) { - unset($classes); - unset($interfaces); - } - } - - // Either the parseCacheFile doesn't exist, or its bad - if(!isset($classes)) { - $tokens = token_get_all($file); - $classes = (array)self::getClassDefParser()->findAll($tokens); - $interfaces = (array)self::getInterfaceDefParser()->findAll($tokens); - - $cacheContent = ' T_CLASS, - 1 => T_WHITESPACE, - 2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'), - 3 => T_WHITESPACE, - 4 => T_EXTENDS, - 5 => T_WHITESPACE, - 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14), - 7 => T_WHITESPACE, - 8 => T_IMPLEMENTS, - 9 => T_WHITESPACE, - 10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'), - 11 => array(T_WHITESPACE, 'optional' => true), - 12 => array(',', 'can_jump_to' => 10), - 13 => array(T_WHITESPACE, 'can_jump_to' => 10), - 14 => array(T_WHITESPACE, 'optional' => true), - 15 => '{', - )); - } - - /** - * Returns a {@link TokenisedRegularExpression} object that will parse class definitions - * @return TokenisedRegularExpression - */ - public static function getInterfaceDefParser() { - require_once('core/TokenisedRegularExpression.php'); - - return new TokenisedRegularExpression(array( - 0 => T_INTERFACE, - 1 => T_WHITESPACE, - 2 => array(T_STRING, 'can_jump_to' => 7, 'save_to' => 'interfaceName'), - 3 => T_WHITESPACE, - 4 => T_EXTENDS, - 5 => T_WHITESPACE, - 6 => array(T_STRING, 'save_to' => 'extends'), - 7 => array(T_WHITESPACE, 'optional' => true), - 8 => '{', - )); - } - - - /** - * Moves through self::$classArray and creates an array containing parent data - * - * @return array - */ - private static function find_parents() { - $parentArray = array(); - foreach(self::$classArray as $class => $info) { - $extendArray = array(); - - $parent = $info["extends"]; - - while($parent) { - $extendArray[$parent] = $parent; - $parent = isset(self::$classArray[$parent]["extends"]) ? self::$classArray[$parent]["extends"] : null; - } - $parentArray[$class] = array_reverse($extendArray); - } - return $parentArray; - } - - /** - * Iterates through self::$classArray and returns an array with any descendant data - * - * @return array - */ - private static function find_children() { - $childrenArray = array(); - foreach(self::$extendsArray as $class => $children) { - $allChildren = $children; - foreach($children as $childName) { - $allChildren = array_merge($allChildren, self::up_children($childName)); - } - $childrenArray[$class] = $allChildren; - } - return $childrenArray; - } - - /** - * Helper function to find all children of give class - * - * @param string $class - * @return array - */ - private static function get_children($class) { - return isset(self::$extendsArray[$class]) ? self::$extendsArray[$class] : array(); - } - - /** - * Returns if the Manifest has been included - * - * @return Boolean - */ - static function has_been_included() { - global $_CLASS_MANIFEST, $_TEMPLATE_MANIFEST, $_CSS_MANIFEST, $_ALL_CLASSES; - return (bool)!(empty($_CLASS_MANIFEST) && empty($_TEMPLATE_MANIFEST) && empty($_CSS_MANIFEST) && empty($_ALL_CLASSES)); - } - - /** - * Returns a flat array with all children of a given class - * - * @param string $class - * @param array $results - */ - static function up_children($class) { - $children = self::get_Children($class); - $results = $children; - foreach($children as $className) { - $results = array_merge($results, self::up_children($className)); - } - return $results;; - } -} diff --git a/core/Object.php b/core/Object.php old mode 100755 new mode 100644 index 2e49fbd76..24efb1c9b --- a/core/Object.php +++ b/core/Object.php @@ -513,7 +513,7 @@ abstract class Object { user_error(sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), E_USER_ERROR); } - if(!ClassInfo::is_subclass_of($extensionClass, 'Extension')) { + if(!is_subclass_of($extensionClass, 'Extension')) { user_error(sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass), E_USER_ERROR); } @@ -537,7 +537,7 @@ abstract class Object { self::set_static($class, 'extensions', $extensions); // load statics now for DataObject classes - if(ClassInfo::is_subclass_of($class, 'DataObject')) { + if(is_subclass_of($class, 'DataObject')) { if(is_subclass_of($extensionClass, 'DataObjectDecorator')) { DataObjectDecorator::load_extra_statics($class, $extension); } diff --git a/core/Requirements.php b/core/Requirements.php index 9e9f38728..599091a57 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -143,15 +143,24 @@ class Requirements { } /** - * Register the given "themeable stylesheet" as required. See {@link Requirements_Backend::themedCSS()} - * - * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile" - * @param $media String Comma-separated list of media-types (e.g. "screen,projector") + * Registers the given themeable stylesheet as required. + * + * A CSS file in the current theme path name "themename/css/$name.css" is + * first searched for, and it that doesn't exist and the module parameter is + * set then a CSS file with that name in the module is used. + * + * NOTE: This API is experimental and may change in the future. + * + * @param string $name The name of the file - e.g. "/css/File.css" would have + * the name "File". + * @param string $module The module to fall back to if the css file does not + * exist in the current theme. + * @param string $media The CSS media attribute. */ - static function themedCSS($name, $media = null) { - return self::backend()->themedCSS($name, $media); + public static function themedCSS($name, $module = null, $media = null) { + return self::backend()->themedCSS($name, $module, $media); } - + /** * Clear either a single or all requirements. * Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}. @@ -1044,27 +1053,22 @@ class Requirements_Backend { return $requirements; } - - /** - * Register the given "themeable stylesheet" as required. - * Themeable stylesheets have globally unique names, just like templates and PHP files. - * Because of this, they can be replaced by similarly named CSS files in the theme directory. - * - * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile" - * @param $media String Comma-separated list of media-types (e.g. "screen,projector") - */ - function themedCSS($name, $media = null) { - global $_CSS_MANIFEST; - - $theme = SSViewer::current_theme(); - - if($theme && isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['themes']) - && isset($_CSS_MANIFEST[$name]['themes'][$theme])) - $this->css($_CSS_MANIFEST[$name]['themes'][$theme], $media); - else if(isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['unthemed'])) $this->css($_CSS_MANIFEST[$name]['unthemed'], $media); - // Normal requirements fails quietly when there is no css - we should do the same - // else user_error("themedCSS - No CSS file '$name.css' found.", E_USER_WARNING); + /** + * @see Requirements::themedCSS() + */ + public function themedCSS($name, $module = null, $media = null) { + $theme = SSViewer::current_theme(); + $path = SSViewer::get_theme_folder() . "/css/$name.css"; + + if (file_exists(BASE_PATH . '/' . $path)) { + $this->css($path, $media); + return; + } + + if ($module) { + $this->css("$module/css/$name.css"); + } } function debug() { diff --git a/core/SSViewer.php b/core/SSViewer.php old mode 100755 new mode 100644 index 506c78e32..72a6ef8db --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -182,46 +182,10 @@ class SSViewer_DataPresenter extends SSViewer_Scope { * * Compiled templates are cached via {@link SS_Cache}, usually on the filesystem. * If you put ?flush=all on your URL, it will force the template to be recompiled. - * - * Manifest File and Structure - * - * Works with the global $_TEMPLATE_MANIFEST which is compiled by {@link ManifestBuilder->getTemplateManifest()}. - * This associative array lists all template filepaths by "identifier", meaning the name - * of the template without its path or extension. - * - * Example: - * - * array( - * 'LeftAndMain' => - * array ( - * 'main' => '/my/system/path/cms/templates/LeftAndMain.ss', - * ), - * 'CMSMain_left' => - * array ( - * 'Includes' => '/my/system/path/cms/templates/Includes/CMSMain_left.ss', - * ), - * 'Page' => - * array ( - * 'themes' => - * array ( - * 'blackcandy' => - * array ( - * 'Layout' => '/my/system/path/themes/blackcandy/templates/Layout/Page.ss', - * 'main' => '/my/system/path/themes/blackcandy/templates/Page.ss', - * ), - * 'blue' => - * array ( - * 'Layout' => '/my/system/path/themes/mysite/templates/Layout/Page.ss', - * 'main' => '/my/system/path/themes/mysite/templates/Page.ss', - * ), - * ), - * ), - * // ... - * ) - * * * @see http://doc.silverstripe.org/themes * @see http://doc.silverstripe.org/themes:developing + * * @package sapphire * @subpackage view @@ -306,6 +270,30 @@ class SSViewer { return self::current_theme() ? THEMES_DIR . "/" . self::current_theme() : project(); } + /** + * Returns an array of theme names present in a directory. + * + * @param string $path + * @param bool $subthemes Include subthemes (default false). + * @return array + */ + public static function get_themes($path = null, $subthemes = false) { + $path = rtrim($path ? $path : THEMES_PATH, '/'); + $themes = array(); + + if (!is_dir($path)) return $themes; + + foreach (scandir($path) as $item) { + if ($item[0] != '.' && is_dir("$path/$item")) { + if ($subthemes || !strpos($item, '_')) { + $themes[$item] = $item; + } + } + } + + return $themes; + } + /** * @return string */ @@ -322,8 +310,6 @@ class SSViewer { * */ public function __construct($templateList) { - global $_TEMPLATE_MANIFEST; - // flush template manifest cache if requested if (isset($_GET['flush']) && $_GET['flush'] == 'all') { if(Director::isDev() || Director::is_cli() || Permission::check('ADMIN')) { @@ -336,64 +322,25 @@ class SSViewer { if(substr((string) $templateList,-3) == '.ss') { $this->chosenTemplates['main'] = $templateList; } else { - if(!is_array($templateList)) $templateList = array($templateList); - - if(isset($_GET['debug_request'])) Debug::message("Selecting templates from the following list: " . implode(", ", $templateList)); - - foreach($templateList as $template) { - // if passed as a partial directory (e.g. "Layout/Page"), split into folder and template components - if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2); - else $templateFolder = null; - - // Use the theme template if available - if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()])) { - $this->chosenTemplates = array_merge( - $_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()], - $this->chosenTemplates - ); - - if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main theme '" . self::current_theme() . "': " . var_export($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()], true)); - } - - // Fall back to unthemed base templates - if(isset($_TEMPLATE_MANIFEST[$template]) && (array_keys($_TEMPLATE_MANIFEST[$template]) != array('themes'))) { - $this->chosenTemplates = array_merge( - $_TEMPLATE_MANIFEST[$template], - $this->chosenTemplates - ); - - if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main template archive, containing the following items: " . var_export($_TEMPLATE_MANIFEST[$template], true)); - - unset($this->chosenTemplates['themes']); - } - - if($templateFolder) { - $this->chosenTemplates['main'] = $this->chosenTemplates[$templateFolder]; - unset($this->chosenTemplates[$templateFolder]); - } - } - - if(isset($_GET['debug_request'])) Debug::message("Final template selections made: " . var_export($this->chosenTemplates, true)); - + $this->chosenTemplates = SS_TemplateLoader::instance()->findTemplates( + $templateList, self::current_theme() + ); } if(!$this->chosenTemplates) user_error("None of these templates can be found in theme '" . self::current_theme() . "': ". implode(".ss, ", $templateList) . ".ss", E_USER_WARNING); - } /** * Returns true if at least one of the listed templates exists */ - static function hasTemplate($templateList) { - if(!is_array($templateList)) $templateList = array($templateList); - - global $_TEMPLATE_MANIFEST; - foreach($templateList as $template) { - if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2); - if(isset($_TEMPLATE_MANIFEST[$template])) return true; + public static function hasTemplate($templates) { + $manifest = SS_TemplateLoader::instance()->getManifest(); + + foreach ((array) $templates as $template) { + if ($manifest->getTemplate($template)) return true; } - + return false; } @@ -433,76 +380,21 @@ class SSViewer { public function exists() { return $this->chosenTemplates; } - - /** - * Searches for a template name in the current theme: - * - themes/mytheme/templates - * - themes/mytheme/templates/Includes - * Falls back to unthemed template files. - * - * Caution: Doesn't search in any /Layout folders. - * - * @param string $identifier A template name without '.ss' extension or path. - * @return string Full system path to a template file - */ - public static function getTemplateFile($identifier) { - global $_TEMPLATE_MANIFEST; - - $includeTemplateFile = self::getTemplateFileByType($identifier, 'Includes'); - if($includeTemplateFile) return $includeTemplateFile; - - $mainTemplateFile = self::getTemplateFileByType($identifier, 'main'); - if($mainTemplateFile) return $mainTemplateFile; - - return false; - } - + /** * @param string $identifier A template name without '.ss' extension or path * @param string $type The template type, either "main", "Includes" or "Layout" * @return string Full system path to a template file */ public static function getTemplateFileByType($identifier, $type) { - global $_TEMPLATE_MANIFEST; - if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type])) { - return $_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type]; - } else if(isset($_TEMPLATE_MANIFEST[$identifier][$type])){ - return $_TEMPLATE_MANIFEST[$identifier][$type]; - } else { - return false; + $loader = SS_TemplateLoader::instance(); + $found = $loader->findTemplates("$type/$identifier", self::current_theme()); + + if ($found) { + return $found['main']; } } - - /** - * Used by <% include Identifier %> statements to get the full - * unparsed content of a template file. - * - * @uses getTemplateFile() - * @param string $identifier A template name without '.ss' extension or path. - * @return string content of template - */ - public static function getTemplateContent($identifier) { - if(!SSViewer::getTemplateFile($identifier)) { - return null; - } - - $content = file_get_contents(SSViewer::getTemplateFile($identifier)); - // $content = "". $content; - // Adds an i18n namespace to all _t(...) calls without an existing one - // to avoid confusion when using the include in different contexts. - // Entities without a namespace are deprecated, but widely used. - $content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '', $content); - - // Remove UTF-8 byte order mark - // This is only necessary if you don't have zend-multibyte enabled. - if(substr($content, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) { - $content = substr($content, 3); - } - - return $content; - } - /** * @ignore */ diff --git a/core/control/ContentController.php b/core/control/ContentController.php old mode 100755 new mode 100644 index a2d14f7bd..afa1de841 --- a/core/control/ContentController.php +++ b/core/control/ContentController.php @@ -230,7 +230,7 @@ class ContentController extends Controller { $hasOnes = $this->dataRecord->has_one(); if(!$hasOnes) return false; foreach($hasOnes as $hasOneName => $hasOneClass) { - if($hasOneClass == 'WidgetArea' || ClassInfo::is_subclass_of($hasOneClass, 'WidgetArea')) { + if($hasOneClass == 'WidgetArea' || is_subclass_of($hasOneClass, 'WidgetArea')) { $widgetAreaRelations[] = $hasOneName; } } diff --git a/core/control/HTTPRequest.php b/core/control/HTTPRequest.php old mode 100755 new mode 100644 index 6756709f0..c82043ab3 --- a/core/control/HTTPRequest.php +++ b/core/control/HTTPRequest.php @@ -348,7 +348,7 @@ class SS_HTTPRequest implements ArrayAccess { if($varRequired && !isset($this->dirParts[$i])) return false; $arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null; - if($part == '$Controller' && (!ClassInfo::exists($arguments['Controller']) || !ClassInfo::is_subclass_of($arguments['Controller'], 'Controller'))) { + if($part == '$Controller' && (!ClassInfo::exists($arguments['Controller']) || !is_subclass_of($arguments['Controller'], 'Controller'))) { return false; } diff --git a/core/i18n.php b/core/i18n.php old mode 100755 new mode 100644 index 409d12b2d..cc95e30a8 --- a/core/i18n.php +++ b/core/i18n.php @@ -1680,41 +1680,25 @@ class i18n extends Object { } /** - * Given a file name (a php class name, without the .php ext, or a template name, including the .ss extension) - * this helper function determines the module where this file is located + * Given a PHP class name, finds the module where it's located. * - * @param string $name php class name or template file name (including *.ss extension) - * @return string Module where the file is located + * @param string $name + * @return string */ public static function get_owner_module($name) { - // if $name is a template file - if(substr($name,-3) == '.ss') { - global $_TEMPLATE_MANIFEST; - $templateManifest = $_TEMPLATE_MANIFEST[substr($name,0,-3)]; - if(is_array($templateManifest) && isset($templateManifest['themes'])) { - $absolutePath = $templateManifest['themes'][SSViewer::current_theme()]; - } else { - $absolutePath = $templateManifest; - } - - $path = str_replace('\\','/',Director::makeRelative(current($absolutePath))); - - ereg('/([^/]+)/',$path,$module); - } - // $name is assumed to be a PHP class - else { - global $_CLASS_MANIFEST; - if(strpos($name,'_') !== false) $name = strtok($name,'_'); - $name = strtolower($name); // Necessary because of r101131 - if(isset($_CLASS_MANIFEST[$name])) { - $path = str_replace('\\','/',Director::makeRelative($_CLASS_MANIFEST[$name])); - ereg('/([^/]+)/', $path, $module); - } - } - return (isset($module)) ? $module[1] : false; + $manifest = SS_ClassLoader::instance()->getManifest(); + $path = $manifest->getItemPath($name); + if (!$path) { + return false; + } + + $path = Director::makeRelative($path); + $path = str_replace('\\', '/', $path); + + return substr($path, 0, strpos($path, '/')); } - + /** * Validates a "long" locale format (e.g. "en_US") * by checking it against {@link $all_locales}. diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 80eb7c8fe..2ca99fc1a 100755 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -2605,8 +2605,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Get the tables to join to $tableClasses = ClassInfo::dataClassesFor($this->class); if(!$tableClasses) { - if(!ManifestBuilder::has_been_included()) { - user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR); + if (!DB::getConn()) { + throw new Exception('DataObjects have been requested before' + . ' a DB connection has been made. Please ensure you' + . ' are not querying the database in _config.php.'); } else { user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR); } diff --git a/core/model/DatabaseAdmin.php b/core/model/DatabaseAdmin.php index fc62b3810..53f47da66 100644 --- a/core/model/DatabaseAdmin.php +++ b/core/model/DatabaseAdmin.php @@ -91,9 +91,8 @@ class DatabaseAdmin extends Controller { increase_time_limit_to(600); // Get all our classes - ManifestBuilder::create_manifest_file(); - require(MANIFEST_FILE); - + SS_ClassLoader::instance()->getManifest()->regenerate(); + if(isset($_GET['returnURL'])) { echo "

Setting up the database; you will be returned to your site shortly....

"; $this->doBuild(true); diff --git a/core/model/SiteConfig.php b/core/model/SiteConfig.php index 02fb87efd..72b8c60f1 100644 --- a/core/model/SiteConfig.php +++ b/core/model/SiteConfig.php @@ -116,7 +116,7 @@ class SiteConfig extends DataObject implements PermissionProvider { * @return array of theme directory names */ public function getAvailableThemes($baseDir = null) { - $themes = ManifestBuilder::get_themes($baseDir); + $themes = SSViewer::get_themes($baseDir); foreach(self::$disabled_themes as $theme) { if(isset($themes[$theme])) unset($themes[$theme]); } diff --git a/core/model/Versioned.php b/core/model/Versioned.php old mode 100755 new mode 100644 index e1d47c242..f564f4a50 --- a/core/model/Versioned.php +++ b/core/model/Versioned.php @@ -479,7 +479,7 @@ class Versioned extends DataObjectDecorator { */ function canBeVersioned($table) { return ClassInfo::exists($table) - && ClassInfo::is_subclass_of($table, 'DataObject') + && is_subclass_of($table, 'DataObject') && DataObject::has_own_table($table); } diff --git a/dev/CodeViewer.php b/dev/CodeViewer.php index 29390bc41..d02e27eab 100644 --- a/dev/CodeViewer.php +++ b/dev/CodeViewer.php @@ -87,7 +87,7 @@ class CodeViewer extends Controller { parent::init(); if(!Permission::check('ADMIN')) return Security::permissionFailure(); - ManifestBuilder::load_test_manifest(); + TestRunner::use_test_manifest(); } public function browse() { diff --git a/dev/ModelViewer.php b/dev/ModelViewer.php index ebf88f570..e23857ee8 100644 --- a/dev/ModelViewer.php +++ b/dev/ModelViewer.php @@ -128,11 +128,12 @@ class ModelViewer_Model extends ViewableData { } function getModule() { - global $_CLASS_MANIFEST; + $classes = SS_ClassLoader::instance()->getManifest()->getClasses(); $className = strtolower($this->className); + if(($pos = strpos($className,'_')) !== false) $className = substr($className,0,$pos); - if(isset($_CLASS_MANIFEST[$className])) { - if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $_CLASS_MANIFEST[$className], $matches)) { + if(isset($classes[$className])) { + if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $classes[$className], $matches)) { return $matches[1]; } } diff --git a/dev/TestRunner.php b/dev/TestRunner.php index 92b831bfc..167f02e56 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -79,7 +79,21 @@ class TestRunner extends Controller { if (is_string($reporter)) $reporter = new $reporter; self::$default_reporter = $reporter; } - + + /** + * Pushes a class and template manifest instance that include tests onto the + * top of the loader stacks. + */ + public static function use_test_manifest() { + SS_ClassLoader::instance()->pushManifest(new SS_ClassManifest( + BASE_PATH, true, isset($_GET['flush']) + )); + + SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( + BASE_PATH, true, isset($_GET['flush']) + )); + } + function init() { parent::init(); @@ -102,7 +116,7 @@ class TestRunner extends Controller { * Currently excludes PhpSyntaxTest */ function all($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); @@ -122,7 +136,7 @@ class TestRunner extends Controller { * Run test classes that should be run before build - i.e., everything possible. */ function build() { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); @@ -138,7 +152,7 @@ class TestRunner extends Controller { * Browse all enabled test cases in the environment */ function browse() { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); self::$default_reporter->writeHeader(); self::$default_reporter->writeInfo('Available Tests', false); if(Director::is_cli()) { @@ -170,7 +184,7 @@ class TestRunner extends Controller { * Run a coverage test across all modules */ function coverageAll($request) { - ManifestBuilder::load_all_classes(); + self::use_test_manifest(); $this->all($request, true); } @@ -197,7 +211,7 @@ class TestRunner extends Controller { * Run only a single test class or a comma-separated list of tests */ function only($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); if($request->param('TestCase') == 'all') { $this->all(); } else { @@ -217,7 +231,7 @@ class TestRunner extends Controller { * A module is generally a toplevel folder, e.g. "mysite" or "sapphire". */ function module($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $classNames = array(); $moduleNames = explode(',', $request->param('ModuleName')); foreach($moduleNames as $moduleName) { diff --git a/filesystem/FileFinder.php b/filesystem/FileFinder.php new file mode 100644 index 000000000..d956828e8 --- /dev/null +++ b/filesystem/FileFinder.php @@ -0,0 +1,222 @@ + null, + 'accept_callback' => null, + 'accept_dir_callback' => null, + 'accept_file_callback' => null, + 'file_callback' => null, + 'dir_callback' => null, + 'ignore_files' => null, + 'ignore_dirs' => null, + 'ignore_vcs' => true, + 'min_depth' => null, + 'max_depth' => null + ); + + /** + * @var array + */ + protected $options; + + public function __construct() { + $this->options = Object::combined_static(get_class($this), 'default_options'); + } + + /** + * Returns an option value set on this instance. + * + * @param string $name + * @return mixed + */ + public function getOption($name) { + if (!array_key_exists($name, $this->options)) { + throw new InvalidArgumentException("The option $name doesn't exist."); + } + + return $this->options[$name]; + } + + /** + * Set an option on this finder instance. See {@link SS_FileFinder} for the + * list of options available. + * + * @param string $name + * @param mixed $value + */ + public function setOption($name, $value) { + if (!array_key_exists($name, $this->options)) { + throw new InvalidArgumentException("The option $name doesn't exist."); + } + + $this->options[$name] = $value; + } + + /** + * Sets several options at once. + * + * @param array $options + */ + public function setOptions(array $options) { + foreach ($options as $k => $v) $this->setOption($k, $v); + } + + /** + * Finds all files matching the options within a directory. The search is + * performed depth first. + * + * @param string $base + * @return array + */ + public function find($base) { + $paths = array(array(rtrim($base, '/'), 0)); + $found = array(); + + $fileCallback = $this->getOption('file_callback'); + $dirCallback = $this->getOption('dir_callback'); + + while ($path = array_shift($paths)) { + list($path, $depth) = $path; + + foreach (scandir($path) as $basename) { + if ($basename == '.' || $basename == '..') { + continue; + } + + if (is_dir("$path/$basename")) { + if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) { + continue; + } + + if ($dirCallback) { + call_user_func( + $dirCallback, $basename, "$path/$basename", $depth + 1 + ); + } + + $paths[] = array("$path/$basename", $depth + 1); + } else { + if (!$this->acceptFile($basename, "$path/$basename", $depth)) { + continue; + } + + if ($fileCallback) { + call_user_func( + $fileCallback, $basename, "$path/$basename", $depth + ); + } + + $found[] = "$path/$basename"; + } + } + } + + return $found; + } + + /** + * Returns TRUE if the directory should be traversed. This can be overloaded + * to customise functionality, or extended with callbacks. + * + * @return bool + */ + protected function acceptDir($basename, $pathname, $depth) { + if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) { + return false; + } + + if ($ignore = $this->getOption('ignore_dirs')) { + if (in_array($basename, $ignore)) return false; + } + + if ($max = $this->getOption('max_depth')) { + if ($depth > $max) return false; + } + + if ($callback = $this->getOption('accept_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + if ($callback = $this->getOption('accept_dir_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + return true; + } + + /** + * Returns TRUE if the file should be included in the results. This can be + * overloaded to customise functionality, or extended via callbacks. + * + * @return bool + */ + protected function acceptFile($basename, $pathname, $depth) { + if ($regex = $this->getOption('name_regex')) { + if (!preg_match($regex, $basename)) return false; + } + + if ($ignore = $this->getOption('ignore_files')) { + if (in_array($basename, $ignore)) return false; + } + + if ($minDepth = $this->getOption('min_depth')) { + if ($depth < $minDepth) return false; + } + + if ($callback = $this->getOption('accept_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + if ($callback = $this->getOption('accept_file_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + return true; + } + +} \ No newline at end of file diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php old mode 100755 new mode 100644 index f46902e62..e8c458d4e --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -937,29 +937,6 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { * ################################# */ - /** - * Get part of class ancestry (even if popup is not subclassed it might be styled differently in css) - */ - function PopupClasses() { - global $_ALL_CLASSES; - - $items = array(); - $parents = isset($_ALL_CLASSES['parents'][$this->class]) ? $_ALL_CLASSES['parents'][$this->class] : null; - - if($parents) { - foreach($parents as $parent) { - if(!in_array($parent, $_ALL_CLASSES['parents']['TableListField'])) { - $items[] = $parent . '_Popup'; - } - } - } - - $items[] = $this->class . '_Popup'; - - return implode(' ', $items); - } - - /** * Returns the db-fieldname of the currently used has_one-relationship. */ diff --git a/manifest/ClassLoader.php b/manifest/ClassLoader.php new file mode 100644 index 000000000..1c1513d70 --- /dev/null +++ b/manifest/ClassLoader.php @@ -0,0 +1,85 @@ +manifests[count($this->manifests) - 1]; + } + + /** + * Pushes a class manifest instance onto the top of the stack. This will + * also include any module configuration files at the same time. + * + * @param SS_ClassManifest $manifest + */ + public function pushManifest(SS_ClassManifest $manifest) { + $this->manifests[] = $manifest; + + foreach ($manifest->getConfigs() as $config) { + require_once $config; + } + } + + /** + * @return SS_ClassManifest + */ + public function popManifest() { + return array_pop($this->manifests); + } + + public function registerAutoloader() { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Loads a class or interface if it is present in the currently active + * manifest. + * + * @param string $class + */ + public function loadClass($class) { + if ($path = $this->getManifest()->getItemPath($class)) { + require_once $path; + } + } + + /** + * Returns true if a class or interface name exists in the manifest. + * + * @param string $class + * @return bool + */ + public function classExists($class) { + return class_exists($class, false) || $this->getManifest()->getItemPath($class); + } + +} \ No newline at end of file diff --git a/manifest/ClassManifest.php b/manifest/ClassManifest.php new file mode 100644 index 000000000..e11633d1b --- /dev/null +++ b/manifest/ClassManifest.php @@ -0,0 +1,361 @@ + T_CLASS, + 1 => T_WHITESPACE, + 2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'), + 3 => T_WHITESPACE, + 4 => T_EXTENDS, + 5 => T_WHITESPACE, + 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14), + 7 => T_WHITESPACE, + 8 => T_IMPLEMENTS, + 9 => T_WHITESPACE, + 10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'), + 11 => array(T_WHITESPACE, 'optional' => true), + 12 => array(',', 'can_jump_to' => 10), + 13 => array(T_WHITESPACE, 'can_jump_to' => 10), + 14 => array(T_WHITESPACE, 'optional' => true), + 15 => '{', + )); + } + + /** + * @return TokenisedRegularExpression + */ + public static function get_interface_parser() { + return new TokenisedRegularExpression(array( + 0 => T_INTERFACE, + 1 => T_WHITESPACE, + 2 => array(T_STRING, 'can_jump_to' => 7, 'save_to' => 'interfaceName'), + 3 => T_WHITESPACE, + 4 => T_EXTENDS, + 5 => T_WHITESPACE, + 6 => array(T_STRING, 'save_to' => 'extends'), + 7 => array(T_WHITESPACE, 'optional' => true), + 8 => '{', + )); + } + + /** + * Constructs and initialises a new class manifest, either loading the data + * from the cache or re-scanning for classes. + * + * @param string $base The manifest base path. + * @param bool $includeTests Include the contents of "tests" directories. + * @param bool $forceRegen Force the manifest to be regenerated. + * @param bool $cache If the manifest is regenerated, cache it. + */ + public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) { + $this->base = $base; + $this->tests = $includeTests; + + $this->cache = SS_Cache::factory('SS_ClassManifest', 'Core', array( + 'automatic_serialization' => true, + 'lifetime' => null + )); + $this->cacheKey = $this->tests ? 'manifest_tests' : 'manifest'; + + if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) { + $this->classes = $data['classes']; + $this->descendants = $data['descendants']; + $this->interfaces = $data['interfaces']; + $this->implementors = $data['implementors']; + $this->configs = $data['configs']; + } else { + $this->regenerate($cache); + } + } + + /** + * Returns the file path to a class or interface if it exists in the + * manifest. + * + * @param string $name + * @return string|null + */ + public function getItemPath($name) { + $name = strtolower($name); + + if (isset($this->classes[$name])) { + return $this->classes[$name]; + } elseif (isset($this->interfaces[$name])) { + return $this->interfaces[$name]; + } + } + + /** + * Returns a map of lowercased class names to file paths. + * + * @return array + */ + public function getClasses() { + return $this->classes; + } + + /** + * Returns a lowercase array of all the class names in the manifest. + * + * @return array + */ + public function getClassNames() { + return array_keys($this->classes); + } + + /** + * Returns an array of all the descendant data. + * + * @return array + */ + public function getDescendants() { + return $this->descendants; + } + + /** + * Returns an array containing all the descendants (direct and indirect) + * of a class. + * + * @param string|object $class + * @return array + */ + public function getDescendantsOf($class) { + if (is_object($class)) { + $class = get_class($class); + } + + $lClass = strtolower($class); + + if (array_key_exists($lClass, $this->descendants)) { + return $this->descendants[$lClass]; + } else { + return array(); + } + } + + /** + * Returns a map of lowercased interface names to file locations. + * + * @return array + */ + public function getInterfaces() { + return $this->interfaces; + } + + /** + * Returns a map of lowercased interface names to the classes the implement + * them. + * + * @return array + */ + public function getImplementors() { + return $this->implementors; + } + + /** + * Returns an array containing the class names that implement a certain + * interface. + * + * @param string $interface + * @return array + */ + public function getImplementorsOf($interface) { + $interface = strtolower($interface); + + if (array_key_exists($interface, $this->implementors)) { + return $this->implementors[$interface]; + } else { + return array(); + } + } + + /** + * Returns an array of paths to module config files. + * + * @return array + */ + public function getConfigs() { + return $this->configs; + } + + /** + * Completely regenerates the manifest file. + * + * @param bool $cache Cache the result. + */ + public function regenerate($cache = true) { + $reset = array( + 'classes', 'roots', 'children', 'descendants', 'interfaces', + 'implementors', 'configs' + ); + + // Reset the manifest so stale info doesn't cause errors. + foreach ($reset as $reset) { + $this->$reset = array(); + } + + $finder = new ManifestFileFinder(); + $finder->setOptions(array( + 'name_regex' => '/\.php$/', + 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), + 'ignore_tests' => !$this->tests, + 'file_callback' => array($this, 'handleFile') + )); + $finder->find($this->base); + + foreach ($this->roots as $root) { + $this->coalesceDescendants($root); + } + + if ($cache) { + $data = array( + 'classes' => $this->classes, + 'descendants' => $this->descendants, + 'interfaces' => $this->interfaces, + 'implementors' => $this->implementors, + 'configs' => $this->configs + ); + $this->cache->save($data, $this->cacheKey); + } + } + + public function handleFile($basename, $pathname, $depth) { + if ($depth == 1 && $basename == self::CONF_FILE) { + $this->configs[] = $pathname; + return; + } + + $classes = null; + $interfaces = null; + + // The results of individual file parses are cached, since only a few + // files will have changed and TokenisedRegularExpression is quite + // slow. A combination of the file name and file contents hash are used, + // since just using the datetime lead to problems with upgrading. + $file = file_get_contents($pathname); + $key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5($file); + + if ($data = $this->cache->load($key)) { + $valid = ( + isset($data['classes']) && isset($data['interfaces']) + && is_array($data['classes']) && is_array($data['interfaces']) + ); + + if ($valid) { + $classes = $data['classes']; + $interfaces = $data['interfaces']; + } + } + + if (!$classes) { + $tokens = token_get_all($file); + $classes = self::get_class_parser()->findAll($tokens); + $interfaces = self::get_interface_parser()->findAll($tokens); + + $cache = array('classes' => $classes, 'interfaces' => $interfaces); + $this->cache->save($cache, $key, array('fileparse')); + } + + foreach ($classes as $class) { + $name = $class['className']; + $extends = isset($class['extends']) ? $class['extends'] : null; + $implements = isset($class['interfaces']) ? $class['interfaces'] : null; + + if (array_key_exists($name, $this->classes)) { + throw new Exception(sprintf( + 'There are two files containing the "%s" class: "%s" and "%s"', + $name, $this->classes[$name], $pathname + )); + } + + $this->classes[strtolower($name)] = $pathname; + + if ($extends) { + $extends = strtolower($extends); + + if (!isset($this->children[$extends])) { + $this->children[$extends] = array($name); + } else { + $this->children[$extends][] = $name; + } + } else { + $this->roots[] = $name; + } + + if ($implements) foreach ($implements as $interface) { + $interface = strtolower($interface); + + if (!isset($this->implementors[$interface])) { + $this->implementors[$interface] = array($name); + } else { + $this->implementors[$interface][] = $name; + } + } + } + + foreach ($interfaces as $interface) { + $this->interfaces[strtolower($interface['interfaceName'])] = $pathname; + } + } + + /** + * Recursively coalesces direct child information into full descendant + * information. + * + * @param string $class + * @return array + */ + protected function coalesceDescendants($class) { + $result = array(); + $lClass = strtolower($class); + + if (array_key_exists($lClass, $this->children)) { + $this->descendants[$lClass] = array(); + + foreach ($this->children[$lClass] as $class) { + $this->descendants[$lClass] = array_merge( + $this->descendants[$lClass], + array($class), + $this->coalesceDescendants($class) + ); + } + + return $this->descendants[$lClass]; + } else { + return array(); + } + } + +} \ No newline at end of file diff --git a/manifest/ManifestFileFinder.php b/manifest/ManifestFileFinder.php new file mode 100644 index 000000000..63e8e2969 --- /dev/null +++ b/manifest/ManifestFileFinder.php @@ -0,0 +1,65 @@ + false, + 'ignore_tests' => true, + 'min_depth' => 1 + ); + + public function acceptDir($basename, $pathname, $depth) { + // Skip over the assets directory in the site root. + if ($depth == 1 && $basename == ASSETS_DIR) { + return false; + } + + // Skip over any lang directories in the top level of the module. + if ($depth == 2 && $basename == self::LANG_DIR) { + return false; + } + + // If we're not in testing mode, then skip over the tests directory in + // the module root. + if ($this->getOption('ignore_tests') && $depth == 2 && $basename == self::TESTS_DIR) { + return false; + } + + // Ignore any directories which contain a _manifest_exclude file. + if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) { + return false; + } + + // Only include top level module directories which have a configuration + // _config.php file. However, if we're in themes mode then include + // the themes dir without a config file. + $lackingConfig = ( + $depth == 1 + && !($this->getOption('include_themes') && $basename == THEMES_DIR) + && !file_exists($pathname . '/' . self::CONFIG_FILE) + ); + + if ($lackingConfig) { + return false; + } + + return parent::acceptDir($basename, $pathname, $depth); + } + +} \ No newline at end of file diff --git a/manifest/TemplateLoader.php b/manifest/TemplateLoader.php new file mode 100644 index 000000000..10226f733 --- /dev/null +++ b/manifest/TemplateLoader.php @@ -0,0 +1,95 @@ +manifests[count($this->manifests) - 1]; + } + + /** + * @param SS_TemplateManifest $manifest + */ + public function pushManifest(SS_TemplateManifest $manifest) { + $this->manifests[] = $manifest; + } + + /** + * @return SS_TemplateManifest + */ + public function popManifest() { + return array_pop($this->manifests); + } + + /** + * Attempts to find possible candidate templates from a set of template + * names and a theme. + * + * The template names can be passed in as plain strings, or be in the + * format "type/name", where type is the type of template to search for + * (e.g. Includes, Layout). + * + * @param string|array $templates + * @param string $theme + * @return array + */ + public function findTemplates($templates, $theme = null) { + $result = array(); + + foreach ((array) $templates as $template) { + $found = false; + + if (strpos($template, '/')) { + list($type, $template) = explode('/', $template, 2); + } else { + $type = null; + } + + if ($candidates = $this->getManifest()->getTemplate($template)) { + if ($theme && isset($candidates['themes'][$theme])) { + $found = $candidates['themes'][$theme]; + } else { + unset($candidates['themes']); + $found = $candidates; + } + + if ($found) { + if ($type && isset($found[$type])) { + $found = array('main' => $found[$type]); + } + + $result = array_merge($found, $result); + } + } + } + + return $result; + } + +} \ No newline at end of file diff --git a/manifest/TemplateManifest.php b/manifest/TemplateManifest.php new file mode 100644 index 000000000..d1e29dace --- /dev/null +++ b/manifest/TemplateManifest.php @@ -0,0 +1,154 @@ +base = $base; + $this->tests = $includeTests; + + $this->cacheKey = $this->tests ? 'manifest_tests' : 'manifest'; + $this->forceRegen = $forceRegen; + + $this->cache = SS_Cache::factory('SS_TemplateManifest', 'Core', array( + 'automatic_serialization' => true, + 'lifetime' => null + )); + } + + /** + * Returns a map of all template information. The map is in the following + * format: + * + * + * array( + * 'moduletemplate' => array( + * 'main' => '/path/to/module/templates/Main.ss' + * ), + * 'include' => array( + * 'include' => '/path/to/module/templates/Includes/Include.ss' + * ), + * 'page' => array( + * 'themes' => array( + * 'blackcandy' => array( + * 'main' => '/path/to/theme/Page.ss' + * 'Layout' => '/path/to/theme/Layout/Page.ss' + * ) + * ) + * ) + * ) + * + * + * @return array + */ + public function getTemplates() { + if (!$this->inited) { + $this->init(); + } + + return $this->templates; + } + + /** + * Returns a set of possible candidate templates that match a certain + * template name. + * + * This is the same as extracting an individual array element from + * {@link SS_TemplateManifest::getTemplates()}. + * + * @param string $name + * @return array + */ + public function getTemplate($name) { + if (!$this->inited) { + $this->init(); + } + + $name = strtolower($name); + + if (array_key_exists($name, $this->templates)) { + return $this->templates[$name]; + } else { + return array(); + } + } + + /** + * Regenerates the manifest by scanning the base path. + * + * @param bool $cache + */ + public function regenerate($cache = true) { + $finder = new ManifestFileFinder(); + $finder->setOptions(array( + 'name_regex' => '/\.ss$/', + 'include_themes' => true, + 'ignore_tests' => !$this->tests, + 'file_callback' => array($this, 'handleFile') + )); + $finder->find($this->base); + + if ($cache) { + $this->cache->save($this->templates, $this->cacheKey); + } + + $this->inited = true; + } + + public function handleFile($basename, $pathname, $depth) { + if (strpos($pathname, $this->base . '/' . THEMES_DIR) === 0) { + $start = strlen($this->base . '/' . THEMES_DIR) + 1; + $theme = substr($pathname, $start); + $theme = substr($theme, 0, strpos($theme, '/')); + $theme = strtok($theme, '_'); + } else { + $theme = null; + } + + $type = basename(dirname($pathname)); + $name = strtolower(substr($basename, 0, -3)); + + if ($type == self::TEMPLATES_DIR) { + $type = 'main'; + } + + if ($theme) { + $this->templates[$name]['themes'][$theme][$type] = $pathname; + } else { + $this->templates[$name][$type] = $pathname; + } + } + + protected function init() { + if (!$this->forceRegen && $data = $this->cache->load($this->cacheKey)) { + $this->templates = $data; + $this->inited = true; + } else { + $this->regenerate(); + } + } + +} \ No newline at end of file diff --git a/core/TokenisedRegularExpression.php b/manifest/TokenisedRegularExpression.php similarity index 97% rename from core/TokenisedRegularExpression.php rename to manifest/TokenisedRegularExpression.php index 0fc65bcc0..dba58e22a 100644 --- a/core/TokenisedRegularExpression.php +++ b/manifest/TokenisedRegularExpression.php @@ -6,7 +6,7 @@ * @package sapphire * @subpackage core */ -class TokenisedRegularExpression extends Object { +class TokenisedRegularExpression { /** * The regular expression definition */ @@ -14,7 +14,6 @@ class TokenisedRegularExpression extends Object { function __construct($expression) { $this->expression = $expression; - parent::__construct(); } function findAll($tokens) { diff --git a/tests/ClassInfoTest.php b/tests/ClassInfoTest.php index 81ba1dd8f..53d261ef4 100644 --- a/tests/ClassInfoTest.php +++ b/tests/ClassInfoTest.php @@ -4,12 +4,18 @@ * @subpackage tests */ class ClassInfoTest extends SapphireTest { - + + public function testExists() { + $this->assertTrue(ClassInfo::exists('Object')); + $this->assertTrue(ClassInfo::exists('ClassInfoTest')); + $this->assertTrue(ClassInfo::exists('stdClass')); + } + function testSubclassesFor() { $this->assertEquals( ClassInfo::subclassesFor('ClassInfoTest_BaseClass'), array( - 0 => 'ClassInfoTest_BaseClass', + 'ClassInfoTest_BaseClass' => 'ClassInfoTest_BaseClass', 'ClassInfoTest_ChildClass' => 'ClassInfoTest_ChildClass', 'ClassInfoTest_GrandChildClass' => 'ClassInfoTest_GrandChildClass' ), @@ -33,10 +39,62 @@ class ClassInfoTest extends SapphireTest { // 'ClassInfo::classes_for_folder() returns additional classes not matching the filename' // ); } - + + /** + * @covers ClassInfo::baseDataClass() + */ + public function testBaseDataClass() { + $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_BaseClass')); + $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_ChildClass')); + $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GrandChildClass')); + + $this->setExpectedException('Exception'); + ClassInfo::baseDataClass('DataObject'); + } + + /** + * @covers ClassInfo::ancestry() + */ + public function testAncestry() { + $ancestry = ClassInfo::ancestry('SiteTree'); + $expect = ArrayLib::valuekey(array( + 'Object', + 'ViewableData', + 'DataObject', + 'SiteTree' + )); + $this->assertEquals($expect, $ancestry); + + $ancestry = ClassInfo::ancestry('SiteTree', true); + $this->assertEquals(array('SiteTree' => 'SiteTree'), $ancestry); + + $this->setExpectedException('Exception'); + ClassInfo::ancestry(42); + } + + /** + * @covers ClassInfo::dataClassesFor() + */ + public function testDataClassesFor() { + $expect = array( + 'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass', + 'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields' + ); + + $classes = array( + 'ClassInfoTest_BaseDataClass', + 'ClassInfoTest_NoFields', + 'ClassInfoTest_HasFields' + ); + + foreach ($classes as $class) { + $this->assertEquals($expect, ClassInfo::dataClassesFor($class)); + } + } + } -class ClassInfoTest_BaseClass { +class ClassInfoTest_BaseClass extends DataObject { } @@ -47,4 +105,11 @@ class ClassInfoTest_ChildClass extends ClassInfoTest_BaseClass { class ClassInfoTest_GrandChildClass extends ClassInfoTest_ChildClass { } -?> \ No newline at end of file + +class ClassInfoTest_BaseDataClass extends DataObject { + public static $db = array('Title' => 'Varchar'); +} +class ClassInfoTest_NoFields extends ClassInfoTest_BaseDataClass {} +class ClassInfoTest_HasFields extends ClassInfoTest_NoFields { + public static $db = array('Description' => 'Varchar'); +} diff --git a/tests/FullTestSuite.php b/tests/FullTestSuite.php index 77e2dc6f9..a1b04e06b 100644 --- a/tests/FullTestSuite.php +++ b/tests/FullTestSuite.php @@ -47,7 +47,7 @@ class FullTestSuite { * @return Array */ public static function get_all_tests() { - ManifestBuilder::load_test_manifest(); + TestRunner::use_test_manifest(); $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); diff --git a/tests/ManifestBuilderTest.fixture.inc b/tests/ManifestBuilderTest.fixture.inc deleted file mode 100644 index 4b0955ae1..000000000 --- a/tests/ManifestBuilderTest.fixture.inc +++ /dev/null @@ -1,111 +0,0 @@ - << << -PHP -, - 'sapphire/subdir/', - 'sapphire/subdir/SubDirClass.php' => << <<assertEquals("$baseFolder/sapphire/MyClass.php", $manifestInfo['globals']['_CLASS_MANIFEST']['myclass']); - $this->assertEquals("$baseFolder/sapphire/subdir/SubDirClass.php", $manifestInfo['globals']['_CLASS_MANIFEST']['subdirclass']); - $this->assertNotContains('OtherFile', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - - $this->assertContains('MyClass', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertContains('MyClass_Other', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertContains('MyClass_Final', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertContains('MyClass_ClassBetweenTwoStrings', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - - // Check aspects of PHP file - $manifest = ManifestBuilder::generate_php_file($manifestInfo); - // Debug::message($manifest); - $this->assertEquals(1, preg_match('/^<\?php/', $manifest), "Starts with assertEquals(1, preg_match('/\$_CLASS_MANIFEST\s*=\s*array/m', $manifest), "\$_CLASS_MANIFEST exists"); - $this->assertEquals(1, preg_match('/\$_TEMPLATE_MANIFEST\s*=\s*array/m', $manifest), "\$_TEMPLATE_MANIFEST exists"); - $this->assertEquals(1, preg_match('/\$_CSS_MANIFEST\s*=\s*array/m', $manifest), "\$_CSS_MANIFEST exists"); - $this->assertEquals(1, preg_match('/\$_ALL_CLASSES\s*=\s*array/m', $manifest), "\$_ALL_CLASSES exists"); - - $this->assertEquals(1, preg_match('/require_once\("[^"]+rahbeast\/_config.php"\);/i', $manifest), "rahbeast/_config.php included"); - $this->assertEquals(1, preg_match('/require_once\("[^"]+sapphire\/_config.php"\);/i', $manifest), "sapphire/_config.php included"); - } - - function testManifestIgnoresClassesInComments() { - $baseFolder = TEMP_FOLDER . '/manifest-test-asdfasdfasdf'; - global $project; - - $manifestInfo = ManifestBuilder::get_manifest_info($baseFolder); - - /* Our fixture defines the class MyClass_InComment inside a comment, so it shouldn't be included in the class manifest. */ - $this->assertNotContains('myclass_incomment', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - $this->assertNotContains('MyClass_InComment', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertNotContains('MyClass_InComment', array_keys($manifestInfo['globals']['_ALL_CLASSES']['parents'])); - - /* Our fixture defines the class MyClass_InSlashSlashComment inside a //-style comment, so it shouldn't be included in the class manifest. */ - $this->assertNotContains('myclass_inslashslashcomment', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - $this->assertNotContains('MyClass_InSlashSlashComment', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertNotContains('MyClass_InSlashSlashComment', array_keys($manifestInfo['globals']['_ALL_CLASSES']['parents'])); - } - - function testManifestIgnoresClassesInStrings() { - $baseFolder = TEMP_FOLDER . '/manifest-test-asdfasdfasdf'; - $manifestInfo = ManifestBuilder::get_manifest_info($baseFolder); - - /* If a class defintion is listed in a single quote string, then it shouldn't be inlcuded. Here we have put a class definition for MyClass_InSingleQuoteString inside a single-quoted string */ - $this->assertNotContains('myclass_insinglequotestring', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - $this->assertNotContains('MyClass_InSingleQuoteString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertNotContains('MyClass_InSingleQuoteString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['parents'])); - - /* Ditto for double quotes. Here we have put a class definition for MyClass_InDoubleQuoteString inside a double-quoted string. */ - $this->assertNotContains('myclass_indoublequotestring', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - $this->assertNotContains('MyClass_InDoubleQuoteString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertNotContains('MyClass_InDoubleQuoteString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['parents'])); - - /* Finally, we need to ensure that class definitions inside heredoc strings aren't included. Here, we have defined the class MyClass_InHeredocString inside a heredoc string. */ - $this->assertNotContains('myclass_inheredocstring', array_keys($manifestInfo['globals']['_CLASS_MANIFEST'])); - $this->assertNotContains('MyClass_InHeredocString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - $this->assertNotContains('MyClass_InHeredocString', array_keys($manifestInfo['globals']['_ALL_CLASSES']['parents'])); - } - - - function testClassNamesDontHaveToBeTheSameAsFileNames() { - $baseFolder = TEMP_FOLDER . '/manifest-test-asdfasdfasdf'; - $manifestInfo = ManifestBuilder::get_manifest_info($baseFolder); - - $this->assertContains('BaseClass', array_keys($manifestInfo['globals']['_ALL_CLASSES']['exists'])); - } - - protected $originalClassManifest, $originalProject, $originalAllClasses; - protected static $test_fixture_project; - - function setUp() { - parent::setUp(); - - // Trick the auto-loder into loading this class before we muck with the manifest - new TokenisedRegularExpression(null); - - include('tests/ManifestBuilderTest.fixture.inc'); - - // Build the fixture specified above - $baseFolder = TEMP_FOLDER . '/manifest-test-asdfasdfasdf/'; - - if(file_exists($baseFolder)) Filesystem::removeFolder($baseFolder); - mkdir($baseFolder); - - foreach($filesystemFixture as $i => $item) { - if(is_numeric($i)) { - $itemContent = null; - } else { - $itemContent = $item; - $item = $i; - } - - // Directory - if(substr($item,-1) == '/') { - mkdir($baseFolder . $item); - } else { - touch($baseFolder . $item); - if($itemContent) { - $fh = fopen($baseFolder . $item, 'wb'); - fwrite($fh, $itemContent); - fclose($fh); - } - } - } - - global $_CLASS_MANIFEST, $_ALL_CLASSES, $project; - - $this->originalAllClasses = $_ALL_CLASSES; - $this->originalClassManifest = $_CLASS_MANIFEST; - $this->originalProject = $project; - - // Because it's difficult to run multiple tests on a piece of code that uses require_once, we keep a copy of the - // $project value. - if(self::$test_fixture_project) $project = self::$test_fixture_project; - - global $project; - } - - function testThemeRetrieval() { - $ds = DIRECTORY_SEPARATOR; - $testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes'; - - if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir); - - mkdir($testThemeBaseDir); - mkdir($testThemeBaseDir . $ds . 'blackcandy'); - mkdir($testThemeBaseDir . $ds . 'blackcandy_blog'); - mkdir($testThemeBaseDir . $ds . 'darkshades'); - mkdir($testThemeBaseDir . $ds . 'darkshades_blog'); - - $this->assertEquals(array( - 'blackcandy' => 'blackcandy', - 'darkshades' => 'darkshades' - ), ManifestBuilder::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes'); - - $this->assertEquals(array( - 'blackcandy' => 'blackcandy', - 'blackcandy_blog' => 'blackcandy_blog', - 'darkshades' => 'darkshades', - 'darkshades_blog' => 'darkshades_blog' - ), ManifestBuilder::get_themes($testThemeBaseDir, true), 'Our test theme directory contains 2 themes and 2 sub-themes'); - - // Remove all the test themes we created - Filesystem::removeFolder($testThemeBaseDir); - } - - function tearDown() { - global $_CLASS_MANIFEST, $_ALL_CLASSES, $project; - - if(!self::$test_fixture_project) self::$test_fixture_project = $project; - - $project = $this->originalProject; - $_CLASS_MANIFEST = $this->originalClassManifest; - $_ALL_CLASSES = $this->originalAllClasses; - - // Kill the folder after we're done - $baseFolder = TEMP_FOLDER . '/manifest-test-asdfasdfasdf/'; - Filesystem::removeFolder($baseFolder); - - parent::tearDown(); - } - -} - -?> \ No newline at end of file diff --git a/tests/SSViewerTest.php b/tests/SSViewerTest.php index 7306947b5..f4ecef103 100644 --- a/tests/SSViewerTest.php +++ b/tests/SSViewerTest.php @@ -499,6 +499,38 @@ after') ) ); } + + /** + * @covers SSViewer::get_themes() + */ + function testThemeRetrieval() { + $ds = DIRECTORY_SEPARATOR; + $testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes'; + + if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir); + + mkdir($testThemeBaseDir); + mkdir($testThemeBaseDir . $ds . 'blackcandy'); + mkdir($testThemeBaseDir . $ds . 'blackcandy_blog'); + mkdir($testThemeBaseDir . $ds . 'darkshades'); + mkdir($testThemeBaseDir . $ds . 'darkshades_blog'); + + $this->assertEquals(array( + 'blackcandy' => 'blackcandy', + 'darkshades' => 'darkshades' + ), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes'); + + $this->assertEquals(array( + 'blackcandy' => 'blackcandy', + 'blackcandy_blog' => 'blackcandy_blog', + 'darkshades' => 'darkshades', + 'darkshades_blog' => 'darkshades_blog' + ), SSViewer::get_themes($testThemeBaseDir, true), 'Our test theme directory contains 2 themes and 2 sub-themes'); + + // Remove all the test themes we created + Filesystem::removeFolder($testThemeBaseDir); + } + } /** diff --git a/tests/TokenisedRegularExpressionTest.php b/tests/TokenisedRegularExpressionTest.php index a711958ad..473b5710b 100644 --- a/tests/TokenisedRegularExpressionTest.php +++ b/tests/TokenisedRegularExpressionTest.php @@ -49,7 +49,7 @@ PHP } function testClassDefParser() { - $parser = ManifestBuilder::getClassDefParser(); + $parser = SS_ClassManifest::get_class_parser(); $tokens = $this->getTokens(); @@ -79,7 +79,7 @@ PHP } function testInterfaceDefParser() { - $parser = ManifestBuilder::getInterfaceDefParser(); + $parser = SS_ClassManifest::get_interface_parser(); $tokens = $this->getTokens(); diff --git a/tests/api/RestfulServiceTest.php b/tests/api/RestfulServiceTest.php index 2387e6354..5ea4f0cf2 100644 --- a/tests/api/RestfulServiceTest.php +++ b/tests/api/RestfulServiceTest.php @@ -94,13 +94,13 @@ class RestfulServiceTest extends SapphireTest { */ function testIncorrectData() { $connection = new RestfulService(Director::absoluteBaseURL(), 0); - $test1 = $connection->request('RestfulServiceTest_Controller/invalid?usetestmanifest=1&flush=1'); + $test1 = $connection->request('RestfulServiceTest_Controller/invalid'); $test1->xpath("\\fail"); } function testHttpErrorWithoutCache() { $connection = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL(), 0); - $response = $connection->request('RestfulServiceTest_Controller/httpErrorWithoutCache?usetestmanifest=1&flush=1'); + $response = $connection->request('RestfulServiceTest_Controller/httpErrorWithoutCache'); $this->assertEquals(400, $response->getStatusCode()); $this->assertFalse($response->getCachedBody()); @@ -110,7 +110,7 @@ class RestfulServiceTest extends SapphireTest { function testHttpErrorWithCache() { $subUrl = 'RestfulServiceTest_Controller/httpErrorWithCache?usetestmanifest=1&flush=1'; - $connection = new RestfulService(Director::absoluteBaseURL(), 0); + $connection = new RestfulServiceTest_MockErrorService(Director::absoluteBaseURL(), 0); $this->createFakeCachedResponse($connection, $subUrl); $response = $connection->request($subUrl); @@ -271,4 +271,15 @@ class RestfulServiceTest_MockRestfulService extends RestfulService { return $response; } -} \ No newline at end of file +} + +/** + * A mock service that returns a 400 error for requests. + */ +class RestfulServiceTest_MockErrorService extends RestfulService { + + public function curlRequest() { + return new RestfulService_Response('HTTP Error', 400); + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php old mode 100755 new mode 100644 index be9aef024..ff5d964c5 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -32,4 +32,4 @@ if(!class_exists('Object')) { $_SERVER['REQUEST_URI'] = BASE_URL . '/dev'; -ManifestBuilder::load_test_manifest(); \ No newline at end of file +TestRunner::use_test_manifest(); \ No newline at end of file diff --git a/tests/filesystem/FileFinderTest.php b/tests/filesystem/FileFinderTest.php new file mode 100644 index 000000000..bb59287b0 --- /dev/null +++ b/tests/filesystem/FileFinderTest.php @@ -0,0 +1,114 @@ +base = dirname(__FILE__) . '/fixtures/filefinder'; + parent::__construct(); + } + + public function testBasicOperation() { + $this->assertFinderFinds(new SS_FileFinder(), array( + 'file1.txt', + 'file2.txt', + 'dir1/dir1file1.txt', + 'dir1/dir1file2.txt', + 'dir1/dir2/dir2file1.txt', + 'dir1/dir2/dir3/dir3file1.txt' + )); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidOptionThrowsException() { + $finder = new SS_FileFinder(); + $finder->setOption('this_doesnt_exist', 'ok'); + } + + public function testFilenameRegex() { + $finder = new SS_FileFinder(); + $finder->setOption('name_regex', '/file2\.txt$/'); + + $this->assertFinderFinds( + $finder, + array( + 'file2.txt', + 'dir1/dir1file2.txt'), + 'The finder only returns files matching the name regex.'); + } + + public function testIgnoreFiles() { + $finder = new SS_FileFinder(); + $finder->setOption('ignore_files', array('file1.txt', 'dir1file1.txt', 'dir2file1.txt')); + + $this->assertFinderFinds( + $finder, + array( + 'file2.txt', + 'dir1/dir1file2.txt', + 'dir1/dir2/dir3/dir3file1.txt'), + 'The finder ignores files with the basename in the ignore_files setting.'); + } + + public function testIgnoreDirs() { + $finder = new SS_FileFinder(); + $finder->setOption('ignore_dirs', array('dir2')); + + $this->assertFinderFinds( + $finder, + array( + 'file1.txt', + 'file2.txt', + 'dir1/dir1file1.txt', + 'dir1/dir1file2.txt'), + 'The finder ignores directories in ignore_dirs.'); + } + + public function testMinDepth() { + $finder = new SS_FileFinder(); + $finder->setOption('min_depth', 2); + + $this->assertFinderFinds( + $finder, + array( + 'dir1/dir2/dir2file1.txt', + 'dir1/dir2/dir3/dir3file1.txt'), + 'The finder respects the min depth setting.'); + } + + public function testMaxDepth() { + $finder = new SS_FileFinder(); + $finder->setOption('max_depth', 1); + + $this->assertFinderFinds( + $finder, + array( + 'file1.txt', + 'file2.txt', + 'dir1/dir1file1.txt', + 'dir1/dir1file2.txt'), + 'The finder respects the max depth setting.'); + } + + public function assertFinderFinds($finder, $expect, $message = null) { + $found = $finder->find($this->base); + + foreach ($expect as $k => $file) { + $expect[$k] = "{$this->base}/$file"; + } + + sort($expect); + sort($found); + + $this->assertEquals($expect, $found, $message); + } + +} \ No newline at end of file diff --git a/tests/filesystem/fixtures/filefinder/dir1/dir1file1.txt b/tests/filesystem/fixtures/filefinder/dir1/dir1file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/filesystem/fixtures/filefinder/dir1/dir1file2.txt b/tests/filesystem/fixtures/filefinder/dir1/dir1file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/filesystem/fixtures/filefinder/dir1/dir2/dir2file1.txt b/tests/filesystem/fixtures/filefinder/dir1/dir2/dir2file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/filesystem/fixtures/filefinder/dir1/dir2/dir3/dir3file1.txt b/tests/filesystem/fixtures/filefinder/dir1/dir2/dir3/dir3file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/filesystem/fixtures/filefinder/file1.txt b/tests/filesystem/fixtures/filefinder/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/filesystem/fixtures/filefinder/file2.txt b/tests/filesystem/fixtures/filefinder/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/i18n/_fakewebroot/i18nothermodule/_config.php b/tests/i18n/_fakewebroot/i18nothermodule/_config.php index 00c807b66..b3d9bbc7f 100644 --- a/tests/i18n/_fakewebroot/i18nothermodule/_config.php +++ b/tests/i18n/_fakewebroot/i18nothermodule/_config.php @@ -1,3 +1 @@ \ No newline at end of file diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index 452f84e48..864ae60a5 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -29,41 +29,17 @@ class i18nTest extends SapphireTest { $this->alternateBasePath = Director::baseFolder() . "/sapphire/tests/i18n/_fakewebroot"; $this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot'; FileSystem::makeFolder($this->alternateBaseSavePath); - - // SSViewer and ManifestBuilder don't support different webroots, hence we set the paths manually - global $_CLASS_MANIFEST; - $_CLASS_MANIFEST['i18nTestModule'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; - $_CLASS_MANIFEST['i18nTestModule_Addition'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; - $_CLASS_MANIFEST['i18nTestModuleDecorator'] = $this->alternateBasePath . '/i18nothermodule/code/i18nTestModuleDecorator.php'; - - global $_ALL_CLASSES; - $_ALL_CLASSES['parents']['i18nTestModule'] = array('DataObject'=>'DataObject','Object'=>'Object'); - $_ALL_CLASSES['parents']['i18nTestModule_Addition'] = array('Object'=>'Object'); - $_ALL_CLASSES['parents']['i18nTestModuleDecorator'] = array('DataObjectDecorator'=>'DataObjectDecorator','Object'=>'Object'); - global $_TEMPLATE_MANIFEST; - $_TEMPLATE_MANIFEST['i18nTestModule.ss'] = array( - 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', - 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', - ); - $_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss'] = array( - 'Includes' => $this->alternateBasePath . '/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss', - ); - + // Push a template loader running from the fake webroot onto the stack. + $manifest = new SS_TemplateManifest($this->alternateBasePath, false, true); + $manifest->regenerate(false); + SS_TemplateLoader::instance()->pushManifest($manifest); + $this->originalLocale = i18n::get_locale(); } function tearDown() { - //FileSystem::removeFolder($this->tmpBasePath); - - global $_CLASS_MANIFEST; - unset($_CLASS_MANIFEST['i18nTestModule']); - unset($_CLASS_MANIFEST['i18nTestModule_Addition']); - - global $_TEMPLATE_MANIFEST; - unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); - unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); - + SS_TemplateLoader::instance()->popManifest(); i18n::set_locale($this->originalLocale); parent::tearDown(); diff --git a/tests/i18n/i18nTextCollectorTest.php b/tests/i18n/i18nTextCollectorTest.php index 2610314e6..a383734b5 100644 --- a/tests/i18n/i18nTextCollectorTest.php +++ b/tests/i18n/i18nTextCollectorTest.php @@ -18,53 +18,28 @@ class i18nTextCollectorTest extends SapphireTest { */ protected $alternateBasePath; + protected $manifest; + function setUp() { parent::setUp(); $this->alternateBasePath = Director::baseFolder() . "/sapphire/tests/i18n/_fakewebroot"; $this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot'; FileSystem::makeFolder($this->alternateBaseSavePath); - - // SSViewer and ManifestBuilder don't support different webroots, hence we set the paths manually - global $_CLASS_MANIFEST; - $_CLASS_MANIFEST['i18nTestModule'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; - $_CLASS_MANIFEST['i18nTestModule_Addition'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; - $_CLASS_MANIFEST['i18nTestModuleDecorator'] = $this->alternateBasePath . '/i18nothermodule/code/i18nTestModuleDecorator.php'; - - global $_ALL_CLASSES; - $_ALL_CLASSES['parents']['i18nTestModule'] = array('DataObject'=>'DataObject','Object'=>'Object'); - $_ALL_CLASSES['parents']['i18nTestModule_Addition'] = array('Object'=>'Object'); - $_ALL_CLASSES['parents']['i18nTestModuleDecorator'] = array('DataObjectDecorator'=>'DataObjectDecorator','Object'=>'Object'); - global $_TEMPLATE_MANIFEST; - $_TEMPLATE_MANIFEST['i18nTestModule.ss'] = array( - 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', - 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', - ); - $_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss'] = array( - 'Includes' => $this->alternateBasePath . '/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss', + // Push a class and template loader running from the fake webroot onto + // the stack. + $this->manifest = new SS_ClassManifest( + $this->alternateBasePath, false, true, false ); - $_TEMPLATE_MANIFEST['i18nTestTheme1.ss'] = array( - 'main' => $this->alternateBasePath . '/themes/testtheme1/templates/i18nTestTheme1.ss', - 'Layout' => $this->alternateBasePath . '/themes/testtheme1/templates/Layout/i18nTestTheme1.ss', - ); - $_TEMPLATE_MANIFEST['i18nTestTheme1Include.ss'] = array( - 'Includes' => $this->alternateBasePath . '/themes/testtheme1/templates/Includes/i18nTestTheme1Include.ss', - ); + $manifest = new SS_TemplateManifest($this->alternateBasePath, false, true); + $manifest->regenerate(false); + SS_TemplateLoader::instance()->pushManifest($manifest); } function tearDown() { - //FileSystem::removeFolder($this->tmpBasePath); - - global $_CLASS_MANIFEST; - unset($_CLASS_MANIFEST['i18nTestModule']); - unset($_CLASS_MANIFEST['i18nTestModule_Addition']); - - global $_TEMPLATE_MANIFEST; - unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); - unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); - + SS_TemplateLoader::instance()->popManifest(); parent::tearDown(); } @@ -413,6 +388,9 @@ PHP; function testCollectFromThemesTemplates() { $c = new i18nTextCollector(); + $theme = SSViewer::current_theme(); + SSViewer::set_theme('testtheme1'); + $templateFilePath = $this->alternateBasePath . '/themes/testtheme1/templates/Layout/i18nTestTheme1.ss'; $html = file_get_contents($templateFilePath); $matches = $c->collectFromTemplate($html, 'themes/testtheme1', 'i18nTestTheme1.ss'); @@ -462,6 +440,8 @@ PHP; $matches['i18nTestTheme1Include.ss.SPRINTFINCLUDENONAMESPACE'], array('Theme1 My include replacement no namespace: %s', null, null) ); + + SSViewer::set_theme($theme); } function testCollectFromFilesystemAndWriteMasterTables() { diff --git a/tests/manifest/ClassManifestTest.php b/tests/manifest/ClassManifestTest.php new file mode 100644 index 000000000..701aa8cac --- /dev/null +++ b/tests/manifest/ClassManifestTest.php @@ -0,0 +1,114 @@ +base = dirname(__FILE__) . '/fixtures/classmanifest'; + $this->manifest = new SS_ClassManifest($this->base, false, true, false); + $this->manifestTests = new SS_ClassManifest($this->base, true, true, false); + } + + public function testGetItemPath() { + $expect = array( + 'CLASSA' => 'module/classes/ClassA.php', + 'ClassA' => 'module/classes/ClassA.php', + 'classa' => 'module/classes/ClassA.php', + 'INTERFACEA' => 'module/interfaces/InterfaceA.php', + 'InterfaceA' => 'module/interfaces/InterfaceA.php', + 'interfacea' => 'module/interfaces/InterfaceA.php' + ); + + foreach ($expect as $name => $path) { + $this->assertEquals("{$this->base}/$path", $this->manifest->getItemPath($name)); + } + } + + public function testGetClasses() { + $expect = array( + 'classa' => "{$this->base}/module/classes/ClassA.php", + 'classb' => "{$this->base}/module/classes/ClassB.php", + 'classc' => "{$this->base}/module/classes/ClassC.php", + 'classd' => "{$this->base}/module/classes/ClassD.php" + ); + $this->assertEquals($expect, $this->manifest->getClasses()); + } + + public function testGetClassNames() { + $this->assertEquals( + array('classa', 'classb', 'classc', 'classd'), + $this->manifest->getClassNames()); + } + + public function testGetDescendants() { + $expect = array( + 'classa' => array('ClassC', 'ClassD'), + 'classc' => array('ClassD') + ); + $this->assertEquals($expect, $this->manifest->getDescendants()); + } + + public function testGetDescendantsOf() { + $expect = array( + 'CLASSA' => array('ClassC', 'ClassD'), + 'classa' => array('ClassC', 'ClassD'), + 'CLASSC' => array('ClassD'), + 'classc' => array('ClassD') + ); + + foreach ($expect as $class => $desc) { + $this->assertEquals($desc, $this->manifest->getDescendantsOf($class)); + } + } + + public function testGetInterfaces() { + $expect = array( + 'interfacea' => "{$this->base}/module/interfaces/InterfaceA.php", + 'interfaceb' => "{$this->base}/module/interfaces/InterfaceB.php" + ); + $this->assertEquals($expect, $this->manifest->getInterfaces()); + } + + public function testGetImplementors() { + $expect = array( + 'interfacea' => array('ClassB'), + 'interfaceb' => array('ClassC') + ); + $this->assertEquals($expect, $this->manifest->getImplementors()); + } + + public function testGetImplementorsOf() { + $expect = array( + 'INTERFACEA' => array('ClassB'), + 'interfacea' => array('ClassB'), + 'INTERFACEB' => array('ClassC'), + 'interfaceb' => array('ClassC') + ); + + foreach ($expect as $interface => $impl) { + $this->assertEquals($impl, $this->manifest->getImplementorsOf($interface)); + } + } + + public function testGetConfigs() { + $expect = array("{$this->base}/module/_config.php"); + $this->assertEquals($expect, $this->manifest->getConfigs()); + $this->assertEquals($expect, $this->manifestTests->getConfigs()); + } + + public function testTestManifestIncludesTestClasses() { + $this->assertNotContains('testclassa', array_keys($this->manifest->getClasses())); + $this->assertContains('testclassa', array_keys($this->manifestTests->getClasses())); + } + +} \ No newline at end of file diff --git a/tests/manifest/ManifestFileFinderTest.php b/tests/manifest/ManifestFileFinderTest.php new file mode 100644 index 000000000..a5045e52f --- /dev/null +++ b/tests/manifest/ManifestFileFinderTest.php @@ -0,0 +1,61 @@ +base = dirname(__FILE__) . '/fixtures/manifestfilefinder'; + parent::__construct(); + } + + public function assertFinderFinds($finder, $expect, $message = null) { + $found = $finder->find($this->base); + + foreach ($expect as $k => $file) { + $expect[$k] = "{$this->base}/$file"; + } + + sort($expect); + sort($found); + + $this->assertEquals($expect, $found, $message); + } + + public function testBasicOperation() { + $finder = new ManifestFileFinder(); + $finder->setOption('name_regex', '/\.txt$/'); + + $this->assertFinderFinds($finder, array( + 'module/module.txt' + )); + } + + public function testIgnoreTests() { + $finder = new ManifestFileFinder(); + $finder->setOption('name_regex', '/\.txt$/'); + $finder->setOption('ignore_tests', false); + + $this->assertFinderFinds($finder, array( + 'module/module.txt', + 'module/tests/tests.txt' + )); + } + + public function testIncludeThemes() { + $finder = new ManifestFileFinder(); + $finder->setOption('name_regex', '/\.txt$/'); + $finder->setOption('include_themes', true); + + $this->assertFinderFinds($finder, array( + 'module/module.txt', + 'themes/themes.txt' + )); + } + +} \ No newline at end of file diff --git a/tests/manifest/TemplateLoaderTest.php b/tests/manifest/TemplateLoaderTest.php new file mode 100644 index 000000000..9f180803a --- /dev/null +++ b/tests/manifest/TemplateLoaderTest.php @@ -0,0 +1,46 @@ +regenerate(false); + $loader->pushManifest($manifest); + + $expectPage = array( + 'main' => "$base/module/templates/Page.ss", + 'Layout' => "$base/module/templates/Layout/Page.ss" + ); + $expectPageThemed = array( + 'main' => "$base/themes/theme/templates/Page.ss", + 'Layout' => "$base/themes/theme/templates/Layout/Page.ss" + ); + + $this->assertEquals($expectPage, $loader->findTemplates('Page')); + $this->assertEquals($expectPage, $loader->findTemplates(array('Foo', 'Page'))); + $this->assertEquals($expectPage, $loader->findTemplates('PAGE')); + $this->assertEquals($expectPageThemed, $loader->findTemplates('Page', 'theme')); + + $expectPageLayout = array('main' => "$base/module/templates/Layout/Page.ss"); + $expectPageLayoutThemed = array('main' => "$base/themes/theme/templates/Layout/Page.ss"); + + $this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/Page')); + $this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/PAGE')); + $this->assertEquals($expectPageLayoutThemed, $loader->findTemplates('Layout/Page', 'theme')); + + $expectCustomPage = array( + 'main' => "$base/module/templates/Page.ss", + 'Layout' => "$base/module/templates/Layout/CustomPage.ss" + ); + $this->assertEquals($expectCustomPage, $loader->findTemplates(array('CustomPage', 'Page'))); + } + +} \ No newline at end of file diff --git a/tests/manifest/TemplateManifestTest.php b/tests/manifest/TemplateManifestTest.php new file mode 100644 index 000000000..444a233b2 --- /dev/null +++ b/tests/manifest/TemplateManifestTest.php @@ -0,0 +1,98 @@ +base = dirname(__FILE__) . '/fixtures/templatemanifest'; + $this->manifest = new SS_TemplateManifest($this->base); + $this->manifestTests = new SS_TemplateManifest($this->base, true); + + $this->manifest->regenerate(false); + $this->manifestTests->regenerate(false); + } + + public function testGetTemplates() { + $expect = array( + 'root' => array( + 'module' => "{$this->base}/module/Root.ss" + ), + 'page' => array( + 'main' => "{$this->base}/module/templates/Page.ss", + 'Layout' => "{$this->base}/module/templates/Layout/Page.ss", + 'themes' => array('theme' => array( + 'main' => "{$this->base}/themes/theme/templates/Page.ss", + 'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss" + )) + ), + 'custompage' => array( + 'Layout' => "{$this->base}/module/templates/Layout/CustomPage.ss" + ), + 'subfolder' => array( + 'main' => "{$this->base}/module/subfolder/templates/Subfolder.ss" + ), + 'include' => array('themes' => array( + 'theme' => array( + 'Includes' => "{$this->base}/themes/theme/templates/Includes/Include.ss" + ) + )) + ); + + $expectTests = $expect; + $expectTests['test'] = array( + 'main' => "{$this->base}/module/tests/templates/Test.ss" + ); + + $manifest = $this->manifest->getTemplates(); + $manifestTests = $this->manifestTests->getTemplates(); + + ksort($expect); + ksort($expectTests); + ksort($manifest); + ksort($manifestTests); + + $this->assertEquals( + $expect, $manifest, + 'All templates are correctly loaded in the manifest.' + ); + + $this->assertEquals( + $expectTests, $manifestTests, + 'The test manifest is the same, but includes test templates.' + ); + } + + public function testGetTemplate() { + $expectPage = array( + 'main' => "{$this->base}/module/templates/Page.ss", + 'Layout' => "{$this->base}/module/templates/Layout/Page.ss", + 'themes' => array('theme' => array( + 'main' => "{$this->base}/themes/theme/templates/Page.ss", + 'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss" + )) + ); + + $expectTests = array( + 'main' => "{$this->base}/module/tests/templates/Test.ss" + ); + + $this->assertEquals($expectPage, $this->manifest->getTemplate('Page')); + $this->assertEquals($expectPage, $this->manifest->getTemplate('PAGE')); + $this->assertEquals($expectPage, $this->manifestTests->getTemplate('Page')); + $this->assertEquals($expectPage, $this->manifestTests->getTemplate('PAGE')); + + $this->assertEquals(array(), $this->manifest->getTemplate('Test')); + $this->assertEquals($expectTests, $this->manifestTests->getTemplate('Test')); + } + +} \ No newline at end of file diff --git a/tests/manifest/fixtures/classmanifest/module/_config.php b/tests/manifest/fixtures/classmanifest/module/_config.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/manifest/fixtures/classmanifest/module/classes/ClassA.php b/tests/manifest/fixtures/classmanifest/module/classes/ClassA.php new file mode 100644 index 000000000..375e6bb56 --- /dev/null +++ b/tests/manifest/fixtures/classmanifest/module/classes/ClassA.php @@ -0,0 +1,5 @@ +