Give dependencies a unique file extension.
[xhtml-compiler.git] / XHTMLCompiler / Page.php
bloba9e07f98a50e8745c27d6080182a2881f829e4e8
1 <?php
3 /**
4 * Represents a page in our content management system. This is loosely
5 * bound to the filesystem, although it doesn't actually refer to a
6 * specific file, just a class of files.
7 */
8 class XHTMLCompiler_Page
11 /**
12 * Filename identifier of this page without extension
14 protected $pathStem;
16 /** File extension of source files (no period) */
17 protected $sourceExt = 'xhtml';
18 /** File extension of cache/served files */
19 protected $cacheExt = 'html';
20 /** File extension of dependency files */
21 protected $depsExt = 'xc-deps';
23 /** Instance of XHTMLCompiler_File for source file */
24 protected $source;
25 /** Instance of XHTMLCompiler_File for cache file */
26 protected $cache;
27 /** Instance of XHTMLCompiler_File for dependency file */
28 protected $deps;
30 /**
31 * Constructs a page object, validates filename for correctness
32 * @param $path String path filename, can be from untrusted source
33 * @param $mute Whether or not to stop the class from complaining when
34 * the source file doesn't exist. This is a stopgap measure,
35 * please replace with better exception handling.
36 * @todo Cleanup into subroutines
37 * @todo Factor out allowed_directories realpath'ing to config class
39 public function __construct($path, $mute = false) {
41 $xc = XHTMLCompiler::getInstance();
42 $php = XHTMLCompiler::getPHPWrapper();
44 // test file extension
45 $info = pathinfo($path);
46 if (
47 $info['extension'] !== $this->sourceExt &&
48 $info['extension'] !== $this->cacheExt
49 ) {
50 throw new XHTMLCompiler_Exception(403);
53 // test for directory's existence and resolve to real path
54 $dir = $info['dirname'];
55 $dir = $php->realpath($dir);
56 if ($dir === false) throw new XHTMLCompiler_Exception(403);
58 $allowed_dirs = $xc->getConf('allowed_dirs');
59 $ok = false;
61 foreach ($allowed_dirs as $allowed_dir => $recursive) {
62 $allowed_dir = $php->realpath($allowed_dir); // factor out!
63 if (!is_string($allowed_dir)) continue;
64 if ($dir === $allowed_dir) {
65 $ok = true;
66 break;
67 // slash is required to prevent $allowed_dir = 'subdir' from
68 // matching $dir = 'subdirectory', thanks Mordred!
69 } elseif (strpos($dir, $allowed_dir . '/') === 0 && $recursive) {
70 $ok = true;
71 break;
75 if (!$ok) throw new XHTMLCompiler_Exception(403);
77 // cannot use pathinfo, since PATHINFO_FILENAME is PHP 5.2.0
78 $this->pathStem = substr($path, 0, strrpos($path, '.'));
80 // setup the files
81 $this->source = new XHTMLCompiler_File($this->pathStem . '.' . $this->sourceExt);
82 $this->cache = new XHTMLCompiler_File($this->pathStem . '.' . $this->cacheExt);
83 $this->deps = new XHTMLCompiler_File($this->pathStem . '.' . $this->depsExt);
85 if (!$mute && !$this->source->exists()) {
86 // Apache may have redirected to an ErrorDocument which got directed
87 // via mod_rewrite to us, in that case, output the corresponding
88 // status code. Otherwise, we can give the regular 404.
89 $code = $php->getRedirectStatus();
90 if (!$code || $code == 200) $code = 404;
91 throw new XHTMLCompiler_Exception($code);
95 // Note: Do not use this functions internally inside the class
97 /** Returns path stem, full filename without file extension */
98 public function getPathStem() { return $this->pathStem; }
99 /** Returns relative path to cache */
100 public function getCachePath() { return $this->cache->getName(); }
101 /** Returns relative path to source */
102 public function getSourcePath() { return $this->source->getName(); }
105 * Returns a fully formed web path to the file
107 public function getWebPath() {
108 $xc = XHTMLCompiler::getInstance();
109 $domain = $xc->getConf('web_domain');
110 if (!$domain) {
111 throw new Exception('Configuration value web_domain must be set for command line');
113 return 'http://' . $domain .
114 $xc->getConf('web_path') . '/' . $this->cache->getName();
117 /** Returns contents of the cache/served file */
118 public function getCache() { return $this->cache->get(); }
119 /** Returns contents of the source file */
120 public function getSource() { return $this->source->get(); }
122 /** Reports whether or not cache file exists and is a file */
123 public function isCacheExistent() { return $this->cache->exists(); }
124 /** Reports whether or not source file exists and is a file */
125 public function isSourceExistent() { return $this->source->exists(); }
128 * Reports whether or not the cache is stale by comparing the file
129 * modification times between the source file and the cache file.
130 * @warning You must not call this function until you've also called
131 * isCacheExistent().
133 public function isCacheStale() {
134 if (!$this->cache->exists()) {
135 throw new Exception('Cannot check for stale cache when cache
136 does not exist, please call isCacheExistent and take
137 appropriate action with the result');
139 if ($this->source->getMTime() > $this->cache->getMTime()) return true;
140 // check dependencies
141 if (!$this->deps->exists()) return true; // we need a dependency file!
142 $deps = unserialize($this->deps->get());
143 foreach ($deps as $filename => $time) {
144 if ($time < filemtime($filename)) return true;
146 return false;
150 * Writes text to the cache file, overwriting any previous contents
151 * and creating the cache file if it doesn't exist.
152 * @param $contents String contents to write to cache
154 public function writeCache($contents) {$this->cache->write($contents);}
157 * Attempts to display contents from the cache, otherwise returns false
158 * @return True if successful, false if not.
159 * @todo Purge check needs to be factored into XHTMLCompiler
161 public function tryCache() {
162 if (
163 !isset($_GET['purge']) &&
164 $this->cache->exists() &&
165 !$this->isCacheStale()
167 // cached version is fresh, serve it. This shouldn't happen normally
168 set_response_code(200); // if we used ErrorDocument, override
169 readfile($this->getCachePath());
170 return true;
172 return false;
176 * Generates the final version of a page from the source file and writes
177 * it to the cache.
178 * @note This function needs to be extended greatly
179 * @return Generated contents from source
181 public function generate() {
182 $source = $this->source->get();
183 $xc = XHTMLCompiler::getInstance();
184 $filters = $xc->getFilterManager();
185 $contents = $filters->process($source, $this);
186 $deps = $filters->getDeps();
187 if (empty($contents)) return ''; // don't write, probably an error
188 $contents .= '<!-- generated by XHTML Compiler -->';
189 $this->cache->write($contents);
190 $this->cache->chmod(0664);
191 $this->deps->write(serialize($deps));
192 return $contents;
196 * Displays the page, either from cache or fresh regeneration.
198 public function display() {
199 if($this->tryCache()) return;
200 echo $this->generate();