Avoid .. and . on directory traversal.
[xhtml-compiler.git] / XHTMLCompiler / Directory.php
blobc4913a4df99cb13225b413e1a6cb33af643f174c
1 <?php
3 /**
4 * Represents a directory in the filesystem
5 */
6 class XHTMLCompiler_Directory
9 /** Location of directory this object represents, w/o trailing slash */
10 protected $name;
12 /** Non-blank location of directory, safe for directory handling functions */
13 protected $safeName;
15 /**
16 * Path to directory you wish to instantiate.
18 public function __construct($name) {
19 $xc = XHTMLCompiler::getInstance();
20 $php = XHTMLCompiler::getPHPWrapper();
22 $name = str_replace('\\', '/', $name); // normalize \ to /
23 $l = strlen($name) - 1; // index of last character
24 if ($l >= 0 && $name[$l] == '/') $name = substr($name, 0, $l); // truncate trailing slash
25 if ($name === '.') $name = ''; // we want the "unsafe" but pretty version
26 $safe_name = ($name === '') ? '.' : $name;
27 if (!$php->isDir($safe_name)) {
28 throw new Exception("Directory $safe_name does not exist");
30 $this->name = $name;
31 $this->safeName = $safe_name;
34 /**
35 * Returns name of directory without trailing slash
36 * @note This function works in all cases, but can be slightly
37 * redundant if the application is prepending it to
38 * a filename (compare index.html versus ./index.html)
40 public function getName() {return $this->safeName;}
42 /**
43 * Returns name of directory with trailing slash, prepared for
44 * a filename to be appended (s = slash, it is NOT safe)
45 * @note If the directory is the current working directory, a blank
46 * string is returned instead. This makes this method good
47 * for web-contexts, and reasonably well when appending it
48 * onto filenames, but will NOT work if you're actually trying
49 * to refer to a directory (see getNAme() for that)
51 public function getSName() {
52 return ($this->name === '') ? '' : $this->name . '/';
55 /**
56 * Recursively scans directory for files
57 * @return Tree of files in the directory, file => size
59 public function getTree() {
60 $dir = new RecursiveDirectoryIterator($this->safeName);
61 $tree = array();
62 $dirs = array(array($dir, &$tree));
64 for($i = 0; $i < count($dirs); ++$i) {
65 $d =& $dirs[$i][0]; // current directory iterator
66 $tier =& $dirs[$i][1]; // current directory tree to write to
67 for($d->rewind(); $d->valid(); $d->next()) {
68 if ($d->isDir()) {
69 if ($d->getFileName() == ".." || $d->getFileName() == ".") continue;
70 // initialize new directory tree
71 $tier[$d->getFilename()] = array();
72 // file away another directory to process
73 $dirs[] = array($d->getChildren(), &$tier[$d->getFilename()]);
74 } else {
75 // add the file to the directory tree
76 $tier[$d->getFilename()] = $d->getSize();
81 return $tree;
84 /**
85 * Scans directory recursively for files with a certain file extension
86 * @param $ext_match Extension with period to look for, cannot have
87 * more than one period within it
88 * @return List of matching files, with fully qualified relative
89 * paths (not tree format)
91 public function scanRecursively($ext_match) {
92 $tree = $this->getTree();
93 $dirs = array(array('', &$tree));
94 $ret = array();
95 for ($i = 0; $i < count($dirs); ++$i) {
96 $base = $dirs[$i][0]; // base directory prefix
97 $tier = $dirs[$i][1]; // tree node we're reading from
98 foreach ($tier as $name => $contents) {
99 if (is_array($contents)) {
100 // directory
101 $dirs[] = array($base . $name . '/', $contents);
102 } else {
103 // name
104 $ext = strrchr($name, '.');
105 if ($ext != $ext_match) continue;
106 $ret[] = $this->getSName() . $base . $name;
110 return $ret;
114 * Scans just the current directory for files matching extension
115 * @param $ext_match Extension with period to look for, cannot have
116 * more than one period within it
117 * @return List of matching files
119 public function scanFlat($ext_match) {
120 $files = scandir($this->safeName);
121 $ret = array();
122 foreach ($files as $name) {
123 if (empty($name) || $name[0] == '.') continue;
124 $ext = strrchr($name, '.');
125 if ($ext != $ext_match) continue;
126 $ret[] = $this->getSName() . $name;
128 return $ret;
132 * Scans directory for files with matching extension
133 * @param $ext_match File extension to match
134 * @param $recursive Whether or not to search recursively
136 public function scan($ext, $recursive) {
137 if ($recursive) return $this->scanRecursively($ext);
138 else return $this->scanFlat($ext);
141 public function getParent() {
142 $last_slash = strrpos($this->name, '/');
143 $parent = substr($this->name, 0, $last_slash);
144 return new XHTMLCompiler_Directory($parent);
147 public function getFile($filename) {
148 return new XHTMLCompiler_File($this->getSName() . $filename);
151 public function isAllowed() {
152 $xc = XHTMLCompiler::getInstance();
153 $php = XHTMLCompiler::getPHPWrapper();
154 $allowed_dirs = $xc->getConf("allowed_dirs");
155 $dir = realpath($this->safeName);
156 if ($dir[strlen($dir)-1] == '/') $dir = substr($dir, 0, -1);
157 $ok = false;
159 foreach ($allowed_dirs as $allowed_dir => $recursive) {
160 $allowed_dir = $php->realpath($allowed_dir); // factor out!
161 if (!is_string($allowed_dir)) continue;
162 if ($dir === $allowed_dir) {
163 $ok = true;
164 break;
165 // slash is required to prevent $allowed_dir = 'subdir' from
166 // matching $dir = 'subdirectory', thanks Mordred!
167 } elseif (strpos($dir, $allowed_dir . '/') === 0 && $recursive) {
168 $ok = true;
169 break;
172 return $ok;