Release 1.6.1, merged in 931 to HEAD.
[htmlpurifier.git] / library / HTMLPurifier / ConfigSchema.php
blob3d5022850cb6548fa8c8979740580463dc656714
1 <?php
3 require_once 'HTMLPurifier/Error.php';
4 require_once 'HTMLPurifier/ConfigDef.php';
5 require_once 'HTMLPurifier/ConfigDef/Namespace.php';
6 require_once 'HTMLPurifier/ConfigDef/Directive.php';
7 require_once 'HTMLPurifier/ConfigDef/DirectiveAlias.php';
9 /**
10 * Configuration definition, defines directives and their defaults.
11 * @todo The ability to define things multiple times is confusing and should
12 * be factored out to its own function named registerDependency() or
13 * addNote(), where only the namespace.name and an extra descriptions
14 * documenting the nature of the dependency are needed. Since it's
15 * possible that the dependency is registered before the configuration
16 * is defined, deferring it to some sort of cache until it actually
17 * gets defined would be wise, keeping it opaque until it does get
18 * defined. We could add a finalize() method which would cause it to
19 * error out if we get a dangling dependency. It's difficult, however,
20 * to know whether or not it's a dependency, or a codependency, that is
21 * neither of them fully depends on it. Where does the configuration go
22 * then? This could be partially resolved by allowing blanket definitions
23 * and then splitting them up into finer-grained versions, however, there
24 * might be implementation difficulties in ini files regarding order of
25 * execution.
27 class HTMLPurifier_ConfigSchema {
29 /**
30 * Defaults of the directives and namespaces.
31 * @note This shares the exact same structure as HTMLPurifier_Config::$conf
33 var $defaults = array();
35 /**
36 * Definition of the directives.
38 var $info = array();
40 /**
41 * Definition of namespaces.
43 var $info_namespace = array();
45 /**
46 * Lookup table of allowed types.
48 var $types = array(
49 'string' => 'String',
50 'istring' => 'Case-insensitive string',
51 'int' => 'Integer',
52 'float' => 'Float',
53 'bool' => 'Boolean',
54 'lookup' => 'Lookup array',
55 'list' => 'Array list',
56 'hash' => 'Associative array',
57 'mixed' => 'Mixed'
60 /**
61 * Initializes the default namespaces.
63 function initialize() {
64 $this->defineNamespace('Core', 'Core features that are always available.');
65 $this->defineNamespace('Attr', 'Features regarding attribute validation.');
66 $this->defineNamespace('URI', 'Features regarding Uniform Resource Identifiers.');
67 $this->defineNamespace('HTML', 'Configuration regarding allowed HTML.');
68 $this->defineNamespace('CSS', 'Configuration regarding allowed CSS.');
69 $this->defineNamespace('Test', 'Developer testing configuration for our unit tests.');
72 /**
73 * Retrieves an instance of the application-wide configuration definition.
74 * @static
76 static function &instance($prototype = null) {
77 static $instance;
78 if ($prototype !== null) {
79 $instance = $prototype;
80 } elseif ($instance === null || $prototype === true) {
81 $instance = new HTMLPurifier_ConfigSchema();
82 $instance->initialize();
84 return $instance;
87 /**
88 * Defines a directive for configuration
89 * @static
90 * @warning Will fail of directive's namespace is defined
91 * @param $namespace Namespace the directive is in
92 * @param $name Key of directive
93 * @param $default Default value of directive
94 * @param $type Allowed type of the directive. See
95 * HTMLPurifier_DirectiveDef::$type for allowed values
96 * @param $description Description of directive for documentation
98 static function define(
99 $namespace, $name, $default, $type,
100 $description
102 $def =& HTMLPurifier_ConfigSchema::instance();
103 if (!isset($def->info[$namespace])) {
104 trigger_error('Cannot define directive for undefined namespace',
105 E_USER_ERROR);
106 return;
108 if (!ctype_alnum($name)) {
109 trigger_error('Directive name must be alphanumeric',
110 E_USER_ERROR);
111 return;
113 if (empty($description)) {
114 trigger_error('Description must be non-empty',
115 E_USER_ERROR);
116 return;
118 if (isset($def->info[$namespace][$name])) {
119 if (
120 $def->info[$namespace][$name]->type !== $type ||
121 $def->defaults[$namespace][$name] !== $default
123 trigger_error('Inconsistent default or type, cannot redefine');
124 return;
126 } else {
127 // process modifiers
128 $type_values = explode('/', $type, 2);
129 $type = $type_values[0];
130 $modifier = isset($type_values[1]) ? $type_values[1] : false;
131 $allow_null = ($modifier === 'null');
133 if (!isset($def->types[$type])) {
134 trigger_error('Invalid type for configuration directive',
135 E_USER_ERROR);
136 return;
138 $default = $def->validate($default, $type, $allow_null);
139 if ($def->isError($default)) {
140 trigger_error('Default value does not match directive type',
141 E_USER_ERROR);
142 return;
144 $def->info[$namespace][$name] =
145 new HTMLPurifier_ConfigDef_Directive();
146 $def->info[$namespace][$name]->type = $type;
147 $def->info[$namespace][$name]->allow_null = $allow_null;
148 $def->defaults[$namespace][$name] = $default;
150 $backtrace = debug_backtrace();
151 $file = $def->mungeFilename($backtrace[0]['file']);
152 $line = $backtrace[0]['line'];
153 $def->info[$namespace][$name]->addDescription($file,$line,$description);
157 * Defines a namespace for directives to be put into.
158 * @static
159 * @param $namespace Namespace's name
160 * @param $description Description of the namespace
162 static function defineNamespace($namespace, $description) {
163 $def =& HTMLPurifier_ConfigSchema::instance();
164 if (isset($def->info[$namespace])) {
165 trigger_error('Cannot redefine namespace', E_USER_ERROR);
166 return;
168 if (!ctype_alnum($namespace)) {
169 trigger_error('Namespace name must be alphanumeric',
170 E_USER_ERROR);
171 return;
173 if (empty($description)) {
174 trigger_error('Description must be non-empty',
175 E_USER_ERROR);
176 return;
178 $def->info[$namespace] = array();
179 $def->info_namespace[$namespace] = new HTMLPurifier_ConfigDef_Namespace();
180 $def->info_namespace[$namespace]->description = $description;
181 $def->defaults[$namespace] = array();
185 * Defines a directive value alias.
187 * Directive value aliases are convenient for developers because it lets
188 * them set a directive to several values and get the same result.
189 * @static
190 * @param $namespace Directive's namespace
191 * @param $name Name of Directive
192 * @param $alias Name of aliased value
193 * @param $real Value aliased value will be converted into
195 static function defineValueAliases($namespace, $name, $aliases) {
196 $def =& HTMLPurifier_ConfigSchema::instance();
197 if (!isset($def->info[$namespace][$name])) {
198 trigger_error('Cannot set value alias for non-existant directive',
199 E_USER_ERROR);
200 return;
202 foreach ($aliases as $alias => $real) {
203 if (!$def->info[$namespace][$name] !== true &&
204 !isset($def->info[$namespace][$name]->allowed[$real])
206 trigger_error('Cannot define alias to value that is not allowed',
207 E_USER_ERROR);
208 return;
210 if (isset($def->info[$namespace][$name]->allowed[$alias])) {
211 trigger_error('Cannot define alias over allowed value',
212 E_USER_ERROR);
213 return;
215 $def->info[$namespace][$name]->aliases[$alias] = $real;
220 * Defines a set of allowed values for a directive.
221 * @static
222 * @param $namespace Namespace of directive
223 * @param $name Name of directive
224 * @param $allowed_values Arraylist of allowed values
226 static function defineAllowedValues($namespace, $name, $allowed_values) {
227 $def =& HTMLPurifier_ConfigSchema::instance();
228 if (!isset($def->info[$namespace][$name])) {
229 trigger_error('Cannot define allowed values for undefined directive',
230 E_USER_ERROR);
231 return;
233 $directive =& $def->info[$namespace][$name];
234 $type = $directive->type;
235 if ($type != 'string' && $type != 'istring') {
236 trigger_error('Cannot define allowed values for directive whose type is not string',
237 E_USER_ERROR);
238 return;
240 if ($directive->allowed === true) {
241 $directive->allowed = array();
243 foreach ($allowed_values as $value) {
244 $directive->allowed[$value] = true;
246 if ($def->defaults[$namespace][$name] !== null &&
247 !isset($directive->allowed[$def->defaults[$namespace][$name]])) {
248 trigger_error('Default value must be in allowed range of variables',
249 E_USER_ERROR);
250 $directive->allowed = true; // undo undo!
251 return;
256 * Defines a directive alias for backwards compatibility
257 * @static
258 * @param $namespace
259 * @param $name Directive that will be aliased
260 * @param $new_namespace
261 * @param $new_name Directive that the alias will be to
263 static function defineAlias($namespace, $name, $new_namespace, $new_name) {
264 $def =& HTMLPurifier_ConfigSchema::instance();
265 if (!isset($def->info[$namespace])) {
266 trigger_error('Cannot define directive alias in undefined namespace',
267 E_USER_ERROR);
268 return;
270 if (!ctype_alnum($name)) {
271 trigger_error('Directive name must be alphanumeric',
272 E_USER_ERROR);
273 return;
275 if (isset($def->info[$namespace][$name])) {
276 trigger_error('Cannot define alias over directive',
277 E_USER_ERROR);
278 return;
280 if (!isset($def->info[$new_namespace][$new_name])) {
281 trigger_error('Cannot define alias to undefined directive',
282 E_USER_ERROR);
283 return;
285 if ($def->info[$new_namespace][$new_name]->class == 'alias') {
286 trigger_error('Cannot define alias to alias',
287 E_USER_ERROR);
288 return;
290 $def->info[$namespace][$name] =
291 new HTMLPurifier_ConfigDef_DirectiveAlias(
292 $new_namespace, $new_name);
296 * Validate a variable according to type. Return null if invalid.
298 function validate($var, $type, $allow_null = false) {
299 if (!isset($this->types[$type])) {
300 trigger_error('Invalid type', E_USER_ERROR);
301 return;
303 if ($allow_null && $var === null) return null;
304 switch ($type) {
305 case 'mixed':
306 return $var;
307 case 'istring':
308 case 'string':
309 if (!is_string($var)) break;
310 if ($type === 'istring') $var = strtolower($var);
311 return $var;
312 case 'int':
313 if (is_string($var) && ctype_digit($var)) $var = (int) $var;
314 elseif (!is_int($var)) break;
315 return $var;
316 case 'float':
317 if (is_string($var) && is_numeric($var)) $var = (float) $var;
318 elseif (!is_float($var)) break;
319 return $var;
320 case 'bool':
321 if (is_int($var) && ($var === 0 || $var === 1)) {
322 $var = (bool) $var;
323 } elseif (is_string($var)) {
324 if ($var == 'on' || $var == 'true' || $var == '1') {
325 $var = true;
326 } elseif ($var == 'off' || $var == 'false' || $var == '0') {
327 $var = false;
328 } else {
329 break;
331 } elseif (!is_bool($var)) break;
332 return $var;
333 case 'list':
334 case 'hash':
335 case 'lookup':
336 if (is_string($var)) {
337 // special case: technically, this is an array with
338 // a single empty string item, but having an empty
339 // array is more intuitive
340 if ($var == '') return array();
341 // simplistic string to array method that only works
342 // for simple lists of tag names or alphanumeric characters
343 $var = explode(',',$var);
344 // remove spaces
345 foreach ($var as $i => $j) $var[$i] = trim($j);
347 if (!is_array($var)) break;
348 $keys = array_keys($var);
349 if ($keys === array_keys($keys)) {
350 if ($type == 'list') return $var;
351 elseif ($type == 'lookup') {
352 $new = array();
353 foreach ($var as $key) {
354 $new[$key] = true;
356 return $new;
357 } else break;
359 if ($type === 'lookup') {
360 foreach ($var as $key => $value) {
361 $var[$key] = true;
364 return $var;
366 $error = new HTMLPurifier_Error();
367 return $error;
371 * Takes an absolute path and munges it into a more manageable relative path
373 function mungeFilename($filename) {
374 $offset = strrpos($filename, 'HTMLPurifier');
375 $filename = substr($filename, $offset);
376 $filename = str_replace('\\', '/', $filename);
377 return $filename;
381 * Checks if var is an HTMLPurifier_Error object
383 function isError($var) {
384 if (!is_object($var)) return false;
385 if (!($var instanceof HTMLPurifier_Error)) return false;
386 return true;