Implement standards compliant error messages using XML+XSLT.
[xhtml-compiler.git] / XHTMLCompiler / Page.php
blob52b8783cad83434d59a5365281182d94c373bcf6
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 /** Instance of XHTMLCompiler_Directory for all of the above files*/
31 protected $dir;
33 /**
34 * Constructs a page object, validates filename for correctness
35 * @param $path String path filename, can be from untrusted source
36 * @param $mute Whether or not to stop the class from complaining when
37 * the source file doesn't exist. This is a stopgap measure,
38 * please replace with better exception handling.
39 * @todo Cleanup into subroutines
40 * @todo Factor out allowed_directories realpath'ing to config class
42 public function __construct($path, $mute = false) {
44 $xc = XHTMLCompiler::getInstance();
45 $php = XHTMLCompiler::getPHPWrapper();
47 // test file extension
48 $info = pathinfo($path);
49 if (
50 empty($info['extension']) || (
51 $info['extension'] !== $this->sourceExt &&
52 $info['extension'] !== $this->cacheExt
54 ) {
55 throw new XHTMLCompiler_Exception(403, 'Forbidden extension',
56 'File extension cannot be processed by XHTML Compiler, check
57 for faulty <code>.htaccess</code> rules.');
60 // test for directory's existence and resolve to real path
61 $dir = $info['dirname'];
62 if ($dir == '.') $dir .= '/';
63 $dir = $php->realpath($dir);
64 if ($dir === false) {
65 throw new XHTMLCompiler_Exception(404, 'Missing directory',
66 'Requested directory cannot be found; check your file
67 path and try again.' );
70 $allowed_dirs = $xc->getConf('allowed_dirs');
71 $ok = false;
73 foreach ($allowed_dirs as $allowed_dir => $recursive) {
74 $allowed_dir = $php->realpath($allowed_dir); // factor out!
75 if (!is_string($allowed_dir)) continue;
76 if ($dir === $allowed_dir) {
77 $ok = true;
78 break;
79 // slash is required to prevent $allowed_dir = 'subdir' from
80 // matching $dir = 'subdirectory', thanks Mordred!
81 } elseif (strpos($dir, $allowed_dir . '/') === 0 && $recursive) {
82 $ok = true;
83 break;
87 if (!$ok) throw new XHTMLCompiler_Exception(403, 'Forbidden directory',
88 'Requested directory is forbidden to XHTML Compiler; try
89 accessing it directly or check for faulty <code>.htaccess</code> rules.');
91 // cannot use pathinfo, since PATHINFO_FILENAME is PHP 5.2.0
92 $this->pathStem = substr($path, 0, strrpos($path, '.'));
94 // setup the files
95 $this->source = new XHTMLCompiler_File($this->pathStem . '.' . $this->sourceExt);
96 $this->cache = new XHTMLCompiler_File($this->pathStem . '.' . $this->cacheExt);
97 $this->deps = new XHTMLCompiler_File($this->pathStem . '.' . $this->depsExt);
99 $this->dir = new XHTMLCompiler_Directory(dirname($this->pathStem));
101 if (!$mute && !$this->source->exists()) {
102 // Apache may have redirected to an ErrorDocument which got directed
103 // via mod_rewrite to us, in that case, output the corresponding
104 // status code. Otherwise, we can give the regular 404.
105 $code = $php->getRedirectStatus();
106 if (!$code || $code == 200) $code = 404;
107 throw new XHTMLCompiler_Exception($code, 'Page not found', 'Requested page not found; check the URL in your address bar.');
111 // Note: Do not use this functions internally inside the class
113 /** Returns path stem, full filename without file extension */
114 public function getPathStem() { return $this->pathStem; }
115 /** Returns relative path to cache */
116 public function getCachePath() { return $this->cache->getName(); }
117 /** Returns relative path to source */
118 public function getSourcePath() { return $this->source->getName(); }
119 /** Returns XHTMLCompiler_Directory representation of directory */
120 public function getDir() { return $this->dir; }
121 /** Returns directory of the files without trailing slash */
122 public function getDirName() { return $this->dir->getName(); }
123 /** Returns directory of the files with trailing slash (unless there is none) */
124 public function getDirSName() { return $this->dir->getSName(); }
125 /** Returns how deep from the root the file is */
126 public function getDepth() { return substr_count($this->getSourcePath(), '/'); }
128 /** Normalizes a relative path as if it were from this page's directory */
129 public function normalizePath($path) {
130 return $this->getDirName() . '/' . $path;
134 * Returns a fully formed web path to the file
136 public function getWebPath() {
137 $xc = XHTMLCompiler::getInstance();
138 $domain = $xc->getConf('web_domain');
139 if (!$domain) {
140 throw new Exception('Configuration value web_domain must be set for command line');
142 return 'http://' . $domain .
143 $xc->getConf('web_path') . '/' . $this->cache->getName();
146 /** Returns contents of the cache/served file */
147 public function getCache() { return $this->cache->get(); }
148 /** Returns contents of the source file */
149 public function getSource() { return $this->source->get(); }
151 /** Reports whether or not cache file exists and is a file */
152 public function isCacheExistent() { return $this->cache->exists(); }
153 /** Reports whether or not source file exists and is a file */
154 public function isSourceExistent() { return $this->source->exists(); }
157 * Reports whether or not the cache is stale by comparing the file
158 * modification times between the source file and the cache file.
159 * @warning You must not call this function until you've also called
160 * isCacheExistent().
162 public function isCacheStale() {
163 if (!$this->cache->exists()) {
164 throw new Exception('Cannot check for stale cache when cache
165 does not exist, please call isCacheExistent and take
166 appropriate action with the result');
168 if ($this->source->getMTime() > $this->cache->getMTime()) return true;
169 // check dependencies
170 if (!$this->deps->exists()) return true; // we need a dependency file!
171 $deps = unserialize($this->deps->get());
172 foreach ($deps as $filename => $time) {
173 if ($time < filemtime($filename)) return true;
175 return false;
179 * Writes text to the cache file, overwriting any previous contents
180 * and creating the cache file if it doesn't exist.
181 * @param $contents String contents to write to cache
183 public function writeCache($contents) {$this->cache->write($contents);}
186 * Attempts to display contents from the cache, otherwise returns false
187 * @return True if successful, false if not.
188 * @todo Purge check needs to be factored into XHTMLCompiler
190 public function tryCache() {
191 if (
192 !isset($_GET['purge']) &&
193 $this->cache->exists() &&
194 !$this->isCacheStale()
196 // cached version is fresh, serve it. This shouldn't happen normally
197 set_response_code(200); // if we used ErrorDocument, override
198 readfile($this->getCachePath());
199 return true;
201 return false;
205 * Generates the final version of a page from the source file and writes
206 * it to the cache.
207 * @note This function needs to be extended greatly
208 * @return Generated contents from source
210 public function generate() {
211 $source = $this->source->get();
212 $xc = XHTMLCompiler::getInstance();
213 $filters = $xc->getFilterManager();
214 $contents = $filters->process($source, $this);
215 $deps = $filters->getDeps();
216 if (empty($contents)) return ''; // don't write, probably an error
217 $contents .= '<!-- generated by XHTML Compiler -->';
218 $this->cache->write($contents);
219 $this->cache->chmod(0664);
220 $this->deps->write(serialize($deps));
221 return $contents;
225 * Displays the page, either from cache or fresh regeneration.
227 public function display() {
228 if($this->tryCache()) return;
229 $ret = $this->generate();
230 if ($ret) {
231 if (stripos($_SERVER["HTTP_ACCEPT"], 'application/xhtml+xml') !== false) {
232 header("Content-type: application/xhtml+xml");
233 } else {
234 header("Content-type: text/html");
237 echo $ret;
240 // Subversion related functions
242 protected $svnDate, $svnRevision, $svnAuthor, $svnHeadURL, $svnHeadURLMunged;
244 public function registerSVNKeywords(
245 $date, $revision, $author, $head_url
247 $this->svnDate = $date;
248 $this->svnRevision = (int) $revision;
249 $this->svnAuthor = $author;
250 $this->svnHeadURL = $head_url;
252 protected function loadSVNKeywords() {
253 // this is an expensive function
254 // we should log calls to it
255 $raw_status = shell_exec('svn info "'.$this->getSourcePath().'"');
256 if (!$raw_status) {
257 throw new Exception('Attempt to grab SVN info for non-versioned file ' . $this->getCachePath());
259 $raw_status = str_replace("\r", '', $raw_status);
260 $raw_status = explode("\n", $raw_status);
261 $status = array();
262 foreach ($raw_status as $i => $keyval) {
263 if (empty($keyval)) continue;
264 if (!strpos($keyval, ':')) continue;
265 list($key, $value) = explode(': ', $keyval, 2);
266 $status[$key] = $value;
268 $this->svnDate = $status['Last Changed Date'];
269 $this->svnRevision = $status['Last Changed Rev'];
270 $this->svnAuthor = $status['Last Changed Author'];
271 $this->svnHeadURL = $status['URL'];
273 public function getSVNDate() {
274 if (empty($this->svnDate)) $this->loadSVNKeywords();
275 return $this->svnDate;
277 public function getSVNRevision() {
278 if (empty($this->svnRevision)) $this->loadSVNKeywords();
279 return $this->svnRevision;
281 public function getSVNAuthor() {
282 if (empty($this->svnAuthor)) $this->loadSVNKeywords();
283 return $this->svnAuthor;
286 * @warning The Head URL may not be publically accessible if
287 * svn+ssh:// or file:// protocols were used in the
288 * working copy.
290 public function getSVNHeadURL() {
291 if (empty($this->svnHeadURL)) $this->loadSVNKeywords();
292 return $this->svnHeadURL;
296 * Returns the Head URL, but munged with svn_headurl_replace to
297 * an accessible representation (see config.default.php for details)
299 public function getSVNHeadURLMunged() {
300 if (!empty($this->svnHeadURLMunged)) return $this->svnHeadURLMunged;
301 $head_url = $this->getSVNHeadURL();
302 $xc = XHTMLCompiler::getInstance();
303 $pairs = $xc->getConf('svn_headurl_munge');
304 foreach ($pairs as $pair) {
305 if (!(strpos($head_url, $pair[0]) === 0)) continue;
306 $head_url = substr_replace($head_url, $pair[1], 0, strlen($pair[0]));
307 break;
309 return $this->svnHeadURLMunged = $head_url;