1<?php 2 3/* 4 * This file is part of the symfony package. 5 * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11 12/** 13 * 14 * Allow to build rules to find files and directories. 15 * 16 * All rules may be invoked several times, except for ->in() method. 17 * Some rules are cumulative (->name() for example) whereas others are destructive 18 * (most recent value is used, ->maxdepth() method for example). 19 * 20 * All methods return the current sfFinder object to allow easy chaining: 21 * 22 * $files = sfFinder::type('file')->name('*.php')->in(.); 23 * 24 * Interface loosely based on perl File::Find::Rule module. 25 * 26 * @package symfony 27 * @subpackage util 28 * @author Fabien Potencier <fabien.potencier@symfony-project.com> 29 * @version SVN: $Id$ 30 */ 31class sfFinder 32{ 33 protected $type = 'file'; 34 protected $names = array(); 35 protected $prunes = array(); 36 protected $discards = array(); 37 protected $execs = array(); 38 protected $mindepth = 0; 39 protected $sizes = array(); 40 protected $maxdepth = 1000000; 41 protected $relative = false; 42 protected $follow_link = false; 43 protected $sort = false; 44 protected $ignore_version_control = true; 45 46 /** 47 * Sets maximum directory depth. 48 * 49 * Finder will descend at most $level levels of directories below the starting point. 50 * 51 * @param int $level 52 * @return sfFinder current sfFinder object 53 */ 54 public function maxdepth($level) 55 { 56 $this->maxdepth = $level; 57 58 return $this; 59 } 60 61 /** 62 * Sets minimum directory depth. 63 * 64 * Finder will start applying tests at level $level. 65 * 66 * @param int $level 67 * @return sfFinder current sfFinder object 68 */ 69 public function mindepth($level) 70 { 71 $this->mindepth = $level; 72 73 return $this; 74 } 75 76 public function get_type() 77 { 78 return $this->type; 79 } 80 81 /** 82 * Sets the type of elements to returns. 83 * 84 * @param string $name directory or file or any (for both file and directory) 85 * @return sfFinder new sfFinder object 86 */ 87 public static function type($name) 88 { 89 $finder = new self(); 90 return $finder->setType($name); 91 } 92 /** 93 * Sets the type of elements to returns. 94 * 95 * @param string $name directory or file or any (for both file and directory) 96 * @return sfFinder Current object 97 */ 98 public function setType($name) 99 { 100 $name = strtolower($name); 101 102 if (substr($name, 0, 3) === 'dir') 103 { 104 $this->type = 'directory'; 105 106 return $this; 107 } 108 if ($name === 'any') 109 { 110 $this->type = 'any'; 111 112 return $this; 113 } 114 115 $this->type = 'file'; 116 117 return $this; 118 } 119 120 /* 121 * glob, patterns (must be //) or strings 122 */ 123 protected function to_regex($str) 124 { 125 if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $str)) 126 { 127 return $str; 128 } 129 130 return sfGlobToRegex::glob_to_regex($str); 131 } 132 133 protected function args_to_array($arg_list, $not = false) 134 { 135 $list = array(); 136 $nbArgList = count($arg_list); 137 for ($i = 0; $i < $nbArgList; $i++) 138 { 139 if (is_array($arg_list[$i])) 140 { 141 foreach ($arg_list[$i] as $arg) 142 { 143 $list[] = array($not, $this->to_regex($arg)); 144 } 145 } 146 else 147 { 148 $list[] = array($not, $this->to_regex($arg_list[$i])); 149 } 150 } 151 152 return $list; 153 } 154 155 /** 156 * Adds rules that files must match. 157 * 158 * You can use patterns (delimited with / sign), globs or simple strings. 159 * 160 * $finder->name('*.php') 161 * $finder->name('/\.php$/') // same as above 162 * $finder->name('test.php') 163 * 164 * @param list a list of patterns, globs or strings 165 * @return sfFinder Current object 166 */ 167 public function name() 168 { 169 $args = func_get_args(); 170 $this->names = array_merge($this->names, $this->args_to_array($args)); 171 172 return $this; 173 } 174 175 /** 176 * Adds rules that files must not match. 177 * 178 * @see ->name() 179 * @param list a list of patterns, globs or strings 180 * @return sfFinder Current object 181 */ 182 public function not_name() 183 { 184 $args = func_get_args(); 185 $this->names = array_merge($this->names, $this->args_to_array($args, true)); 186 187 return $this; 188 } 189 190 /** 191 * Adds tests for file sizes. 192 * 193 * $finder->size('> 10K'); 194 * $finder->size('<= 1Ki'); 195 * $finder->size(4); 196 * 197 * @param list a list of comparison strings 198 * @return sfFinder Current object 199 */ 200 public function size() 201 { 202 $args = func_get_args(); 203 $numargs = count($args); 204 for ($i = 0; $i < $numargs; $i++) 205 { 206 $this->sizes[] = new sfNumberCompare($args[$i]); 207 } 208 209 return $this; 210 } 211 212 /** 213 * Traverses no further. 214 * 215 * @param list a list of patterns, globs to match 216 * @return sfFinder Current object 217 */ 218 public function prune() 219 { 220 $args = func_get_args(); 221 $this->prunes = array_merge($this->prunes, $this->args_to_array($args)); 222 223 return $this; 224 } 225 226 /** 227 * Discards elements that matches. 228 * 229 * @param list a list of patterns, globs to match 230 * @return sfFinder Current object 231 */ 232 public function discard() 233 { 234 $args = func_get_args(); 235 $this->discards = array_merge($this->discards, $this->args_to_array($args)); 236 237 return $this; 238 } 239 240 /** 241 * Ignores version control directories. 242 * 243 * Currently supports Subversion, CVS, DARCS, Gnu Arch, Monotone, Bazaar-NG, GIT, Mercurial 244 * 245 * @param bool $ignore falase when version control directories shall be included (default is true) 246 * 247 * @return sfFinder Current object 248 */ 249 public function ignore_version_control($ignore = true) 250 { 251 $this->ignore_version_control = $ignore; 252 253 return $this; 254 } 255 256 /** 257 * Returns files and directories ordered by name 258 * 259 * @return sfFinder Current object 260 */ 261 public function sort_by_name() 262 { 263 $this->sort = 'name'; 264 265 return $this; 266 } 267 268 /** 269 * Returns files and directories ordered by type (directories before files), then by name 270 * 271 * @return sfFinder Current object 272 */ 273 public function sort_by_type() 274 { 275 $this->sort = 'type'; 276 277 return $this; 278 } 279 280 /** 281 * Executes function or method for each element. 282 * 283 * Element match if functino or method returns true. 284 * 285 * $finder->exec('myfunction'); 286 * $finder->exec(array($object, 'mymethod')); 287 * 288 * @param mixed function or method to call 289 * @return sfFinder Current object 290 */ 291 public function exec() 292 { 293 $args = func_get_args(); 294 $numargs = count($args); 295 for ($i = 0; $i < $numargs; $i++) 296 { 297 if (is_array($args[$i]) && !method_exists($args[$i][0], $args[$i][1])) 298 { 299 throw new sfException(sprintf('method "%s" does not exist for object "%s".', $args[$i][1], $args[$i][0])); 300 } 301 if (!is_array($args[$i]) && !function_exists($args[$i])) 302 { 303 throw new sfException(sprintf('function "%s" does not exist.', $args[$i])); 304 } 305 306 $this->execs[] = $args[$i]; 307 } 308 309 return $this; 310 } 311 312 /** 313 * Returns relative paths for all files and directories. 314 * 315 * @return sfFinder Current object 316 */ 317 public function relative() 318 { 319 $this->relative = true; 320 321 return $this; 322 } 323 324 /** 325 * Symlink following. 326 * 327 * @return sfFinder Current object 328 */ 329 public function follow_link() 330 { 331 $this->follow_link = true; 332 333 return $this; 334 } 335 336 /** 337 * Searches files and directories which match defined rules. 338 * 339 * @return array list of files and directories 340 */ 341 public function in() 342 { 343 $files = array(); 344 $here_dir = getcwd(); 345 346 $finder = clone $this; 347 348 if ($this->ignore_version_control) 349 { 350 $ignores = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'); 351 352 $finder->discard($ignores)->prune($ignores); 353 } 354 355 // first argument is an array? 356 $numargs = func_num_args(); 357 $arg_list = func_get_args(); 358 if ($numargs === 1 && is_array($arg_list[0])) 359 { 360 $arg_list = $arg_list[0]; 361 $numargs = count($arg_list); 362 } 363 364 for ($i = 0; $i < $numargs; $i++) 365 { 366 $dir = realpath($arg_list[$i]); 367 368 if (!is_dir($dir)) 369 { 370 continue; 371 } 372 373 $dir = str_replace('\\', '/', $dir); 374 375 // absolute path? 376 if (!self::isPathAbsolute($dir)) 377 { 378 $dir = $here_dir.'/'.$dir; 379 } 380 381 $new_files = str_replace('\\', '/', $finder->search_in($dir)); 382 383 if ($this->relative) 384 { 385 $new_files = preg_replace('#^'.preg_quote(rtrim($dir, '/'), '#').'/#', '', $new_files); 386 } 387 388 $files = array_merge($files, $new_files); 389 } 390 391 if ($this->sort === 'name') 392 { 393 sort($files); 394 } 395 396 return array_unique($files); 397 } 398 399 protected function search_in($dir, $depth = 0) 400 { 401 if ($depth > $this->maxdepth) 402 { 403 return array(); 404 } 405 406 $dir = realpath($dir); 407 408 if ((!$this->follow_link) && is_link($dir)) 409 { 410 return array(); 411 } 412 413 $files = array(); 414 $temp_files = array(); 415 $temp_folders = array(); 416 if (is_dir($dir) && is_readable($dir)) 417 { 418 $current_dir = opendir($dir); 419 while (false !== $entryname = readdir($current_dir)) 420 { 421 if ($entryname == '.' || $entryname == '..') continue; 422 423 $current_entry = $dir.DIRECTORY_SEPARATOR.$entryname; 424 if ((!$this->follow_link) && is_link($current_entry)) 425 { 426 continue; 427 } 428 429 if (is_dir($current_entry)) 430 { 431 if ($this->sort === 'type') 432 { 433 $temp_folders[$entryname] = $current_entry; 434 } 435 else 436 { 437 if (($this->type === 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname)) 438 { 439 $files[] = $current_entry; 440 } 441 442 if (!$this->is_pruned($dir, $entryname)) 443 { 444 $files = array_merge($files, $this->search_in($current_entry, $depth + 1)); 445 } 446 } 447 } 448 else 449 { 450 if (($this->type !== 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->size_ok($dir, $entryname) && $this->exec_ok($dir, $entryname)) 451 { 452 if ($this->sort === 'type') 453 { 454 $temp_files[] = $current_entry; 455 } 456 else 457 { 458 $files[] = $current_entry; 459 } 460 } 461 } 462 } 463 464 if ($this->sort === 'type') 465 { 466 ksort($temp_folders); 467 foreach($temp_folders as $entryname => $current_entry) 468 { 469 if (($this->type === 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname)) 470 { 471 $files[] = $current_entry; 472 } 473 474 if (!$this->is_pruned($dir, $entryname)) 475 { 476 $files = array_merge($files, $this->search_in($current_entry, $depth + 1)); 477 } 478 } 479 480 sort($temp_files); 481 $files = array_merge($files, $temp_files); 482 } 483 484 closedir($current_dir); 485 } 486 487 return $files; 488 } 489 490 protected function match_names($dir, $entry) 491 { 492 if (!count($this->names)) return true; 493 494 // Flags indicating that there was attempts to match 495 // at least one "not_name" or "name" rule respectively 496 // to following variables: 497 $one_not_name_rule = false; 498 $one_name_rule = false; 499 500 foreach ($this->names as $args) 501 { 502 list($not, $regex) = $args; 503 $not ? $one_not_name_rule = true : $one_name_rule = true; 504 if (preg_match($regex, $entry)) 505 { 506 // We must match ONLY ONE "not_name" or "name" rule: 507 // if "not_name" rule matched then we return "false" 508 // if "name" rule matched then we return "true" 509 return $not ? false : true; 510 } 511 } 512 513 if ($one_not_name_rule && $one_name_rule) 514 { 515 return false; 516 } 517 else if ($one_not_name_rule) 518 { 519 return true; 520 } 521 else if ($one_name_rule) 522 { 523 return false; 524 } 525 return true; 526 } 527 528 protected function size_ok($dir, $entry) 529 { 530 if (0 === count($this->sizes)) return true; 531 532 if (!is_file($dir.DIRECTORY_SEPARATOR.$entry)) return true; 533 534 $filesize = filesize($dir.DIRECTORY_SEPARATOR.$entry); 535 foreach ($this->sizes as $number_compare) 536 { 537 if (!$number_compare->test($filesize)) return false; 538 } 539 540 return true; 541 } 542 543 protected function is_pruned($dir, $entry) 544 { 545 if (0 === count($this->prunes)) return false; 546 547 foreach ($this->prunes as $args) 548 { 549 $regex = $args[1]; 550 if (preg_match($regex, $entry)) return true; 551 } 552 553 return false; 554 } 555 556 protected function is_discarded($dir, $entry) 557 { 558 if (0 === count($this->discards)) return false; 559 560 foreach ($this->discards as $args) 561 { 562 $regex = $args[1]; 563 if (preg_match($regex, $entry)) return true; 564 } 565 566 return false; 567 } 568 569 protected function exec_ok($dir, $entry) 570 { 571 if (0 === count($this->execs)) return true; 572 573 foreach ($this->execs as $exec) 574 { 575 if (!call_user_func_array($exec, array($dir, $entry))) return false; 576 } 577 578 return true; 579 } 580 581 public static function isPathAbsolute($path) 582 { 583 if ($path[0] === '/' || $path[0] === '\\' || 584 (strlen($path) > 3 && ctype_alpha($path[0]) && 585 $path[1] === ':' && 586 ($path[2] === '\\' || $path[2] === '/') 587 ) 588 ) 589 { 590 return true; 591 } 592 593 return false; 594 } 595} 596 597/** 598 * Match globbing patterns against text. 599 * 600 * if match_glob("foo.*", "foo.bar") echo "matched\n"; 601 * 602 * // prints foo.bar and foo.baz 603 * $regex = glob_to_regex("foo.*"); 604 * for (array('foo.bar', 'foo.baz', 'foo', 'bar') as $t) 605 * { 606 * if (/$regex/) echo "matched: $car\n"; 607 * } 608 * 609 * sfGlobToRegex implements glob(3) style matching that can be used to match 610 * against text, rather than fetching names from a filesystem. 611 * 612 * based on perl Text::Glob module. 613 * 614 * @package symfony 615 * @subpackage util 616 * @author Fabien Potencier <fabien.potencier@gmail.com> php port 617 * @author Richard Clamp <richardc@unixbeard.net> perl version 618 * @copyright 2004-2005 Fabien Potencier <fabien.potencier@gmail.com> 619 * @copyright 2002 Richard Clamp <richardc@unixbeard.net> 620 * @version SVN: $Id$ 621 */ 622class sfGlobToRegex 623{ 624 protected static $strict_leading_dot = true; 625 protected static $strict_wildcard_slash = true; 626 627 public static function setStrictLeadingDot($boolean) 628 { 629 self::$strict_leading_dot = $boolean; 630 } 631 632 public static function setStrictWildcardSlash($boolean) 633 { 634 self::$strict_wildcard_slash = $boolean; 635 } 636 637 /** 638 * Returns a compiled regex which is the equiavlent of the globbing pattern. 639 * 640 * @param string $glob pattern 641 * @return string regex 642 */ 643 public static function glob_to_regex($glob) 644 { 645 $first_byte = true; 646 $escaping = false; 647 $in_curlies = 0; 648 $regex = ''; 649 $sizeGlob = strlen($glob); 650 for ($i = 0; $i < $sizeGlob; $i++) 651 { 652 $car = $glob[$i]; 653 if ($first_byte) 654 { 655 if (self::$strict_leading_dot && $car !== '.') 656 { 657 $regex .= '(?=[^\.])'; 658 } 659 660 $first_byte = false; 661 } 662 663 if ($car === '/') 664 { 665 $first_byte = true; 666 } 667 668 if ($car === '.' || $car === '(' || $car === ')' || $car === '|' || $car === '+' || $car === '^' || $car === '$') 669 { 670 $regex .= "\\$car"; 671 } 672 elseif ($car === '*') 673 { 674 $regex .= ($escaping ? '\\*' : (self::$strict_wildcard_slash ? '[^/]*' : '.*')); 675 } 676 elseif ($car === '?') 677 { 678 $regex .= ($escaping ? '\\?' : (self::$strict_wildcard_slash ? '[^/]' : '.')); 679 } 680 elseif ($car === '{') 681 { 682 $regex .= ($escaping ? '\\{' : '('); 683 if (!$escaping) ++$in_curlies; 684 } 685 elseif ($car === '}' && $in_curlies) 686 { 687 $regex .= ($escaping ? '}' : ')'); 688 if (!$escaping) --$in_curlies; 689 } 690 elseif ($car === ',' && $in_curlies) 691 { 692 $regex .= ($escaping ? ',' : '|'); 693 } 694 elseif ($car === '\\') 695 { 696 if ($escaping) 697 { 698 $regex .= '\\\\'; 699 $escaping = false; 700 } 701 else 702 { 703 $escaping = true; 704 } 705 706 continue; 707 } 708 else 709 { 710 $regex .= $car; 711 } 712 $escaping = false; 713 } 714 715 return '#^'.$regex.'$#'; 716 } 717} 718 719/** 720 * Numeric comparisons. 721 * 722 * sfNumberCompare compiles a simple comparison to an anonymous 723 * subroutine, which you can call with a value to be tested again. 724 725 * Now this would be very pointless, if sfNumberCompare didn't understand 726 * magnitudes. 727 728 * The target value may use magnitudes of kilobytes (k, ki), 729 * megabytes (m, mi), or gigabytes (g, gi). Those suffixed 730 * with an i use the appropriate 2**n version in accordance with the 731 * IEC standard: http://physics.nist.gov/cuu/Units/binary.html 732 * 733 * based on perl Number::Compare module. 734 * 735 * @package symfony 736 * @subpackage util 737 * @author Fabien Potencier <fabien.potencier@gmail.com> php port 738 * @author Richard Clamp <richardc@unixbeard.net> perl version 739 * @copyright 2004-2005 Fabien Potencier <fabien.potencier@gmail.com> 740 * @copyright 2002 Richard Clamp <richardc@unixbeard.net> 741 * @see http://physics.nist.gov/cuu/Units/binary.html 742 * @version SVN: $Id$ 743 */ 744class sfNumberCompare 745{ 746 protected $test = ''; 747 748 public function __construct($test) 749 { 750 $this->test = $test; 751 } 752 753 public function test($number) 754 { 755 if (!preg_match('{^([<>]=?)?(.*?)([kmg]i?)?$}i', $this->test, $matches)) 756 { 757 throw new sfException(sprintf('don\'t understand "%s" as a test.', $this->test)); 758 } 759 760 $target = array_key_exists(2, $matches) ? $matches[2] : ''; 761 $magnitude = array_key_exists(3, $matches) ? $matches[3] : ''; 762 if (strtolower($magnitude) === 'k') $target *= 1000; 763 if (strtolower($magnitude) === 'ki') $target *= 1024; 764 if (strtolower($magnitude) === 'm') $target *= 1000000; 765 if (strtolower($magnitude) === 'mi') $target *= 1024*1024; 766 if (strtolower($magnitude) === 'g') $target *= 1000000000; 767 if (strtolower($magnitude) === 'gi') $target *= 1024*1024*1024; 768 769 $comparison = array_key_exists(1, $matches) ? $matches[1] : '=='; 770 if ($comparison === '==' || $comparison == '') 771 { 772 return ($number == $target); 773 } 774 if ($comparison === '>') 775 { 776 return ($number > $target); 777 } 778 if ($comparison === '>=') 779 { 780 return ($number >= $target); 781 } 782 if ($comparison === '<') 783 { 784 return ($number < $target); 785 } 786 if ($comparison === '<=') 787 { 788 return ($number <= $target); 789 } 790 791 return false; 792 } 793} 794