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