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