From c3c55c4b0eabc06eb8e2758f1089aca51ba06d17 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Fri, 21 Sep 2012 17:47:26 +1200 Subject: [PATCH] Initial import --- LICENSE | 24 ++ README.md | 18 + _config.php | 3 + code/CachedPHPPage.tmpl | 31 ++ code/CachedPHPRedirection.tmpl | 23 ++ code/controllers/StaticExporter.php | 167 +++++++++ code/extensions/FilesystemPublisher.php | 360 ++++++++++++++++++++ code/extensions/RsyncMultiHostPublisher.php | 63 ++++ code/extensions/StaticPublisher.php | 157 +++++++++ code/static-main.php | 118 +++++++ tasks/StaticExporterTask.php | 26 ++ 11 files changed, 990 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 _config.php create mode 100644 code/CachedPHPPage.tmpl create mode 100644 code/CachedPHPRedirection.tmpl create mode 100644 code/controllers/StaticExporter.php create mode 100644 code/extensions/FilesystemPublisher.php create mode 100644 code/extensions/RsyncMultiHostPublisher.php create mode 100644 code/extensions/StaticPublisher.php create mode 100644 code/static-main.php create mode 100644 tasks/StaticExporterTask.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b4b9641 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +* Copyright (c) 2012, SilverStripe Ltd. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of the SilverStripe nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY SilverStripe Ltd. ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL SilverStripe Ltd. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..886e188 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Static + +## Introduction + +Static provides several extensions for exporting a SilverStripe application to +both local or remote file systems + +## Maintainer Contact + + * Will Rossiter (Nickname: wrossiter, willr) `` + * Sam MinneƩ (Nickname: sminnee) + +## Requirements + + * SilverStripe 3.1 + * Tar archive + +Note this is untested on Windows. \ No newline at end of file diff --git a/_config.php b/_config.php new file mode 100644 index 0000000..c79d6e3 --- /dev/null +++ b/_config.php @@ -0,0 +1,3 @@ + 0) { + header("Cache-Control: max-age=" . MAX_AGE); + header("Pragma:"); +} else { + header("Cache-Control: no-cache, max-age=0, must-revalidate"); +} + +header("Expires: " . gmdate('D, d M Y H:i:s', time() + MAX_AGE) . ' GMT'); +header("Last-modified: " . gmdate('D, d M Y H:i:s', strtotime(LAST_MODIFIED)) . ' GMT'); + +if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + if(strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= strtotime(LAST_MODIFIED)) { + header("Last-modified: " . gmdate('D, d M Y H:i:s', strtotime(LAST_MODIFIED)) . ' GMT', true, 304); + exit; + } +} + +?> +**CONTENT** diff --git a/code/CachedPHPRedirection.tmpl b/code/CachedPHPRedirection.tmpl new file mode 100644 index 0000000..990bb77 --- /dev/null +++ b/code/CachedPHPRedirection.tmpl @@ -0,0 +1,23 @@ + 0) { + header("Cache-Control: max-age=" . MAX_AGE); + header("Pragma:"); +} else { + header("Cache-Control: no-cache, max-age=0, must-revalidate"); +} + +header("Expires: " . gmdate('D, d M Y H:i:s', time() + MAX_AGE) . ' GMT'); +header("Location: " . DESTINATION, true, 301); + +?> diff --git a/code/controllers/StaticExporter.php b/code/controllers/StaticExporter.php new file mode 100644 index 0000000..9bf125d --- /dev/null +++ b/code/controllers/StaticExporter.php @@ -0,0 +1,167 @@ + _t('StaticExporter.NAME','Static exporter'), + 'Form' => $this->StaticExportForm()->forTemplate() + ); + } + + public function StaticExportForm() { + $form = new Form($this, 'StaticExportForm', new FieldList( + new TextField('baseurl', _t('StaticExporter.BASEURL','Base URL')) + ), new FieldList( + new FormAction('export', _t('StaticExporter.EXPORT','Export')) + )); + + return $form; + } + + + public function export() { + if(isset($_REQUEST['baseurl'])) { + $base = $_REQUEST['baseurl']; + if(substr($base,-1) != '/') $base .= '/'; + } + else { + $base = Director::baseURL(); + } + + $folder = TEMP_FOLDER . '/static-export'; + $project = project(); + + $exported = $this->doExport($base, $folder .'/'. $project, false); + + `cd $folder; tar -czhf $project-export.tar.gz $project`; + + $archiveContent = file_get_contents("$folder/$project-export.tar.gz"); + + + // return as download to the client + $response = SS_HTTPRequest::send_file( + $archiveContent, + "$project-export.tar.gz", + 'application/x-tar-gz' + ); + + echo $response->output(); + } + + /** + * Exports the website with the given base url. Returns the path where the + * exported version of the website is located. + * + * @param string website base url + * @param string folder to export the site into + * @param bool symlink assets + * @param bool suppress output progress + * + * @return string path to export + */ + public function doExport($base, $folder, $symlink = true, $quiet = true) { + ini_set('max_execution_time', 0); + Director::setBaseURL($base); + + if(is_dir($folder)) { + Filesystem::removeFolder($folder); + } + + Filesystem::makeFolder($folder); + + // symlink or copy /assets + $f1 = ASSETS_PATH; + $f2 = Director::baseFolder() . '/' . project(); + + if($symlink) { + `cd $folder; ln -s $f1; ln -s $f2`; + } + else { + `cp -R $f1 $folder; cp -R $f2 $folder`; + } + + // iterate through items we need to export + $objs = $this->getObjectsToExport(); + + if($objs) { + $total = $objs->count(); + $i = 1; + + foreach($objs as $obj) { + $link = $obj->RelativeLink(null, true); + + $subfolder = "$folder/" . trim($link, '/'); + $contentfile = "$folder/" . trim($link, '/') . '/index.html'; + + // Make the folder + if(!file_exists($subfolder)) { + Filesystem::makeFolder($subfolder); + } + + // Run the page + Requirements::clear(); + $link = Director::makeRelative($obj->Link()); + $response = Director::test($link); + + // Write to file + if($fh = fopen($contentfile, 'w')) { + if(!$quiet) printf("-- (%s/%s) Outputting page (%s)%s", $i, $total, $obj->RelativeLink(null, true), PHP_EOL); + + fwrite($fh, $response->getBody()); + fclose($fh); + } + + $i++; + } + } + + return $folder; + } + + /** + * Return a list of publishable instances for the exporter to include. The + * only requirement is that for this list of objects, each one implements + * the RelativeLink() and Link() method. + * + * @return SS_List + */ + public function getObjectsToExport() { + $objs = SiteTree::get(); + $this->extend('alterObjectsToExport', $objs); + + return $objs; + } +} \ No newline at end of file diff --git a/code/extensions/FilesystemPublisher.php b/code/extensions/FilesystemPublisher.php new file mode 100644 index 0000000..5919503 --- /dev/null +++ b/code/extensions/FilesystemPublisher.php @@ -0,0 +1,360 @@ +pagesAffectedByUnpublishing() to return other URLS + * that should be de-cached if $page is unpublished. + * + * @see http://doc.silverstripe.com/doku.php?id=staticpublisher + * + * @package cms + * @subpackage publishers + */ +class FilesystemPublisher extends StaticPublisher { + + /** + * @var string + */ + protected $destFolder = 'cache'; + + /** + * @var string + */ + protected $fileExtension = 'html'; + + /** + * @var string + */ + protected static $static_base_url = null; + + /** + * Use domain based caching (put cache files into a domain subfolder). + * + * This must be true if you are using this with the "subsites" module. + * + * Please note that this form of caching requires all URLs to be provided absolute + * (not relative to the webroot) via {@link SiteTree->AbsoluteLink()}. + * + * @var boolean + */ + public static $domain_based_caching = false; + + /** + * Set a different base URL for the static copy of the site. + * + * This can be useful if you are running the CMS on a different domain + * from the website. + * + * @param string + */ + public static function set_static_base_url($url) { + self::$static_base_url = $url; + } + + /** + * @param $destFolder The folder to save the cached site into. + * This needs to be set in framework/static-main.php as well through the {@link $cacheBaseDir} variable. + * @param $fileExtension The file extension to use, e.g 'html'. + * If omitted, then each page will be placed in its own directory, + * with the filename 'index.html'. If you set the extension to PHP, then a simple PHP script will + * be generated that can do appropriate cache & redirect header negotation. + */ + public function __construct($destFolder = 'cache', $fileExtension = null) { + // Remove trailing slash from folder + if(substr($destFolder, -1) == '/') $destFolder = substr($destFolder, 0, -1); + + $this->destFolder = $destFolder; + $this->fileExtension = $fileExtension; + + parent::__construct(); + } + + /** + * Transforms relative or absolute URLs to their static path equivalent. + * + * This needs to be the same logic that's used to look up these paths through + * static-main.php. Does not include the {@link $destFolder} prefix. + * + * URL filtering will have already taken place for direct SiteTree links via + * SiteTree->generateURLSegment()). For all other links (e.g. custom + * controller actions), we assume that they're pre-sanitized to suit the + * filesystem needs, as its impossible to sanitize them without risking to + * break the underlying naming assumptions in URL routing (e.g. controller + * method names). + * + * Examples (without $domain_based_caching): + * - http://mysite.com/mywebroot/ => /index.html + * - http://mysite.com/about-us => /about-us.html + * - http://mysite.com/parent/child => /parent/child.html + * + * Examples (with $domain_based_caching): + * - http://mysite.com/mywebroot/ => /mysite.com/index.html (assuming your webroot is in a subfolder) + * - http://mysite.com/about-us => /mysite.com/about-us.html + * - http://myothersite.com/about-us => /myothersite.com/about-us.html + * - http://subdomain.mysite.com/parent/child => /subdomain.mysite.com/parent/child.html + * + * @param array $urls Absolute or relative URLs + * + * @return array Map of original URLs to file system paths (relative to {@link $destFolder}). + */ + public function urlsToPaths($urls) { + $mappedUrls = array(); + + foreach($urls as $url) { + + // parse_url() is not multibyte safe, see https://bugs.php.net/bug.php?id=52923. + // We assume that the URL hsa been correctly encoded either on storage (for SiteTree->URLSegment), + // or through URL collection (for controller method names etc.). + $urlParts = @parse_url($url); + + // Remove base folders from the URL if webroot is hosted in a subfolder (same as static-main.php) + $path = isset($urlParts['path']) ? $urlParts['path'] : ''; + if(mb_substr(mb_strtolower($path), 0, mb_strlen(BASE_URL)) == mb_strtolower(BASE_URL)) { + $urlSegment = mb_substr($path, mb_strlen(BASE_URL)); + } else { + $urlSegment = $path; + } + + // Normalize URLs + $urlSegment = trim($urlSegment, '/'); + + $filename = $urlSegment ? "$urlSegment.$this->fileExtension" : "index.$this->fileExtension"; + + if (self::$domain_based_caching) { + if (!$urlParts) continue; // seriously malformed url here... + $filename = $urlParts['host'] . '/' . $filename; + } + + $mappedUrls[$url] = ((dirname($filename) == '/') ? '' : (dirname($filename).'/')).basename($filename); + } + + return $mappedUrls; + } + + public function unpublishPages($urls) { + // Do we need to map these? + // Detect a numerically indexed arrays + if (is_numeric(join('', array_keys($urls)))) $urls = $this->urlsToPaths($urls); + + // This can be quite memory hungry and time-consuming + // @todo - Make a more memory efficient publisher + increase_time_limit_to(); + increase_memory_limit_to(); + + $cacheBaseDir = $this->getDestDir(); + + foreach($urls as $url => $path) { + if (file_exists($cacheBaseDir.'/'.$path)) { + @unlink($cacheBaseDir.'/'.$path); + } + } + } + + public function publishPages($urls) { + // Do we need to map these? + // Detect a numerically indexed arrays + if (is_numeric(join('', array_keys($urls)))) $urls = $this->urlsToPaths($urls); + + // This can be quite memory hungry and time-consuming + // @todo - Make a more memory efficient publisher + increase_time_limit_to(); + increase_memory_limit_to(); + + // Set the appropriate theme for this publication batch. + // This may have been set explicitly via StaticPublisher::static_publisher_theme, + // or we can use the last non-null theme. + if(!StaticPublisher::static_publisher_theme()) + SSViewer::set_theme(SSViewer::current_custom_theme()); + else + SSViewer::set_theme(StaticPublisher::static_publisher_theme()); + + $currentBaseURL = Director::baseURL(); + if(self::$static_base_url) Director::setBaseURL(self::$static_base_url); + if($this->fileExtension == 'php') SSViewer::setOption('rewriteHashlinks', 'php'); + if(StaticPublisher::echo_progress()) echo $this->class.": Publishing to " . self::$static_base_url . "\n"; + $files = array(); + $i = 0; + $totalURLs = sizeof($urls); + + foreach($urls as $url => $path) { + + if(self::$static_base_url) Director::setBaseURL(self::$static_base_url); + $i++; + + if($url && !is_string($url)) { + user_error("Bad url:" . var_export($url,true), E_USER_WARNING); + continue; + } + + if(StaticPublisher::echo_progress()) { + echo " * Publishing page $i/$totalURLs: $url\n"; + flush(); + } + + Requirements::clear(); + + if($url == "") $url = "/"; + if(Director::is_relative_url($url)) $url = Director::absoluteURL($url); + $response = Director::test(str_replace('+', ' ', $url)); + + Requirements::clear(); + + singleton('DataObject')->flushCache(); + + //skip any responses with a 404 status code. We don't want to turn those into statically cached pages + if (!$response || $response->getStatusCode() == '404') continue; + + // Generate file content + // PHP file caching will generate a simple script from a template + if($this->fileExtension == 'php') { + if(is_object($response)) { + if($response->getStatusCode() == '301' || $response->getStatusCode() == '302') { + $content = $this->generatePHPCacheRedirection($response->getHeader('Location')); + } else { + $content = $this->generatePHPCacheFile($response->getBody(), HTTP::get_cache_age(), date('Y-m-d H:i:s')); + } + } else { + $content = $this->generatePHPCacheFile($response . '', HTTP::get_cache_age(), date('Y-m-d H:i:s')); + } + + // HTML file caching generally just creates a simple file + } else { + if(is_object($response)) { + if($response->getStatusCode() == '301' || $response->getStatusCode() == '302') { + $absoluteURL = Director::absoluteURL($response->getHeader('Location')); + $content = ""; + } else { + $content = $response->getBody(); + } + } else { + $content = $response . ''; + } + } + + $files[] = array( + 'Content' => $content, + 'Folder' => dirname($path).'/', + 'Filename' => basename($path), + ); + + // Add externals + /* + $externals = $this->externalReferencesFor($content); + if($externals) foreach($externals as $external) { + // Skip absolute URLs + if(preg_match('/^[a-zA-Z]+:\/\//', $external)) continue; + // Drop querystring parameters + $external = strtok($external, '?'); + + if(file_exists("../" . $external)) { + // Break into folder and filename + if(preg_match('/^(.*\/)([^\/]+)$/', $external, $matches)) { + $files[$external] = array( + "Copy" => "../$external", + "Folder" => $matches[1], + "Filename" => $matches[2], + ); + + } else { + user_error("Can't parse external: $external", E_USER_WARNING); + } + } else { + $missingFiles[$external] = true; + } + }*/ + } + + if(self::$static_base_url) Director::setBaseURL($currentBaseURL); + if($this->fileExtension == 'php') SSViewer::setOption('rewriteHashlinks', true); + + $base = BASE_PATH . "/$this->destFolder"; + + foreach($files as $file) { + Filesystem::makeFolder("$base/$file[Folder]"); + + if(isset($file['Content'])) { + $fh = fopen("$base/$file[Folder]$file[Filename]", "w"); + fwrite($fh, $file['Content']); + fclose($fh); + } else if(isset($file['Copy'])) { + copy($file['Copy'], "$base/$file[Folder]$file[Filename]"); + } + } + } + + /** + * Generate the templated content for a PHP script that can serve up the + * given piece of content with the given age and expiry. + * + * @param string + * @param int + * @param string + */ + protected function generatePHPCacheFile($content, $age, $lastModified) { + $template = file_get_contents(STATIC_MODULE_DIR . '/code/CachedPHPPage.tmpl'); + + return str_replace( + array('**MAX_AGE**', '**LAST_MODIFIED**', '**CONTENT**'), + array((int)$age, $lastModified, $content), + $template); + } + + /** + * Generate the templated content for a PHP script that can serve up a 301 + * redirect to the given destination. + * + * @param string + * @return string + */ + protected function generatePHPCacheRedirection($destination) { + $template = file_get_contents(STATIC_MODULE_DIR . '/code/CachedPHPRedirection.tmpl'); + + return str_replace( + array('**DESTINATION**'), + array($destination), + $template + ); + } + + /** + * @return string + */ + public function getDestDir() { + return BASE_PATH . '/' . $this->destFolder; + } + + /** + * Return an array of all the existing static cache files, as a map of + * URL => file. Only returns cache files that will actually map to a URL, + * based on urlsToPaths. + * + * @return array + */ + public function getExistingStaticCacheFiles() { + $cacheDir = BASE_PATH . '/' . $this->destFolder; + + $urlMapper = array_flip( + $this->urlsToPaths($this->owner->allPagesToCache()) + ); + + $output = array(); + + // Glob each dir, then glob each one of those + foreach(glob("$cacheDir/*", GLOB_ONLYDIR) as $cacheDir) { + foreach(glob($cacheDir.'/*') as $cacheFile) { + $mapKey = str_replace(BASE_PATH . "/cache/","",$cacheFile); + if(isset($urlMapper[$mapKey])) { + $url = $urlMapper[$mapKey]; + $output[$url] = $cacheFile; + } + } + } + + return $output; + } +} \ No newline at end of file diff --git a/code/extensions/RsyncMultiHostPublisher.php b/code/extensions/RsyncMultiHostPublisher.php new file mode 100644 index 0000000..1838316 --- /dev/null +++ b/code/extensions/RsyncMultiHostPublisher.php @@ -0,0 +1,63 @@ +republish($original); + } + + /** + * Called after link assets have been renamed, and the live site has been updated, without + * an actual publish event. + * + * Only called if the published content exists and has been modified. + */ + function onRenameLinkedAsset($original) { + $this->republish($original); + } + + function republish($original) { + if (self::$disable_realtime) return; + + $urls = array(); + + if($this->owner->hasMethod('pagesAffectedByChanges')) { + $urls = $this->owner->pagesAffectedByChanges($original); + } else { + $pages = Versioned::get_by_stage('SiteTree', 'Live', '', '', '', 10); + if($pages) { + foreach($pages as $page) { + $urls[] = $page->AbsoluteLink(); + } + } + } + + // Note: Similiar to RebuildStaticCacheTask->rebuildCache() + foreach($urls as $i => $url) { + if(!is_string($url)) { + user_error("Bad URL: " . var_export($url, true), E_USER_WARNING); + continue; + } + + // Remove leading slashes from all URLs (apart from the homepage) + if(substr($url,-1) == '/' && $url != '/') $url = substr($url,0,-1); + + $urls[$i] = $url; + } + + $urls = array_unique($urls); + + $this->publishPages($urls); + } + + /** + * On after unpublish, get changes and hook into underlying + * functionality + */ + function onAfterUnpublish($page) { + if (self::$disable_realtime) return; + + // Get the affected URLs + if($this->owner->hasMethod('pagesAffectedByUnpublishing')) { + $urls = $this->owner->pagesAffectedByUnpublishing(); + $urls = array_unique($urls); + } else { + $urls = array($this->owner->AbsoluteLink()); + } + + $legalPages = singleton('Page')->allPagesToCache(); + + $urlsToRepublish = array_intersect($urls, $legalPages); + $urlsToUnpublish = array_diff($urls, $legalPages); + + $this->unpublishPages($urlsToUnpublish); + $this->publishPages($urlsToRepublish); + } + + /** + * Get all external references to CSS, JS, + */ + function externalReferencesFor($content) { + $CLI_content = escapeshellarg($content); + $tidy = `echo $CLI_content | tidy -numeric -asxhtml`; + $tidy = preg_replace('/xmlns="[^"]+"/','', $tidy); + $xContent = new SimpleXMLElement($tidy); + //Debug::message($xContent->asXML()); + + $xlinks = array( + "//link[@rel='stylesheet']/@href" => false, + "//script/@src" => false, + "//img/@src" => false, + "//a/@href" => true, + ); + + $urls = array(); + foreach($xlinks as $xlink => $assetsOnly) { + $matches = $xContent->xpath($xlink); + if($matches) foreach($matches as $item) { + $url = $item . ''; + if($assetsOnly && substr($url,0,7) != ASSETS_DIR . '/') continue; + + $urls[] = $url; + } + } + + return $urls; + } + +} + diff --git a/code/static-main.php b/code/static-main.php new file mode 100644 index 0000000..97bd5ef --- /dev/null +++ b/code/static-main.php @@ -0,0 +1,118 @@ + + * FilesystemPublisher::$domain_based_caching = true; + * + * + * and added main site host mapping in subsites/host-map.php after everytime + * a new subsite is created or modified. + * + * If you are not using subsites, the host-map.php file will not exist (it is + * automatically generated by the Subsites module) and the cache will default + * to no subdirectory. + * + * @package static + */ + +$cacheEnabled = true; +$cacheDebug = false; +$cacheBaseDir = '../cache/'; // Should point to the same folder as FilesystemPublisher->destFolder + +// Optional settings for FilesystemPublisher::$domain_based_mapping=TRUE +$hostmapLocation = '../subsites/host-map.php'; + +// Specific to 'homepagefordomain' module +$homepageMapLocation = '../assets/_homepage-map.php'; + +if ( + $cacheEnabled + && empty($_COOKIE['bypassStaticCache']) + // No GET params other than cache relevant config is passed (e.g. "?stage=Stage"), + // which would mean that we have to bypass the cache + && count(array_diff(array_keys($_GET), array('url', 'cacheSubdir'))) == 0 + // Request is not POST (which would have to be handled dynamically) + && count($_POST) == 0 +) { + // Define system paths (copied from Core.php) + if(!defined('BASE_PATH')) { + // Assuming that this file is framework/static-main.php we can then determine the base path + define('BASE_PATH', rtrim(dirname(dirname(dirname(__FILE__)))), DIRECTORY_SEPARATOR); + } + if(!defined('BASE_URL')) { + // Determine the base URL by comparing SCRIPT_NAME to SCRIPT_FILENAME and getting common elements + $path = realpath($_SERVER['SCRIPT_FILENAME']); + + if(substr($path, 0, strlen(BASE_PATH)) == BASE_PATH) { + $urlSegmentToRemove = substr($path, strlen(BASE_PATH)); + + if(substr($_SERVER['SCRIPT_NAME'], -strlen($urlSegmentToRemove)) == $urlSegmentToRemove) { + $baseURL = substr($_SERVER['SCRIPT_NAME'], 0, -strlen($urlSegmentToRemove)); + define('BASE_URL', rtrim($baseURL, DIRECTORY_SEPARATOR)); + } + } + } + + $url = $_GET['url']; + // Remove base folders from the URL if webroot is hosted in a subfolder + if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) { + $url = substr($url, strlen(BASE_URL)); + } + + $host = str_replace('www.', '', $_SERVER['HTTP_HOST']); + + // Custom cache dir for debugging purposes + if (isset($_GET['cacheSubdir']) && !preg_match('/[^a-zA-Z0-9\-_]/', $_GET['cacheSubdir'])) { + $cacheDir = $_GET['cacheSubdir'].'/'; + } + // Custom mapping through PHP file (assumed FilesystemPublisher::$domain_based_mapping=TRUE) + else if (file_exists($hostmapLocation)) { + include_once $hostmapLocation; + $subsiteHostmap['default'] = isset($subsiteHostmap['default']) ? $subsiteHostmap['default'] : ''; + $cacheDir = (isset($subsiteHostmap[$host]) ? $subsiteHostmap[$host] : $subsiteHostmap['default']) . '/'; + } + // No subfolder (for FilesystemPublisher::$domain_based_mapping=FALSE) + else { + $cacheDir = ''; + } + + // Look for the file in the cachedir + $file = trim($url, '/'); + $file = $file ? $file : 'index'; + + // Route to the 'correct' index file (if applicable) + if ($file == 'index' && file_exists($homepageMapLocation)) { + include_once $homepageMapLocation; + $file = isset($homepageMap[$_SERVER['HTTP_HOST']]) ? $homepageMap[$_SERVER['HTTP_HOST']] : $file; + } + + // Encode each part of the path individually, in order to support multibyte paths. + // SiteTree.URLSegment and hence the static folder and filenames are stored in encoded form, + // to avoid filesystem incompatibilities. + $file = implode('/', array_map('rawurlencode', explode('/', $file))); + // Find file by extension (either *.html or *.php) + if (file_exists($cacheBaseDir . $cacheDir . $file . '.html')) { + header('X-SilverStripe-Cache: hit at '.@date('r')); + echo file_get_contents($cacheBaseDir . $cacheDir . $file . '.html'); + if ($cacheDebug) echo "

