Version: current

      1 <?php
      2 /**
      3  * Define a constant for the name of the manifest file
      4  */
      5 if(!defined('MANIFEST_FILE')) define("MANIFEST_FILE", TEMP_FOLDER . "/manifest-" . basename($_SERVER['SCRIPT_FILENAME']));
      6
      7 /**
      8  * The ManifestBuilder class generates the manifest file and keeps it fresh.
      9  *
     10  * The manifest file is a PHP include that contains global variables that
     11  * represent the collected contents of the application:
     12  *   - all classes ({@link __autoload()})
     13  *   - all templates ({@link SSViewer})
     14  *   - all _config.php files
     15  *
     16  * Traversing the filesystem to collect this information on everypage.
     17  * This information is cached so that it need not be regenerated on every
     18  * pageview.
     19  *
     20  * @see main.php, __autoload(), SSViewer, Requirements::themedCSS()
     21  * @package sapphire
     22  * @subpackage core
     23  */
     24 class ManifestBuilder {
     25
     26     static $restrict_to_modules = array();
     27     static $extendsArray = array();
     28     static $classArray = array();
     29     static $implementsArray = array();
     30
     31     /**
     32      * @var array $ignore_files Full filenames (without directory-path) which
     33      * should be ignored by the manifest.
     34      */
     35     public static $ignore_files = array(
     36         'main.php',
     37         'cli-script.php',
     38         'install.php',
     39         'index.php',
     40     );
     41
     42     /**
     43      * @var array $ignore_folders Foldernames (without path) which
     44      * should be ignored by the manifest.
     45      */
     46     public static $ignore_folders = array(
     47         'mysql',
     48         'assets',
     49         'shortstat',
     50         'HTML',
     51     );
     52
     53     /**
     54      * Include the manifest, regenerating it if necessary
     55      */
     56     static function include_manifest() {
     57         if(isset($_REQUEST['usetestmanifest'])) {
     58             self::load_test_manifest();
     59         } else {
     60             // The dev/build reference is some coupling but it solves an annoying bug
     61             if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH))
     62                 || isset($_GET['flush']) || (isset($_REQUEST['url']) && ($_REQUEST['url'] == 'dev/build'
     63                 || $_REQUEST['url'] == BASE_URL . '/dev/build'))) {
     64                 self::create_manifest_file();
     65             }
     66             require_once(MANIFEST_FILE);
     67         }
     68     }
     69
     70     /**
     71      * Load a copy of the manifest with tests/ folders included.
     72      * Only loads the ClassInfo and __autoload() globals; this assumes that _config.php files are already included.
     73      */
     74     static function load_test_manifest() {
     75         $testManifestFile = MANIFEST_FILE . '-test';
     76
     77         // The dev/build reference is some coupling but it solves an annoying bug
     78         if(!file_exists($testManifestFile)
     79             || (filemtime($testManifestFile) < filemtime(BASE_PATH))
     80             || isset($_GET['flush'])) {
     81
     82             // Build the manifest, including the tests/ folders
     83             $manifestInfo = self::get_manifest_info(BASE_PATH);
     84             $manifest = self::generate_php_file($manifestInfo);
     85             if($fh = fopen($testManifestFile, 'wb')) {
     86                 fwrite($fh, $manifest);
     87                 fclose($fh);
     88             } else {
     89                 user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR);
     90             }
     91         }
     92
     93         require($testManifestFile);
     94     }
     95
     96     /**
     97      * Loads all PHP class files - actually opening them and executing them.
     98      */
     99     static function load_all_classes() {
    100         global $_CLASS_MANIFEST;
    101         foreach($_CLASS_MANIFEST as $classFile) require_once($classFile);
    102     }
    103
    104     /**
    105      * Generates a new manifest file and saves it to {@link MANIFEST_FILE}.
    106      */
    107     static function create_manifest_file() {
    108         // Build the manifest, ignoring the tests/ folders
    109         $manifestInfo = self::get_manifest_info(BASE_PATH, array("tests"));
    110
    111         $manifest = self::generate_php_file($manifestInfo);
    112         if($fh = fopen(MANIFEST_FILE, 'wb')) {
    113             fwrite($fh, $manifest);
    114             fclose($fh);
    115         } else {
    116             user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR);
    117         }
    118     }
    119
    120     /**
    121      * Turn an array produced by get_manifest_info() into the content of the manifest PHP include
    122      */
    123     static function generate_php_file($manifestInfo) {
    124         $output = "<?php\n";
    125
    126         foreach($manifestInfo['globals'] as $globalName => $globalVal) {
    127             $output .= "global \$$globalName;\n\$$globalName = " . var_export($globalVal, true) . ";\n\n";
    128         }
    129         foreach($manifestInfo['require_once'] as $requireItem) {
    130             $output .= "require_once('" . addslashes($requireItem) . "');\n";
    131         }
    132
    133         return preg_replace("!'".BASE_PATH."!", "BASE_PATH . '", $output);
    134     }
    135
    136
    137     /**
    138      * Parse the $manifestInfo array, updating the appropriate globals and loading the appropriate _config files.
    139      */
    140     static function process_manifest($manifestInfo) {
    141         foreach($manifestInfo['globals'] as $globalName => $globalVal) {
    142             global $$globalName;
    143             $$globalName = $globalVal;
    144         }
    145         foreach($manifestInfo['require_once'] as $requireItem) {
    146             require_once("$requireItem");
    147         }
    148     }
    149
    150     /**
    151      * Get themes from a particular directory.
    152      *
    153      * @param string $baseDir Optional: Absolute path to theme directory for testing e.g. "/Users/sharvey/Sites/test24/themes"
    154      * @param boolean $includeSubThemes If set to TRUE, sub-themes such as "blackcandy_blog" are included too
    155      * @return array Listing of theme directories
    156      */
    157     public static function get_themes($baseDir = null, $includeSubThemes = false) {
    158         // If no base directory specified, the default is the project root
    159         if(!$baseDir) $baseDir = BASE_PATH . DIRECTORY_SEPARATOR . THEMES_DIR;
    160         $themes = array();
    161         if(!file_exists($baseDir)) return $themes;
    162
    163         $handle = opendir($baseDir);
    164         if($handle) {
    165             while(false !== ($file = readdir($handle))) {
    166                 $fullPath = $baseDir . DIRECTORY_SEPARATOR . $file;
    167                 if(strpos($file, '.') === false && is_dir($fullPath)) {
    168                     $include = $includeSubThemes ? true : false;
    169                     if(strpos($file, '_') === false) {
    170                         $include = true;
    171                     }
    172                     if($include) $themes[$file] = $file;
    173                 }
    174             }
    175             closedir($handle);
    176         }
    177         return $themes;
    178     }
    179
    180     /**
    181      * Return an array containing information for the manifest
    182      * @param $baseDir The root directory to analyse
    183      * @param $excludedFolders An array folder names to exclude.  These don't care about where the
    184      *        folder appears in the hierarchy, so be careful
    185      */
    186     static function get_manifest_info($baseDir, $excludedFolders = array()) {
    187         global $project;
    188         // locate and include the exclude files
    189         $topLevel = scandir($baseDir);
    190         foreach($topLevel as $file) {
    191             if($file[0] == '.') continue;
    192
    193             $fullPath = '';
    194             $fullPath = $baseDir . '/' . $file;
    195
    196             if(@is_dir($fullPath . '/') && file_exists($fullPath . '/_exclude.php')) {
    197                 require_once($fullPath . '/_exclude.php');
    198             }
    199         }
    200
    201         // Project - used to give precedence to template files
    202         if (!isset($project) || !$project) $project = 'site';
    203
    204         // Class, CSS, template manifest
    205         $allPhpFiles = array();
    206         $templateManifest = array();
    207         $cssManifest = array();
    208
    209
    210         if(is_array(self::$restrict_to_modules) && count(self::$restrict_to_modules)) {
    211             // $restrict_to_modules is set, so we include only those specified
    212             // modules
    213             foreach(self::$restrict_to_modules as $module)
    214                 ManifestBuilder::get_all_php_files($baseDir . '/' . $module, $excludedFolders, $allPhpFiles);
    215         } else {
    216             // Include all directories which have an _config.php file but don't
    217             // have an _manifest_exclude file
    218             $topLevel = scandir($baseDir);
    219             foreach($topLevel as $filename) {
    220                 if($filename[0] == '.') continue;
    221                 if($filename == 'themes') continue;
    222                 if($filename == 'assets') continue;
    223                 if(in_array($filename, $excludedFolders)) continue;
    224
    225                 if(@is_dir("$baseDir/$filename") &&
    226                          file_exists("$baseDir/$filename/_config.php") &&
    227                          !file_exists("$baseDir/$filename/_manifest_exclude")) {
    228
    229                     // Get classes, templates, and CSS files
    230                     ManifestBuilder::get_all_php_files("$baseDir/$filename", $excludedFolders, $allPhpFiles);
    231                     ManifestBuilder::getTemplateManifest($baseDir, $filename, $excludedFolders, $templateManifest, $cssManifest);
    232
    233                     // List the _config.php files (project config include later)
    234                     if ($filename != $project) {
    235                         $manifestInfo["require_once"][] = "$baseDir/$filename/_config.php";
    236                         // _00config.php - pre config files
    237                         if (file_exists("$baseDir/$filename/_00config.php"))
    238                             $manifestInfo["require_once00"][] = "$baseDir/$filename/_00config.php";
    239                     }
    240                 }
    241             }
    242         }
    243         if (array_key_exists("require_once00", $manifestInfo) && is_array($manifestInfo["require_once00"]) && count($manifestInfo["require_once00"])) {
    244             $manifestInfo["require_once"] = array_merge($manifestInfo["require_once00"],$manifestInfo["require_once"]);
    245         }
    246         // project configs placed before all (_00config.php) and after all (_config.php)
    247         if (file_exists("$baseDir/$project/_00config.php"))
    248             array_unshift($manifestInfo["require_once"], "$baseDir/$project/_00config.php");
    249
    250         $manifestInfo["require_once"][] = "$baseDir/$project/_config.php";
    251
    252         // Get themes
    253         if(file_exists("$baseDir/themes")) {
    254             $themeDirs = self::get_themes("$baseDir/themes", true);
    255             foreach($themeDirs as $themeDir) {
    256                 $themeName = strtok($themeDir, '_');
    257                 ManifestBuilder::getTemplateManifest($baseDir, "themes/$themeDir", $excludedFolders, $templateManifest, $cssManifest, $themeName);
    258             }
    259         }
    260
    261         // Build class-info array from class manifest
    262         $allClasses = ManifestBuilder::allClasses($allPhpFiles);
    263
    264         // Pull the class filenames out
    265         $classManifest = $allClasses['file'];
    266         unset($allClasses['file']);
    267
    268         // Ensure that any custom templates get favoured
    269         if(!$project) user_error("\$project isn't set", E_USER_WARNING);
    270         else if(!file_exists("$baseDir/$project")) user_error("\$project is set to '$project' but no such folder exists.", E_USER_WARNING);
    271         else ManifestBuilder::getTemplateManifest($baseDir, $project, $excludedFolders, $templateManifest, $cssManifest);
    272
    273         $manifestInfo["globals"]["_CLASS_MANIFEST"] = $classManifest;
    274         $manifestInfo["globals"]["_ALL_CLASSES"] = $allClasses;
    275         $manifestInfo["globals"]["_TEMPLATE_MANIFEST"] = $templateManifest;
    276         $manifestInfo["globals"]["_CSS_MANIFEST"] = $cssManifest;
    277
    278         return $manifestInfo;
    279     }
    280
    281
    282     /**
    283      * Generates a list of all the PHP files that should be analysed by the manifest builder.
    284      *
    285      * @param string $folder The folder to traverse (recursively)
    286      * @param array $classMap The already built class map
    287      */
    288     private static function get_all_php_files($folder, $excludedFolders, &$allPhpFiles) {
    289         $items = scandir($folder);
    290         if($items) foreach($items as $item) {
    291             // Skip some specific PHP files
    292             if(in_array($item, self::$ignore_files)) continue;
    293
    294             // ignore hidden files and folders
    295             if(substr($item,0,1) == '.') continue;
    296
    297             // ignore files without php-extension
    298             if(substr($item,-4) != '.php' && !@is_dir("$folder/$item")) continue;
    299
    300             // ignore files and folders with underscore-prefix
    301             if(substr($item,0,1) == '_') continue;
    302
    303             // ignore certain directories
    304             if(@is_dir("$folder/$item") && in_array($item, self::$ignore_folders)) continue;
    305
    306             // ignore directories with _manifest_exlude file
    307             if(@is_dir("$folder/$item") && file_exists("$folder/$item/_manifest_exclude")) continue;
    308
    309             // i18n: ignore language files (loaded on demand)
    310             if($item == 'lang' && @is_dir("$folder/$item") && ereg_replace("/[^/]+/\\.\\.","",$folder.'/..') == Director::baseFolder()) continue;
    311
    312             if(@is_dir("$folder/$item")) {
    313                 // Folder exclusion - used to skip over tests/ folders
    314                 if(in_array($item, $excludedFolders)) continue;
    315
    316                 // recurse into directories (if not in $ignore_folders)
    317                 ManifestBuilder::get_all_php_files("$folder/$item", $excludedFolders, $allPhpFiles);
    318             } else {
    319                 $allPhpFiles[] = "$folder/$item";
    320             }
    321
    322         }
    323     }
    324
    325
    326     /**
    327      * Generates the template manifest - a list of all the .SS files in the
    328      * application
    329      */
    330     private static function getTemplateManifest($baseDir, $folder, $excludedFolders, &$templateManifest, &$cssManifest, $themeName = null) {
    331         $items = scandir("$baseDir/$folder");
    332         if($items) foreach($items as $item) {
    333             if(substr($item,0,1) == '.') continue;
    334             if(substr($item,-3) == '.ss') {
    335                 $templateName = substr($item, 0, -3);
    336                 $templateType = substr($folder,strrpos($folder,'/')+1);
    337                 if($templateType == "templates") $templateType = "main";
    338
    339                 if($themeName) {
    340                     $templateManifest[$templateName]['themes'][$themeName][$templateType] = "$baseDir/$folder/$item";
    341                 } else {
    342                     $templateManifest[$templateName][$templateType] = "$baseDir/$folder/$item";
    343                 }
    344
    345             } else if(substr($item,-4) == '.css') {
    346                     $cssName = substr($item, 0, -4);
    347                     // Debug::message($item);
    348
    349                     if($themeName) {
    350                         $cssManifest[$cssName]['themes'][$themeName] = "$folder/$item";
    351                     } else {
    352                         $cssManifest[$cssName]['unthemed'] = "$folder/$item";
    353                     }
    354
    355
    356             } else if(@is_dir("$baseDir/$folder/$item")) {
    357                 // Folder exclusion - used to skip over tests/ folders
    358                 if(in_array($item, $excludedFolders)) continue;
    359
    360                 ManifestBuilder::getTemplateManifest($baseDir, "$folder/$item", $excludedFolders, $templateManifest, $cssManifest, $themeName);
    361             }
    362         }
    363     }
    364
    365
    366     /**
    367      * Include everything, so that actually *all* classes are available and
    368      * build a map of classes and their subclasses
    369      *
    370      * @param $classManifest An array of all Sapphire classes; keys are class names and values are filenames
    371      *
    372      * @return array Returns an array that holds all class relevant
    373      *               information.
    374      */
    375     private static function allClasses($classManifest) {
    376         self::$classArray = array();
    377         self::$extendsArray = array();
    378         self::$implementsArray = array();
    379
    380         // Include everything, so we actually have *all* classes
    381         foreach($classManifest as $file) {
    382             $b = basename($file);
    383             if($b != 'cli-script.php' && $b != 'main.php')
    384                 self::parse_file($file);
    385         }
    386
    387         $allClasses["parents"] = self::find_parents();
    388         $allClasses["children"] = self::find_children();
    389         $allClasses["implementors"] = self::$implementsArray;
    390
    391         foreach(self::$classArray as $class => $info) {
    392             $allClasses['exists'][$class] = $class;
    393             $allClasses['file'][strtolower($class)] = $info['file'];
    394         }
    395
    396         // Build a map of classes and their subclasses
    397         $_classes = get_declared_classes();
    398
    399         foreach($_classes as $class) {
    400             $allClasses['exists'][$class] = $class;
    401             foreach($_classes as $subclass) {
    402                 if(is_subclass_of($class, $subclass)) $allClasses['parents'][$class][$subclass] = $subclass;
    403                 if(is_subclass_of($subclass, $class)) $allClasses['children'][$class][$subclass] = $subclass;
    404             }
    405         }
    406
    407         return $allClasses;
    408     }
    409
    410     /**
    411      * Parses a php file and adds any class or interface information into self::$classArray
    412      *
    413      * @param string $filename
    414      */
    415     private static function parse_file($filename) {
    416         $file = file_get_contents($filename);
    417
    418         $implements = "";
    419         $extends = "";
    420         $class="";
    421
    422         if($file === null) user_error("ManifestBuilder::parse_file(): Couldn't open $filename", E_USER_ERROR);
    423         if(!$file) return;
    424
    425         // We cache the parse results of each file, since only a few files will have changed between flushings
    426         // And, although it's accurate, TokenisedRegularExpression isn't particularly fast.
    427         // We use an MD5 of the file as a part of the cache key because using datetime caused problems when users
    428         // were upgrading their sites
    429         $fileMD5 = md5($file);
    430         $parseCacheFile = TEMP_FOLDER . "/manifestClassParse-" . str_replace(array("/", ":", "\\", "."), "_", basename($filename)) . "-${fileMD5}.php";
    431         if(file_exists($parseCacheFile)) {
    432             include($parseCacheFile);
    433             // Check for a bad cache file
    434             if(!isset($classes) || !isset($interfaces) || !is_array($classes) || !is_array($interfaces)) {
    435                 unset($classes);
    436                 unset($interfaces);
    437             }
    438         }
    439
    440         // Either the parseCacheFile doesn't exist, or its bad
    441         if(!isset($classes)) {
    442             $tokens = token_get_all($file);
    443             $classes = (array)self::getClassDefParser()->findAll($tokens);
    444             $interfaces = (array)self::getInterfaceDefParser()->findAll($tokens);
    445
    446             $cacheContent = '<?php
    447                 $classes = ' . var_export($classes,true) . ';
    448                 $interfaces = ' . var_export($interfaces,true) . ';';
    449
    450             if($fh = fopen($parseCacheFile, 'wb')) {
    451                 fwrite($fh, $cacheContent);
    452                 fclose($fh);
    453             }
    454         }
    455
    456         foreach($classes as $class) {
    457             $className = $class['className'];
    458             unset($class['className']);
    459             $class['file'] = $filename;
    460             if(!isset($class['extends'])) $class['extends'] = null;
    461
    462             if($class['extends']) self::$extendsArray[$class['extends']][$className] = $className;
    463             if(isset($class['interfaces'])) foreach($class['interfaces'] as $interface) {
    464                 self::$implementsArray[$interface][$className] = $className;
    465             }
    466
    467             if(isset(self::$classArray[$className])) {
    468                 $file1 = self::$classArray[$className]['file'];
    469                 $file2 = $class['file'];
    470                 user_error("There are two files both containing the same class: '$file1' and " .
    471                     "'$file2'. This might mean that the wrong code is being used.", E_USER_WARNING);
    472             }
    473
    474             self::$classArray[$className] = $class;
    475         }
    476
    477         foreach($interfaces as $interface) {
    478             $className = $interface['interfaceName'];
    479             unset($interface['interfaceName']);
    480             $interface['file'] = $filename;
    481             if(!isset($interface['extends'])) $interface['extends'] = null;
    482
    483             if(isset(self::$classArray[$className])) {
    484                 $file1 = self::$classArray[$className]['file'];
    485                 $file2 = $interface[$className];
    486                 user_error("There are two files both containing the same class: '$file1' and " .
    487                     "'$file2'. This might mean that the wrong code is being used.", E_USER_WARNING);
    488             }
    489
    490             self::$classArray[$className] = $interface;
    491         }
    492     }
    493
    494     /**
    495      * Returns a {@link TokenisedRegularExpression} object that will parse class definitions
    496      * @return TokenisedRegularExpression
    497      */
    498     public static function getClassDefParser() {
    499         require_once('core/TokenisedRegularExpression.php');
    500
    501         return new TokenisedRegularExpression(array(
    502             0 => T_CLASS,
    503             1 => T_WHITESPACE,
    504             2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'),
    505             3 => T_WHITESPACE,
    506             4 => T_EXTENDS,
    507             5 => T_WHITESPACE,
    508             6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14),
    509             7 => T_WHITESPACE,
    510             8 => T_IMPLEMENTS,
    511             9 => T_WHITESPACE,
    512             10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'),
    513             11 => array(T_WHITESPACE, 'optional' => true),
    514             12 => array(',', 'can_jump_to' => 10),
    515             13 => array(T_WHITESPACE, 'can_jump_to' => 10),
    516             14 => array(T_WHITESPACE, 'optional' => true),
    517             15 => '{',
    518         ));
    519     }
    520
    521     /**
    522      * Returns a {@link TokenisedRegularExpression} object that will parse class definitions
    523      * @return TokenisedRegularExpression
    524      */
    525     public static function getInterfaceDefParser() {
    526         require_once('core/TokenisedRegularExpression.php');
    527
    528         return new TokenisedRegularExpression(array(
    529             0 => T_INTERFACE,
    530             1 => T_WHITESPACE,
    531             2 => array(T_STRING, 'can_jump_to' => 7, 'save_to' => 'interfaceName'),
    532             3 => T_WHITESPACE,
    533             4 => T_EXTENDS,
    534             5 => T_WHITESPACE,
    535             6 => array(T_STRING, 'save_to' => 'extends'),
    536             7 => array(T_WHITESPACE, 'optional' => true),
    537             8 => '{',
    538         ));
    539     }
    540
    541
    542     /**
    543      * Moves through self::$classArray and creates an array containing parent data
    544      *
    545      * @return array
    546      */
    547     private static function find_parents() {
    548         $parentArray = array();
    549         foreach(self::$classArray as $class => $info) {
    550             $extendArray = array();
    551
    552             $parent = $info["extends"];
    553
    554             while($parent) {
    555                 $extendArray[$parent] = $parent;
    556                 $parent = isset(self::$classArray[$parent]["extends"]) ? self::$classArray[$parent]["extends"] : null;
    557             }
    558             $parentArray[$class] = array_reverse($extendArray);
    559         }
    560         return $parentArray;
    561     }
    562
    563     /**
    564      * Iterates through self::$classArray and returns an array with any descendant data
    565      *
    566      * @return array
    567      */
    568     private static function find_children() {
    569         $childrenArray = array();
    570         foreach(self::$extendsArray as $class => $children) {
    571             $allChildren = $children;
    572             foreach($children as $childName) {
    573                 $allChildren = array_merge($allChildren, self::up_children($childName));
    574             }
    575             $childrenArray[$class] = $allChildren;
    576         }
    577         return $childrenArray;
    578     }
    579
    580     /**
    581      * Helper function to find all children of give class
    582      *
    583      * @param string $class
    584      * @return array
    585      */
    586     private static function get_children($class) {
    587         return isset(self::$extendsArray[$class]) ? self::$extendsArray[$class] : array();
    588     }
    589
    590     /**
    591      * Returns if the Manifest has been included
    592      *
    593      * @return Boolean
    594      */
    595     static function has_been_included() {
    596         global $_CLASS_MANIFEST, $_TEMPLATE_MANIFEST, $_CSS_MANIFEST, $_ALL_CLASSES;
    597         return (bool)!(empty($_CLASS_MANIFEST) && empty($_TEMPLATE_MANIFEST) && empty($_CSS_MANIFEST) && empty($_ALL_CLASSES));
    598     }
    599
    600     /**
    601      * Returns a flat array with all children of a given class
    602      *
    603      * @param string $class
    604      * @param array $results
    605      */
    606     static function up_children($class) {
    607         $children = self::get_Children($class);
    608         $results = $children;
    609             foreach($children as $className) {
    610                 $results = array_merge($results, self::up_children($className));
    611             }
    612             return $results;;
    613     }
    614 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -