AbsoluteLink()}. */ private 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. * * @deprecated 3.2 Use the "FilesystemPublisher.static_base_url" config setting instead */ public static function set_static_base_url($url) { Deprecation::notice('3.2', 'Use the "FilesystemPublisher.static_base_url" config setting instead'); Config::inst()->update('FilesystemPublisher', '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; if ($fileExtension) { $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 * framework/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 (assuming your webroot is in a subfolder) * - 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 filesystem 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 (Config::inst()->get('FilesystemPublisher', 'domain_based_caching')) { if (!$urlParts) { continue; } // seriously malformed url here... $filename = $urlParts['host'] . '/' . $filename; } $mappedUrls[$url] = ((dirname($filename) == '/') ? '' : (dirname($filename).'/')).basename($filename); } return $mappedUrls; } /** * @param array $urls */ 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); } } } /** * Uses {@link Director::test()} to perform in-memory HTTP requests * on the passed-in URLs. * * @param array $urls Relative URLs * @return array Result, keyed by URL. Keys: * - "statuscode": The HTTP status code * - "redirect": A redirect location (if applicable) * - "path": The filesystem path where the cache has been written */ public function publishPages($urls) { // Save current stage and temporarily force it to Live $oldStage = Versioned::current_stage(); Versioned::reading_stage("Live"); $result = array(); //nest the config so we can make changes to the config and revert easily Config::nest(); // 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. $customTheme = Config::inst()->get('StaticPublisher', 'static_publisher_theme'); if ($customTheme) { Config::inst()->update('SSViewer', 'theme', $customTheme); } // Ensure that the theme that is set gets used. Config::inst()->update('SSViewer', 'theme_enabled', true); $staticBaseUrl = Config::inst()->get('FilesystemPublisher', 'static_base_url'); if ($staticBaseUrl) { Config::inst()->update('Director', 'alternate_base_url', $staticBaseUrl); } if ($this->fileExtension == 'php') { Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php'); } if (Config::inst()->get('StaticPublisher', 'echo_progress')) { echo $this->class.": Publishing to " . $staticBaseUrl . "\n"; } $files = array(); $i = 0; $totalURLs = sizeof($urls); foreach ($urls as $url => $path) { $origUrl = $url; $result[$origUrl] = array( 'statuscode' => null, 'redirect' => null, 'path' => null ); $i++; if ($url && !is_string($url)) { user_error("Bad url:" . var_export($url, true), E_USER_WARNING); continue; } if (Config::inst()->get('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)); if (!$response) { continue; } if ($response) { $result[$origUrl]['statuscode'] = $response->getStatusCode(); } Requirements::clear(); singleton('DataObject')->flushCache(); // Check for ErrorPages generating output - we want to handle this in a special way below. $isErrorPage = false; $pageObject = null; if ($response && is_object($response) && ((int)$response->getStatusCode())>=400) { $pageObject = SiteTree::get_by_link($url); if ($pageObject && $pageObject instanceof ErrorPage) { $isErrorPage = true; } } // Skip any responses with a 404 status code unless it's the ErrorPage itself. if (!$isErrorPage && is_object($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'), $response->getHeader('Content-Type')); } } else { $content = $this->generatePHPCacheFile($response . '', HTTP::get_cache_age(), date('Y-m-d H:i:s'), $response->getHeader('Content-Type')); } // 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')); $result[$origUrl]['redirect'] = $response->getHeader('Location'); $content = ""; } else { $content = $response->getBody(); } } else { $content = $response . ''; } } if (Config::inst()->get('StaticPublisher', 'include_caching_metadata')) { $content = str_replace( '', sprintf("\n\n", implode(" ", $this->getMetadata($url))), $content ); } if (!$isErrorPage) { $files[$origUrl] = array( 'Content' => $content, 'Folder' => dirname($path).'/', 'Filename' => basename($path), ); } else { // Generate a static version of the error page with a standardised name, so they can be plugged // into catch-all webserver statements such as Apache's ErrorDocument. $code = (int)$response->getStatusCode(); $files[$origUrl] = array( 'Content' => $content, 'Folder' => dirname($path).'/', 'Filename' => "error-$code.html", ); } } //return config to its previous state Config::unnest(); $base = BASE_PATH . "/$this->destFolder"; foreach ($files as $origUrl => $file) { Filesystem::makeFolder("$base/$file[Folder]"); $path = "$base/$file[Folder]$file[Filename]"; $result[$origUrl]['path'] = $path; if (isset($file['Content'])) { $fh = fopen($path, "w"); fwrite($fh, $file['Content']); fclose($fh); } elseif (isset($file['Copy'])) { copy($file['Copy'], $path); } } // Revert the stage back to what it was Versioned::reading_stage($oldStage); return $result; } /** * 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 $content * @param string $age * @param string $lastModified * @param string $contentType * * @return string */ protected function generatePHPCacheFile($content, $age, $lastModified, $contentType) { $template = file_get_contents(STATIC_MODULE_DIR . '/code/CachedPHPPage.tmpl'); return str_replace( array('**MAX_AGE**', '**LAST_MODIFIED**', '**CONTENT**', '**CONTENT_TYPE**'), array((int)$age, $lastModified, $content, $contentType), $template ); } /** * Generate the templated content for a PHP script that can serve up a 301 * redirect to the given destination. * * @param string $destination * * @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; } }