Version: current

      1 <?php
      2
      3 /**
      4  * Usage: SS_Object::add_extension("SiteTree", "FilesystemPublisher('static-folder', 'html')");
      5  *
      6  * Usage: To work with Subsite module you need to:
      7  * - Add FilesystemPublisher::$domain_based_caching = true; in mysite/_config.php
      8  * - Added main site host mapping in subsites/host-map.php after everytime a new subsite is created or modified
      9  *
     10  * You may also have a method $page->pagesAffectedByUnpublishing() to return other URLS
     11  * that should be de-cached if $page is unpublished.
     12  *
     13  * @see http://doc.silverstripe.com/doku.php?id=staticpublisher
     14  *
     15  * @package cms
     16  * @subpackage publishers
     17  */
     18 class FilesystemPublisher extends StaticPublisher {
     19
     20     /**
     21      * @var String
     22      */
     23     protected $destFolder = 'cache';
     24
     25     /**
     26      * @var String
     27      */
     28     protected $fileExtension = 'html';
     29
     30     /**
     31      * @var String
     32      */
     33     protected static $static_base_url = null;
     34
     35     /**
     36      * @var Boolean Use domain based cacheing (put cache files into a domain subfolder)
     37      * This must be true if you are using this with the "subsites" module.
     38      * Please note that this form of caching requires all URLs to be provided absolute
     39      * (not relative to the webroot) via {@link SiteTree->AbsoluteLink()}.
     40      */
     41     public static $domain_based_caching = false;
     42
     43     /**
     44      * Set a different base URL for the static copy of the site.
     45      * This can be useful if you are running the CMS on a different domain from the website.
     46      */
     47     static function set_static_base_url($url) {
     48         self::$static_base_url = $url;
     49     }
     50
     51     /**
     52      * @param $destFolder The folder to save the cached site into.
     53      *   This needs to be set in sapphire/static-main.php as well through the {@link $cacheBaseDir} variable.
     54      * @param $fileExtension  The file extension to use, e.g 'html'.
     55      *   If omitted, then each page will be placed in its own directory,
     56      *   with the filename 'index.html'.  If you set the extension to PHP, then a simple PHP script will
     57      *   be generated that can do appropriate cache & redirect header negotation.
     58      */
     59     function __construct($destFolder, $fileExtension = null) {
     60         // Remove trailing slash from folder
     61         if(substr($destFolder, -1) == '/') $destFolder = substr($destFolder, 0, -1);
     62
     63         $this->destFolder = $destFolder;
     64         $this->fileExtension = $fileExtension;
     65
     66         parent::__construct();
     67     }
     68
     69     /**
     70      * Transforms relative or absolute URLs to their static path equivalent.
     71      * This needs to be the same logic that's used to look up these paths through
     72      * sapphire/static-main.php. Does not include the {@link $destFolder} prefix.
     73      * Replaces various special characters in the resulting filename similar to {@link SiteTree::generateURLSegment()}.
     74      *
     75      * Examples (without $domain_based_caching):
     76      *  - http://mysite.com/mywebroot/ => /index.html (assuming your webroot is in a subfolder)
     77      *  - http://mysite.com/about-us => /about-us.html
     78      *  - http://mysite.com/parent/child => /parent/child.html
     79      *
     80      * Examples (with $domain_based_caching):
     81      *  - http://mysite.com/mywebroot/ => /mysite.com/index.html (assuming your webroot is in a subfolder)
     82      *  - http://mysite.com/about-us => /mysite.com/about-us.html
     83      *  - http://myothersite.com/about-us => /myothersite.com/about-us.html
     84      *  - http://subdomain.mysite.com/parent/child => /subdomain.mysite.com/parent/child.html
     85      *
     86      * @param Array $urls Absolute or relative URLs
     87      * @return Array Map of original URLs to filesystem paths (relative to {@link $destFolder}).
     88      */
     89     function urlsToPaths($urls) {
     90         $mappedUrls = array();
     91         foreach($urls as $url) {
     92             $urlParts = @parse_url($url);
     93
     94             // Remove base folders from the URL if webroot is hosted in a subfolder (same as static-main.php)
     95             $path = isset($urlParts['path']) ? $urlParts['path'] : '';
     96             if(substr(strtolower($path), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) {
     97                 $urlSegment = substr($path, strlen(BASE_URL));
     98             } else {
     99                 $urlSegment = $path;
    100             }
    101
    102             // perform similar transformations to SiteTree::generateURLSegment()
    103             $urlSegment = str_replace('&amp;','-and-',$urlSegment);
    104             $urlSegment = str_replace('&','-and-',$urlSegment);
    105             $urlSegment = preg_replace('![^A-Za-z0-9\/-]+!','-',$urlSegment);
    106             $urlSegment = preg_replace('!-+!','-',$urlSegment);
    107             $urlSegment = trim($urlSegment, '/');
    108
    109             $filename = $urlSegment ? "$urlSegment.$this->fileExtension" : "index.$this->fileExtension";
    110
    111             if (self::$domain_based_caching) {
    112                 if (!$urlParts) continue; // seriously malformed url here...
    113                 $filename = $urlParts['host'] . '/' . $filename;
    114             }
    115
    116             $mappedUrls[$url] = ((dirname($filename) == '/') ? '' :  (dirname($filename).'/')).basename($filename);
    117         }
    118
    119         return $mappedUrls;
    120     }
    121
    122     function unpublishPages($urls) {
    123         // Do we need to map these?
    124         // Detect a numerically indexed arrays
    125         if (is_numeric(join('', array_keys($urls)))) $urls = $this->urlsToPaths($urls);
    126
    127         // This can be quite memory hungry and time-consuming
    128         // @todo - Make a more memory efficient publisher
    129         increase_time_limit_to();
    130         increase_memory_limit_to();
    131
    132         $cacheBaseDir = $this->getDestDir();
    133
    134         foreach($urls as $url => $path) {
    135             if (file_exists($cacheBaseDir.'/'.$path)) {
    136                 @unlink($cacheBaseDir.'/'.$path);
    137             }
    138         }
    139     }
    140
    141     function publishPages($urls) {
    142         // Do we need to map these?
    143         // Detect a numerically indexed arrays
    144         if (is_numeric(join('', array_keys($urls)))) $urls = $this->urlsToPaths($urls);
    145
    146         // This can be quite memory hungry and time-consuming
    147         // @todo - Make a more memory efficient publisher
    148         increase_time_limit_to();
    149         increase_memory_limit_to();
    150
    151         // Set the appropriate theme for this publication batch.
    152         // This may have been set explicitly via StaticPublisher::static_publisher_theme,
    153         // or we can use the last non-null theme.
    154         if(!StaticPublisher::static_publisher_theme())
    155             SSViewer::set_theme(SSViewer::current_custom_theme());
    156         else
    157             SSViewer::set_theme(StaticPublisher::static_publisher_theme());
    158
    159         $currentBaseURL = Director::baseURL();
    160         if(self::$static_base_url) Director::setBaseURL(self::$static_base_url);
    161         if($this->fileExtension == 'php') SSViewer::setOption('rewriteHashlinks', 'php');
    162         if(StaticPublisher::echo_progress()) echo $this->class.": Publishing to " . self::$static_base_url . "\n";
    163         $files = array();
    164         $i = 0;
    165         $totalURLs = sizeof($urls);
    166
    167         foreach($urls as $url => $path) {
    168
    169             if(self::$static_base_url) Director::setBaseURL(self::$static_base_url);
    170             $i++;
    171
    172             if($url && !is_string($url)) {
    173                 user_error("Bad url:" . var_export($url,true), E_USER_WARNING);
    174                 continue;
    175             }
    176
    177             if(StaticPublisher::echo_progress()) {
    178                 echo " * Publishing page $i/$totalURLs: $url\n";
    179                 flush();
    180             }
    181
    182             Requirements::clear();
    183
    184             if(Director::is_relative_url($url)) $url = Director::absoluteURL($url);
    185             $response = Director::test(str_replace('+', ' ', $url));
    186
    187             Requirements::clear();
    188
    189
    190
    191             singleton('DataObject')->flushCache();
    192
    193             // Generate file content
    194             // PHP file caching will generate a simple script from a template
    195             if($this->fileExtension == 'php') {
    196                 if(is_object($response)) {
    197                     if($response->getStatusCode() == '301' || $response->getStatusCode() == '302') {
    198                         $content = $this->generatePHPCacheRedirection($response->getHeader('Location'));
    199                     } else {
    200                         $content = $this->generatePHPCacheFile($response->getBody(), HTTP::get_cache_age(), date('Y-m-d H:i:s'));
    201                     }
    202                 } else {
    203                     $content = $this->generatePHPCacheFile($response . '', HTTP::get_cache_age(), date('Y-m-d H:i:s'));
    204                 }
    205
    206             // HTML file caching generally just creates a simple file
    207             } else {
    208                 if(is_object($response)) {
    209                     if($response->getStatusCode() == '301' || $response->getStatusCode() == '302') {
    210                         $absoluteURL = Director::absoluteURL($response->getHeader('Location'));
    211                         $content = "<meta http-equiv=\"refresh\" content=\"2; URL=$absoluteURL\">";
    212                     } else {
    213                         $content = $response->getBody();
    214                     }
    215                 } else {
    216                     $content = $response . '';
    217                 }
    218             }
    219
    220             $files[] = array(
    221                 'Content' => $content,
    222                 'Folder' => dirname($path).'/',
    223                 'Filename' => basename($path),
    224             );
    225
    226             // Add externals
    227             /*
    228             $externals = $this->externalReferencesFor($content);
    229             if($externals) foreach($externals as $external) {
    230                 // Skip absolute URLs
    231                 if(preg_match('/^[a-zA-Z]+:\/\//', $external)) continue;
    232                 // Drop querystring parameters
    233                 $external = strtok($external, '?');
    234
    235                 if(file_exists("../" . $external)) {
    236                     // Break into folder and filename
    237                     if(preg_match('/^(.*\/)([^\/]+)$/', $external, $matches)) {
    238                         $files[$external] = array(
    239                             "Copy" => "../$external",
    240                             "Folder" => $matches[1],
    241                             "Filename" => $matches[2],
    242                         );
    243
    244                     } else {
    245                         user_error("Can't parse external: $external", E_USER_WARNING);
    246                     }
    247                 } else {
    248                     $missingFiles[$external] = true;
    249                 }
    250             }*/
    251         }
    252
    253         if(self::$static_base_url) Director::setBaseURL($currentBaseURL);
    254         if($this->fileExtension == 'php') SSViewer::setOption('rewriteHashlinks', true);
    255
    256         $base = BASE_PATH . "/$this->destFolder";
    257         foreach($files as $file) {
    258             Filesystem::makeFolder("$base/$file[Folder]");
    259
    260             if(isset($file['Content'])) {
    261                 $fh = fopen("$base/$file[Folder]$file[Filename]", "w");
    262                 fwrite($fh, $file['Content']);
    263                 fclose($fh);
    264             } else if(isset($file['Copy'])) {
    265                 copy($file['Copy'], "$base/$file[Folder]$file[Filename]");
    266             }
    267         }
    268     }
    269
    270     /**
    271      * Generate the templated content for a PHP script that can serve up the given piece of content with the given age and expiry
    272      */
    273     protected function generatePHPCacheFile($content, $age, $lastModified) {
    274         $template = file_get_contents(BASE_PATH . '/cms/code/staticpublisher/CachedPHPPage.tmpl');
    275         return str_replace(
    276                 array('**MAX_AGE**', '**LAST_MODIFIED**', '**CONTENT**'),
    277                 array((int)$age, $lastModified, $content),
    278                 $template);
    279     }
    280     /**
    281      * Generate the templated content for a PHP script that can serve up a 301 redirect to the given destionation
    282      */
    283     protected function generatePHPCacheRedirection($destination) {
    284         $template = file_get_contents(BASE_PATH . '/cms/code/staticpublisher/CachedPHPRedirection.tmpl');
    285         return str_replace(
    286                 array('**DESTINATION**'),
    287                 array($destination),
    288                 $template);
    289     }
    290
    291     public function getDestDir() {
    292         return BASE_PATH . '/' . $this->destFolder;
    293     }
    294
    295     /**
    296      * Return an array of all the existing static cache files, as a map of URL => file.
    297      * Only returns cache files that will actually map to a URL, based on urlsToPaths.
    298      */
    299     public function getExistingStaticCacheFiles() {
    300         $cacheDir = BASE_PATH . '/' . $this->destFolder;
    301
    302         $urlMapper = array_flip($this->urlsToPaths($this->owner->allPagesToCache()));
    303
    304         $output = array();
    305
    306         // Glob each dir, then glob each one of those
    307         foreach(glob("$cacheDir/*", GLOB_ONLYDIR) as $cacheDir) {
    308             foreach(glob($cacheDir.'/*') as $cacheFile) {
    309                 $mapKey = str_replace(BASE_PATH . "/cache/","",$cacheFile);
    310                 if(isset($urlMapper[$mapKey])) {
    311                     $url = $urlMapper[$mapKey];
    312                     $output[$url] = $cacheFile;
    313                 }
    314             }
    315         }
    316
    317         return $output;
    318     }
    319
    320 }
    321
    322 ?>

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