1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Loaders;
13:
14: use Nette,
15: Nette\Utils\Strings,
16: Nette\Caching\Cache;
17:
18:
19:
20: 21: 22: 23: 24:
25: class RobotLoader extends AutoLoader
26: {
27:
28: public $scanDirs;
29:
30:
31: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
32:
33:
34: public $acceptFiles = '*.php, *.php5';
35:
36:
37: public $autoRebuild = TRUE;
38:
39:
40: private $list = array();
41:
42:
43: private $files;
44:
45:
46: private $rebuilt = FALSE;
47:
48:
49: private $cacheStorage;
50:
51:
52:
53: 54:
55: public function __construct()
56: {
57: if (!extension_loaded('tokenizer')) {
58: throw new Nette\NotSupportedException("PHP extension Tokenizer is not loaded.");
59: }
60: }
61:
62:
63:
64: 65: 66: 67:
68: public function register()
69: {
70: $cache = $this->getCache();
71: $key = $this->getKey();
72: if (isset($cache[$key])) {
73: $this->list = $cache[$key];
74: } else {
75: $this->rebuild();
76: }
77:
78: if (isset($this->list[strtolower(__CLASS__)]) && class_exists('Nette\Loaders\NetteLoader', FALSE)) {
79: NetteLoader::getInstance()->unregister();
80: }
81:
82: parent::register();
83: }
84:
85:
86:
87: 88: 89: 90: 91:
92: public function tryLoad($type)
93: {
94: $type = ltrim(strtolower($type), '\\');
95:
96: if (isset($this->list[$type][0]) && !is_file($this->list[$type][0])) {
97: unset($this->list[$type]);
98: }
99:
100: if (!isset($this->list[$type])) {
101: $trace = debug_backtrace();
102: $initiator = & $trace[2]['function'];
103: if ($initiator === 'class_exists' || $initiator === 'interface_exists') {
104: $this->list[$type] = FALSE;
105: if ($this->autoRebuild && $this->rebuilt) {
106: $this->getCache()->save($this->getKey(), $this->list, array(
107: Cache::CONSTS => 'Nette\Framework::REVISION',
108: ));
109: }
110: }
111:
112: if ($this->autoRebuild && !$this->rebuilt) {
113: $this->rebuild();
114: }
115: }
116:
117: if (isset($this->list[$type][0])) {
118: Nette\Utils\LimitedScope::load($this->list[$type][0]);
119: self::$count++;
120: }
121: }
122:
123:
124:
125: 126: 127: 128:
129: public function rebuild()
130: {
131: $this->getCache()->save($this->getKey(), callback($this, '_rebuildCallback'), array(
132: Cache::CONSTS => 'Nette\Framework::REVISION',
133: ));
134: $this->rebuilt = TRUE;
135: }
136:
137:
138:
139: 140: 141:
142: public function _rebuildCallback()
143: {
144: foreach ($this->list as $pair) {
145: if ($pair) {
146: $this->files[$pair[0]] = $pair[1];
147: }
148: }
149: foreach (array_unique($this->scanDirs) as $dir) {
150: $this->scanDirectory($dir);
151: }
152: $this->files = NULL;
153: return $this->list;
154: }
155:
156:
157:
158: 159: 160:
161: public function getIndexedClasses()
162: {
163: $res = array();
164: foreach ($this->list as $class => $pair) {
165: if ($pair) {
166: $res[$pair[2]] = $pair[0];
167: }
168: }
169: return $res;
170: }
171:
172:
173:
174: 175: 176: 177: 178: 179:
180: public function addDirectory($path)
181: {
182: foreach ((array) $path as $val) {
183: $real = realpath($val);
184: if ($real === FALSE) {
185: throw new Nette\DirectoryNotFoundException("Directory '$val' not found.");
186: }
187: $this->scanDirs[] = $real;
188: }
189: return $this;
190: }
191:
192:
193:
194: 195: 196: 197: 198: 199: 200:
201: private function addClass($class, $file, $time)
202: {
203: $lClass = strtolower($class);
204: if (isset($this->list[$lClass][0]) && ($file2 = $this->list[$lClass][0]) !== $file && is_file($file2)) {
205: if ($this->files[$file2] !== filemtime($file2)) {
206: $this->scanScript($file2);
207: return $this->addClass($class, $file, $time);
208: }
209: $e = new Nette\InvalidStateException("Ambiguous class '$class' resolution; defined in $file and in " . $this->list[$lClass][0] . ".");
210: {
211: throw $e;
212: }
213: }
214: $this->list[$lClass] = array($file, $time, $class);
215: $this->files[$file] = $time;
216: }
217:
218:
219:
220: 221: 222: 223: 224:
225: private function scanDirectory($dir)
226: {
227: if (is_dir($dir)) {
228: $ignoreDirs = is_array($this->ignoreDirs) ? $this->ignoreDirs : Strings::split($this->ignoreDirs, '#[,\s]+#');
229: $disallow = array();
230: foreach ($ignoreDirs as $item) {
231: if ($item = realpath($item)) {
232: $disallow[$item] = TRUE;
233: }
234: }
235: $iterator = Nette\Utils\Finder::findFiles(is_array($this->acceptFiles) ? $this->acceptFiles : Strings::split($this->acceptFiles, '#[,\s]+#'))
236: ->filter(function($file) use (&$disallow){
237: return !isset($disallow[$file->getPathname()]);
238: })
239: ->from($dir)
240: ->exclude($ignoreDirs)
241: ->filter($filter = function($dir) use (&$disallow){
242: $path = $dir->getPathname();
243: if (is_file("$path/netterobots.txt")) {
244: foreach (file("$path/netterobots.txt") as $s) {
245: if ($matches = Strings::match($s, '#^disallow\\s*:\\s*(\\S+)#i')) {
246: $disallow[$path . str_replace('/', DIRECTORY_SEPARATOR, rtrim('/' . ltrim($matches[1], '/'), '/'))] = TRUE;
247: }
248: }
249: }
250: return !isset($disallow[$path]);
251: });
252: $filter(new \SplFileInfo($dir));
253: } else {
254: $iterator = new \ArrayIterator(array(new \SplFileInfo($dir)));
255: }
256:
257: foreach ($iterator as $entry) {
258: $path = $entry->getPathname();
259: if (!isset($this->files[$path]) || $this->files[$path] !== $entry->getMTime()) {
260: $this->scanScript($path);
261: }
262: }
263: }
264:
265:
266:
267: 268: 269: 270: 271:
272: private function scanScript($file)
273: {
274: $T_NAMESPACE = PHP_VERSION_ID < 50300 ? -1 : T_NAMESPACE;
275: $T_NS_SEPARATOR = PHP_VERSION_ID < 50300 ? -1 : T_NS_SEPARATOR;
276:
277: $expected = FALSE;
278: $namespace = '';
279: $level = $minLevel = 0;
280: $time = filemtime($file);
281: $s = file_get_contents($file);
282:
283: foreach ($this->list as $class => $pair) {
284: if ($pair && $pair[0] === $file) {
285: unset($this->list[$class]);
286: }
287: }
288:
289: if ($matches = Strings::match($s, '#//nette'.'loader=(\S*)#')) {
290: foreach (explode(',', $matches[1]) as $name) {
291: $this->addClass($name, $file, $time);
292: }
293: return;
294: }
295:
296: foreach (token_get_all($s) as $token) {
297: if (is_array($token)) {
298: switch ($token[0]) {
299: case T_COMMENT:
300: case T_DOC_COMMENT:
301: case T_WHITESPACE:
302: continue 2;
303:
304: case $T_NS_SEPARATOR:
305: case T_STRING:
306: if ($expected) {
307: $name .= $token[1];
308: }
309: continue 2;
310:
311: case $T_NAMESPACE:
312: case T_CLASS:
313: case T_INTERFACE:
314: $expected = $token[0];
315: $name = '';
316: continue 2;
317: case T_CURLY_OPEN:
318: case T_DOLLAR_OPEN_CURLY_BRACES:
319: $level++;
320: }
321: }
322:
323: if ($expected) {
324: switch ($expected) {
325: case T_CLASS:
326: case T_INTERFACE:
327: if ($level === $minLevel) {
328: $this->addClass($namespace . $name, $file, $time);
329: }
330: break;
331:
332: case $T_NAMESPACE:
333: $namespace = $name ? $name . '\\' : '';
334: $minLevel = $token === '{' ? 1 : 0;
335: }
336:
337: $expected = NULL;
338: }
339:
340: if ($token === '{') {
341: $level++;
342: } elseif ($token === '}') {
343: $level--;
344: }
345: }
346: }
347:
348:
349:
350:
351:
352:
353:
354: 355: 356: 357:
358: public function setCacheStorage(Nette\Caching\IStorage $storage)
359: {
360: $this->cacheStorage = $storage;
361: return $this;
362: }
363:
364:
365:
366: 367: 368:
369: public function getCacheStorage()
370: {
371: return $this->cacheStorage;
372: }
373:
374:
375:
376: 377: 378:
379: protected function getCache()
380: {
381: if (!$this->cacheStorage) {
382: trigger_error('Missing cache storage.', E_USER_WARNING);
383: $this->cacheStorage = new Nette\Caching\Storages\DevNullStorage;
384: }
385: return new Cache($this->cacheStorage, 'Nette.RobotLoader');
386: }
387:
388:
389:
390: 391: 392:
393: protected function getKey()
394: {
395: return array($this->ignoreDirs, $this->acceptFiles, $this->scanDirs);
396: }
397:
398: }
399: