1 <?php
2
3 /**
4 * Usage: 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('&','-and-',$urlSegment);
104 $urlSegment = str_replace('&','-and-',$urlSegment);
105 $urlSegment = ereg_replace('[^A-Za-z0-9\/-]+','-',$urlSegment);
106 $urlSegment = ereg_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 ?>