File was cached

"; + } elseif (file_exists($cacheBaseDir . $cacheDir . $file . '.php')) { + header('X-SilverStripe-Cache: hit at '.@date('r')); + include_once $cacheBaseDir . $cacheDir . $file . '.php'; + if ($cacheDebug) echo "

File was cached

"; + } else { + header('X-SilverStripe-Cache: miss at '.@date('r') . ' on ' . $cacheDir . $file); + // No cache hit... fallback to dynamic routing + include 'main.php'; + if ($cacheDebug) echo "

File was NOT cached

"; + } +} else { + // Fall back to dynamic generation via normal routing if caching has been explicitly disabled + include 'main.php'; +} + diff --git a/tasks/StaticExporterTask.php b/tasks/StaticExporterTask.php new file mode 100644 index 0000000..9df476d --- /dev/null +++ b/tasks/StaticExporterTask.php @@ -0,0 +1,26 @@ +getVar('baseurl'); + $sym = $request->getVar('symlink'); + $quiet = $request->getVar('quiet'); + $folder = $request->getVar('path'); + + if(!$folder) $folder = TEMP_FOLDER . '/static-export'; + + $url = ($url) ? $url : Director::baseURL(); + $symlink = ($sym != "false"); + $quiet = ($quiet) ? $quiet : false; + + if(!$quiet) printf("Exporting website with %s base URL... %s", $url, PHP_EOL); + $path = $export->doExport($url, $folder, $symlink, $quiet); + + if(!$quiet) printf("Completed. Website exported to %s. %s", $path, PHP_EOL); + } +} \ No newline at end of file