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