Version: current

       1 <?php
       2 /**
       3  * The SSViewer executes a .ss template file.
       4  * The SSViewer class handles rendering of .ss templates.  In addition to a full template in
       5  * the views folder, a template in views/Content or views/Layout will be rendered into $Content and
       6  * $Layout, respectively.
       7  *
       8  * Compiled templates are cached.  If you put ?flush=1 on your URL, it will force the template to be recompiled.  This
       9  * is a hack; the system should really detect when a page needs re-fetching.
      10  *
      11  * Works with the global $_TEMPLATE_MANIFEST which is compiled by {@link ManifestBuilder->getTemplateManifest()}.
      12  * This associative array lists all template filepaths by "identifier", meaning the name
      13  * of the template without its path or extension.
      14  *
      15  * Example:
      16  * <code>
      17  * array(
      18  *  'LeftAndMain' =>
      19  *  array (
      20  *  'main' => '/my/system/path/cms/templates/LeftAndMain.ss',
      21  *  ),
      22  * 'CMSMain_left' =>
      23  *   array (
      24  *     'Includes' => '/my/system/path/cms/templates/Includes/CMSMain_left.ss',
      25  *   ),
      26  * 'Page' =>
      27  *   array (
      28  *     'themes' =>
      29  *     array (
      30  *       'blackcandy' =>
      31  *       array (
      32  *         'Layout' => '/my/system/path/themes/blackcandy/templates/Layout/Page.ss',
      33  *         'main' => '/my/system/path/themes/blackcandy/templates/Page.ss',
      34  *       ),
      35  *       'blue' =>
      36  *       array (
      37  *         'Layout' => '/my/system/path/themes/mysite/templates/Layout/Page.ss',
      38  *         'main' => '/my/system/path/themes/mysite/templates/Page.ss',
      39  *       ),
      40  *     ),
      41  *   ),
      42  *   // ...
      43  * )
      44  * </code>
      45  *
      46  * @todo Fix the broken caching.
      47  * @package sapphire
      48  * @subpackage view
      49  */
      50 class SSViewer {
      51
      52     /**
      53      * @var boolean $source_file_comments
      54      */
      55     protected static $source_file_comments = false;
      56
      57     /**
      58      * Set whether HTML comments indicating the source .SS file used to render this page should be
      59      * included in the output.  This is enabled by default
      60      *
      61      * @param boolean $val
      62      */
      63     static function set_source_file_comments($val) {
      64         self::$source_file_comments = $val;
      65     }
      66
      67     /**
      68      * @return boolean
      69      */
      70     static function get_source_file_comments() {
      71         return self::$source_file_comments;
      72     }
      73
      74     /**
      75      * @var array $chosenTemplates Associative array for the different
      76      * template containers: "main" and "Layout".
      77      */
      78     private $chosenTemplates = array();
      79
      80     /**
      81      * @var boolean
      82      */
      83     protected $rewriteHashlinks = true;
      84
      85     /**
      86      * @var string
      87      */
      88     protected static $current_theme = null;
      89
      90     /**
      91      * @var string
      92      */
      93     protected static $current_custom_theme = null;
      94
      95     /**
      96      * Create a template from a string instead of a .ss file
      97      *
      98      * @return SSViewer
      99      */
     100     static function fromString($content) {
     101         return new SSViewer_FromString($content);
     102     }
     103
     104     /**
     105      * @param string $theme
     106      */
     107     static function set_theme($theme) {
     108         self::$current_theme = $theme;
     109         //Static publishing needs to have a theme set, otherwise it defaults to the content controller theme
     110         if(!is_null($theme))
     111             self::$current_custom_theme=$theme;
     112     }
     113
     114     /**
     115      * @return string
     116      */
     117     static function current_theme() {
     118         return self::$current_theme;
     119     }
     120
     121     /**
     122      * @return string
     123      */
     124     static function current_custom_theme(){
     125         return self::$current_custom_theme;
     126     }
     127
     128     /**
     129      * Pass the SilverStripe template to be used.
     130      *
     131      * @param string|array $templateList
     132      *   If passed as a string with .ss extension, used as the "main" template
     133      */
     134     public function __construct($templateList) {
     135         global $_TEMPLATE_MANIFEST;
     136
     137         // flush template manifest cache if requested
     138         if (isset($_GET['flush']) && $_GET['flush'] == 'all') {
     139             if(Director::isDev() || Director::is_cli() || Permission::check('ADMIN')) {
     140                 self::flush_template_cache();
     141             } else {
     142                 return Security::permissionFailure(null, 'Please log in as an administrator to flush the template cache.');
     143             }
     144         }
     145
     146         if(is_string($templateList) && substr((string) $templateList,-3) == '.ss') {
     147             $this->chosenTemplates['main'] = $templateList;
     148         } else {
     149             if(!is_array($templateList)) $templateList = array($templateList);
     150
     151             if(isset($_GET['debug_request'])) Debug::message("Selecting templates from the following list: " . implode(", ", $templateList));
     152
     153             foreach($templateList as $template) {
     154                 // if passed as a partial directory (e.g. "Layout/Page"), split into folder and template components
     155                 if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
     156                 else $templateFolder = null;
     157
     158                 // Use the theme template if available
     159                 if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()])) {
     160                     $this->chosenTemplates = array_merge(
     161                         $_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()],
     162                         $this->chosenTemplates
     163                     );
     164
     165                     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));
     166                 }
     167
     168                 // Fall back to unthemed base templates
     169                 if(isset($_TEMPLATE_MANIFEST[$template]) && (array_keys($_TEMPLATE_MANIFEST[$template]) != array('themes'))) {
     170                     $this->chosenTemplates = array_merge(
     171                         $_TEMPLATE_MANIFEST[$template],
     172                         $this->chosenTemplates
     173                     );
     174
     175                     if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main template archive, containing the following items: " . var_export($_TEMPLATE_MANIFEST[$template], true));
     176
     177                     unset($this->chosenTemplates['themes']);
     178                 }
     179
     180                 if($templateFolder) {
     181                     $this->chosenTemplates['main'] = $this->chosenTemplates[$templateFolder];
     182                     unset($this->chosenTemplates[$templateFolder]);
     183                 }
     184             }
     185
     186             if(isset($_GET['debug_request'])) Debug::message("Final template selections made: " . var_export($this->chosenTemplates, true));
     187
     188         }
     189
     190         if(!$this->chosenTemplates) user_error("None of these templates can be found in theme '"
     191             . self::current_theme() . "': ". implode(".ss, ", $templateList) . ".ss", E_USER_WARNING);
     192     }
     193
     194     /**
     195      * Returns true if at least one of the listed templates exists
     196      */
     197     static function hasTemplate($templateList) {
     198         if(!is_array($templateList)) $templateList = array($templateList);
     199
     200         global $_TEMPLATE_MANIFEST;
     201         foreach($templateList as $template) {
     202             if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
     203             if(isset($_TEMPLATE_MANIFEST[$template])) return true;
     204         }
     205
     206         return false;
     207     }
     208
     209     /**
     210      * Set a global rendering option.
     211      * The following options are available:
     212      *  - rewriteHashlinks: If true (the default), <a href="#..."> will be rewritten to contain the
     213      *    current URL.  This lets it play nicely with our <base> tag.
     214      *  - If rewriteHashlinks = 'php' then, a piece of PHP script will be inserted before the hash
     215      *    links: "<?php echo $_SERVER['REQUEST_URI']; ?>".  This is useful if you're generating a
     216      *    page that will be saved to a .php file and may be accessed from different URLs.
     217      */
     218     public static function setOption($optionName, $optionVal) {
     219         SSViewer::$options[$optionName] = $optionVal;
     220     }
     221     protected static $options = array(
     222         'rewriteHashlinks' => true,
     223     );
     224
     225     protected static $topLevel = array();
     226     public static function topLevel() {
     227         if(SSViewer::$topLevel) {
     228             return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
     229         }
     230     }
     231
     232     /**
     233      * Call this to disable rewriting of <a href="#xxx"> links.  This is useful in Ajax applications.
     234      * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
     235      */
     236     public function dontRewriteHashlinks() {
     237         $this->rewriteHashlinks = false;
     238         self::$options['rewriteHashlinks'] = false;
     239         return $this;
     240     }
     241
     242     public function exists() {
     243         return $this->chosenTemplates;
     244     }
     245
     246     /**
     247      * Searches for a template name in the current theme:
     248      * - themes/mytheme/templates
     249      * - themes/mytheme/templates/Includes
     250      * Falls back to unthemed template files.
     251      *
     252      * Caution: Doesn't search in any /Layout folders.
     253      *
     254      * @param string $identifier A template name without '.ss' extension or path.
     255      * @return string Full system path to a template file
     256      */
     257     public static function getTemplateFile($identifier) {
     258         global $_TEMPLATE_MANIFEST;
     259
     260         $includeTemplateFile = self::getTemplateFileByType($identifier, 'Includes');
     261         if($includeTemplateFile) return $includeTemplateFile;
     262
     263         $mainTemplateFile = self::getTemplateFileByType($identifier, 'main');
     264         if($mainTemplateFile) return $mainTemplateFile;
     265
     266         return false;
     267     }
     268
     269     /**
     270      * @param string $identifier A template name without '.ss' extension or path
     271      * @param string $type The template type, either "main", "Includes" or "Layout"
     272      * @return string Full system path to a template file
     273      */
     274     public static function getTemplateFileByType($identifier, $type) {
     275         global $_TEMPLATE_MANIFEST;
     276         if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type])) {
     277             return $_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type];
     278         } else if(isset($_TEMPLATE_MANIFEST[$identifier][$type])){
     279             return $_TEMPLATE_MANIFEST[$identifier][$type];
     280         } else {
     281             return false;
     282         }
     283     }
     284
     285     /**
     286      * Used by <% include Identifier %> statements to get the full
     287      * unparsed content of a template file.
     288      *
     289      * @uses getTemplateFile()
     290      * @param string $identifier A template name without '.ss' extension or path.
     291      * @return string content of template
     292      */
     293     public static function getTemplateContent($identifier) {
     294         if(!SSViewer::getTemplateFile($identifier)) {
     295             return null;
     296         }
     297
     298         $content = file_get_contents(SSViewer::getTemplateFile($identifier));
     299
     300         // $content = "<!-- getTemplateContent() :: identifier: $identifier -->". $content;
     301         // Adds an i18n namespace to all <% _t(...) %> calls without an existing one
     302         // to avoid confusion when using the include in different contexts.
     303         // Entities without a namespace are deprecated, but widely used.
     304         $content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\''. $identifier . '.ss' . '.\\2\\3\'\\4) ?>', $content);
     305         $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\), \$([a-zA-Z][a-zA-Z0-9_]*)\) +%' . '>', '<?= sprintf(_t(\'' . $identifier . '.ss.\\2\\3\'\\4),  {dlr}item->XML_val("\\6",null,true)) ?>', $content);
     306
     307         // Remove UTF-8 byte order mark
     308         // This is only necessary if you don't have zend-multibyte enabled.
     309         if(substr($content, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
     310             $content = substr($content, 3);
     311         }
     312
     313         return $content;
     314     }
     315
     316     /**
     317      * @ignore
     318      */
     319     static private $flushed = false;
     320
     321     /**
     322      * Clears all parsed template files in the cache folder.
     323      *
     324      * Can only be called once per request (there may be multiple SSViewer instances).
     325      */
     326     static function flush_template_cache() {
     327         if (!self::$flushed) {
     328             $dir = dir(TEMP_FOLDER);
     329             while (false !== ($file = $dir->read())) {
     330                 if (strstr($file, '.cache')) { unlink(TEMP_FOLDER.'/'.$file); }
     331             }
     332             self::$flushed = true;
     333         }
     334     }
     335
     336     /**
     337      * The process() method handles the "meat" of the template processing.
     338      */
     339     public function process($item, $cache = null) {
     340         SSViewer::$topLevel[] = $item;
     341
     342         if (!$cache) $cache = SS_Cache::factory('cacheblock');
     343
     344         if(isset($this->chosenTemplates['main'])) {
     345             $template = $this->chosenTemplates['main'];
     346         } else {
     347             $template = $this->chosenTemplates[ reset($dummy = array_keys($this->chosenTemplates)) ];
     348         }
     349
     350         if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process", " for $template");
     351 //      $cacheFile = TEMP_FOLDER . "/.cache" . str_replace(array("\\", '/', ':'), '.', realpath($template));
     352         $cacheFile = TEMP_FOLDER . "/.cache" . str_replace(array("\\", '/', ':'), '.', str_replace(BASE_PATH, '', $template));
     353
     354         $lastEdited = filemtime($template);
     355
     356         if(!file_exists($cacheFile)
     357             || (Director::isDev() && filemtime($cacheFile) < $lastEdited)
     358             || (isset($_GET['flush']) && (Director::isDev() || Director::is_cli() || Permission::check('ADMIN')))
     359         ) {
     360             if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process - compile", " for $template");
     361
     362             $content = file_get_contents($template);
     363             $content = SSViewer::parseTemplateContent($content, $template);
     364
     365             $fh = fopen($cacheFile,'w');
     366             fwrite($fh, $content);
     367             fclose($fh);
     368
     369             if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process - compile", " for $template");
     370         }
     371
     372
     373         if(isset($_GET['showtemplate']) && !Director::isLive()) {
     374             $lines = file($cacheFile);
     375             echo "<h2>Template: $cacheFile</h2>";
     376             echo "<pre>";
     377             foreach($lines as $num => $line) {
     378                 echo str_pad($num+1,5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
     379             }
     380             echo "</pre>";
     381         }
     382
     383
     384         foreach(array('Content', 'Layout') as $subtemplate) {
     385             if(isset($this->chosenTemplates[$subtemplate])) {
     386                 $subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]);
     387                 $item = $item->customise(array(
     388                     $subtemplate => $subtemplateViewer->process($item, $cache)
     389                 ));
     390             }
     391         }
     392
     393         $itemStack = array();
     394         $val = "";
     395         $valStack = array();
     396
     397         include($cacheFile);
     398
     399         $output = $val;
     400         $output = Requirements::includeInHTML($template, $output);
     401
     402         array_pop(SSViewer::$topLevel);
     403
     404         if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template");
     405
     406         // If we have our crazy base tag, then fix # links referencing the current page.
     407         if($this->rewriteHashlinks && self::$options['rewriteHashlinks']) {
     408             if(strpos($output, '<base') !== false) {
     409                 if(SSViewer::$options['rewriteHashlinks'] === 'php') {
     410                     $thisURLRelativeToBase = "<?php echo \$_SERVER['REQUEST_URI']; ?>";
     411                 } else {
     412                     $thisURLRelativeToBase = Director::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
     413                 }
     414                 $output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
     415             }
     416         }
     417
     418         return $output;
     419     }
     420
     421     static function parseTemplateContent($content, $template="") {
     422         // Remove UTF-8 byte order mark:
     423         // This is only necessary if you don't have zend-multibyte enabled.
     424         if(substr($content, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
     425             $content = substr($content, 3);
     426         }
     427
     428         // Add template filename comments on dev sites
     429         if(Director::isDev() && self::$source_file_comments && $template && stripos($content, "<?xml") === false) {
     430             // If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE glitches
     431             if(stripos($content, "<html") !== false) {
     432                 $content = preg_replace('/(<html[^>]*>)/i', "\\1<!-- template $template -->", $content);
     433                 $content = preg_replace('/(<\/html[^>]*>)/i', "\\1<!-- end template $template -->", $content);
     434             } else {
     435                 $content = "<!-- template $template -->\n" . $content . "\n<!-- end template $template -->";
     436             }
     437         }
     438
     439         while(true) {
     440             $oldContent = $content;
     441
     442             // Add include filename comments on dev sites
     443             if(Director::isDev() && self::$source_file_comments) $replacementCode = 'return "<!-- include " . SSViewer::getTemplateFile($matches[1]) . " -->\n"
     444                 . SSViewer::getTemplateContent($matches[1])
     445                 . "\n<!-- end include " . SSViewer::getTemplateFile($matches[1]) . " -->";';
     446             else $replacementCode = 'return SSViewer::getTemplateContent($matches[1]);';
     447
     448             $content = preg_replace_callback('/<' . '% include +([A-Za-z0-9_]+) +%' . '>/', create_function(
     449                 '$matches', $replacementCode
     450                 ), $content);
     451             if($oldContent == $content) break;
     452         }
     453
     454         // $val, $val.property, $val(param), etc.
     455         $replacements = array(
     456             '/<%--.*--%>/U' =>  '',
     457             '/\$Iteration/' =>  '<?= {dlr}key ?>',
     458             // {$funct(arg1, arg2, arg3)}
     459             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+), *([^),]+)\\)}/' => '<?= {dlr}item->XML_val("\\1",array("\\2","\\3","\\4"),true) ?>',
     460             // {$funct(arg1, arg2).prop.subporp}
     461             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",array("\\2","\\3"),true)->obj("\\4",null,true)->XML_val("\\5",null,true) ?>',
     462             // {$funct(arg1, arg2).prop}
     463             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",array("\\2","\\3"),true)->XML_val("\\4",null,true) ?>',
     464             // {$funct(arg1, arg2)}
     465             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)}/' => '<?= {dlr}item->XML_val("\\1",array("\\2","\\3"),true) ?>',
     466             // {$funct(arg1).prop.subporp}
     467             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",array("\\2"),true)->obj("\\3",null,true)->XML_val("\\4",null,true) ?>',
     468             // {$funct(arg1).prop}
     469             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",array("\\2"),true)->XML_val("\\3",null,true) ?>',
     470             // {$funct(arg1)}
     471             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)}/' => '<?= {dlr}item->XML_val("\\1",array("\\2"),true) ?>',
     472             // {$obj.funct($arg).prop}
     473             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\(([^),]+)\\)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",null,true)->obj("\\2",array("\\3"),true)->XML_val("\\4",null,true) ?>',
     474             // {$obj.funct($arg)}
     475             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",null,true)->XML_val("\\2",array("\\3"),true) ?>',
     476             // {$obj.prop.subporp}
     477             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",null,true)->obj("\\2",null,true)->XML_val("\\3",null,true) ?>',
     478             // {$obj.prop}
     479             '/{\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)}/' => '<?= {dlr}item->obj("\\1",null,true)->XML_val("\\2",null,true) ?>',
     480             // {$obj}
     481             '/{\\$([A-Za-z_][A-Za-z0-9_]*)}/' => '<?= {dlr}item->XML_val("\\1",null,true) ?>\\2',
     482
     483             // $funct(arg1, arg2, arg3)
     484             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+), *([^),]+)\\)/' => '<?= {dlr}item->XML_val("\\1",array("\\2","\\3","\\4"),true) ?>',
     485             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\(([^),]+)\\)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1")->XML_val("\\2",array("\\3"),true) ?>\\4',
     486             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\(([^),]+), *([^),]+)\\)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1")->XML_val("\\2",array("\\3", "\\4"),true) ?>\\5',
     487
     488             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",array("\\2","\\3"),true)->obj("\\4",null,true)->XML_val("\\5",null,true) ?>\\6',
     489             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",array("\\2","\\3"),true)->XML_val("\\4",null,true) ?>\\5',
     490             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+), *([^),]+)\\)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->XML_val("\\1",array("\\2","\\3"),true) ?>\\4',
     491             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",array("\\2"),true)->obj("\\3",null,true)->XML_val("\\4",null,true) ?>\\5',
     492             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",array("\\2"),true)->XML_val("\\3",null,true) ?>\\4',
     493             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\(([^),]+)\\)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->XML_val("\\1",array("\\2"),true) ?>\\3',
     494             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",null,true)->obj("\\2",null,true)->XML_val("\\3",null,true) ?>\\4',
     495             '/\\$([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z0-9_]+)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->obj("\\1",null,true)->XML_val("\\2",null,true) ?>\\3',
     496             '/\\$([A-Za-z_][A-Za-z0-9_]*)([^A-Za-z0-9]|$)/' => '<?= {dlr}item->XML_val("\\1",null,true) ?>\\2',
     497         );
     498
     499         $content = preg_replace(array_keys($replacements), array_values($replacements), $content);
     500         $content = str_replace('{dlr}','$',$content);
     501
     502         // Cache block
     503         $content = SSViewer_PartialParser::process($template, $content);
     504
     505         // legacy
     506         $content = ereg_replace('<!-- +pc +([A-Za-z0-9_(),]+) +-->', '<' . '% control \\1 %' . '>', $content);
     507         $content = ereg_replace('<!-- +pc_end +-->', '<' . '% end_control %' . '>', $content);
     508
     509         // < % select Foo % >
     510         $content = ereg_replace('<' . '% +select +([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if ($item = $item->obj("\\1")) { ?>', $content);
     511         // < % select Foo(Bar) % >
     512         $content = ereg_replace('<' . '% +select +([A-Za-z0-9_]+)\\(([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if ($item = $item->obj("\\1", array("\\2"))) { ?>', $content);
     513         // < % select Foo.Bar % >
     514         $content = ereg_replace('<' . '% +select +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if ($item = $item->obj("\\1")->hasValue("\\2")) {  ?>', $content);
     515         // < % select Foo.Bar(Baz) % >
     516         $content = ereg_replace('<' . '% +select +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)\\(([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if (($item = $item->obj("\\1")) && ($item = $item->obj("\\2", array("\\3")))) { ?>', $content);
     517         $content = ereg_replace('<' . '% +end_select +%' . '>', '<? } $item = array_pop($itemStack); ?>', $content);
     518
     519         // < % control Foo % >
     520         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1")) foreach($loop as $key => $item) { ?>', $content);
     521         // < % control Foo.Bar % >
     522         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if(($loop = $item->obj("\\1")) && ($loop = $loop->obj("\\2"))) foreach($loop as $key => $item) { ?>', $content);
     523         // < % control Foo.Bar(Baz) % >
     524         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)\\(([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if(($loop = $item->obj("\\1")) && ($loop = $loop->obj("\\2", array("\\3")))) foreach($loop as $key => $item) { ?>', $content);
     525         // < % control Foo.Bar(Baz, Buz) % >
     526         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)\\(([^),]+), *([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if(($loop = $item->obj("\\1")) && ($loop = $loop->obj("\\2", array("\\3", "\\4")))) foreach($loop as $key => $item) { ?>', $content);
     527         // < % control Foo(Bar) % >
     528         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\(([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1", array("\\2"))) foreach($loop as $key => $item) { ?>', $content);
     529         // < % control Foo(Bar, Baz) % >
     530         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\(([^),]+), *([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1", array("\\2","\\3"))) foreach($loop as $key => $item) { ?>', $content);
     531         // < % control Foo(Bar, Baz, Buz) % >
     532         $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+)\\(([^),]+), *([^),]+), *([^),]+)\\) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1", array("\\2", "\\3", "\\4"))) foreach($loop as $key => $item) { ?>', $content);
     533         $content = ereg_replace('<' . '% +end_control +%' . '>', '<? } $item = array_pop($itemStack); ?>', $content);
     534
     535         // < % debug % >
     536         $content = ereg_replace('<' . '% +debug +%' . '>', '<? Debug::show($item) ?>', $content);
     537         // < % debug Foo % >
     538         $content = ereg_replace('<' . '% +debug +([A-Za-z0-9_]+) +%' . '>', '<? Debug::show($item->cachedCall("\\1")) ?>', $content);
     539
     540         // < % if val1.property % >
     541         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? if($item->obj("\\1",null,true)->hasValue("\\2")) {  ?>', $content);
     542
     543         // < % if val1(parameter) % >
     544         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\(([^),]+)\\) +%' . '>', '<? if($item->hasValue("\\1",array("\\2"))) {  ?>', $content);
     545         // < % if val1(par1, par2) % >
     546         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\(([^),]+), *([^),]+)\\) +%' . '>', '<? if($item->hasValue("\\1",array("\\2","\\3"))) {  ?>', $content);
     547         // < % if val1(parameter).property % >
     548         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\(([^),]+)\\)\\.([A-Za-z0-9_]+) +%' . '>', '<? if ($item->obj("\\1",array("\\2"),true)->hasValue("\\3")) {  ?>', $content);
     549
     550         // < % if val1 % >
     551         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) +%' . '>', '<? if($item->hasValue("\\1")) {  ?>', $content);
     552         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) +%' . '>', '<? } else if($item->hasValue("\\1")) {  ?>', $content);
     553
     554         // < % if val1 || val2 % >
     555         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *\\|\\|? *([A-Za-z0-9_]+) +%' . '>', '<? if($item->hasValue("\\1") || $item->hasValue("\\2")) { ?>', $content);
     556         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *\\|\\|? *([A-Za-z0-9_]+) +%' . '>', '<? } else if($item->hasValue("\\1") || $item->hasValue("\\2")) { ?>', $content);
     557
     558         // < % if val1 && val2 % >
     559         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *&&? *([A-Za-z0-9_]+) +%' . '>', '<? if($item->hasValue("\\1") && $item->hasValue("\\2")) { ?>', $content);
     560         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *&&? *([A-Za-z0-9_]+) +%' . '>', '<? } else if($item->hasValue("\\1") && $item->hasValue("\\2")) { ?>', $content);
     561
     562         // < % if val1.property || val2 % >
     563         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *\\|\\|? *([A-Za-z0-9_]+) +%' . '>', '<? if($item->obj("\\1",null,true)->hasValue("\\2") || $item->hasValue("\\3")) {  ?>', $content);
     564         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *\\|\\|? *([A-Za-z0-9_]+) +%' . '>', '<? else if($item->obj("\\1",null,true)->hasValue("\\2") || $item->hasValue("\\3")) {  ?>', $content);
     565
     566         // < % if val1.property && val2 % >
     567         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *&&? *([A-Za-z0-9_]+) +%' . '>', '<? if($item->obj("\\1",null,true)->hasValue("\\2") && $item->hasValue("\\3")) {  ?>', $content);
     568         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *&&? *([A-Za-z0-9_]+) +%' . '>', '<? else if($item->obj("\\1",null,true)->hasValue("\\2") && $item->hasValue("\\3")) {  ?>', $content);
     569
     570         // < % if val1 == val2 % >
     571         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) == "\\2") {  ?>', $content);
     572         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) == "\\2") {  ?>', $content);
     573
     574         // < % if val1 > val2 % >
     575         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *> *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) > "\\2") {  ?>', $content);
     576         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *> *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) > "\\2") {  ?>', $content);
     577
     578         // < % if val1 < val2 % >
     579         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *< *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) < "\\2") {  ?>', $content);
     580         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *< *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) < "\\2") {  ?>', $content);
     581
     582         // < % if val1 >= val2 % >
     583         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *>= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) >= "\\2") {  ?>', $content);
     584         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *>= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) > "\\2") {  ?>', $content);
     585
     586         // < % if val1 <= val2 % >
     587         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *<= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) <= "\\2") {  ?>', $content);
     588         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *<= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) < "\\2") {  ?>', $content);
     589
     590         // <% if val1.property == val2 % >
     591         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->obj("\\1",null,true)->XML_val("\\2") == "\\3") {  ?>', $content);
     592         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->obj("\\1",null,true)->XML_val("\\2") == "\\3") {  ?>', $content);
     593
     594         // <% if val1.property > val2 % >
     595         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *> *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->obj("\\1",null,true)->XML_val("\\2") > "\\3") {  ?>', $content);
     596         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *> *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->obj("\\1",null,true)->XML_val("\\2") > "\\3") {  ?>', $content);
     597
     598         // <% if val1.property < val2 % >
     599         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *< *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->obj("\\1",null,true)->XML_val("\\2") < "\\3") {  ?>', $content);
     600         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) *< *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->obj("\\1",null,true)->XML_val("\\2") < "\\3") {  ?>', $content);
     601
     602         // <% if val1(parametr) == val2 % >
     603         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\(([^),]+)\\) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",array("\\2"),true) == "\\3") {  ?>', $content);
     604         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\(([^),]+)\\) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",array("\\2"),true) == "\\3") {  ?>', $content);
     605
     606         // <% if val1(parametr).property == val2 % >
     607         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\(([^),]+)\\)\\.([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->obj("\\1",array("\\2"),true)->XML_val("\\3") == "\\4") {  ?>', $content);
     608         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\(([^),]+)\\)\\.([A-Za-z0-9_]+) *==? *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->obj("\\1",array("\\2"),true)->XML_val("\\3") == "\\4") {  ?>', $content);
     609
     610         // < % if val1 != val2 % >
     611         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+) *!= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? if($item->XML_val("\\1",null,true) != "\\2") {  ?>', $content);
     612         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) *!= *"?([A-Za-z0-9_-]+)"? +%' . '>', '<? } else if($item->XML_val("\\1",null,true) != "\\2") {  ?>', $content);
     613
     614         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+) +%' . '>', '<? } else if(($test = $item->cachedCall("\\1")) && ((!is_object($test) && $test) || ($test && $test->exists()) )) {  ?>', $content);
     615
     616         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? $test = $item->obj("\\1",null,true)->cachedCall("\\2"); if((!is_object($test) && $test) || ($test && $test->exists())) {  ?>', $content);
     617         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? } else if(($test = $item->obj("\\1",null,true)->cachedCall("\\2")) && ((!is_object($test) && $test) || ($test && $test->exists()) )) {  ?>', $content);
     618
     619         $content = ereg_replace('<' . '% +if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? $test = $item->obj("\\1",null,true)->obj("\\2",null,true)->cachedCall("\\3"); if((!is_object($test) && $test) || ($test && $test->exists())) {  ?>', $content);
     620         $content = ereg_replace('<' . '% +else_if +([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+) +%' . '>', '<? } else if(($test = $item->obj("\\1",null,true)->obj("\\2",null,true)->cachedCall("\\3")) && ((!is_object($test) && $test) || ($test && $test->exists()) )) {  ?>', $content);
     621
     622         $content = ereg_replace('<' . '% +else +%' . '>', '<? } else { ?>', $content);
     623         $content = ereg_replace('<' . '% +end_if +%' . '>', '<? }  ?>', $content);
     624
     625         // i18n - get filename of currently parsed template
     626         // CAUTION: No spaces allowed between arguments for all i18n calls!
     627         ereg('.*[\/](.*)',$template,$path);
     628
     629         // i18n _t(...) - with entity only (no dots in namespace),
     630         // meaning the current template filename will be added as a namespace.
     631         // This applies only to "root" templates, not includes which should always have their namespace set already.
     632         // See getTemplateContent() for more information.
     633         $content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\''. $path[1] . '.\\2\\3\'\\4) ?>', $content);
     634         // i18n _t(...)
     635         $content = ereg_replace('<' . '% +_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\'\\2\\3\'\\4) ?>', $content);
     636
     637         // i18n sprintf(_t(...),$argument) with entity only (no dots in namespace), meaning the current template filename will be added as a namespace
     638         $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\), *\<\?= +([^\?]*) +\?\>) +%' . '>', '<?= sprintf(_t(\''. $path[1] . '.\\2\\3\'\\4),\\6) ?>', $content);
     639         // i18n sprintf(_t(...),$argument)
     640         $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\), *\<\?= +([^\?]*) +\?\>) +%' . '>', '<?= sprintf(_t(\'\\2\\3\'\\4),\\6) ?>', $content);
     641
     642         // </base> isnt valid html? !?
     643         $content = ereg_replace('<' . '% +base_tag +%' . '>', '<?= SSViewer::get_base_tag($val); ?>', $content);
     644
     645         $content = ereg_replace('<' . '% +current_page +%' . '>', '<?= @$_SERVER["REQUEST_URI"] ?>', $content);
     646
     647         // change < % require x() % > calls to corresponding Requirement::x() ones, including 0, 1 or 2 options
     648         $content = preg_replace('/<% +require +([a-zA-Z]+)(?:\(([^),]+)\))? +%>/', '<? Requirements::\\1("\\2"); ?>', $content);
     649         $content = preg_replace('/<% +require +([a-zA-Z]+)\(([^),]+), *([^),]+)\) +%>/', '<? Requirements::\\1("\\2", "\\3"); ?>', $content);
     650
     651         // legacy
     652         $content = ereg_replace('<!-- +if +([A-Za-z0-9_]+) +-->', '<? if($item->cachedCall("\\1")) { ?>', $content);
     653         $content = ereg_replace('<!-- +else +-->', '<? } else { ?>', $content);
     654         $content = ereg_replace('<!-- +if_end +-->', '<? }  ?>', $content);
     655
     656         // Fix link stuff
     657         $content = ereg_replace('href *= *"#', 'href="<?= SSViewer::$options[\'rewriteHashlinks\'] ? Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "" ?>#', $content);
     658
     659         // Protect xml header
     660         $content = ereg_replace('<\?xml([^>]+)\?' . '>', '<##xml\\1##>', $content);
     661
     662         // Turn PHP file into string definition
     663         $content = str_replace('<?=',"\nSSVIEWER;\n\$val .= ", $content);
     664         $content = str_replace('<?',"\nSSVIEWER;\n", $content);
     665         $content = str_replace('?>',";\n \$val .= <<<SSVIEWER\n", $content);
     666
     667         $output  = "<?php\n";
     668         $output .= '$val .= <<<SSVIEWER' . "\n" . $content . "\nSSVIEWER;\n";
     669
     670         // Protect xml header @sam why is this run twice ?
     671         $output = ereg_replace('<##xml([^>]+)##>', '<' . '?xml\\1?' . '>', $output);
     672
     673         return $output;
     674     }
     675
     676     /**
     677      * Returns the filenames of the template that will be rendered.  It is a map that may contain
     678      * 'Content' & 'Layout', and will have to contain 'main'
     679      */
     680     public function templates() {
     681         return $this->chosenTemplates;
     682     }
     683
     684     /**
     685      * @param string $type "Layout" or "main"
     686      * @param string $file Full system path to the template file
     687      */
     688     public function setTemplateFile($type, $file) {
     689         $this->chosenTemplates[$type] = $file;
     690     }
     691
     692     /**
     693      * Return an appropriate base tag for the given template.
     694      * It will be closed on an XHTML document, and unclosed on an HTML document.
     695      *
     696      * @param $contentGeneratedSoFar The content of the template generated so far; it should contain
     697      * the DOCTYPE declaration.
     698      */
     699     static function get_base_tag($contentGeneratedSoFar) {
     700         $base = Director::absoluteBaseURL();
     701
     702         // Is the document XHTML?
     703         if(preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
     704             return "<base href=\"$base\"></base>";
     705         } else {
     706             return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
     707         }
     708     }
     709 }
     710
     711 /**
     712  * Special SSViewer that will process a template passed as a string, rather than a filename.
     713  * @package sapphire
     714  * @subpackage view
     715  */
     716 class SSViewer_FromString extends SSViewer {
     717     protected $content;
     718
     719     public function __construct($content) {
     720         $this->content = $content;
     721     }
     722
     723     public function process($item, $cache = null) {
     724         $template = SSViewer::parseTemplateContent($this->content, "string sha1=".sha1($this->content));
     725
     726         $tmpFile = tempnam(TEMP_FOLDER,"");
     727         $fh = fopen($tmpFile, 'w');
     728         fwrite($fh, $template);
     729         fclose($fh);
     730
     731         if(isset($_GET['showtemplate']) && $_GET['showtemplate']) {
     732             $lines = file($tmpFile);
     733             echo "<h2>Template: $tmpFile</h2>";
     734             echo "<pre>";
     735             foreach($lines as $num => $line) {
     736                 echo str_pad($num+1,5) . htmlentities($line);
     737             }
     738             echo "</pre>";
     739         }
     740
     741         $itemStack = array();
     742         $val = "";
     743         $valStack = array();
     744
     745         $cache = SS_Cache::factory('cacheblock');
     746
     747         include($tmpFile);
     748         unlink($tmpFile);
     749
     750
     751         return $val;
     752     }
     753 }
     754
     755 /**
     756  * Handle the parsing for cacheblock tags.
     757  *
     758  * Needs to be handled differently from the other tags, because cacheblock can take any number of arguments
     759  *
     760  * This shouldn't be used as an example of how to add functionality to SSViewer - the eventual plan is to re-write
     761  * SSViewer using a proper parser (probably http://github.com/hafriedlander/php-peg), so that extra functionality
     762  * can be added without relying on ad-hoc parsers like this.
     763  *
     764  * @package sapphire
     765  * @subpackage view
     766  */
     767 class SSViewer_PartialParser {
     768
     769     static $tag = '/< % [ \t]+ (cached|cacheblock|uncached|end_cached|end_cacheblock|end_uncached) [ \t]+ ([^%]+ [ \t]+)? % >/xS';
     770
     771     static $argument_splitter = '/^\s*
     772         # The argument itself
     773         (
     774             (?P<conditional> if | unless ) |                     # The if or unless keybreak
     775             (?P<property> (?P<identifier> \w+) \s*               # A property lookup or a function call
     776                 ( \( (?P<arguments> [^\)]*) \) )?
     777             ) |
     778             (?P<sqstring> \' (\\\'|[^\'])+ \' ) |                # A string surrounded by \'
     779             (?P<dqstring> " (\\"|[^"])+ " )                      # A string surrounded by "
     780         )
     781         # Some seperator after the argument
     782         (
     783             \s*(?P<comma>,)\s* |                                 # A comma (maybe with whitespace before or after)
     784             (?P<fullstop>\.)                                     # A period (no whitespace before)
     785         )?
     786     /xS';
     787
     788     static function process($template, $content) {
     789         $parser = new SSViewer_PartialParser($template, $content, 0);
     790         $parser->parse();
     791         return $parser->generate();
     792     }
     793
     794     function __construct($template, $content, $offset) {
     795         $this->template = $template;
     796         $this->content = $content;
     797         $this->offset = $offset;
     798
     799         $this->blocks = array();
     800     }
     801
     802     function controlcheck($text) {
     803         // NOP - hook for Cached_PartialParser
     804     }
     805
     806     function parse() {
     807         $current_tag_offset = 0;
     808
     809         while (preg_match(self::$tag, $this->content, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
     810             $tag = $matches[1][0];
     811
     812             $startpos = $matches[0][1];
     813             $endpos = $matches[0][1] + strlen($matches[0][0]);
     814
     815             switch($tag) {
     816                 case 'cached':
     817                 case 'uncached':
     818                 case 'cacheblock':
     819
     820                     $pretext = substr($this->content, $this->offset, $startpos - $this->offset);
     821                     $this->controlcheck($pretext);
     822                     $this->blocks[] = $pretext;
     823
     824                     if ($tag == 'cached' || $tag == 'cacheblock') {
     825                         list($keyparts, $conditional, $condition) = $this->parseargs(@$matches[2][0]);
     826                         $parser = new SSViewer_Cached_PartialParser($this->template, $this->content, $endpos, $keyparts, $conditional, $condition);
     827                     }
     828                     else {
     829                         $parser = new SSViewer_PartialParser($this->template, $this->content, $endpos);
     830                     }
     831
     832                     $parser->parse();
     833                     $this->blocks[] = $parser;
     834                     $this->offset = $parser->offset;
     835                     break;
     836
     837                 case 'end_cached':
     838                 case 'end_cacheblock':
     839                 case 'end_uncached':
     840                     $this->blocks[] = substr($this->content, $this->offset, $startpos - $this->offset);
     841                     $this->content = null;
     842
     843                     $this->offset = $endpos;
     844                     return $this;
     845             }
     846         }
     847
     848         $this->blocks[] = substr($this->content, $this->offset);
     849         $this->content = null;
     850     }
     851
     852     function parseargs($string) {
     853         preg_match_all(self::$argument_splitter, $string, $matches, PREG_SET_ORDER);
     854
     855         $parts = array();
     856         $conditional = null; $condition = null;
     857
     858         $current = '$item->';
     859
     860         while (strlen($string) && preg_match(self::$argument_splitter, $string, $match)) {
     861
     862             $string = substr($string, strlen($match[0]));
     863
     864             // If this is a conditional keyword, break, and the next loop will grab the conditional
     865             if (@$match['conditional']) {
     866                 $conditional = $match['conditional'];
     867                 continue;
     868             }
     869
     870             // If it's a property lookup or a function call
     871             if (@$match['property']) {
     872                 // Get the property
     873                 $what = $match['identifier'];
     874                 $args = array();
     875
     876                 // Extract any arguments passed to the function call
     877                 if (@$match['arguments']) {
     878                     foreach (explode(',', $match['arguments']) as $arg) {
     879                         $args[] = is_numeric($arg) ? (string)$arg : '"'.$arg.'"';
     880                     }
     881                 }
     882
     883                 $args = empty($args) ? 'null' : 'array('.implode(',',$args).')';
     884
     885                 // If this fragment ended with '.', then there's another lookup coming, so return an obj for that lookup
     886                 if (@$match['fullstop']) {
     887                     $current .= "obj('$what', $args, true)->";
     888                 }
     889                 // Otherwise this is the end of the lookup chain, so add the resultant value to the key array and reset the key-get php fragement
     890                 else {
     891                     $accessor = $current . "XML_val('$what', $args, true)"; $current = '$item->';
     892
     893                     // If we've hit a conditional already, this is the condition. Set it and be done.
     894                     if ($conditional) {
     895                         $condition = $accessor;
     896                         break;
     897                     }
     898                     // Otherwise we're another key component. Add it to array.
     899                     else $parts[] = $accessor;
     900                 }
     901             }
     902
     903             // Else it's a quoted string of some kind
     904             else if (@$match['sqstring']) $parts[] = $match['sqstring'];
     905             else if (@$match['dqstring']) $parts[] = $match['dqstring'];
     906         }
     907
     908         if ($conditional && !$condition) {
     909             throw new Exception("You need to have a condition after the conditional $conditional in your cache block");
     910         }
     911
     912         return array($parts, $conditional, $condition);
     913     }
     914
     915     function generate() {
     916         $res = array();
     917
     918         foreach ($this->blocks as $i => $block) {
     919             if ($block instanceof SSViewer_PartialParser)
     920                 $res[] = $block->generate();
     921             else {
     922                 $res[] = $block;
     923             }
     924         }
     925
     926         return implode('', $res);
     927     }
     928 }
     929
     930 /**
     931  * @package sapphire
     932  * @subpackage view
     933  */
     934 class SSViewer_Cached_PartialParser extends SSViewer_PartialParser {
     935
     936     function __construct($template, $content, $offset, $keyparts, $conditional, $condition) {
     937         $this->keyparts = $keyparts;
     938         $this->conditional = $conditional;
     939         $this->condition = $condition;
     940
     941         parent::__construct($template, $content, $offset);
     942     }
     943
     944     function controlcheck($text) {
     945         $ifs = preg_match_all('/<'.'% +if +/', $text, $matches);
     946         $end_ifs = preg_match_all('/<'.'% +end_if +/', $text, $matches);
     947
     948         if ($ifs != $end_ifs) throw new Exception('You can\'t have cached or uncached blocks within condition structures');
     949
     950         $controls = preg_match_all('/<'.'% +control +/', $text, $matches);
     951         $end_controls = preg_match_all('/<'.'% +end_control +/', $text, $matches);
     952
     953         if ($controls != $end_controls) throw new Exception('You can\'t have cached or uncached blocks within control structures');
     954     }
     955
     956     function key() {
     957         if (empty($this->keyparts)) return "''";
     958         return 'sha1(' . implode(".'_'.", $this->keyparts) . ')';
     959     }
     960
     961     function generate() {
     962         $res = array();
     963         $key = $this->key();
     964
     965         $condition = "";
     966
     967         switch ($this->conditional) {
     968             case 'if':
     969                 $condition = "{$this->condition} && ";
     970                 break;
     971             case 'unless':
     972                 $condition = "!({$this->condition}) && ";
     973                 break;
     974         }
     975
     976         /* Output this set of blocks */
     977
     978         foreach ($this->blocks as $i => $block) {
     979             if ($block instanceof SSViewer_PartialParser)
     980                 $res[] = $block->generate();
     981             else {
     982                 // Include the template name and this cache block's current contents as a sha hash, so we get auto-seperation
     983                 // of cache blocks, and invalidation of the cache when the template changes
     984                 $partialkey = "'".sha1($this->template . $block)."_'.$key.'_$i'";
     985
     986                 // Try to load from cache
     987                 $res[] = "<?\n".'if ('.$condition.' ($partial = $cache->load('.$partialkey.'))) $val .= $partial;'."\n";
     988
     989                 // Cache miss - regenerate
     990                 $res[] = "else {\n";
     991                 $res[] = '$oldval = $val; $val = "";'."\n";
     992                 $res[] = "\n?>" . $block . "<?\n";
     993                 $res[] = $condition . ' $cache->save($val); $val = $oldval . $val ;'."\n";
     994                 $res[] = "}\n?>";
     995             }
     996         }
     997
     998         return implode('', $res);
     999     }
    1000 }
    1001
    1002 function supressOutput() {
    1003     return "";
    1004 }
    1005
    1006 ?>

[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. -