aboutsummaryrefslogtreecommitdiffstats
path: root/lib/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php
blob: f374f6a02259f8fae5303d3f794c74b5b6461a99 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<?php

/**
 * Performs validations on HTMLPurifier_ConfigSchema_Interchange
 *
 * @note If you see '// handled by InterchangeBuilder', that means a
 *       design decision in that class would prevent this validation from
 *       ever being necessary. We have them anyway, however, for
 *       redundancy.
 */
class HTMLPurifier_ConfigSchema_Validator
{

    /**
     * Easy to access global objects.
     */
    protected $interchange, $aliases;

    /**
     * Context-stack to provide easy to read error messages.
     */
    protected $context = array();

    /**
     * HTMLPurifier_VarParser to test default's type.
     */
    protected $parser;

    public function __construct() {
        $this->parser = new HTMLPurifier_VarParser();
    }

    /**
     * Validates a fully-formed interchange object. Throws an
     * HTMLPurifier_ConfigSchema_Exception if there's a problem.
     */
    public function validate($interchange) {
        $this->interchange = $interchange;
        $this->aliases = array();
        // PHP is a bit lax with integer <=> string conversions in
        // arrays, so we don't use the identical !== comparison
        foreach ($interchange->directives as $i => $directive) {
            $id = $directive->id->toString();
            if ($i != $id) $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'");
            $this->validateDirective($directive);
        }
        return true;
    }

    /**
     * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object.
     */
    public function validateId($id) {
        $id_string = $id->toString();
        $this->context[] = "id '$id_string'";
        if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) {
            // handled by InterchangeBuilder
            $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id');
        }
        // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.)
        // we probably should check that it has at least one namespace
        $this->with($id, 'key')
            ->assertNotEmpty()
            ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder
        array_pop($this->context);
    }

    /**
     * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object.
     */
    public function validateDirective($d) {
        $id = $d->id->toString();
        $this->context[] = "directive '$id'";
        $this->validateId($d->id);

        $this->with($d, 'description')
            ->assertNotEmpty();

        // BEGIN - handled by InterchangeBuilder
        $this->with($d, 'type')
            ->assertNotEmpty();
        $this->with($d, 'typeAllowsNull')
            ->assertIsBool();
        try {
            // This also tests validity of $d->type
            $this->parser->parse($d->default, $d->type, $d->typeAllowsNull);
        } catch (HTMLPurifier_VarParserException $e) {
            $this->error('default', 'had error: ' . $e->getMessage());
        }
        // END - handled by InterchangeBuilder

        if (!is_null($d->allowed) || !empty($d->valueAliases)) {
            // allowed and valueAliases require that we be dealing with
            // strings, so check for that early.
            $d_int = HTMLPurifier_VarParser::$types[$d->type];
            if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) {
                $this->error('type', 'must be a string type when used with allowed or value aliases');
            }
        }

        $this->validateDirectiveAllowed($d);
        $this->validateDirectiveValueAliases($d);
        $this->validateDirectiveAliases($d);

        array_pop($this->context);
    }

    /**
     * Extra validation if $allowed member variable of
     * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
     */
    public function validateDirectiveAllowed($d) {
        if (is_null($d->allowed)) return;
        $this->with($d, 'allowed')
            ->assertNotEmpty()
            ->assertIsLookup(); // handled by InterchangeBuilder
        if (is_string($d->default) && !isset($d->allowed[$d->default])) {
            $this->error('default', 'must be an allowed value');
        }
        $this->context[] = 'allowed';
        foreach ($d->allowed as $val => $x) {
            if (!is_string($val)) $this->error("value $val", 'must be a string');
        }
        array_pop($this->context);
    }

    /**
     * Extra validation if $valueAliases member variable of
     * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
     */
    public function validateDirectiveValueAliases($d) {
        if (is_null($d->valueAliases)) return;
        $this->with($d, 'valueAliases')
            ->assertIsArray(); // handled by InterchangeBuilder
        $this->context[] = 'valueAliases';
        foreach ($d->valueAliases as $alias => $real) {
            if (!is_string($alias)) $this->error("alias $alias", 'must be a string');
            if (!is_string($real))  $this->error("alias target $real from alias '$alias'",  'must be a string');
            if ($alias === $real) {
                $this->error("alias '$alias'", "must not be an alias to itself");
            }
        }
        if (!is_null($d->allowed)) {
            foreach ($d->valueAliases as $alias => $real) {
                if (isset($d->allowed[$alias])) {
                    $this->error("alias '$alias'", 'must not be an allowed value');
                } elseif (!isset($d->allowed[$real])) {
                    $this->error("alias '$alias'", 'must be an alias to an allowed value');
                }
            }
        }
        array_pop($this->context);
    }

    /**
     * Extra validation if $aliases member variable of
     * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
     */
    public function validateDirectiveAliases($d) {
        $this->with($d, 'aliases')
            ->assertIsArray(); // handled by InterchangeBuilder
        $this->context[] = 'aliases';
        foreach ($d->aliases as $alias) {
            $this->validateId($alias);
            $s = $alias->toString();
            if (isset($this->interchange->directives[$s])) {
                $this->error("alias '$s'", 'collides with another directive');
            }
            if (isset($this->aliases[$s])) {
                $other_directive = $this->aliases[$s];
                $this->error("alias '$s'", "collides with alias for directive '$other_directive'");
            }
            $this->aliases[$s] = $d->id->toString();
        }
        array_pop($this->context);
    }

    // protected helper functions

    /**
     * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom
     * for validating simple member variables of objects.
     */
    protected function with($obj, $member) {
        return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member);
    }

    /**
     * Emits an error, providing helpful context.
     */
    protected function error($target, $msg) {
        if ($target !== false) $prefix = ucfirst($target) . ' in ' .  $this->getFormattedContext();
        else $prefix = ucfirst($this->getFormattedContext());
        throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg));
    }

    /**
     * Returns a formatted context string.
     */
    protected function getFormattedContext() {
        return implode(' in ', array_reverse($this->context));
    }

}

// vim: et sw=4 sts=4