1<?php
2/**
3 * PEAR_Validate
4 *
5 * PHP versions 4 and 5
6 *
7 * @category   pear
8 * @package    PEAR
9 * @author     Greg Beaver <cellog@php.net>
10 * @copyright  1997-2009 The Authors
11 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
12 * @link       http://pear.php.net/package/PEAR
13 * @since      File available since Release 1.4.0a1
14 */
15/**#@+
16 * Constants for install stage
17 */
18define('PEAR_VALIDATE_INSTALLING', 1);
19define('PEAR_VALIDATE_UNINSTALLING', 2); // this is not bit-mapped like the others
20define('PEAR_VALIDATE_NORMAL', 3);
21define('PEAR_VALIDATE_DOWNLOADING', 4); // this is not bit-mapped like the others
22define('PEAR_VALIDATE_PACKAGING', 7);
23/**#@-*/
24require_once 'PEAR/Common.php';
25require_once 'PEAR/Validator/PECL.php';
26
27/**
28 * Validation class for package.xml - channel-level advanced validation
29 * @category   pear
30 * @package    PEAR
31 * @author     Greg Beaver <cellog@php.net>
32 * @copyright  1997-2009 The Authors
33 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
34 * @version    Release: @package_version@
35 * @link       http://pear.php.net/package/PEAR
36 * @since      Class available since Release 1.4.0a1
37 */
38class PEAR_Validate
39{
40    var $packageregex = _PEAR_COMMON_PACKAGE_NAME_PREG;
41    /**
42     * @var PEAR_PackageFile_v1|PEAR_PackageFile_v2
43     */
44    var $_packagexml;
45    /**
46     * @var int one of the PEAR_VALIDATE_* constants
47     */
48    var $_state = PEAR_VALIDATE_NORMAL;
49    /**
50     * Format: ('error' => array('field' => name, 'reason' => reason), 'warning' => same)
51     * @var array
52     * @access private
53     */
54    var $_failures = array('error' => array(), 'warning' => array());
55
56    /**
57     * Override this method to handle validation of normal package names
58     * @param string
59     * @return bool
60     * @access protected
61     */
62    function _validPackageName($name)
63    {
64        return (bool) preg_match('/^' . $this->packageregex . '\\z/', $name);
65    }
66
67    /**
68     * @param string package name to validate
69     * @param string name of channel-specific validation package
70     * @final
71     */
72    function validPackageName($name, $validatepackagename = false)
73    {
74        if ($validatepackagename) {
75            if (strtolower($name) == strtolower($validatepackagename)) {
76                return (bool) preg_match('/^[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*\\z/', $name);
77            }
78        }
79        return $this->_validPackageName($name);
80    }
81
82    /**
83     * This validates a bundle name, and bundle names must conform
84     * to the PEAR naming convention, so the method is final and static.
85     * @param string
86     * @final
87     */
88    public static function validGroupName($name)
89    {
90        return (bool) preg_match('/^' . _PEAR_COMMON_PACKAGE_NAME_PREG . '\\z/', $name);
91    }
92
93    /**
94     * Determine whether $state represents a valid stability level
95     * @param string
96     * @return bool
97     * @final
98     */
99    public static function validState($state)
100    {
101        return in_array($state, array('snapshot', 'devel', 'alpha', 'beta', 'stable'));
102    }
103
104    /**
105     * Get a list of valid stability levels
106     * @return array
107     * @final
108     */
109    public static function getValidStates()
110    {
111        return array('snapshot', 'devel', 'alpha', 'beta', 'stable');
112    }
113
114    /**
115     * Determine whether a version is a properly formatted version number that can be used
116     * by version_compare
117     * @param string
118     * @return bool
119     * @final
120     */
121    public static function validVersion($ver)
122    {
123        return (bool) preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $ver);
124    }
125
126    /**
127     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
128     */
129    function setPackageFile(&$pf)
130    {
131        $this->_packagexml = &$pf;
132    }
133
134    /**
135     * @access private
136     */
137    function _addFailure($field, $reason)
138    {
139        $this->_failures['errors'][] = array('field' => $field, 'reason' => $reason);
140    }
141
142    /**
143     * @access private
144     */
145    function _addWarning($field, $reason)
146    {
147        $this->_failures['warnings'][] = array('field' => $field, 'reason' => $reason);
148    }
149
150    function getFailures()
151    {
152        $failures = $this->_failures;
153        $this->_failures = array('warnings' => array(), 'errors' => array());
154        return $failures;
155    }
156
157    /**
158     * @param int one of the PEAR_VALIDATE_* constants
159     */
160    function validate($state = null)
161    {
162        if (!isset($this->_packagexml)) {
163            return false;
164        }
165        if ($state !== null) {
166            $this->_state = $state;
167        }
168        $this->_failures = array('warnings' => array(), 'errors' => array());
169        $this->validatePackageName();
170        $this->validateVersion();
171        $this->validateMaintainers();
172        $this->validateDate();
173        $this->validateSummary();
174        $this->validateDescription();
175        $this->validateLicense();
176        $this->validateNotes();
177        if ($this->_packagexml->getPackagexmlVersion() == '1.0') {
178            $this->validateState();
179            $this->validateFilelist();
180        } elseif ($this->_packagexml->getPackagexmlVersion() == '2.0' ||
181                  $this->_packagexml->getPackagexmlVersion() == '2.1') {
182            $this->validateTime();
183            $this->validateStability();
184            $this->validateDeps();
185            $this->validateMainFilelist();
186            $this->validateReleaseFilelist();
187            //$this->validateGlobalTasks();
188            $this->validateChangelog();
189        }
190        return !((bool) count($this->_failures['errors']));
191    }
192
193    /**
194     * @access protected
195     */
196    function validatePackageName()
197    {
198        if ($this->_state == PEAR_VALIDATE_PACKAGING ||
199              $this->_state == PEAR_VALIDATE_NORMAL) {
200            if (($this->_packagexml->getPackagexmlVersion() == '2.0' ||
201                 $this->_packagexml->getPackagexmlVersion() == '2.1') &&
202                  $this->_packagexml->getExtends()) {
203                $version = $this->_packagexml->getVersion() . '';
204                $name = $this->_packagexml->getPackage();
205                $a = explode('.', $version);
206                $test = array_shift($a);
207                if ($test == '0') {
208                    return true;
209                }
210                $vlen = strlen($test);
211                $majver = substr($name, strlen($name) - $vlen);
212                while ($majver && !is_numeric($majver{0})) {
213                    $majver = substr($majver, 1);
214                }
215                if ($majver != $test) {
216                    $this->_addWarning('package', "package $name extends package " .
217                        $this->_packagexml->getExtends() . ' and so the name should ' .
218                        'have a postfix equal to the major version like "' .
219                        $this->_packagexml->getExtends() . $test . '"');
220                    return true;
221                } elseif (substr($name, 0, strlen($name) - $vlen) !=
222                            $this->_packagexml->getExtends()) {
223                    $this->_addWarning('package', "package $name extends package " .
224                        $this->_packagexml->getExtends() . ' and so the name must ' .
225                        'be an extension like "' . $this->_packagexml->getExtends() .
226                        $test . '"');
227                    return true;
228                }
229            }
230        }
231        if (!$this->validPackageName($this->_packagexml->getPackage())) {
232            $this->_addFailure('name', 'package name "' .
233                $this->_packagexml->getPackage() . '" is invalid');
234            return false;
235        }
236    }
237
238    /**
239     * @access protected
240     */
241    function validateVersion()
242    {
243        if ($this->_state != PEAR_VALIDATE_PACKAGING) {
244            if (!$this->validVersion($this->_packagexml->getVersion())) {
245                $this->_addFailure('version',
246                    'Invalid version number "' . $this->_packagexml->getVersion() . '"');
247            }
248            return false;
249        }
250        $version = $this->_packagexml->getVersion();
251        $versioncomponents = explode('.', $version);
252        if (count($versioncomponents) != 3) {
253            $this->_addWarning('version',
254                'A version number should have 3 decimals (x.y.z)');
255            return true;
256        }
257        $name = $this->_packagexml->getPackage();
258        // version must be based upon state
259        switch ($this->_packagexml->getState()) {
260            case 'snapshot' :
261                return true;
262            case 'devel' :
263                if ($versioncomponents[0] . 'a' == '0a') {
264                    return true;
265                }
266                if ($versioncomponents[0] == 0) {
267                    $versioncomponents[0] = '0';
268                    $this->_addWarning('version',
269                        'version "' . $version . '" should be "' .
270                        implode('.' ,$versioncomponents) . '"');
271                } else {
272                    $this->_addWarning('version',
273                        'packages with devel stability must be < version 1.0.0');
274                }
275                return true;
276            break;
277            case 'alpha' :
278            case 'beta' :
279                // check for a package that extends a package,
280                // like Foo and Foo2
281                if ($this->_state == PEAR_VALIDATE_PACKAGING) {
282                    if (substr($versioncomponents[2], 1, 2) == 'rc') {
283                        $this->_addFailure('version', 'Release Candidate versions ' .
284                            'must have capital RC, not lower-case rc');
285                        return false;
286                    }
287                }
288                if (!$this->_packagexml->getExtends()) {
289                    if ($versioncomponents[0] == '1') {
290                        if ($versioncomponents[2]{0} == '0') {
291                            if ($versioncomponents[2] == '0') {
292                                // version 1.*.0000
293                                $this->_addWarning('version',
294                                    'version 1.' . $versioncomponents[1] .
295                                        '.0 probably should not be alpha or beta');
296                                return true;
297                            } elseif (strlen($versioncomponents[2]) > 1) {
298                                // version 1.*.0RC1 or 1.*.0beta24 etc.
299                                return true;
300                            } else {
301                                // version 1.*.0
302                                $this->_addWarning('version',
303                                    'version 1.' . $versioncomponents[1] .
304                                        '.0 probably should not be alpha or beta');
305                                return true;
306                            }
307                        } else {
308                            $this->_addWarning('version',
309                                'bugfix versions (1.3.x where x > 0) probably should ' .
310                                'not be alpha or beta');
311                            return true;
312                        }
313                    } elseif ($versioncomponents[0] != '0') {
314                        $this->_addWarning('version',
315                            'major versions greater than 1 are not allowed for packages ' .
316                            'without an <extends> tag or an identical postfix (foo2 v2.0.0)');
317                        return true;
318                    }
319                    if ($versioncomponents[0] . 'a' == '0a') {
320                        return true;
321                    }
322                    if ($versioncomponents[0] == 0) {
323                        $versioncomponents[0] = '0';
324                        $this->_addWarning('version',
325                            'version "' . $version . '" should be "' .
326                            implode('.' ,$versioncomponents) . '"');
327                    }
328                } else {
329                    $vlen = strlen($versioncomponents[0] . '');
330                    $majver = substr($name, strlen($name) - $vlen);
331                    while ($majver && !is_numeric($majver{0})) {
332                        $majver = substr($majver, 1);
333                    }
334                    if (($versioncomponents[0] != 0) && $majver != $versioncomponents[0]) {
335                        $this->_addWarning('version', 'first version number "' .
336                            $versioncomponents[0] . '" must match the postfix of ' .
337                            'package name "' . $name . '" (' .
338                            $majver . ')');
339                        return true;
340                    }
341                    if ($versioncomponents[0] == $majver) {
342                        if ($versioncomponents[2]{0} == '0') {
343                            if ($versioncomponents[2] == '0') {
344                                // version 2.*.0000
345                                $this->_addWarning('version',
346                                    "version $majver." . $versioncomponents[1] .
347                                        '.0 probably should not be alpha or beta');
348                                return false;
349                            } elseif (strlen($versioncomponents[2]) > 1) {
350                                // version 2.*.0RC1 or 2.*.0beta24 etc.
351                                return true;
352                            } else {
353                                // version 2.*.0
354                                $this->_addWarning('version',
355                                    "version $majver." . $versioncomponents[1] .
356                                        '.0 cannot be alpha or beta');
357                                return true;
358                            }
359                        } else {
360                            $this->_addWarning('version',
361                                "bugfix versions ($majver.x.y where y > 0) should " .
362                                'not be alpha or beta');
363                            return true;
364                        }
365                    } elseif ($versioncomponents[0] != '0') {
366                        $this->_addWarning('version',
367                            "only versions 0.x.y and $majver.x.y are allowed for alpha/beta releases");
368                        return true;
369                    }
370                    if ($versioncomponents[0] . 'a' == '0a') {
371                        return true;
372                    }
373                    if ($versioncomponents[0] == 0) {
374                        $versioncomponents[0] = '0';
375                        $this->_addWarning('version',
376                            'version "' . $version . '" should be "' .
377                            implode('.' ,$versioncomponents) . '"');
378                    }
379                }
380                return true;
381            break;
382            case 'stable' :
383                if ($versioncomponents[0] == '0') {
384                    $this->_addWarning('version', 'versions less than 1.0.0 cannot ' .
385                    'be stable');
386                    return true;
387                }
388                if (!is_numeric($versioncomponents[2])) {
389                    if (preg_match('/\d+(rc|a|alpha|b|beta)\d*/i',
390                          $versioncomponents[2])) {
391                        $this->_addWarning('version', 'version "' . $version . '" or any ' .
392                            'RC/beta/alpha version cannot be stable');
393                        return true;
394                    }
395                }
396                // check for a package that extends a package,
397                // like Foo and Foo2
398                if ($this->_packagexml->getExtends()) {
399                    $vlen = strlen($versioncomponents[0] . '');
400                    $majver = substr($name, strlen($name) - $vlen);
401                    while ($majver && !is_numeric($majver{0})) {
402                        $majver = substr($majver, 1);
403                    }
404                    if (($versioncomponents[0] != 0) && $majver != $versioncomponents[0]) {
405                        $this->_addWarning('version', 'first version number "' .
406                            $versioncomponents[0] . '" must match the postfix of ' .
407                            'package name "' . $name . '" (' .
408                            $majver . ')');
409                        return true;
410                    }
411                } elseif ($versioncomponents[0] > 1) {
412                    $this->_addWarning('version', 'major version x in x.y.z may not be greater than ' .
413                        '1 for any package that does not have an <extends> tag');
414                }
415                return true;
416            break;
417            default :
418                return false;
419            break;
420        }
421    }
422
423    /**
424     * @access protected
425     */
426    function validateMaintainers()
427    {
428        // maintainers can only be truly validated server-side for most channels
429        // but allow this customization for those who wish it
430        return true;
431    }
432
433    /**
434     * @access protected
435     */
436    function validateDate()
437    {
438        if ($this->_state == PEAR_VALIDATE_NORMAL ||
439              $this->_state == PEAR_VALIDATE_PACKAGING) {
440
441            if (!preg_match('/(\d\d\d\d)\-(\d\d)\-(\d\d)/',
442                  $this->_packagexml->getDate(), $res) ||
443                  count($res) < 4
444                  || !checkdate($res[2], $res[3], $res[1])
445                ) {
446                $this->_addFailure('date', 'invalid release date "' .
447                    $this->_packagexml->getDate() . '"');
448                return false;
449            }
450
451            if ($this->_state == PEAR_VALIDATE_PACKAGING &&
452                  $this->_packagexml->getDate() != date('Y-m-d')) {
453                $this->_addWarning('date', 'Release Date "' .
454                    $this->_packagexml->getDate() . '" is not today');
455            }
456        }
457        return true;
458    }
459
460    /**
461     * @access protected
462     */
463    function validateTime()
464    {
465        if (!$this->_packagexml->getTime()) {
466            // default of no time value set
467            return true;
468        }
469
470        // packager automatically sets time, so only validate if pear validate is called
471        if ($this->_state = PEAR_VALIDATE_NORMAL) {
472            if (!preg_match('/\d\d:\d\d:\d\d/',
473                  $this->_packagexml->getTime())) {
474                $this->_addFailure('time', 'invalid release time "' .
475                    $this->_packagexml->getTime() . '"');
476                return false;
477            }
478
479            $result = preg_match('|\d{2}\:\d{2}\:\d{2}|', $this->_packagexml->getTime(), $matches);
480            if ($result === false || empty($matches)) {
481                $this->_addFailure('time', 'invalid release time "' .
482                    $this->_packagexml->getTime() . '"');
483                return false;
484            }
485        }
486
487        return true;
488    }
489
490    /**
491     * @access protected
492     */
493    function validateState()
494    {
495        // this is the closest to "final" php4 can get
496        if (!PEAR_Validate::validState($this->_packagexml->getState())) {
497            if (strtolower($this->_packagexml->getState() == 'rc')) {
498                $this->_addFailure('state', 'RC is not a state, it is a version ' .
499                    'postfix, use ' . $this->_packagexml->getVersion() . 'RC1, state beta');
500            }
501            $this->_addFailure('state', 'invalid release state "' .
502                $this->_packagexml->getState() . '", must be one of: ' .
503                implode(', ', PEAR_Validate::getValidStates()));
504            return false;
505        }
506        return true;
507    }
508
509    /**
510     * @access protected
511     */
512    function validateStability()
513    {
514        $ret = true;
515        $packagestability = $this->_packagexml->getState();
516        $apistability = $this->_packagexml->getState('api');
517        if (!PEAR_Validate::validState($packagestability)) {
518            $this->_addFailure('state', 'invalid release stability "' .
519                $this->_packagexml->getState() . '", must be one of: ' .
520                implode(', ', PEAR_Validate::getValidStates()));
521            $ret = false;
522        }
523        $apistates = PEAR_Validate::getValidStates();
524        array_shift($apistates); // snapshot is not allowed
525        if (!in_array($apistability, $apistates)) {
526            $this->_addFailure('state', 'invalid API stability "' .
527                $this->_packagexml->getState('api') . '", must be one of: ' .
528                implode(', ', $apistates));
529            $ret = false;
530        }
531        return $ret;
532    }
533
534    /**
535     * @access protected
536     */
537    function validateSummary()
538    {
539        return true;
540    }
541
542    /**
543     * @access protected
544     */
545    function validateDescription()
546    {
547        return true;
548    }
549
550    /**
551     * @access protected
552     */
553    function validateLicense()
554    {
555        return true;
556    }
557
558    /**
559     * @access protected
560     */
561    function validateNotes()
562    {
563        return true;
564    }
565
566    /**
567     * for package.xml 2.0 only - channels can't use package.xml 1.0
568     * @access protected
569     */
570    function validateDependencies()
571    {
572        return true;
573    }
574
575    /**
576     * for package.xml 1.0 only
577     * @access private
578     */
579    function _validateFilelist()
580    {
581        return true; // placeholder for now
582    }
583
584    /**
585     * for package.xml 2.0 only
586     * @access protected
587     */
588    function validateMainFilelist()
589    {
590        return true; // placeholder for now
591    }
592
593    /**
594     * for package.xml 2.0 only
595     * @access protected
596     */
597    function validateReleaseFilelist()
598    {
599        return true; // placeholder for now
600    }
601
602    /**
603     * @access protected
604     */
605    function validateChangelog()
606    {
607        return true;
608    }
609
610    /**
611     * @access protected
612     */
613    function validateFilelist()
614    {
615        return true;
616    }
617
618    /**
619     * @access protected
620     */
621    function validateDeps()
622    {
623        return true;
624    }
625}