~fusonic/chive/1.1

« back to all changes in this revision

Viewing changes to yii/vendors/htmlpurifier/HTMLPurifier.standalone.php

  • Committer: Matthias Burtscher
  • Date: 2010-02-12 09:12:35 UTC
  • Revision ID: matthias.burtscher@fusonic.net-20100212091235-jqxrb62klx872ajc
* Updated Yii to 1.1.0
* Removed CodePress and CodeMirror
* Updated jQuery and some plugins
* Cleaned some code ...

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
<?php
2
 
 
3
 
/**
4
 
 * @file
5
 
 * This file was auto-generated by generate-includes.php and includes all of
6
 
 * the core files required by HTML Purifier. Use this if performance is a
7
 
 * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
8
 
 * FILE, changes will be overwritten the next time the script is run.
9
 
 * 
10
 
 * @version 3.2.0
11
 
 * 
12
 
 * @warning
13
 
 *      You must *not* include any other HTML Purifier files before this file,
14
 
 *      because 'require' not 'require_once' is used.
15
 
 * 
16
 
 * @warning
17
 
 *      This file requires that the include path contains the HTML Purifier
18
 
 *      library directory; this is not auto-set.
19
 
 */
20
 
 
21
 
 
22
 
 
23
 
/*! @mainpage
24
 
 * 
25
 
 * HTML Purifier is an HTML filter that will take an arbitrary snippet of
26
 
 * HTML and rigorously test, validate and filter it into a version that
27
 
 * is safe for output onto webpages. It achieves this by:
28
 
 * 
29
 
 *  -# Lexing (parsing into tokens) the document,
30
 
 *  -# Executing various strategies on the tokens:
31
 
 *      -# Removing all elements not in the whitelist,
32
 
 *      -# Making the tokens well-formed,
33
 
 *      -# Fixing the nesting of the nodes, and
34
 
 *      -# Validating attributes of the nodes; and
35
 
 *  -# Generating HTML from the purified tokens.
36
 
 * 
37
 
 * However, most users will only need to interface with the HTMLPurifier
38
 
 * and HTMLPurifier_Config.
39
 
 */
40
 
 
41
 
/*
42
 
    HTML Purifier 3.2.0 - Standards Compliant HTML Filtering
43
 
    Copyright (C) 2006-2008 Edward Z. Yang
44
 
 
45
 
    This library is free software; you can redistribute it and/or
46
 
    modify it under the terms of the GNU Lesser General Public
47
 
    License as published by the Free Software Foundation; either
48
 
    version 2.1 of the License, or (at your option) any later version.
49
 
 
50
 
    This library is distributed in the hope that it will be useful,
51
 
    but WITHOUT ANY WARRANTY; without even the implied warranty of
52
 
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
53
 
    Lesser General Public License for more details.
54
 
 
55
 
    You should have received a copy of the GNU Lesser General Public
56
 
    License along with this library; if not, write to the Free Software
57
 
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
58
 
 */
59
 
 
60
 
/**
61
 
 * Facade that coordinates HTML Purifier's subsystems in order to purify HTML.
62
 
 * 
63
 
 * @note There are several points in which configuration can be specified 
64
 
 *       for HTML Purifier.  The precedence of these (from lowest to
65
 
 *       highest) is as follows:
66
 
 *          -# Instance: new HTMLPurifier($config)
67
 
 *          -# Invocation: purify($html, $config)
68
 
 *       These configurations are entirely independent of each other and
69
 
 *       are *not* merged (this behavior may change in the future).
70
 
 * 
71
 
 * @todo We need an easier way to inject strategies using the configuration
72
 
 *       object.
73
 
 */
74
 
class HTMLPurifier
75
 
{
76
 
    
77
 
    /** Version of HTML Purifier */
78
 
    public $version = '3.2.0';
79
 
    
80
 
    /** Constant with version of HTML Purifier */
81
 
    const VERSION = '3.2.0';
82
 
    
83
 
    /** Global configuration object */
84
 
    public $config;
85
 
    
86
 
    /** Array of extra HTMLPurifier_Filter objects to run on HTML, for backwards compatibility */
87
 
    private $filters = array();
88
 
    
89
 
    /** Single instance of HTML Purifier */
90
 
    private static $instance;
91
 
    
92
 
    protected $strategy, $generator;
93
 
    
94
 
    /**
95
 
     * Resultant HTMLPurifier_Context of last run purification. Is an array
96
 
     * of contexts if the last called method was purifyArray().
97
 
     */
98
 
    public $context;
99
 
    
100
 
    /**
101
 
     * Initializes the purifier.
102
 
     * @param $config Optional HTMLPurifier_Config object for all instances of
103
 
     *                the purifier, if omitted, a default configuration is
104
 
     *                supplied (which can be overridden on a per-use basis).
105
 
     *                The parameter can also be any type that
106
 
     *                HTMLPurifier_Config::create() supports.
107
 
     */
108
 
    public function __construct($config = null) {
109
 
        
110
 
        $this->config = HTMLPurifier_Config::create($config);
111
 
        
112
 
        $this->strategy     = new HTMLPurifier_Strategy_Core();
113
 
        
114
 
    }
115
 
    
116
 
    /**
117
 
     * Adds a filter to process the output. First come first serve
118
 
     * @param $filter HTMLPurifier_Filter object
119
 
     */
120
 
    public function addFilter($filter) {
121
 
        trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING);
122
 
        $this->filters[] = $filter;
123
 
    }
124
 
    
125
 
    /**
126
 
     * Filters an HTML snippet/document to be XSS-free and standards-compliant.
127
 
     * 
128
 
     * @param $html String of HTML to purify
129
 
     * @param $config HTMLPurifier_Config object for this operation, if omitted,
130
 
     *                defaults to the config object specified during this
131
 
     *                object's construction. The parameter can also be any type
132
 
     *                that HTMLPurifier_Config::create() supports.
133
 
     * @return Purified HTML
134
 
     */
135
 
    public function purify($html, $config = null) {
136
 
        
137
 
        // :TODO: make the config merge in, instead of replace
138
 
        $config = $config ? HTMLPurifier_Config::create($config) : $this->config;
139
 
        
140
 
        // implementation is partially environment dependant, partially
141
 
        // configuration dependant
142
 
        $lexer = HTMLPurifier_Lexer::create($config);
143
 
        
144
 
        $context = new HTMLPurifier_Context();
145
 
        
146
 
        // setup HTML generator
147
 
        $this->generator = new HTMLPurifier_Generator($config, $context);
148
 
        $context->register('Generator', $this->generator);
149
 
        
150
 
        // set up global context variables
151
 
        if ($config->get('Core', 'CollectErrors')) {
152
 
            // may get moved out if other facilities use it
153
 
            $language_factory = HTMLPurifier_LanguageFactory::instance();
154
 
            $language = $language_factory->create($config, $context);
155
 
            $context->register('Locale', $language);
156
 
            
157
 
            $error_collector = new HTMLPurifier_ErrorCollector($context);
158
 
            $context->register('ErrorCollector', $error_collector);
159
 
        }
160
 
        
161
 
        // setup id_accumulator context, necessary due to the fact that
162
 
        // AttrValidator can be called from many places
163
 
        $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
164
 
        $context->register('IDAccumulator', $id_accumulator);
165
 
        
166
 
        $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context);
167
 
        
168
 
        // setup filters
169
 
        $filter_flags = $config->getBatch('Filter');
170
 
        $custom_filters = $filter_flags['Custom'];
171
 
        unset($filter_flags['Custom']);
172
 
        $filters = array();
173
 
        foreach ($filter_flags as $filter => $flag) {
174
 
            if (!$flag) continue;
175
 
            $class = "HTMLPurifier_Filter_$filter";
176
 
            $filters[] = new $class;
177
 
        }
178
 
        foreach ($custom_filters as $filter) {
179
 
            // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
180
 
            $filters[] = $filter;
181
 
        }
182
 
        $filters = array_merge($filters, $this->filters);
183
 
        // maybe prepare(), but later
184
 
        
185
 
        for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) {
186
 
            $html = $filters[$i]->preFilter($html, $config, $context);
187
 
        }
188
 
        
189
 
        // purified HTML
190
 
        $html = 
191
 
            $this->generator->generateFromTokens(
192
 
                // list of tokens
193
 
                $this->strategy->execute(
194
 
                    // list of un-purified tokens
195
 
                    $lexer->tokenizeHTML(
196
 
                        // un-purified HTML
197
 
                        $html, $config, $context
198
 
                    ),
199
 
                    $config, $context
200
 
                )
201
 
            );
202
 
        
203
 
        for ($i = $filter_size - 1; $i >= 0; $i--) {
204
 
            $html = $filters[$i]->postFilter($html, $config, $context);
205
 
        }
206
 
        
207
 
        $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context);
208
 
        $this->context =& $context;
209
 
        return $html;
210
 
    }
211
 
    
212
 
    /**
213
 
     * Filters an array of HTML snippets
214
 
     * @param $config Optional HTMLPurifier_Config object for this operation.
215
 
     *                See HTMLPurifier::purify() for more details.
216
 
     * @return Array of purified HTML
217
 
     */
218
 
    public function purifyArray($array_of_html, $config = null) {
219
 
        $context_array = array();
220
 
        foreach ($array_of_html as $key => $html) {
221
 
            $array_of_html[$key] = $this->purify($html, $config);
222
 
            $context_array[$key] = $this->context;
223
 
        }
224
 
        $this->context = $context_array;
225
 
        return $array_of_html;
226
 
    }
227
 
    
228
 
    /**
229
 
     * Singleton for enforcing just one HTML Purifier in your system
230
 
     * @param $prototype Optional prototype HTMLPurifier instance to
231
 
     *                   overload singleton with, or HTMLPurifier_Config
232
 
     *                   instance to configure the generated version with.
233
 
     */
234
 
    public static function instance($prototype = null) {
235
 
        if (!self::$instance || $prototype) {
236
 
            if ($prototype instanceof HTMLPurifier) {
237
 
                self::$instance = $prototype;
238
 
            } elseif ($prototype) {
239
 
                self::$instance = new HTMLPurifier($prototype);
240
 
            } else {
241
 
                self::$instance = new HTMLPurifier();
242
 
            }
243
 
        }
244
 
        return self::$instance;
245
 
    }
246
 
    
247
 
    /**
248
 
     * @note Backwards compatibility, see instance()
249
 
     */
250
 
    public static function getInstance($prototype = null) {
251
 
        return HTMLPurifier::instance($prototype);
252
 
    }
253
 
    
254
 
}
255
 
 
256
 
 
257
 
 
258
 
/**
259
 
 * Defines common attribute collections that modules reference
260
 
 */
261
 
 
262
 
class HTMLPurifier_AttrCollections
263
 
{
264
 
    
265
 
    /**
266
 
     * Associative array of attribute collections, indexed by name
267
 
     */
268
 
    public $info = array();
269
 
    
270
 
    /**
271
 
     * Performs all expansions on internal data for use by other inclusions
272
 
     * It also collects all attribute collection extensions from
273
 
     * modules
274
 
     * @param $attr_types HTMLPurifier_AttrTypes instance
275
 
     * @param $modules Hash array of HTMLPurifier_HTMLModule members
276
 
     */
277
 
    public function __construct($attr_types, $modules) {
278
 
        // load extensions from the modules
279
 
        foreach ($modules as $module) {
280
 
            foreach ($module->attr_collections as $coll_i => $coll) {
281
 
                if (!isset($this->info[$coll_i])) {
282
 
                    $this->info[$coll_i] = array();
283
 
                }
284
 
                foreach ($coll as $attr_i => $attr) {
285
 
                    if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) {
286
 
                        // merge in includes
287
 
                        $this->info[$coll_i][$attr_i] = array_merge(
288
 
                            $this->info[$coll_i][$attr_i], $attr);
289
 
                        continue;
290
 
                    }
291
 
                    $this->info[$coll_i][$attr_i] = $attr;
292
 
                }
293
 
            }
294
 
        }
295
 
        // perform internal expansions and inclusions
296
 
        foreach ($this->info as $name => $attr) {
297
 
            // merge attribute collections that include others
298
 
            $this->performInclusions($this->info[$name]);
299
 
            // replace string identifiers with actual attribute objects
300
 
            $this->expandIdentifiers($this->info[$name], $attr_types);
301
 
        }
302
 
    }
303
 
    
304
 
    /**
305
 
     * Takes a reference to an attribute associative array and performs
306
 
     * all inclusions specified by the zero index.
307
 
     * @param &$attr Reference to attribute array
308
 
     */
309
 
    public function performInclusions(&$attr) {
310
 
        if (!isset($attr[0])) return;
311
 
        $merge = $attr[0];
312
 
        $seen  = array(); // recursion guard
313
 
        // loop through all the inclusions
314
 
        for ($i = 0; isset($merge[$i]); $i++) {
315
 
            if (isset($seen[$merge[$i]])) continue;
316
 
            $seen[$merge[$i]] = true;
317
 
            // foreach attribute of the inclusion, copy it over
318
 
            if (!isset($this->info[$merge[$i]])) continue;
319
 
            foreach ($this->info[$merge[$i]] as $key => $value) {
320
 
                if (isset($attr[$key])) continue; // also catches more inclusions
321
 
                $attr[$key] = $value;
322
 
            }
323
 
            if (isset($this->info[$merge[$i]][0])) {
324
 
                // recursion
325
 
                $merge = array_merge($merge, $this->info[$merge[$i]][0]);
326
 
            }
327
 
        }
328
 
        unset($attr[0]);
329
 
    }
330
 
    
331
 
    /**
332
 
     * Expands all string identifiers in an attribute array by replacing
333
 
     * them with the appropriate values inside HTMLPurifier_AttrTypes
334
 
     * @param &$attr Reference to attribute array
335
 
     * @param $attr_types HTMLPurifier_AttrTypes instance
336
 
     */
337
 
    public function expandIdentifiers(&$attr, $attr_types) {
338
 
        
339
 
        // because foreach will process new elements we add, make sure we
340
 
        // skip duplicates
341
 
        $processed = array();
342
 
        
343
 
        foreach ($attr as $def_i => $def) {
344
 
            // skip inclusions
345
 
            if ($def_i === 0) continue;
346
 
            
347
 
            if (isset($processed[$def_i])) continue;
348
 
            
349
 
            // determine whether or not attribute is required
350
 
            if ($required = (strpos($def_i, '*') !== false)) {
351
 
                // rename the definition
352
 
                unset($attr[$def_i]);
353
 
                $def_i = trim($def_i, '*');
354
 
                $attr[$def_i] = $def;
355
 
            }
356
 
            
357
 
            $processed[$def_i] = true;
358
 
            
359
 
            // if we've already got a literal object, move on
360
 
            if (is_object($def)) {
361
 
                // preserve previous required
362
 
                $attr[$def_i]->required = ($required || $attr[$def_i]->required);
363
 
                continue;
364
 
            }
365
 
            
366
 
            if ($def === false) {
367
 
                unset($attr[$def_i]);
368
 
                continue;
369
 
            }
370
 
            
371
 
            if ($t = $attr_types->get($def)) {
372
 
                $attr[$def_i] = $t;
373
 
                $attr[$def_i]->required = $required;
374
 
            } else {
375
 
                unset($attr[$def_i]);
376
 
            }
377
 
        }
378
 
        
379
 
    }
380
 
    
381
 
}
382
 
 
383
 
 
384
 
 
385
 
 
386
 
/**
387
 
 * Base class for all validating attribute definitions.
388
 
 * 
389
 
 * This family of classes forms the core for not only HTML attribute validation,
390
 
 * but also any sort of string that needs to be validated or cleaned (which
391
 
 * means CSS properties and composite definitions are defined here too).  
392
 
 * Besides defining (through code) what precisely makes the string valid,
393
 
 * subclasses are also responsible for cleaning the code if possible.
394
 
 */
395
 
 
396
 
abstract class HTMLPurifier_AttrDef
397
 
{
398
 
    
399
 
    /**
400
 
     * Tells us whether or not an HTML attribute is minimized. Has no
401
 
     * meaning in other contexts.
402
 
     */
403
 
    public $minimized = false;
404
 
    
405
 
    /**
406
 
     * Tells us whether or not an HTML attribute is required. Has no
407
 
     * meaning in other contexts
408
 
     */
409
 
    public $required = false;
410
 
    
411
 
    /**
412
 
     * Validates and cleans passed string according to a definition.
413
 
     * 
414
 
     * @param $string String to be validated and cleaned.
415
 
     * @param $config Mandatory HTMLPurifier_Config object.
416
 
     * @param $context Mandatory HTMLPurifier_AttrContext object.
417
 
     */
418
 
    abstract public function validate($string, $config, $context);
419
 
    
420
 
    /**
421
 
     * Convenience method that parses a string as if it were CDATA.
422
 
     * 
423
 
     * This method process a string in the manner specified at
424
 
     * <http://www.w3.org/TR/html4/types.html#h-6.2> by removing
425
 
     * leading and trailing whitespace, ignoring line feeds, and replacing
426
 
     * carriage returns and tabs with spaces.  While most useful for HTML
427
 
     * attributes specified as CDATA, it can also be applied to most CSS
428
 
     * values.
429
 
     * 
430
 
     * @note This method is not entirely standards compliant, as trim() removes
431
 
     *       more types of whitespace than specified in the spec. In practice,
432
 
     *       this is rarely a problem, as those extra characters usually have
433
 
     *       already been removed by HTMLPurifier_Encoder.
434
 
     * 
435
 
     * @warning This processing is inconsistent with XML's whitespace handling
436
 
     *          as specified by section 3.3.3 and referenced XHTML 1.0 section
437
 
     *          4.7.  However, note that we are NOT necessarily
438
 
     *          parsing XML, thus, this behavior may still be correct. We
439
 
     *          assume that newlines have been normalized.
440
 
     */
441
 
    public function parseCDATA($string) {
442
 
        $string = trim($string);
443
 
        $string = str_replace(array("\n", "\t", "\r"), ' ', $string);
444
 
        return $string;
445
 
    }
446
 
    
447
 
    /**
448
 
     * Factory method for creating this class from a string.
449
 
     * @param $string String construction info
450
 
     * @return Created AttrDef object corresponding to $string
451
 
     */
452
 
    public function make($string) {
453
 
        // default implementation, return a flyweight of this object.
454
 
        // If $string has an effect on the returned object (i.e. you
455
 
        // need to overload this method), it is best
456
 
        // to clone or instantiate new copies. (Instantiation is safer.)
457
 
        return $this;
458
 
    }
459
 
    
460
 
    /**
461
 
     * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work
462
 
     * properly. THIS IS A HACK!
463
 
     */
464
 
    protected function mungeRgb($string) {
465
 
        return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
466
 
    }
467
 
    
468
 
}
469
 
 
470
 
 
471
 
 
472
 
 
473
 
/**
474
 
 * Processes an entire attribute array for corrections needing multiple values.
475
 
 * 
476
 
 * Occasionally, a certain attribute will need to be removed and popped onto
477
 
 * another value.  Instead of creating a complex return syntax for
478
 
 * HTMLPurifier_AttrDef, we just pass the whole attribute array to a
479
 
 * specialized object and have that do the special work.  That is the
480
 
 * family of HTMLPurifier_AttrTransform.
481
 
 * 
482
 
 * An attribute transformation can be assigned to run before or after
483
 
 * HTMLPurifier_AttrDef validation.  See HTMLPurifier_HTMLDefinition for
484
 
 * more details.
485
 
 */
486
 
 
487
 
abstract class HTMLPurifier_AttrTransform
488
 
{
489
 
    
490
 
    /**
491
 
     * Abstract: makes changes to the attributes dependent on multiple values.
492
 
     * 
493
 
     * @param $attr Assoc array of attributes, usually from
494
 
     *              HTMLPurifier_Token_Tag::$attr
495
 
     * @param $config Mandatory HTMLPurifier_Config object.
496
 
     * @param $context Mandatory HTMLPurifier_Context object
497
 
     * @returns Processed attribute array.
498
 
     */
499
 
    abstract public function transform($attr, $config, $context);
500
 
    
501
 
    /**
502
 
     * Prepends CSS properties to the style attribute, creating the
503
 
     * attribute if it doesn't exist.
504
 
     * @param $attr Attribute array to process (passed by reference)
505
 
     * @param $css CSS to prepend
506
 
     */
507
 
    public function prependCSS(&$attr, $css) {
508
 
        $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
509
 
        $attr['style'] = $css . $attr['style'];
510
 
    }
511
 
    
512
 
    /**
513
 
     * Retrieves and removes an attribute
514
 
     * @param $attr Attribute array to process (passed by reference)
515
 
     * @param $key Key of attribute to confiscate
516
 
     */
517
 
    public function confiscateAttr(&$attr, $key) {
518
 
        if (!isset($attr[$key])) return null;
519
 
        $value = $attr[$key];
520
 
        unset($attr[$key]);
521
 
        return $value;
522
 
    }
523
 
    
524
 
}
525
 
 
526
 
 
527
 
 
528
 
 
529
 
/**
530
 
 * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects
531
 
 */
532
 
class HTMLPurifier_AttrTypes
533
 
{
534
 
    /**
535
 
     * Lookup array of attribute string identifiers to concrete implementations
536
 
     */
537
 
    protected $info = array();
538
 
    
539
 
    /**
540
 
     * Constructs the info array, supplying default implementations for attribute
541
 
     * types.
542
 
     */
543
 
    public function __construct() {
544
 
        // pseudo-types, must be instantiated via shorthand
545
 
        $this->info['Enum']    = new HTMLPurifier_AttrDef_Enum();
546
 
        $this->info['Bool']    = new HTMLPurifier_AttrDef_HTML_Bool();
547
 
        
548
 
        $this->info['CDATA']    = new HTMLPurifier_AttrDef_Text();
549
 
        $this->info['ID']       = new HTMLPurifier_AttrDef_HTML_ID();
550
 
        $this->info['Length']   = new HTMLPurifier_AttrDef_HTML_Length();
551
 
        $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
552
 
        $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
553
 
        $this->info['Pixels']   = new HTMLPurifier_AttrDef_HTML_Pixels();
554
 
        $this->info['Text']     = new HTMLPurifier_AttrDef_Text();
555
 
        $this->info['URI']      = new HTMLPurifier_AttrDef_URI();
556
 
        $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
557
 
        $this->info['Color']    = new HTMLPurifier_AttrDef_HTML_Color();
558
 
        
559
 
        // unimplemented aliases
560
 
        $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
561
 
        $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text();
562
 
        $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text();
563
 
        $this->info['Character'] = new HTMLPurifier_AttrDef_Text();
564
 
        
565
 
        // number is really a positive integer (one or more digits)
566
 
        // FIXME: ^^ not always, see start and value of list items
567
 
        $this->info['Number']   = new HTMLPurifier_AttrDef_Integer(false, false, true);
568
 
    }
569
 
    
570
 
    /**
571
 
     * Retrieves a type
572
 
     * @param $type String type name
573
 
     * @return Object AttrDef for type
574
 
     */
575
 
    public function get($type) {
576
 
        
577
 
        // determine if there is any extra info tacked on
578
 
        if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2);
579
 
        else $string = '';
580
 
        
581
 
        if (!isset($this->info[$type])) {
582
 
            trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR);
583
 
            return;
584
 
        }
585
 
        
586
 
        return $this->info[$type]->make($string);
587
 
        
588
 
    }
589
 
    
590
 
    /**
591
 
     * Sets a new implementation for a type
592
 
     * @param $type String type name
593
 
     * @param $impl Object AttrDef for type
594
 
     */
595
 
    public function set($type, $impl) {
596
 
        $this->info[$type] = $impl;
597
 
    }
598
 
}
599
 
 
600
 
 
601
 
 
602
 
 
603
 
 
604
 
/**
605
 
 * Validates the attributes of a token. Doesn't manage required attributes
606
 
 * very well. The only reason we factored this out was because RemoveForeignElements
607
 
 * also needed it besides ValidateAttributes.
608
 
 */
609
 
class HTMLPurifier_AttrValidator
610
 
{
611
 
    
612
 
    /**
613
 
     * Validates the attributes of a token, returning a modified token
614
 
     * that has valid tokens
615
 
     * @param $token Reference to token to validate. We require a reference
616
 
     *     because the operation this class performs on the token are
617
 
     *     not atomic, so the context CurrentToken to be updated
618
 
     *     throughout
619
 
     * @param $config Instance of HTMLPurifier_Config
620
 
     * @param $context Instance of HTMLPurifier_Context
621
 
     */
622
 
    public function validateToken(&$token, &$config, $context) {
623
 
            
624
 
        $definition = $config->getHTMLDefinition();
625
 
        $e =& $context->get('ErrorCollector', true);
626
 
        
627
 
        // initialize IDAccumulator if necessary
628
 
        $ok =& $context->get('IDAccumulator', true);
629
 
        if (!$ok) {
630
 
            $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
631
 
            $context->register('IDAccumulator', $id_accumulator);
632
 
        }
633
 
        
634
 
        // initialize CurrentToken if necessary
635
 
        $current_token =& $context->get('CurrentToken', true);
636
 
        if (!$current_token) $context->register('CurrentToken', $token);
637
 
        
638
 
        if (
639
 
            !$token instanceof HTMLPurifier_Token_Start &&
640
 
            !$token instanceof HTMLPurifier_Token_Empty
641
 
        ) return $token;
642
 
        
643
 
        // create alias to global definition array, see also $defs
644
 
        // DEFINITION CALL
645
 
        $d_defs = $definition->info_global_attr;
646
 
        
647
 
        // don't update token until the very end, to ensure an atomic update
648
 
        $attr = $token->attr;
649
 
        
650
 
        // do global transformations (pre)
651
 
        // nothing currently utilizes this
652
 
        foreach ($definition->info_attr_transform_pre as $transform) {
653
 
            $attr = $transform->transform($o = $attr, $config, $context);
654
 
            if ($e) {
655
 
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
656
 
            }
657
 
        }
658
 
        
659
 
        // do local transformations only applicable to this element (pre)
660
 
        // ex. <p align="right"> to <p style="text-align:right;">
661
 
        foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
662
 
            $attr = $transform->transform($o = $attr, $config, $context);
663
 
            if ($e) {
664
 
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
665
 
            }
666
 
        }
667
 
        
668
 
        // create alias to this element's attribute definition array, see
669
 
        // also $d_defs (global attribute definition array)
670
 
        // DEFINITION CALL
671
 
        $defs = $definition->info[$token->name]->attr;
672
 
        
673
 
        $attr_key = false;
674
 
        $context->register('CurrentAttr', $attr_key);
675
 
        
676
 
        // iterate through all the attribute keypairs
677
 
        // Watch out for name collisions: $key has previously been used
678
 
        foreach ($attr as $attr_key => $value) {
679
 
            
680
 
            // call the definition
681
 
            if ( isset($defs[$attr_key]) ) {
682
 
                // there is a local definition defined
683
 
                if ($defs[$attr_key] === false) {
684
 
                    // We've explicitly been told not to allow this element.
685
 
                    // This is usually when there's a global definition
686
 
                    // that must be overridden.
687
 
                    // Theoretically speaking, we could have a
688
 
                    // AttrDef_DenyAll, but this is faster!
689
 
                    $result = false;
690
 
                } else {
691
 
                    // validate according to the element's definition
692
 
                    $result = $defs[$attr_key]->validate(
693
 
                                    $value, $config, $context
694
 
                               );
695
 
                }
696
 
            } elseif ( isset($d_defs[$attr_key]) ) {
697
 
                // there is a global definition defined, validate according
698
 
                // to the global definition
699
 
                $result = $d_defs[$attr_key]->validate(
700
 
                                $value, $config, $context
701
 
                           );
702
 
            } else {
703
 
                // system never heard of the attribute? DELETE!
704
 
                $result = false;
705
 
            }
706
 
            
707
 
            // put the results into effect
708
 
            if ($result === false || $result === null) {
709
 
                // this is a generic error message that should replaced
710
 
                // with more specific ones when possible
711
 
                if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed');
712
 
                
713
 
                // remove the attribute
714
 
                unset($attr[$attr_key]);
715
 
            } elseif (is_string($result)) {
716
 
                // generally, if a substitution is happening, there
717
 
                // was some sort of implicit correction going on. We'll
718
 
                // delegate it to the attribute classes to say exactly what.
719
 
                
720
 
                // simple substitution
721
 
                $attr[$attr_key] = $result;
722
 
            } else {
723
 
                // nothing happens
724
 
            }
725
 
            
726
 
            // we'd also want slightly more complicated substitution
727
 
            // involving an array as the return value,
728
 
            // although we're not sure how colliding attributes would
729
 
            // resolve (certain ones would be completely overriden,
730
 
            // others would prepend themselves).
731
 
        }
732
 
        
733
 
        $context->destroy('CurrentAttr');
734
 
        
735
 
        // post transforms
736
 
        
737
 
        // global (error reporting untested)
738
 
        foreach ($definition->info_attr_transform_post as $transform) {
739
 
            $attr = $transform->transform($o = $attr, $config, $context);
740
 
            if ($e) {
741
 
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
742
 
            }
743
 
        }
744
 
        
745
 
        // local (error reporting untested)
746
 
        foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
747
 
            $attr = $transform->transform($o = $attr, $config, $context);
748
 
            if ($e) {
749
 
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
750
 
            }
751
 
        }
752
 
        
753
 
        $token->attr = $attr;
754
 
        
755
 
        // destroy CurrentToken if we made it ourselves
756
 
        if (!$current_token) $context->destroy('CurrentToken');
757
 
        
758
 
    }
759
 
    
760
 
    
761
 
}
762
 
 
763
 
 
764
 
 
765
 
 
766
 
// constants are slow, so we use as few as possible
767
 
if (!defined('HTMLPURIFIER_PREFIX')) {
768
 
    define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone');
769
 
    set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path());
770
 
}
771
 
 
772
 
// accomodations for versions earlier than 5.0.2
773
 
// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net>
774
 
if (!defined('PHP_EOL')) {
775
 
    switch (strtoupper(substr(PHP_OS, 0, 3))) {
776
 
        case 'WIN':
777
 
            define('PHP_EOL', "\r\n");
778
 
            break;
779
 
        case 'DAR':
780
 
            define('PHP_EOL', "\r");
781
 
            break;
782
 
        default:
783
 
            define('PHP_EOL', "\n");
784
 
    }
785
 
}
786
 
 
787
 
/**
788
 
 * Bootstrap class that contains meta-functionality for HTML Purifier such as
789
 
 * the autoload function.
790
 
 *
791
 
 * @note
792
 
 *      This class may be used without any other files from HTML Purifier.
793
 
 */
794
 
class HTMLPurifier_Bootstrap
795
 
{
796
 
    
797
 
    /**
798
 
     * Autoload function for HTML Purifier
799
 
     * @param $class Class to load
800
 
     */
801
 
    public static function autoload($class) {
802
 
        $file = HTMLPurifier_Bootstrap::getPath($class);
803
 
        if (!$file) return false;
804
 
        require HTMLPURIFIER_PREFIX . '/' . $file;
805
 
        return true;
806
 
    }
807
 
    
808
 
    /**
809
 
     * Returns the path for a specific class.
810
 
     */
811
 
    public static function getPath($class) {
812
 
        if (strncmp('HTMLPurifier', $class, 12) !== 0) return false;
813
 
        // Custom implementations
814
 
        if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) {
815
 
            $code = str_replace('_', '-', substr($class, 22));
816
 
            $file = 'HTMLPurifier/Language/classes/' . $code . '.php';
817
 
        } else {
818
 
            $file = str_replace('_', '/', $class) . '.php';
819
 
        }
820
 
        if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false;
821
 
        return $file;
822
 
    }
823
 
    
824
 
    /**
825
 
     * "Pre-registers" our autoloader on the SPL stack.
826
 
     */
827
 
    public static function registerAutoload() {
828
 
        $autoload = array('HTMLPurifier_Bootstrap', 'autoload');
829
 
        if ( ($funcs = spl_autoload_functions()) === false ) {
830
 
            spl_autoload_register($autoload);
831
 
        } elseif (function_exists('spl_autoload_unregister')) {
832
 
            $compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
833
 
                      version_compare(PHP_VERSION, '5.1.0', '>=');
834
 
            foreach ($funcs as $func) {
835
 
                if (is_array($func)) {
836
 
                    // :TRICKY: There are some compatibility issues and some
837
 
                    // places where we need to error out
838
 
                    $reflector = new ReflectionMethod($func[0], $func[1]);
839
 
                    if (!$reflector->isStatic()) {
840
 
                        throw new Exception('
841
 
                            HTML Purifier autoloader registrar is not compatible
842
 
                            with non-static object methods due to PHP Bug #44144;
843
 
                            Please do not use HTMLPurifier.autoload.php (or any
844
 
                            file that includes this file); instead, place the code:
845
 
                            spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\'))
846
 
                            after your own autoloaders.
847
 
                        ');
848
 
                    }
849
 
                    // Suprisingly, spl_autoload_register supports the
850
 
                    // Class::staticMethod callback format, although call_user_func doesn't
851
 
                    if ($compat) $func = implode('::', $func);
852
 
                }
853
 
                spl_autoload_unregister($func);
854
 
            }
855
 
            spl_autoload_register($autoload);
856
 
            foreach ($funcs as $func) spl_autoload_register($func);
857
 
        }
858
 
    }
859
 
    
860
 
}
861
 
 
862
 
 
863
 
 
864
 
/**
865
 
 * Super-class for definition datatype objects, implements serialization
866
 
 * functions for the class.
867
 
 */
868
 
abstract class HTMLPurifier_Definition
869
 
{
870
 
    
871
 
    /**
872
 
     * Has setup() been called yet?
873
 
     */
874
 
    public $setup = false;
875
 
    
876
 
    /**
877
 
     * What type of definition is it?
878
 
     */
879
 
    public $type;
880
 
    
881
 
    /**
882
 
     * Sets up the definition object into the final form, something
883
 
     * not done by the constructor
884
 
     * @param $config HTMLPurifier_Config instance
885
 
     */
886
 
    abstract protected function doSetup($config);
887
 
    
888
 
    /**
889
 
     * Setup function that aborts if already setup
890
 
     * @param $config HTMLPurifier_Config instance
891
 
     */
892
 
    public function setup($config) {
893
 
        if ($this->setup) return;
894
 
        $this->setup = true;
895
 
        $this->doSetup($config);
896
 
    }
897
 
    
898
 
}
899
 
 
900
 
 
901
 
 
902
 
 
903
 
/**
904
 
 * Defines allowed CSS attributes and what their values are.
905
 
 * @see HTMLPurifier_HTMLDefinition
906
 
 */
907
 
class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
908
 
{
909
 
    
910
 
    public $type = 'CSS';
911
 
    
912
 
    /**
913
 
     * Assoc array of attribute name to definition object.
914
 
     */
915
 
    public $info = array();
916
 
    
917
 
    /**
918
 
     * Constructs the info array.  The meat of this class.
919
 
     */
920
 
    protected function doSetup($config) {
921
 
        
922
 
        $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum(
923
 
            array('left', 'right', 'center', 'justify'), false);
924
 
        
925
 
        $border_style =
926
 
        $this->info['border-bottom-style'] = 
927
 
        $this->info['border-right-style'] = 
928
 
        $this->info['border-left-style'] = 
929
 
        $this->info['border-top-style'] =  new HTMLPurifier_AttrDef_Enum(
930
 
            array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double',
931
 
            'groove', 'ridge', 'inset', 'outset'), false);
932
 
        
933
 
        $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style);
934
 
        
935
 
        $this->info['clear'] = new HTMLPurifier_AttrDef_Enum(
936
 
            array('none', 'left', 'right', 'both'), false);
937
 
        $this->info['float'] = new HTMLPurifier_AttrDef_Enum(
938
 
            array('none', 'left', 'right'), false);
939
 
        $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
940
 
            array('normal', 'italic', 'oblique'), false);
941
 
        $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum(
942
 
            array('normal', 'small-caps'), false);
943
 
        
944
 
        $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite(
945
 
            array(
946
 
                new HTMLPurifier_AttrDef_Enum(array('none')),
947
 
                new HTMLPurifier_AttrDef_CSS_URI()
948
 
            )
949
 
        );
950
 
        
951
 
        $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum(
952
 
            array('inside', 'outside'), false);
953
 
        $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum(
954
 
            array('disc', 'circle', 'square', 'decimal', 'lower-roman',
955
 
            'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false);
956
 
        $this->info['list-style-image'] = $uri_or_none;
957
 
        
958
 
        $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config);
959
 
        
960
 
        $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum(
961
 
            array('capitalize', 'uppercase', 'lowercase', 'none'), false);
962
 
        $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color();
963
 
        
964
 
        $this->info['background-image'] = $uri_or_none;
965
 
        $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum(
966
 
            array('repeat', 'repeat-x', 'repeat-y', 'no-repeat')
967
 
        );
968
 
        $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
969
 
            array('scroll', 'fixed')
970
 
        );
971
 
        $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
972
 
        
973
 
        $border_color = 
974
 
        $this->info['border-top-color'] = 
975
 
        $this->info['border-bottom-color'] = 
976
 
        $this->info['border-left-color'] = 
977
 
        $this->info['border-right-color'] = 
978
 
        $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
979
 
            new HTMLPurifier_AttrDef_Enum(array('transparent')),
980
 
            new HTMLPurifier_AttrDef_CSS_Color()
981
 
        ));
982
 
        
983
 
        $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config);
984
 
        
985
 
        $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color);
986
 
        
987
 
        $border_width = 
988
 
        $this->info['border-top-width'] = 
989
 
        $this->info['border-bottom-width'] = 
990
 
        $this->info['border-left-width'] = 
991
 
        $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
992
 
            new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')),
993
 
            new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative
994
 
        ));
995
 
        
996
 
        $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width);
997
 
        
998
 
        $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
999
 
            new HTMLPurifier_AttrDef_Enum(array('normal')),
1000
 
            new HTMLPurifier_AttrDef_CSS_Length()
1001
 
        ));
1002
 
        
1003
 
        $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1004
 
            new HTMLPurifier_AttrDef_Enum(array('normal')),
1005
 
            new HTMLPurifier_AttrDef_CSS_Length()
1006
 
        ));
1007
 
        
1008
 
        $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1009
 
            new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small',
1010
 
                'small', 'medium', 'large', 'x-large', 'xx-large',
1011
 
                'larger', 'smaller')),
1012
 
            new HTMLPurifier_AttrDef_CSS_Percentage(),
1013
 
            new HTMLPurifier_AttrDef_CSS_Length()
1014
 
        ));
1015
 
        
1016
 
        $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1017
 
            new HTMLPurifier_AttrDef_Enum(array('normal')),
1018
 
            new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives
1019
 
            new HTMLPurifier_AttrDef_CSS_Length('0'),
1020
 
            new HTMLPurifier_AttrDef_CSS_Percentage(true)
1021
 
        ));
1022
 
        
1023
 
        $margin =
1024
 
        $this->info['margin-top'] = 
1025
 
        $this->info['margin-bottom'] = 
1026
 
        $this->info['margin-left'] = 
1027
 
        $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1028
 
            new HTMLPurifier_AttrDef_CSS_Length(),
1029
 
            new HTMLPurifier_AttrDef_CSS_Percentage(),
1030
 
            new HTMLPurifier_AttrDef_Enum(array('auto'))
1031
 
        ));
1032
 
        
1033
 
        $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin);
1034
 
        
1035
 
        // non-negative
1036
 
        $padding =
1037
 
        $this->info['padding-top'] = 
1038
 
        $this->info['padding-bottom'] = 
1039
 
        $this->info['padding-left'] = 
1040
 
        $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1041
 
            new HTMLPurifier_AttrDef_CSS_Length('0'),
1042
 
            new HTMLPurifier_AttrDef_CSS_Percentage(true)
1043
 
        ));
1044
 
        
1045
 
        $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding);
1046
 
        
1047
 
        $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1048
 
            new HTMLPurifier_AttrDef_CSS_Length(),
1049
 
            new HTMLPurifier_AttrDef_CSS_Percentage()
1050
 
        ));
1051
 
        
1052
 
        $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array(
1053
 
            new HTMLPurifier_AttrDef_CSS_Length('0'),
1054
 
            new HTMLPurifier_AttrDef_CSS_Percentage(true),
1055
 
            new HTMLPurifier_AttrDef_Enum(array('auto'))
1056
 
        ));
1057
 
        $max = $config->get('CSS', 'MaxImgLength');
1058
 
        
1059
 
        $this->info['width'] =
1060
 
        $this->info['height'] =
1061
 
            $max === null ?
1062
 
            $trusted_wh : 
1063
 
            new HTMLPurifier_AttrDef_Switch('img',
1064
 
                // For img tags:
1065
 
                new HTMLPurifier_AttrDef_CSS_Composite(array(
1066
 
                    new HTMLPurifier_AttrDef_CSS_Length('0', $max),
1067
 
                    new HTMLPurifier_AttrDef_Enum(array('auto'))
1068
 
                )),
1069
 
                // For everyone else:
1070
 
                $trusted_wh
1071
 
            );
1072
 
        
1073
 
        $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration();
1074
 
        
1075
 
        $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily();
1076
 
        
1077
 
        // this could use specialized code
1078
 
        $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum(
1079
 
            array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300',
1080
 
            '400', '500', '600', '700', '800', '900'), false);
1081
 
        
1082
 
        // MUST be called after other font properties, as it references
1083
 
        // a CSSDefinition object
1084
 
        $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config);
1085
 
        
1086
 
        // same here
1087
 
        $this->info['border'] =
1088
 
        $this->info['border-bottom'] = 
1089
 
        $this->info['border-top'] = 
1090
 
        $this->info['border-left'] = 
1091
 
        $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config);
1092
 
        
1093
 
        $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array(
1094
 
            'collapse', 'separate'));
1095
 
        
1096
 
        $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array(
1097
 
            'top', 'bottom'));
1098
 
        
1099
 
        $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array(
1100
 
            'auto', 'fixed'));
1101
 
        
1102
 
        $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
1103
 
            new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super',
1104
 
                'top', 'text-top', 'middle', 'bottom', 'text-bottom')),
1105
 
            new HTMLPurifier_AttrDef_CSS_Length(),
1106
 
            new HTMLPurifier_AttrDef_CSS_Percentage()
1107
 
        ));
1108
 
        
1109
 
        $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2);
1110
 
        
1111
 
        // partial support
1112
 
        $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap'));
1113
 
        
1114
 
        if ($config->get('CSS', 'Proprietary')) {
1115
 
            $this->doSetupProprietary($config);
1116
 
        }
1117
 
        
1118
 
        if ($config->get('CSS', 'AllowTricky')) {
1119
 
            $this->doSetupTricky($config);
1120
 
        }
1121
 
        
1122
 
        $allow_important = $config->get('CSS', 'AllowImportant');
1123
 
        // wrap all attr-defs with decorator that handles !important
1124
 
        foreach ($this->info as $k => $v) {
1125
 
            $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important);
1126
 
        }
1127
 
        
1128
 
        $this->setupConfigStuff($config);
1129
 
    }
1130
 
    
1131
 
    protected function doSetupProprietary($config) {
1132
 
        // Internet Explorer only scrollbar colors
1133
 
        $this->info['scrollbar-arrow-color']        = new HTMLPurifier_AttrDef_CSS_Color();
1134
 
        $this->info['scrollbar-base-color']         = new HTMLPurifier_AttrDef_CSS_Color();
1135
 
        $this->info['scrollbar-darkshadow-color']   = new HTMLPurifier_AttrDef_CSS_Color();
1136
 
        $this->info['scrollbar-face-color']         = new HTMLPurifier_AttrDef_CSS_Color();
1137
 
        $this->info['scrollbar-highlight-color']    = new HTMLPurifier_AttrDef_CSS_Color();
1138
 
        $this->info['scrollbar-shadow-color']       = new HTMLPurifier_AttrDef_CSS_Color();
1139
 
        
1140
 
        // technically not proprietary, but CSS3, and no one supports it
1141
 
        $this->info['opacity']          = new HTMLPurifier_AttrDef_CSS_AlphaValue();
1142
 
        $this->info['-moz-opacity']     = new HTMLPurifier_AttrDef_CSS_AlphaValue();
1143
 
        $this->info['-khtml-opacity']   = new HTMLPurifier_AttrDef_CSS_AlphaValue();
1144
 
        
1145
 
        // only opacity, for now
1146
 
        $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter();
1147
 
        
1148
 
    }
1149
 
    
1150
 
    protected function doSetupTricky($config) {
1151
 
        $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array(
1152
 
            'inline', 'block', 'list-item', 'run-in', 'compact',
1153
 
            'marker', 'table', 'inline-table', 'table-row-group',
1154
 
            'table-header-group', 'table-footer-group', 'table-row',
1155
 
            'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none'
1156
 
        ));
1157
 
        $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array(
1158
 
            'visible', 'hidden', 'collapse'
1159
 
        ));
1160
 
    }
1161
 
    
1162
 
    
1163
 
    /**
1164
 
     * Performs extra config-based processing. Based off of
1165
 
     * HTMLPurifier_HTMLDefinition.
1166
 
     * @todo Refactor duplicate elements into common class (probably using
1167
 
     *       composition, not inheritance).
1168
 
     */
1169
 
    protected function setupConfigStuff($config) {
1170
 
        
1171
 
        // setup allowed elements
1172
 
        $support = "(for information on implementing this, see the ".
1173
 
                   "support forums) ";
1174
 
        $allowed_attributes = $config->get('CSS', 'AllowedProperties');
1175
 
        if ($allowed_attributes !== null) {
1176
 
            foreach ($this->info as $name => $d) {
1177
 
                if(!isset($allowed_attributes[$name])) unset($this->info[$name]);
1178
 
                unset($allowed_attributes[$name]);
1179
 
            }
1180
 
            // emit errors
1181
 
            foreach ($allowed_attributes as $name => $d) {
1182
 
                // :TODO: Is this htmlspecialchars() call really necessary?
1183
 
                $name = htmlspecialchars($name);
1184
 
                trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING);
1185
 
            }
1186
 
        }
1187
 
        
1188
 
    }
1189
 
}
1190
 
 
1191
 
 
1192
 
 
1193
 
 
1194
 
/**
1195
 
 * Defines allowed child nodes and validates tokens against it.
1196
 
 */
1197
 
abstract class HTMLPurifier_ChildDef
1198
 
{
1199
 
    /**
1200
 
     * Type of child definition, usually right-most part of class name lowercase.
1201
 
     * Used occasionally in terms of context.
1202
 
     */
1203
 
    public $type;
1204
 
    
1205
 
    /**
1206
 
     * Bool that indicates whether or not an empty array of children is okay
1207
 
     * 
1208
 
     * This is necessary for redundant checking when changes affecting
1209
 
     * a child node may cause a parent node to now be disallowed.
1210
 
     */
1211
 
    public $allow_empty;
1212
 
    
1213
 
    /**
1214
 
     * Lookup array of all elements that this definition could possibly allow
1215
 
     */
1216
 
    public $elements = array();
1217
 
    
1218
 
    /**
1219
 
     * Get lookup of tag names that should not close this element automatically.
1220
 
     * All other elements will do so.
1221
 
     */
1222
 
    public function getNonAutoCloseElements($config) {
1223
 
        return $this->elements;
1224
 
    }
1225
 
    
1226
 
    /**
1227
 
     * Validates nodes according to definition and returns modification.
1228
 
     * 
1229
 
     * @param $tokens_of_children Array of HTMLPurifier_Token
1230
 
     * @param $config HTMLPurifier_Config object
1231
 
     * @param $context HTMLPurifier_Context object
1232
 
     * @return bool true to leave nodes as is
1233
 
     * @return bool false to remove parent node
1234
 
     * @return array of replacement child tokens
1235
 
     */
1236
 
    abstract public function validateChildren($tokens_of_children, $config, $context);
1237
 
}
1238
 
 
1239
 
 
1240
 
 
1241
 
 
1242
 
 
1243
 
/**
1244
 
 * Configuration object that triggers customizable behavior.
1245
 
 *
1246
 
 * @warning This class is strongly defined: that means that the class
1247
 
 *          will fail if an undefined directive is retrieved or set.
1248
 
 * 
1249
 
 * @note Many classes that could (although many times don't) use the
1250
 
 *       configuration object make it a mandatory parameter.  This is
1251
 
 *       because a configuration object should always be forwarded,
1252
 
 *       otherwise, you run the risk of missing a parameter and then
1253
 
 *       being stumped when a configuration directive doesn't work.
1254
 
 * 
1255
 
 * @todo Reconsider some of the public member variables
1256
 
 */
1257
 
class HTMLPurifier_Config
1258
 
{
1259
 
    
1260
 
    /**
1261
 
     * HTML Purifier's version
1262
 
     */
1263
 
    public $version = '3.2.0';
1264
 
    
1265
 
    /**
1266
 
     * Bool indicator whether or not to automatically finalize 
1267
 
     * the object if a read operation is done
1268
 
     */
1269
 
    public $autoFinalize = true;
1270
 
    
1271
 
    // protected member variables
1272
 
    
1273
 
    /**
1274
 
     * Namespace indexed array of serials for specific namespaces (see
1275
 
     * getSerial() for more info).
1276
 
     */
1277
 
    protected $serials = array();
1278
 
    
1279
 
    /**
1280
 
     * Serial for entire configuration object
1281
 
     */
1282
 
    protected $serial;
1283
 
    
1284
 
    /**
1285
 
     * Two-level associative array of configuration directives
1286
 
     */
1287
 
    protected $conf;
1288
 
    
1289
 
    /**
1290
 
     * Parser for variables
1291
 
     */
1292
 
    protected $parser;
1293
 
    
1294
 
    /**
1295
 
     * Reference HTMLPurifier_ConfigSchema for value checking
1296
 
     * @note This is public for introspective purposes. Please don't
1297
 
     *       abuse!
1298
 
     */
1299
 
    public $def;
1300
 
    
1301
 
    /**
1302
 
     * Indexed array of definitions
1303
 
     */
1304
 
    protected $definitions;
1305
 
    
1306
 
    /**
1307
 
     * Bool indicator whether or not config is finalized
1308
 
     */
1309
 
    protected $finalized = false;
1310
 
    
1311
 
    /**
1312
 
     * @param $definition HTMLPurifier_ConfigSchema that defines what directives
1313
 
     *                    are allowed.
1314
 
     */
1315
 
    public function __construct($definition) {
1316
 
        $this->conf = $definition->defaults; // set up, copy in defaults
1317
 
        $this->def  = $definition; // keep a copy around for checking
1318
 
        $this->parser = new HTMLPurifier_VarParser_Flexible();
1319
 
    }
1320
 
    
1321
 
    /**
1322
 
     * Convenience constructor that creates a config object based on a mixed var
1323
 
     * @param mixed $config Variable that defines the state of the config
1324
 
     *                      object. Can be: a HTMLPurifier_Config() object,
1325
 
     *                      an array of directives based on loadArray(),
1326
 
     *                      or a string filename of an ini file.
1327
 
     * @param HTMLPurifier_ConfigSchema Schema object
1328
 
     * @return Configured HTMLPurifier_Config object
1329
 
     */
1330
 
    public static function create($config, $schema = null) {
1331
 
        if ($config instanceof HTMLPurifier_Config) {
1332
 
            // pass-through
1333
 
            return $config;
1334
 
        }
1335
 
        if (!$schema) {
1336
 
            $ret = HTMLPurifier_Config::createDefault();
1337
 
        } else {
1338
 
            $ret = new HTMLPurifier_Config($schema);
1339
 
        }
1340
 
        if (is_string($config)) $ret->loadIni($config);
1341
 
        elseif (is_array($config)) $ret->loadArray($config);
1342
 
        return $ret;
1343
 
    }
1344
 
    
1345
 
    /**
1346
 
     * Convenience constructor that creates a default configuration object.
1347
 
     * @return Default HTMLPurifier_Config object.
1348
 
     */
1349
 
    public static function createDefault() {
1350
 
        $definition = HTMLPurifier_ConfigSchema::instance();
1351
 
        $config = new HTMLPurifier_Config($definition);
1352
 
        return $config;
1353
 
    }
1354
 
    
1355
 
    /**
1356
 
     * Retreives a value from the configuration.
1357
 
     * @param $namespace String namespace
1358
 
     * @param $key String key
1359
 
     */
1360
 
    public function get($namespace, $key) {
1361
 
        if (!$this->finalized && $this->autoFinalize) $this->finalize();
1362
 
        if (!isset($this->def->info[$namespace][$key])) {
1363
 
            // can't add % due to SimpleTest bug
1364
 
            trigger_error('Cannot retrieve value of undefined directive ' . htmlspecialchars("$namespace.$key"),
1365
 
                E_USER_WARNING);
1366
 
            return;
1367
 
        }
1368
 
        if (isset($this->def->info[$namespace][$key]->isAlias)) {
1369
 
            $d = $this->def->info[$namespace][$key];
1370
 
            trigger_error('Cannot get value from aliased directive, use real name ' . $d->namespace . '.' . $d->name,
1371
 
                E_USER_ERROR);
1372
 
            return;
1373
 
        }
1374
 
        return $this->conf[$namespace][$key];
1375
 
    }
1376
 
    
1377
 
    /**
1378
 
     * Retreives an array of directives to values from a given namespace
1379
 
     * @param $namespace String namespace
1380
 
     */
1381
 
    public function getBatch($namespace) {
1382
 
        if (!$this->finalized && $this->autoFinalize) $this->finalize();
1383
 
        if (!isset($this->def->info[$namespace])) {
1384
 
            trigger_error('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace),
1385
 
                E_USER_WARNING);
1386
 
            return;
1387
 
        }
1388
 
        return $this->conf[$namespace];
1389
 
    }
1390
 
    
1391
 
    /**
1392
 
     * Returns a md5 signature of a segment of the configuration object
1393
 
     * that uniquely identifies that particular configuration
1394
 
     * @note Revision is handled specially and is removed from the batch
1395
 
     *       before processing!
1396
 
     * @param $namespace Namespace to get serial for
1397
 
     */
1398
 
    public function getBatchSerial($namespace) {
1399
 
        if (empty($this->serials[$namespace])) {
1400
 
            $batch = $this->getBatch($namespace);
1401
 
            unset($batch['DefinitionRev']);
1402
 
            $this->serials[$namespace] = md5(serialize($batch));
1403
 
        }
1404
 
        return $this->serials[$namespace];
1405
 
    }
1406
 
    
1407
 
    /**
1408
 
     * Returns a md5 signature for the entire configuration object
1409
 
     * that uniquely identifies that particular configuration
1410
 
     */
1411
 
    public function getSerial() {
1412
 
        if (empty($this->serial)) {
1413
 
            $this->serial = md5(serialize($this->getAll()));
1414
 
        }
1415
 
        return $this->serial;
1416
 
    }
1417
 
    
1418
 
    /**
1419
 
     * Retrieves all directives, organized by namespace
1420
 
     */
1421
 
    public function getAll() {
1422
 
        if (!$this->finalized && $this->autoFinalize) $this->finalize();
1423
 
        return $this->conf;
1424
 
    }
1425
 
    
1426
 
    /**
1427
 
     * Sets a value to configuration.
1428
 
     * @param $namespace String namespace
1429
 
     * @param $key String key
1430
 
     * @param $value Mixed value
1431
 
     */
1432
 
    public function set($namespace, $key, $value, $from_alias = false) {
1433
 
        if ($this->isFinalized('Cannot set directive after finalization')) return;
1434
 
        if (!isset($this->def->info[$namespace][$key])) {
1435
 
            trigger_error('Cannot set undefined directive ' . htmlspecialchars("$namespace.$key") . ' to value',
1436
 
                E_USER_WARNING);
1437
 
            return;
1438
 
        }
1439
 
        $def = $this->def->info[$namespace][$key];
1440
 
        
1441
 
        if (isset($def->isAlias)) {
1442
 
            if ($from_alias) {
1443
 
                trigger_error('Double-aliases not allowed, please fix '.
1444
 
                    'ConfigSchema bug with' . "$namespace.$key", E_USER_ERROR);
1445
 
                return;
1446
 
            }
1447
 
            $this->set($new_ns  = $def->namespace,
1448
 
                       $new_dir = $def->name,
1449
 
                       $value, true);
1450
 
            trigger_error("$namespace.$key is an alias, preferred directive name is $new_ns.$new_dir", E_USER_NOTICE);
1451
 
            return;
1452
 
        }
1453
 
        
1454
 
        // Raw type might be negative when using the fully optimized form
1455
 
        // of stdclass, which indicates allow_null == true
1456
 
        $rtype = is_int($def) ? $def : $def->type;
1457
 
        if ($rtype < 0) {
1458
 
            $type = -$rtype;
1459
 
            $allow_null = true;
1460
 
        } else {
1461
 
            $type = $rtype;
1462
 
            $allow_null = isset($def->allow_null);
1463
 
        }
1464
 
        
1465
 
        try {
1466
 
            $value = $this->parser->parse($value, $type, $allow_null);
1467
 
        } catch (HTMLPurifier_VarParserException $e) {
1468
 
            trigger_error('Value for ' . "$namespace.$key" . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING);
1469
 
            return;
1470
 
        }
1471
 
        if (is_string($value) && is_object($def)) {
1472
 
            // resolve value alias if defined
1473
 
            if (isset($def->aliases[$value])) {
1474
 
                $value = $def->aliases[$value];
1475
 
            }
1476
 
            // check to see if the value is allowed
1477
 
            if (isset($def->allowed) && !isset($def->allowed[$value])) {
1478
 
                trigger_error('Value not supported, valid values are: ' .
1479
 
                    $this->_listify($def->allowed), E_USER_WARNING);
1480
 
                return;
1481
 
            }
1482
 
        }
1483
 
        $this->conf[$namespace][$key] = $value;
1484
 
        
1485
 
        // reset definitions if the directives they depend on changed
1486
 
        // this is a very costly process, so it's discouraged 
1487
 
        // with finalization
1488
 
        if ($namespace == 'HTML' || $namespace == 'CSS') {
1489
 
            $this->definitions[$namespace] = null;
1490
 
        }
1491
 
        
1492
 
        $this->serials[$namespace] = false;
1493
 
    }
1494
 
    
1495
 
    /**
1496
 
     * Convenience function for error reporting
1497
 
     */
1498
 
    private function _listify($lookup) {
1499
 
        $list = array();
1500
 
        foreach ($lookup as $name => $b) $list[] = $name;
1501
 
        return implode(', ', $list);
1502
 
    }
1503
 
    
1504
 
    /**
1505
 
     * Retrieves object reference to the HTML definition.
1506
 
     * @param $raw Return a copy that has not been setup yet. Must be
1507
 
     *             called before it's been setup, otherwise won't work.
1508
 
     */
1509
 
    public function getHTMLDefinition($raw = false) {
1510
 
        return $this->getDefinition('HTML', $raw);
1511
 
    }
1512
 
    
1513
 
    /**
1514
 
     * Retrieves object reference to the CSS definition
1515
 
     * @param $raw Return a copy that has not been setup yet. Must be
1516
 
     *             called before it's been setup, otherwise won't work.
1517
 
     */
1518
 
    public function getCSSDefinition($raw = false) {
1519
 
        return $this->getDefinition('CSS', $raw);
1520
 
    }
1521
 
    
1522
 
    /**
1523
 
     * Retrieves a definition
1524
 
     * @param $type Type of definition: HTML, CSS, etc
1525
 
     * @param $raw  Whether or not definition should be returned raw
1526
 
     */
1527
 
    public function getDefinition($type, $raw = false) {
1528
 
        if (!$this->finalized && $this->autoFinalize) $this->finalize();
1529
 
        $factory = HTMLPurifier_DefinitionCacheFactory::instance();
1530
 
        $cache = $factory->create($type, $this);
1531
 
        if (!$raw) {
1532
 
            // see if we can quickly supply a definition
1533
 
            if (!empty($this->definitions[$type])) {
1534
 
                if (!$this->definitions[$type]->setup) {
1535
 
                    $this->definitions[$type]->setup($this);
1536
 
                    $cache->set($this->definitions[$type], $this);
1537
 
                }
1538
 
                return $this->definitions[$type];
1539
 
            }
1540
 
            // memory check missed, try cache
1541
 
            $this->definitions[$type] = $cache->get($this);
1542
 
            if ($this->definitions[$type]) {
1543
 
                // definition in cache, return it
1544
 
                return $this->definitions[$type];
1545
 
            }
1546
 
        } elseif (
1547
 
            !empty($this->definitions[$type]) &&
1548
 
            !$this->definitions[$type]->setup
1549
 
        ) {
1550
 
            // raw requested, raw in memory, quick return
1551
 
            return $this->definitions[$type];
1552
 
        }
1553
 
        // quick checks failed, let's create the object
1554
 
        if ($type == 'HTML') {
1555
 
            $this->definitions[$type] = new HTMLPurifier_HTMLDefinition();
1556
 
        } elseif ($type == 'CSS') {
1557
 
            $this->definitions[$type] = new HTMLPurifier_CSSDefinition();
1558
 
        } elseif ($type == 'URI') {
1559
 
            $this->definitions[$type] = new HTMLPurifier_URIDefinition();
1560
 
        } else {
1561
 
            throw new HTMLPurifier_Exception("Definition of $type type not supported");
1562
 
        }
1563
 
        // quick abort if raw
1564
 
        if ($raw) {
1565
 
            if (is_null($this->get($type, 'DefinitionID'))) {
1566
 
                // fatally error out if definition ID not set
1567
 
                throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
1568
 
            }
1569
 
            return $this->definitions[$type];
1570
 
        }
1571
 
        // set it up
1572
 
        $this->definitions[$type]->setup($this);
1573
 
        // save in cache
1574
 
        $cache->set($this->definitions[$type], $this);
1575
 
        return $this->definitions[$type];
1576
 
    }
1577
 
    
1578
 
    /**
1579
 
     * Loads configuration values from an array with the following structure:
1580
 
     * Namespace.Directive => Value
1581
 
     * @param $config_array Configuration associative array
1582
 
     */
1583
 
    public function loadArray($config_array) {
1584
 
        if ($this->isFinalized('Cannot load directives after finalization')) return;
1585
 
        foreach ($config_array as $key => $value) {
1586
 
            $key = str_replace('_', '.', $key);
1587
 
            if (strpos($key, '.') !== false) {
1588
 
                // condensed form
1589
 
                list($namespace, $directive) = explode('.', $key);
1590
 
                $this->set($namespace, $directive, $value);
1591
 
            } else {
1592
 
                $namespace = $key;
1593
 
                $namespace_values = $value;
1594
 
                foreach ($namespace_values as $directive => $value) {
1595
 
                    $this->set($namespace, $directive, $value);
1596
 
                }
1597
 
            }
1598
 
        }
1599
 
    }
1600
 
    
1601
 
    /**
1602
 
     * Returns a list of array(namespace, directive) for all directives
1603
 
     * that are allowed in a web-form context as per an allowed
1604
 
     * namespaces/directives list.
1605
 
     * @param $allowed List of allowed namespaces/directives
1606
 
     */
1607
 
    public static function getAllowedDirectivesForForm($allowed, $schema = null) {
1608
 
        if (!$schema) {
1609
 
            $schema = HTMLPurifier_ConfigSchema::instance();
1610
 
        }
1611
 
        if ($allowed !== true) {
1612
 
             if (is_string($allowed)) $allowed = array($allowed);
1613
 
             $allowed_ns = array();
1614
 
             $allowed_directives = array();
1615
 
             $blacklisted_directives = array();
1616
 
             foreach ($allowed as $ns_or_directive) {
1617
 
                 if (strpos($ns_or_directive, '.') !== false) {
1618
 
                     // directive
1619
 
                     if ($ns_or_directive[0] == '-') {
1620
 
                         $blacklisted_directives[substr($ns_or_directive, 1)] = true;
1621
 
                     } else {
1622
 
                         $allowed_directives[$ns_or_directive] = true;
1623
 
                     }
1624
 
                 } else {
1625
 
                     // namespace
1626
 
                     $allowed_ns[$ns_or_directive] = true;
1627
 
                 }
1628
 
             }
1629
 
        }
1630
 
        $ret = array();
1631
 
        foreach ($schema->info as $ns => $keypairs) {
1632
 
            foreach ($keypairs as $directive => $def) {
1633
 
                if ($allowed !== true) {
1634
 
                    if (isset($blacklisted_directives["$ns.$directive"])) continue;
1635
 
                    if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue;
1636
 
                }
1637
 
                if (isset($def->isAlias)) continue;
1638
 
                if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue;
1639
 
                $ret[] = array($ns, $directive);
1640
 
            }
1641
 
        }
1642
 
        return $ret;
1643
 
    }
1644
 
    
1645
 
    /**
1646
 
     * Loads configuration values from $_GET/$_POST that were posted
1647
 
     * via ConfigForm
1648
 
     * @param $array $_GET or $_POST array to import
1649
 
     * @param $index Index/name that the config variables are in
1650
 
     * @param $allowed List of allowed namespaces/directives 
1651
 
     * @param $mq_fix Boolean whether or not to enable magic quotes fix
1652
 
     * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy
1653
 
     */
1654
 
    public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
1655
 
        $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
1656
 
        $config = HTMLPurifier_Config::create($ret, $schema);
1657
 
        return $config;
1658
 
    }
1659
 
    
1660
 
    /**
1661
 
     * Merges in configuration values from $_GET/$_POST to object. NOT STATIC.
1662
 
     * @note Same parameters as loadArrayFromForm
1663
 
     */
1664
 
    public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) {
1665
 
         $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
1666
 
         $this->loadArray($ret);
1667
 
    }
1668
 
    
1669
 
    /**
1670
 
     * Prepares an array from a form into something usable for the more
1671
 
     * strict parts of HTMLPurifier_Config
1672
 
     */
1673
 
    public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
1674
 
        if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
1675
 
        $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
1676
 
        
1677
 
        $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
1678
 
        $ret = array();
1679
 
        foreach ($allowed as $key) {
1680
 
            list($ns, $directive) = $key;
1681
 
            $skey = "$ns.$directive";
1682
 
            if (!empty($array["Null_$skey"])) {
1683
 
                $ret[$ns][$directive] = null;
1684
 
                continue;
1685
 
            }
1686
 
            if (!isset($array[$skey])) continue;
1687
 
            $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
1688
 
            $ret[$ns][$directive] = $value;
1689
 
        }
1690
 
        return $ret;
1691
 
    }
1692
 
    
1693
 
    /**
1694
 
     * Loads configuration values from an ini file
1695
 
     * @param $filename Name of ini file
1696
 
     */
1697
 
    public function loadIni($filename) {
1698
 
        if ($this->isFinalized('Cannot load directives after finalization')) return;
1699
 
        $array = parse_ini_file($filename, true);
1700
 
        $this->loadArray($array);
1701
 
    }
1702
 
    
1703
 
    /**
1704
 
     * Checks whether or not the configuration object is finalized.
1705
 
     * @param $error String error message, or false for no error
1706
 
     */
1707
 
    public function isFinalized($error = false) {
1708
 
        if ($this->finalized && $error) {
1709
 
            trigger_error($error, E_USER_ERROR);
1710
 
        }
1711
 
        return $this->finalized;
1712
 
    }
1713
 
    
1714
 
    /**
1715
 
     * Finalizes configuration only if auto finalize is on and not
1716
 
     * already finalized
1717
 
     */
1718
 
    public function autoFinalize() {
1719
 
        if (!$this->finalized && $this->autoFinalize) $this->finalize();
1720
 
    }
1721
 
    
1722
 
    /**
1723
 
     * Finalizes a configuration object, prohibiting further change
1724
 
     */
1725
 
    public function finalize() {
1726
 
        $this->finalized = true;
1727
 
    }
1728
 
    
1729
 
}
1730
 
 
1731
 
 
1732
 
 
1733
 
 
1734
 
 
1735
 
 
1736
 
/**
1737
 
 * Configuration definition, defines directives and their defaults.
1738
 
 */
1739
 
class HTMLPurifier_ConfigSchema {
1740
 
    
1741
 
    /**
1742
 
     * Defaults of the directives and namespaces.
1743
 
     * @note This shares the exact same structure as HTMLPurifier_Config::$conf
1744
 
     */
1745
 
    public $defaults = array();
1746
 
    
1747
 
    /**
1748
 
     * Definition of the directives. The structure of this is:
1749
 
     * 
1750
 
     *  array(
1751
 
     *      'Namespace' => array(
1752
 
     *          'Directive' => new stdclass(),
1753
 
     *      )
1754
 
     *  )
1755
 
     * 
1756
 
     * The stdclass may have the following properties:
1757
 
     * 
1758
 
     *  - If isAlias isn't set:
1759
 
     *      - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
1760
 
     *      - allow_null: If set, this directive allows null values
1761
 
     *      - aliases: If set, an associative array of value aliases to real values
1762
 
     *      - allowed: If set, a lookup array of allowed (string) values
1763
 
     *  - If isAlias is set:
1764
 
     *      - namespace: Namespace this directive aliases to
1765
 
     *      - name: Directive name this directive aliases to
1766
 
     * 
1767
 
     * In certain degenerate cases, stdclass will actually be an integer. In
1768
 
     * that case, the value is equivalent to an stdclass with the type
1769
 
     * property set to the integer. If the integer is negative, type is
1770
 
     * equal to the absolute value of integer, and allow_null is true.
1771
 
     * 
1772
 
     * This class is friendly with HTMLPurifier_Config. If you need introspection
1773
 
     * about the schema, you're better of using the ConfigSchema_Interchange,
1774
 
     * which uses more memory but has much richer information.
1775
 
     */
1776
 
    public $info = array();
1777
 
    
1778
 
    /**
1779
 
     * Application-wide singleton
1780
 
     */
1781
 
    static protected $singleton;
1782
 
    
1783
 
    /**
1784
 
     * Unserializes the default ConfigSchema.
1785
 
     */
1786
 
    public static function makeFromSerial() {
1787
 
        return unserialize(file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'));
1788
 
    }
1789
 
    
1790
 
    /**
1791
 
     * Retrieves an instance of the application-wide configuration definition.
1792
 
     */
1793
 
    public static function instance($prototype = null) {
1794
 
        if ($prototype !== null) {
1795
 
            HTMLPurifier_ConfigSchema::$singleton = $prototype;
1796
 
        } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
1797
 
            HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
1798
 
        }
1799
 
        return HTMLPurifier_ConfigSchema::$singleton;
1800
 
    }
1801
 
    
1802
 
    /**
1803
 
     * Defines a directive for configuration
1804
 
     * @warning Will fail of directive's namespace is defined.
1805
 
     * @warning This method's signature is slightly different from the legacy
1806
 
     *          define() static method! Beware!
1807
 
     * @param $namespace Namespace the directive is in
1808
 
     * @param $name Key of directive
1809
 
     * @param $default Default value of directive
1810
 
     * @param $type Allowed type of the directive. See
1811
 
     *      HTMLPurifier_DirectiveDef::$type for allowed values
1812
 
     * @param $allow_null Whether or not to allow null values
1813
 
     */
1814
 
    public function add($namespace, $name, $default, $type, $allow_null) {
1815
 
        $obj = new stdclass();
1816
 
        $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
1817
 
        if ($allow_null) $obj->allow_null = true;
1818
 
        $this->info[$namespace][$name] = $obj;
1819
 
        $this->defaults[$namespace][$name] = $default;
1820
 
    }
1821
 
    
1822
 
    /**
1823
 
     * Defines a namespace for directives to be put into.
1824
 
     * @warning This is slightly different from the corresponding static
1825
 
     *          method.
1826
 
     * @param $namespace Namespace's name
1827
 
     */
1828
 
    public function addNamespace($namespace) {
1829
 
        $this->info[$namespace] = array();
1830
 
        $this->defaults[$namespace] = array();
1831
 
    }
1832
 
    
1833
 
    /**
1834
 
     * Defines a directive value alias.
1835
 
     * 
1836
 
     * Directive value aliases are convenient for developers because it lets
1837
 
     * them set a directive to several values and get the same result.
1838
 
     * @param $namespace Directive's namespace
1839
 
     * @param $name Name of Directive
1840
 
     * @param $aliases Hash of aliased values to the real alias
1841
 
     */
1842
 
    public function addValueAliases($namespace, $name, $aliases) {
1843
 
        if (!isset($this->info[$namespace][$name]->aliases)) {
1844
 
            $this->info[$namespace][$name]->aliases = array();
1845
 
        }
1846
 
        foreach ($aliases as $alias => $real) {
1847
 
            $this->info[$namespace][$name]->aliases[$alias] = $real;
1848
 
        }
1849
 
    }
1850
 
    
1851
 
    /**
1852
 
     * Defines a set of allowed values for a directive.
1853
 
     * @warning This is slightly different from the corresponding static
1854
 
     *          method definition.
1855
 
     * @param $namespace Namespace of directive
1856
 
     * @param $name Name of directive
1857
 
     * @param $allowed Lookup array of allowed values
1858
 
     */
1859
 
    public function addAllowedValues($namespace, $name, $allowed) {
1860
 
        $this->info[$namespace][$name]->allowed = $allowed;
1861
 
    }
1862
 
    
1863
 
    /**
1864
 
     * Defines a directive alias for backwards compatibility
1865
 
     * @param $namespace
1866
 
     * @param $name Directive that will be aliased
1867
 
     * @param $new_namespace
1868
 
     * @param $new_name Directive that the alias will be to
1869
 
     */
1870
 
    public function addAlias($namespace, $name, $new_namespace, $new_name) {
1871
 
        $obj = new stdclass;
1872
 
        $obj->namespace = $new_namespace;
1873
 
        $obj->name = $new_name;
1874
 
        $obj->isAlias = true;
1875
 
        $this->info[$namespace][$name] = $obj;
1876
 
    }
1877
 
    
1878
 
    /**
1879
 
     * Replaces any stdclass that only has the type property with type integer.
1880
 
     */
1881
 
    public function postProcess() {
1882
 
        foreach ($this->info as $namespace => $info) {
1883
 
            foreach ($info as $directive => $v) {
1884
 
                if (count((array) $v) == 1) {
1885
 
                    $this->info[$namespace][$directive] = $v->type;
1886
 
                } elseif (count((array) $v) == 2 && isset($v->allow_null)) {
1887
 
                    $this->info[$namespace][$directive] = -$v->type;
1888
 
                }
1889
 
            }
1890
 
        }
1891
 
    }
1892
 
    
1893
 
    // DEPRECATED METHODS
1894
 
    
1895
 
    /** @see HTMLPurifier_ConfigSchema->set() */
1896
 
    public static function define($namespace, $name, $default, $type, $description) {
1897
 
        HTMLPurifier_ConfigSchema::deprecated(__METHOD__);
1898
 
        $type_values = explode('/', $type, 2);
1899
 
        $type = $type_values[0];
1900
 
        $modifier = isset($type_values[1]) ? $type_values[1] : false;
1901
 
        $allow_null = ($modifier === 'null');
1902
 
        $def = HTMLPurifier_ConfigSchema::instance();
1903
 
        $def->add($namespace, $name, $default, $type, $allow_null);
1904
 
    }
1905
 
    
1906
 
    /** @see HTMLPurifier_ConfigSchema->addNamespace() */
1907
 
    public static function defineNamespace($namespace, $description) {
1908
 
        HTMLPurifier_ConfigSchema::deprecated(__METHOD__);
1909
 
        $def = HTMLPurifier_ConfigSchema::instance();
1910
 
        $def->addNamespace($namespace);
1911
 
    }
1912
 
    
1913
 
    /** @see HTMLPurifier_ConfigSchema->addValueAliases() */
1914
 
    public static function defineValueAliases($namespace, $name, $aliases) {
1915
 
        HTMLPurifier_ConfigSchema::deprecated(__METHOD__);
1916
 
        $def = HTMLPurifier_ConfigSchema::instance();
1917
 
        $def->addValueAliases($namespace, $name, $aliases);
1918
 
    }
1919
 
    
1920
 
    /** @see HTMLPurifier_ConfigSchema->addAllowedValues() */
1921
 
    public static function defineAllowedValues($namespace, $name, $allowed_values) {
1922
 
        HTMLPurifier_ConfigSchema::deprecated(__METHOD__);
1923
 
        $allowed = array();
1924
 
        foreach ($allowed_values as $value) {
1925
 
            $allowed[$value] = true;
1926
 
        }
1927
 
        $def = HTMLPurifier_ConfigSchema::instance();
1928
 
        $def->addAllowedValues($namespace, $name, $allowed);
1929
 
    }
1930
 
    
1931
 
    /** @see HTMLPurifier_ConfigSchema->addAlias() */
1932
 
    public static function defineAlias($namespace, $name, $new_namespace, $new_name) {
1933
 
        HTMLPurifier_ConfigSchema::deprecated(__METHOD__);
1934
 
        $def = HTMLPurifier_ConfigSchema::instance();
1935
 
        $def->addAlias($namespace, $name, $new_namespace, $new_name);
1936
 
    }
1937
 
    
1938
 
    /** @deprecated, use HTMLPurifier_VarParser->parse() */
1939
 
    public function validate($a, $b, $c = false) {
1940
 
        trigger_error("HTMLPurifier_ConfigSchema->validate deprecated, use HTMLPurifier_VarParser->parse instead", E_USER_NOTICE);
1941
 
        $parser = new HTMLPurifier_VarParser();
1942
 
        return $parser->parse($a, $b, $c);
1943
 
    }
1944
 
    
1945
 
    /**
1946
 
     * Throws an E_USER_NOTICE stating that a method is deprecated.
1947
 
     */
1948
 
    private static function deprecated($method) {
1949
 
        trigger_error("Static HTMLPurifier_ConfigSchema::$method deprecated, use add*() method instead", E_USER_NOTICE);
1950
 
    }
1951
 
    
1952
 
}
1953
 
 
1954
 
 
1955
 
 
1956
 
 
1957
 
 
1958
 
/**
1959
 
 * @todo Unit test
1960
 
 */
1961
 
class HTMLPurifier_ContentSets
1962
 
{
1963
 
    
1964
 
    /**
1965
 
     * List of content set strings (pipe seperators) indexed by name.
1966
 
     */
1967
 
    public $info = array();
1968
 
    
1969
 
    /**
1970
 
     * List of content set lookups (element => true) indexed by name.
1971
 
     * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
1972
 
     */
1973
 
    public $lookup = array();
1974
 
    
1975
 
    /**
1976
 
     * Synchronized list of defined content sets (keys of info)
1977
 
     */
1978
 
    protected $keys = array();
1979
 
    /**
1980
 
     * Synchronized list of defined content values (values of info)
1981
 
     */
1982
 
    protected $values = array();
1983
 
    
1984
 
    /**
1985
 
     * Merges in module's content sets, expands identifiers in the content
1986
 
     * sets and populates the keys, values and lookup member variables.
1987
 
     * @param $modules List of HTMLPurifier_HTMLModule
1988
 
     */
1989
 
    public function __construct($modules) {
1990
 
        if (!is_array($modules)) $modules = array($modules);
1991
 
        // populate content_sets based on module hints
1992
 
        // sorry, no way of overloading
1993
 
        foreach ($modules as $module_i => $module) {
1994
 
            foreach ($module->content_sets as $key => $value) {
1995
 
                $temp = $this->convertToLookup($value);
1996
 
                if (isset($this->lookup[$key])) {
1997
 
                    // add it into the existing content set
1998
 
                    $this->lookup[$key] = array_merge($this->lookup[$key], $temp);
1999
 
                } else {
2000
 
                    $this->lookup[$key] = $temp;
2001
 
                }
2002
 
            }
2003
 
        }
2004
 
        $old_lookup = false;
2005
 
        while ($old_lookup !== $this->lookup) {
2006
 
            $old_lookup = $this->lookup;
2007
 
            foreach ($this->lookup as $i => $set) {
2008
 
                $add = array();
2009
 
                foreach ($set as $element => $x) {
2010
 
                    if (isset($this->lookup[$element])) {
2011
 
                        $add += $this->lookup[$element];
2012
 
                        unset($this->lookup[$i][$element]);
2013
 
                    }
2014
 
                }
2015
 
                $this->lookup[$i] += $add;
2016
 
            }
2017
 
        }
2018
 
        
2019
 
        foreach ($this->lookup as $key => $lookup) {
2020
 
            $this->info[$key] = implode(' | ', array_keys($lookup));
2021
 
        }
2022
 
        $this->keys   = array_keys($this->info);
2023
 
        $this->values = array_values($this->info);
2024
 
    }
2025
 
    
2026
 
    /**
2027
 
     * Accepts a definition; generates and assigns a ChildDef for it
2028
 
     * @param $def HTMLPurifier_ElementDef reference
2029
 
     * @param $module Module that defined the ElementDef
2030
 
     */
2031
 
    public function generateChildDef(&$def, $module) {
2032
 
        if (!empty($def->child)) return; // already done!
2033
 
        $content_model = $def->content_model;
2034
 
        if (is_string($content_model)) {
2035
 
            // Assume that $this->keys is alphanumeric
2036
 
            $def->content_model = preg_replace_callback(
2037
 
                '/\b(' . implode('|', $this->keys) . ')\b/',
2038
 
                array($this, 'generateChildDefCallback'),
2039
 
                $content_model
2040
 
            );
2041
 
            //$def->content_model = str_replace(
2042
 
            //    $this->keys, $this->values, $content_model);
2043
 
        }
2044
 
        $def->child = $this->getChildDef($def, $module);
2045
 
    }
2046
 
    
2047
 
    public function generateChildDefCallback($matches) {
2048
 
        return $this->info[$matches[0]];
2049
 
    }
2050
 
    
2051
 
    /**
2052
 
     * Instantiates a ChildDef based on content_model and content_model_type
2053
 
     * member variables in HTMLPurifier_ElementDef
2054
 
     * @note This will also defer to modules for custom HTMLPurifier_ChildDef
2055
 
     *       subclasses that need content set expansion
2056
 
     * @param $def HTMLPurifier_ElementDef to have ChildDef extracted
2057
 
     * @return HTMLPurifier_ChildDef corresponding to ElementDef
2058
 
     */
2059
 
    public function getChildDef($def, $module) {
2060
 
        $value = $def->content_model;
2061
 
        if (is_object($value)) {
2062
 
            trigger_error(
2063
 
                'Literal object child definitions should be stored in '.
2064
 
                'ElementDef->child not ElementDef->content_model',
2065
 
                E_USER_NOTICE
2066
 
            );
2067
 
            return $value;
2068
 
        }
2069
 
        switch ($def->content_model_type) {
2070
 
            case 'required':
2071
 
                return new HTMLPurifier_ChildDef_Required($value);
2072
 
            case 'optional':
2073
 
                return new HTMLPurifier_ChildDef_Optional($value);
2074
 
            case 'empty':
2075
 
                return new HTMLPurifier_ChildDef_Empty();
2076
 
            case 'custom':
2077
 
                return new HTMLPurifier_ChildDef_Custom($value);
2078
 
        }
2079
 
        // defer to its module
2080
 
        $return = false;
2081
 
        if ($module->defines_child_def) { // save a func call
2082
 
            $return = $module->getChildDef($def);
2083
 
        }
2084
 
        if ($return !== false) return $return;
2085
 
        // error-out
2086
 
        trigger_error(
2087
 
            'Could not determine which ChildDef class to instantiate',
2088
 
            E_USER_ERROR
2089
 
        );
2090
 
        return false;
2091
 
    }
2092
 
    
2093
 
    /**
2094
 
     * Converts a string list of elements separated by pipes into
2095
 
     * a lookup array.
2096
 
     * @param $string List of elements
2097
 
     * @return Lookup array of elements
2098
 
     */
2099
 
    protected function convertToLookup($string) {
2100
 
        $array = explode('|', str_replace(' ', '', $string));
2101
 
        $ret = array();
2102
 
        foreach ($array as $i => $k) {
2103
 
            $ret[$k] = true;
2104
 
        }
2105
 
        return $ret;
2106
 
    }
2107
 
    
2108
 
}
2109
 
 
2110
 
 
2111
 
 
2112
 
 
2113
 
/**
2114
 
 * Registry object that contains information about the current context.
2115
 
 * @warning Is a bit buggy when variables are set to null: it thinks
2116
 
 *          they don't exist! So use false instead, please.
2117
 
 * @note Since the variables Context deals with may not be objects,
2118
 
 *       references are very important here! Do not remove!
2119
 
 */
2120
 
class HTMLPurifier_Context
2121
 
{
2122
 
    
2123
 
    /**
2124
 
     * Private array that stores the references.
2125
 
     */
2126
 
    private $_storage = array();
2127
 
    
2128
 
    /**
2129
 
     * Registers a variable into the context.
2130
 
     * @param $name String name
2131
 
     * @param $ref Reference to variable to be registered
2132
 
     */
2133
 
    public function register($name, &$ref) {
2134
 
        if (isset($this->_storage[$name])) {
2135
 
            trigger_error("Name $name produces collision, cannot re-register",
2136
 
                          E_USER_ERROR);
2137
 
            return;
2138
 
        }
2139
 
        $this->_storage[$name] =& $ref;
2140
 
    }
2141
 
    
2142
 
    /**
2143
 
     * Retrieves a variable reference from the context.
2144
 
     * @param $name String name
2145
 
     * @param $ignore_error Boolean whether or not to ignore error
2146
 
     */
2147
 
    public function &get($name, $ignore_error = false) {
2148
 
        if (!isset($this->_storage[$name])) {
2149
 
            if (!$ignore_error) {
2150
 
                trigger_error("Attempted to retrieve non-existent variable $name",
2151
 
                              E_USER_ERROR);
2152
 
            }
2153
 
            $var = null; // so we can return by reference
2154
 
            return $var;
2155
 
        }
2156
 
        return $this->_storage[$name];
2157
 
    }
2158
 
    
2159
 
    /**
2160
 
     * Destorys a variable in the context.
2161
 
     * @param $name String name
2162
 
     */
2163
 
    public function destroy($name) {
2164
 
        if (!isset($this->_storage[$name])) {
2165
 
            trigger_error("Attempted to destroy non-existent variable $name",
2166
 
                          E_USER_ERROR);
2167
 
            return;
2168
 
        }
2169
 
        unset($this->_storage[$name]);
2170
 
    }
2171
 
    
2172
 
    /**
2173
 
     * Checks whether or not the variable exists.
2174
 
     * @param $name String name
2175
 
     */
2176
 
    public function exists($name) {
2177
 
        return isset($this->_storage[$name]);
2178
 
    }
2179
 
    
2180
 
    /**
2181
 
     * Loads a series of variables from an associative array
2182
 
     * @param $context_array Assoc array of variables to load
2183
 
     */
2184
 
    public function loadArray($context_array) {
2185
 
        foreach ($context_array as $key => $discard) {
2186
 
            $this->register($key, $context_array[$key]);
2187
 
        }
2188
 
    }
2189
 
    
2190
 
}
2191
 
 
2192
 
 
2193
 
 
2194
 
 
2195
 
/**
2196
 
 * Abstract class representing Definition cache managers that implements
2197
 
 * useful common methods and is a factory.
2198
 
 * @todo Create a separate maintenance file advanced users can use to
2199
 
 *       cache their custom HTMLDefinition, which can be loaded
2200
 
 *       via a configuration directive
2201
 
 * @todo Implement memcached
2202
 
 */
2203
 
abstract class HTMLPurifier_DefinitionCache
2204
 
{
2205
 
    
2206
 
    public $type;
2207
 
    
2208
 
    /**
2209
 
     * @param $name Type of definition objects this instance of the
2210
 
     *      cache will handle.
2211
 
     */
2212
 
    public function __construct($type) {
2213
 
        $this->type = $type;
2214
 
    }
2215
 
    
2216
 
    /**
2217
 
     * Generates a unique identifier for a particular configuration
2218
 
     * @param Instance of HTMLPurifier_Config
2219
 
     */
2220
 
    public function generateKey($config) {
2221
 
        return $config->version . ',' . // possibly replace with function calls
2222
 
               $config->getBatchSerial($this->type) . ',' .
2223
 
               $config->get($this->type, 'DefinitionRev');
2224
 
    }
2225
 
    
2226
 
    /**
2227
 
     * Tests whether or not a key is old with respect to the configuration's
2228
 
     * version and revision number.
2229
 
     * @param $key Key to test
2230
 
     * @param $config Instance of HTMLPurifier_Config to test against
2231
 
     */
2232
 
    public function isOld($key, $config) {
2233
 
        if (substr_count($key, ',') < 2) return true;
2234
 
        list($version, $hash, $revision) = explode(',', $key, 3);
2235
 
        $compare = version_compare($version, $config->version);
2236
 
        // version mismatch, is always old
2237
 
        if ($compare != 0) return true;
2238
 
        // versions match, ids match, check revision number
2239
 
        if (
2240
 
            $hash == $config->getBatchSerial($this->type) &&
2241
 
            $revision < $config->get($this->type, 'DefinitionRev')
2242
 
        ) return true;
2243
 
        return false;
2244
 
    }
2245
 
    
2246
 
    /**
2247
 
     * Checks if a definition's type jives with the cache's type
2248
 
     * @note Throws an error on failure
2249
 
     * @param $def Definition object to check
2250
 
     * @return Boolean true if good, false if not
2251
 
     */
2252
 
    public function checkDefType($def) {
2253
 
        if ($def->type !== $this->type) {
2254
 
            trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}");
2255
 
            return false;
2256
 
        }
2257
 
        return true;
2258
 
    }
2259
 
    
2260
 
    /**
2261
 
     * Adds a definition object to the cache
2262
 
     */
2263
 
    abstract public function add($def, $config);
2264
 
    
2265
 
    /**
2266
 
     * Unconditionally saves a definition object to the cache
2267
 
     */
2268
 
    abstract public function set($def, $config);
2269
 
    
2270
 
    /**
2271
 
     * Replace an object in the cache
2272
 
     */
2273
 
    abstract public function replace($def, $config);
2274
 
    
2275
 
    /**
2276
 
     * Retrieves a definition object from the cache
2277
 
     */
2278
 
    abstract public function get($config);
2279
 
    
2280
 
    /**
2281
 
     * Removes a definition object to the cache
2282
 
     */
2283
 
    abstract public function remove($config);
2284
 
    
2285
 
    /**
2286
 
     * Clears all objects from cache
2287
 
     */
2288
 
    abstract public function flush($config);
2289
 
    
2290
 
    /**
2291
 
     * Clears all expired (older version or revision) objects from cache
2292
 
     * @note Be carefuly implementing this method as flush. Flush must
2293
 
     *       not interfere with other Definition types, and cleanup()
2294
 
     *       should not be repeatedly called by userland code.
2295
 
     */
2296
 
    abstract public function cleanup($config);
2297
 
    
2298
 
}
2299
 
 
2300
 
 
2301
 
 
2302
 
 
2303
 
/**
2304
 
 * Responsible for creating definition caches.
2305
 
 */
2306
 
class HTMLPurifier_DefinitionCacheFactory
2307
 
{
2308
 
    
2309
 
    protected $caches = array('Serializer' => array());
2310
 
    protected $implementations = array();
2311
 
    protected $decorators = array();
2312
 
    
2313
 
    /**
2314
 
     * Initialize default decorators
2315
 
     */
2316
 
    public function setup() {
2317
 
        $this->addDecorator('Cleanup');
2318
 
    }
2319
 
    
2320
 
    /**
2321
 
     * Retrieves an instance of global definition cache factory.
2322
 
     */
2323
 
    public static function instance($prototype = null) {
2324
 
        static $instance;
2325
 
        if ($prototype !== null) {
2326
 
            $instance = $prototype;
2327
 
        } elseif ($instance === null || $prototype === true) {
2328
 
            $instance = new HTMLPurifier_DefinitionCacheFactory();
2329
 
            $instance->setup();
2330
 
        }
2331
 
        return $instance;
2332
 
    }
2333
 
    
2334
 
    /**
2335
 
     * Registers a new definition cache object
2336
 
     * @param $short Short name of cache object, for reference
2337
 
     * @param $long Full class name of cache object, for construction 
2338
 
     */
2339
 
    public function register($short, $long) {
2340
 
        $this->implementations[$short] = $long;
2341
 
    }
2342
 
    
2343
 
    /**
2344
 
     * Factory method that creates a cache object based on configuration
2345
 
     * @param $name Name of definitions handled by cache
2346
 
     * @param $config Instance of HTMLPurifier_Config
2347
 
     */
2348
 
    public function create($type, $config) {
2349
 
        $method = $config->get('Cache', 'DefinitionImpl');
2350
 
        if ($method === null) {
2351
 
            return new HTMLPurifier_DefinitionCache_Null($type);
2352
 
        }
2353
 
        if (!empty($this->caches[$method][$type])) {
2354
 
            return $this->caches[$method][$type];
2355
 
        }
2356
 
        if (
2357
 
          isset($this->implementations[$method]) &&
2358
 
          class_exists($class = $this->implementations[$method], false)
2359
 
        ) {
2360
 
            $cache = new $class($type);
2361
 
        } else {
2362
 
            if ($method != 'Serializer') {
2363
 
                trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING);
2364
 
            }
2365
 
            $cache = new HTMLPurifier_DefinitionCache_Serializer($type);
2366
 
        }
2367
 
        foreach ($this->decorators as $decorator) {
2368
 
            $new_cache = $decorator->decorate($cache);
2369
 
            // prevent infinite recursion in PHP 4
2370
 
            unset($cache);
2371
 
            $cache = $new_cache;
2372
 
        }
2373
 
        $this->caches[$method][$type] = $cache;
2374
 
        return $this->caches[$method][$type];
2375
 
    }
2376
 
    
2377
 
    /**
2378
 
     * Registers a decorator to add to all new cache objects
2379
 
     * @param 
2380
 
     */
2381
 
    public function addDecorator($decorator) {
2382
 
        if (is_string($decorator)) {
2383
 
            $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator";
2384
 
            $decorator = new $class;
2385
 
        }
2386
 
        $this->decorators[$decorator->name] = $decorator;
2387
 
    }
2388
 
    
2389
 
}
2390
 
 
2391
 
 
2392
 
 
2393
 
 
2394
 
/**
2395
 
 * Represents a document type, contains information on which modules
2396
 
 * need to be loaded.
2397
 
 * @note This class is inspected by Printer_HTMLDefinition->renderDoctype.
2398
 
 *       If structure changes, please update that function.
2399
 
 */
2400
 
class HTMLPurifier_Doctype
2401
 
{
2402
 
    /**
2403
 
     * Full name of doctype
2404
 
     */
2405
 
    public $name;
2406
 
    
2407
 
    /**
2408
 
     * List of standard modules (string identifiers or literal objects)
2409
 
     * that this doctype uses
2410
 
     */
2411
 
    public $modules = array();
2412
 
    
2413
 
    /**
2414
 
     * List of modules to use for tidying up code
2415
 
     */
2416
 
    public $tidyModules = array();
2417
 
    
2418
 
    /**
2419
 
     * Is the language derived from XML (i.e. XHTML)?
2420
 
     */
2421
 
    public $xml = true;
2422
 
    
2423
 
    /**
2424
 
     * List of aliases for this doctype
2425
 
     */
2426
 
    public $aliases = array();
2427
 
    
2428
 
    /**
2429
 
     * Public DTD identifier
2430
 
     */
2431
 
    public $dtdPublic;
2432
 
    
2433
 
    /**
2434
 
     * System DTD identifier
2435
 
     */
2436
 
    public $dtdSystem;
2437
 
    
2438
 
    public function __construct($name = null, $xml = true, $modules = array(),
2439
 
        $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
2440
 
    ) {
2441
 
        $this->name         = $name;
2442
 
        $this->xml          = $xml;
2443
 
        $this->modules      = $modules;
2444
 
        $this->tidyModules  = $tidyModules;
2445
 
        $this->aliases      = $aliases;
2446
 
        $this->dtdPublic    = $dtd_public;
2447
 
        $this->dtdSystem    = $dtd_system;
2448
 
    }
2449
 
}
2450
 
 
2451
 
 
2452
 
 
2453
 
 
2454
 
class HTMLPurifier_DoctypeRegistry
2455
 
{
2456
 
    
2457
 
    /**
2458
 
     * Hash of doctype names to doctype objects
2459
 
     */
2460
 
    protected $doctypes;
2461
 
    
2462
 
    /**
2463
 
     * Lookup table of aliases to real doctype names
2464
 
     */
2465
 
    protected $aliases;
2466
 
    
2467
 
    /**
2468
 
     * Registers a doctype to the registry
2469
 
     * @note Accepts a fully-formed doctype object, or the
2470
 
     *       parameters for constructing a doctype object
2471
 
     * @param $doctype Name of doctype or literal doctype object
2472
 
     * @param $modules Modules doctype will load
2473
 
     * @param $modules_for_modes Modules doctype will load for certain modes
2474
 
     * @param $aliases Alias names for doctype
2475
 
     * @return Editable registered doctype
2476
 
     */
2477
 
    public function register($doctype, $xml = true, $modules = array(),
2478
 
        $tidy_modules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
2479
 
    ) {
2480
 
        if (!is_array($modules)) $modules = array($modules);
2481
 
        if (!is_array($tidy_modules)) $tidy_modules = array($tidy_modules);
2482
 
        if (!is_array($aliases)) $aliases = array($aliases);
2483
 
        if (!is_object($doctype)) {
2484
 
            $doctype = new HTMLPurifier_Doctype(
2485
 
                $doctype, $xml, $modules, $tidy_modules, $aliases, $dtd_public, $dtd_system
2486
 
            );
2487
 
        }
2488
 
        $this->doctypes[$doctype->name] = $doctype;
2489
 
        $name = $doctype->name;
2490
 
        // hookup aliases
2491
 
        foreach ($doctype->aliases as $alias) {
2492
 
            if (isset($this->doctypes[$alias])) continue;
2493
 
            $this->aliases[$alias] = $name;
2494
 
        }
2495
 
        // remove old aliases
2496
 
        if (isset($this->aliases[$name])) unset($this->aliases[$name]);
2497
 
        return $doctype;
2498
 
    }
2499
 
    
2500
 
    /**
2501
 
     * Retrieves reference to a doctype of a certain name
2502
 
     * @note This function resolves aliases
2503
 
     * @note When possible, use the more fully-featured make()
2504
 
     * @param $doctype Name of doctype
2505
 
     * @return Editable doctype object
2506
 
     */
2507
 
    public function get($doctype) {
2508
 
        if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype];
2509
 
        if (!isset($this->doctypes[$doctype])) {
2510
 
            trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR);
2511
 
            $anon = new HTMLPurifier_Doctype($doctype);
2512
 
            return $anon;
2513
 
        }
2514
 
        return $this->doctypes[$doctype];
2515
 
    }
2516
 
    
2517
 
    /**
2518
 
     * Creates a doctype based on a configuration object,
2519
 
     * will perform initialization on the doctype
2520
 
     * @note Use this function to get a copy of doctype that config
2521
 
     *       can hold on to (this is necessary in order to tell
2522
 
     *       Generator whether or not the current document is XML
2523
 
     *       based or not).
2524
 
     */
2525
 
    public function make($config) {
2526
 
        return clone $this->get($this->getDoctypeFromConfig($config));
2527
 
    }
2528
 
    
2529
 
    /**
2530
 
     * Retrieves the doctype from the configuration object
2531
 
     */
2532
 
    public function getDoctypeFromConfig($config) {
2533
 
        // recommended test
2534
 
        $doctype = $config->get('HTML', 'Doctype');
2535
 
        if (!empty($doctype)) return $doctype;
2536
 
        $doctype = $config->get('HTML', 'CustomDoctype');
2537
 
        if (!empty($doctype)) return $doctype;
2538
 
        // backwards-compatibility
2539
 
        if ($config->get('HTML', 'XHTML')) {
2540
 
            $doctype = 'XHTML 1.0';
2541
 
        } else {
2542
 
            $doctype = 'HTML 4.01';
2543
 
        }
2544
 
        if ($config->get('HTML', 'Strict')) {
2545
 
            $doctype .= ' Strict';
2546
 
        } else {
2547
 
            $doctype .= ' Transitional';
2548
 
        }
2549
 
        return $doctype;
2550
 
    }
2551
 
    
2552
 
}
2553
 
 
2554
 
 
2555
 
 
2556
 
 
2557
 
/**
2558
 
 * Structure that stores an HTML element definition. Used by
2559
 
 * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule.
2560
 
 * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition.
2561
 
 *       Please update that class too.
2562
 
 */
2563
 
class HTMLPurifier_ElementDef
2564
 
{
2565
 
    
2566
 
    /**
2567
 
     * Does the definition work by itself, or is it created solely
2568
 
     * for the purpose of merging into another definition?
2569
 
     */
2570
 
    public $standalone = true;
2571
 
    
2572
 
    /**
2573
 
     * Associative array of attribute name to HTMLPurifier_AttrDef
2574
 
     * @note Before being processed by HTMLPurifier_AttrCollections
2575
 
     *       when modules are finalized during
2576
 
     *       HTMLPurifier_HTMLDefinition->setup(), this array may also
2577
 
     *       contain an array at index 0 that indicates which attribute
2578
 
     *       collections to load into the full array. It may also
2579
 
     *       contain string indentifiers in lieu of HTMLPurifier_AttrDef,
2580
 
     *       see HTMLPurifier_AttrTypes on how they are expanded during
2581
 
     *       HTMLPurifier_HTMLDefinition->setup() processing.
2582
 
     */
2583
 
    public $attr = array();
2584
 
    
2585
 
    /**
2586
 
     * Indexed list of tag's HTMLPurifier_AttrTransform to be done before validation
2587
 
     */
2588
 
    public $attr_transform_pre = array();
2589
 
    
2590
 
    /**
2591
 
     * Indexed list of tag's HTMLPurifier_AttrTransform to be done after validation
2592
 
     */
2593
 
    public $attr_transform_post = array();
2594
 
    
2595
 
    /**
2596
 
     * HTMLPurifier_ChildDef of this tag.
2597
 
     */
2598
 
    public $child;
2599
 
    
2600
 
    /**
2601
 
     * Abstract string representation of internal ChildDef rules. See
2602
 
     * HTMLPurifier_ContentSets for how this is parsed and then transformed
2603
 
     * into an HTMLPurifier_ChildDef.
2604
 
     * @warning This is a temporary variable that is not available after
2605
 
     *      being processed by HTMLDefinition
2606
 
     */
2607
 
    public $content_model;
2608
 
    
2609
 
    /**
2610
 
     * Value of $child->type, used to determine which ChildDef to use,
2611
 
     * used in combination with $content_model.
2612
 
     * @warning This must be lowercase
2613
 
     * @warning This is a temporary variable that is not available after
2614
 
     *      being processed by HTMLDefinition
2615
 
     */
2616
 
    public $content_model_type;
2617
 
    
2618
 
    
2619
 
    
2620
 
    /**
2621
 
     * Does the element have a content model (#PCDATA | Inline)*? This
2622
 
     * is important for chameleon ins and del processing in 
2623
 
     * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't
2624
 
     * have to worry about this one.
2625
 
     */
2626
 
    public $descendants_are_inline = false;
2627
 
    
2628
 
    /**
2629
 
     * List of the names of required attributes this element has. Dynamically
2630
 
     * populated by HTMLPurifier_HTMLDefinition::getElement
2631
 
     */
2632
 
    public $required_attr = array();
2633
 
    
2634
 
    /**
2635
 
     * Lookup table of tags excluded from all descendants of this tag.
2636
 
     * @note SGML permits exclusions for all descendants, but this is
2637
 
     *       not possible with DTDs or XML Schemas. W3C has elected to
2638
 
     *       use complicated compositions of content_models to simulate
2639
 
     *       exclusion for children, but we go the simpler, SGML-style
2640
 
     *       route of flat-out exclusions, which correctly apply to
2641
 
     *       all descendants and not just children. Note that the XHTML
2642
 
     *       Modularization Abstract Modules are blithely unaware of such
2643
 
     *       distinctions.
2644
 
     */
2645
 
    public $excludes = array();
2646
 
    
2647
 
    /**
2648
 
     * Low-level factory constructor for creating new standalone element defs
2649
 
     */
2650
 
    public static function create($content_model, $content_model_type, $attr) {
2651
 
        $def = new HTMLPurifier_ElementDef();
2652
 
        $def->content_model = $content_model;
2653
 
        $def->content_model_type = $content_model_type;
2654
 
        $def->attr = $attr;
2655
 
        return $def;
2656
 
    }
2657
 
    
2658
 
    /**
2659
 
     * Merges the values of another element definition into this one.
2660
 
     * Values from the new element def take precedence if a value is
2661
 
     * not mergeable.
2662
 
     */
2663
 
    public function mergeIn($def) {
2664
 
        
2665
 
        // later keys takes precedence
2666
 
        foreach($def->attr as $k => $v) {
2667
 
            if ($k === 0) {
2668
 
                // merge in the includes
2669
 
                // sorry, no way to override an include
2670
 
                foreach ($v as $v2) {
2671
 
                    $this->attr[0][] = $v2;
2672
 
                }
2673
 
                continue;
2674
 
            }
2675
 
            if ($v === false) {
2676
 
                if (isset($this->attr[$k])) unset($this->attr[$k]);
2677
 
                continue;
2678
 
            }
2679
 
            $this->attr[$k] = $v;
2680
 
        }
2681
 
        $this->_mergeAssocArray($this->attr_transform_pre, $def->attr_transform_pre);
2682
 
        $this->_mergeAssocArray($this->attr_transform_post, $def->attr_transform_post);
2683
 
        $this->_mergeAssocArray($this->excludes, $def->excludes);
2684
 
        
2685
 
        if(!empty($def->content_model)) {
2686
 
            $this->content_model .= ' | ' . $def->content_model;
2687
 
            $this->child = false;
2688
 
        }
2689
 
        if(!empty($def->content_model_type)) {
2690
 
            $this->content_model_type = $def->content_model_type;
2691
 
            $this->child = false;
2692
 
        }
2693
 
        if(!is_null($def->child)) $this->child = $def->child;
2694
 
        if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline;
2695
 
        
2696
 
    }
2697
 
    
2698
 
    /**
2699
 
     * Merges one array into another, removes values which equal false
2700
 
     * @param $a1 Array by reference that is merged into
2701
 
     * @param $a2 Array that merges into $a1
2702
 
     */
2703
 
    private function _mergeAssocArray(&$a1, $a2) {
2704
 
        foreach ($a2 as $k => $v) {
2705
 
            if ($v === false) {
2706
 
                if (isset($a1[$k])) unset($a1[$k]);
2707
 
                continue;
2708
 
            }
2709
 
            $a1[$k] = $v;
2710
 
        }
2711
 
    }
2712
 
    
2713
 
}
2714
 
 
2715
 
 
2716
 
 
2717
 
 
2718
 
 
2719
 
/**
2720
 
 * A UTF-8 specific character encoder that handles cleaning and transforming.
2721
 
 * @note All functions in this class should be static.
2722
 
 */
2723
 
class HTMLPurifier_Encoder
2724
 
{
2725
 
    
2726
 
    /**
2727
 
     * Constructor throws fatal error if you attempt to instantiate class
2728
 
     */
2729
 
    private function __construct() {
2730
 
        trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
2731
 
    }
2732
 
    
2733
 
    /**
2734
 
     * Error-handler that mutes errors, alternative to shut-up operator.
2735
 
     */
2736
 
    private static function muteErrorHandler() {}
2737
 
    
2738
 
    /**
2739
 
     * Cleans a UTF-8 string for well-formedness and SGML validity
2740
 
     * 
2741
 
     * It will parse according to UTF-8 and return a valid UTF8 string, with
2742
 
     * non-SGML codepoints excluded.
2743
 
     * 
2744
 
     * @note Just for reference, the non-SGML code points are 0 to 31 and
2745
 
     *       127 to 159, inclusive.  However, we allow code points 9, 10
2746
 
     *       and 13, which are the tab, line feed and carriage return
2747
 
     *       respectively. 128 and above the code points map to multibyte
2748
 
     *       UTF-8 representations.
2749
 
     * 
2750
 
     * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
2751
 
     *       hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
2752
 
     *       LGPL license.  Notes on what changed are inside, but in general,
2753
 
     *       the original code transformed UTF-8 text into an array of integer
2754
 
     *       Unicode codepoints. Understandably, transforming that back to
2755
 
     *       a string would be somewhat expensive, so the function was modded to
2756
 
     *       directly operate on the string.  However, this discourages code
2757
 
     *       reuse, and the logic enumerated here would be useful for any
2758
 
     *       function that needs to be able to understand UTF-8 characters.
2759
 
     *       As of right now, only smart lossless character encoding converters
2760
 
     *       would need that, and I'm probably not going to implement them.
2761
 
     *       Once again, PHP 6 should solve all our problems.
2762
 
     */
2763
 
    public static function cleanUTF8($str, $force_php = false) {
2764
 
        
2765
 
        // UTF-8 validity is checked since PHP 4.3.5
2766
 
        // This is an optimization: if the string is already valid UTF-8, no
2767
 
        // need to do PHP stuff. 99% of the time, this will be the case.
2768
 
        // The regexp matches the XML char production, as well as well as excluding
2769
 
        // non-SGML codepoints U+007F to U+009F
2770
 
        if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) {
2771
 
            return $str;
2772
 
        }
2773
 
        
2774
 
        $mState = 0; // cached expected number of octets after the current octet
2775
 
                     // until the beginning of the next UTF8 character sequence
2776
 
        $mUcs4  = 0; // cached Unicode character
2777
 
        $mBytes = 1; // cached expected number of octets in the current sequence
2778
 
        
2779
 
        // original code involved an $out that was an array of Unicode
2780
 
        // codepoints.  Instead of having to convert back into UTF-8, we've
2781
 
        // decided to directly append valid UTF-8 characters onto a string
2782
 
        // $out once they're done.  $char accumulates raw bytes, while $mUcs4
2783
 
        // turns into the Unicode code point, so there's some redundancy.
2784
 
        
2785
 
        $out = '';
2786
 
        $char = '';
2787
 
        
2788
 
        $len = strlen($str);
2789
 
        for($i = 0; $i < $len; $i++) {
2790
 
            $in = ord($str{$i});
2791
 
            $char .= $str[$i]; // append byte to char
2792
 
            if (0 == $mState) {
2793
 
                // When mState is zero we expect either a US-ASCII character 
2794
 
                // or a multi-octet sequence.
2795
 
                if (0 == (0x80 & ($in))) {
2796
 
                    // US-ASCII, pass straight through.
2797
 
                    if (($in <= 31 || $in == 127) && 
2798
 
                        !($in == 9 || $in == 13 || $in == 10) // save \r\t\n
2799
 
                    ) {
2800
 
                        // control characters, remove
2801
 
                    } else {
2802
 
                        $out .= $char;
2803
 
                    }
2804
 
                    // reset
2805
 
                    $char = '';
2806
 
                    $mBytes = 1;
2807
 
                } elseif (0xC0 == (0xE0 & ($in))) {
2808
 
                    // First octet of 2 octet sequence
2809
 
                    $mUcs4 = ($in);
2810
 
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
2811
 
                    $mState = 1;
2812
 
                    $mBytes = 2;
2813
 
                } elseif (0xE0 == (0xF0 & ($in))) {
2814
 
                    // First octet of 3 octet sequence
2815
 
                    $mUcs4 = ($in);
2816
 
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
2817
 
                    $mState = 2;
2818
 
                    $mBytes = 3;
2819
 
                } elseif (0xF0 == (0xF8 & ($in))) {
2820
 
                    // First octet of 4 octet sequence
2821
 
                    $mUcs4 = ($in);
2822
 
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
2823
 
                    $mState = 3;
2824
 
                    $mBytes = 4;
2825
 
                } elseif (0xF8 == (0xFC & ($in))) {
2826
 
                    // First octet of 5 octet sequence.
2827
 
                    // 
2828
 
                    // This is illegal because the encoded codepoint must be 
2829
 
                    // either:
2830
 
                    // (a) not the shortest form or
2831
 
                    // (b) outside the Unicode range of 0-0x10FFFF.
2832
 
                    // Rather than trying to resynchronize, we will carry on 
2833
 
                    // until the end of the sequence and let the later error
2834
 
                    // handling code catch it.
2835
 
                    $mUcs4 = ($in);
2836
 
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
2837
 
                    $mState = 4;
2838
 
                    $mBytes = 5;
2839
 
                } elseif (0xFC == (0xFE & ($in))) {
2840
 
                    // First octet of 6 octet sequence, see comments for 5
2841
 
                    // octet sequence.
2842
 
                    $mUcs4 = ($in);
2843
 
                    $mUcs4 = ($mUcs4 & 1) << 30;
2844
 
                    $mState = 5;
2845
 
                    $mBytes = 6;
2846
 
                } else {
2847
 
                    // Current octet is neither in the US-ASCII range nor a 
2848
 
                    // legal first octet of a multi-octet sequence.
2849
 
                    $mState = 0;
2850
 
                    $mUcs4  = 0;
2851
 
                    $mBytes = 1;
2852
 
                    $char = '';
2853
 
                }
2854
 
            } else {
2855
 
                // When mState is non-zero, we expect a continuation of the
2856
 
                // multi-octet sequence
2857
 
                if (0x80 == (0xC0 & ($in))) {
2858
 
                    // Legal continuation.
2859
 
                    $shift = ($mState - 1) * 6;
2860
 
                    $tmp = $in;
2861
 
                    $tmp = ($tmp & 0x0000003F) << $shift;
2862
 
                    $mUcs4 |= $tmp;
2863
 
                    
2864
 
                    if (0 == --$mState) {
2865
 
                        // End of the multi-octet sequence. mUcs4 now contains
2866
 
                        // the final Unicode codepoint to be output
2867
 
                        
2868
 
                        // Check for illegal sequences and codepoints.
2869
 
                        
2870
 
                        // From Unicode 3.1, non-shortest form is illegal
2871
 
                        if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
2872
 
                            ((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
2873
 
                            ((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
2874
 
                            (4 < $mBytes) ||
2875
 
                            // From Unicode 3.2, surrogate characters = illegal
2876
 
                            (($mUcs4 & 0xFFFFF800) == 0xD800) ||
2877
 
                            // Codepoints outside the Unicode range are illegal
2878
 
                            ($mUcs4 > 0x10FFFF)
2879
 
                        ) {
2880
 
                            
2881
 
                        } elseif (0xFEFF != $mUcs4 && // omit BOM
2882
 
                            // check for valid Char unicode codepoints
2883
 
                            (
2884
 
                                0x9 == $mUcs4 ||
2885
 
                                0xA == $mUcs4 ||
2886
 
                                0xD == $mUcs4 ||
2887
 
                                (0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
2888
 
                                // 7F-9F is not strictly prohibited by XML,
2889
 
                                // but it is non-SGML, and thus we don't allow it
2890
 
                                (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
2891
 
                                (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
2892
 
                            )
2893
 
                        ) {
2894
 
                            $out .= $char;
2895
 
                        }
2896
 
                        // initialize UTF8 cache (reset)
2897
 
                        $mState = 0;
2898
 
                        $mUcs4  = 0;
2899
 
                        $mBytes = 1;
2900
 
                        $char = '';
2901
 
                    }
2902
 
                } else {
2903
 
                    // ((0xC0 & (*in) != 0x80) && (mState != 0))
2904
 
                    // Incomplete multi-octet sequence.
2905
 
                    // used to result in complete fail, but we'll reset
2906
 
                    $mState = 0;
2907
 
                    $mUcs4  = 0;
2908
 
                    $mBytes = 1;
2909
 
                    $char ='';
2910
 
                }
2911
 
            }
2912
 
        }
2913
 
        return $out;
2914
 
    }
2915
 
    
2916
 
    /**
2917
 
     * Translates a Unicode codepoint into its corresponding UTF-8 character.
2918
 
     * @note Based on Feyd's function at
2919
 
     *       <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
2920
 
     *       which is in public domain.
2921
 
     * @note While we're going to do code point parsing anyway, a good
2922
 
     *       optimization would be to refuse to translate code points that
2923
 
     *       are non-SGML characters.  However, this could lead to duplication.
2924
 
     * @note This is very similar to the unichr function in
2925
 
     *       maintenance/generate-entity-file.php (although this is superior,
2926
 
     *       due to its sanity checks).
2927
 
     */
2928
 
    
2929
 
    // +----------+----------+----------+----------+
2930
 
    // | 33222222 | 22221111 | 111111   |          |
2931
 
    // | 10987654 | 32109876 | 54321098 | 76543210 | bit
2932
 
    // +----------+----------+----------+----------+
2933
 
    // |          |          |          | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
2934
 
    // |          |          | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
2935
 
    // |          | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
2936
 
    // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
2937
 
    // +----------+----------+----------+----------+
2938
 
    // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
2939
 
    // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
2940
 
    // +----------+----------+----------+----------+ 
2941
 
    
2942
 
    public static function unichr($code) {
2943
 
        if($code > 1114111 or $code < 0 or
2944
 
          ($code >= 55296 and $code <= 57343) ) {
2945
 
            // bits are set outside the "valid" range as defined
2946
 
            // by UNICODE 4.1.0 
2947
 
            return '';
2948
 
        }
2949
 
        
2950
 
        $x = $y = $z = $w = 0; 
2951
 
        if ($code < 128) {
2952
 
            // regular ASCII character
2953
 
            $x = $code;
2954
 
        } else {
2955
 
            // set up bits for UTF-8
2956
 
            $x = ($code & 63) | 128;
2957
 
            if ($code < 2048) {
2958
 
                $y = (($code & 2047) >> 6) | 192;
2959
 
            } else {
2960
 
                $y = (($code & 4032) >> 6) | 128;
2961
 
                if($code < 65536) {
2962
 
                    $z = (($code >> 12) & 15) | 224;
2963
 
                } else {
2964
 
                    $z = (($code >> 12) & 63) | 128;
2965
 
                    $w = (($code >> 18) & 7)  | 240;
2966
 
                }
2967
 
            } 
2968
 
        }
2969
 
        // set up the actual character
2970
 
        $ret = '';
2971
 
        if($w) $ret .= chr($w);
2972
 
        if($z) $ret .= chr($z);
2973
 
        if($y) $ret .= chr($y);
2974
 
        $ret .= chr($x); 
2975
 
        
2976
 
        return $ret;
2977
 
    }
2978
 
    
2979
 
    /**
2980
 
     * Converts a string to UTF-8 based on configuration.
2981
 
     */
2982
 
    public static function convertToUTF8($str, $config, $context) {
2983
 
        $encoding = $config->get('Core', 'Encoding');
2984
 
        if ($encoding === 'utf-8') return $str;
2985
 
        static $iconv = null;
2986
 
        if ($iconv === null) $iconv = function_exists('iconv');
2987
 
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
2988
 
        if ($iconv && !$config->get('Test', 'ForceNoIconv')) {
2989
 
            $str = iconv($encoding, 'utf-8//IGNORE', $str);
2990
 
            if ($str === false) {
2991
 
                // $encoding is not a valid encoding
2992
 
                restore_error_handler();
2993
 
                trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
2994
 
                return '';
2995
 
            }
2996
 
            // If the string is bjorked by Shift_JIS or a similar encoding
2997
 
            // that doesn't support all of ASCII, convert the naughty
2998
 
            // characters to their true byte-wise ASCII/UTF-8 equivalents.
2999
 
            $str = strtr($str, HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding));
3000
 
            restore_error_handler();
3001
 
            return $str;
3002
 
        } elseif ($encoding === 'iso-8859-1') {
3003
 
            $str = utf8_encode($str);
3004
 
            restore_error_handler();
3005
 
            return $str;
3006
 
        }
3007
 
        trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
3008
 
    }
3009
 
    
3010
 
    /**
3011
 
     * Converts a string from UTF-8 based on configuration.
3012
 
     * @note Currently, this is a lossy conversion, with unexpressable
3013
 
     *       characters being omitted.
3014
 
     */
3015
 
    public static function convertFromUTF8($str, $config, $context) {
3016
 
        $encoding = $config->get('Core', 'Encoding');
3017
 
        if ($encoding === 'utf-8') return $str;
3018
 
        static $iconv = null;
3019
 
        if ($iconv === null) $iconv = function_exists('iconv');
3020
 
        if ($escape = $config->get('Core', 'EscapeNonASCIICharacters')) {
3021
 
            $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str);
3022
 
        }
3023
 
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
3024
 
        if ($iconv && !$config->get('Test', 'ForceNoIconv')) {
3025
 
            // Undo our previous fix in convertToUTF8, otherwise iconv will barf
3026
 
            $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding);
3027
 
            if (!$escape && !empty($ascii_fix)) {
3028
 
                $clear_fix = array();
3029
 
                foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
3030
 
                $str = strtr($str, $clear_fix);
3031
 
            }
3032
 
            $str = strtr($str, array_flip($ascii_fix));
3033
 
            // Normal stuff
3034
 
            $str = iconv('utf-8', $encoding . '//IGNORE', $str);
3035
 
            restore_error_handler();
3036
 
            return $str;
3037
 
        } elseif ($encoding === 'iso-8859-1') {
3038
 
            $str = utf8_decode($str);
3039
 
            restore_error_handler();
3040
 
            return $str;
3041
 
        }
3042
 
        trigger_error('Encoding not supported', E_USER_ERROR);
3043
 
    }
3044
 
    
3045
 
    /**
3046
 
     * Lossless (character-wise) conversion of HTML to ASCII
3047
 
     * @param $str UTF-8 string to be converted to ASCII
3048
 
     * @returns ASCII encoded string with non-ASCII character entity-ized
3049
 
     * @warning Adapted from MediaWiki, claiming fair use: this is a common
3050
 
     *       algorithm. If you disagree with this license fudgery,
3051
 
     *       implement it yourself.
3052
 
     * @note Uses decimal numeric entities since they are best supported.
3053
 
     * @note This is a DUMB function: it has no concept of keeping
3054
 
     *       character entities that the projected character encoding
3055
 
     *       can allow. We could possibly implement a smart version
3056
 
     *       but that would require it to also know which Unicode
3057
 
     *       codepoints the charset supported (not an easy task).
3058
 
     * @note Sort of with cleanUTF8() but it assumes that $str is
3059
 
     *       well-formed UTF-8
3060
 
     */
3061
 
    public static function convertToASCIIDumbLossless($str) {
3062
 
        $bytesleft = 0;
3063
 
        $result = '';
3064
 
        $working = 0;
3065
 
        $len = strlen($str);
3066
 
        for( $i = 0; $i < $len; $i++ ) {
3067
 
            $bytevalue = ord( $str[$i] );
3068
 
            if( $bytevalue <= 0x7F ) { //0xxx xxxx
3069
 
                $result .= chr( $bytevalue );
3070
 
                $bytesleft = 0;
3071
 
            } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
3072
 
                $working = $working << 6;
3073
 
                $working += ($bytevalue & 0x3F);
3074
 
                $bytesleft--;
3075
 
                if( $bytesleft <= 0 ) {
3076
 
                    $result .= "&#" . $working . ";";
3077
 
                }
3078
 
            } elseif( $bytevalue <= 0xDF ) { //110x xxxx
3079
 
                $working = $bytevalue & 0x1F;
3080
 
                $bytesleft = 1;
3081
 
            } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
3082
 
                $working = $bytevalue & 0x0F;
3083
 
                $bytesleft = 2;
3084
 
            } else { //1111 0xxx
3085
 
                $working = $bytevalue & 0x07;
3086
 
                $bytesleft = 3;
3087
 
            }
3088
 
        }
3089
 
        return $result;
3090
 
    }
3091
 
    
3092
 
    /**
3093
 
     * This expensive function tests whether or not a given character
3094
 
     * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
3095
 
     * fail this test, and require special processing. Variable width
3096
 
     * encodings shouldn't ever fail.
3097
 
     * 
3098
 
     * @param string $encoding Encoding name to test, as per iconv format
3099
 
     * @param bool $bypass Whether or not to bypass the precompiled arrays.
3100
 
     * @return Array of UTF-8 characters to their corresponding ASCII,
3101
 
     *      which can be used to "undo" any overzealous iconv action.
3102
 
     */
3103
 
    public static function testEncodingSupportsASCII($encoding, $bypass = false) {
3104
 
        static $encodings = array();
3105
 
        if (!$bypass) {
3106
 
            if (isset($encodings[$encoding])) return $encodings[$encoding];
3107
 
            $lenc = strtolower($encoding);
3108
 
            switch ($lenc) {
3109
 
                case 'shift_jis':
3110
 
                    return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
3111
 
                case 'johab':
3112
 
                    return array("\xE2\x82\xA9" => '\\');
3113
 
            }
3114
 
            if (strpos($lenc, 'iso-8859-') === 0) return array();
3115
 
        }
3116
 
        $ret = array();
3117
 
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
3118
 
        if (iconv('UTF-8', $encoding, 'a') === false) return false;
3119
 
        for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
3120
 
            $c = chr($i);
3121
 
            if (iconv('UTF-8', "$encoding//IGNORE", $c) === '') {
3122
 
                // Reverse engineer: what's the UTF-8 equiv of this byte
3123
 
                // sequence? This assumes that there's no variable width
3124
 
                // encoding that doesn't support ASCII.
3125
 
                $ret[iconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
3126
 
            }
3127
 
        }
3128
 
        restore_error_handler();
3129
 
        $encodings[$encoding] = $ret;
3130
 
        return $ret;
3131
 
    }
3132
 
    
3133
 
    
3134
 
}
3135
 
 
3136
 
 
3137
 
 
3138
 
 
3139
 
/**
3140
 
 * Object that provides entity lookup table from entity name to character
3141
 
 */
3142
 
class HTMLPurifier_EntityLookup {
3143
 
    
3144
 
    /**
3145
 
     * Assoc array of entity name to character represented.
3146
 
     */
3147
 
    public $table;
3148
 
    
3149
 
    /**
3150
 
     * Sets up the entity lookup table from the serialized file contents.
3151
 
     * @note The serialized contents are versioned, but were generated
3152
 
     *       using the maintenance script generate_entity_file.php
3153
 
     * @warning This is not in constructor to help enforce the Singleton
3154
 
     */
3155
 
    public function setup($file = false) {
3156
 
        if (!$file) {
3157
 
            $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser';
3158
 
        }
3159
 
        $this->table = unserialize(file_get_contents($file));
3160
 
    }
3161
 
    
3162
 
    /**
3163
 
     * Retrieves sole instance of the object.
3164
 
     * @param Optional prototype of custom lookup table to overload with.
3165
 
     */
3166
 
    public static function instance($prototype = false) {
3167
 
        // no references, since PHP doesn't copy unless modified
3168
 
        static $instance = null;
3169
 
        if ($prototype) {
3170
 
            $instance = $prototype;
3171
 
        } elseif (!$instance) {
3172
 
            $instance = new HTMLPurifier_EntityLookup();
3173
 
            $instance->setup();
3174
 
        }
3175
 
        return $instance;
3176
 
    }
3177
 
    
3178
 
}
3179
 
 
3180
 
 
3181
 
 
3182
 
 
3183
 
// if want to implement error collecting here, we'll need to use some sort
3184
 
// of global data (probably trigger_error) because it's impossible to pass
3185
 
// $config or $context to the callback functions.
3186
 
 
3187
 
/**
3188
 
 * Handles referencing and derefencing character entities
3189
 
 */
3190
 
class HTMLPurifier_EntityParser
3191
 
{
3192
 
    
3193
 
    /**
3194
 
     * Reference to entity lookup table.
3195
 
     */
3196
 
    protected $_entity_lookup;
3197
 
    
3198
 
    /**
3199
 
     * Callback regex string for parsing entities.
3200
 
     */                             
3201
 
    protected $_substituteEntitiesRegex =
3202
 
'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/';
3203
 
//     1. hex             2. dec      3. string (XML style)
3204
 
    
3205
 
    
3206
 
    /**
3207
 
     * Decimal to parsed string conversion table for special entities.
3208
 
     */
3209
 
    protected $_special_dec2str =
3210
 
            array(
3211
 
                    34 => '"',
3212
 
                    38 => '&',
3213
 
                    39 => "'",
3214
 
                    60 => '<',
3215
 
                    62 => '>'
3216
 
            );
3217
 
    
3218
 
    /**
3219
 
     * Stripped entity names to decimal conversion table for special entities.
3220
 
     */
3221
 
    protected $_special_ent2dec =
3222
 
            array(
3223
 
                    'quot' => 34,
3224
 
                    'amp'  => 38,
3225
 
                    'lt'   => 60,
3226
 
                    'gt'   => 62
3227
 
            );
3228
 
    
3229
 
    /**
3230
 
     * Substitutes non-special entities with their parsed equivalents. Since
3231
 
     * running this whenever you have parsed character is t3h 5uck, we run
3232
 
     * it before everything else.
3233
 
     * 
3234
 
     * @param $string String to have non-special entities parsed.
3235
 
     * @returns Parsed string.
3236
 
     */
3237
 
    public function substituteNonSpecialEntities($string) {
3238
 
        // it will try to detect missing semicolons, but don't rely on it
3239
 
        return preg_replace_callback(
3240
 
            $this->_substituteEntitiesRegex,
3241
 
            array($this, 'nonSpecialEntityCallback'),
3242
 
            $string
3243
 
            );
3244
 
    }
3245
 
    
3246
 
    /**
3247
 
     * Callback function for substituteNonSpecialEntities() that does the work.
3248
 
     * 
3249
 
     * @param $matches  PCRE matches array, with 0 the entire match, and
3250
 
     *                  either index 1, 2 or 3 set with a hex value, dec value,
3251
 
     *                  or string (respectively).
3252
 
     * @returns Replacement string.
3253
 
     */
3254
 
    
3255
 
    protected function nonSpecialEntityCallback($matches) {
3256
 
        // replaces all but big five
3257
 
        $entity = $matches[0];
3258
 
        $is_num = (@$matches[0][1] === '#');
3259
 
        if ($is_num) {
3260
 
            $is_hex = (@$entity[2] === 'x');
3261
 
            $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
3262
 
            
3263
 
            // abort for special characters
3264
 
            if (isset($this->_special_dec2str[$code]))  return $entity;
3265
 
            
3266
 
            return HTMLPurifier_Encoder::unichr($code);
3267
 
        } else {
3268
 
            if (isset($this->_special_ent2dec[$matches[3]])) return $entity;
3269
 
            if (!$this->_entity_lookup) {
3270
 
                $this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
3271
 
            }
3272
 
            if (isset($this->_entity_lookup->table[$matches[3]])) {
3273
 
                return $this->_entity_lookup->table[$matches[3]];
3274
 
            } else {
3275
 
                return $entity;
3276
 
            }
3277
 
        }
3278
 
    }
3279
 
    
3280
 
    /**
3281
 
     * Substitutes only special entities with their parsed equivalents.
3282
 
     * 
3283
 
     * @notice We try to avoid calling this function because otherwise, it
3284
 
     * would have to be called a lot (for every parsed section).
3285
 
     * 
3286
 
     * @param $string String to have non-special entities parsed.
3287
 
     * @returns Parsed string.
3288
 
     */
3289
 
    public function substituteSpecialEntities($string) {
3290
 
        return preg_replace_callback(
3291
 
            $this->_substituteEntitiesRegex,
3292
 
            array($this, 'specialEntityCallback'),
3293
 
            $string);
3294
 
    }
3295
 
    
3296
 
    /**
3297
 
     * Callback function for substituteSpecialEntities() that does the work.
3298
 
     * 
3299
 
     * This callback has same syntax as nonSpecialEntityCallback().
3300
 
     * 
3301
 
     * @param $matches  PCRE-style matches array, with 0 the entire match, and
3302
 
     *                  either index 1, 2 or 3 set with a hex value, dec value,
3303
 
     *                  or string (respectively).
3304
 
     * @returns Replacement string.
3305
 
     */
3306
 
    protected function specialEntityCallback($matches) {
3307
 
        $entity = $matches[0];
3308
 
        $is_num = (@$matches[0][1] === '#');
3309
 
        if ($is_num) {
3310
 
            $is_hex = (@$entity[2] === 'x');
3311
 
            $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
3312
 
            return isset($this->_special_dec2str[$int]) ?
3313
 
                $this->_special_dec2str[$int] :
3314
 
                $entity;
3315
 
        } else {
3316
 
            return isset($this->_special_ent2dec[$matches[3]]) ?
3317
 
                $this->_special_ent2dec[$matches[3]] :
3318
 
                $entity;
3319
 
        }
3320
 
    }
3321
 
    
3322
 
}
3323
 
 
3324
 
 
3325
 
 
3326
 
 
3327
 
/**
3328
 
 * Error collection class that enables HTML Purifier to report HTML
3329
 
 * problems back to the user
3330
 
 */
3331
 
class HTMLPurifier_ErrorCollector
3332
 
{
3333
 
    
3334
 
    /**
3335
 
     * Identifiers for the returned error array. These are purposely numeric
3336
 
     * so list() can be used.
3337
 
     */
3338
 
    const LINENO   = 0;
3339
 
    const SEVERITY = 1;
3340
 
    const MESSAGE  = 2;
3341
 
    const CHILDREN = 3;
3342
 
    
3343
 
    protected $errors;
3344
 
    protected $_current;
3345
 
    protected $_stacks = array(array());
3346
 
    protected $locale;
3347
 
    protected $generator;
3348
 
    protected $context;
3349
 
    
3350
 
    protected $lines = array();
3351
 
    
3352
 
    public function __construct($context) {
3353
 
        $this->locale    =& $context->get('Locale');
3354
 
        $this->context   = $context;
3355
 
        $this->_current  =& $this->_stacks[0];
3356
 
        $this->errors    =& $this->_stacks[0];
3357
 
    }
3358
 
    
3359
 
    /**
3360
 
     * Sends an error message to the collector for later use
3361
 
     * @param $severity int Error severity, PHP error style (don't use E_USER_)
3362
 
     * @param $msg string Error message text
3363
 
     * @param $subst1 string First substitution for $msg
3364
 
     * @param $subst2 string ...
3365
 
     */
3366
 
    public function send($severity, $msg) {
3367
 
        
3368
 
        $args = array();
3369
 
        if (func_num_args() > 2) {
3370
 
            $args = func_get_args();
3371
 
            array_shift($args);
3372
 
            unset($args[0]);
3373
 
        }
3374
 
        
3375
 
        $token = $this->context->get('CurrentToken', true);
3376
 
        $line  = $token ? $token->line : $this->context->get('CurrentLine', true);
3377
 
        $col   = $token ? $token->col  : $this->context->get('CurrentCol',  true);
3378
 
        $attr  = $this->context->get('CurrentAttr', true);
3379
 
        
3380
 
        // perform special substitutions, also add custom parameters
3381
 
        $subst = array();
3382
 
        if (!is_null($token)) {
3383
 
            $args['CurrentToken'] = $token;
3384
 
        }
3385
 
        if (!is_null($attr)) {
3386
 
            $subst['$CurrentAttr.Name'] = $attr;
3387
 
            if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr];
3388
 
        }
3389
 
        
3390
 
        if (empty($args)) {
3391
 
            $msg = $this->locale->getMessage($msg);
3392
 
        } else {
3393
 
            $msg = $this->locale->formatMessage($msg, $args);
3394
 
        }
3395
 
        
3396
 
        if (!empty($subst)) $msg = strtr($msg, $subst);
3397
 
        
3398
 
        // (numerically indexed)
3399
 
        $error = array(
3400
 
            self::LINENO   => $line,
3401
 
            self::SEVERITY => $severity,
3402
 
            self::MESSAGE  => $msg,
3403
 
            self::CHILDREN => array()
3404
 
        );
3405
 
        $this->_current[] = $error;
3406
 
        
3407
 
        
3408
 
        // NEW CODE BELOW ...
3409
 
        
3410
 
        $struct = null;
3411
 
        // Top-level errors are either:
3412
 
        //  TOKEN type, if $value is set appropriately, or
3413
 
        //  "syntax" type, if $value is null
3414
 
        $new_struct = new HTMLPurifier_ErrorStruct();
3415
 
        $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN;
3416
 
        if ($token) $new_struct->value = clone $token;
3417
 
        if (is_int($line) && is_int($col)) {
3418
 
            if (isset($this->lines[$line][$col])) {
3419
 
                $struct = $this->lines[$line][$col];
3420
 
            } else {
3421
 
                $struct = $this->lines[$line][$col] = $new_struct;
3422
 
            }
3423
 
            // These ksorts may present a performance problem
3424
 
            ksort($this->lines[$line], SORT_NUMERIC);
3425
 
        } else {
3426
 
            if (isset($this->lines[-1])) {
3427
 
                $struct = $this->lines[-1];
3428
 
            } else {
3429
 
                $struct = $this->lines[-1] = $new_struct;
3430
 
            }
3431
 
        }
3432
 
        ksort($this->lines, SORT_NUMERIC);
3433
 
        
3434
 
        // Now, check if we need to operate on a lower structure
3435
 
        if (!empty($attr)) {
3436
 
            $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr);
3437
 
            if (!$struct->value) {
3438
 
                $struct->value = array($attr, 'PUT VALUE HERE');
3439
 
            }
3440
 
        }
3441
 
        if (!empty($cssprop)) {
3442
 
            $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop);
3443
 
            if (!$struct->value) {
3444
 
                // if we tokenize CSS this might be a little more difficult to do
3445
 
                $struct->value = array($cssprop, 'PUT VALUE HERE');
3446
 
            }
3447
 
        }
3448
 
        
3449
 
        // Ok, structs are all setup, now time to register the error
3450
 
        $struct->addError($severity, $msg);
3451
 
    }
3452
 
    
3453
 
    /**
3454
 
     * Retrieves raw error data for custom formatter to use
3455
 
     * @param List of arrays in format of array(line of error,
3456
 
     *        error severity, error message,
3457
 
     *        recursive sub-errors array)
3458
 
     */
3459
 
    public function getRaw() {
3460
 
        return $this->errors;
3461
 
    }
3462
 
    
3463
 
    /**
3464
 
     * Default HTML formatting implementation for error messages
3465
 
     * @param $config Configuration array, vital for HTML output nature
3466
 
     * @param $errors Errors array to display; used for recursion.
3467
 
     */
3468
 
    public function getHTMLFormatted($config, $errors = null) {
3469
 
        $ret = array();
3470
 
        
3471
 
        $this->generator = new HTMLPurifier_Generator($config, $this->context);
3472
 
        if ($errors === null) $errors = $this->errors;
3473
 
        
3474
 
        // 'At line' message needs to be removed
3475
 
        
3476
 
        // generation code for new structure goes here. It needs to be recursive.
3477
 
        foreach ($this->lines as $line => $col_array) {
3478
 
            if ($line == -1) continue;
3479
 
            foreach ($col_array as $col => $struct) {
3480
 
                $this->_renderStruct($ret, $struct, $line, $col);
3481
 
            }
3482
 
        }
3483
 
        if (isset($this->lines[-1])) {
3484
 
            $this->_renderStruct($ret, $this->lines[-1]);
3485
 
        }
3486
 
        
3487
 
        if (empty($errors)) {
3488
 
            return '<p>' . $this->locale->getMessage('ErrorCollector: No errors') . '</p>';
3489
 
        } else {
3490
 
            return '<ul><li>' . implode('</li><li>', $ret) . '</li></ul>';
3491
 
        }
3492
 
        
3493
 
    }
3494
 
    
3495
 
    private function _renderStruct(&$ret, $struct, $line = null, $col = null) {
3496
 
        $stack = array($struct);
3497
 
        $context_stack = array(array());
3498
 
        while ($current = array_pop($stack)) {
3499
 
            $context = array_pop($context_stack);
3500
 
            foreach ($current->errors as $error) {
3501
 
                list($severity, $msg) = $error;
3502
 
                $string = '';
3503
 
                $string .= '<div>';
3504
 
                // W3C uses an icon to indicate the severity of the error.
3505
 
                $error = $this->locale->getErrorName($severity);
3506
 
                $string .= "<span class=\"error e$severity\"><strong>$error</strong></span> ";
3507
 
                if (!is_null($line) && !is_null($col)) {
3508
 
                    $string .= "<em class=\"location\">Line $line, Column $col: </em> ";
3509
 
                } else {
3510
 
                    $string .= '<em class="location">End of Document: </em> ';
3511
 
                }
3512
 
                $string .= '<strong class="description">' . $this->generator->escape($msg) . '</strong> ';
3513
 
                $string .= '</div>';
3514
 
                // Here, have a marker for the character on the column appropriate.
3515
 
                // Be sure to clip extremely long lines.
3516
 
                //$string .= '<pre>';
3517
 
                //$string .= '';
3518
 
                //$string .= '</pre>';
3519
 
                $ret[] = $string;
3520
 
            }
3521
 
            foreach ($current->children as $type => $array) {
3522
 
                $context[] = $current;
3523
 
                $stack = array_merge($stack, array_reverse($array, true));
3524
 
                for ($i = count($array); $i > 0; $i--) {
3525
 
                    $context_stack[] = $context;
3526
 
                }
3527
 
            }
3528
 
        }
3529
 
    }
3530
 
    
3531
 
}
3532
 
 
3533
 
 
3534
 
 
3535
 
 
3536
 
/**
3537
 
 * Records errors for particular segments of an HTML document such as tokens,
3538
 
 * attributes or CSS properties. They can contain error structs (which apply
3539
 
 * to components of what they represent), but their main purpose is to hold
3540
 
 * errors applying to whatever struct is being used.
3541
 
 */
3542
 
class HTMLPurifier_ErrorStruct
3543
 
{
3544
 
    
3545
 
    /**
3546
 
     * Possible values for $children first-key. Note that top-level structures
3547
 
     * are automatically token-level.
3548
 
     */
3549
 
    const TOKEN     = 0;
3550
 
    const ATTR      = 1;
3551
 
    const CSSPROP   = 2;
3552
 
    
3553
 
    /**
3554
 
     * Type of this struct.
3555
 
     */
3556
 
    public $type;
3557
 
    
3558
 
    /**
3559
 
     * Value of the struct we are recording errors for. There are various
3560
 
     * values for this:
3561
 
     *  - TOKEN: Instance of HTMLPurifier_Token
3562
 
     *  - ATTR: array('attr-name', 'value')
3563
 
     *  - CSSPROP: array('prop-name', 'value')
3564
 
     */
3565
 
    public $value;
3566
 
    
3567
 
    /**
3568
 
     * Errors registered for this structure.
3569
 
     */
3570
 
    public $errors = array();
3571
 
    
3572
 
    /**
3573
 
     * Child ErrorStructs that are from this structure. For example, a TOKEN
3574
 
     * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional
3575
 
     * array in structure: [TYPE]['identifier']
3576
 
     */
3577
 
    public $children = array();
3578
 
    
3579
 
    public function getChild($type, $id) {
3580
 
        if (!isset($this->children[$type][$id])) {
3581
 
            $this->children[$type][$id] = new HTMLPurifier_ErrorStruct();
3582
 
            $this->children[$type][$id]->type = $type;
3583
 
        }
3584
 
        return $this->children[$type][$id];
3585
 
    }
3586
 
    
3587
 
    public function addError($severity, $message) {
3588
 
        $this->errors[] = array($severity, $message);
3589
 
    }
3590
 
    
3591
 
}
3592
 
 
3593
 
 
3594
 
 
3595
 
/**
3596
 
 * Global exception class for HTML Purifier; any exceptions we throw
3597
 
 * are from here.
3598
 
 */
3599
 
class HTMLPurifier_Exception extends Exception
3600
 
{
3601
 
    
3602
 
}
3603
 
 
3604
 
 
3605
 
 
3606
 
 
3607
 
/**
3608
 
 * Represents a pre or post processing filter on HTML Purifier's output
3609
 
 * 
3610
 
 * Sometimes, a little ad-hoc fixing of HTML has to be done before
3611
 
 * it gets sent through HTML Purifier: you can use filters to acheive
3612
 
 * this effect. For instance, YouTube videos can be preserved using
3613
 
 * this manner. You could have used a decorator for this task, but
3614
 
 * PHP's support for them is not terribly robust, so we're going
3615
 
 * to just loop through the filters.
3616
 
 * 
3617
 
 * Filters should be exited first in, last out. If there are three filters,
3618
 
 * named 1, 2 and 3, the order of execution should go 1->preFilter,
3619
 
 * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
3620
 
 * 1->postFilter.
3621
 
 * 
3622
 
 * @note Methods are not declared abstract as it is perfectly legitimate
3623
 
 *       for an implementation not to want anything to happen on a step
3624
 
 */
3625
 
 
3626
 
class HTMLPurifier_Filter
3627
 
{
3628
 
    
3629
 
    /**
3630
 
     * Name of the filter for identification purposes
3631
 
     */
3632
 
    public $name;
3633
 
    
3634
 
    /**
3635
 
     * Pre-processor function, handles HTML before HTML Purifier 
3636
 
     */
3637
 
    public function preFilter($html, $config, $context) {
3638
 
        return $html;
3639
 
    }
3640
 
    
3641
 
    /**
3642
 
     * Post-processor function, handles HTML after HTML Purifier
3643
 
     */
3644
 
    public function postFilter($html, $config, $context) {
3645
 
        return $html;
3646
 
    }
3647
 
    
3648
 
}
3649
 
 
3650
 
 
3651
 
 
3652
 
 
3653
 
/**
3654
 
 * Generates HTML from tokens.
3655
 
 * @todo Refactor interface so that configuration/context is determined
3656
 
 *       upon instantiation, no need for messy generateFromTokens() calls
3657
 
 * @todo Make some of the more internal functions protected, and have
3658
 
 *       unit tests work around that
3659
 
 */
3660
 
class HTMLPurifier_Generator
3661
 
{
3662
 
    
3663
 
    /**
3664
 
     * Whether or not generator should produce XML output
3665
 
     */
3666
 
    private $_xhtml = true;
3667
 
    
3668
 
    /**
3669
 
     * :HACK: Whether or not generator should comment the insides of <script> tags
3670
 
     */
3671
 
    private $_scriptFix = false;
3672
 
    
3673
 
    /**
3674
 
     * Cache of HTMLDefinition during HTML output to determine whether or
3675
 
     * not attributes should be minimized.
3676
 
     */
3677
 
    private $_def;
3678
 
    
3679
 
    /**
3680
 
     * Cache of %Output.SortAttr
3681
 
     */
3682
 
    private $_sortAttr;
3683
 
    
3684
 
    /**
3685
 
     * Configuration for the generator
3686
 
     */
3687
 
    protected $config;
3688
 
    
3689
 
    /**
3690
 
     * @param $config Instance of HTMLPurifier_Config
3691
 
     * @param $context Instance of HTMLPurifier_Context
3692
 
     */
3693
 
    public function __construct($config, $context) {
3694
 
        $this->config = $config;
3695
 
        $this->_scriptFix = $config->get('Output', 'CommentScriptContents');
3696
 
        $this->_sortAttr = $config->get('Output', 'SortAttr');
3697
 
        $this->_def = $config->getHTMLDefinition();
3698
 
        $this->_xhtml = $this->_def->doctype->xml;
3699
 
    }
3700
 
    
3701
 
    /**
3702
 
     * Generates HTML from an array of tokens.
3703
 
     * @param $tokens Array of HTMLPurifier_Token
3704
 
     * @param $config HTMLPurifier_Config object
3705
 
     * @return Generated HTML
3706
 
     */
3707
 
    public function generateFromTokens($tokens) {
3708
 
        if (!$tokens) return '';
3709
 
        
3710
 
        // Basic algorithm
3711
 
        $html = '';
3712
 
        for ($i = 0, $size = count($tokens); $i < $size; $i++) {
3713
 
            if ($this->_scriptFix && $tokens[$i]->name === 'script'
3714
 
                && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
3715
 
                // script special case
3716
 
                // the contents of the script block must be ONE token
3717
 
                // for this to work.
3718
 
                $html .= $this->generateFromToken($tokens[$i++]);
3719
 
                $html .= $this->generateScriptFromToken($tokens[$i++]);
3720
 
            }
3721
 
            $html .= $this->generateFromToken($tokens[$i]);
3722
 
        }
3723
 
        
3724
 
        // Tidy cleanup
3725
 
        if (extension_loaded('tidy') && $this->config->get('Output', 'TidyFormat')) {
3726
 
            $tidy = new Tidy;
3727
 
            $tidy->parseString($html, array(
3728
 
               'indent'=> true,
3729
 
               'output-xhtml' => $this->_xhtml,
3730
 
               'show-body-only' => true,
3731
 
               'indent-spaces' => 2,
3732
 
               'wrap' => 68,
3733
 
            ), 'utf8');
3734
 
            $tidy->cleanRepair();
3735
 
            $html = (string) $tidy; // explicit cast necessary
3736
 
        }
3737
 
        
3738
 
        // Normalize newlines to system defined value
3739
 
        $nl = $this->config->get('Output', 'Newline');
3740
 
        if ($nl === null) $nl = PHP_EOL;
3741
 
        if ($nl !== "\n") $html = str_replace("\n", $nl, $html);
3742
 
        return $html;
3743
 
    }
3744
 
    
3745
 
    /**
3746
 
     * Generates HTML from a single token.
3747
 
     * @param $token HTMLPurifier_Token object.
3748
 
     * @return Generated HTML
3749
 
     */
3750
 
    public function generateFromToken($token) {
3751
 
        if (!$token instanceof HTMLPurifier_Token) {
3752
 
            trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING);
3753
 
            return '';
3754
 
            
3755
 
        } elseif ($token instanceof HTMLPurifier_Token_Start) {
3756
 
            $attr = $this->generateAttributes($token->attr, $token->name);
3757
 
            return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>';
3758
 
            
3759
 
        } elseif ($token instanceof HTMLPurifier_Token_End) {
3760
 
            return '</' . $token->name . '>';
3761
 
            
3762
 
        } elseif ($token instanceof HTMLPurifier_Token_Empty) {
3763
 
            $attr = $this->generateAttributes($token->attr, $token->name);
3764
 
             return '<' . $token->name . ($attr ? ' ' : '') . $attr .
3765
 
                ( $this->_xhtml ? ' /': '' ) // <br /> v. <br>
3766
 
                . '>';
3767
 
            
3768
 
        } elseif ($token instanceof HTMLPurifier_Token_Text) {
3769
 
            return $this->escape($token->data, ENT_NOQUOTES);
3770
 
            
3771
 
        } elseif ($token instanceof HTMLPurifier_Token_Comment) {
3772
 
            return '<!--' . $token->data . '-->';
3773
 
        } else {
3774
 
            return '';
3775
 
            
3776
 
        }
3777
 
    }
3778
 
    
3779
 
    /**
3780
 
     * Special case processor for the contents of script tags
3781
 
     * @warning This runs into problems if there's already a literal
3782
 
     *          --> somewhere inside the script contents.
3783
 
     */
3784
 
    public function generateScriptFromToken($token) {
3785
 
        if (!$token instanceof HTMLPurifier_Token_Text) return $this->generateFromToken($token);
3786
 
        // Thanks <http://lachy.id.au/log/2005/05/script-comments>
3787
 
        $data = preg_replace('#//\s*$#', '', $token->data);
3788
 
        return '<!--//--><![CDATA[//><!--' . "\n" . trim($data) . "\n" . '//--><!]]>';
3789
 
    }
3790
 
    
3791
 
    /**
3792
 
     * Generates attribute declarations from attribute array.
3793
 
     * @note This does not include the leading or trailing space.
3794
 
     * @param $assoc_array_of_attributes Attribute array
3795
 
     * @param $element Name of element attributes are for, used to check
3796
 
     *        attribute minimization.
3797
 
     * @return Generate HTML fragment for insertion.
3798
 
     */
3799
 
    public function generateAttributes($assoc_array_of_attributes, $element = false) {
3800
 
        $html = '';
3801
 
        if ($this->_sortAttr) ksort($assoc_array_of_attributes);
3802
 
        foreach ($assoc_array_of_attributes as $key => $value) {
3803
 
            if (!$this->_xhtml) {
3804
 
                // Remove namespaced attributes
3805
 
                if (strpos($key, ':') !== false) continue;
3806
 
                // Check if we should minimize the attribute: val="val" -> val
3807
 
                if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
3808
 
                    $html .= $key . ' ';
3809
 
                    continue;
3810
 
                }
3811
 
            }
3812
 
            $html .= $key.'="'.$this->escape($value).'" ';
3813
 
        }
3814
 
        return rtrim($html);
3815
 
    }
3816
 
    
3817
 
    /**
3818
 
     * Escapes raw text data.
3819
 
     * @todo This really ought to be protected, but until we have a facility
3820
 
     *       for properly generating HTML here w/o using tokens, it stays
3821
 
     *       public.
3822
 
     * @param $string String data to escape for HTML.
3823
 
     * @param $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
3824
 
     *               permissible for non-attribute output.
3825
 
     * @return String escaped data.
3826
 
     */
3827
 
    public function escape($string, $quote = ENT_COMPAT) {
3828
 
        return htmlspecialchars($string, $quote, 'UTF-8');
3829
 
    }
3830
 
    
3831
 
}
3832
 
 
3833
 
 
3834
 
 
3835
 
 
3836
 
/**
3837
 
 * Definition of the purified HTML that describes allowed children,
3838
 
 * attributes, and many other things.
3839
 
 * 
3840
 
 * Conventions:
3841
 
 * 
3842
 
 * All member variables that are prefixed with info
3843
 
 * (including the main $info array) are used by HTML Purifier internals
3844
 
 * and should not be directly edited when customizing the HTMLDefinition.
3845
 
 * They can usually be set via configuration directives or custom
3846
 
 * modules.
3847
 
 * 
3848
 
 * On the other hand, member variables without the info prefix are used
3849
 
 * internally by the HTMLDefinition and MUST NOT be used by other HTML
3850
 
 * Purifier internals. Many of them, however, are public, and may be
3851
 
 * edited by userspace code to tweak the behavior of HTMLDefinition.
3852
 
 * 
3853
 
 * @note This class is inspected by Printer_HTMLDefinition; please
3854
 
 *       update that class if things here change.
3855
 
 *
3856
 
 * @warning Directives that change this object's structure must be in
3857
 
 *          the HTML or Attr namespace!
3858
 
 */
3859
 
class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition
3860
 
{
3861
 
    
3862
 
    // FULLY-PUBLIC VARIABLES ---------------------------------------------
3863
 
    
3864
 
    /**
3865
 
     * Associative array of element names to HTMLPurifier_ElementDef
3866
 
     */
3867
 
    public $info = array();
3868
 
    
3869
 
    /**
3870
 
     * Associative array of global attribute name to attribute definition.
3871
 
     */
3872
 
    public $info_global_attr = array();
3873
 
    
3874
 
    /**
3875
 
     * String name of parent element HTML will be going into.
3876
 
     */
3877
 
    public $info_parent = 'div';
3878
 
    
3879
 
    /**
3880
 
     * Definition for parent element, allows parent element to be a
3881
 
     * tag that's not allowed inside the HTML fragment.
3882
 
     */
3883
 
    public $info_parent_def;
3884
 
    
3885
 
    /**
3886
 
     * String name of element used to wrap inline elements in block context
3887
 
     * @note This is rarely used except for BLOCKQUOTEs in strict mode
3888
 
     */
3889
 
    public $info_block_wrapper = 'p';
3890
 
    
3891
 
    /**
3892
 
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform
3893
 
     */
3894
 
    public $info_tag_transform = array();
3895
 
    
3896
 
    /**
3897
 
     * Indexed list of HTMLPurifier_AttrTransform to be performed before validation.
3898
 
     */
3899
 
    public $info_attr_transform_pre = array();
3900
 
    
3901
 
    /**
3902
 
     * Indexed list of HTMLPurifier_AttrTransform to be performed after validation.
3903
 
     */
3904
 
    public $info_attr_transform_post = array();
3905
 
    
3906
 
    /**
3907
 
     * Nested lookup array of content set name (Block, Inline) to
3908
 
     * element name to whether or not it belongs in that content set.
3909
 
     */
3910
 
    public $info_content_sets = array();
3911
 
    
3912
 
    /**
3913
 
     * Indexed list of HTMLPurifier_Injector to be used.
3914
 
     */
3915
 
    public $info_injector = array();
3916
 
    
3917
 
    /**
3918
 
     * Doctype object
3919
 
     */
3920
 
    public $doctype;
3921
 
    
3922
 
    
3923
 
    
3924
 
    // RAW CUSTOMIZATION STUFF --------------------------------------------
3925
 
    
3926
 
    /**
3927
 
     * Adds a custom attribute to a pre-existing element
3928
 
     * @note This is strictly convenience, and does not have a corresponding
3929
 
     *       method in HTMLPurifier_HTMLModule
3930
 
     * @param $element_name String element name to add attribute to
3931
 
     * @param $attr_name String name of attribute
3932
 
     * @param $def Attribute definition, can be string or object, see
3933
 
     *             HTMLPurifier_AttrTypes for details
3934
 
     */
3935
 
    public function addAttribute($element_name, $attr_name, $def) {
3936
 
        $module = $this->getAnonymousModule();
3937
 
        if (!isset($module->info[$element_name])) {
3938
 
            $element = $module->addBlankElement($element_name);
3939
 
        } else {
3940
 
            $element = $module->info[$element_name];
3941
 
        }
3942
 
        $element->attr[$attr_name] = $def;
3943
 
    }
3944
 
    
3945
 
    /**
3946
 
     * Adds a custom element to your HTML definition
3947
 
     * @note See HTMLPurifier_HTMLModule::addElement for detailed 
3948
 
     *       parameter and return value descriptions.
3949
 
     */
3950
 
    public function addElement($element_name, $type, $contents, $attr_collections, $attributes) {
3951
 
        $module = $this->getAnonymousModule();
3952
 
        // assume that if the user is calling this, the element
3953
 
        // is safe. This may not be a good idea
3954
 
        $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes);
3955
 
        return $element;
3956
 
    }
3957
 
    
3958
 
    /**
3959
 
     * Adds a blank element to your HTML definition, for overriding
3960
 
     * existing behavior
3961
 
     * @note See HTMLPurifier_HTMLModule::addBlankElement for detailed
3962
 
     *       parameter and return value descriptions.
3963
 
     */
3964
 
    public function addBlankElement($element_name) {
3965
 
        $module  = $this->getAnonymousModule();
3966
 
        $element = $module->addBlankElement($element_name);
3967
 
        return $element;
3968
 
    }
3969
 
    
3970
 
    /**
3971
 
     * Retrieves a reference to the anonymous module, so you can
3972
 
     * bust out advanced features without having to make your own
3973
 
     * module.
3974
 
     */
3975
 
    public function getAnonymousModule() {
3976
 
        if (!$this->_anonModule) {
3977
 
            $this->_anonModule = new HTMLPurifier_HTMLModule();
3978
 
            $this->_anonModule->name = 'Anonymous';
3979
 
        }
3980
 
        return $this->_anonModule;
3981
 
    }
3982
 
    
3983
 
    private $_anonModule;
3984
 
    
3985
 
    
3986
 
    // PUBLIC BUT INTERNAL VARIABLES --------------------------------------
3987
 
    
3988
 
    public $type = 'HTML';
3989
 
    public $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */
3990
 
    
3991
 
    /**
3992
 
     * Performs low-cost, preliminary initialization.
3993
 
     */
3994
 
    public function __construct() {
3995
 
        $this->manager = new HTMLPurifier_HTMLModuleManager();
3996
 
    }
3997
 
    
3998
 
    protected function doSetup($config) {
3999
 
        $this->processModules($config);
4000
 
        $this->setupConfigStuff($config);
4001
 
        unset($this->manager);
4002
 
        
4003
 
        // cleanup some of the element definitions
4004
 
        foreach ($this->info as $k => $v) {
4005
 
            unset($this->info[$k]->content_model);
4006
 
            unset($this->info[$k]->content_model_type);
4007
 
        }
4008
 
    }
4009
 
    
4010
 
    /**
4011
 
     * Extract out the information from the manager
4012
 
     */
4013
 
    protected function processModules($config) {
4014
 
        
4015
 
        if ($this->_anonModule) {
4016
 
            // for user specific changes
4017
 
            // this is late-loaded so we don't have to deal with PHP4
4018
 
            // reference wonky-ness
4019
 
            $this->manager->addModule($this->_anonModule);
4020
 
            unset($this->_anonModule);
4021
 
        }
4022
 
        
4023
 
        $this->manager->setup($config);
4024
 
        $this->doctype = $this->manager->doctype;
4025
 
        
4026
 
        foreach ($this->manager->modules as $module) {
4027
 
            foreach($module->info_tag_transform as $k => $v) {
4028
 
                if ($v === false) unset($this->info_tag_transform[$k]);
4029
 
                else $this->info_tag_transform[$k] = $v;
4030
 
            }
4031
 
            foreach($module->info_attr_transform_pre as $k => $v) {
4032
 
                if ($v === false) unset($this->info_attr_transform_pre[$k]);
4033
 
                else $this->info_attr_transform_pre[$k] = $v;
4034
 
            }
4035
 
            foreach($module->info_attr_transform_post as $k => $v) {
4036
 
                if ($v === false) unset($this->info_attr_transform_post[$k]);
4037
 
                else $this->info_attr_transform_post[$k] = $v;
4038
 
            }
4039
 
            foreach ($module->info_injector as $k => $v) {
4040
 
                if ($v === false) unset($this->info_injector[$k]);
4041
 
                else $this->info_injector[$k] = $v;
4042
 
            }
4043
 
        }
4044
 
        
4045
 
        $this->info = $this->manager->getElements();
4046
 
        $this->info_content_sets = $this->manager->contentSets->lookup;
4047
 
        
4048
 
    }
4049
 
    
4050
 
    /**
4051
 
     * Sets up stuff based on config. We need a better way of doing this.
4052
 
     */
4053
 
    protected function setupConfigStuff($config) {
4054
 
        
4055
 
        $block_wrapper = $config->get('HTML', 'BlockWrapper');
4056
 
        if (isset($this->info_content_sets['Block'][$block_wrapper])) {
4057
 
            $this->info_block_wrapper = $block_wrapper;
4058
 
        } else {
4059
 
            trigger_error('Cannot use non-block element as block wrapper',
4060
 
                E_USER_ERROR);
4061
 
        }
4062
 
        
4063
 
        $parent = $config->get('HTML', 'Parent');
4064
 
        $def = $this->manager->getElement($parent, true);
4065
 
        if ($def) {
4066
 
            $this->info_parent = $parent;
4067
 
            $this->info_parent_def = $def;
4068
 
        } else {
4069
 
            trigger_error('Cannot use unrecognized element as parent',
4070
 
                E_USER_ERROR);
4071
 
            $this->info_parent_def = $this->manager->getElement($this->info_parent, true);
4072
 
        }
4073
 
        
4074
 
        // support template text
4075
 
        $support = "(for information on implementing this, see the ".
4076
 
                   "support forums) ";
4077
 
        
4078
 
        // setup allowed elements -----------------------------------------
4079
 
        
4080
 
        $allowed_elements = $config->get('HTML', 'AllowedElements');
4081
 
        $allowed_attributes = $config->get('HTML', 'AllowedAttributes'); // retrieve early
4082
 
        
4083
 
        if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
4084
 
            $allowed = $config->get('HTML', 'Allowed');
4085
 
            if (is_string($allowed)) {
4086
 
                list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
4087
 
            }
4088
 
        }
4089
 
        
4090
 
        if (is_array($allowed_elements)) {
4091
 
            foreach ($this->info as $name => $d) {
4092
 
                if(!isset($allowed_elements[$name])) unset($this->info[$name]);
4093
 
                unset($allowed_elements[$name]);
4094
 
            }
4095
 
            // emit errors
4096
 
            foreach ($allowed_elements as $element => $d) {
4097
 
                $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful!
4098
 
                trigger_error("Element '$element' is not supported $support", E_USER_WARNING);
4099
 
            }
4100
 
        }
4101
 
        
4102
 
        // setup allowed attributes ---------------------------------------
4103
 
        
4104
 
        $allowed_attributes_mutable = $allowed_attributes; // by copy!
4105
 
        if (is_array($allowed_attributes)) {
4106
 
            
4107
 
            // This actually doesn't do anything, since we went away from
4108
 
            // global attributes. It's possible that userland code uses
4109
 
            // it, but HTMLModuleManager doesn't!
4110
 
            foreach ($this->info_global_attr as $attr => $x) {
4111
 
                $keys = array($attr, "*@$attr", "*.$attr");
4112
 
                $delete = true;
4113
 
                foreach ($keys as $key) {
4114
 
                    if ($delete && isset($allowed_attributes[$key])) {
4115
 
                        $delete = false;
4116
 
                    }
4117
 
                    if (isset($allowed_attributes_mutable[$key])) {
4118
 
                        unset($allowed_attributes_mutable[$key]);
4119
 
                    }
4120
 
                }
4121
 
                if ($delete) unset($this->info_global_attr[$attr]);
4122
 
            }
4123
 
            
4124
 
            foreach ($this->info as $tag => $info) {
4125
 
                foreach ($info->attr as $attr => $x) {
4126
 
                    $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr");
4127
 
                    $delete = true;
4128
 
                    foreach ($keys as $key) {
4129
 
                        if ($delete && isset($allowed_attributes[$key])) {
4130
 
                            $delete = false;
4131
 
                        }
4132
 
                        if (isset($allowed_attributes_mutable[$key])) {
4133
 
                            unset($allowed_attributes_mutable[$key]);
4134
 
                        }
4135
 
                    }
4136
 
                    if ($delete) unset($this->info[$tag]->attr[$attr]);
4137
 
                }
4138
 
            }
4139
 
            // emit errors
4140
 
            foreach ($allowed_attributes_mutable as $elattr => $d) {
4141
 
                $bits = preg_split('/[.@]/', $elattr, 2);
4142
 
                $c = count($bits);
4143
 
                switch ($c) {
4144
 
                    case 2:
4145
 
                        if ($bits[0] !== '*') {
4146
 
                            $element = htmlspecialchars($bits[0]);
4147
 
                            $attribute = htmlspecialchars($bits[1]);
4148
 
                            if (!isset($this->info[$element])) {
4149
 
                                trigger_error("Cannot allow attribute '$attribute' if element '$element' is not allowed/supported $support");
4150
 
                            } else {
4151
 
                                trigger_error("Attribute '$attribute' in element '$element' not supported $support",
4152
 
                                    E_USER_WARNING);
4153
 
                            }
4154
 
                            break;
4155
 
                        }
4156
 
                        // otherwise fall through
4157
 
                    case 1:
4158
 
                        $attribute = htmlspecialchars($bits[0]);
4159
 
                        trigger_error("Global attribute '$attribute' is not ".
4160
 
                            "supported in any elements $support",
4161
 
                            E_USER_WARNING);
4162
 
                        break;
4163
 
                }
4164
 
            }
4165
 
            
4166
 
        }
4167
 
        
4168
 
        // setup forbidden elements ---------------------------------------
4169
 
        
4170
 
        $forbidden_elements   = $config->get('HTML', 'ForbiddenElements');
4171
 
        $forbidden_attributes = $config->get('HTML', 'ForbiddenAttributes');
4172
 
        
4173
 
        foreach ($this->info as $tag => $info) {
4174
 
            if (isset($forbidden_elements[$tag])) {
4175
 
                unset($this->info[$tag]);
4176
 
                continue;
4177
 
            }
4178
 
            foreach ($info->attr as $attr => $x) {
4179
 
                if (
4180
 
                    isset($forbidden_attributes["$tag@$attr"]) ||
4181
 
                    isset($forbidden_attributes["*@$attr"]) ||
4182
 
                    isset($forbidden_attributes[$attr])
4183
 
                ) {
4184
 
                    unset($this->info[$tag]->attr[$attr]);
4185
 
                    continue;
4186
 
                } // this segment might get removed eventually
4187
 
                elseif (isset($forbidden_attributes["$tag.$attr"])) {
4188
 
                    // $tag.$attr are not user supplied, so no worries!
4189
 
                    trigger_error("Error with $tag.$attr: tag.attr syntax not supported for HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING);
4190
 
                }
4191
 
            }
4192
 
        }
4193
 
        foreach ($forbidden_attributes as $key => $v) {
4194
 
            if (strlen($key) < 2) continue;
4195
 
            if ($key[0] != '*') continue;
4196
 
            if ($key[1] == '.') {
4197
 
                trigger_error("Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING);
4198
 
            }
4199
 
        }
4200
 
        
4201
 
        // setup injectors -----------------------------------------------------
4202
 
        foreach ($this->info_injector as $i => $injector) {
4203
 
            if ($injector->checkNeeded($config) !== false) {
4204
 
                // remove injector that does not have it's required
4205
 
                // elements/attributes present, and is thus not needed.
4206
 
                unset($this->info_injector[$i]);
4207
 
            }
4208
 
        }
4209
 
    }
4210
 
    
4211
 
    /**
4212
 
     * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
4213
 
     * separate lists for processing. Format is element[attr1|attr2],element2...
4214
 
     * @warning Although it's largely drawn from TinyMCE's implementation,
4215
 
     *      it is different, and you'll probably have to modify your lists
4216
 
     * @param $list String list to parse
4217
 
     * @param array($allowed_elements, $allowed_attributes)
4218
 
     * @todo Give this its own class, probably static interface
4219
 
     */
4220
 
    public function parseTinyMCEAllowedList($list) {
4221
 
        
4222
 
        $list = str_replace(array(' ', "\t"), '', $list);
4223
 
        
4224
 
        $elements = array();
4225
 
        $attributes = array();
4226
 
        
4227
 
        $chunks = preg_split('/(,|[\n\r]+)/', $list);
4228
 
        foreach ($chunks as $chunk) {
4229
 
            if (empty($chunk)) continue;
4230
 
            // remove TinyMCE element control characters
4231
 
            if (!strpos($chunk, '[')) {
4232
 
                $element = $chunk;
4233
 
                $attr = false;
4234
 
            } else {
4235
 
                list($element, $attr) = explode('[', $chunk);
4236
 
            }
4237
 
            if ($element !== '*') $elements[$element] = true;
4238
 
            if (!$attr) continue;
4239
 
            $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ]
4240
 
            $attr = explode('|', $attr);
4241
 
            foreach ($attr as $key) {
4242
 
                $attributes["$element.$key"] = true;
4243
 
            }
4244
 
        }
4245
 
        
4246
 
        return array($elements, $attributes);
4247
 
        
4248
 
    }
4249
 
    
4250
 
    
4251
 
}
4252
 
 
4253
 
 
4254
 
 
4255
 
 
4256
 
 
4257
 
/**
4258
 
 * Represents an XHTML 1.1 module, with information on elements, tags
4259
 
 * and attributes.
4260
 
 * @note Even though this is technically XHTML 1.1, it is also used for
4261
 
 *       regular HTML parsing. We are using modulization as a convenient
4262
 
 *       way to represent the internals of HTMLDefinition, and our
4263
 
 *       implementation is by no means conforming and does not directly
4264
 
 *       use the normative DTDs or XML schemas.
4265
 
 * @note The public variables in a module should almost directly
4266
 
 *       correspond to the variables in HTMLPurifier_HTMLDefinition.
4267
 
 *       However, the prefix info carries no special meaning in these
4268
 
 *       objects (include it anyway if that's the correspondence though).
4269
 
 * @todo Consider making some member functions protected
4270
 
 */
4271
 
 
4272
 
class HTMLPurifier_HTMLModule
4273
 
{
4274
 
    
4275
 
    // -- Overloadable ----------------------------------------------------
4276
 
    
4277
 
    /**
4278
 
     * Short unique string identifier of the module
4279
 
     */
4280
 
    public $name;
4281
 
    
4282
 
    /**
4283
 
     * Informally, a list of elements this module changes. Not used in
4284
 
     * any significant way.
4285
 
     */
4286
 
    public $elements = array();
4287
 
    
4288
 
    /**
4289
 
     * Associative array of element names to element definitions.
4290
 
     * Some definitions may be incomplete, to be merged in later
4291
 
     * with the full definition.
4292
 
     */
4293
 
    public $info = array();
4294
 
    
4295
 
    /**
4296
 
     * Associative array of content set names to content set additions.
4297
 
     * This is commonly used to, say, add an A element to the Inline
4298
 
     * content set. This corresponds to an internal variable $content_sets
4299
 
     * and NOT info_content_sets member variable of HTMLDefinition.
4300
 
     */
4301
 
    public $content_sets = array();
4302
 
    
4303
 
    /**
4304
 
     * Associative array of attribute collection names to attribute
4305
 
     * collection additions. More rarely used for adding attributes to
4306
 
     * the global collections. Example is the StyleAttribute module adding
4307
 
     * the style attribute to the Core. Corresponds to HTMLDefinition's
4308
 
     * attr_collections->info, since the object's data is only info,
4309
 
     * with extra behavior associated with it.
4310
 
     */
4311
 
    public $attr_collections = array();
4312
 
    
4313
 
    /**
4314
 
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform
4315
 
     */
4316
 
    public $info_tag_transform = array();
4317
 
    
4318
 
    /**
4319
 
     * List of HTMLPurifier_AttrTransform to be performed before validation.
4320
 
     */
4321
 
    public $info_attr_transform_pre = array();
4322
 
    
4323
 
    /**
4324
 
     * List of HTMLPurifier_AttrTransform to be performed after validation.
4325
 
     */
4326
 
    public $info_attr_transform_post = array();
4327
 
    
4328
 
    /**
4329
 
     * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
4330
 
     * An injector will only be invoked if all of it's pre-requisites are met;
4331
 
     * if an injector fails setup, there will be no error; it will simply be
4332
 
     * silently disabled.
4333
 
     */
4334
 
    public $info_injector = array();
4335
 
    
4336
 
    /**
4337
 
     * Boolean flag that indicates whether or not getChildDef is implemented.
4338
 
     * For optimization reasons: may save a call to a function. Be sure
4339
 
     * to set it if you do implement getChildDef(), otherwise it will have
4340
 
     * no effect!
4341
 
     */
4342
 
    public $defines_child_def = false;
4343
 
    
4344
 
    /**
4345
 
     * Boolean flag whether or not this module is safe. If it is not safe, all
4346
 
     * of its members are unsafe. Modules are safe by default (this might be
4347
 
     * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
4348
 
     * which is based off of safe HTML, to explicitly say, "This is safe," even
4349
 
     * though there are modules which are "unsafe")
4350
 
     * 
4351
 
     * @note Previously, safety could be applied at an element level granularity.
4352
 
     *       We've removed this ability, so in order to add "unsafe" elements
4353
 
     *       or attributes, a dedicated module with this property set to false
4354
 
     *       must be used.
4355
 
     */
4356
 
    public $safe = true;
4357
 
    
4358
 
    /**
4359
 
     * Retrieves a proper HTMLPurifier_ChildDef subclass based on 
4360
 
     * content_model and content_model_type member variables of
4361
 
     * the HTMLPurifier_ElementDef class. There is a similar function
4362
 
     * in HTMLPurifier_HTMLDefinition.
4363
 
     * @param $def HTMLPurifier_ElementDef instance
4364
 
     * @return HTMLPurifier_ChildDef subclass
4365
 
     */
4366
 
    public function getChildDef($def) {return false;}
4367
 
    
4368
 
    // -- Convenience -----------------------------------------------------
4369
 
    
4370
 
    /**
4371
 
     * Convenience function that sets up a new element
4372
 
     * @param $element Name of element to add
4373
 
     * @param $type What content set should element be registered to?
4374
 
     *              Set as false to skip this step.
4375
 
     * @param $contents Allowed children in form of:
4376
 
     *              "$content_model_type: $content_model"
4377
 
     * @param $attr_includes What attribute collections to register to
4378
 
     *              element?
4379
 
     * @param $attr What unique attributes does the element define?
4380
 
     * @note See ElementDef for in-depth descriptions of these parameters.
4381
 
     * @return Created element definition object, so you 
4382
 
     *         can set advanced parameters
4383
 
     */
4384
 
    public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) {
4385
 
        $this->elements[] = $element;
4386
 
        // parse content_model
4387
 
        list($content_model_type, $content_model) = $this->parseContents($contents);
4388
 
        // merge in attribute inclusions
4389
 
        $this->mergeInAttrIncludes($attr, $attr_includes);
4390
 
        // add element to content sets
4391
 
        if ($type) $this->addElementToContentSet($element, $type);
4392
 
        // create element
4393
 
        $this->info[$element] = HTMLPurifier_ElementDef::create(
4394
 
            $content_model, $content_model_type, $attr
4395
 
        );
4396
 
        // literal object $contents means direct child manipulation
4397
 
        if (!is_string($contents)) $this->info[$element]->child = $contents;
4398
 
        return $this->info[$element];
4399
 
    }
4400
 
    
4401
 
    /**
4402
 
     * Convenience function that creates a totally blank, non-standalone
4403
 
     * element.
4404
 
     * @param $element Name of element to create
4405
 
     * @return Created element
4406
 
     */
4407
 
    public function addBlankElement($element) {
4408
 
        if (!isset($this->info[$element])) {
4409
 
            $this->elements[] = $element;
4410
 
            $this->info[$element] = new HTMLPurifier_ElementDef();
4411
 
            $this->info[$element]->standalone = false;
4412
 
        } else {
4413
 
            trigger_error("Definition for $element already exists in module, cannot redefine");
4414
 
        }
4415
 
        return $this->info[$element];
4416
 
    }
4417
 
    
4418
 
    /**
4419
 
     * Convenience function that registers an element to a content set
4420
 
     * @param Element to register
4421
 
     * @param Name content set (warning: case sensitive, usually upper-case
4422
 
     *        first letter)
4423
 
     */
4424
 
    public function addElementToContentSet($element, $type) {
4425
 
        if (!isset($this->content_sets[$type])) $this->content_sets[$type] = '';
4426
 
        else $this->content_sets[$type] .= ' | ';
4427
 
        $this->content_sets[$type] .= $element;
4428
 
    }
4429
 
    
4430
 
    /**
4431
 
     * Convenience function that transforms single-string contents
4432
 
     * into separate content model and content model type
4433
 
     * @param $contents Allowed children in form of:
4434
 
     *                  "$content_model_type: $content_model"
4435
 
     * @note If contents is an object, an array of two nulls will be
4436
 
     *       returned, and the callee needs to take the original $contents
4437
 
     *       and use it directly.
4438
 
     */
4439
 
    public function parseContents($contents) {
4440
 
        if (!is_string($contents)) return array(null, null); // defer
4441
 
        switch ($contents) {
4442
 
            // check for shorthand content model forms
4443
 
            case 'Empty':
4444
 
                return array('empty', '');
4445
 
            case 'Inline':
4446
 
                return array('optional', 'Inline | #PCDATA');
4447
 
            case 'Flow':
4448
 
                return array('optional', 'Flow | #PCDATA');
4449
 
        }
4450
 
        list($content_model_type, $content_model) = explode(':', $contents);
4451
 
        $content_model_type = strtolower(trim($content_model_type));
4452
 
        $content_model = trim($content_model);
4453
 
        return array($content_model_type, $content_model);
4454
 
    }
4455
 
    
4456
 
    /**
4457
 
     * Convenience function that merges a list of attribute includes into
4458
 
     * an attribute array.
4459
 
     * @param $attr Reference to attr array to modify
4460
 
     * @param $attr_includes Array of includes / string include to merge in
4461
 
     */
4462
 
    public function mergeInAttrIncludes(&$attr, $attr_includes) {
4463
 
        if (!is_array($attr_includes)) {
4464
 
            if (empty($attr_includes)) $attr_includes = array();
4465
 
            else $attr_includes = array($attr_includes);
4466
 
        }
4467
 
        $attr[0] = $attr_includes;
4468
 
    }
4469
 
    
4470
 
    /**
4471
 
     * Convenience function that generates a lookup table with boolean
4472
 
     * true as value.
4473
 
     * @param $list List of values to turn into a lookup
4474
 
     * @note You can also pass an arbitrary number of arguments in
4475
 
     *       place of the regular argument
4476
 
     * @return Lookup array equivalent of list
4477
 
     */
4478
 
    public function makeLookup($list) {
4479
 
        if (is_string($list)) $list = func_get_args();
4480
 
        $ret = array();
4481
 
        foreach ($list as $value) {
4482
 
            if (is_null($value)) continue;
4483
 
            $ret[$value] = true;
4484
 
        }
4485
 
        return $ret;
4486
 
    }
4487
 
    
4488
 
    /**
4489
 
     * Lazy load construction of the module after determining whether
4490
 
     * or not it's needed, and also when a finalized configuration object
4491
 
     * is available.
4492
 
     * @param $config Instance of HTMLPurifier_Config
4493
 
     */
4494
 
    public function setup($config) {}
4495
 
    
4496
 
}
4497
 
 
4498
 
 
4499
 
 
4500
 
 
4501
 
class HTMLPurifier_HTMLModuleManager
4502
 
{
4503
 
    
4504
 
    /**
4505
 
     * Instance of HTMLPurifier_DoctypeRegistry
4506
 
     */
4507
 
    public $doctypes;
4508
 
    
4509
 
    /**
4510
 
     * Instance of current doctype
4511
 
     */
4512
 
    public $doctype;
4513
 
    
4514
 
    /**
4515
 
     * Instance of HTMLPurifier_AttrTypes
4516
 
     */
4517
 
    public $attrTypes;
4518
 
    
4519
 
    /**
4520
 
     * Active instances of modules for the specified doctype are
4521
 
     * indexed, by name, in this array.
4522
 
     */
4523
 
    public $modules = array();
4524
 
    
4525
 
    /**
4526
 
     * Array of recognized HTMLPurifier_Module instances, indexed by 
4527
 
     * module's class name. This array is usually lazy loaded, but a
4528
 
     * user can overload a module by pre-emptively registering it.
4529
 
     */
4530
 
    public $registeredModules = array();
4531
 
    
4532
 
    /**
4533
 
     * List of extra modules that were added by the user using addModule().
4534
 
     * These get unconditionally merged into the current doctype, whatever
4535
 
     * it may be.
4536
 
     */
4537
 
    public $userModules = array();
4538
 
    
4539
 
    /**
4540
 
     * Associative array of element name to list of modules that have
4541
 
     * definitions for the element; this array is dynamically filled.
4542
 
     */
4543
 
    public $elementLookup = array();
4544
 
    
4545
 
    /** List of prefixes we should use for registering small names */
4546
 
    public $prefixes = array('HTMLPurifier_HTMLModule_');
4547
 
    
4548
 
    public $contentSets;     /**< Instance of HTMLPurifier_ContentSets */
4549
 
    public $attrCollections; /**< Instance of HTMLPurifier_AttrCollections */
4550
 
    
4551
 
    /** If set to true, unsafe elements and attributes will be allowed */
4552
 
    public $trusted = false;
4553
 
    
4554
 
    public function __construct() {
4555
 
        
4556
 
        // editable internal objects
4557
 
        $this->attrTypes = new HTMLPurifier_AttrTypes();
4558
 
        $this->doctypes  = new HTMLPurifier_DoctypeRegistry();
4559
 
        
4560
 
        // setup basic modules
4561
 
        $common = array(
4562
 
            'CommonAttributes', 'Text', 'Hypertext', 'List',
4563
 
            'Presentation', 'Edit', 'Bdo', 'Tables', 'Image',
4564
 
            'StyleAttribute',
4565
 
            // Unsafe:
4566
 
            'Scripting', 'Object',  'Forms',
4567
 
            // Sorta legacy, but present in strict:
4568
 
            'Name', 
4569
 
        );
4570
 
        $transitional = array('Legacy', 'Target');
4571
 
        $xml = array('XMLCommonAttributes');
4572
 
        $non_xml = array('NonXMLCommonAttributes');
4573
 
        
4574
 
        // setup basic doctypes
4575
 
        $this->doctypes->register(
4576
 
            'HTML 4.01 Transitional', false,
4577
 
            array_merge($common, $transitional, $non_xml),
4578
 
            array('Tidy_Transitional', 'Tidy_Proprietary'),
4579
 
            array(),
4580
 
            '-//W3C//DTD HTML 4.01 Transitional//EN',
4581
 
            'http://www.w3.org/TR/html4/loose.dtd'
4582
 
        );
4583
 
        
4584
 
        $this->doctypes->register(
4585
 
            'HTML 4.01 Strict', false,
4586
 
            array_merge($common, $non_xml),
4587
 
            array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
4588
 
            array(),
4589
 
            '-//W3C//DTD HTML 4.01//EN',
4590
 
            'http://www.w3.org/TR/html4/strict.dtd'
4591
 
        );
4592
 
        
4593
 
        $this->doctypes->register(
4594
 
            'XHTML 1.0 Transitional', true,
4595
 
            array_merge($common, $transitional, $xml, $non_xml),
4596
 
            array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'),
4597
 
            array(),
4598
 
            '-//W3C//DTD XHTML 1.0 Transitional//EN',
4599
 
            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
4600
 
        );
4601
 
        
4602
 
        $this->doctypes->register(
4603
 
            'XHTML 1.0 Strict', true,
4604
 
            array_merge($common, $xml, $non_xml),
4605
 
            array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
4606
 
            array(),
4607
 
            '-//W3C//DTD XHTML 1.0 Strict//EN',
4608
 
            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
4609
 
        );
4610
 
        
4611
 
        $this->doctypes->register(
4612
 
            'XHTML 1.1', true,
4613
 
            array_merge($common, $xml, array('Ruby')),
4614
 
            array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1
4615
 
            array(),
4616
 
            '-//W3C//DTD XHTML 1.1//EN',
4617
 
            'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
4618
 
        );
4619
 
        
4620
 
    }
4621
 
    
4622
 
    /**
4623
 
     * Registers a module to the recognized module list, useful for
4624
 
     * overloading pre-existing modules.
4625
 
     * @param $module Mixed: string module name, with or without
4626
 
     *                HTMLPurifier_HTMLModule prefix, or instance of
4627
 
     *                subclass of HTMLPurifier_HTMLModule.
4628
 
     * @param $overload Boolean whether or not to overload previous modules.
4629
 
     *                  If this is not set, and you do overload a module,
4630
 
     *                  HTML Purifier will complain with a warning.
4631
 
     * @note This function will not call autoload, you must instantiate
4632
 
     *       (and thus invoke) autoload outside the method.
4633
 
     * @note If a string is passed as a module name, different variants
4634
 
     *       will be tested in this order:
4635
 
     *          - Check for HTMLPurifier_HTMLModule_$name
4636
 
     *          - Check all prefixes with $name in order they were added
4637
 
     *          - Check for literal object name
4638
 
     *          - Throw fatal error
4639
 
     *       If your object name collides with an internal class, specify
4640
 
     *       your module manually. All modules must have been included
4641
 
     *       externally: registerModule will not perform inclusions for you!
4642
 
     */
4643
 
    public function registerModule($module, $overload = false) {
4644
 
        if (is_string($module)) {
4645
 
            // attempt to load the module
4646
 
            $original_module = $module;
4647
 
            $ok = false;
4648
 
            foreach ($this->prefixes as $prefix) {
4649
 
                $module = $prefix . $original_module;
4650
 
                if (class_exists($module)) {
4651
 
                    $ok = true;
4652
 
                    break;
4653
 
                }
4654
 
            }
4655
 
            if (!$ok) {
4656
 
                $module = $original_module;
4657
 
                if (!class_exists($module)) {
4658
 
                    trigger_error($original_module . ' module does not exist',
4659
 
                        E_USER_ERROR);
4660
 
                    return;
4661
 
                }
4662
 
            }
4663
 
            $module = new $module();
4664
 
        }
4665
 
        if (empty($module->name)) {
4666
 
            trigger_error('Module instance of ' . get_class($module) . ' must have name');
4667
 
            return;
4668
 
        }
4669
 
        if (!$overload && isset($this->registeredModules[$module->name])) {
4670
 
            trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING);
4671
 
        }
4672
 
        $this->registeredModules[$module->name] = $module;
4673
 
    }
4674
 
    
4675
 
    /**
4676
 
     * Adds a module to the current doctype by first registering it,
4677
 
     * and then tacking it on to the active doctype
4678
 
     */
4679
 
    public function addModule($module) {
4680
 
        $this->registerModule($module);
4681
 
        if (is_object($module)) $module = $module->name;
4682
 
        $this->userModules[] = $module;
4683
 
    }
4684
 
    
4685
 
    /**
4686
 
     * Adds a class prefix that registerModule() will use to resolve a
4687
 
     * string name to a concrete class
4688
 
     */
4689
 
    public function addPrefix($prefix) {
4690
 
        $this->prefixes[] = $prefix;
4691
 
    }
4692
 
    
4693
 
    /**
4694
 
     * Performs processing on modules, after being called you may
4695
 
     * use getElement() and getElements()
4696
 
     * @param $config Instance of HTMLPurifier_Config
4697
 
     */
4698
 
    public function setup($config) {
4699
 
        
4700
 
        $this->trusted = $config->get('HTML', 'Trusted');
4701
 
        
4702
 
        // generate
4703
 
        $this->doctype = $this->doctypes->make($config);
4704
 
        $modules = $this->doctype->modules;
4705
 
        
4706
 
        // take out the default modules that aren't allowed
4707
 
        $lookup = $config->get('HTML', 'AllowedModules');
4708
 
        $special_cases = $config->get('HTML', 'CoreModules');
4709
 
        
4710
 
        if (is_array($lookup)) {
4711
 
            foreach ($modules as $k => $m) {
4712
 
                if (isset($special_cases[$m])) continue;
4713
 
                if (!isset($lookup[$m])) unset($modules[$k]);
4714
 
            }
4715
 
        }
4716
 
        
4717
 
        // add proprietary module (this gets special treatment because
4718
 
        // it is completely removed from doctypes, etc.)
4719
 
        if ($config->get('HTML', 'Proprietary')) {
4720
 
            $modules[] = 'Proprietary';
4721
 
        }
4722
 
        
4723
 
        // add SafeObject/Safeembed modules
4724
 
        if ($config->get('HTML', 'SafeObject')) {
4725
 
            $modules[] = 'SafeObject';
4726
 
        }
4727
 
        if ($config->get('HTML', 'SafeEmbed')) {
4728
 
            $modules[] = 'SafeEmbed';
4729
 
        }
4730
 
        
4731
 
        // merge in custom modules
4732
 
        $modules = array_merge($modules, $this->userModules);
4733
 
        
4734
 
        foreach ($modules as $module) {
4735
 
            $this->processModule($module);
4736
 
            $this->modules[$module]->setup($config);
4737
 
        }
4738
 
        
4739
 
        foreach ($this->doctype->tidyModules as $module) {
4740
 
            $this->processModule($module);
4741
 
            $this->modules[$module]->setup($config);
4742
 
        }
4743
 
        
4744
 
        // prepare any injectors
4745
 
        foreach ($this->modules as $module) {
4746
 
            $n = array();
4747
 
            foreach ($module->info_injector as $i => $injector) {
4748
 
                if (!is_object($injector)) {
4749
 
                    $class = "HTMLPurifier_Injector_$injector";
4750
 
                    $injector = new $class;
4751
 
                }
4752
 
                $n[$injector->name] = $injector;
4753
 
            }
4754
 
            $module->info_injector = $n;
4755
 
        }
4756
 
        
4757
 
        // setup lookup table based on all valid modules
4758
 
        foreach ($this->modules as $module) {
4759
 
            foreach ($module->info as $name => $def) {
4760
 
                if (!isset($this->elementLookup[$name])) {
4761
 
                    $this->elementLookup[$name] = array();
4762
 
                }
4763
 
                $this->elementLookup[$name][] = $module->name;
4764
 
            }
4765
 
        }
4766
 
        
4767
 
        // note the different choice
4768
 
        $this->contentSets = new HTMLPurifier_ContentSets(
4769
 
            // content set assembly deals with all possible modules,
4770
 
            // not just ones deemed to be "safe"
4771
 
            $this->modules
4772
 
        );
4773
 
        $this->attrCollections = new HTMLPurifier_AttrCollections(
4774
 
            $this->attrTypes,
4775
 
            // there is no way to directly disable a global attribute,
4776
 
            // but using AllowedAttributes or simply not including
4777
 
            // the module in your custom doctype should be sufficient
4778
 
            $this->modules
4779
 
        );
4780
 
    }
4781
 
    
4782
 
    /**
4783
 
     * Takes a module and adds it to the active module collection,
4784
 
     * registering it if necessary.
4785
 
     */
4786
 
    public function processModule($module) {
4787
 
        if (!isset($this->registeredModules[$module]) || is_object($module)) {
4788
 
            $this->registerModule($module);
4789
 
        }
4790
 
        $this->modules[$module] = $this->registeredModules[$module];
4791
 
    }
4792
 
    
4793
 
    /**
4794
 
     * Retrieves merged element definitions.
4795
 
     * @return Array of HTMLPurifier_ElementDef
4796
 
     */
4797
 
    public function getElements() {
4798
 
        
4799
 
        $elements = array();
4800
 
        foreach ($this->modules as $module) {
4801
 
            if (!$this->trusted && !$module->safe) continue;
4802
 
            foreach ($module->info as $name => $v) {
4803
 
                if (isset($elements[$name])) continue;
4804
 
                $elements[$name] = $this->getElement($name);
4805
 
            }
4806
 
        }
4807
 
        
4808
 
        // remove dud elements, this happens when an element that
4809
 
        // appeared to be safe actually wasn't
4810
 
        foreach ($elements as $n => $v) {
4811
 
            if ($v === false) unset($elements[$n]);
4812
 
        }
4813
 
        
4814
 
        return $elements;
4815
 
        
4816
 
    }
4817
 
    
4818
 
    /**
4819
 
     * Retrieves a single merged element definition
4820
 
     * @param $name Name of element
4821
 
     * @param $trusted Boolean trusted overriding parameter: set to true
4822
 
     *                 if you want the full version of an element
4823
 
     * @return Merged HTMLPurifier_ElementDef
4824
 
     * @note You may notice that modules are getting iterated over twice (once
4825
 
     *       in getElements() and once here). This
4826
 
     *       is because 
4827
 
     */
4828
 
    public function getElement($name, $trusted = null) {
4829
 
        
4830
 
        if (!isset($this->elementLookup[$name])) {
4831
 
            return false;
4832
 
        }
4833
 
        
4834
 
        // setup global state variables
4835
 
        $def = false;
4836
 
        if ($trusted === null) $trusted = $this->trusted;
4837
 
        
4838
 
        // iterate through each module that has registered itself to this
4839
 
        // element
4840
 
        foreach($this->elementLookup[$name] as $module_name) {
4841
 
            
4842
 
            $module = $this->modules[$module_name];
4843
 
            
4844
 
            // refuse to create/merge from a module that is deemed unsafe--
4845
 
            // pretend the module doesn't exist--when trusted mode is not on.
4846
 
            if (!$trusted && !$module->safe) {
4847
 
                continue;
4848
 
            }
4849
 
            
4850
 
            // clone is used because, ideally speaking, the original
4851
 
            // definition should not be modified. Usually, this will
4852
 
            // make no difference, but for consistency's sake
4853
 
            $new_def = clone $module->info[$name];
4854
 
            
4855
 
            if (!$def && $new_def->standalone) {
4856
 
                $def = $new_def;
4857
 
            } elseif ($def) {
4858
 
                // This will occur even if $new_def is standalone. In practice,
4859
 
                // this will usually result in a full replacement.
4860
 
                $def->mergeIn($new_def);
4861
 
            } else {
4862
 
                // :TODO:
4863
 
                // non-standalone definitions that don't have a standalone
4864
 
                // to merge into could be deferred to the end
4865
 
                continue;
4866
 
            }
4867
 
            
4868
 
            // attribute value expansions
4869
 
            $this->attrCollections->performInclusions($def->attr);
4870
 
            $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes);
4871
 
            
4872
 
            // descendants_are_inline, for ChildDef_Chameleon
4873
 
            if (is_string($def->content_model) &&
4874
 
                strpos($def->content_model, 'Inline') !== false) {
4875
 
                if ($name != 'del' && $name != 'ins') {
4876
 
                    // this is for you, ins/del
4877
 
                    $def->descendants_are_inline = true;
4878
 
                }
4879
 
            }
4880
 
            
4881
 
            $this->contentSets->generateChildDef($def, $module);
4882
 
        }
4883
 
        
4884
 
        // This can occur if there is a blank definition, but no base to
4885
 
        // mix it in with
4886
 
        if (!$def) return false;
4887
 
        
4888
 
        // add information on required attributes
4889
 
        foreach ($def->attr as $attr_name => $attr_def) {
4890
 
            if ($attr_def->required) {
4891
 
                $def->required_attr[] = $attr_name;
4892
 
            }
4893
 
        }
4894
 
        
4895
 
        return $def;
4896
 
        
4897
 
    }
4898
 
    
4899
 
}
4900
 
 
4901
 
 
4902
 
 
4903
 
 
4904
 
 
4905
 
/**
4906
 
 * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes
4907
 
 * @note In Slashdot-speak, dupe means duplicate.
4908
 
 * @note The default constructor does not accept $config or $context objects:
4909
 
 *       use must use the static build() factory method to perform initialization.
4910
 
 */
4911
 
class HTMLPurifier_IDAccumulator
4912
 
{
4913
 
    
4914
 
    /**
4915
 
     * Lookup table of IDs we've accumulated.
4916
 
     * @public
4917
 
     */
4918
 
    public $ids = array();
4919
 
    
4920
 
    /**
4921
 
     * Builds an IDAccumulator, also initializing the default blacklist
4922
 
     * @param $config Instance of HTMLPurifier_Config
4923
 
     * @param $context Instance of HTMLPurifier_Context
4924
 
     * @return Fully initialized HTMLPurifier_IDAccumulator
4925
 
     */
4926
 
    public static function build($config, $context) {
4927
 
        $id_accumulator = new HTMLPurifier_IDAccumulator();
4928
 
        $id_accumulator->load($config->get('Attr', 'IDBlacklist'));
4929
 
        return $id_accumulator;
4930
 
    }
4931
 
    
4932
 
    /**
4933
 
     * Add an ID to the lookup table.
4934
 
     * @param $id ID to be added.
4935
 
     * @return Bool status, true if success, false if there's a dupe
4936
 
     */
4937
 
    public function add($id) {
4938
 
        if (isset($this->ids[$id])) return false;
4939
 
        return $this->ids[$id] = true;
4940
 
    }
4941
 
    
4942
 
    /**
4943
 
     * Load a list of IDs into the lookup table
4944
 
     * @param $array_of_ids Array of IDs to load
4945
 
     * @note This function doesn't care about duplicates
4946
 
     */
4947
 
    public function load($array_of_ids) {
4948
 
        foreach ($array_of_ids as $id) {
4949
 
            $this->ids[$id] = true;
4950
 
        }
4951
 
    }
4952
 
    
4953
 
}
4954
 
 
4955
 
 
4956
 
 
4957
 
 
4958
 
/**
4959
 
 * Injects tokens into the document while parsing for well-formedness.
4960
 
 * This enables "formatter-like" functionality such as auto-paragraphing,
4961
 
 * smiley-ification and linkification to take place.
4962
 
 * 
4963
 
 * A note on how handlers create changes; this is done by assigning a new
4964
 
 * value to the $token reference. These values can take a variety of forms and
4965
 
 * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken()
4966
 
 * documentation.
4967
 
 * 
4968
 
 * @todo Allow injectors to request a re-run on their output. This 
4969
 
 *       would help if an operation is recursive.
4970
 
 */
4971
 
abstract class HTMLPurifier_Injector
4972
 
{
4973
 
    
4974
 
    /**
4975
 
     * Advisory name of injector, this is for friendly error messages
4976
 
     */
4977
 
    public $name;
4978
 
    
4979
 
    /**
4980
 
     * Instance of HTMLPurifier_HTMLDefinition
4981
 
     */
4982
 
    protected $htmlDefinition;
4983
 
    
4984
 
    /**
4985
 
     * Reference to CurrentNesting variable in Context. This is an array
4986
 
     * list of tokens that we are currently "inside"
4987
 
     */
4988
 
    protected $currentNesting;
4989
 
    
4990
 
    /**
4991
 
     * Reference to InputTokens variable in Context. This is an array
4992
 
     * list of the input tokens that are being processed.
4993
 
     */
4994
 
    protected $inputTokens;
4995
 
    
4996
 
    /**
4997
 
     * Reference to InputIndex variable in Context. This is an integer
4998
 
     * array index for $this->inputTokens that indicates what token
4999
 
     * is currently being processed.
5000
 
     */
5001
 
    protected $inputIndex;
5002
 
    
5003
 
    /**
5004
 
     * Array of elements and attributes this injector creates and therefore
5005
 
     * need to be allowed by the definition. Takes form of
5006
 
     * array('element' => array('attr', 'attr2'), 'element2')
5007
 
     */
5008
 
    public $needed = array();
5009
 
    
5010
 
    /**
5011
 
     * Index of inputTokens to rewind to.
5012
 
     */
5013
 
    protected $rewind = false;
5014
 
    
5015
 
    /**
5016
 
     * Rewind to a spot to re-perform processing. This is useful if you
5017
 
     * deleted a node, and now need to see if this change affected any
5018
 
     * earlier nodes. Rewinding does not affect other injectors, and can
5019
 
     * result in infinite loops if not used carefully.
5020
 
     * @warning HTML Purifier will prevent you from fast-forwarding with this
5021
 
     *          function.
5022
 
     */
5023
 
    public function rewind($index) {
5024
 
        $this->rewind = $index;
5025
 
    }
5026
 
    
5027
 
    /**
5028
 
     * Retrieves rewind, and then unsets it.
5029
 
     */
5030
 
    public function getRewind() {
5031
 
        $r = $this->rewind;
5032
 
        $this->rewind = false;
5033
 
        return $r;
5034
 
    }
5035
 
    
5036
 
    /**
5037
 
     * Prepares the injector by giving it the config and context objects:
5038
 
     * this allows references to important variables to be made within
5039
 
     * the injector. This function also checks if the HTML environment
5040
 
     * will work with the Injector (see checkNeeded()).
5041
 
     * @param $config Instance of HTMLPurifier_Config
5042
 
     * @param $context Instance of HTMLPurifier_Context
5043
 
     * @return Boolean false if success, string of missing needed element/attribute if failure
5044
 
     */
5045
 
    public function prepare($config, $context) {
5046
 
        $this->htmlDefinition = $config->getHTMLDefinition();
5047
 
        // Even though this might fail, some unit tests ignore this and
5048
 
        // still test checkNeeded, so be careful. Maybe get rid of that
5049
 
        // dependency.
5050
 
        $result = $this->checkNeeded($config);
5051
 
        if ($result !== false) return $result;
5052
 
        $this->currentNesting =& $context->get('CurrentNesting');
5053
 
        $this->inputTokens    =& $context->get('InputTokens');
5054
 
        $this->inputIndex     =& $context->get('InputIndex');
5055
 
        return false;
5056
 
    }
5057
 
    
5058
 
    /**
5059
 
     * This function checks if the HTML environment
5060
 
     * will work with the Injector: if p tags are not allowed, the
5061
 
     * Auto-Paragraphing injector should not be enabled.
5062
 
     * @param $config Instance of HTMLPurifier_Config
5063
 
     * @param $context Instance of HTMLPurifier_Context
5064
 
     * @return Boolean false if success, string of missing needed element/attribute if failure
5065
 
     */
5066
 
    public function checkNeeded($config) {
5067
 
        $def = $config->getHTMLDefinition();
5068
 
        foreach ($this->needed as $element => $attributes) {
5069
 
            if (is_int($element)) $element = $attributes;
5070
 
            if (!isset($def->info[$element])) return $element;
5071
 
            if (!is_array($attributes)) continue;
5072
 
            foreach ($attributes as $name) {
5073
 
                if (!isset($def->info[$element]->attr[$name])) return "$element.$name";
5074
 
            }
5075
 
        }
5076
 
        return false;
5077
 
    }
5078
 
    
5079
 
    /**
5080
 
     * Tests if the context node allows a certain element
5081
 
     * @param $name Name of element to test for
5082
 
     * @return True if element is allowed, false if it is not
5083
 
     */
5084
 
    public function allowsElement($name) {
5085
 
        if (!empty($this->currentNesting)) {
5086
 
            $parent_token = array_pop($this->currentNesting);
5087
 
            $this->currentNesting[] = $parent_token;
5088
 
            $parent = $this->htmlDefinition->info[$parent_token->name];
5089
 
        } else {
5090
 
            $parent = $this->htmlDefinition->info_parent_def;
5091
 
        }
5092
 
        if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) {
5093
 
            return false;
5094
 
        }
5095
 
        return true;
5096
 
    }
5097
 
    
5098
 
    /**
5099
 
     * Iterator function, which starts with the next token and continues until
5100
 
     * you reach the end of the input tokens.
5101
 
     * @warning Please prevent previous references from interfering with this
5102
 
     *          functions by setting $i = null beforehand!
5103
 
     * @param &$i Current integer index variable for inputTokens
5104
 
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
5105
 
     */
5106
 
    protected function forward(&$i, &$current) {
5107
 
        if ($i === null) $i = $this->inputIndex + 1;
5108
 
        else $i++;
5109
 
        if (!isset($this->inputTokens[$i])) return false;
5110
 
        $current = $this->inputTokens[$i];
5111
 
        return true;
5112
 
    }
5113
 
    
5114
 
    /**
5115
 
     * Similar to _forward, but accepts a third parameter $nesting (which
5116
 
     * should be initialized at 0) and stops when we hit the end tag
5117
 
     * for the node $this->inputIndex starts in.
5118
 
     */
5119
 
    protected function forwardUntilEndToken(&$i, &$current, &$nesting) {
5120
 
        $result = $this->forward($i, $current);
5121
 
        if (!$result) return false;
5122
 
        if ($nesting === null) $nesting = 0;
5123
 
        if     ($current instanceof HTMLPurifier_Token_Start) $nesting++;
5124
 
        elseif ($current instanceof HTMLPurifier_Token_End) {
5125
 
            if ($nesting <= 0) return false;
5126
 
            $nesting--;
5127
 
        }
5128
 
        return true;
5129
 
    }
5130
 
    
5131
 
    /**
5132
 
     * Iterator function, starts with the previous token and continues until
5133
 
     * you reach the beginning of input tokens.
5134
 
     * @warning Please prevent previous references from interfering with this
5135
 
     *          functions by setting $i = null beforehand!
5136
 
     * @param &$i Current integer index variable for inputTokens
5137
 
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
5138
 
     */
5139
 
    protected function backward(&$i, &$current) {
5140
 
        if ($i === null) $i = $this->inputIndex - 1;
5141
 
        else $i--;
5142
 
        if ($i < 0) return false;
5143
 
        $current = $this->inputTokens[$i];
5144
 
        return true;
5145
 
    }
5146
 
    
5147
 
    /**
5148
 
     * Initializes the iterator at the current position. Use in a do {} while;
5149
 
     * loop to force the _forward and _backward functions to start at the
5150
 
     * current location.
5151
 
     * @warning Please prevent previous references from interfering with this
5152
 
     *          functions by setting $i = null beforehand!
5153
 
     * @param &$i Current integer index variable for inputTokens
5154
 
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
5155
 
     */
5156
 
    protected function current(&$i, &$current) {
5157
 
        if ($i === null) $i = $this->inputIndex;
5158
 
        $current = $this->inputTokens[$i];
5159
 
    }
5160
 
    
5161
 
    /**
5162
 
     * Handler that is called when a text token is processed
5163
 
     */
5164
 
    public function handleText(&$token) {}
5165
 
    
5166
 
    /**
5167
 
     * Handler that is called when a start or empty token is processed
5168
 
     */
5169
 
    public function handleElement(&$token) {}
5170
 
    
5171
 
    /**
5172
 
     * Handler that is called when an end token is processed
5173
 
     */
5174
 
    public function handleEnd(&$token) {
5175
 
        $this->notifyEnd($token);
5176
 
    }
5177
 
    
5178
 
    /**
5179
 
     * Notifier that is called when an end token is processed
5180
 
     * @note This differs from handlers in that the token is read-only
5181
 
     * @deprecated
5182
 
     */
5183
 
    public function notifyEnd($token) {}
5184
 
    
5185
 
    
5186
 
}
5187
 
 
5188
 
 
5189
 
 
5190
 
 
5191
 
/**
5192
 
 * Represents a language and defines localizable string formatting and
5193
 
 * other functions, as well as the localized messages for HTML Purifier.
5194
 
 */
5195
 
class HTMLPurifier_Language
5196
 
{
5197
 
    
5198
 
    /**
5199
 
     * ISO 639 language code of language. Prefers shortest possible version
5200
 
     */
5201
 
    public $code = 'en';
5202
 
    
5203
 
    /**
5204
 
     * Fallback language code
5205
 
     */
5206
 
    public $fallback = false;
5207
 
    
5208
 
    /**
5209
 
     * Array of localizable messages
5210
 
     */
5211
 
    public $messages = array();
5212
 
    
5213
 
    /**
5214
 
     * Array of localizable error codes
5215
 
     */
5216
 
    public $errorNames = array();
5217
 
    
5218
 
    /**
5219
 
     * True if no message file was found for this language, so English
5220
 
     * is being used instead. Check this if you'd like to notify the
5221
 
     * user that they've used a non-supported language.
5222
 
     */
5223
 
    public $error = false;
5224
 
    
5225
 
    /**
5226
 
     * Has the language object been loaded yet?
5227
 
     * @todo Make it private, fix usage in HTMLPurifier_LanguageTest
5228
 
     */
5229
 
    public $_loaded = false;
5230
 
    
5231
 
    /**
5232
 
     * Instances of HTMLPurifier_Config and HTMLPurifier_Context
5233
 
     */
5234
 
    protected $config, $context;
5235
 
    
5236
 
    public function __construct($config, $context) {
5237
 
        $this->config  = $config;
5238
 
        $this->context = $context;
5239
 
    }
5240
 
    
5241
 
    /**
5242
 
     * Loads language object with necessary info from factory cache
5243
 
     * @note This is a lazy loader
5244
 
     */
5245
 
    public function load() {
5246
 
        if ($this->_loaded) return;
5247
 
        $factory = HTMLPurifier_LanguageFactory::instance();
5248
 
        $factory->loadLanguage($this->code);
5249
 
        foreach ($factory->keys as $key) {
5250
 
            $this->$key = $factory->cache[$this->code][$key];
5251
 
        }
5252
 
        $this->_loaded = true;
5253
 
    }
5254
 
    
5255
 
    /**
5256
 
     * Retrieves a localised message.
5257
 
     * @param $key string identifier of message
5258
 
     * @return string localised message
5259
 
     */
5260
 
    public function getMessage($key) {
5261
 
        if (!$this->_loaded) $this->load();
5262
 
        if (!isset($this->messages[$key])) return "[$key]";
5263
 
        return $this->messages[$key];
5264
 
    }
5265
 
    
5266
 
    /**
5267
 
     * Retrieves a localised error name.
5268
 
     * @param $int integer error number, corresponding to PHP's error
5269
 
     *             reporting
5270
 
     * @return string localised message
5271
 
     */
5272
 
    public function getErrorName($int) {
5273
 
        if (!$this->_loaded) $this->load();
5274
 
        if (!isset($this->errorNames[$int])) return "[Error: $int]";
5275
 
        return $this->errorNames[$int];
5276
 
    }
5277
 
    
5278
 
    /**
5279
 
     * Converts an array list into a string readable representation
5280
 
     */
5281
 
    public function listify($array) {
5282
 
        $sep      = $this->getMessage('Item separator');
5283
 
        $sep_last = $this->getMessage('Item separator last');
5284
 
        $ret = '';
5285
 
        for ($i = 0, $c = count($array); $i < $c; $i++) {
5286
 
            if ($i == 0) {
5287
 
            } elseif ($i + 1 < $c) {
5288
 
                $ret .= $sep;
5289
 
            } else {
5290
 
                $ret .= $sep_last;
5291
 
            }
5292
 
            $ret .= $array[$i];
5293
 
        }
5294
 
        return $ret;
5295
 
    }
5296
 
    
5297
 
    /**
5298
 
     * Formats a localised message with passed parameters
5299
 
     * @param $key string identifier of message
5300
 
     * @param $args Parameters to substitute in
5301
 
     * @return string localised message
5302
 
     * @todo Implement conditionals? Right now, some messages make
5303
 
     *     reference to line numbers, but those aren't always available
5304
 
     */
5305
 
    public function formatMessage($key, $args = array()) {
5306
 
        if (!$this->_loaded) $this->load();
5307
 
        if (!isset($this->messages[$key])) return "[$key]";
5308
 
        $raw = $this->messages[$key];
5309
 
        $subst = array();
5310
 
        $generator = false;
5311
 
        foreach ($args as $i => $value) {
5312
 
            if (is_object($value)) {
5313
 
                if ($value instanceof HTMLPurifier_Token) {
5314
 
                    // factor this out some time
5315
 
                    if (!$generator) $generator = $this->context->get('Generator');
5316
 
                    if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
5317
 
                    if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
5318
 
                    $subst['$'.$i.'.Compact'] = 
5319
 
                    $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
5320
 
                    // a more complex algorithm for compact representation
5321
 
                    // could be introduced for all types of tokens. This
5322
 
                    // may need to be factored out into a dedicated class
5323
 
                    if (!empty($value->attr)) {
5324
 
                        $stripped_token = clone $value;
5325
 
                        $stripped_token->attr = array();
5326
 
                        $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
5327
 
                    }
5328
 
                    $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
5329
 
                }
5330
 
                continue;
5331
 
            } elseif (is_array($value)) {
5332
 
                $keys = array_keys($value);
5333
 
                if (array_keys($keys) === $keys) {
5334
 
                    // list
5335
 
                    $subst['$'.$i] = $this->listify($value);
5336
 
                } else {
5337
 
                    // associative array
5338
 
                    // no $i implementation yet, sorry
5339
 
                    $subst['$'.$i.'.Keys'] = $this->listify($keys);
5340
 
                    $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
5341
 
                }
5342
 
                continue;
5343
 
            }
5344
 
            $subst['$' . $i] = $value;
5345
 
        }
5346
 
        return strtr($raw, $subst);
5347
 
    }
5348
 
    
5349
 
}
5350
 
 
5351
 
 
5352
 
 
5353
 
 
5354
 
/**
5355
 
 * Class responsible for generating HTMLPurifier_Language objects, managing
5356
 
 * caching and fallbacks.
5357
 
 * @note Thanks to MediaWiki for the general logic, although this version
5358
 
 *       has been entirely rewritten
5359
 
 * @todo Serialized cache for languages
5360
 
 */
5361
 
class HTMLPurifier_LanguageFactory
5362
 
{
5363
 
    
5364
 
    /**
5365
 
     * Cache of language code information used to load HTMLPurifier_Language objects
5366
 
     * Structure is: $factory->cache[$language_code][$key] = $value
5367
 
     * @value array map
5368
 
     */
5369
 
    public $cache;
5370
 
    
5371
 
    /**
5372
 
     * Valid keys in the HTMLPurifier_Language object. Designates which
5373
 
     * variables to slurp out of a message file.
5374
 
     * @value array list
5375
 
     */
5376
 
    public $keys = array('fallback', 'messages', 'errorNames');
5377
 
    
5378
 
    /**
5379
 
     * Instance of HTMLPurifier_AttrDef_Lang to validate language codes
5380
 
     * @value object HTMLPurifier_AttrDef_Lang
5381
 
     */
5382
 
    protected $validator;
5383
 
    
5384
 
    /**
5385
 
     * Cached copy of dirname(__FILE__), directory of current file without
5386
 
     * trailing slash
5387
 
     * @value string filename
5388
 
     */
5389
 
    protected $dir;
5390
 
    
5391
 
    /**
5392
 
     * Keys whose contents are a hash map and can be merged
5393
 
     * @value array lookup
5394
 
     */
5395
 
    protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true);
5396
 
    
5397
 
    /**
5398
 
     * Keys whose contents are a list and can be merged
5399
 
     * @value array lookup
5400
 
     */
5401
 
    protected $mergeable_keys_list = array();
5402
 
    
5403
 
    /**
5404
 
     * Retrieve sole instance of the factory.
5405
 
     * @param $prototype Optional prototype to overload sole instance with,
5406
 
     *                   or bool true to reset to default factory.
5407
 
     */
5408
 
    public static function instance($prototype = null) {
5409
 
        static $instance = null;
5410
 
        if ($prototype !== null) {
5411
 
            $instance = $prototype;
5412
 
        } elseif ($instance === null || $prototype == true) {
5413
 
            $instance = new HTMLPurifier_LanguageFactory();
5414
 
            $instance->setup();
5415
 
        }
5416
 
        return $instance;
5417
 
    }
5418
 
    
5419
 
    /**
5420
 
     * Sets up the singleton, much like a constructor
5421
 
     * @note Prevents people from getting this outside of the singleton
5422
 
     */
5423
 
    public function setup() {
5424
 
        $this->validator = new HTMLPurifier_AttrDef_Lang();
5425
 
        $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier';
5426
 
    }
5427
 
    
5428
 
    /**
5429
 
     * Creates a language object, handles class fallbacks
5430
 
     * @param $config Instance of HTMLPurifier_Config
5431
 
     * @param $context Instance of HTMLPurifier_Context
5432
 
     * @param $code Code to override configuration with. Private parameter.
5433
 
     */
5434
 
    public function create($config, $context, $code = false) {
5435
 
        
5436
 
        // validate language code
5437
 
        if ($code === false) {
5438
 
            $code = $this->validator->validate(
5439
 
              $config->get('Core', 'Language'), $config, $context
5440
 
            );
5441
 
        } else {
5442
 
            $code = $this->validator->validate($code, $config, $context);
5443
 
        }
5444
 
        if ($code === false) $code = 'en'; // malformed code becomes English
5445
 
        
5446
 
        $pcode = str_replace('-', '_', $code); // make valid PHP classname
5447
 
        static $depth = 0; // recursion protection
5448
 
        
5449
 
        if ($code == 'en') {
5450
 
            $lang = new HTMLPurifier_Language($config, $context);
5451
 
        } else {
5452
 
            $class = 'HTMLPurifier_Language_' . $pcode;
5453
 
            $file  = $this->dir . '/Language/classes/' . $code . '.php';
5454
 
            if (file_exists($file) || class_exists($class, false)) {
5455
 
                $lang = new $class($config, $context);
5456
 
            } else {
5457
 
                // Go fallback
5458
 
                $raw_fallback = $this->getFallbackFor($code);
5459
 
                $fallback = $raw_fallback ? $raw_fallback : 'en';
5460
 
                $depth++;
5461
 
                $lang = $this->create($config, $context, $fallback);
5462
 
                if (!$raw_fallback) {
5463
 
                    $lang->error = true;
5464
 
                }
5465
 
                $depth--;
5466
 
            }
5467
 
        }
5468
 
        
5469
 
        $lang->code = $code;
5470
 
        
5471
 
        return $lang;
5472
 
        
5473
 
    }
5474
 
    
5475
 
    /**
5476
 
     * Returns the fallback language for language
5477
 
     * @note Loads the original language into cache
5478
 
     * @param $code string language code
5479
 
     */
5480
 
    public function getFallbackFor($code) {
5481
 
        $this->loadLanguage($code);
5482
 
        return $this->cache[$code]['fallback'];
5483
 
    }
5484
 
    
5485
 
    /**
5486
 
     * Loads language into the cache, handles message file and fallbacks
5487
 
     * @param $code string language code
5488
 
     */
5489
 
    public function loadLanguage($code) {
5490
 
        static $languages_seen = array(); // recursion guard
5491
 
        
5492
 
        // abort if we've already loaded it
5493
 
        if (isset($this->cache[$code])) return;
5494
 
        
5495
 
        // generate filename
5496
 
        $filename = $this->dir . '/Language/messages/' . $code . '.php';
5497
 
        
5498
 
        // default fallback : may be overwritten by the ensuing include
5499
 
        $fallback = ($code != 'en') ? 'en' : false;
5500
 
        
5501
 
        // load primary localisation
5502
 
        if (!file_exists($filename)) {
5503
 
            // skip the include: will rely solely on fallback
5504
 
            $filename = $this->dir . '/Language/messages/en.php';
5505
 
            $cache = array();
5506
 
        } else {
5507
 
            include $filename;
5508
 
            $cache = compact($this->keys);
5509
 
        }
5510
 
        
5511
 
        // load fallback localisation
5512
 
        if (!empty($fallback)) {
5513
 
            
5514
 
            // infinite recursion guard
5515
 
            if (isset($languages_seen[$code])) {
5516
 
                trigger_error('Circular fallback reference in language ' .
5517
 
                    $code, E_USER_ERROR);
5518
 
                $fallback = 'en';
5519
 
            }
5520
 
            $language_seen[$code] = true;
5521
 
            
5522
 
            // load the fallback recursively
5523
 
            $this->loadLanguage($fallback);
5524
 
            $fallback_cache = $this->cache[$fallback];
5525
 
            
5526
 
            // merge fallback with current language
5527
 
            foreach ( $this->keys as $key ) {
5528
 
                if (isset($cache[$key]) && isset($fallback_cache[$key])) {
5529
 
                    if (isset($this->mergeable_keys_map[$key])) {
5530
 
                        $cache[$key] = $cache[$key] + $fallback_cache[$key];
5531
 
                    } elseif (isset($this->mergeable_keys_list[$key])) {
5532
 
                        $cache[$key] = array_merge( $fallback_cache[$key], $cache[$key] );
5533
 
                    }
5534
 
                } else {
5535
 
                    $cache[$key] = $fallback_cache[$key];
5536
 
                }
5537
 
            }
5538
 
            
5539
 
        }
5540
 
        
5541
 
        // save to cache for later retrieval
5542
 
        $this->cache[$code] = $cache;
5543
 
        
5544
 
        return;
5545
 
    }
5546
 
    
5547
 
}
5548
 
 
5549
 
 
5550
 
 
5551
 
 
5552
 
/**
5553
 
 * Represents a measurable length, with a string numeric magnitude
5554
 
 * and a unit. This object is immutable.
5555
 
 */
5556
 
class HTMLPurifier_Length
5557
 
{
5558
 
    
5559
 
    /**
5560
 
     * String numeric magnitude.
5561
 
     */
5562
 
    protected $n;
5563
 
    
5564
 
    /**
5565
 
     * String unit. False is permitted if $n = 0.
5566
 
     */
5567
 
    protected $unit;
5568
 
    
5569
 
    /**
5570
 
     * Whether or not this length is valid. Null if not calculated yet.
5571
 
     */
5572
 
    protected $isValid;
5573
 
    
5574
 
    /**
5575
 
     * Lookup array of units recognized by CSS 2.1
5576
 
     */
5577
 
    protected static $allowedUnits = array(
5578
 
        'em' => true, 'ex' => true, 'px' => true, 'in' => true,
5579
 
        'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true
5580
 
    );
5581
 
    
5582
 
    /**
5583
 
     * @param number $n Magnitude
5584
 
     * @param string $u Unit
5585
 
     */
5586
 
    public function __construct($n = '0', $u = false) {
5587
 
        $this->n = (string) $n;
5588
 
        $this->unit = $u !== false ? (string) $u : false;
5589
 
    }
5590
 
    
5591
 
    /**
5592
 
     * @param string $s Unit string, like '2em' or '3.4in'
5593
 
     * @warning Does not perform validation.
5594
 
     */
5595
 
    static public function make($s) {
5596
 
        if ($s instanceof HTMLPurifier_Length) return $s;
5597
 
        $n_length = strspn($s, '1234567890.+-');
5598
 
        $n = substr($s, 0, $n_length);
5599
 
        $unit = substr($s, $n_length);
5600
 
        if ($unit === '') $unit = false;
5601
 
        return new HTMLPurifier_Length($n, $unit);
5602
 
    }
5603
 
    
5604
 
    /**
5605
 
     * Validates the number and unit.
5606
 
     */
5607
 
    protected function validate() {
5608
 
        // Special case:
5609
 
        if ($this->n === '+0' || $this->n === '-0') $this->n = '0';
5610
 
        if ($this->n === '0' && $this->unit === false) return true;
5611
 
        if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit);
5612
 
        if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false;
5613
 
        // Hack:
5614
 
        $def = new HTMLPurifier_AttrDef_CSS_Number();
5615
 
        $result = $def->validate($this->n, false, false);
5616
 
        if ($result === false) return false;
5617
 
        $this->n = $result;
5618
 
        return true;
5619
 
    }
5620
 
    
5621
 
    /**
5622
 
     * Returns string representation of number.
5623
 
     */
5624
 
    public function toString() {
5625
 
        if (!$this->isValid()) return false;
5626
 
        return $this->n . $this->unit;
5627
 
    }
5628
 
    
5629
 
    /**
5630
 
     * Retrieves string numeric magnitude.
5631
 
     */
5632
 
    public function getN() {return $this->n;}
5633
 
    
5634
 
    /**
5635
 
     * Retrieves string unit.
5636
 
     */
5637
 
    public function getUnit() {return $this->unit;}
5638
 
    
5639
 
    /**
5640
 
     * Returns true if this length unit is valid.
5641
 
     */
5642
 
    public function isValid() {
5643
 
        if ($this->isValid === null) $this->isValid = $this->validate();
5644
 
        return $this->isValid;
5645
 
    }
5646
 
    
5647
 
    /**
5648
 
     * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
5649
 
     * @warning If both values are too large or small, this calculation will
5650
 
     *          not work properly
5651
 
     */
5652
 
    public function compareTo($l) {
5653
 
        if ($l === false) return false;
5654
 
        if ($l->unit !== $this->unit) {
5655
 
            $converter = new HTMLPurifier_UnitConverter();
5656
 
            $l = $converter->convert($l, $this->unit);
5657
 
            if ($l === false) return false;
5658
 
        }
5659
 
        return $this->n - $l->n;
5660
 
    }
5661
 
    
5662
 
}
5663
 
 
5664
 
 
5665
 
 
5666
 
/**
5667
 
 * Forgivingly lexes HTML (SGML-style) markup into tokens.
5668
 
 * 
5669
 
 * A lexer parses a string of SGML-style markup and converts them into
5670
 
 * corresponding tokens.  It doesn't check for well-formedness, although its
5671
 
 * internal mechanism may make this automatic (such as the case of
5672
 
 * HTMLPurifier_Lexer_DOMLex).  There are several implementations to choose
5673
 
 * from.
5674
 
 * 
5675
 
 * A lexer is HTML-oriented: it might work with XML, but it's not
5676
 
 * recommended, as we adhere to a subset of the specification for optimization
5677
 
 * reasons. This might change in the future. Also, most tokenizers are not
5678
 
 * expected to handle DTDs or PIs.
5679
 
 * 
5680
 
 * This class should not be directly instantiated, but you may use create() to
5681
 
 * retrieve a default copy of the lexer.  Being a supertype, this class
5682
 
 * does not actually define any implementation, but offers commonly used
5683
 
 * convenience functions for subclasses.
5684
 
 * 
5685
 
 * @note The unit tests will instantiate this class for testing purposes, as
5686
 
 *       many of the utility functions require a class to be instantiated.
5687
 
 *       This means that, even though this class is not runnable, it will
5688
 
 *       not be declared abstract.
5689
 
 * 
5690
 
 * @par
5691
 
 * 
5692
 
 * @note
5693
 
 * We use tokens rather than create a DOM representation because DOM would:
5694
 
 * 
5695
 
 * @par
5696
 
 *  -# Require more processing and memory to create,
5697
 
 *  -# Is not streamable, and
5698
 
 *  -# Has the entire document structure (html and body not needed).
5699
 
 * 
5700
 
 * @par
5701
 
 * However, DOM is helpful in that it makes it easy to move around nodes
5702
 
 * without a lot of lookaheads to see when a tag is closed. This is a
5703
 
 * limitation of the token system and some workarounds would be nice.
5704
 
 */
5705
 
class HTMLPurifier_Lexer
5706
 
{
5707
 
    
5708
 
    /**
5709
 
     * Whether or not this lexer implements line-number/column-number tracking.
5710
 
     * If it does, set to true.
5711
 
     */
5712
 
    public $tracksLineNumbers = false;
5713
 
    
5714
 
    // -- STATIC ----------------------------------------------------------
5715
 
    
5716
 
    /**
5717
 
     * Retrieves or sets the default Lexer as a Prototype Factory.
5718
 
     * 
5719
 
     * By default HTMLPurifier_Lexer_DOMLex will be returned. There are
5720
 
     * a few exceptions involving special features that only DirectLex
5721
 
     * implements.
5722
 
     * 
5723
 
     * @note The behavior of this class has changed, rather than accepting
5724
 
     *       a prototype object, it now accepts a configuration object.
5725
 
     *       To specify your own prototype, set %Core.LexerImpl to it.
5726
 
     *       This change in behavior de-singletonizes the lexer object.
5727
 
     * 
5728
 
     * @param $config Instance of HTMLPurifier_Config
5729
 
     * @return Concrete lexer.
5730
 
     */
5731
 
    public static function create($config) {
5732
 
        
5733
 
        if (!($config instanceof HTMLPurifier_Config)) {
5734
 
            $lexer = $config;
5735
 
            trigger_error("Passing a prototype to 
5736
 
              HTMLPurifier_Lexer::create() is deprecated, please instead
5737
 
              use %Core.LexerImpl", E_USER_WARNING);
5738
 
        } else {
5739
 
            $lexer = $config->get('Core', 'LexerImpl');
5740
 
        }
5741
 
        
5742
 
        $needs_tracking =
5743
 
            $config->get('Core', 'MaintainLineNumbers') ||
5744
 
            $config->get('Core', 'CollectErrors');
5745
 
        
5746
 
        $inst = null;
5747
 
        if (is_object($lexer)) {
5748
 
            $inst = $lexer;
5749
 
        } else {
5750
 
            
5751
 
            if (is_null($lexer)) { do {
5752
 
                // auto-detection algorithm
5753
 
                
5754
 
                if ($needs_tracking) {
5755
 
                    $lexer = 'DirectLex';
5756
 
                    break;
5757
 
                }
5758
 
                
5759
 
                if (
5760
 
                    class_exists('DOMDocument') &&
5761
 
                    method_exists('DOMDocument', 'loadHTML') &&
5762
 
                    !extension_loaded('domxml')
5763
 
                ) {
5764
 
                    // check for DOM support, because while it's part of the
5765
 
                    // core, it can be disabled compile time. Also, the PECL
5766
 
                    // domxml extension overrides the default DOM, and is evil
5767
 
                    // and nasty and we shan't bother to support it
5768
 
                    $lexer = 'DOMLex';
5769
 
                } else {
5770
 
                    $lexer = 'DirectLex';
5771
 
                }
5772
 
                
5773
 
            } while(0); } // do..while so we can break
5774
 
            
5775
 
            // instantiate recognized string names
5776
 
            switch ($lexer) {
5777
 
                case 'DOMLex':
5778
 
                    $inst = new HTMLPurifier_Lexer_DOMLex();
5779
 
                    break;
5780
 
                case 'DirectLex':
5781
 
                    $inst = new HTMLPurifier_Lexer_DirectLex();
5782
 
                    break;
5783
 
                case 'PH5P':
5784
 
                    $inst = new HTMLPurifier_Lexer_PH5P();
5785
 
                    break;
5786
 
                default:
5787
 
                    throw new HTMLPurifier_Exception("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer));
5788
 
            }
5789
 
        }
5790
 
        
5791
 
        if (!$inst) throw new HTMLPurifier_Exception('No lexer was instantiated');
5792
 
        
5793
 
        // once PHP DOM implements native line numbers, or we
5794
 
        // hack out something using XSLT, remove this stipulation
5795
 
        if ($needs_tracking && !$inst->tracksLineNumbers) {
5796
 
            throw new HTMLPurifier_Exception('Cannot use lexer that does not support line numbers with Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)');
5797
 
        }
5798
 
        
5799
 
        return $inst;
5800
 
        
5801
 
    }
5802
 
    
5803
 
    // -- CONVENIENCE MEMBERS ---------------------------------------------
5804
 
    
5805
 
    public function __construct() {
5806
 
        $this->_entity_parser = new HTMLPurifier_EntityParser();
5807
 
    }
5808
 
    
5809
 
    /**
5810
 
     * Most common entity to raw value conversion table for special entities.
5811
 
     */
5812
 
    protected $_special_entity2str =
5813
 
            array(
5814
 
                    '&quot;' => '"',
5815
 
                    '&amp;'  => '&',
5816
 
                    '&lt;'   => '<',
5817
 
                    '&gt;'   => '>',
5818
 
                    '&#39;'  => "'",
5819
 
                    '&#039;' => "'",
5820
 
                    '&#x27;' => "'"
5821
 
            );
5822
 
    
5823
 
    /**
5824
 
     * Parses special entities into the proper characters.
5825
 
     * 
5826
 
     * This string will translate escaped versions of the special characters
5827
 
     * into the correct ones.
5828
 
     * 
5829
 
     * @warning
5830
 
     * You should be able to treat the output of this function as
5831
 
     * completely parsed, but that's only because all other entities should
5832
 
     * have been handled previously in substituteNonSpecialEntities()
5833
 
     * 
5834
 
     * @param $string String character data to be parsed.
5835
 
     * @returns Parsed character data.
5836
 
     */
5837
 
    public function parseData($string) {
5838
 
        
5839
 
        // following functions require at least one character
5840
 
        if ($string === '') return '';
5841
 
        
5842
 
        // subtracts amps that cannot possibly be escaped
5843
 
        $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
5844
 
            ($string[strlen($string)-1] === '&' ? 1 : 0);
5845
 
        
5846
 
        if (!$num_amp) return $string; // abort if no entities
5847
 
        $num_esc_amp = substr_count($string, '&amp;');
5848
 
        $string = strtr($string, $this->_special_entity2str);
5849
 
        
5850
 
        // code duplication for sake of optimization, see above
5851
 
        $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
5852
 
            ($string[strlen($string)-1] === '&' ? 1 : 0);
5853
 
        
5854
 
        if ($num_amp_2 <= $num_esc_amp) return $string;
5855
 
        
5856
 
        // hmm... now we have some uncommon entities. Use the callback.
5857
 
        $string = $this->_entity_parser->substituteSpecialEntities($string);
5858
 
        return $string;
5859
 
    }
5860
 
    
5861
 
    /**
5862
 
     * Lexes an HTML string into tokens.
5863
 
     * 
5864
 
     * @param $string String HTML.
5865
 
     * @return HTMLPurifier_Token array representation of HTML.
5866
 
     */
5867
 
    public function tokenizeHTML($string, $config, $context) {
5868
 
        trigger_error('Call to abstract class', E_USER_ERROR);
5869
 
    }
5870
 
    
5871
 
    /**
5872
 
     * Translates CDATA sections into regular sections (through escaping).
5873
 
     * 
5874
 
     * @param $string HTML string to process.
5875
 
     * @returns HTML with CDATA sections escaped.
5876
 
     */
5877
 
    protected static function escapeCDATA($string) {
5878
 
        return preg_replace_callback(
5879
 
            '/<!\[CDATA\[(.+?)\]\]>/s',
5880
 
            array('HTMLPurifier_Lexer', 'CDATACallback'),
5881
 
            $string
5882
 
        );
5883
 
    }
5884
 
    
5885
 
    /**
5886
 
     * Special CDATA case that is especially convoluted for <script>
5887
 
     */
5888
 
    protected static function escapeCommentedCDATA($string) {
5889
 
        return preg_replace_callback(
5890
 
            '#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s',
5891
 
            array('HTMLPurifier_Lexer', 'CDATACallback'),
5892
 
            $string
5893
 
        );
5894
 
    }
5895
 
    
5896
 
    /**
5897
 
     * Callback function for escapeCDATA() that does the work.
5898
 
     * 
5899
 
     * @warning Though this is public in order to let the callback happen,
5900
 
     *          calling it directly is not recommended.
5901
 
     * @params $matches PCRE matches array, with index 0 the entire match
5902
 
     *                  and 1 the inside of the CDATA section.
5903
 
     * @returns Escaped internals of the CDATA section.
5904
 
     */
5905
 
    protected static function CDATACallback($matches) {
5906
 
        // not exactly sure why the character set is needed, but whatever
5907
 
        return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8');
5908
 
    }
5909
 
    
5910
 
    /**
5911
 
     * Takes a piece of HTML and normalizes it by converting entities, fixing
5912
 
     * encoding, extracting bits, and other good stuff.
5913
 
     * @todo Consider making protected
5914
 
     */
5915
 
    public function normalize($html, $config, $context) {
5916
 
        
5917
 
        // normalize newlines to \n
5918
 
        $html = str_replace("\r\n", "\n", $html);
5919
 
        $html = str_replace("\r", "\n", $html);
5920
 
        
5921
 
        if ($config->get('HTML', 'Trusted')) {
5922
 
            // escape convoluted CDATA
5923
 
            $html = $this->escapeCommentedCDATA($html);
5924
 
        }
5925
 
        
5926
 
        // escape CDATA
5927
 
        $html = $this->escapeCDATA($html);
5928
 
        
5929
 
        // extract body from document if applicable
5930
 
        if ($config->get('Core', 'ConvertDocumentToFragment')) {
5931
 
            $html = $this->extractBody($html);
5932
 
        }
5933
 
        
5934
 
        // expand entities that aren't the big five
5935
 
        $html = $this->_entity_parser->substituteNonSpecialEntities($html);
5936
 
        
5937
 
        // clean into wellformed UTF-8 string for an SGML context: this has
5938
 
        // to be done after entity expansion because the entities sometimes
5939
 
        // represent non-SGML characters (horror, horror!)
5940
 
        $html = HTMLPurifier_Encoder::cleanUTF8($html);
5941
 
        
5942
 
        return $html;
5943
 
    }
5944
 
    
5945
 
    /**
5946
 
     * Takes a string of HTML (fragment or document) and returns the content
5947
 
     * @todo Consider making protected
5948
 
     */
5949
 
    public function extractBody($html) {
5950
 
        $matches = array();
5951
 
        $result = preg_match('!<body[^>]*>(.+?)</body>!is', $html, $matches);
5952
 
        if ($result) {
5953
 
            return $matches[1];
5954
 
        } else {
5955
 
            return $html;
5956
 
        }
5957
 
    }
5958
 
    
5959
 
}
5960
 
 
5961
 
 
5962
 
 
5963
 
 
5964
 
/**
5965
 
 * Class that handles operations involving percent-encoding in URIs.
5966
 
 *
5967
 
 * @warning
5968
 
 *      Be careful when reusing instances of PercentEncoder. The object
5969
 
 *      you use for normalize() SHOULD NOT be used for encode(), or
5970
 
 *      vice-versa.
5971
 
 */
5972
 
class HTMLPurifier_PercentEncoder
5973
 
{
5974
 
    
5975
 
    /**
5976
 
     * Reserved characters to preserve when using encode().
5977
 
     */
5978
 
    protected $preserve = array();
5979
 
    
5980
 
    /**
5981
 
     * String of characters that should be preserved while using encode().
5982
 
     */
5983
 
    public function __construct($preserve = false) {
5984
 
        // unreserved letters, ought to const-ify
5985
 
        for ($i = 48; $i <= 57;  $i++) $this->preserve[$i] = true; // digits
5986
 
        for ($i = 65; $i <= 90;  $i++) $this->preserve[$i] = true; // upper-case
5987
 
        for ($i = 97; $i <= 122; $i++) $this->preserve[$i] = true; // lower-case
5988
 
        $this->preserve[45] = true; // Dash         -
5989
 
        $this->preserve[46] = true; // Period       .
5990
 
        $this->preserve[95] = true; // Underscore   _
5991
 
        $this->preserve[126]= true; // Tilde        ~
5992
 
        
5993
 
        // extra letters not to escape
5994
 
        if ($preserve !== false) {
5995
 
            for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
5996
 
                $this->preserve[ord($preserve[$i])] = true;
5997
 
            }
5998
 
        }
5999
 
    }
6000
 
    
6001
 
    /**
6002
 
     * Our replacement for urlencode, it encodes all non-reserved characters,
6003
 
     * as well as any extra characters that were instructed to be preserved.
6004
 
     * @note
6005
 
     *      Assumes that the string has already been normalized, making any
6006
 
     *      and all percent escape sequences valid. Percents will not be
6007
 
     *      re-escaped, regardless of their status in $preserve
6008
 
     * @param $string String to be encoded
6009
 
     * @return Encoded string.
6010
 
     */
6011
 
    public function encode($string) {
6012
 
        $ret = '';
6013
 
        for ($i = 0, $c = strlen($string); $i < $c; $i++) {
6014
 
            if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) {
6015
 
                $ret .= '%' . sprintf('%02X', $int);
6016
 
            } else {
6017
 
                $ret .= $string[$i];
6018
 
            }
6019
 
        }
6020
 
        return $ret;
6021
 
    }
6022
 
    
6023
 
    /**
6024
 
     * Fix up percent-encoding by decoding unreserved characters and normalizing.
6025
 
     * @warning This function is affected by $preserve, even though the
6026
 
     *          usual desired behavior is for this not to preserve those
6027
 
     *          characters. Be careful when reusing instances of PercentEncoder!
6028
 
     * @param $string String to normalize
6029
 
     */
6030
 
    public function normalize($string) {
6031
 
        if ($string == '') return '';
6032
 
        $parts = explode('%', $string);
6033
 
        $ret = array_shift($parts);
6034
 
        foreach ($parts as $part) {
6035
 
            $length = strlen($part);
6036
 
            if ($length < 2) {
6037
 
                $ret .= '%25' . $part;
6038
 
                continue;
6039
 
            }
6040
 
            $encoding = substr($part, 0, 2);
6041
 
            $text     = substr($part, 2);
6042
 
            if (!ctype_xdigit($encoding)) {
6043
 
                $ret .= '%25' . $part;
6044
 
                continue;
6045
 
            }
6046
 
            $int = hexdec($encoding);
6047
 
            if (isset($this->preserve[$int])) {
6048
 
                $ret .= chr($int) . $text;
6049
 
                continue;
6050
 
            }
6051
 
            $encoding = strtoupper($encoding);
6052
 
            $ret .= '%' . $encoding . $text;
6053
 
        }
6054
 
        return $ret;
6055
 
    }
6056
 
    
6057
 
}
6058
 
 
6059
 
 
6060
 
 
6061
 
 
6062
 
/**
6063
 
 * Supertype for classes that define a strategy for modifying/purifying tokens.
6064
 
 * 
6065
 
 * While HTMLPurifier's core purpose is fixing HTML into something proper, 
6066
 
 * strategies provide plug points for extra configuration or even extra
6067
 
 * features, such as custom tags, custom parsing of text, etc.
6068
 
 */
6069
 
 
6070
 
 
6071
 
abstract class HTMLPurifier_Strategy
6072
 
{
6073
 
    
6074
 
    /**
6075
 
     * Executes the strategy on the tokens.
6076
 
     * 
6077
 
     * @param $tokens Array of HTMLPurifier_Token objects to be operated on.
6078
 
     * @param $config Configuration options
6079
 
     * @returns Processed array of token objects.
6080
 
     */
6081
 
    abstract public function execute($tokens, $config, $context);
6082
 
    
6083
 
}
6084
 
 
6085
 
 
6086
 
 
6087
 
 
6088
 
/**
6089
 
 * This is in almost every respect equivalent to an array except
6090
 
 * that it keeps track of which keys were accessed.
6091
 
 *
6092
 
 * @warning For the sake of backwards compatibility with early versions
6093
 
 *     of PHP 5, you must not use the $hash[$key] syntax; if you do
6094
 
 *     our version of offsetGet is never called.
6095
 
 */
6096
 
class HTMLPurifier_StringHash extends ArrayObject
6097
 
{
6098
 
    protected $accessed = array();
6099
 
    
6100
 
    /**
6101
 
     * Retrieves a value, and logs the access.
6102
 
     */
6103
 
    public function offsetGet($index) {
6104
 
        $this->accessed[$index] = true;
6105
 
        return parent::offsetGet($index);
6106
 
    }
6107
 
    
6108
 
    /**
6109
 
     * Returns a lookup array of all array indexes that have been accessed.
6110
 
     * @return Array in form array($index => true).
6111
 
     */
6112
 
    public function getAccessed() {
6113
 
        return $this->accessed;
6114
 
    }
6115
 
    
6116
 
    /**
6117
 
     * Resets the access array.
6118
 
     */
6119
 
    public function resetAccessed() {
6120
 
        $this->accessed = array();
6121
 
    }
6122
 
}
6123
 
 
6124
 
 
6125
 
 
6126
 
/**
6127
 
 * Parses string hash files. File format is as such:
6128
 
 * 
6129
 
 *      DefaultKeyValue
6130
 
 *      KEY: Value
6131
 
 *      KEY2: Value2
6132
 
 *      --MULTILINE-KEY--
6133
 
 *      Multiline
6134
 
 *      value.
6135
 
 *
6136
 
 * Which would output something similar to:
6137
 
 *
6138
 
 *      array(
6139
 
 *          'ID' => 'DefaultKeyValue',
6140
 
 *          'KEY' => 'Value',
6141
 
 *          'KEY2' => 'Value2',
6142
 
 *          'MULTILINE-KEY' => "Multiline\nvalue.\n",
6143
 
 *      )
6144
 
 *
6145
 
 * We use this as an easy to use file-format for configuration schema
6146
 
 * files, but the class itself is usage agnostic.
6147
 
 *
6148
 
 * You can use ---- to forcibly terminate parsing of a single string-hash;
6149
 
 * this marker is used in multi string-hashes to delimit boundaries.
6150
 
 */
6151
 
class HTMLPurifier_StringHashParser
6152
 
{
6153
 
    
6154
 
    public $default = 'ID';
6155
 
    
6156
 
    /**
6157
 
     * Parses a file that contains a single string-hash.
6158
 
     */
6159
 
    public function parseFile($file) {
6160
 
        if (!file_exists($file)) return false;
6161
 
        $fh = fopen($file, 'r');
6162
 
        if (!$fh) return false;
6163
 
        $ret = $this->parseHandle($fh);
6164
 
        fclose($fh);
6165
 
        return $ret;
6166
 
    }
6167
 
    
6168
 
    /**
6169
 
     * Parses a file that contains multiple string-hashes delimited by '----'
6170
 
     */
6171
 
    public function parseMultiFile($file) {
6172
 
        if (!file_exists($file)) return false;
6173
 
        $ret = array();
6174
 
        $fh = fopen($file, 'r');
6175
 
        if (!$fh) return false;
6176
 
        while (!feof($fh)) {
6177
 
            $ret[] = $this->parseHandle($fh);
6178
 
        }
6179
 
        fclose($fh);
6180
 
        return $ret;
6181
 
    }
6182
 
    
6183
 
    /**
6184
 
     * Internal parser that acepts a file handle.
6185
 
     * @note While it's possible to simulate in-memory parsing by using
6186
 
     *       custom stream wrappers, if such a use-case arises we should
6187
 
     *       factor out the file handle into its own class.
6188
 
     * @param $fh File handle with pointer at start of valid string-hash
6189
 
     *            block.
6190
 
     */
6191
 
    protected function parseHandle($fh) {
6192
 
        $state   = false;
6193
 
        $single  = false;
6194
 
        $ret     = array();
6195
 
        do {
6196
 
            $line = fgets($fh);
6197
 
            if ($line === false) break;
6198
 
            $line = rtrim($line, "\n\r");
6199
 
            if (!$state && $line === '') continue;
6200
 
            if ($line === '----') break;
6201
 
            if (strncmp('--', $line, 2) === 0) {
6202
 
                // Multiline declaration
6203
 
                $state = trim($line, '- ');
6204
 
                if (!isset($ret[$state])) $ret[$state] = '';
6205
 
                continue;
6206
 
            } elseif (!$state) {
6207
 
                $single = true;
6208
 
                if (strpos($line, ':') !== false) {
6209
 
                    // Single-line declaration
6210
 
                    list($state, $line) = explode(': ', $line, 2);
6211
 
                } else {
6212
 
                    // Use default declaration
6213
 
                    $state  = $this->default;
6214
 
                }
6215
 
            }
6216
 
            if ($single) {
6217
 
                $ret[$state] = $line;
6218
 
                $single = false;
6219
 
                $state  = false;
6220
 
            } else {
6221
 
                $ret[$state] .= "$line\n";
6222
 
            }
6223
 
        } while (!feof($fh));
6224
 
        return $ret;
6225
 
    }
6226
 
    
6227
 
}
6228
 
 
6229
 
 
6230
 
 
6231
 
/**
6232
 
 * Defines a mutation of an obsolete tag into a valid tag.
6233
 
 */
6234
 
abstract class HTMLPurifier_TagTransform
6235
 
{
6236
 
    
6237
 
    /**
6238
 
     * Tag name to transform the tag to.
6239
 
     */
6240
 
    public $transform_to;
6241
 
    
6242
 
    /**
6243
 
     * Transforms the obsolete tag into the valid tag.
6244
 
     * @param $tag Tag to be transformed.
6245
 
     * @param $config Mandatory HTMLPurifier_Config object
6246
 
     * @param $context Mandatory HTMLPurifier_Context object
6247
 
     */
6248
 
    abstract public function transform($tag, $config, $context);
6249
 
    
6250
 
    /**
6251
 
     * Prepends CSS properties to the style attribute, creating the
6252
 
     * attribute if it doesn't exist.
6253
 
     * @warning Copied over from AttrTransform, be sure to keep in sync
6254
 
     * @param $attr Attribute array to process (passed by reference)
6255
 
     * @param $css CSS to prepend
6256
 
     */
6257
 
    protected function prependCSS(&$attr, $css) {
6258
 
        $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
6259
 
        $attr['style'] = $css . $attr['style'];
6260
 
    }
6261
 
    
6262
 
}
6263
 
 
6264
 
 
6265
 
 
6266
 
 
6267
 
/**
6268
 
 * Abstract base token class that all others inherit from.
6269
 
 */
6270
 
class HTMLPurifier_Token {
6271
 
    public $line; /**< Line number node was on in source document. Null if unknown. */
6272
 
    public $col;  /**< Column of line node was on in source document. Null if unknown. */
6273
 
    
6274
 
    /**
6275
 
     * Lookup array of processing that this token is exempt from.
6276
 
     * Currently, valid values are "ValidateAttributes" and
6277
 
     * "MakeWellFormed_TagClosedError"
6278
 
     */
6279
 
    public $armor = array();
6280
 
    
6281
 
    /**
6282
 
     * Used during MakeWellFormed.
6283
 
     */
6284
 
    public $skip;
6285
 
    public $rewind;
6286
 
    
6287
 
    public function __get($n) {
6288
 
      if ($n === 'type') {
6289
 
        trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE);
6290
 
        switch (get_class($this)) {
6291
 
          case 'HTMLPurifier_Token_Start':      return 'start';
6292
 
          case 'HTMLPurifier_Token_Empty':      return 'empty';
6293
 
          case 'HTMLPurifier_Token_End':        return 'end';
6294
 
          case 'HTMLPurifier_Token_Text':       return 'text';
6295
 
          case 'HTMLPurifier_Token_Comment':    return 'comment';
6296
 
          default: return null;
6297
 
        }
6298
 
      }
6299
 
    }
6300
 
    
6301
 
    /**
6302
 
     * Sets the position of the token in the source document.
6303
 
     */
6304
 
    public function position($l = null, $c = null) {
6305
 
        $this->line = $l;
6306
 
        $this->col  = $c;
6307
 
    }
6308
 
    
6309
 
    /**
6310
 
     * Convenience function for DirectLex settings line/col position.
6311
 
     */
6312
 
    public function rawPosition($l, $c) {
6313
 
        if ($c === -1) $l++;
6314
 
        $this->line = $l;
6315
 
        $this->col  = $c;
6316
 
    }
6317
 
    
6318
 
}
6319
 
 
6320
 
 
6321
 
 
6322
 
/**
6323
 
 * Factory for token generation.
6324
 
 * 
6325
 
 * @note Doing some benchmarking indicates that the new operator is much
6326
 
 *       slower than the clone operator (even discounting the cost of the
6327
 
 *       constructor).  This class is for that optimization.
6328
 
 *       Other then that, there's not much point as we don't
6329
 
 *       maintain parallel HTMLPurifier_Token hierarchies (the main reason why
6330
 
 *       you'd want to use an abstract factory).
6331
 
 * @todo Port DirectLex to use this
6332
 
 */
6333
 
class HTMLPurifier_TokenFactory
6334
 
{
6335
 
    
6336
 
    /**
6337
 
     * Prototypes that will be cloned.
6338
 
     * @private
6339
 
     */
6340
 
    // p stands for prototype
6341
 
    private $p_start, $p_end, $p_empty, $p_text, $p_comment;
6342
 
    
6343
 
    /**
6344
 
     * Generates blank prototypes for cloning.
6345
 
     */
6346
 
    public function __construct() {
6347
 
        $this->p_start  = new HTMLPurifier_Token_Start('', array());
6348
 
        $this->p_end    = new HTMLPurifier_Token_End('');
6349
 
        $this->p_empty  = new HTMLPurifier_Token_Empty('', array());
6350
 
        $this->p_text   = new HTMLPurifier_Token_Text('');
6351
 
        $this->p_comment= new HTMLPurifier_Token_Comment('');
6352
 
    }
6353
 
    
6354
 
    /**
6355
 
     * Creates a HTMLPurifier_Token_Start.
6356
 
     * @param $name Tag name
6357
 
     * @param $attr Associative array of attributes
6358
 
     * @return Generated HTMLPurifier_Token_Start
6359
 
     */
6360
 
    public function createStart($name, $attr = array()) {
6361
 
        $p = clone $this->p_start;
6362
 
        $p->__construct($name, $attr);
6363
 
        return $p;
6364
 
    }
6365
 
    
6366
 
    /**
6367
 
     * Creates a HTMLPurifier_Token_End.
6368
 
     * @param $name Tag name
6369
 
     * @return Generated HTMLPurifier_Token_End
6370
 
     */
6371
 
    public function createEnd($name) {
6372
 
        $p = clone $this->p_end;
6373
 
        $p->__construct($name);
6374
 
        return $p;
6375
 
    }
6376
 
    
6377
 
    /**
6378
 
     * Creates a HTMLPurifier_Token_Empty.
6379
 
     * @param $name Tag name
6380
 
     * @param $attr Associative array of attributes
6381
 
     * @return Generated HTMLPurifier_Token_Empty
6382
 
     */
6383
 
    public function createEmpty($name, $attr = array()) {
6384
 
        $p = clone $this->p_empty;
6385
 
        $p->__construct($name, $attr);
6386
 
        return $p;
6387
 
    }
6388
 
    
6389
 
    /**
6390
 
     * Creates a HTMLPurifier_Token_Text.
6391
 
     * @param $data Data of text token
6392
 
     * @return Generated HTMLPurifier_Token_Text
6393
 
     */
6394
 
    public function createText($data) {
6395
 
        $p = clone $this->p_text;
6396
 
        $p->__construct($data);
6397
 
        return $p;
6398
 
    }
6399
 
    
6400
 
    /**
6401
 
     * Creates a HTMLPurifier_Token_Comment.
6402
 
     * @param $data Data of comment token
6403
 
     * @return Generated HTMLPurifier_Token_Comment
6404
 
     */
6405
 
    public function createComment($data) {
6406
 
        $p = clone $this->p_comment;
6407
 
        $p->__construct($data);
6408
 
        return $p;
6409
 
    }
6410
 
    
6411
 
}
6412
 
 
6413
 
 
6414
 
 
6415
 
 
6416
 
/**
6417
 
 * HTML Purifier's internal representation of a URI.
6418
 
 * @note
6419
 
 *      Internal data-structures are completely escaped. If the data needs
6420
 
 *      to be used in a non-URI context (which is very unlikely), be sure
6421
 
 *      to decode it first. The URI may not necessarily be well-formed until
6422
 
 *      validate() is called.
6423
 
 */
6424
 
class HTMLPurifier_URI
6425
 
{
6426
 
    
6427
 
    public $scheme, $userinfo, $host, $port, $path, $query, $fragment;
6428
 
    
6429
 
    /**
6430
 
     * @note Automatically normalizes scheme and port
6431
 
     */
6432
 
    public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment) {
6433
 
        $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
6434
 
        $this->userinfo = $userinfo;
6435
 
        $this->host = $host;
6436
 
        $this->port = is_null($port) ? $port : (int) $port;
6437
 
        $this->path = $path;
6438
 
        $this->query = $query;
6439
 
        $this->fragment = $fragment;
6440
 
    }
6441
 
    
6442
 
    /**
6443
 
     * Retrieves a scheme object corresponding to the URI's scheme/default
6444
 
     * @param $config Instance of HTMLPurifier_Config
6445
 
     * @param $context Instance of HTMLPurifier_Context
6446
 
     * @return Scheme object appropriate for validating this URI
6447
 
     */
6448
 
    public function getSchemeObj($config, $context) {
6449
 
        $registry = HTMLPurifier_URISchemeRegistry::instance();
6450
 
        if ($this->scheme !== null) {
6451
 
            $scheme_obj = $registry->getScheme($this->scheme, $config, $context);
6452
 
            if (!$scheme_obj) return false; // invalid scheme, clean it out
6453
 
        } else {
6454
 
            // no scheme: retrieve the default one
6455
 
            $def = $config->getDefinition('URI');
6456
 
            $scheme_obj = $registry->getScheme($def->defaultScheme, $config, $context);
6457
 
            if (!$scheme_obj) {
6458
 
                // something funky happened to the default scheme object
6459
 
                trigger_error(
6460
 
                    'Default scheme object "' . $def->defaultScheme . '" was not readable',
6461
 
                    E_USER_WARNING
6462
 
                );
6463
 
                return false;
6464
 
            }
6465
 
        }
6466
 
        return $scheme_obj;
6467
 
    }
6468
 
    
6469
 
    /**
6470
 
     * Generic validation method applicable for all schemes. May modify
6471
 
     * this URI in order to get it into a compliant form.
6472
 
     * @param $config Instance of HTMLPurifier_Config
6473
 
     * @param $context Instance of HTMLPurifier_Context
6474
 
     * @return True if validation/filtering succeeds, false if failure
6475
 
     */
6476
 
    public function validate($config, $context) {
6477
 
        
6478
 
        // ABNF definitions from RFC 3986
6479
 
        $chars_sub_delims = '!$&\'()*+,;=';
6480
 
        $chars_gen_delims = ':/?#[]@';
6481
 
        $chars_pchar = $chars_sub_delims . ':@';
6482
 
        
6483
 
        // validate scheme (MUST BE FIRST!)
6484
 
        if (!is_null($this->scheme) && is_null($this->host)) {
6485
 
            $def = $config->getDefinition('URI');
6486
 
            if ($def->defaultScheme === $this->scheme) {
6487
 
                $this->scheme = null;
6488
 
            }
6489
 
        }
6490
 
        
6491
 
        // validate host
6492
 
        if (!is_null($this->host)) {
6493
 
            $host_def = new HTMLPurifier_AttrDef_URI_Host();
6494
 
            $this->host = $host_def->validate($this->host, $config, $context);
6495
 
            if ($this->host === false) $this->host = null;
6496
 
        }
6497
 
        
6498
 
        // validate username
6499
 
        if (!is_null($this->userinfo)) {
6500
 
            $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
6501
 
            $this->userinfo = $encoder->encode($this->userinfo);
6502
 
        }
6503
 
        
6504
 
        // validate port
6505
 
        if (!is_null($this->port)) {
6506
 
            if ($this->port < 1 || $this->port > 65535) $this->port = null;
6507
 
        }
6508
 
        
6509
 
        // validate path
6510
 
        $path_parts = array();
6511
 
        $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
6512
 
        if (!is_null($this->host)) {
6513
 
            // path-abempty (hier and relative)
6514
 
            $this->path = $segments_encoder->encode($this->path);
6515
 
        } elseif ($this->path !== '' && $this->path[0] === '/') {
6516
 
            // path-absolute (hier and relative)
6517
 
            if (strlen($this->path) >= 2 && $this->path[1] === '/') {
6518
 
                // This shouldn't ever happen!
6519
 
                $this->path = '';
6520
 
            } else {
6521
 
                $this->path = $segments_encoder->encode($this->path);
6522
 
            }
6523
 
        } elseif (!is_null($this->scheme) && $this->path !== '') {
6524
 
            // path-rootless (hier)
6525
 
            // Short circuit evaluation means we don't need to check nz
6526
 
            $this->path = $segments_encoder->encode($this->path);
6527
 
        } elseif (is_null($this->scheme) && $this->path !== '') {
6528
 
            // path-noscheme (relative)
6529
 
            // (once again, not checking nz)
6530
 
            $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
6531
 
            $c = strpos($this->path, '/');
6532
 
            if ($c !== false) {
6533
 
                $this->path = 
6534
 
                    $segment_nc_encoder->encode(substr($this->path, 0, $c)) .
6535
 
                    $segments_encoder->encode(substr($this->path, $c));
6536
 
            } else {
6537
 
                $this->path = $segment_nc_encoder->encode($this->path);
6538
 
            }
6539
 
        } else {
6540
 
            // path-empty (hier and relative)
6541
 
            $this->path = ''; // just to be safe
6542
 
        }
6543
 
        
6544
 
        // qf = query and fragment
6545
 
        $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
6546
 
        
6547
 
        if (!is_null($this->query)) {
6548
 
            $this->query = $qf_encoder->encode($this->query);
6549
 
        }
6550
 
        
6551
 
        if (!is_null($this->fragment)) {
6552
 
            $this->fragment = $qf_encoder->encode($this->fragment);
6553
 
        }
6554
 
        
6555
 
        return true;
6556
 
        
6557
 
    }
6558
 
    
6559
 
    /**
6560
 
     * Convert URI back to string
6561
 
     * @return String URI appropriate for output
6562
 
     */
6563
 
    public function toString() {
6564
 
        // reconstruct authority
6565
 
        $authority = null;
6566
 
        if (!is_null($this->host)) {
6567
 
            $authority = '';
6568
 
            if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@';
6569
 
            $authority .= $this->host;
6570
 
            if(!is_null($this->port))     $authority .= ':' . $this->port;
6571
 
        }
6572
 
        
6573
 
        // reconstruct the result
6574
 
        $result = '';
6575
 
        if (!is_null($this->scheme))    $result .= $this->scheme . ':';
6576
 
        if (!is_null($authority))       $result .=  '//' . $authority;
6577
 
        $result .= $this->path;
6578
 
        if (!is_null($this->query))     $result .= '?' . $this->query;
6579
 
        if (!is_null($this->fragment))  $result .= '#' . $this->fragment;
6580
 
        
6581
 
        return $result;
6582
 
    }
6583
 
    
6584
 
}
6585
 
 
6586
 
 
6587
 
 
6588
 
 
6589
 
class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
6590
 
{
6591
 
    
6592
 
    public $type = 'URI';
6593
 
    protected $filters = array();
6594
 
    protected $postFilters = array();
6595
 
    protected $registeredFilters = array();
6596
 
    
6597
 
    /**
6598
 
     * HTMLPurifier_URI object of the base specified at %URI.Base
6599
 
     */
6600
 
    public $base;
6601
 
    
6602
 
    /**
6603
 
     * String host to consider "home" base, derived off of $base
6604
 
     */
6605
 
    public $host;
6606
 
    
6607
 
    /**
6608
 
     * Name of default scheme based on %URI.DefaultScheme and %URI.Base
6609
 
     */
6610
 
    public $defaultScheme;
6611
 
    
6612
 
    public function __construct() {
6613
 
        $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
6614
 
        $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
6615
 
        $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
6616
 
        $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
6617
 
        $this->registerFilter(new HTMLPurifier_URIFilter_Munge());
6618
 
    }
6619
 
    
6620
 
    public function registerFilter($filter) {
6621
 
        $this->registeredFilters[$filter->name] = $filter;
6622
 
    }
6623
 
    
6624
 
    public function addFilter($filter, $config) {
6625
 
        $r = $filter->prepare($config);
6626
 
        if ($r === false) return; // null is ok, for backwards compat
6627
 
        if ($filter->post) {
6628
 
            $this->postFilters[$filter->name] = $filter;
6629
 
        } else {
6630
 
            $this->filters[$filter->name] = $filter;
6631
 
        }
6632
 
    }
6633
 
    
6634
 
    protected function doSetup($config) {
6635
 
        $this->setupMemberVariables($config);
6636
 
        $this->setupFilters($config);
6637
 
    }
6638
 
    
6639
 
    protected function setupFilters($config) {
6640
 
        foreach ($this->registeredFilters as $name => $filter) {
6641
 
            $conf = $config->get('URI', $name);
6642
 
            if ($conf !== false && $conf !== null) {
6643
 
                $this->addFilter($filter, $config);
6644
 
            }
6645
 
        }
6646
 
        unset($this->registeredFilters);
6647
 
    }
6648
 
    
6649
 
    protected function setupMemberVariables($config) {
6650
 
        $this->host = $config->get('URI', 'Host');
6651
 
        $base_uri = $config->get('URI', 'Base');
6652
 
        if (!is_null($base_uri)) {
6653
 
            $parser = new HTMLPurifier_URIParser();
6654
 
            $this->base = $parser->parse($base_uri);
6655
 
            $this->defaultScheme = $this->base->scheme;
6656
 
            if (is_null($this->host)) $this->host = $this->base->host;
6657
 
        }
6658
 
        if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI', 'DefaultScheme');
6659
 
    }
6660
 
    
6661
 
    public function filter(&$uri, $config, $context) {
6662
 
        foreach ($this->filters as $name => $f) {
6663
 
            $result = $f->filter($uri, $config, $context);
6664
 
            if (!$result) return false;
6665
 
        }
6666
 
        return true;
6667
 
    }
6668
 
    
6669
 
    public function postFilter(&$uri, $config, $context) {
6670
 
        foreach ($this->postFilters as $name => $f) {
6671
 
            $result = $f->filter($uri, $config, $context);
6672
 
            if (!$result) return false;
6673
 
        }
6674
 
        return true;
6675
 
    }
6676
 
    
6677
 
}
6678
 
 
6679
 
 
6680
 
 
6681
 
/**
6682
 
 * Chainable filters for custom URI processing.
6683
 
 * 
6684
 
 * These filters can perform custom actions on a URI filter object,
6685
 
 * including transformation or blacklisting.
6686
 
 * 
6687
 
 * @warning This filter is called before scheme object validation occurs.
6688
 
 *          Make sure, if you require a specific scheme object, you
6689
 
 *          you check that it exists. This allows filters to convert
6690
 
 *          proprietary URI schemes into regular ones.
6691
 
 */
6692
 
abstract class HTMLPurifier_URIFilter
6693
 
{
6694
 
    
6695
 
    /**
6696
 
     * Unique identifier of filter
6697
 
     */
6698
 
    public $name;
6699
 
    
6700
 
    /**
6701
 
     * True if this filter should be run after scheme validation.
6702
 
     */
6703
 
    public $post = false;
6704
 
    
6705
 
    /**
6706
 
     * Performs initialization for the filter
6707
 
     */
6708
 
    public function prepare($config) {return true;}
6709
 
    
6710
 
    /**
6711
 
     * Filter a URI object
6712
 
     * @param $uri Reference to URI object variable
6713
 
     * @param $config Instance of HTMLPurifier_Config
6714
 
     * @param $context Instance of HTMLPurifier_Context
6715
 
     * @return bool Whether or not to continue processing: false indicates
6716
 
     *         URL is no good, true indicates continue processing. Note that
6717
 
     *         all changes are committed directly on the URI object
6718
 
     */
6719
 
    abstract public function filter(&$uri, $config, $context);
6720
 
    
6721
 
}
6722
 
 
6723
 
 
6724
 
 
6725
 
/**
6726
 
 * Parses a URI into the components and fragment identifier as specified
6727
 
 * by RFC 3986.
6728
 
 */
6729
 
class HTMLPurifier_URIParser
6730
 
{
6731
 
    
6732
 
    /**
6733
 
     * Instance of HTMLPurifier_PercentEncoder to do normalization with.
6734
 
     */
6735
 
    protected $percentEncoder;
6736
 
    
6737
 
    public function __construct() {
6738
 
        $this->percentEncoder = new HTMLPurifier_PercentEncoder();
6739
 
    }
6740
 
    
6741
 
    /**
6742
 
     * Parses a URI.
6743
 
     * @param $uri string URI to parse
6744
 
     * @return HTMLPurifier_URI representation of URI. This representation has
6745
 
     *         not been validated yet and may not conform to RFC.
6746
 
     */
6747
 
    public function parse($uri) {
6748
 
        
6749
 
        $uri = $this->percentEncoder->normalize($uri);
6750
 
        
6751
 
        // Regexp is as per Appendix B.
6752
 
        // Note that ["<>] are an addition to the RFC's recommended 
6753
 
        // characters, because they represent external delimeters.
6754
 
        $r_URI = '!'.
6755
 
            '(([^:/?#"<>]+):)?'. // 2. Scheme
6756
 
            '(//([^/?#"<>]*))?'. // 4. Authority
6757
 
            '([^?#"<>]*)'.       // 5. Path
6758
 
            '(\?([^#"<>]*))?'.   // 7. Query
6759
 
            '(#([^"<>]*))?'.     // 8. Fragment
6760
 
            '!';
6761
 
        
6762
 
        $matches = array();
6763
 
        $result = preg_match($r_URI, $uri, $matches);
6764
 
        
6765
 
        if (!$result) return false; // *really* invalid URI
6766
 
        
6767
 
        // seperate out parts
6768
 
        $scheme     = !empty($matches[1]) ? $matches[2] : null;
6769
 
        $authority  = !empty($matches[3]) ? $matches[4] : null;
6770
 
        $path       = $matches[5]; // always present, can be empty
6771
 
        $query      = !empty($matches[6]) ? $matches[7] : null;
6772
 
        $fragment   = !empty($matches[8]) ? $matches[9] : null;
6773
 
        
6774
 
        // further parse authority
6775
 
        if ($authority !== null) {
6776
 
            $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/";
6777
 
            $matches = array();
6778
 
            preg_match($r_authority, $authority, $matches);
6779
 
            $userinfo   = !empty($matches[1]) ? $matches[2] : null;
6780
 
            $host       = !empty($matches[3]) ? $matches[3] : '';
6781
 
            $port       = !empty($matches[4]) ? (int) $matches[5] : null;
6782
 
        } else {
6783
 
            $port = $host = $userinfo = null;
6784
 
        }
6785
 
        
6786
 
        return new HTMLPurifier_URI(
6787
 
            $scheme, $userinfo, $host, $port, $path, $query, $fragment);
6788
 
    }
6789
 
    
6790
 
}
6791
 
 
6792
 
 
6793
 
 
6794
 
 
6795
 
/**
6796
 
 * Validator for the components of a URI for a specific scheme
6797
 
 */
6798
 
class HTMLPurifier_URIScheme
6799
 
{
6800
 
    
6801
 
    /**
6802
 
     * Scheme's default port (integer)
6803
 
     */
6804
 
    public $default_port = null;
6805
 
    
6806
 
    /**
6807
 
     * Whether or not URIs of this schem are locatable by a browser
6808
 
     * http and ftp are accessible, while mailto and news are not.
6809
 
     */
6810
 
    public $browsable = false;
6811
 
    
6812
 
    /**
6813
 
     * Whether or not the URI always uses <hier_part>, resolves edge cases
6814
 
     * with making relative URIs absolute
6815
 
     */
6816
 
    public $hierarchical = false;
6817
 
    
6818
 
    /**
6819
 
     * Validates the components of a URI
6820
 
     * @note This implementation should be called by children if they define
6821
 
     *       a default port, as it does port processing.
6822
 
     * @param $uri Instance of HTMLPurifier_URI
6823
 
     * @param $config HTMLPurifier_Config object
6824
 
     * @param $context HTMLPurifier_Context object
6825
 
     * @return Bool success or failure
6826
 
     */
6827
 
    public function validate(&$uri, $config, $context) {
6828
 
        if ($this->default_port == $uri->port) $uri->port = null;
6829
 
        return true;
6830
 
    }
6831
 
    
6832
 
}
6833
 
 
6834
 
 
6835
 
 
6836
 
 
6837
 
/**
6838
 
 * Registry for retrieving specific URI scheme validator objects.
6839
 
 */
6840
 
class HTMLPurifier_URISchemeRegistry
6841
 
{
6842
 
    
6843
 
    /**
6844
 
     * Retrieve sole instance of the registry.
6845
 
     * @param $prototype Optional prototype to overload sole instance with,
6846
 
     *                   or bool true to reset to default registry.
6847
 
     * @note Pass a registry object $prototype with a compatible interface and
6848
 
     *       the function will copy it and return it all further times.
6849
 
     */
6850
 
    public static function instance($prototype = null) {
6851
 
        static $instance = null;
6852
 
        if ($prototype !== null) {
6853
 
            $instance = $prototype;
6854
 
        } elseif ($instance === null || $prototype == true) {
6855
 
            $instance = new HTMLPurifier_URISchemeRegistry();
6856
 
        }
6857
 
        return $instance;
6858
 
    }
6859
 
    
6860
 
    /**
6861
 
     * Cache of retrieved schemes.
6862
 
     */
6863
 
    protected $schemes = array();
6864
 
    
6865
 
    /**
6866
 
     * Retrieves a scheme validator object
6867
 
     * @param $scheme String scheme name like http or mailto
6868
 
     * @param $config HTMLPurifier_Config object
6869
 
     * @param $config HTMLPurifier_Context object
6870
 
     */
6871
 
    public function getScheme($scheme, $config, $context) {
6872
 
        if (!$config) $config = HTMLPurifier_Config::createDefault();
6873
 
        $null = null; // for the sake of passing by reference
6874
 
        
6875
 
        // important, otherwise attacker could include arbitrary file
6876
 
        $allowed_schemes = $config->get('URI', 'AllowedSchemes');
6877
 
        if (!$config->get('URI', 'OverrideAllowedSchemes') &&
6878
 
            !isset($allowed_schemes[$scheme])
6879
 
        ) {
6880
 
            return $null;
6881
 
        }
6882
 
        
6883
 
        if (isset($this->schemes[$scheme])) return $this->schemes[$scheme];
6884
 
        if (!isset($allowed_schemes[$scheme])) return $null;
6885
 
        
6886
 
        $class = 'HTMLPurifier_URIScheme_' . $scheme;
6887
 
        if (!class_exists($class)) return $null;
6888
 
        $this->schemes[$scheme] = new $class();
6889
 
        return $this->schemes[$scheme];
6890
 
    }
6891
 
    
6892
 
    /**
6893
 
     * Registers a custom scheme to the cache, bypassing reflection.
6894
 
     * @param $scheme Scheme name
6895
 
     * @param $scheme_obj HTMLPurifier_URIScheme object
6896
 
     */
6897
 
    public function register($scheme, $scheme_obj) {
6898
 
        $this->schemes[$scheme] = $scheme_obj;
6899
 
    }
6900
 
    
6901
 
}
6902
 
 
6903
 
 
6904
 
 
6905
 
 
6906
 
 
6907
 
/**
6908
 
 * Class for converting between different unit-lengths as specified by
6909
 
 * CSS.
6910
 
 */
6911
 
class HTMLPurifier_UnitConverter
6912
 
{
6913
 
    
6914
 
    const ENGLISH = 1;
6915
 
    const METRIC = 2;
6916
 
    const DIGITAL = 3;
6917
 
    
6918
 
    /**
6919
 
     * Units information array. Units are grouped into measuring systems
6920
 
     * (English, Metric), and are assigned an integer representing
6921
 
     * the conversion factor between that unit and the smallest unit in
6922
 
     * the system. Numeric indexes are actually magical constants that
6923
 
     * encode conversion data from one system to the next, with a O(n^2)
6924
 
     * constraint on memory (this is generally not a problem, since
6925
 
     * the number of measuring systems is small.)
6926
 
     */
6927
 
    protected static $units = array(
6928
 
        self::ENGLISH => array(
6929
 
            'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
6930
 
            'pt' => 4,
6931
 
            'pc' => 48,
6932
 
            'in' => 288,
6933
 
            self::METRIC => array('pt', '0.352777778', 'mm'),
6934
 
        ),
6935
 
        self::METRIC => array(
6936
 
            'mm' => 1,
6937
 
            'cm' => 10,
6938
 
            self::ENGLISH => array('mm', '2.83464567', 'pt'),
6939
 
        ),
6940
 
    );
6941
 
    
6942
 
    /**
6943
 
     * Minimum bcmath precision for output.
6944
 
     */
6945
 
    protected $outputPrecision;
6946
 
    
6947
 
    /**
6948
 
     * Bcmath precision for internal calculations.
6949
 
     */
6950
 
    protected $internalPrecision;
6951
 
    
6952
 
    /**
6953
 
     * Whether or not BCMath is available
6954
 
     */
6955
 
    private $bcmath;
6956
 
    
6957
 
    public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
6958
 
        $this->outputPrecision = $output_precision;
6959
 
        $this->internalPrecision = $internal_precision;
6960
 
        $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
6961
 
    }
6962
 
    
6963
 
    /**
6964
 
     * Converts a length object of one unit into another unit.
6965
 
     * @param HTMLPurifier_Length $length
6966
 
     *      Instance of HTMLPurifier_Length to convert. You must validate()
6967
 
     *      it before passing it here!
6968
 
     * @param string $to_unit
6969
 
     *      Unit to convert to.
6970
 
     * @note
6971
 
     *      About precision: This conversion function pays very special
6972
 
     *      attention to the incoming precision of values and attempts
6973
 
     *      to maintain a number of significant figure. Results are
6974
 
     *      fairly accurate up to nine digits. Some caveats:
6975
 
     *          - If a number is zero-padded as a result of this significant
6976
 
     *            figure tracking, the zeroes will be eliminated.
6977
 
     *          - If a number contains less than four sigfigs ($outputPrecision)
6978
 
     *            and this causes some decimals to be excluded, those
6979
 
     *            decimals will be added on.
6980
 
     */
6981
 
    public function convert($length, $to_unit) {
6982
 
        
6983
 
        if (!$length->isValid()) return false;
6984
 
        
6985
 
        $n    = $length->getN();
6986
 
        $unit = $length->getUnit();
6987
 
        
6988
 
        if ($n === '0' || $unit === false) {
6989
 
            return new HTMLPurifier_Length('0', false);
6990
 
        }
6991
 
        
6992
 
        $state = $dest_state = false;
6993
 
        foreach (self::$units as $k => $x) {
6994
 
            if (isset($x[$unit])) $state = $k;
6995
 
            if (isset($x[$to_unit])) $dest_state = $k;
6996
 
        }
6997
 
        if (!$state || !$dest_state) return false;
6998
 
        
6999
 
        // Some calculations about the initial precision of the number;
7000
 
        // this will be useful when we need to do final rounding.
7001
 
        $sigfigs = $this->getSigFigs($n);
7002
 
        if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
7003
 
        
7004
 
        // BCMath's internal precision deals only with decimals. Use
7005
 
        // our default if the initial number has no decimals, or increase
7006
 
        // it by how ever many decimals, thus, the number of guard digits
7007
 
        // will always be greater than or equal to internalPrecision.
7008
 
        $log = (int) floor(log(abs($n), 10));
7009
 
        $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
7010
 
        
7011
 
        for ($i = 0; $i < 2; $i++) {
7012
 
            
7013
 
            // Determine what unit IN THIS SYSTEM we need to convert to
7014
 
            if ($dest_state === $state) {
7015
 
                // Simple conversion
7016
 
                $dest_unit = $to_unit;
7017
 
            } else {
7018
 
                // Convert to the smallest unit, pending a system shift
7019
 
                $dest_unit = self::$units[$state][$dest_state][0];
7020
 
            }
7021
 
            
7022
 
            // Do the conversion if necessary
7023
 
            if ($dest_unit !== $unit) {
7024
 
                $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
7025
 
                $n = $this->mul($n, $factor, $cp);
7026
 
                $unit = $dest_unit;
7027
 
            }
7028
 
            
7029
 
            // Output was zero, so bail out early. Shouldn't ever happen.
7030
 
            if ($n === '') {
7031
 
                $n = '0';
7032
 
                $unit = $to_unit;
7033
 
                break;
7034
 
            }
7035
 
            
7036
 
            // It was a simple conversion, so bail out
7037
 
            if ($dest_state === $state) {
7038
 
                break;
7039
 
            }
7040
 
            
7041
 
            if ($i !== 0) {
7042
 
                // Conversion failed! Apparently, the system we forwarded
7043
 
                // to didn't have this unit. This should never happen!
7044
 
                return false;
7045
 
            }
7046
 
            
7047
 
            // Pre-condition: $i == 0
7048
 
            
7049
 
            // Perform conversion to next system of units
7050
 
            $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
7051
 
            $unit = self::$units[$state][$dest_state][2];
7052
 
            $state = $dest_state;
7053
 
            
7054
 
            // One more loop around to convert the unit in the new system.
7055
 
            
7056
 
        }
7057
 
        
7058
 
        // Post-condition: $unit == $to_unit
7059
 
        if ($unit !== $to_unit) return false;
7060
 
        
7061
 
        // Useful for debugging:
7062
 
        //echo "<pre>n";
7063
 
        //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
7064
 
        
7065
 
        $n = $this->round($n, $sigfigs);
7066
 
        if (strpos($n, '.') !== false) $n = rtrim($n, '0');
7067
 
        $n = rtrim($n, '.');
7068
 
        
7069
 
        return new HTMLPurifier_Length($n, $unit);
7070
 
    }
7071
 
    
7072
 
    /**
7073
 
     * Returns the number of significant figures in a string number.
7074
 
     * @param string $n Decimal number
7075
 
     * @return int number of sigfigs
7076
 
     */
7077
 
    public function getSigFigs($n) {
7078
 
        $n = ltrim($n, '0+-');
7079
 
        $dp = strpos($n, '.'); // decimal position
7080
 
        if ($dp === false) {
7081
 
            $sigfigs = strlen(rtrim($n, '0'));
7082
 
        } else {
7083
 
            $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
7084
 
            if ($dp !== 0) $sigfigs--;
7085
 
        }
7086
 
        return $sigfigs;
7087
 
    }
7088
 
    
7089
 
    /**
7090
 
     * Adds two numbers, using arbitrary precision when available.
7091
 
     */
7092
 
    private function add($s1, $s2, $scale) {
7093
 
        if ($this->bcmath) return bcadd($s1, $s2, $scale);
7094
 
        else return $this->scale($s1 + $s2, $scale);
7095
 
    }
7096
 
    
7097
 
    /**
7098
 
     * Multiples two numbers, using arbitrary precision when available.
7099
 
     */
7100
 
    private function mul($s1, $s2, $scale) {
7101
 
        if ($this->bcmath) return bcmul($s1, $s2, $scale);
7102
 
        else return $this->scale($s1 * $s2, $scale);
7103
 
    }
7104
 
    
7105
 
    /**
7106
 
     * Divides two numbers, using arbitrary precision when available.
7107
 
     */
7108
 
    private function div($s1, $s2, $scale) {
7109
 
        if ($this->bcmath) return bcdiv($s1, $s2, $scale);
7110
 
        else return $this->scale($s1 / $s2, $scale);
7111
 
    }
7112
 
    
7113
 
    /**
7114
 
     * Rounds a number according to the number of sigfigs it should have,
7115
 
     * using arbitrary precision when available.
7116
 
     */
7117
 
    private function round($n, $sigfigs) {
7118
 
        $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1
7119
 
        $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
7120
 
        $neg = $n < 0 ? '-' : ''; // Negative sign
7121
 
        if ($this->bcmath) {
7122
 
            if ($rp >= 0) {
7123
 
                $n = bcadd($n, $neg . '0.' .  str_repeat('0', $rp) . '5', $rp + 1);
7124
 
                $n = bcdiv($n, '1', $rp);
7125
 
            } else {
7126
 
                // This algorithm partially depends on the standardized
7127
 
                // form of numbers that comes out of bcmath.
7128
 
                $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
7129
 
                $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
7130
 
            }
7131
 
            return $n;
7132
 
        } else {
7133
 
            return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
7134
 
        }
7135
 
    }
7136
 
    
7137
 
    /**
7138
 
     * Scales a float to $scale digits right of decimal point, like BCMath.
7139
 
     */
7140
 
    private function scale($r, $scale) {
7141
 
        if ($scale < 0) {
7142
 
            // The f sprintf type doesn't support negative numbers, so we
7143
 
            // need to cludge things manually. First get the string.
7144
 
            $r = sprintf('%.0f', (float) $r);
7145
 
            // Due to floating point precision loss, $r will more than likely
7146
 
            // look something like 4652999999999.9234. We grab one more digit
7147
 
            // than we need to precise from $r and then use that to round
7148
 
            // appropriately.
7149
 
            $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1);
7150
 
            // Now we return it, truncating the zero that was rounded off.
7151
 
            return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
7152
 
        }
7153
 
        return sprintf('%.' . $scale . 'f', (float) $r);
7154
 
    }
7155
 
    
7156
 
}
7157
 
 
7158
 
 
7159
 
 
7160
 
/**
7161
 
 * Parses string representations into their corresponding native PHP
7162
 
 * variable type. The base implementation does a simple type-check.
7163
 
 */
7164
 
class HTMLPurifier_VarParser
7165
 
{
7166
 
    
7167
 
    const STRING    = 1;
7168
 
    const ISTRING   = 2;
7169
 
    const TEXT      = 3;
7170
 
    const ITEXT     = 4;
7171
 
    const INT       = 5;
7172
 
    const FLOAT     = 6;
7173
 
    const BOOL      = 7;
7174
 
    const LOOKUP    = 8;
7175
 
    const ALIST     = 9;
7176
 
    const HASH      = 10;
7177
 
    const MIXED     = 11;
7178
 
    
7179
 
    /**
7180
 
     * Lookup table of allowed types. Mainly for backwards compatibility, but
7181
 
     * also convenient for transforming string type names to the integer constants.
7182
 
     */
7183
 
    static public $types = array(
7184
 
        'string'    => self::STRING,
7185
 
        'istring'   => self::ISTRING,
7186
 
        'text'      => self::TEXT,
7187
 
        'itext'     => self::ITEXT,
7188
 
        'int'       => self::INT,
7189
 
        'float'     => self::FLOAT,
7190
 
        'bool'      => self::BOOL,
7191
 
        'lookup'    => self::LOOKUP,
7192
 
        'list'      => self::ALIST,
7193
 
        'hash'      => self::HASH,
7194
 
        'mixed'     => self::MIXED
7195
 
    );
7196
 
    
7197
 
    /**
7198
 
     * Lookup table of types that are string, and can have aliases or
7199
 
     * allowed value lists.
7200
 
     */
7201
 
    static public $stringTypes = array(
7202
 
        self::STRING    => true,
7203
 
        self::ISTRING   => true,
7204
 
        self::TEXT      => true,
7205
 
        self::ITEXT     => true,
7206
 
    );
7207
 
    
7208
 
    /**
7209
 
     * Validate a variable according to type. Throws
7210
 
     * HTMLPurifier_VarParserException if invalid.
7211
 
     * It may return NULL as a valid type if $allow_null is true.
7212
 
     *
7213
 
     * @param $var Variable to validate
7214
 
     * @param $type Type of variable, see HTMLPurifier_VarParser->types
7215
 
     * @param $allow_null Whether or not to permit null as a value
7216
 
     * @return Validated and type-coerced variable
7217
 
     */
7218
 
    final public function parse($var, $type, $allow_null = false) {
7219
 
        if (is_string($type)) {
7220
 
            if (!isset(HTMLPurifier_VarParser::$types[$type])) {
7221
 
                throw new HTMLPurifier_VarParserException("Invalid type '$type'");
7222
 
            } else {
7223
 
                $type = HTMLPurifier_VarParser::$types[$type];
7224
 
            }
7225
 
        }
7226
 
        $var = $this->parseImplementation($var, $type, $allow_null);
7227
 
        if ($allow_null && $var === null) return null;
7228
 
        // These are basic checks, to make sure nothing horribly wrong
7229
 
        // happened in our implementations.
7230
 
        switch ($type) {
7231
 
            case (self::STRING):
7232
 
            case (self::ISTRING):
7233
 
            case (self::TEXT):
7234
 
            case (self::ITEXT):
7235
 
                if (!is_string($var)) break;
7236
 
                if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var);
7237
 
                return $var;
7238
 
            case (self::INT):
7239
 
                if (!is_int($var)) break;
7240
 
                return $var;
7241
 
            case (self::FLOAT):
7242
 
                if (!is_float($var)) break;
7243
 
                return $var;
7244
 
            case (self::BOOL):
7245
 
                if (!is_bool($var)) break;
7246
 
                return $var;
7247
 
            case (self::LOOKUP):
7248
 
            case (self::ALIST):
7249
 
            case (self::HASH):
7250
 
                if (!is_array($var)) break;
7251
 
                if ($type === self::LOOKUP) {
7252
 
                    foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true');
7253
 
                } elseif ($type === self::ALIST) {
7254
 
                    $keys = array_keys($var);
7255
 
                    if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform');
7256
 
                }
7257
 
                return $var;
7258
 
            case (self::MIXED):
7259
 
                return $var;
7260
 
            default:
7261
 
                $this->errorInconsistent(get_class($this), $type);
7262
 
        }
7263
 
        $this->errorGeneric($var, $type);
7264
 
    }
7265
 
    
7266
 
    /**
7267
 
     * Actually implements the parsing. Base implementation is to not
7268
 
     * do anything to $var. Subclasses should overload this!
7269
 
     */
7270
 
    protected function parseImplementation($var, $type, $allow_null) {
7271
 
        return $var;
7272
 
    }
7273
 
    
7274
 
    /**
7275
 
     * Throws an exception.
7276
 
     */
7277
 
    protected function error($msg) {
7278
 
        throw new HTMLPurifier_VarParserException($msg);
7279
 
    }
7280
 
    
7281
 
    /**
7282
 
     * Throws an inconsistency exception.
7283
 
     * @note This should not ever be called. It would be called if we
7284
 
     *       extend the allowed values of HTMLPurifier_VarParser without
7285
 
     *       updating subclasses.
7286
 
     */
7287
 
    protected function errorInconsistent($class, $type) {
7288
 
        throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented");
7289
 
    }
7290
 
    
7291
 
    /**
7292
 
     * Generic error for if a type didn't work.
7293
 
     */
7294
 
    protected function errorGeneric($var, $type) {
7295
 
        $vtype = gettype($var);
7296
 
        $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype");
7297
 
    }
7298
 
    
7299
 
    static public function getTypeName($type) {
7300
 
        static $lookup;
7301
 
        if (!$lookup) {
7302
 
            // Lazy load the alternative lookup table
7303
 
            $lookup = array_flip(HTMLPurifier_VarParser::$types);
7304
 
        }
7305
 
        if (!isset($lookup[$type])) return 'unknown';
7306
 
        return $lookup[$type];
7307
 
    }
7308
 
    
7309
 
}
7310
 
 
7311
 
 
7312
 
 
7313
 
/**
7314
 
 * Exception type for HTMLPurifier_VarParser
7315
 
 */
7316
 
class HTMLPurifier_VarParserException extends HTMLPurifier_Exception
7317
 
{
7318
 
    
7319
 
}
7320
 
 
7321
 
 
7322
 
 
7323
 
/**
7324
 
 * Validates the HTML attribute style, otherwise known as CSS.
7325
 
 * @note We don't implement the whole CSS specification, so it might be
7326
 
 *       difficult to reuse this component in the context of validating
7327
 
 *       actual stylesheet declarations.
7328
 
 * @note If we were really serious about validating the CSS, we would
7329
 
 *       tokenize the styles and then parse the tokens. Obviously, we
7330
 
 *       are not doing that. Doing that could seriously harm performance,
7331
 
 *       but would make these components a lot more viable for a CSS
7332
 
 *       filtering solution.
7333
 
 */
7334
 
class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
7335
 
{
7336
 
    
7337
 
    public function validate($css, $config, $context) {
7338
 
        
7339
 
        $css = $this->parseCDATA($css);
7340
 
        
7341
 
        $definition = $config->getCSSDefinition();
7342
 
        
7343
 
        // we're going to break the spec and explode by semicolons.
7344
 
        // This is because semicolon rarely appears in escaped form
7345
 
        // Doing this is generally flaky but fast
7346
 
        // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI
7347
 
        // for details
7348
 
        
7349
 
        $declarations = explode(';', $css);
7350
 
        $propvalues = array();
7351
 
        
7352
 
        /**
7353
 
         * Name of the current CSS property being validated.
7354
 
         */
7355
 
        $property = false;
7356
 
        $context->register('CurrentCSSProperty', $property);
7357
 
        
7358
 
        foreach ($declarations as $declaration) {
7359
 
            if (!$declaration) continue;
7360
 
            if (!strpos($declaration, ':')) continue;
7361
 
            list($property, $value) = explode(':', $declaration, 2);
7362
 
            $property = trim($property);
7363
 
            $value    = trim($value);
7364
 
            $ok = false;
7365
 
            do {
7366
 
                if (isset($definition->info[$property])) {
7367
 
                    $ok = true;
7368
 
                    break;
7369
 
                }
7370
 
                if (ctype_lower($property)) break;
7371
 
                $property = strtolower($property);
7372
 
                if (isset($definition->info[$property])) {
7373
 
                    $ok = true;
7374
 
                    break;
7375
 
                }
7376
 
            } while(0);
7377
 
            if (!$ok) continue;
7378
 
            // inefficient call, since the validator will do this again
7379
 
            if (strtolower(trim($value)) !== 'inherit') {
7380
 
                // inherit works for everything (but only on the base property)
7381
 
                $result = $definition->info[$property]->validate(
7382
 
                    $value, $config, $context );
7383
 
            } else {
7384
 
                $result = 'inherit';
7385
 
            }
7386
 
            if ($result === false) continue;
7387
 
            $propvalues[$property] = $result;
7388
 
        }
7389
 
        
7390
 
        $context->destroy('CurrentCSSProperty');
7391
 
        
7392
 
        // procedure does not write the new CSS simultaneously, so it's
7393
 
        // slightly inefficient, but it's the only way of getting rid of
7394
 
        // duplicates. Perhaps config to optimize it, but not now.
7395
 
        
7396
 
        $new_declarations = '';
7397
 
        foreach ($propvalues as $prop => $value) {
7398
 
            $new_declarations .= "$prop:$value;";
7399
 
        }
7400
 
        
7401
 
        return $new_declarations ? $new_declarations : false;
7402
 
        
7403
 
    }
7404
 
    
7405
 
}
7406
 
 
7407
 
 
7408
 
 
7409
 
 
7410
 
// Enum = Enumerated
7411
 
/**
7412
 
 * Validates a keyword against a list of valid values.
7413
 
 * @warning The case-insensitive compare of this function uses PHP's
7414
 
 *          built-in strtolower and ctype_lower functions, which may
7415
 
 *          cause problems with international comparisons
7416
 
 */
7417
 
class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef
7418
 
{
7419
 
    
7420
 
    /**
7421
 
     * Lookup table of valid values.
7422
 
     * @todo Make protected
7423
 
     */
7424
 
    public $valid_values   = array();
7425
 
    
7426
 
    /**
7427
 
     * Bool indicating whether or not enumeration is case sensitive.
7428
 
     * @note In general this is always case insensitive.
7429
 
     */
7430
 
    protected $case_sensitive = false; // values according to W3C spec
7431
 
    
7432
 
    /**
7433
 
     * @param $valid_values List of valid values
7434
 
     * @param $case_sensitive Bool indicating whether or not case sensitive
7435
 
     */
7436
 
    public function __construct(
7437
 
        $valid_values = array(), $case_sensitive = false
7438
 
    ) {
7439
 
        $this->valid_values = array_flip($valid_values);
7440
 
        $this->case_sensitive = $case_sensitive;
7441
 
    }
7442
 
    
7443
 
    public function validate($string, $config, $context) {
7444
 
        $string = trim($string);
7445
 
        if (!$this->case_sensitive) {
7446
 
            // we may want to do full case-insensitive libraries
7447
 
            $string = ctype_lower($string) ? $string : strtolower($string);
7448
 
        }
7449
 
        $result = isset($this->valid_values[$string]);
7450
 
        
7451
 
        return $result ? $string : false;
7452
 
    }
7453
 
    
7454
 
    /**
7455
 
     * @param $string In form of comma-delimited list of case-insensitive
7456
 
     *      valid values. Example: "foo,bar,baz". Prepend "s:" to make
7457
 
     *      case sensitive
7458
 
     */
7459
 
    public function make($string) {
7460
 
        if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') {
7461
 
            $string = substr($string, 2);
7462
 
            $sensitive = true;
7463
 
        } else {
7464
 
            $sensitive = false;
7465
 
        }
7466
 
        $values = explode(',', $string);
7467
 
        return new HTMLPurifier_AttrDef_Enum($values, $sensitive);
7468
 
    }
7469
 
    
7470
 
}
7471
 
 
7472
 
 
7473
 
 
7474
 
 
7475
 
/**
7476
 
 * Validates an integer.
7477
 
 * @note While this class was modeled off the CSS definition, no currently
7478
 
 *       allowed CSS uses this type.  The properties that do are: widows,
7479
 
 *       orphans, z-index, counter-increment, counter-reset.  Some of the
7480
 
 *       HTML attributes, however, find use for a non-negative version of this.
7481
 
 */
7482
 
class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef
7483
 
{
7484
 
    
7485
 
    /**
7486
 
     * Bool indicating whether or not negative values are allowed
7487
 
     */
7488
 
    protected $negative = true;
7489
 
    
7490
 
    /**
7491
 
     * Bool indicating whether or not zero is allowed
7492
 
     */
7493
 
    protected $zero = true;
7494
 
    
7495
 
    /**
7496
 
     * Bool indicating whether or not positive values are allowed
7497
 
     */
7498
 
    protected $positive = true;
7499
 
    
7500
 
    /**
7501
 
     * @param $negative Bool indicating whether or not negative values are allowed
7502
 
     * @param $zero Bool indicating whether or not zero is allowed
7503
 
     * @param $positive Bool indicating whether or not positive values are allowed
7504
 
     */
7505
 
    public function __construct(
7506
 
        $negative = true, $zero = true, $positive = true
7507
 
    ) {
7508
 
        $this->negative = $negative;
7509
 
        $this->zero     = $zero;
7510
 
        $this->positive = $positive;
7511
 
    }
7512
 
    
7513
 
    public function validate($integer, $config, $context) {
7514
 
        
7515
 
        $integer = $this->parseCDATA($integer);
7516
 
        if ($integer === '') return false;
7517
 
        
7518
 
        // we could possibly simply typecast it to integer, but there are
7519
 
        // certain fringe cases that must not return an integer.
7520
 
        
7521
 
        // clip leading sign
7522
 
        if ( $this->negative && $integer[0] === '-' ) {
7523
 
            $digits = substr($integer, 1);
7524
 
            if ($digits === '0') $integer = '0'; // rm minus sign for zero
7525
 
        } elseif( $this->positive && $integer[0] === '+' ) {
7526
 
            $digits = $integer = substr($integer, 1); // rm unnecessary plus
7527
 
        } else {
7528
 
            $digits = $integer;
7529
 
        }
7530
 
        
7531
 
        // test if it's numeric
7532
 
        if (!ctype_digit($digits)) return false;
7533
 
        
7534
 
        // perform scope tests
7535
 
        if (!$this->zero     && $integer == 0) return false;
7536
 
        if (!$this->positive && $integer > 0) return false;
7537
 
        if (!$this->negative && $integer < 0) return false;
7538
 
        
7539
 
        return $integer;
7540
 
        
7541
 
    }
7542
 
    
7543
 
}
7544
 
 
7545
 
 
7546
 
 
7547
 
 
7548
 
/**
7549
 
 * Validates the HTML attribute lang, effectively a language code.
7550
 
 * @note Built according to RFC 3066, which obsoleted RFC 1766
7551
 
 */
7552
 
class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef
7553
 
{
7554
 
    
7555
 
    public function validate($string, $config, $context) {
7556
 
        
7557
 
        $string = trim($string);
7558
 
        if (!$string) return false;
7559
 
        
7560
 
        $subtags = explode('-', $string);
7561
 
        $num_subtags = count($subtags);
7562
 
        
7563
 
        if ($num_subtags == 0) return false; // sanity check
7564
 
        
7565
 
        // process primary subtag : $subtags[0]
7566
 
        $length = strlen($subtags[0]);
7567
 
        switch ($length) {
7568
 
            case 0:
7569
 
                return false;
7570
 
            case 1:
7571
 
                if (! ($subtags[0] == 'x' || $subtags[0] == 'i') ) {
7572
 
                    return false;
7573
 
                }
7574
 
                break;
7575
 
            case 2:
7576
 
            case 3:
7577
 
                if (! ctype_alpha($subtags[0]) ) {
7578
 
                    return false;
7579
 
                } elseif (! ctype_lower($subtags[0]) ) {
7580
 
                    $subtags[0] = strtolower($subtags[0]);
7581
 
                }
7582
 
                break;
7583
 
            default:
7584
 
                return false;
7585
 
        }
7586
 
        
7587
 
        $new_string = $subtags[0];
7588
 
        if ($num_subtags == 1) return $new_string;
7589
 
        
7590
 
        // process second subtag : $subtags[1]
7591
 
        $length = strlen($subtags[1]);
7592
 
        if ($length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) {
7593
 
            return $new_string;
7594
 
        }
7595
 
        if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]);
7596
 
        
7597
 
        $new_string .= '-' . $subtags[1];
7598
 
        if ($num_subtags == 2) return $new_string;
7599
 
        
7600
 
        // process all other subtags, index 2 and up
7601
 
        for ($i = 2; $i < $num_subtags; $i++) {
7602
 
            $length = strlen($subtags[$i]);
7603
 
            if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
7604
 
                return $new_string;
7605
 
            }
7606
 
            if (!ctype_lower($subtags[$i])) {
7607
 
                $subtags[$i] = strtolower($subtags[$i]);
7608
 
            }
7609
 
            $new_string .= '-' . $subtags[$i];
7610
 
        }
7611
 
        
7612
 
        return $new_string;
7613
 
        
7614
 
    }
7615
 
    
7616
 
}
7617
 
 
7618
 
 
7619
 
 
7620
 
 
7621
 
/**
7622
 
 * Decorator that, depending on a token, switches between two definitions.
7623
 
 */
7624
 
class HTMLPurifier_AttrDef_Switch
7625
 
{
7626
 
    
7627
 
    protected $tag;
7628
 
    protected $withTag, $withoutTag;
7629
 
    
7630
 
    /**
7631
 
     * @param string $tag Tag name to switch upon
7632
 
     * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag
7633
 
     * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token
7634
 
     */
7635
 
    public function __construct($tag, $with_tag, $without_tag) {
7636
 
        $this->tag = $tag;
7637
 
        $this->withTag = $with_tag;
7638
 
        $this->withoutTag = $without_tag;
7639
 
    }
7640
 
    
7641
 
    public function validate($string, $config, $context) {
7642
 
        $token = $context->get('CurrentToken', true);
7643
 
        if (!$token || $token->name !== $this->tag) {
7644
 
            return $this->withoutTag->validate($string, $config, $context);
7645
 
        } else {
7646
 
            return $this->withTag->validate($string, $config, $context);
7647
 
        }
7648
 
    }
7649
 
    
7650
 
}
7651
 
 
7652
 
 
7653
 
 
7654
 
/**
7655
 
 * Validates arbitrary text according to the HTML spec.
7656
 
 */
7657
 
class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef
7658
 
{
7659
 
    
7660
 
    public function validate($string, $config, $context) {
7661
 
        return $this->parseCDATA($string);
7662
 
    }
7663
 
    
7664
 
}
7665
 
 
7666
 
 
7667
 
 
7668
 
 
7669
 
/**
7670
 
 * Validates a URI as defined by RFC 3986.
7671
 
 * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme
7672
 
 */
7673
 
class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
7674
 
{
7675
 
    
7676
 
    protected $parser;
7677
 
    protected $embedsResource;
7678
 
    
7679
 
    /**
7680
 
     * @param $embeds_resource_resource Does the URI here result in an extra HTTP request?
7681
 
     */
7682
 
    public function __construct($embeds_resource = false) {
7683
 
        $this->parser = new HTMLPurifier_URIParser();
7684
 
        $this->embedsResource = (bool) $embeds_resource;
7685
 
    }
7686
 
    
7687
 
    public function make($string) {
7688
 
        $embeds = (bool) $string;
7689
 
        return new HTMLPurifier_AttrDef_URI($embeds);
7690
 
    }
7691
 
    
7692
 
    public function validate($uri, $config, $context) {
7693
 
        
7694
 
        if ($config->get('URI', 'Disable')) return false;
7695
 
        
7696
 
        $uri = $this->parseCDATA($uri);
7697
 
        
7698
 
        // parse the URI
7699
 
        $uri = $this->parser->parse($uri);
7700
 
        if ($uri === false) return false;
7701
 
        
7702
 
        // add embedded flag to context for validators
7703
 
        $context->register('EmbeddedURI', $this->embedsResource); 
7704
 
        
7705
 
        $ok = false;
7706
 
        do {
7707
 
            
7708
 
            // generic validation
7709
 
            $result = $uri->validate($config, $context);
7710
 
            if (!$result) break;
7711
 
            
7712
 
            // chained filtering
7713
 
            $uri_def = $config->getDefinition('URI');
7714
 
            $result = $uri_def->filter($uri, $config, $context);
7715
 
            if (!$result) break;
7716
 
            
7717
 
            // scheme-specific validation 
7718
 
            $scheme_obj = $uri->getSchemeObj($config, $context);
7719
 
            if (!$scheme_obj) break;
7720
 
            if ($this->embedsResource && !$scheme_obj->browsable) break;
7721
 
            $result = $scheme_obj->validate($uri, $config, $context);
7722
 
            if (!$result) break;
7723
 
            
7724
 
            // Post chained filtering
7725
 
            $result = $uri_def->postFilter($uri, $config, $context);
7726
 
            if (!$result) break;
7727
 
            
7728
 
            // survived gauntlet
7729
 
            $ok = true;
7730
 
            
7731
 
        } while (false);
7732
 
        
7733
 
        $context->destroy('EmbeddedURI');
7734
 
        if (!$ok) return false;
7735
 
        
7736
 
        // back to string
7737
 
        return $uri->toString();
7738
 
        
7739
 
    }
7740
 
    
7741
 
}
7742
 
 
7743
 
 
7744
 
 
7745
 
 
7746
 
 
7747
 
/**
7748
 
 * Validates a number as defined by the CSS spec.
7749
 
 */
7750
 
class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
7751
 
{
7752
 
    
7753
 
    /**
7754
 
     * Bool indicating whether or not only positive values allowed.
7755
 
     */
7756
 
    protected $non_negative = false;
7757
 
    
7758
 
    /**
7759
 
     * @param $non_negative Bool indicating whether negatives are forbidden
7760
 
     */
7761
 
    public function __construct($non_negative = false) {
7762
 
        $this->non_negative = $non_negative;
7763
 
    }
7764
 
    
7765
 
    /**
7766
 
     * @warning Some contexts do not pass $config, $context. These
7767
 
     *          variables should not be used without checking HTMLPurifier_Length
7768
 
     */
7769
 
    public function validate($number, $config, $context) {
7770
 
        
7771
 
        $number = $this->parseCDATA($number);
7772
 
        
7773
 
        if ($number === '') return false;
7774
 
        if ($number === '0') return '0';
7775
 
        
7776
 
        $sign = '';
7777
 
        switch ($number[0]) {
7778
 
            case '-':
7779
 
                if ($this->non_negative) return false;
7780
 
                $sign = '-';
7781
 
            case '+':
7782
 
                $number = substr($number, 1);
7783
 
        }
7784
 
        
7785
 
        if (ctype_digit($number)) {
7786
 
            $number = ltrim($number, '0');
7787
 
            return $number ? $sign . $number : '0';
7788
 
        }
7789
 
        
7790
 
        // Period is the only non-numeric character allowed
7791
 
        if (strpos($number, '.') === false) return false;
7792
 
        
7793
 
        list($left, $right) = explode('.', $number, 2);
7794
 
        
7795
 
        if ($left === '' && $right === '') return false;
7796
 
        if ($left !== '' && !ctype_digit($left)) return false;
7797
 
        
7798
 
        $left  = ltrim($left,  '0');
7799
 
        $right = rtrim($right, '0');
7800
 
        
7801
 
        if ($right === '') {
7802
 
            return $left ? $sign . $left : '0';
7803
 
        } elseif (!ctype_digit($right)) {
7804
 
            return false;
7805
 
        }
7806
 
        
7807
 
        return $sign . $left . '.' . $right;
7808
 
        
7809
 
    }
7810
 
    
7811
 
}
7812
 
 
7813
 
 
7814
 
 
7815
 
 
7816
 
class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number
7817
 
{
7818
 
    
7819
 
    public function __construct() {
7820
 
        parent::__construct(false); // opacity is non-negative, but we will clamp it
7821
 
    }
7822
 
    
7823
 
    public function validate($number, $config, $context) {
7824
 
        $result = parent::validate($number, $config, $context);
7825
 
        if ($result === false) return $result;
7826
 
        $float = (float) $result;
7827
 
        if ($float < 0.0) $result = '0';
7828
 
        if ($float > 1.0) $result = '1';
7829
 
        return $result;
7830
 
    }
7831
 
    
7832
 
}
7833
 
 
7834
 
 
7835
 
 
7836
 
/**
7837
 
 * Validates shorthand CSS property background.
7838
 
 * @warning Does not support url tokens that have internal spaces.
7839
 
 */
7840
 
class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
7841
 
{
7842
 
    
7843
 
    /**
7844
 
     * Local copy of component validators.
7845
 
     * @note See HTMLPurifier_AttrDef_Font::$info for a similar impl.
7846
 
     */
7847
 
    protected $info;
7848
 
    
7849
 
    public function __construct($config) {
7850
 
        $def = $config->getCSSDefinition();
7851
 
        $this->info['background-color'] = $def->info['background-color'];
7852
 
        $this->info['background-image'] = $def->info['background-image'];
7853
 
        $this->info['background-repeat'] = $def->info['background-repeat'];
7854
 
        $this->info['background-attachment'] = $def->info['background-attachment'];
7855
 
        $this->info['background-position'] = $def->info['background-position'];
7856
 
    }
7857
 
    
7858
 
    public function validate($string, $config, $context) {
7859
 
        
7860
 
        // regular pre-processing
7861
 
        $string = $this->parseCDATA($string);
7862
 
        if ($string === '') return false;
7863
 
        
7864
 
        // munge rgb() decl if necessary
7865
 
        $string = $this->mungeRgb($string);
7866
 
        
7867
 
        // assumes URI doesn't have spaces in it
7868
 
        $bits = explode(' ', strtolower($string)); // bits to process
7869
 
        
7870
 
        $caught = array();
7871
 
        $caught['color']    = false;
7872
 
        $caught['image']    = false;
7873
 
        $caught['repeat']   = false;
7874
 
        $caught['attachment'] = false;
7875
 
        $caught['position'] = false;
7876
 
        
7877
 
        $i = 0; // number of catches
7878
 
        $none = false;
7879
 
        
7880
 
        foreach ($bits as $bit) {
7881
 
            if ($bit === '') continue;
7882
 
            foreach ($caught as $key => $status) {
7883
 
                if ($key != 'position') {
7884
 
                    if ($status !== false) continue;
7885
 
                    $r = $this->info['background-' . $key]->validate($bit, $config, $context);
7886
 
                } else {
7887
 
                    $r = $bit;
7888
 
                }
7889
 
                if ($r === false) continue;
7890
 
                if ($key == 'position') {
7891
 
                    if ($caught[$key] === false) $caught[$key] = '';
7892
 
                    $caught[$key] .= $r . ' ';
7893
 
                } else {
7894
 
                    $caught[$key] = $r;
7895
 
                }
7896
 
                $i++;
7897
 
                break;
7898
 
            }
7899
 
        }
7900
 
        
7901
 
        if (!$i) return false;
7902
 
        if ($caught['position'] !== false) {
7903
 
            $caught['position'] = $this->info['background-position']->
7904
 
                validate($caught['position'], $config, $context);
7905
 
        }
7906
 
        
7907
 
        $ret = array();
7908
 
        foreach ($caught as $value) {
7909
 
            if ($value === false) continue;
7910
 
            $ret[] = $value;
7911
 
        }
7912
 
        
7913
 
        if (empty($ret)) return false;
7914
 
        return implode(' ', $ret);
7915
 
        
7916
 
    }
7917
 
    
7918
 
}
7919
 
 
7920
 
 
7921
 
 
7922
 
 
7923
 
/* W3C says:
7924
 
    [ // adjective and number must be in correct order, even if
7925
 
      // you could switch them without introducing ambiguity.
7926
 
      // some browsers support that syntax
7927
 
        [
7928
 
            <percentage> | <length> | left | center | right
7929
 
        ]
7930
 
        [ 
7931
 
            <percentage> | <length> | top | center | bottom
7932
 
        ]?
7933
 
    ] |
7934
 
    [ // this signifies that the vertical and horizontal adjectives
7935
 
      // can be arbitrarily ordered, however, there can only be two,
7936
 
      // one of each, or none at all
7937
 
        [
7938
 
            left | center | right
7939
 
        ] ||
7940
 
        [
7941
 
            top | center | bottom
7942
 
        ]
7943
 
    ]
7944
 
    top, left = 0%
7945
 
    center, (none) = 50%
7946
 
    bottom, right = 100%
7947
 
*/
7948
 
 
7949
 
/* QuirksMode says:
7950
 
    keyword + length/percentage must be ordered correctly, as per W3C
7951
 
    
7952
 
    Internet Explorer and Opera, however, support arbitrary ordering. We
7953
 
    should fix it up.
7954
 
    
7955
 
    Minor issue though, not strictly necessary.
7956
 
*/
7957
 
 
7958
 
// control freaks may appreciate the ability to convert these to
7959
 
// percentages or something, but it's not necessary
7960
 
 
7961
 
/**
7962
 
 * Validates the value of background-position.
7963
 
 */
7964
 
class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
7965
 
{
7966
 
    
7967
 
    protected $length;
7968
 
    protected $percentage;
7969
 
    
7970
 
    public function __construct() {
7971
 
        $this->length     = new HTMLPurifier_AttrDef_CSS_Length();
7972
 
        $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
7973
 
    }
7974
 
    
7975
 
    public function validate($string, $config, $context) {
7976
 
        $string = $this->parseCDATA($string);
7977
 
        $bits = explode(' ', $string);
7978
 
        
7979
 
        $keywords = array();
7980
 
        $keywords['h'] = false; // left, right
7981
 
        $keywords['v'] = false; // top, bottom
7982
 
        $keywords['c'] = false; // center
7983
 
        $measures = array();
7984
 
        
7985
 
        $i = 0;
7986
 
        
7987
 
        $lookup = array(
7988
 
            'top' => 'v',
7989
 
            'bottom' => 'v',
7990
 
            'left' => 'h',
7991
 
            'right' => 'h',
7992
 
            'center' => 'c'
7993
 
        );
7994
 
        
7995
 
        foreach ($bits as $bit) {
7996
 
            if ($bit === '') continue;
7997
 
            
7998
 
            // test for keyword
7999
 
            $lbit = ctype_lower($bit) ? $bit : strtolower($bit);
8000
 
            if (isset($lookup[$lbit])) {
8001
 
                $status = $lookup[$lbit];
8002
 
                $keywords[$status] = $lbit;
8003
 
                $i++;
8004
 
            }
8005
 
            
8006
 
            // test for length
8007
 
            $r = $this->length->validate($bit, $config, $context);
8008
 
            if ($r !== false) {
8009
 
                $measures[] = $r;
8010
 
                $i++;
8011
 
            }
8012
 
            
8013
 
            // test for percentage
8014
 
            $r = $this->percentage->validate($bit, $config, $context);
8015
 
            if ($r !== false) {
8016
 
                $measures[] = $r;
8017
 
                $i++;
8018
 
            }
8019
 
            
8020
 
        }
8021
 
        
8022
 
        if (!$i) return false; // no valid values were caught
8023
 
        
8024
 
        
8025
 
        $ret = array();
8026
 
        
8027
 
        // first keyword
8028
 
        if     ($keywords['h'])     $ret[] = $keywords['h'];
8029
 
        elseif (count($measures))   $ret[] = array_shift($measures);
8030
 
        elseif ($keywords['c']) {
8031
 
            $ret[] = $keywords['c'];
8032
 
            $keywords['c'] = false; // prevent re-use: center = center center
8033
 
        }
8034
 
        
8035
 
        if     ($keywords['v'])     $ret[] = $keywords['v'];
8036
 
        elseif (count($measures))   $ret[] = array_shift($measures);
8037
 
        elseif ($keywords['c'])     $ret[] = $keywords['c'];
8038
 
        
8039
 
        if (empty($ret)) return false;
8040
 
        return implode(' ', $ret);
8041
 
        
8042
 
    }
8043
 
    
8044
 
}
8045
 
 
8046
 
 
8047
 
 
8048
 
 
8049
 
/**
8050
 
 * Validates the border property as defined by CSS.
8051
 
 */
8052
 
class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef
8053
 
{
8054
 
    
8055
 
    /**
8056
 
     * Local copy of properties this property is shorthand for.
8057
 
     */
8058
 
    protected $info = array();
8059
 
    
8060
 
    public function __construct($config) {
8061
 
        $def = $config->getCSSDefinition();
8062
 
        $this->info['border-width'] = $def->info['border-width'];
8063
 
        $this->info['border-style'] = $def->info['border-style'];
8064
 
        $this->info['border-top-color'] = $def->info['border-top-color'];
8065
 
    }
8066
 
    
8067
 
    public function validate($string, $config, $context) {
8068
 
        $string = $this->parseCDATA($string);
8069
 
        $string = $this->mungeRgb($string);
8070
 
        $bits = explode(' ', $string);
8071
 
        $done = array(); // segments we've finished
8072
 
        $ret = ''; // return value
8073
 
        foreach ($bits as $bit) {
8074
 
            foreach ($this->info as $propname => $validator) {
8075
 
                if (isset($done[$propname])) continue;
8076
 
                $r = $validator->validate($bit, $config, $context);
8077
 
                if ($r !== false) {
8078
 
                    $ret .= $r . ' ';
8079
 
                    $done[$propname] = true;
8080
 
                    break;
8081
 
                }
8082
 
            }
8083
 
        }
8084
 
        return rtrim($ret);
8085
 
    }
8086
 
    
8087
 
}
8088
 
 
8089
 
 
8090
 
 
8091
 
 
8092
 
/**
8093
 
 * Validates Color as defined by CSS.
8094
 
 */
8095
 
class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
8096
 
{
8097
 
    
8098
 
    public function validate($color, $config, $context) {
8099
 
        
8100
 
        static $colors = null;
8101
 
        if ($colors === null) $colors = $config->get('Core', 'ColorKeywords');
8102
 
        
8103
 
        $color = trim($color);
8104
 
        if ($color === '') return false;
8105
 
        
8106
 
        $lower = strtolower($color);
8107
 
        if (isset($colors[$lower])) return $colors[$lower];
8108
 
        
8109
 
        if (strpos($color, 'rgb(') !== false) {
8110
 
            // rgb literal handling
8111
 
            $length = strlen($color);
8112
 
            if (strpos($color, ')') !== $length - 1) return false;
8113
 
            $triad = substr($color, 4, $length - 4 - 1);
8114
 
            $parts = explode(',', $triad);
8115
 
            if (count($parts) !== 3) return false;
8116
 
            $type = false; // to ensure that they're all the same type
8117
 
            $new_parts = array();
8118
 
            foreach ($parts as $part) {
8119
 
                $part = trim($part);
8120
 
                if ($part === '') return false;
8121
 
                $length = strlen($part);
8122
 
                if ($part[$length - 1] === '%') {
8123
 
                    // handle percents
8124
 
                    if (!$type) {
8125
 
                        $type = 'percentage';
8126
 
                    } elseif ($type !== 'percentage') {
8127
 
                        return false;
8128
 
                    }
8129
 
                    $num = (float) substr($part, 0, $length - 1);
8130
 
                    if ($num < 0) $num = 0;
8131
 
                    if ($num > 100) $num = 100;
8132
 
                    $new_parts[] = "$num%";
8133
 
                } else {
8134
 
                    // handle integers
8135
 
                    if (!$type) {
8136
 
                        $type = 'integer';
8137
 
                    } elseif ($type !== 'integer') {
8138
 
                        return false;
8139
 
                    }
8140
 
                    $num = (int) $part;
8141
 
                    if ($num < 0) $num = 0;
8142
 
                    if ($num > 255) $num = 255;
8143
 
                    $new_parts[] = (string) $num;
8144
 
                }
8145
 
            }
8146
 
            $new_triad = implode(',', $new_parts);
8147
 
            $color = "rgb($new_triad)";
8148
 
        } else {
8149
 
            // hexadecimal handling
8150
 
            if ($color[0] === '#') {
8151
 
                $hex = substr($color, 1);
8152
 
            } else {
8153
 
                $hex = $color;
8154
 
                $color = '#' . $color;
8155
 
            }
8156
 
            $length = strlen($hex);
8157
 
            if ($length !== 3 && $length !== 6) return false;
8158
 
            if (!ctype_xdigit($hex)) return false;
8159
 
        }
8160
 
        
8161
 
        return $color;
8162
 
        
8163
 
    }
8164
 
    
8165
 
}
8166
 
 
8167
 
 
8168
 
 
8169
 
 
8170
 
/**
8171
 
 * Allows multiple validators to attempt to validate attribute.
8172
 
 * 
8173
 
 * Composite is just what it sounds like: a composite of many validators.
8174
 
 * This means that multiple HTMLPurifier_AttrDef objects will have a whack
8175
 
 * at the string.  If one of them passes, that's what is returned.  This is
8176
 
 * especially useful for CSS values, which often are a choice between
8177
 
 * an enumerated set of predefined values or a flexible data type.
8178
 
 */
8179
 
class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef
8180
 
{
8181
 
    
8182
 
    /**
8183
 
     * List of HTMLPurifier_AttrDef objects that may process strings
8184
 
     * @todo Make protected
8185
 
     */
8186
 
    public $defs;
8187
 
    
8188
 
    /**
8189
 
     * @param $defs List of HTMLPurifier_AttrDef objects
8190
 
     */
8191
 
    public function __construct($defs) {
8192
 
        $this->defs = $defs;
8193
 
    }
8194
 
    
8195
 
    public function validate($string, $config, $context) {
8196
 
        foreach ($this->defs as $i => $def) {
8197
 
            $result = $this->defs[$i]->validate($string, $config, $context);
8198
 
            if ($result !== false) return $result;
8199
 
        }
8200
 
        return false;
8201
 
    }
8202
 
    
8203
 
}
8204
 
 
8205
 
 
8206
 
 
8207
 
 
8208
 
/**
8209
 
 * Decorator which enables CSS properties to be disabled for specific elements.
8210
 
 */
8211
 
class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef
8212
 
{
8213
 
    protected $def, $element;
8214
 
    
8215
 
    /**
8216
 
     * @param $def Definition to wrap
8217
 
     * @param $element Element to deny
8218
 
     */
8219
 
    public function __construct($def, $element) {
8220
 
        $this->def = $def;
8221
 
        $this->element = $element;
8222
 
    }
8223
 
    /**
8224
 
     * Checks if CurrentToken is set and equal to $this->element
8225
 
     */
8226
 
    public function validate($string, $config, $context) {
8227
 
        $token = $context->get('CurrentToken', true);
8228
 
        if ($token && $token->name == $this->element) return false;
8229
 
        return $this->def->validate($string, $config, $context);
8230
 
    }
8231
 
}
8232
 
 
8233
 
 
8234
 
 
8235
 
/**
8236
 
 * Microsoft's proprietary filter: CSS property
8237
 
 * @note Currently supports the alpha filter. In the future, this will
8238
 
 *       probably need an extensible framework
8239
 
 */
8240
 
class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef
8241
 
{
8242
 
    
8243
 
    protected $intValidator;
8244
 
    
8245
 
    public function __construct() {
8246
 
        $this->intValidator = new HTMLPurifier_AttrDef_Integer();
8247
 
    }
8248
 
    
8249
 
    public function validate($value, $config, $context) {
8250
 
        $value = $this->parseCDATA($value);
8251
 
        if ($value === 'none') return $value;
8252
 
        // if we looped this we could support multiple filters
8253
 
        $function_length = strcspn($value, '(');
8254
 
        $function = trim(substr($value, 0, $function_length));
8255
 
        if ($function !== 'alpha' &&
8256
 
            $function !== 'Alpha' &&
8257
 
            $function !== 'progid:DXImageTransform.Microsoft.Alpha'
8258
 
            ) return false;
8259
 
        $cursor = $function_length + 1;
8260
 
        $parameters_length = strcspn($value, ')', $cursor);
8261
 
        $parameters = substr($value, $cursor, $parameters_length);
8262
 
        $params = explode(',', $parameters);
8263
 
        $ret_params = array();
8264
 
        $lookup = array();
8265
 
        foreach ($params as $param) {
8266
 
            list($key, $value) = explode('=', $param);
8267
 
            $key   = trim($key);
8268
 
            $value = trim($value);
8269
 
            if (isset($lookup[$key])) continue;
8270
 
            if ($key !== 'opacity') continue;
8271
 
            $value = $this->intValidator->validate($value, $config, $context);
8272
 
            if ($value === false) continue;
8273
 
            $int = (int) $value;
8274
 
            if ($int > 100) $value = '100';
8275
 
            if ($int < 0) $value = '0';
8276
 
            $ret_params[] = "$key=$value";
8277
 
            $lookup[$key] = true;
8278
 
        }
8279
 
        $ret_parameters = implode(',', $ret_params);
8280
 
        $ret_function = "$function($ret_parameters)";
8281
 
        return $ret_function;
8282
 
    }
8283
 
    
8284
 
}
8285
 
 
8286
 
 
8287
 
 
8288
 
/**
8289
 
 * Validates shorthand CSS property font.
8290
 
 */
8291
 
class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef
8292
 
{
8293
 
    
8294
 
    /**
8295
 
     * Local copy of component validators.
8296
 
     * 
8297
 
     * @note If we moved specific CSS property definitions to their own
8298
 
     *       classes instead of having them be assembled at run time by
8299
 
     *       CSSDefinition, this wouldn't be necessary.  We'd instantiate
8300
 
     *       our own copies.
8301
 
     */
8302
 
    protected $info = array();
8303
 
    
8304
 
    public function __construct($config) {
8305
 
        $def = $config->getCSSDefinition();
8306
 
        $this->info['font-style']   = $def->info['font-style'];
8307
 
        $this->info['font-variant'] = $def->info['font-variant'];
8308
 
        $this->info['font-weight']  = $def->info['font-weight'];
8309
 
        $this->info['font-size']    = $def->info['font-size'];
8310
 
        $this->info['line-height']  = $def->info['line-height'];
8311
 
        $this->info['font-family']  = $def->info['font-family'];
8312
 
    }
8313
 
    
8314
 
    public function validate($string, $config, $context) {
8315
 
        
8316
 
        static $system_fonts = array(
8317
 
            'caption' => true,
8318
 
            'icon' => true,
8319
 
            'menu' => true,
8320
 
            'message-box' => true,
8321
 
            'small-caption' => true,
8322
 
            'status-bar' => true
8323
 
        );
8324
 
        
8325
 
        // regular pre-processing
8326
 
        $string = $this->parseCDATA($string);
8327
 
        if ($string === '') return false;
8328
 
        
8329
 
        // check if it's one of the keywords
8330
 
        $lowercase_string = strtolower($string);
8331
 
        if (isset($system_fonts[$lowercase_string])) {
8332
 
            return $lowercase_string;
8333
 
        }
8334
 
        
8335
 
        $bits = explode(' ', $string); // bits to process
8336
 
        $stage = 0; // this indicates what we're looking for
8337
 
        $caught = array(); // which stage 0 properties have we caught?
8338
 
        $stage_1 = array('font-style', 'font-variant', 'font-weight');
8339
 
        $final = ''; // output
8340
 
        
8341
 
        for ($i = 0, $size = count($bits); $i < $size; $i++) {
8342
 
            if ($bits[$i] === '') continue;
8343
 
            switch ($stage) {
8344
 
                
8345
 
                // attempting to catch font-style, font-variant or font-weight
8346
 
                case 0:
8347
 
                    foreach ($stage_1 as $validator_name) {
8348
 
                        if (isset($caught[$validator_name])) continue;
8349
 
                        $r = $this->info[$validator_name]->validate(
8350
 
                                                $bits[$i], $config, $context);
8351
 
                        if ($r !== false) {
8352
 
                            $final .= $r . ' ';
8353
 
                            $caught[$validator_name] = true;
8354
 
                            break;
8355
 
                        }
8356
 
                    }
8357
 
                    // all three caught, continue on
8358
 
                    if (count($caught) >= 3) $stage = 1;
8359
 
                    if ($r !== false) break;
8360
 
                
8361
 
                // attempting to catch font-size and perhaps line-height
8362
 
                case 1:
8363
 
                    $found_slash = false;
8364
 
                    if (strpos($bits[$i], '/') !== false) {
8365
 
                        list($font_size, $line_height) =
8366
 
                                                    explode('/', $bits[$i]);
8367
 
                        if ($line_height === '') {
8368
 
                            // ooh, there's a space after the slash!
8369
 
                            $line_height = false;
8370
 
                            $found_slash = true;
8371
 
                        }
8372
 
                    } else {
8373
 
                        $font_size = $bits[$i];
8374
 
                        $line_height = false;
8375
 
                    }
8376
 
                    $r = $this->info['font-size']->validate(
8377
 
                                              $font_size, $config, $context);
8378
 
                    if ($r !== false) {
8379
 
                        $final .= $r;
8380
 
                        // attempt to catch line-height
8381
 
                        if ($line_height === false) {
8382
 
                            // we need to scroll forward
8383
 
                            for ($j = $i + 1; $j < $size; $j++) {
8384
 
                                if ($bits[$j] === '') continue;
8385
 
                                if ($bits[$j] === '/') {
8386
 
                                    if ($found_slash) {
8387
 
                                        return false;
8388
 
                                    } else {
8389
 
                                        $found_slash = true;
8390
 
                                        continue;
8391
 
                                    }
8392
 
                                }
8393
 
                                $line_height = $bits[$j];
8394
 
                                break;
8395
 
                            }
8396
 
                        } else {
8397
 
                            // slash already found
8398
 
                            $found_slash = true;
8399
 
                            $j = $i;
8400
 
                        }
8401
 
                        if ($found_slash) {
8402
 
                            $i = $j;
8403
 
                            $r = $this->info['line-height']->validate(
8404
 
                                              $line_height, $config, $context);
8405
 
                            if ($r !== false) {
8406
 
                                $final .= '/' . $r;
8407
 
                            }
8408
 
                        }
8409
 
                        $final .= ' ';
8410
 
                        $stage = 2;
8411
 
                        break;
8412
 
                    }
8413
 
                    return false;
8414
 
                
8415
 
                // attempting to catch font-family
8416
 
                case 2:
8417
 
                    $font_family =
8418
 
                        implode(' ', array_slice($bits, $i, $size - $i));
8419
 
                    $r = $this->info['font-family']->validate(
8420
 
                                              $font_family, $config, $context);
8421
 
                    if ($r !== false) {
8422
 
                        $final .= $r . ' ';
8423
 
                        // processing completed successfully
8424
 
                        return rtrim($final);
8425
 
                    }
8426
 
                    return false;
8427
 
            }
8428
 
        }
8429
 
        return false;
8430
 
    }
8431
 
    
8432
 
}
8433
 
 
8434
 
 
8435
 
 
8436
 
 
8437
 
/**
8438
 
 * Validates a font family list according to CSS spec
8439
 
 * @todo whitelisting allowed fonts would be nice
8440
 
 */
8441
 
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
8442
 
{
8443
 
    
8444
 
    public function validate($string, $config, $context) {
8445
 
        static $generic_names = array(
8446
 
            'serif' => true,
8447
 
            'sans-serif' => true,
8448
 
            'monospace' => true,
8449
 
            'fantasy' => true,
8450
 
            'cursive' => true
8451
 
        );
8452
 
        
8453
 
        // assume that no font names contain commas in them
8454
 
        $fonts = explode(',', $string);
8455
 
        $final = '';
8456
 
        foreach($fonts as $font) {
8457
 
            $font = trim($font);
8458
 
            if ($font === '') continue;
8459
 
            // match a generic name
8460
 
            if (isset($generic_names[$font])) {
8461
 
                $final .= $font . ', ';
8462
 
                continue;
8463
 
            }
8464
 
            // match a quoted name
8465
 
            if ($font[0] === '"' || $font[0] === "'") {
8466
 
                $length = strlen($font);
8467
 
                if ($length <= 2) continue;
8468
 
                $quote = $font[0];
8469
 
                if ($font[$length - 1] !== $quote) continue;
8470
 
                $font = substr($font, 1, $length - 2);
8471
 
                
8472
 
                $new_font = '';
8473
 
                for ($i = 0, $c = strlen($font); $i < $c; $i++) {
8474
 
                    if ($font[$i] === '\\') {
8475
 
                        $i++;
8476
 
                        if ($i >= $c) {
8477
 
                            $new_font .= '\\';
8478
 
                            break;
8479
 
                        }
8480
 
                        if (ctype_xdigit($font[$i])) {
8481
 
                            $code = $font[$i];
8482
 
                            for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
8483
 
                                if (!ctype_xdigit($font[$i])) break;
8484
 
                                $code .= $font[$i];
8485
 
                            }
8486
 
                            // We have to be extremely careful when adding
8487
 
                            // new characters, to make sure we're not breaking
8488
 
                            // the encoding.
8489
 
                            $char = HTMLPurifier_Encoder::unichr(hexdec($code));
8490
 
                            if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue;
8491
 
                            $new_font .= $char;
8492
 
                            if ($i < $c && trim($font[$i]) !== '') $i--;
8493
 
                            continue;
8494
 
                        }
8495
 
                        if ($font[$i] === "\n") continue;
8496
 
                    }
8497
 
                    $new_font .= $font[$i];
8498
 
                }
8499
 
                
8500
 
                $font = $new_font;
8501
 
            }
8502
 
            // $font is a pure representation of the font name
8503
 
            
8504
 
            if (ctype_alnum($font) && $font !== '') {
8505
 
                // very simple font, allow it in unharmed
8506
 
                $final .= $font . ', ';
8507
 
                continue;
8508
 
            }
8509
 
            
8510
 
            // complicated font, requires quoting
8511
 
            
8512
 
            // armor single quotes and new lines
8513
 
            $font = str_replace("\\", "\\\\", $font);
8514
 
            $font = str_replace("'", "\\'", $font);
8515
 
            $final .= "'$font', ";
8516
 
        }
8517
 
        $final = rtrim($final, ', ');
8518
 
        if ($final === '') return false;
8519
 
        return $final;
8520
 
    }
8521
 
    
8522
 
}
8523
 
 
8524
 
 
8525
 
 
8526
 
 
8527
 
/**
8528
 
 * Decorator which enables !important to be used in CSS values.
8529
 
 */
8530
 
class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef
8531
 
{
8532
 
    protected $def, $allow;
8533
 
    
8534
 
    /**
8535
 
     * @param $def Definition to wrap
8536
 
     * @param $allow Whether or not to allow !important
8537
 
     */
8538
 
    public function __construct($def, $allow = false) {
8539
 
        $this->def = $def;
8540
 
        $this->allow = $allow;
8541
 
    }
8542
 
    /**
8543
 
     * Intercepts and removes !important if necessary
8544
 
     */
8545
 
    public function validate($string, $config, $context) {
8546
 
        // test for ! and important tokens
8547
 
        $string = trim($string);
8548
 
        $is_important = false;
8549
 
        // :TODO: optimization: test directly for !important and ! important
8550
 
        if (strlen($string) >= 9 && substr($string, -9) === 'important') {
8551
 
            $temp = rtrim(substr($string, 0, -9));
8552
 
            // use a temp, because we might want to restore important
8553
 
            if (strlen($temp) >= 1 && substr($temp, -1) === '!') {
8554
 
                $string = rtrim(substr($temp, 0, -1));
8555
 
                $is_important = true;
8556
 
            }
8557
 
        }
8558
 
        $string = $this->def->validate($string, $config, $context);
8559
 
        if ($this->allow && $is_important) $string .= ' !important';
8560
 
        return $string;
8561
 
    }
8562
 
}
8563
 
 
8564
 
 
8565
 
 
8566
 
/**
8567
 
 * Represents a Length as defined by CSS.
8568
 
 */
8569
 
class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef
8570
 
{
8571
 
    
8572
 
    protected $min, $max;
8573
 
    
8574
 
    /**
8575
 
     * @param HTMLPurifier_Length $max Minimum length, or null for no bound. String is also acceptable.
8576
 
     * @param HTMLPurifier_Length $max Maximum length, or null for no bound. String is also acceptable.
8577
 
     */
8578
 
    public function __construct($min = null, $max = null) {
8579
 
        $this->min = $min !== null ? HTMLPurifier_Length::make($min) : null;
8580
 
        $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null;
8581
 
    }
8582
 
    
8583
 
    public function validate($string, $config, $context) {
8584
 
        $string = $this->parseCDATA($string);
8585
 
        
8586
 
        // Optimizations
8587
 
        if ($string === '') return false;
8588
 
        if ($string === '0') return '0';
8589
 
        if (strlen($string) === 1) return false;
8590
 
        
8591
 
        $length = HTMLPurifier_Length::make($string);
8592
 
        if (!$length->isValid()) return false;
8593
 
        
8594
 
        if ($this->min) {
8595
 
            $c = $length->compareTo($this->min);
8596
 
            if ($c === false) return false;
8597
 
            if ($c < 0) return false;
8598
 
        }
8599
 
        if ($this->max) {
8600
 
            $c = $length->compareTo($this->max);
8601
 
            if ($c === false) return false;
8602
 
            if ($c > 0) return false;
8603
 
        }
8604
 
        
8605
 
        return $length->toString();
8606
 
    }
8607
 
    
8608
 
}
8609
 
 
8610
 
 
8611
 
 
8612
 
 
8613
 
/**
8614
 
 * Validates shorthand CSS property list-style.
8615
 
 * @warning Does not support url tokens that have internal spaces.
8616
 
 */
8617
 
class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef
8618
 
{
8619
 
    
8620
 
    /**
8621
 
     * Local copy of component validators.
8622
 
     * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl.
8623
 
     */
8624
 
    protected $info;
8625
 
    
8626
 
    public function __construct($config) {
8627
 
        $def = $config->getCSSDefinition();
8628
 
        $this->info['list-style-type']     = $def->info['list-style-type'];
8629
 
        $this->info['list-style-position'] = $def->info['list-style-position'];
8630
 
        $this->info['list-style-image'] = $def->info['list-style-image'];
8631
 
    }
8632
 
    
8633
 
    public function validate($string, $config, $context) {
8634
 
        
8635
 
        // regular pre-processing
8636
 
        $string = $this->parseCDATA($string);
8637
 
        if ($string === '') return false;
8638
 
        
8639
 
        // assumes URI doesn't have spaces in it
8640
 
        $bits = explode(' ', strtolower($string)); // bits to process
8641
 
        
8642
 
        $caught = array();
8643
 
        $caught['type']     = false;
8644
 
        $caught['position'] = false;
8645
 
        $caught['image']    = false;
8646
 
        
8647
 
        $i = 0; // number of catches
8648
 
        $none = false;
8649
 
        
8650
 
        foreach ($bits as $bit) {
8651
 
            if ($i >= 3) return; // optimization bit
8652
 
            if ($bit === '') continue;
8653
 
            foreach ($caught as $key => $status) {
8654
 
                if ($status !== false) continue;
8655
 
                $r = $this->info['list-style-' . $key]->validate($bit, $config, $context);
8656
 
                if ($r === false) continue;
8657
 
                if ($r === 'none') {
8658
 
                    if ($none) continue;
8659
 
                    else $none = true;
8660
 
                    if ($key == 'image') continue;
8661
 
                }
8662
 
                $caught[$key] = $r;
8663
 
                $i++;
8664
 
                break;
8665
 
            }
8666
 
        }
8667
 
        
8668
 
        if (!$i) return false;
8669
 
        
8670
 
        $ret = array();
8671
 
        
8672
 
        // construct type
8673
 
        if ($caught['type']) $ret[] = $caught['type'];
8674
 
        
8675
 
        // construct image
8676
 
        if ($caught['image']) $ret[] = $caught['image'];
8677
 
        
8678
 
        // construct position
8679
 
        if ($caught['position']) $ret[] = $caught['position'];
8680
 
        
8681
 
        if (empty($ret)) return false;
8682
 
        return implode(' ', $ret);
8683
 
        
8684
 
    }
8685
 
    
8686
 
}
8687
 
 
8688
 
 
8689
 
 
8690
 
 
8691
 
/**
8692
 
 * Framework class for strings that involve multiple values.
8693
 
 * 
8694
 
 * Certain CSS properties such as border-width and margin allow multiple
8695
 
 * lengths to be specified.  This class can take a vanilla border-width
8696
 
 * definition and multiply it, usually into a max of four.
8697
 
 * 
8698
 
 * @note Even though the CSS specification isn't clear about it, inherit
8699
 
 *       can only be used alone: it will never manifest as part of a multi
8700
 
 *       shorthand declaration.  Thus, this class does not allow inherit.
8701
 
 */
8702
 
class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef
8703
 
{
8704
 
    
8705
 
    /**
8706
 
     * Instance of component definition to defer validation to.
8707
 
     * @todo Make protected
8708
 
     */
8709
 
    public $single;
8710
 
    
8711
 
    /**
8712
 
     * Max number of values allowed.
8713
 
     * @todo Make protected
8714
 
     */
8715
 
    public $max;
8716
 
    
8717
 
    /**
8718
 
     * @param $single HTMLPurifier_AttrDef to multiply
8719
 
     * @param $max Max number of values allowed (usually four)
8720
 
     */
8721
 
    public function __construct($single, $max = 4) {
8722
 
        $this->single = $single;
8723
 
        $this->max = $max;
8724
 
    }
8725
 
    
8726
 
    public function validate($string, $config, $context) {
8727
 
        $string = $this->parseCDATA($string);
8728
 
        if ($string === '') return false;
8729
 
        $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n
8730
 
        $length = count($parts);
8731
 
        $final = '';
8732
 
        for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) {
8733
 
            if (ctype_space($parts[$i])) continue;
8734
 
            $result = $this->single->validate($parts[$i], $config, $context);
8735
 
            if ($result !== false) {
8736
 
                $final .= $result . ' ';
8737
 
                $num++;
8738
 
            }
8739
 
        }
8740
 
        if ($final === '') return false;
8741
 
        return rtrim($final);
8742
 
    }
8743
 
    
8744
 
}
8745
 
 
8746
 
 
8747
 
 
8748
 
 
8749
 
/**
8750
 
 * Validates a Percentage as defined by the CSS spec.
8751
 
 */
8752
 
class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef
8753
 
{
8754
 
    
8755
 
    /**
8756
 
     * Instance of HTMLPurifier_AttrDef_CSS_Number to defer number validation
8757
 
     */
8758
 
    protected $number_def;
8759
 
    
8760
 
    /**
8761
 
     * @param Bool indicating whether to forbid negative values
8762
 
     */
8763
 
    public function __construct($non_negative = false) {
8764
 
        $this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
8765
 
    }
8766
 
    
8767
 
    public function validate($string, $config, $context) {
8768
 
        
8769
 
        $string = $this->parseCDATA($string);
8770
 
        
8771
 
        if ($string === '') return false;
8772
 
        $length = strlen($string);
8773
 
        if ($length === 1) return false;
8774
 
        if ($string[$length - 1] !== '%') return false;
8775
 
        
8776
 
        $number = substr($string, 0, $length - 1);
8777
 
        $number = $this->number_def->validate($number, $config, $context);
8778
 
        
8779
 
        if ($number === false) return false;
8780
 
        return "$number%";
8781
 
        
8782
 
    }
8783
 
    
8784
 
}
8785
 
 
8786
 
 
8787
 
 
8788
 
 
8789
 
/**
8790
 
 * Validates the value for the CSS property text-decoration
8791
 
 * @note This class could be generalized into a version that acts sort of
8792
 
 *       like Enum except you can compound the allowed values.
8793
 
 */
8794
 
class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef
8795
 
{
8796
 
    
8797
 
    public function validate($string, $config, $context) {
8798
 
        
8799
 
        static $allowed_values = array(
8800
 
            'line-through' => true,
8801
 
            'overline' => true,
8802
 
            'underline' => true,
8803
 
        );
8804
 
        
8805
 
        $string = strtolower($this->parseCDATA($string));
8806
 
        
8807
 
        if ($string === 'none') return $string;
8808
 
        
8809
 
        $parts = explode(' ', $string);
8810
 
        $final = '';
8811
 
        foreach ($parts as $part) {
8812
 
            if (isset($allowed_values[$part])) {
8813
 
                $final .= $part . ' ';
8814
 
            }
8815
 
        }
8816
 
        $final = rtrim($final);
8817
 
        if ($final === '') return false;
8818
 
        return $final;
8819
 
        
8820
 
    }
8821
 
    
8822
 
}
8823
 
 
8824
 
 
8825
 
 
8826
 
 
8827
 
/**
8828
 
 * Validates a URI in CSS syntax, which uses url('http://example.com')
8829
 
 * @note While theoretically speaking a URI in a CSS document could
8830
 
 *       be non-embedded, as of CSS2 there is no such usage so we're
8831
 
 *       generalizing it. This may need to be changed in the future.
8832
 
 * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as
8833
 
 *          the separator, you cannot put a literal semicolon in
8834
 
 *          in the URI. Try percent encoding it, in that case.
8835
 
 */
8836
 
class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
8837
 
{
8838
 
    
8839
 
    public function __construct() {
8840
 
        parent::__construct(true); // always embedded
8841
 
    }
8842
 
    
8843
 
    public function validate($uri_string, $config, $context) {
8844
 
        // parse the URI out of the string and then pass it onto
8845
 
        // the parent object
8846
 
        
8847
 
        $uri_string = $this->parseCDATA($uri_string);
8848
 
        if (strpos($uri_string, 'url(') !== 0) return false;
8849
 
        $uri_string = substr($uri_string, 4);
8850
 
        $new_length = strlen($uri_string) - 1;
8851
 
        if ($uri_string[$new_length] != ')') return false;
8852
 
        $uri = trim(substr($uri_string, 0, $new_length));
8853
 
        
8854
 
        if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) {
8855
 
            $quote = $uri[0];
8856
 
            $new_length = strlen($uri) - 1;
8857
 
            if ($uri[$new_length] !== $quote) return false;
8858
 
            $uri = substr($uri, 1, $new_length - 1);
8859
 
        }
8860
 
        
8861
 
        $keys   = array(  '(',   ')',   ',',   ' ',   '"',   "'");
8862
 
        $values = array('\\(', '\\)', '\\,', '\\ ', '\\"', "\\'");
8863
 
        $uri = str_replace($values, $keys, $uri);
8864
 
        
8865
 
        $result = parent::validate($uri, $config, $context);
8866
 
        
8867
 
        if ($result === false) return false;
8868
 
        
8869
 
        // escape necessary characters according to CSS spec
8870
 
        // except for the comma, none of these should appear in the
8871
 
        // URI at all
8872
 
        $result = str_replace($keys, $values, $result);
8873
 
        
8874
 
        return "url($result)";
8875
 
        
8876
 
    }
8877
 
    
8878
 
}
8879
 
 
8880
 
 
8881
 
 
8882
 
 
8883
 
/**
8884
 
 * Validates a boolean attribute
8885
 
 */
8886
 
class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef
8887
 
{
8888
 
    
8889
 
    protected $name;
8890
 
    public $minimized = true;
8891
 
    
8892
 
    public function __construct($name = false) {$this->name = $name;}
8893
 
    
8894
 
    public function validate($string, $config, $context) {
8895
 
        if (empty($string)) return false;
8896
 
        return $this->name;
8897
 
    }
8898
 
    
8899
 
    /**
8900
 
     * @param $string Name of attribute
8901
 
     */
8902
 
    public function make($string) {
8903
 
        return new HTMLPurifier_AttrDef_HTML_Bool($string);
8904
 
    }
8905
 
    
8906
 
}
8907
 
 
8908
 
 
8909
 
 
8910
 
 
8911
 
/**
8912
 
 * Validates a color according to the HTML spec.
8913
 
 */
8914
 
class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef
8915
 
{
8916
 
    
8917
 
    public function validate($string, $config, $context) {
8918
 
        
8919
 
        static $colors = null;
8920
 
        if ($colors === null) $colors = $config->get('Core', 'ColorKeywords');
8921
 
        
8922
 
        $string = trim($string);
8923
 
        
8924
 
        if (empty($string)) return false;
8925
 
        if (isset($colors[$string])) return $colors[$string];
8926
 
        if ($string[0] === '#') $hex = substr($string, 1);
8927
 
        else $hex = $string;
8928
 
        
8929
 
        $length = strlen($hex);
8930
 
        if ($length !== 3 && $length !== 6) return false;
8931
 
        if (!ctype_xdigit($hex)) return false;
8932
 
        if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
8933
 
        
8934
 
        return "#$hex";
8935
 
        
8936
 
    }
8937
 
    
8938
 
}
8939
 
 
8940
 
 
8941
 
 
8942
 
 
8943
 
/**
8944
 
 * Special-case enum attribute definition that lazy loads allowed frame targets
8945
 
 */
8946
 
class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum
8947
 
{
8948
 
    
8949
 
    public $valid_values = false; // uninitialized value
8950
 
    protected $case_sensitive = false;
8951
 
    
8952
 
    public function __construct() {}
8953
 
    
8954
 
    public function validate($string, $config, $context) {
8955
 
        if ($this->valid_values === false) $this->valid_values = $config->get('Attr', 'AllowedFrameTargets');
8956
 
        return parent::validate($string, $config, $context);
8957
 
    }
8958
 
    
8959
 
}
8960
 
 
8961
 
 
8962
 
 
8963
 
 
8964
 
/**
8965
 
 * Validates the HTML attribute ID.
8966
 
 * @warning Even though this is the id processor, it
8967
 
 *          will ignore the directive Attr:IDBlacklist, since it will only
8968
 
 *          go according to the ID accumulator. Since the accumulator is
8969
 
 *          automatically generated, it will have already absorbed the
8970
 
 *          blacklist. If you're hacking around, make sure you use load()!
8971
 
 */
8972
 
 
8973
 
class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef
8974
 
{
8975
 
    
8976
 
    // ref functionality disabled, since we also have to verify
8977
 
    // whether or not the ID it refers to exists
8978
 
    
8979
 
    public function validate($id, $config, $context) {
8980
 
        
8981
 
        if (!$config->get('Attr', 'EnableID')) return false;
8982
 
        
8983
 
        $id = trim($id); // trim it first
8984
 
        
8985
 
        if ($id === '') return false;
8986
 
        
8987
 
        $prefix = $config->get('Attr', 'IDPrefix');
8988
 
        if ($prefix !== '') {
8989
 
            $prefix .= $config->get('Attr', 'IDPrefixLocal');
8990
 
            // prevent re-appending the prefix
8991
 
            if (strpos($id, $prefix) !== 0) $id = $prefix . $id;
8992
 
        } elseif ($config->get('Attr', 'IDPrefixLocal') !== '') {
8993
 
            trigger_error('%Attr.IDPrefixLocal cannot be used unless '.
8994
 
                '%Attr.IDPrefix is set', E_USER_WARNING);
8995
 
        }
8996
 
        
8997
 
        //if (!$this->ref) {
8998
 
            $id_accumulator =& $context->get('IDAccumulator');
8999
 
            if (isset($id_accumulator->ids[$id])) return false;
9000
 
        //}
9001
 
        
9002
 
        // we purposely avoid using regex, hopefully this is faster
9003
 
        
9004
 
        if (ctype_alpha($id)) {
9005
 
            $result = true;
9006
 
        } else {
9007
 
            if (!ctype_alpha(@$id[0])) return false;
9008
 
            $trim = trim( // primitive style of regexps, I suppose
9009
 
                $id,
9010
 
                'A..Za..z0..9:-._'
9011
 
              );
9012
 
            $result = ($trim === '');
9013
 
        }
9014
 
        
9015
 
        $regexp = $config->get('Attr', 'IDBlacklistRegexp');
9016
 
        if ($regexp && preg_match($regexp, $id)) {
9017
 
            return false;
9018
 
        }
9019
 
        
9020
 
        if (/*!$this->ref && */$result) $id_accumulator->add($id);
9021
 
        
9022
 
        // if no change was made to the ID, return the result
9023
 
        // else, return the new id if stripping whitespace made it
9024
 
        //     valid, or return false.
9025
 
        return $result ? $id : false;
9026
 
        
9027
 
    }
9028
 
    
9029
 
}
9030
 
 
9031
 
 
9032
 
 
9033
 
 
9034
 
/**
9035
 
 * Validates an integer representation of pixels according to the HTML spec.
9036
 
 */
9037
 
class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef
9038
 
{
9039
 
    
9040
 
    protected $max;
9041
 
    
9042
 
    public function __construct($max = null) {
9043
 
        $this->max = $max;
9044
 
    }
9045
 
    
9046
 
    public function validate($string, $config, $context) {
9047
 
        
9048
 
        $string = trim($string);
9049
 
        if ($string === '0') return $string;
9050
 
        if ($string === '')  return false;
9051
 
        $length = strlen($string);
9052
 
        if (substr($string, $length - 2) == 'px') {
9053
 
            $string = substr($string, 0, $length - 2);
9054
 
        }
9055
 
        if (!is_numeric($string)) return false;
9056
 
        $int = (int) $string;
9057
 
        
9058
 
        if ($int < 0) return '0';
9059
 
        
9060
 
        // upper-bound value, extremely high values can
9061
 
        // crash operating systems, see <http://ha.ckers.org/imagecrash.html>
9062
 
        // WARNING, above link WILL crash you if you're using Windows
9063
 
        
9064
 
        if ($this->max !== null && $int > $this->max) return (string) $this->max;
9065
 
        
9066
 
        return (string) $int;
9067
 
        
9068
 
    }
9069
 
    
9070
 
    public function make($string) {
9071
 
        if ($string === '') $max = null;
9072
 
        else $max = (int) $string;
9073
 
        $class = get_class($this);
9074
 
        return new $class($max);
9075
 
    }
9076
 
    
9077
 
}
9078
 
 
9079
 
 
9080
 
 
9081
 
 
9082
 
/**
9083
 
 * Validates the HTML type length (not to be confused with CSS's length).
9084
 
 * 
9085
 
 * This accepts integer pixels or percentages as lengths for certain
9086
 
 * HTML attributes.
9087
 
 */
9088
 
 
9089
 
class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels
9090
 
{
9091
 
    
9092
 
    public function validate($string, $config, $context) {
9093
 
        
9094
 
        $string = trim($string);
9095
 
        if ($string === '') return false;
9096
 
        
9097
 
        $parent_result = parent::validate($string, $config, $context);
9098
 
        if ($parent_result !== false) return $parent_result;
9099
 
        
9100
 
        $length = strlen($string);
9101
 
        $last_char = $string[$length - 1];
9102
 
        
9103
 
        if ($last_char !== '%') return false;
9104
 
        
9105
 
        $points = substr($string, 0, $length - 1);
9106
 
        
9107
 
        if (!is_numeric($points)) return false;
9108
 
        
9109
 
        $points = (int) $points;
9110
 
        
9111
 
        if ($points < 0) return '0%';
9112
 
        if ($points > 100) return '100%';
9113
 
        
9114
 
        return ((string) $points) . '%';
9115
 
        
9116
 
    }
9117
 
    
9118
 
}
9119
 
 
9120
 
 
9121
 
 
9122
 
 
9123
 
/**
9124
 
 * Validates a rel/rev link attribute against a directive of allowed values
9125
 
 * @note We cannot use Enum because link types allow multiple
9126
 
 *       values.
9127
 
 * @note Assumes link types are ASCII text
9128
 
 */
9129
 
class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef
9130
 
{
9131
 
    
9132
 
    /** Name config attribute to pull. */
9133
 
    protected $name;
9134
 
    
9135
 
    public function __construct($name) {
9136
 
        $configLookup = array(
9137
 
            'rel' => 'AllowedRel',
9138
 
            'rev' => 'AllowedRev'
9139
 
        );
9140
 
        if (!isset($configLookup[$name])) {
9141
 
            trigger_error('Unrecognized attribute name for link '.
9142
 
                'relationship.', E_USER_ERROR);
9143
 
            return;
9144
 
        }
9145
 
        $this->name = $configLookup[$name];
9146
 
    }
9147
 
    
9148
 
    public function validate($string, $config, $context) {
9149
 
        
9150
 
        $allowed = $config->get('Attr', $this->name);
9151
 
        if (empty($allowed)) return false;
9152
 
        
9153
 
        $string = $this->parseCDATA($string);
9154
 
        $parts = explode(' ', $string);
9155
 
        
9156
 
        // lookup to prevent duplicates
9157
 
        $ret_lookup = array();
9158
 
        foreach ($parts as $part) {
9159
 
            $part = strtolower(trim($part));
9160
 
            if (!isset($allowed[$part])) continue;
9161
 
            $ret_lookup[$part] = true;
9162
 
        }
9163
 
        
9164
 
        if (empty($ret_lookup)) return false;
9165
 
        $string = implode(' ', array_keys($ret_lookup));
9166
 
        
9167
 
        return $string;
9168
 
        
9169
 
    }
9170
 
    
9171
 
}
9172
 
 
9173
 
 
9174
 
 
9175
 
 
9176
 
/**
9177
 
 * Validates a MultiLength as defined by the HTML spec.
9178
 
 * 
9179
 
 * A multilength is either a integer (pixel count), a percentage, or
9180
 
 * a relative number.
9181
 
 */
9182
 
class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length
9183
 
{
9184
 
    
9185
 
    public function validate($string, $config, $context) {
9186
 
        
9187
 
        $string = trim($string);
9188
 
        if ($string === '') return false;
9189
 
        
9190
 
        $parent_result = parent::validate($string, $config, $context);
9191
 
        if ($parent_result !== false) return $parent_result;
9192
 
        
9193
 
        $length = strlen($string);
9194
 
        $last_char = $string[$length - 1];
9195
 
        
9196
 
        if ($last_char !== '*') return false;
9197
 
        
9198
 
        $int = substr($string, 0, $length - 1);
9199
 
        
9200
 
        if ($int == '') return '*';
9201
 
        if (!is_numeric($int)) return false;
9202
 
        
9203
 
        $int = (int) $int;
9204
 
        
9205
 
        if ($int < 0) return false;
9206
 
        if ($int == 0) return '0';
9207
 
        if ($int == 1) return '*';
9208
 
        return ((string) $int) . '*';
9209
 
        
9210
 
    }
9211
 
    
9212
 
}
9213
 
 
9214
 
 
9215
 
 
9216
 
 
9217
 
/**
9218
 
 * Validates contents based on NMTOKENS attribute type.
9219
 
 * @note The only current use for this is the class attribute in HTML
9220
 
 * @note Could have some functionality factored out into Nmtoken class
9221
 
 * @warning We cannot assume this class will be used only for 'class'
9222
 
 *          attributes. Not sure how to hook in magic behavior, then.
9223
 
 */
9224
 
class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef
9225
 
{
9226
 
    
9227
 
    public function validate($string, $config, $context) {
9228
 
        
9229
 
        $string = trim($string);
9230
 
        
9231
 
        // early abort: '' and '0' (strings that convert to false) are invalid
9232
 
        if (!$string) return false;
9233
 
        
9234
 
        // OPTIMIZABLE!
9235
 
        // do the preg_match, capture all subpatterns for reformulation
9236
 
        
9237
 
        // we don't support U+00A1 and up codepoints or
9238
 
        // escaping because I don't know how to do that with regexps
9239
 
        // and plus it would complicate optimization efforts (you never
9240
 
        // see that anyway).
9241
 
        $matches = array();
9242
 
        $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start
9243
 
                   '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'.
9244
 
                   '(?:(?=\s)|\z)/'; // look ahead for space or string end
9245
 
        preg_match_all($pattern, $string, $matches);
9246
 
        
9247
 
        if (empty($matches[1])) return false;
9248
 
        
9249
 
        // reconstruct string
9250
 
        $new_string = '';
9251
 
        foreach ($matches[1] as $token) {
9252
 
            $new_string .= $token . ' ';
9253
 
        }
9254
 
        $new_string = rtrim($new_string);
9255
 
        
9256
 
        return $new_string;
9257
 
        
9258
 
    }
9259
 
    
9260
 
}
9261
 
 
9262
 
 
9263
 
 
9264
 
 
9265
 
abstract class HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef
9266
 
{
9267
 
    
9268
 
    /**
9269
 
     * Unpacks a mailbox into its display-name and address
9270
 
     */
9271
 
    function unpack($string) {
9272
 
        // needs to be implemented
9273
 
    }
9274
 
    
9275
 
}
9276
 
 
9277
 
// sub-implementations
9278
 
 
9279
 
 
9280
 
 
9281
 
/**
9282
 
 * Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
9283
 
 */
9284
 
class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
9285
 
{
9286
 
    
9287
 
    /**
9288
 
     * Instance of HTMLPurifier_AttrDef_URI_IPv4 sub-validator
9289
 
     */
9290
 
    protected $ipv4;
9291
 
    
9292
 
    /**
9293
 
     * Instance of HTMLPurifier_AttrDef_URI_IPv6 sub-validator
9294
 
     */
9295
 
    protected $ipv6;
9296
 
    
9297
 
    public function __construct() {
9298
 
        $this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
9299
 
        $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
9300
 
    }
9301
 
    
9302
 
    public function validate($string, $config, $context) {
9303
 
        $length = strlen($string);
9304
 
        if ($string === '') return '';
9305
 
        if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') {
9306
 
            //IPv6
9307
 
            $ip = substr($string, 1, $length - 2);
9308
 
            $valid = $this->ipv6->validate($ip, $config, $context);
9309
 
            if ($valid === false) return false;
9310
 
            return '['. $valid . ']';
9311
 
        }
9312
 
        
9313
 
        // need to do checks on unusual encodings too
9314
 
        $ipv4 = $this->ipv4->validate($string, $config, $context);
9315
 
        if ($ipv4 !== false) return $ipv4;
9316
 
        
9317
 
        // A regular domain name.
9318
 
        
9319
 
        // This breaks I18N domain names, but we don't have proper IRI support,
9320
 
        // so force users to insert Punycode. If there's complaining we'll 
9321
 
        // try to fix things into an international friendly form.
9322
 
        
9323
 
        // The productions describing this are:
9324
 
        $a   = '[a-z]';     // alpha
9325
 
        $an  = '[a-z0-9]';  // alphanum
9326
 
        $and = '[a-z0-9-]'; // alphanum | "-"
9327
 
        // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
9328
 
        $domainlabel   = "$an($and*$an)?";
9329
 
        // toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
9330
 
        $toplabel      = "$a($and*$an)?";
9331
 
        // hostname    = *( domainlabel "." ) toplabel [ "." ]
9332
 
        $match = preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string);
9333
 
        if (!$match) return false;
9334
 
        
9335
 
        return $string;
9336
 
    }
9337
 
    
9338
 
}
9339
 
 
9340
 
 
9341
 
 
9342
 
 
9343
 
/**
9344
 
 * Validates an IPv4 address
9345
 
 * @author Feyd @ forums.devnetwork.net (public domain)
9346
 
 */
9347
 
class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef
9348
 
{
9349
 
    
9350
 
    /**
9351
 
     * IPv4 regex, protected so that IPv6 can reuse it
9352
 
     */
9353
 
    protected $ip4;
9354
 
    
9355
 
    public function validate($aIP, $config, $context) {
9356
 
        
9357
 
        if (!$this->ip4) $this->_loadRegex();
9358
 
        
9359
 
        if (preg_match('#^' . $this->ip4 . '$#s', $aIP))
9360
 
        {
9361
 
                return $aIP;
9362
 
        }
9363
 
        
9364
 
        return false;
9365
 
        
9366
 
    }
9367
 
    
9368
 
    /**
9369
 
     * Lazy load function to prevent regex from being stuffed in
9370
 
     * cache.
9371
 
     */
9372
 
    protected function _loadRegex() {
9373
 
        $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255
9374
 
        $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})";
9375
 
    }
9376
 
    
9377
 
}
9378
 
 
9379
 
 
9380
 
 
9381
 
 
9382
 
/**
9383
 
 * Validates an IPv6 address.
9384
 
 * @author Feyd @ forums.devnetwork.net (public domain)
9385
 
 * @note This function requires brackets to have been removed from address
9386
 
 *       in URI.
9387
 
 */
9388
 
class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4
9389
 
{
9390
 
    
9391
 
    public function validate($aIP, $config, $context) {
9392
 
        
9393
 
        if (!$this->ip4) $this->_loadRegex();
9394
 
        
9395
 
        $original = $aIP;
9396
 
        
9397
 
        $hex = '[0-9a-fA-F]';
9398
 
        $blk = '(?:' . $hex . '{1,4})';
9399
 
        $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))';   // /0 - /128
9400
 
        
9401
 
        //      prefix check
9402
 
        if (strpos($aIP, '/') !== false)
9403
 
        {
9404
 
                if (preg_match('#' . $pre . '$#s', $aIP, $find))
9405
 
                {
9406
 
                        $aIP = substr($aIP, 0, 0-strlen($find[0]));
9407
 
                        unset($find);
9408
 
                }
9409
 
                else
9410
 
                {
9411
 
                        return false;
9412
 
                }
9413
 
        }
9414
 
        
9415
 
        //      IPv4-compatiblity check       
9416
 
        if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find))
9417
 
        {
9418
 
                $aIP = substr($aIP, 0, 0-strlen($find[0]));
9419
 
                $ip = explode('.', $find[0]);
9420
 
                $ip = array_map('dechex', $ip);
9421
 
                $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3];
9422
 
                unset($find, $ip);
9423
 
        }
9424
 
        
9425
 
        //      compression check
9426
 
        $aIP = explode('::', $aIP);
9427
 
        $c = count($aIP);
9428
 
        if ($c > 2)
9429
 
        {
9430
 
                return false;
9431
 
        }
9432
 
        elseif ($c == 2)
9433
 
        {
9434
 
                list($first, $second) = $aIP;
9435
 
                $first = explode(':', $first);
9436
 
                $second = explode(':', $second);
9437
 
               
9438
 
                if (count($first) + count($second) > 8)
9439
 
                {
9440
 
                        return false;
9441
 
                }
9442
 
               
9443
 
                while(count($first) < 8)
9444
 
                {
9445
 
                        array_push($first, '0');
9446
 
                }
9447
 
 
9448
 
                array_splice($first, 8 - count($second), 8, $second);
9449
 
                $aIP = $first;
9450
 
                unset($first,$second);
9451
 
        }
9452
 
        else
9453
 
        {
9454
 
                $aIP = explode(':', $aIP[0]);
9455
 
        }
9456
 
        $c = count($aIP);
9457
 
        
9458
 
        if ($c != 8)
9459
 
        {
9460
 
                return false;
9461
 
        }
9462
 
       
9463
 
        //      All the pieces should be 16-bit hex strings. Are they?
9464
 
        foreach ($aIP as $piece)
9465
 
        {
9466
 
                if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece)))
9467
 
                {
9468
 
                        return false;
9469
 
                }
9470
 
        }
9471
 
        
9472
 
        return $original;
9473
 
        
9474
 
    }
9475
 
    
9476
 
}
9477
 
 
9478
 
 
9479
 
 
9480
 
 
9481
 
/**
9482
 
 * Primitive email validation class based on the regexp found at 
9483
 
 * http://www.regular-expressions.info/email.html
9484
 
 */
9485
 
class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email
9486
 
{
9487
 
    
9488
 
    public function validate($string, $config, $context) {
9489
 
        // no support for named mailboxes i.e. "Bob <bob@example.com>"
9490
 
        // that needs more percent encoding to be done
9491
 
        if ($string == '') return false;
9492
 
        $string = trim($string);
9493
 
        $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string);
9494
 
        return $result ? $string : false;
9495
 
    }
9496
 
    
9497
 
}
9498
 
 
9499
 
 
9500
 
 
9501
 
 
9502
 
/**
9503
 
 * Pre-transform that changes proprietary background attribute to CSS.
9504
 
 */
9505
 
class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform {
9506
 
 
9507
 
    public function transform($attr, $config, $context) {
9508
 
        
9509
 
        if (!isset($attr['background'])) return $attr;
9510
 
        
9511
 
        $background = $this->confiscateAttr($attr, 'background');
9512
 
        // some validation should happen here
9513
 
        
9514
 
        $this->prependCSS($attr, "background-image:url($background);");
9515
 
        
9516
 
        return $attr;
9517
 
        
9518
 
    }
9519
 
    
9520
 
}
9521
 
 
9522
 
 
9523
 
 
9524
 
 
9525
 
// this MUST be placed in post, as it assumes that any value in dir is valid
9526
 
 
9527
 
/**
9528
 
 * Post-trasnform that ensures that bdo tags have the dir attribute set.
9529
 
 */
9530
 
class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform
9531
 
{
9532
 
    
9533
 
    public function transform($attr, $config, $context) {
9534
 
        if (isset($attr['dir'])) return $attr;
9535
 
        $attr['dir'] = $config->get('Attr', 'DefaultTextDir');
9536
 
        return $attr;
9537
 
    }
9538
 
    
9539
 
}
9540
 
 
9541
 
 
9542
 
 
9543
 
 
9544
 
/**
9545
 
 * Pre-transform that changes deprecated bgcolor attribute to CSS.
9546
 
 */
9547
 
class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform {
9548
 
 
9549
 
    public function transform($attr, $config, $context) {
9550
 
        
9551
 
        if (!isset($attr['bgcolor'])) return $attr;
9552
 
        
9553
 
        $bgcolor = $this->confiscateAttr($attr, 'bgcolor');
9554
 
        // some validation should happen here
9555
 
        
9556
 
        $this->prependCSS($attr, "background-color:$bgcolor;");
9557
 
        
9558
 
        return $attr;
9559
 
        
9560
 
    }
9561
 
    
9562
 
}
9563
 
 
9564
 
 
9565
 
 
9566
 
 
9567
 
/**
9568
 
 * Pre-transform that changes converts a boolean attribute to fixed CSS
9569
 
 */
9570
 
class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform {
9571
 
    
9572
 
    /**
9573
 
     * Name of boolean attribute that is trigger
9574
 
     */
9575
 
    protected $attr;
9576
 
    
9577
 
    /**
9578
 
     * CSS declarations to add to style, needs trailing semicolon
9579
 
     */
9580
 
    protected $css;
9581
 
    
9582
 
    /**
9583
 
     * @param $attr string attribute name to convert from
9584
 
     * @param $css string CSS declarations to add to style (needs semicolon)
9585
 
     */
9586
 
    public function __construct($attr, $css) {
9587
 
        $this->attr = $attr;
9588
 
        $this->css  = $css;
9589
 
    }
9590
 
    
9591
 
    public function transform($attr, $config, $context) {
9592
 
        if (!isset($attr[$this->attr])) return $attr;
9593
 
        unset($attr[$this->attr]);
9594
 
        $this->prependCSS($attr, $this->css);
9595
 
        return $attr;
9596
 
    }
9597
 
    
9598
 
}
9599
 
 
9600
 
 
9601
 
 
9602
 
 
9603
 
/**
9604
 
 * Pre-transform that changes deprecated border attribute to CSS.
9605
 
 */
9606
 
class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform {
9607
 
 
9608
 
    public function transform($attr, $config, $context) {
9609
 
        if (!isset($attr['border'])) return $attr;
9610
 
        $border_width = $this->confiscateAttr($attr, 'border');
9611
 
        // some validation should happen here
9612
 
        $this->prependCSS($attr, "border:{$border_width}px solid;");
9613
 
        return $attr;
9614
 
    }
9615
 
    
9616
 
}
9617
 
 
9618
 
 
9619
 
 
9620
 
 
9621
 
/**
9622
 
 * Generic pre-transform that converts an attribute with a fixed number of
9623
 
 * values (enumerated) to CSS.
9624
 
 */
9625
 
class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform {
9626
 
    
9627
 
    /**
9628
 
     * Name of attribute to transform from
9629
 
     */
9630
 
    protected $attr;
9631
 
    
9632
 
    /**
9633
 
     * Lookup array of attribute values to CSS
9634
 
     */
9635
 
    protected $enumToCSS = array();
9636
 
    
9637
 
    /**
9638
 
     * Case sensitivity of the matching
9639
 
     * @warning Currently can only be guaranteed to work with ASCII
9640
 
     *          values.
9641
 
     */
9642
 
    protected $caseSensitive = false;
9643
 
    
9644
 
    /**
9645
 
     * @param $attr String attribute name to transform from
9646
 
     * @param $enumToCSS Lookup array of attribute values to CSS
9647
 
     * @param $case_sensitive Boolean case sensitivity indicator, default false
9648
 
     */
9649
 
    public function __construct($attr, $enum_to_css, $case_sensitive = false) {
9650
 
        $this->attr = $attr;
9651
 
        $this->enumToCSS = $enum_to_css;
9652
 
        $this->caseSensitive = (bool) $case_sensitive;
9653
 
    }
9654
 
    
9655
 
    public function transform($attr, $config, $context) {
9656
 
        
9657
 
        if (!isset($attr[$this->attr])) return $attr;
9658
 
        
9659
 
        $value = trim($attr[$this->attr]);
9660
 
        unset($attr[$this->attr]);
9661
 
        
9662
 
        if (!$this->caseSensitive) $value = strtolower($value);
9663
 
        
9664
 
        if (!isset($this->enumToCSS[$value])) {
9665
 
            return $attr;
9666
 
        }
9667
 
        
9668
 
        $this->prependCSS($attr, $this->enumToCSS[$value]);
9669
 
        
9670
 
        return $attr;
9671
 
        
9672
 
    }
9673
 
    
9674
 
}
9675
 
 
9676
 
 
9677
 
 
9678
 
 
9679
 
// must be called POST validation
9680
 
 
9681
 
/**
9682
 
 * Transform that supplies default values for the src and alt attributes
9683
 
 * in img tags, as well as prevents the img tag from being removed
9684
 
 * because of a missing alt tag. This needs to be registered as both
9685
 
 * a pre and post attribute transform.
9686
 
 */
9687
 
class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform
9688
 
{
9689
 
    
9690
 
    public function transform($attr, $config, $context) {
9691
 
        
9692
 
        $src = true;
9693
 
        if (!isset($attr['src'])) {
9694
 
            if ($config->get('Core', 'RemoveInvalidImg')) return $attr;
9695
 
            $attr['src'] = $config->get('Attr', 'DefaultInvalidImage');
9696
 
            $src = false;
9697
 
        }
9698
 
        
9699
 
        if (!isset($attr['alt'])) {
9700
 
            if ($src) {
9701
 
                $alt = $config->get('Attr', 'DefaultImageAlt');
9702
 
                if ($alt === null) {
9703
 
                    $attr['alt'] = basename($attr['src']);
9704
 
                } else {
9705
 
                    $attr['alt'] = $alt;
9706
 
                }
9707
 
            } else {
9708
 
                $attr['alt'] = $config->get('Attr', 'DefaultInvalidImageAlt');
9709
 
            }
9710
 
        }
9711
 
        
9712
 
        return $attr;
9713
 
        
9714
 
    }
9715
 
    
9716
 
}
9717
 
 
9718
 
 
9719
 
 
9720
 
 
9721
 
/**
9722
 
 * Pre-transform that changes deprecated hspace and vspace attributes to CSS
9723
 
 */
9724
 
class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform {
9725
 
    
9726
 
    protected $attr;
9727
 
    protected $css = array(
9728
 
        'hspace' => array('left', 'right'),
9729
 
        'vspace' => array('top', 'bottom')
9730
 
    );
9731
 
    
9732
 
    public function __construct($attr) {
9733
 
        $this->attr = $attr;
9734
 
        if (!isset($this->css[$attr])) {
9735
 
            trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
9736
 
        }
9737
 
    }
9738
 
    
9739
 
    public function transform($attr, $config, $context) {
9740
 
        
9741
 
        if (!isset($attr[$this->attr])) return $attr;
9742
 
        
9743
 
        $width = $this->confiscateAttr($attr, $this->attr);
9744
 
        // some validation could happen here
9745
 
        
9746
 
        if (!isset($this->css[$this->attr])) return $attr;
9747
 
        
9748
 
        $style = '';
9749
 
        foreach ($this->css[$this->attr] as $suffix) {
9750
 
            $property = "margin-$suffix";
9751
 
            $style .= "$property:{$width}px;";
9752
 
        }
9753
 
        
9754
 
        $this->prependCSS($attr, $style);
9755
 
        
9756
 
        return $attr;
9757
 
        
9758
 
    }
9759
 
    
9760
 
}
9761
 
 
9762
 
 
9763
 
 
9764
 
 
9765
 
/**
9766
 
 * Performs miscellaneous cross attribute validation and filtering for
9767
 
 * input elements. This is meant to be a post-transform.
9768
 
 */
9769
 
class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform {
9770
 
    
9771
 
    protected $pixels;
9772
 
    
9773
 
    public function __construct() {
9774
 
        $this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
9775
 
    }
9776
 
    
9777
 
    public function transform($attr, $config, $context) {
9778
 
        if (!isset($attr['type'])) $t = 'text';
9779
 
        else $t = strtolower($attr['type']);
9780
 
        if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
9781
 
            unset($attr['checked']);
9782
 
        }
9783
 
        if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') {
9784
 
            unset($attr['maxlength']);
9785
 
        }
9786
 
        if (isset($attr['size']) && $t !== 'text' && $t !== 'password') {
9787
 
            $result = $this->pixels->validate($attr['size'], $config, $context);
9788
 
            if ($result === false) unset($attr['size']);
9789
 
            else $attr['size'] = $result;
9790
 
        }
9791
 
        if (isset($attr['src']) && $t !== 'image') {
9792
 
            unset($attr['src']);
9793
 
        }
9794
 
        if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
9795
 
            $attr['value'] = '';
9796
 
        }
9797
 
        return $attr;
9798
 
    }
9799
 
    
9800
 
}
9801
 
 
9802
 
 
9803
 
 
9804
 
 
9805
 
/**
9806
 
 * Post-transform that copies lang's value to xml:lang (and vice-versa)
9807
 
 * @note Theoretically speaking, this could be a pre-transform, but putting
9808
 
 *       post is more efficient.
9809
 
 */
9810
 
class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform
9811
 
{
9812
 
    
9813
 
    public function transform($attr, $config, $context) {
9814
 
        
9815
 
        $lang     = isset($attr['lang']) ? $attr['lang'] : false;
9816
 
        $xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false;
9817
 
        
9818
 
        if ($lang !== false && $xml_lang === false) {
9819
 
            $attr['xml:lang'] = $lang;
9820
 
        } elseif ($xml_lang !== false) {
9821
 
            $attr['lang'] = $xml_lang;
9822
 
        }
9823
 
        
9824
 
        return $attr;
9825
 
        
9826
 
    }
9827
 
    
9828
 
}
9829
 
 
9830
 
 
9831
 
 
9832
 
 
9833
 
/**
9834
 
 * Class for handling width/height length attribute transformations to CSS
9835
 
 */
9836
 
class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform
9837
 
{
9838
 
    
9839
 
    protected $name;
9840
 
    protected $cssName;
9841
 
    
9842
 
    public function __construct($name, $css_name = null) {
9843
 
        $this->name = $name;
9844
 
        $this->cssName = $css_name ? $css_name : $name;
9845
 
    }
9846
 
    
9847
 
    public function transform($attr, $config, $context) {
9848
 
        if (!isset($attr[$this->name])) return $attr;
9849
 
        $length = $this->confiscateAttr($attr, $this->name);
9850
 
        if(ctype_digit($length)) $length .= 'px';
9851
 
        $this->prependCSS($attr, $this->cssName . ":$length;");
9852
 
        return $attr;
9853
 
    }
9854
 
    
9855
 
}
9856
 
 
9857
 
 
9858
 
 
9859
 
 
9860
 
/**
9861
 
 * Pre-transform that changes deprecated name attribute to ID if necessary
9862
 
 */
9863
 
class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform
9864
 
{
9865
 
    
9866
 
    public function transform($attr, $config, $context) {
9867
 
        if (!isset($attr['name'])) return $attr;
9868
 
        $id = $this->confiscateAttr($attr, 'name');
9869
 
        if ( isset($attr['id']))   return $attr;
9870
 
        $attr['id'] = $id;
9871
 
        return $attr;
9872
 
    }
9873
 
    
9874
 
}
9875
 
 
9876
 
 
9877
 
 
9878
 
 
9879
 
class HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform 
9880
 
{
9881
 
    public $name = "SafeEmbed";
9882
 
 
9883
 
    public function transform($attr, $config, $context) {
9884
 
        $attr['allowscriptaccess'] = 'never';
9885
 
        $attr['allownetworking'] = 'internal';
9886
 
        $attr['type'] = 'application/x-shockwave-flash';
9887
 
        return $attr;
9888
 
    }
9889
 
}
9890
 
 
9891
 
 
9892
 
 
9893
 
/**
9894
 
 * Writes default type for all objects. Currently only supports flash.
9895
 
 */
9896
 
class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform
9897
 
{
9898
 
    public $name = "SafeObject";
9899
 
 
9900
 
    function transform($attr, $config, $context) {
9901
 
        if (!isset($attr['type'])) $attr['type'] = 'application/x-shockwave-flash';
9902
 
        return $attr;
9903
 
    }
9904
 
}
9905
 
 
9906
 
 
9907
 
 
9908
 
/**
9909
 
 * Validates name/value pairs in param tags to be used in safe objects. This
9910
 
 * will only allow name values it recognizes, and pre-fill certain attributes
9911
 
 * with required values.
9912
 
 * 
9913
 
 * @note
9914
 
 *      This class only supports Flash. In the future, Quicktime support
9915
 
 *      may be added.
9916
 
 * 
9917
 
 * @warning
9918
 
 *      This class expects an injector to add the necessary parameters tags.
9919
 
 */
9920
 
class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform 
9921
 
{
9922
 
    public $name = "SafeParam";
9923
 
    private $uri;
9924
 
    
9925
 
    public function __construct() {
9926
 
        $this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
9927
 
    }
9928
 
    
9929
 
    public function transform($attr, $config, $context) {
9930
 
        // If we add support for other objects, we'll need to alter the
9931
 
        // transforms.
9932
 
        switch ($attr['name']) {
9933
 
            // application/x-shockwave-flash
9934
 
            // Keep this synchronized with Injector/SafeObject.php
9935
 
            case 'allowScriptAccess':
9936
 
                $attr['value'] = 'never';
9937
 
                break;
9938
 
            case 'allowNetworking':
9939
 
                $attr['value'] = 'internal';
9940
 
                break;
9941
 
            case 'wmode':
9942
 
                $attr['value'] = 'window';
9943
 
                break;
9944
 
            case 'movie':
9945
 
                $attr['value'] = $this->uri->validate($attr['value'], $config, $context);
9946
 
                break;
9947
 
            // add other cases to support other param name/value pairs
9948
 
            default:
9949
 
                $attr['name'] = $attr['value'] = null;
9950
 
        }
9951
 
        return $attr;
9952
 
    }
9953
 
}
9954
 
 
9955
 
 
9956
 
 
9957
 
/**
9958
 
 * Implements required attribute stipulation for <script>
9959
 
 */
9960
 
class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
9961
 
{
9962
 
    public function transform($attr, $config, $context) {
9963
 
        if (!isset($attr['type'])) {
9964
 
            $attr['type'] = 'text/javascript';
9965
 
        }
9966
 
        return $attr;
9967
 
    }
9968
 
}
9969
 
 
9970
 
 
9971
 
 
9972
 
/**
9973
 
 * Sets height/width defaults for <textarea>
9974
 
 */
9975
 
class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
9976
 
{
9977
 
    
9978
 
    public function transform($attr, $config, $context) {
9979
 
        // Calculated from Firefox
9980
 
        if (!isset($attr['cols'])) $attr['cols'] = '22';
9981
 
        if (!isset($attr['rows'])) $attr['rows'] = '3';
9982
 
        return $attr;
9983
 
    }
9984
 
    
9985
 
}
9986
 
 
9987
 
 
9988
 
/**
9989
 
 * Definition that uses different definitions depending on context.
9990
 
 * 
9991
 
 * The del and ins tags are notable because they allow different types of
9992
 
 * elements depending on whether or not they're in a block or inline context.
9993
 
 * Chameleon allows this behavior to happen by using two different
9994
 
 * definitions depending on context.  While this somewhat generalized,
9995
 
 * it is specifically intended for those two tags.
9996
 
 */
9997
 
class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef
9998
 
{
9999
 
    
10000
 
    /**
10001
 
     * Instance of the definition object to use when inline. Usually stricter.
10002
 
     */
10003
 
    public $inline;
10004
 
    
10005
 
    /**
10006
 
     * Instance of the definition object to use when block.
10007
 
     */
10008
 
    public $block;
10009
 
    
10010
 
    public $type = 'chameleon';
10011
 
    
10012
 
    /**
10013
 
     * @param $inline List of elements to allow when inline.
10014
 
     * @param $block List of elements to allow when block.
10015
 
     */
10016
 
    public function __construct($inline, $block) {
10017
 
        $this->inline = new HTMLPurifier_ChildDef_Optional($inline);
10018
 
        $this->block  = new HTMLPurifier_ChildDef_Optional($block);
10019
 
        $this->elements = $this->block->elements;
10020
 
    }
10021
 
    
10022
 
    public function validateChildren($tokens_of_children, $config, $context) {
10023
 
        if ($context->get('IsInline') === false) {
10024
 
            return $this->block->validateChildren(
10025
 
                $tokens_of_children, $config, $context);
10026
 
        } else {
10027
 
            return $this->inline->validateChildren(
10028
 
                $tokens_of_children, $config, $context);
10029
 
        }
10030
 
    }
10031
 
}
10032
 
 
10033
 
 
10034
 
 
10035
 
 
10036
 
/**
10037
 
 * Custom validation class, accepts DTD child definitions
10038
 
 * 
10039
 
 * @warning Currently this class is an all or nothing proposition, that is,
10040
 
 *          it will only give a bool return value.
10041
 
 */
10042
 
class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef
10043
 
{
10044
 
    public $type = 'custom';
10045
 
    public $allow_empty = false;
10046
 
    /**
10047
 
     * Allowed child pattern as defined by the DTD
10048
 
     */
10049
 
    public $dtd_regex;
10050
 
    /**
10051
 
     * PCRE regex derived from $dtd_regex
10052
 
     * @private
10053
 
     */
10054
 
    private $_pcre_regex;
10055
 
    /**
10056
 
     * @param $dtd_regex Allowed child pattern from the DTD
10057
 
     */
10058
 
    public function __construct($dtd_regex) {
10059
 
        $this->dtd_regex = $dtd_regex;
10060
 
        $this->_compileRegex();
10061
 
    }
10062
 
    /**
10063
 
     * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
10064
 
     */
10065
 
    protected function _compileRegex() {
10066
 
        $raw = str_replace(' ', '', $this->dtd_regex);
10067
 
        if ($raw{0} != '(') {
10068
 
            $raw = "($raw)";
10069
 
        }
10070
 
        $el = '[#a-zA-Z0-9_.-]+';
10071
 
        $reg = $raw;
10072
 
        
10073
 
        // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
10074
 
        // DOING! Seriously: if there's problems, please report them.
10075
 
        
10076
 
        // collect all elements into the $elements array
10077
 
        preg_match_all("/$el/", $reg, $matches);
10078
 
        foreach ($matches[0] as $match) {
10079
 
            $this->elements[$match] = true;
10080
 
        }
10081
 
        
10082
 
        // setup all elements as parentheticals with leading commas
10083
 
        $reg = preg_replace("/$el/", '(,\\0)', $reg);
10084
 
        
10085
 
        // remove commas when they were not solicited
10086
 
        $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg);
10087
 
        
10088
 
        // remove all non-paranthetical commas: they are handled by first regex
10089
 
        $reg = preg_replace("/,\(/", '(', $reg);
10090
 
        
10091
 
        $this->_pcre_regex = $reg;
10092
 
    }
10093
 
    public function validateChildren($tokens_of_children, $config, $context) {
10094
 
        $list_of_children = '';
10095
 
        $nesting = 0; // depth into the nest
10096
 
        foreach ($tokens_of_children as $token) {
10097
 
            if (!empty($token->is_whitespace)) continue;
10098
 
            
10099
 
            $is_child = ($nesting == 0); // direct
10100
 
            
10101
 
            if ($token instanceof HTMLPurifier_Token_Start) {
10102
 
                $nesting++;
10103
 
            } elseif ($token instanceof HTMLPurifier_Token_End) {
10104
 
                $nesting--;
10105
 
            }
10106
 
            
10107
 
            if ($is_child) {
10108
 
                $list_of_children .= $token->name . ',';
10109
 
            }
10110
 
        }
10111
 
        // add leading comma to deal with stray comma declarations
10112
 
        $list_of_children = ',' . rtrim($list_of_children, ',');
10113
 
        $okay =
10114
 
            preg_match(
10115
 
                '/^,?'.$this->_pcre_regex.'$/',
10116
 
                $list_of_children
10117
 
            );
10118
 
        
10119
 
        return (bool) $okay;
10120
 
    }
10121
 
}
10122
 
 
10123
 
 
10124
 
 
10125
 
 
10126
 
/**
10127
 
 * Definition that disallows all elements.
10128
 
 * @warning validateChildren() in this class is actually never called, because
10129
 
 *          empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed
10130
 
 *          before child definitions are parsed in earnest by
10131
 
 *          HTMLPurifier_Strategy_FixNesting.
10132
 
 */
10133
 
class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef
10134
 
{
10135
 
    public $allow_empty = true;
10136
 
    public $type = 'empty';
10137
 
    public function __construct() {}
10138
 
    public function validateChildren($tokens_of_children, $config, $context) {
10139
 
        return array();
10140
 
    }
10141
 
}
10142
 
 
10143
 
 
10144
 
 
10145
 
 
10146
 
/**
10147
 
 * Definition that allows a set of elements, but disallows empty children.
10148
 
 */
10149
 
class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef
10150
 
{
10151
 
    /**
10152
 
     * Lookup table of allowed elements.
10153
 
     * @public
10154
 
     */
10155
 
    public $elements = array();
10156
 
    /**
10157
 
     * @param $elements List of allowed element names (lowercase).
10158
 
     */
10159
 
    public function __construct($elements) {
10160
 
        if (is_string($elements)) {
10161
 
            $elements = str_replace(' ', '', $elements);
10162
 
            $elements = explode('|', $elements);
10163
 
        }
10164
 
        $keys = array_keys($elements);
10165
 
        if ($keys == array_keys($keys)) {
10166
 
            $elements = array_flip($elements);
10167
 
            foreach ($elements as $i => $x) {
10168
 
                $elements[$i] = true;
10169
 
                if (empty($i)) unset($elements[$i]); // remove blank
10170
 
            }
10171
 
        }
10172
 
        $this->elements = $elements;
10173
 
    }
10174
 
    public $allow_empty = false;
10175
 
    public $type = 'required';
10176
 
    public function validateChildren($tokens_of_children, $config, $context) {
10177
 
        // if there are no tokens, delete parent node
10178
 
        if (empty($tokens_of_children)) return false;
10179
 
        
10180
 
        // the new set of children
10181
 
        $result = array();
10182
 
        
10183
 
        // current depth into the nest
10184
 
        $nesting = 0;
10185
 
        
10186
 
        // whether or not we're deleting a node
10187
 
        $is_deleting = false;
10188
 
        
10189
 
        // whether or not parsed character data is allowed
10190
 
        // this controls whether or not we silently drop a tag
10191
 
        // or generate escaped HTML from it
10192
 
        $pcdata_allowed = isset($this->elements['#PCDATA']);
10193
 
        
10194
 
        // a little sanity check to make sure it's not ALL whitespace
10195
 
        $all_whitespace = true;
10196
 
        
10197
 
        // some configuration
10198
 
        $escape_invalid_children = $config->get('Core', 'EscapeInvalidChildren');
10199
 
        
10200
 
        // generator
10201
 
        $gen = new HTMLPurifier_Generator($config, $context);
10202
 
        
10203
 
        foreach ($tokens_of_children as $token) {
10204
 
            if (!empty($token->is_whitespace)) {
10205
 
                $result[] = $token;
10206
 
                continue;
10207
 
            }
10208
 
            $all_whitespace = false; // phew, we're not talking about whitespace
10209
 
            
10210
 
            $is_child = ($nesting == 0);
10211
 
            
10212
 
            if ($token instanceof HTMLPurifier_Token_Start) {
10213
 
                $nesting++;
10214
 
            } elseif ($token instanceof HTMLPurifier_Token_End) {
10215
 
                $nesting--;
10216
 
            }
10217
 
            
10218
 
            if ($is_child) {
10219
 
                $is_deleting = false;
10220
 
                if (!isset($this->elements[$token->name])) {
10221
 
                    $is_deleting = true;
10222
 
                    if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) {
10223
 
                        $result[] = $token;
10224
 
                    } elseif ($pcdata_allowed && $escape_invalid_children) {
10225
 
                        $result[] = new HTMLPurifier_Token_Text(
10226
 
                            $gen->generateFromToken($token)
10227
 
                        );
10228
 
                    }
10229
 
                    continue;
10230
 
                }
10231
 
            }
10232
 
            if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) {
10233
 
                $result[] = $token;
10234
 
            } elseif ($pcdata_allowed && $escape_invalid_children) {
10235
 
                $result[] =
10236
 
                    new HTMLPurifier_Token_Text(
10237
 
                        $gen->generateFromToken($token)
10238
 
                    );
10239
 
            } else {
10240
 
                // drop silently
10241
 
            }
10242
 
        }
10243
 
        if (empty($result)) return false;
10244
 
        if ($all_whitespace) return false;
10245
 
        if ($tokens_of_children == $result) return true;
10246
 
        return $result;
10247
 
    }
10248
 
}
10249
 
 
10250
 
 
10251
 
 
10252
 
 
10253
 
/**
10254
 
 * Definition that allows a set of elements, and allows no children.
10255
 
 * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required,
10256
 
 *       really, one shouldn't inherit from the other.  Only altered behavior
10257
 
 *       is to overload a returned false with an array.  Thus, it will never
10258
 
 *       return false.
10259
 
 */
10260
 
class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required
10261
 
{
10262
 
    public $allow_empty = true;
10263
 
    public $type = 'optional';
10264
 
    public function validateChildren($tokens_of_children, $config, $context) {
10265
 
        $result = parent::validateChildren($tokens_of_children, $config, $context);
10266
 
        if ($result === false) {
10267
 
            if (empty($tokens_of_children)) return true;
10268
 
            else return array();
10269
 
        }
10270
 
        return $result;
10271
 
    }
10272
 
}
10273
 
 
10274
 
 
10275
 
 
10276
 
 
10277
 
/**
10278
 
 * Takes the contents of blockquote when in strict and reformats for validation.
10279
 
 */
10280
 
class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required
10281
 
{
10282
 
    protected $real_elements;
10283
 
    protected $fake_elements;
10284
 
    public $allow_empty = true;
10285
 
    public $type = 'strictblockquote';
10286
 
    protected $init = false;
10287
 
    
10288
 
    /**
10289
 
     * @note We don't want MakeWellFormed to auto-close inline elements since
10290
 
     *       they might be allowed.
10291
 
     */
10292
 
    public function getNonAutoCloseElements($config) {
10293
 
        $this->init($config);
10294
 
        return $this->fake_elements;
10295
 
    }
10296
 
    
10297
 
    public function validateChildren($tokens_of_children, $config, $context) {
10298
 
        
10299
 
        $this->init($config);
10300
 
        
10301
 
        // trick the parent class into thinking it allows more
10302
 
        $this->elements = $this->fake_elements;
10303
 
        $result = parent::validateChildren($tokens_of_children, $config, $context);
10304
 
        $this->elements = $this->real_elements;
10305
 
        
10306
 
        if ($result === false) return array();
10307
 
        if ($result === true) $result = $tokens_of_children;
10308
 
        
10309
 
        $def = $config->getHTMLDefinition();
10310
 
        $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper);
10311
 
        $block_wrap_end   = new HTMLPurifier_Token_End(  $def->info_block_wrapper);
10312
 
        $is_inline = false;
10313
 
        $depth = 0;
10314
 
        $ret = array();
10315
 
        
10316
 
        // assuming that there are no comment tokens
10317
 
        foreach ($result as $i => $token) {
10318
 
            $token = $result[$i];
10319
 
            // ifs are nested for readability
10320
 
            if (!$is_inline) {
10321
 
                if (!$depth) {
10322
 
                     if (
10323
 
                        ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) ||
10324
 
                        (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name]))
10325
 
                     ) {
10326
 
                        $is_inline = true;
10327
 
                        $ret[] = $block_wrap_start;
10328
 
                     }
10329
 
                }
10330
 
            } else {
10331
 
                if (!$depth) {
10332
 
                    // starting tokens have been inline text / empty
10333
 
                    if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) {
10334
 
                        if (isset($this->elements[$token->name])) {
10335
 
                            // ended
10336
 
                            $ret[] = $block_wrap_end;
10337
 
                            $is_inline = false;
10338
 
                        }
10339
 
                    }
10340
 
                }
10341
 
            }
10342
 
            $ret[] = $token;
10343
 
            if ($token instanceof HTMLPurifier_Token_Start) $depth++;
10344
 
            if ($token instanceof HTMLPurifier_Token_End)   $depth--;
10345
 
        }
10346
 
        if ($is_inline) $ret[] = $block_wrap_end;
10347
 
        return $ret;
10348
 
    }
10349
 
    
10350
 
    private function init($config) {
10351
 
        if (!$this->init) {
10352
 
            $def = $config->getHTMLDefinition();
10353
 
            // allow all inline elements
10354
 
            $this->real_elements = $this->elements;
10355
 
            $this->fake_elements = $def->info_content_sets['Flow'];
10356
 
            $this->fake_elements['#PCDATA'] = true;
10357
 
            $this->init = true;
10358
 
        }
10359
 
    }
10360
 
}
10361
 
 
10362
 
 
10363
 
 
10364
 
 
10365
 
/**
10366
 
 * Definition for tables
10367
 
 */
10368
 
class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
10369
 
{
10370
 
    public $allow_empty = false;
10371
 
    public $type = 'table';
10372
 
    public $elements = array('tr' => true, 'tbody' => true, 'thead' => true,
10373
 
        'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true);
10374
 
    public function __construct() {}
10375
 
    public function validateChildren($tokens_of_children, $config, $context) {
10376
 
        if (empty($tokens_of_children)) return false;
10377
 
        
10378
 
        // this ensures that the loop gets run one last time before closing
10379
 
        // up. It's a little bit of a hack, but it works! Just make sure you
10380
 
        // get rid of the token later.
10381
 
        $tokens_of_children[] = false;
10382
 
        
10383
 
        // only one of these elements is allowed in a table
10384
 
        $caption = false;
10385
 
        $thead   = false;
10386
 
        $tfoot   = false;
10387
 
        
10388
 
        // as many of these as you want
10389
 
        $cols    = array();
10390
 
        $content = array();
10391
 
        
10392
 
        $nesting = 0; // current depth so we can determine nodes
10393
 
        $is_collecting = false; // are we globbing together tokens to package
10394
 
                                // into one of the collectors?
10395
 
        $collection = array(); // collected nodes
10396
 
        $tag_index = 0; // the first node might be whitespace,
10397
 
                            // so this tells us where the start tag is
10398
 
        
10399
 
        foreach ($tokens_of_children as $token) {
10400
 
            $is_child = ($nesting == 0);
10401
 
            
10402
 
            if ($token === false) {
10403
 
                // terminating sequence started
10404
 
            } elseif ($token instanceof HTMLPurifier_Token_Start) {
10405
 
                $nesting++;
10406
 
            } elseif ($token instanceof HTMLPurifier_Token_End) {
10407
 
                $nesting--;
10408
 
            }
10409
 
            
10410
 
            // handle node collection
10411
 
            if ($is_collecting) {
10412
 
                if ($is_child) {
10413
 
                    // okay, let's stash the tokens away
10414
 
                    // first token tells us the type of the collection
10415
 
                    switch ($collection[$tag_index]->name) {
10416
 
                        case 'tr':
10417
 
                        case 'tbody':
10418
 
                            $content[] = $collection;
10419
 
                            break;
10420
 
                        case 'caption':
10421
 
                            if ($caption !== false) break;
10422
 
                            $caption = $collection;
10423
 
                            break;
10424
 
                        case 'thead':
10425
 
                        case 'tfoot':
10426
 
                            // access the appropriate variable, $thead or $tfoot
10427
 
                            $var = $collection[$tag_index]->name;
10428
 
                            if ($$var === false) {
10429
 
                                $$var = $collection;
10430
 
                            } else {
10431
 
                                // transmutate the first and less entries into
10432
 
                                // tbody tags, and then put into content
10433
 
                                $collection[$tag_index]->name = 'tbody';
10434
 
                                $collection[count($collection)-1]->name = 'tbody';
10435
 
                                $content[] = $collection;
10436
 
                            }
10437
 
                            break;
10438
 
                         case 'colgroup':
10439
 
                            $cols[] = $collection;
10440
 
                            break;
10441
 
                    }
10442
 
                    $collection = array();
10443
 
                    $is_collecting = false;
10444
 
                    $tag_index = 0;
10445
 
                } else {
10446
 
                    // add the node to the collection
10447
 
                    $collection[] = $token;
10448
 
                }
10449
 
            }
10450
 
            
10451
 
            // terminate
10452
 
            if ($token === false) break;
10453
 
            
10454
 
            if ($is_child) {
10455
 
                // determine what we're dealing with
10456
 
                if ($token->name == 'col') {
10457
 
                    // the only empty tag in the possie, we can handle it
10458
 
                    // immediately
10459
 
                    $cols[] = array_merge($collection, array($token));
10460
 
                    $collection = array();
10461
 
                    $tag_index = 0;
10462
 
                    continue;
10463
 
                }
10464
 
                switch($token->name) {
10465
 
                    case 'caption':
10466
 
                    case 'colgroup':
10467
 
                    case 'thead':
10468
 
                    case 'tfoot':
10469
 
                    case 'tbody':
10470
 
                    case 'tr':
10471
 
                        $is_collecting = true;
10472
 
                        $collection[] = $token;
10473
 
                        continue;
10474
 
                    default:
10475
 
                        if ($token instanceof HTMLPurifier_Token_Text && $token->is_whitespace) {
10476
 
                            $collection[] = $token;
10477
 
                            $tag_index++;
10478
 
                        }
10479
 
                        continue;
10480
 
                }
10481
 
            }
10482
 
        }
10483
 
        
10484
 
        if (empty($content)) return false;
10485
 
        
10486
 
        $ret = array();
10487
 
        if ($caption !== false) $ret = array_merge($ret, $caption);
10488
 
        if ($cols !== false)    foreach ($cols as $token_array) $ret = array_merge($ret, $token_array);
10489
 
        if ($thead !== false)   $ret = array_merge($ret, $thead);
10490
 
        if ($tfoot !== false)   $ret = array_merge($ret, $tfoot);
10491
 
        foreach ($content as $token_array) $ret = array_merge($ret, $token_array);
10492
 
        if (!empty($collection) && $is_collecting == false){
10493
 
            // grab the trailing space
10494
 
            $ret = array_merge($ret, $collection);
10495
 
        }
10496
 
        
10497
 
        array_pop($tokens_of_children); // remove phantom token
10498
 
        
10499
 
        return ($ret === $tokens_of_children) ? true : $ret;
10500
 
        
10501
 
    }
10502
 
}
10503
 
 
10504
 
 
10505
 
 
10506
 
 
10507
 
class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache
10508
 
{
10509
 
    
10510
 
    /**
10511
 
     * Cache object we are decorating
10512
 
     */
10513
 
    public $cache;
10514
 
    
10515
 
    public function __construct() {}
10516
 
    
10517
 
    /**
10518
 
     * Lazy decorator function
10519
 
     * @param $cache Reference to cache object to decorate
10520
 
     */
10521
 
    public function decorate(&$cache) {
10522
 
        $decorator = $this->copy();
10523
 
        // reference is necessary for mocks in PHP 4
10524
 
        $decorator->cache =& $cache;
10525
 
        $decorator->type  = $cache->type;
10526
 
        return $decorator;
10527
 
    }
10528
 
    
10529
 
    /**
10530
 
     * Cross-compatible clone substitute
10531
 
     */
10532
 
    public function copy() {
10533
 
        return new HTMLPurifier_DefinitionCache_Decorator();
10534
 
    }
10535
 
    
10536
 
    public function add($def, $config) {
10537
 
        return $this->cache->add($def, $config);
10538
 
    }
10539
 
    
10540
 
    public function set($def, $config) {
10541
 
        return $this->cache->set($def, $config);
10542
 
    }
10543
 
    
10544
 
    public function replace($def, $config) {
10545
 
        return $this->cache->replace($def, $config);
10546
 
    }
10547
 
    
10548
 
    public function get($config) {
10549
 
        return $this->cache->get($config);
10550
 
    }
10551
 
    
10552
 
    public function remove($config) {
10553
 
        return $this->cache->remove($config);
10554
 
    }
10555
 
    
10556
 
    public function flush($config) {
10557
 
        return $this->cache->flush($config);
10558
 
    }
10559
 
    
10560
 
    public function cleanup($config) {
10561
 
        return $this->cache->cleanup($config);
10562
 
    }
10563
 
    
10564
 
}
10565
 
 
10566
 
 
10567
 
 
10568
 
 
10569
 
/**
10570
 
 * Null cache object to use when no caching is on.
10571
 
 */
10572
 
class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache
10573
 
{
10574
 
    
10575
 
    public function add($def, $config) {
10576
 
        return false;
10577
 
    }
10578
 
    
10579
 
    public function set($def, $config) {
10580
 
        return false;
10581
 
    }
10582
 
    
10583
 
    public function replace($def, $config) {
10584
 
        return false;
10585
 
    }
10586
 
    
10587
 
    public function remove($config) {
10588
 
        return false;
10589
 
    }
10590
 
    
10591
 
    public function get($config) {
10592
 
        return false;
10593
 
    }
10594
 
    
10595
 
    public function flush($config) {
10596
 
        return false;
10597
 
    }
10598
 
    
10599
 
    public function cleanup($config) {
10600
 
        return false;
10601
 
    }
10602
 
    
10603
 
}
10604
 
 
10605
 
 
10606
 
 
10607
 
 
10608
 
class HTMLPurifier_DefinitionCache_Serializer extends
10609
 
      HTMLPurifier_DefinitionCache
10610
 
{
10611
 
    
10612
 
    public function add($def, $config) {
10613
 
        if (!$this->checkDefType($def)) return;
10614
 
        $file = $this->generateFilePath($config);
10615
 
        if (file_exists($file)) return false;
10616
 
        if (!$this->_prepareDir($config)) return false;
10617
 
        return $this->_write($file, serialize($def));
10618
 
    }
10619
 
    
10620
 
    public function set($def, $config) {
10621
 
        if (!$this->checkDefType($def)) return;
10622
 
        $file = $this->generateFilePath($config);
10623
 
        if (!$this->_prepareDir($config)) return false;
10624
 
        return $this->_write($file, serialize($def));
10625
 
    }
10626
 
    
10627
 
    public function replace($def, $config) {
10628
 
        if (!$this->checkDefType($def)) return;
10629
 
        $file = $this->generateFilePath($config);
10630
 
        if (!file_exists($file)) return false;
10631
 
        if (!$this->_prepareDir($config)) return false;
10632
 
        return $this->_write($file, serialize($def));
10633
 
    }
10634
 
    
10635
 
    public function get($config) {
10636
 
        $file = $this->generateFilePath($config);
10637
 
        if (!file_exists($file)) return false;
10638
 
        return unserialize(file_get_contents($file));
10639
 
    }
10640
 
    
10641
 
    public function remove($config) {
10642
 
        $file = $this->generateFilePath($config);
10643
 
        if (!file_exists($file)) return false;
10644
 
        return unlink($file);
10645
 
    }
10646
 
    
10647
 
    public function flush($config) {
10648
 
        if (!$this->_prepareDir($config)) return false;
10649
 
        $dir = $this->generateDirectoryPath($config);
10650
 
        $dh  = opendir($dir);
10651
 
        while (false !== ($filename = readdir($dh))) {
10652
 
            if (empty($filename)) continue;
10653
 
            if ($filename[0] === '.') continue;
10654
 
            unlink($dir . '/' . $filename);
10655
 
        }
10656
 
    }
10657
 
    
10658
 
    public function cleanup($config) {
10659
 
        if (!$this->_prepareDir($config)) return false;
10660
 
        $dir = $this->generateDirectoryPath($config);
10661
 
        $dh  = opendir($dir);
10662
 
        while (false !== ($filename = readdir($dh))) {
10663
 
            if (empty($filename)) continue;
10664
 
            if ($filename[0] === '.') continue;
10665
 
            $key = substr($filename, 0, strlen($filename) - 4);
10666
 
            if ($this->isOld($key, $config)) unlink($dir . '/' . $filename);
10667
 
        }
10668
 
    }
10669
 
    
10670
 
    /**
10671
 
     * Generates the file path to the serial file corresponding to
10672
 
     * the configuration and definition name
10673
 
     * @todo Make protected
10674
 
     */
10675
 
    public function generateFilePath($config) {
10676
 
        $key = $this->generateKey($config);
10677
 
        return $this->generateDirectoryPath($config) . '/' . $key . '.ser';
10678
 
    }
10679
 
    
10680
 
    /**
10681
 
     * Generates the path to the directory contain this cache's serial files
10682
 
     * @note No trailing slash
10683
 
     * @todo Make protected
10684
 
     */
10685
 
    public function generateDirectoryPath($config) {
10686
 
        $base = $this->generateBaseDirectoryPath($config);
10687
 
        return $base . '/' . $this->type;
10688
 
    }
10689
 
    
10690
 
    /**
10691
 
     * Generates path to base directory that contains all definition type
10692
 
     * serials
10693
 
     * @todo Make protected
10694
 
     */
10695
 
    public function generateBaseDirectoryPath($config) {
10696
 
        $base = $config->get('Cache', 'SerializerPath');
10697
 
        $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base;
10698
 
        return $base;
10699
 
    }
10700
 
    
10701
 
    /**
10702
 
     * Convenience wrapper function for file_put_contents
10703
 
     * @param $file File name to write to
10704
 
     * @param $data Data to write into file
10705
 
     * @return Number of bytes written if success, or false if failure.
10706
 
     */
10707
 
    private function _write($file, $data) {
10708
 
        return file_put_contents($file, $data);
10709
 
    }
10710
 
    
10711
 
    /**
10712
 
     * Prepares the directory that this type stores the serials in
10713
 
     * @return True if successful
10714
 
     */
10715
 
    private function _prepareDir($config) {
10716
 
        $directory = $this->generateDirectoryPath($config);
10717
 
        if (!is_dir($directory)) {
10718
 
            $base = $this->generateBaseDirectoryPath($config);
10719
 
            if (!is_dir($base)) {
10720
 
                trigger_error('Base directory '.$base.' does not exist,
10721
 
                    please create or change using %Cache.SerializerPath',
10722
 
                    E_USER_ERROR);
10723
 
                return false;
10724
 
            } elseif (!$this->_testPermissions($base)) {
10725
 
                return false;
10726
 
            }
10727
 
            $old = umask(0022); // disable group and world writes
10728
 
            mkdir($directory);
10729
 
            umask($old);
10730
 
        } elseif (!$this->_testPermissions($directory)) {
10731
 
            return false;
10732
 
        }
10733
 
        return true;
10734
 
    }
10735
 
    
10736
 
    /**
10737
 
     * Tests permissions on a directory and throws out friendly
10738
 
     * error messages and attempts to chmod it itself if possible
10739
 
     */
10740
 
    private function _testPermissions($dir) {
10741
 
        // early abort, if it is writable, everything is hunky-dory
10742
 
        if (is_writable($dir)) return true;
10743
 
        if (!is_dir($dir)) {
10744
 
            // generally, you'll want to handle this beforehand
10745
 
            // so a more specific error message can be given
10746
 
            trigger_error('Directory '.$dir.' does not exist',
10747
 
                E_USER_ERROR);
10748
 
            return false;
10749
 
        }
10750
 
        if (function_exists('posix_getuid')) {
10751
 
            // POSIX system, we can give more specific advice
10752
 
            if (fileowner($dir) === posix_getuid()) {
10753
 
                // we can chmod it ourselves
10754
 
                chmod($dir, 0755);
10755
 
                return true;
10756
 
            } elseif (filegroup($dir) === posix_getgid()) {
10757
 
                $chmod = '775';
10758
 
            } else {
10759
 
                // PHP's probably running as nobody, so we'll
10760
 
                // need to give global permissions
10761
 
                $chmod = '777';
10762
 
            }
10763
 
            trigger_error('Directory '.$dir.' not writable, '.
10764
 
                'please chmod to ' . $chmod,
10765
 
                E_USER_ERROR);
10766
 
        } else {
10767
 
            // generic error message
10768
 
            trigger_error('Directory '.$dir.' not writable, '.
10769
 
                'please alter file permissions',
10770
 
                E_USER_ERROR);
10771
 
        }
10772
 
        return false;
10773
 
    }
10774
 
    
10775
 
}
10776
 
 
10777
 
 
10778
 
 
10779
 
 
10780
 
/**
10781
 
 * Definition cache decorator class that cleans up the cache
10782
 
 * whenever there is a cache miss.
10783
 
 */
10784
 
class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends
10785
 
      HTMLPurifier_DefinitionCache_Decorator
10786
 
{
10787
 
    
10788
 
    public $name = 'Cleanup';
10789
 
    
10790
 
    public function copy() {
10791
 
        return new HTMLPurifier_DefinitionCache_Decorator_Cleanup();
10792
 
    }
10793
 
    
10794
 
    public function add($def, $config) {
10795
 
        $status = parent::add($def, $config);
10796
 
        if (!$status) parent::cleanup($config);
10797
 
        return $status;
10798
 
    }
10799
 
    
10800
 
    public function set($def, $config) {
10801
 
        $status = parent::set($def, $config);
10802
 
        if (!$status) parent::cleanup($config);
10803
 
        return $status;
10804
 
    }
10805
 
    
10806
 
    public function replace($def, $config) {
10807
 
        $status = parent::replace($def, $config);
10808
 
        if (!$status) parent::cleanup($config);
10809
 
        return $status;
10810
 
    }
10811
 
    
10812
 
    public function get($config) {
10813
 
        $ret = parent::get($config);
10814
 
        if (!$ret) parent::cleanup($config);
10815
 
        return $ret;
10816
 
    }
10817
 
    
10818
 
}
10819
 
 
10820
 
 
10821
 
 
10822
 
 
10823
 
/**
10824
 
 * Definition cache decorator class that saves all cache retrievals
10825
 
 * to PHP's memory; good for unit tests or circumstances where 
10826
 
 * there are lots of configuration objects floating around.
10827
 
 */
10828
 
class HTMLPurifier_DefinitionCache_Decorator_Memory extends
10829
 
      HTMLPurifier_DefinitionCache_Decorator
10830
 
{
10831
 
    
10832
 
    protected $definitions;
10833
 
    public $name = 'Memory';
10834
 
    
10835
 
    public function copy() {
10836
 
        return new HTMLPurifier_DefinitionCache_Decorator_Memory();
10837
 
    }
10838
 
    
10839
 
    public function add($def, $config) {
10840
 
        $status = parent::add($def, $config);
10841
 
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
10842
 
        return $status;
10843
 
    }
10844
 
    
10845
 
    public function set($def, $config) {
10846
 
        $status = parent::set($def, $config);
10847
 
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
10848
 
        return $status;
10849
 
    }
10850
 
    
10851
 
    public function replace($def, $config) {
10852
 
        $status = parent::replace($def, $config);
10853
 
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
10854
 
        return $status;
10855
 
    }
10856
 
    
10857
 
    public function get($config) {
10858
 
        $key = $this->generateKey($config);
10859
 
        if (isset($this->definitions[$key])) return $this->definitions[$key];
10860
 
        $this->definitions[$key] = parent::get($config);
10861
 
        return $this->definitions[$key];
10862
 
    }
10863
 
    
10864
 
}
10865
 
 
10866
 
 
10867
 
 
10868
 
 
10869
 
/**
10870
 
 * XHTML 1.1 Bi-directional Text Module, defines elements that
10871
 
 * declare directionality of content. Text Extension Module.
10872
 
 */
10873
 
class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule
10874
 
{
10875
 
    
10876
 
    public $name = 'Bdo';
10877
 
    public $attr_collections = array(
10878
 
        'I18N' => array('dir' => false)
10879
 
    );
10880
 
    
10881
 
    public function setup($config) {
10882
 
        $bdo = $this->addElement(
10883
 
            'bdo', 'Inline', 'Inline', array('Core', 'Lang'),
10884
 
            array(
10885
 
                'dir' => 'Enum#ltr,rtl', // required
10886
 
                // The Abstract Module specification has the attribute
10887
 
                // inclusions wrong for bdo: bdo allows Lang
10888
 
            )
10889
 
        );
10890
 
        $bdo->attr_transform_post['required-dir'] = new HTMLPurifier_AttrTransform_BdoDir();
10891
 
        
10892
 
        $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
10893
 
    }
10894
 
    
10895
 
}
10896
 
 
10897
 
 
10898
 
 
10899
 
 
10900
 
class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
10901
 
{
10902
 
    public $name = 'CommonAttributes';
10903
 
    
10904
 
    public $attr_collections = array(
10905
 
        'Core' => array(
10906
 
            0 => array('Style'),
10907
 
            // 'xml:space' => false,
10908
 
            'class' => 'NMTOKENS',
10909
 
            'id' => 'ID',
10910
 
            'title' => 'CDATA',
10911
 
        ),
10912
 
        'Lang' => array(),
10913
 
        'I18N' => array(
10914
 
            0 => array('Lang'), // proprietary, for xml:lang/lang
10915
 
        ),
10916
 
        'Common' => array(
10917
 
            0 => array('Core', 'I18N')
10918
 
        )
10919
 
    );
10920
 
}
10921
 
 
10922
 
 
10923
 
 
10924
 
 
10925
 
/**
10926
 
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
10927
 
 * Module.
10928
 
 */
10929
 
class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule
10930
 
{
10931
 
    
10932
 
    public $name = 'Edit';
10933
 
    
10934
 
    public function setup($config) {
10935
 
        $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow';
10936
 
        $attr = array(
10937
 
            'cite' => 'URI',
10938
 
            // 'datetime' => 'Datetime', // not implemented
10939
 
        );
10940
 
        $this->addElement('del', 'Inline', $contents, 'Common', $attr);
10941
 
        $this->addElement('ins', 'Inline', $contents, 'Common', $attr);
10942
 
    }
10943
 
    
10944
 
    // HTML 4.01 specifies that ins/del must not contain block
10945
 
    // elements when used in an inline context, chameleon is
10946
 
    // a complicated workaround to acheive this effect
10947
 
    
10948
 
    // Inline context ! Block context (exclamation mark is
10949
 
    // separator, see getChildDef for parsing)
10950
 
    
10951
 
    public $defines_child_def = true;
10952
 
    public function getChildDef($def) {
10953
 
        if ($def->content_model_type != 'chameleon') return false;
10954
 
        $value = explode('!', $def->content_model);
10955
 
        return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
10956
 
    }
10957
 
    
10958
 
}
10959
 
 
10960
 
 
10961
 
 
10962
 
 
10963
 
/**
10964
 
 * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4.
10965
 
 */
10966
 
class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
10967
 
{
10968
 
    public $name = 'Forms';
10969
 
    public $safe = false;
10970
 
    
10971
 
    public $content_sets = array(
10972
 
        'Block' => 'Form',
10973
 
        'Inline' => 'Formctrl',
10974
 
    );
10975
 
    
10976
 
    public function setup($config) {
10977
 
        $form = $this->addElement('form', 'Form',
10978
 
          'Required: Heading | List | Block | fieldset', 'Common', array(
10979
 
            'accept' => 'ContentTypes',
10980
 
            'accept-charset' => 'Charsets',
10981
 
            'action*' => 'URI',
10982
 
            'method' => 'Enum#get,post',
10983
 
            // really ContentType, but these two are the only ones used today
10984
 
            'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
10985
 
        ));
10986
 
        $form->excludes = array('form' => true);
10987
 
        
10988
 
        $input = $this->addElement('input', 'Formctrl', 'Empty', 'Common', array(
10989
 
            'accept' => 'ContentTypes',
10990
 
            'accesskey' => 'Character',
10991
 
            'alt' => 'Text',
10992
 
            'checked' => 'Bool#checked',
10993
 
            'disabled' => 'Bool#disabled',
10994
 
            'maxlength' => 'Number',
10995
 
            'name' => 'CDATA',
10996
 
            'readonly' => 'Bool#readonly',
10997
 
            'size' => 'Number',
10998
 
            'src' => 'URI#embeds',
10999
 
            'tabindex' => 'Number',
11000
 
            'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
11001
 
            'value' => 'CDATA',
11002
 
        ));
11003
 
        $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();
11004
 
        
11005
 
        $this->addElement('select', 'Formctrl', 'Required: optgroup | option', 'Common', array(
11006
 
            'disabled' => 'Bool#disabled',
11007
 
            'multiple' => 'Bool#multiple',
11008
 
            'name' => 'CDATA',
11009
 
            'size' => 'Number',
11010
 
            'tabindex' => 'Number',
11011
 
        ));
11012
 
        
11013
 
        $this->addElement('option', false, 'Optional: #PCDATA', 'Common', array(
11014
 
            'disabled' => 'Bool#disabled',
11015
 
            'label' => 'Text',
11016
 
            'selected' => 'Bool#selected',
11017
 
            'value' => 'CDATA',
11018
 
        ));
11019
 
        // It's illegal for there to be more than one selected, but not
11020
 
        // be multiple. Also, no selected means undefined behavior. This might
11021
 
        // be difficult to implement; perhaps an injector, or a context variable.
11022
 
        
11023
 
        $textarea = $this->addElement('textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array(
11024
 
            'accesskey' => 'Character',
11025
 
            'cols*' => 'Number',
11026
 
            'disabled' => 'Bool#disabled',
11027
 
            'name' => 'CDATA',
11028
 
            'readonly' => 'Bool#readonly',
11029
 
            'rows*' => 'Number',
11030
 
            'tabindex' => 'Number',
11031
 
        ));
11032
 
        $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();
11033
 
        
11034
 
        $button = $this->addElement('button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array(
11035
 
            'accesskey' => 'Character',
11036
 
            'disabled' => 'Bool#disabled',
11037
 
            'name' => 'CDATA',
11038
 
            'tabindex' => 'Number',
11039
 
            'type' => 'Enum#button,submit,reset',
11040
 
            'value' => 'CDATA',
11041
 
        ));
11042
 
        
11043
 
        // For exclusions, ideally we'd specify content sets, not literal elements
11044
 
        $button->excludes = $this->makeLookup(
11045
 
            'form', 'fieldset', // Form
11046
 
            'input', 'select', 'textarea', 'label', 'button', // Formctrl
11047
 
            'a' // as per HTML 4.01 spec, this is omitted by modularization
11048
 
        );
11049
 
        
11050
 
        // Extra exclusion: img usemap="" is not permitted within this element.
11051
 
        // We'll omit this for now, since we don't have any good way of
11052
 
        // indicating it yet.
11053
 
        
11054
 
        // This is HIGHLY user-unfriendly; we need a custom child-def for this
11055
 
        $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common');
11056
 
        
11057
 
        $label = $this->addElement('label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array(
11058
 
            'accesskey' => 'Character',
11059
 
            // 'for' => 'IDREF', // IDREF not implemented, cannot allow
11060
 
        ));
11061
 
        $label->excludes = array('label' => true);
11062
 
        
11063
 
        $this->addElement('legend', false, 'Optional: #PCDATA | Inline', 'Common', array(
11064
 
            'accesskey' => 'Character',
11065
 
        ));
11066
 
        
11067
 
        $this->addElement('optgroup', false, 'Required: option', 'Common', array(
11068
 
            'disabled' => 'Bool#disabled',
11069
 
            'label*' => 'Text',
11070
 
        ));
11071
 
        
11072
 
        // Don't forget an injector for <isindex>. This one's a little complex
11073
 
        // because it maps to multiple elements.
11074
 
        
11075
 
    }
11076
 
}
11077
 
 
11078
 
 
11079
 
 
11080
 
 
11081
 
/**
11082
 
 * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module.
11083
 
 */
11084
 
class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule
11085
 
{
11086
 
    
11087
 
    public $name = 'Hypertext';
11088
 
    
11089
 
    public function setup($config) {
11090
 
        $a = $this->addElement(
11091
 
            'a', 'Inline', 'Inline', 'Common',
11092
 
            array(
11093
 
                // 'accesskey' => 'Character',
11094
 
                // 'charset' => 'Charset',
11095
 
                'href' => 'URI',
11096
 
                // 'hreflang' => 'LanguageCode',
11097
 
                'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
11098
 
                'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
11099
 
                // 'tabindex' => 'Number',
11100
 
                // 'type' => 'ContentType',
11101
 
            )
11102
 
        );
11103
 
        $a->excludes = array('a' => true);
11104
 
    }
11105
 
    
11106
 
}
11107
 
 
11108
 
 
11109
 
 
11110
 
 
11111
 
/**
11112
 
 * XHTML 1.1 Image Module provides basic image embedding.
11113
 
 * @note There is specialized code for removing empty images in
11114
 
 *       HTMLPurifier_Strategy_RemoveForeignElements
11115
 
 */
11116
 
class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule
11117
 
{
11118
 
    
11119
 
    public $name = 'Image';
11120
 
    
11121
 
    public function setup($config) {
11122
 
        $max = $config->get('HTML', 'MaxImgLength');
11123
 
        $img = $this->addElement(
11124
 
            'img', 'Inline', 'Empty', 'Common',
11125
 
            array(
11126
 
                'alt*' => 'Text',
11127
 
                // According to the spec, it's Length, but percents can
11128
 
                // be abused, so we allow only Pixels.
11129
 
                'height' => 'Pixels#' . $max,
11130
 
                'width'  => 'Pixels#' . $max,
11131
 
                'longdesc' => 'URI', 
11132
 
                'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
11133
 
            )
11134
 
        );
11135
 
        if ($max === null || $config->get('HTML', 'Trusted')) {
11136
 
            $img->attr['height'] =
11137
 
            $img->attr['width'] = 'Length';
11138
 
        }
11139
 
        
11140
 
        // kind of strange, but splitting things up would be inefficient
11141
 
        $img->attr_transform_pre[] =
11142
 
        $img->attr_transform_post[] =
11143
 
            new HTMLPurifier_AttrTransform_ImgRequired();
11144
 
    }
11145
 
    
11146
 
}
11147
 
 
11148
 
 
11149
 
 
11150
 
 
11151
 
/**
11152
 
 * XHTML 1.1 Legacy module defines elements that were previously 
11153
 
 * deprecated.
11154
 
 * 
11155
 
 * @note Not all legacy elements have been implemented yet, which
11156
 
 *       is a bit of a reverse problem as compared to browsers! In
11157
 
 *       addition, this legacy module may implement a bit more than
11158
 
 *       mandated by XHTML 1.1.
11159
 
 * 
11160
 
 * This module can be used in combination with TransformToStrict in order
11161
 
 * to transform as many deprecated elements as possible, but retain
11162
 
 * questionably deprecated elements that do not have good alternatives
11163
 
 * as well as transform elements that don't have an implementation.
11164
 
 * See docs/ref-strictness.txt for more details.
11165
 
 */
11166
 
 
11167
 
class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule
11168
 
{
11169
 
    
11170
 
    public $name = 'Legacy';
11171
 
    
11172
 
    public function setup($config) {
11173
 
        
11174
 
        $this->addElement('basefont', 'Inline', 'Empty', false, array(
11175
 
            'color' => 'Color',
11176
 
            'face' => 'Text', // extremely broad, we should
11177
 
            'size' => 'Text', // tighten it
11178
 
            'id' => 'ID'
11179
 
        ));
11180
 
        $this->addElement('center', 'Block', 'Flow', 'Common');
11181
 
        $this->addElement('dir', 'Block', 'Required: li', 'Common', array(
11182
 
            'compact' => 'Bool#compact'
11183
 
        ));
11184
 
        $this->addElement('font', 'Inline', 'Inline', array('Core', 'I18N'), array(
11185
 
            'color' => 'Color',
11186
 
            'face' => 'Text', // extremely broad, we should
11187
 
            'size' => 'Text', // tighten it
11188
 
        ));
11189
 
        $this->addElement('menu', 'Block', 'Required: li', 'Common', array(
11190
 
            'compact' => 'Bool#compact'
11191
 
        ));
11192
 
        $this->addElement('s', 'Inline', 'Inline', 'Common');
11193
 
        $this->addElement('strike', 'Inline', 'Inline', 'Common');
11194
 
        $this->addElement('u', 'Inline', 'Inline', 'Common');
11195
 
        
11196
 
        // setup modifications to old elements
11197
 
        
11198
 
        $align = 'Enum#left,right,center,justify';
11199
 
        
11200
 
        $address = $this->addBlankElement('address');
11201
 
        $address->content_model = 'Inline | #PCDATA | p';
11202
 
        $address->content_model_type = 'optional';
11203
 
        $address->child = false;
11204
 
        
11205
 
        $blockquote = $this->addBlankElement('blockquote');
11206
 
        $blockquote->content_model = 'Flow | #PCDATA';
11207
 
        $blockquote->content_model_type = 'optional';
11208
 
        $blockquote->child = false;
11209
 
        
11210
 
        $br = $this->addBlankElement('br');
11211
 
        $br->attr['clear'] = 'Enum#left,all,right,none';
11212
 
        
11213
 
        $caption = $this->addBlankElement('caption');
11214
 
        $caption->attr['align'] = 'Enum#top,bottom,left,right';
11215
 
        
11216
 
        $div = $this->addBlankElement('div');
11217
 
        $div->attr['align'] = $align;
11218
 
        
11219
 
        $dl = $this->addBlankElement('dl');
11220
 
        $dl->attr['compact'] = 'Bool#compact';
11221
 
        
11222
 
        for ($i = 1; $i <= 6; $i++) {
11223
 
            $h = $this->addBlankElement("h$i");
11224
 
            $h->attr['align'] = $align;
11225
 
        }
11226
 
        
11227
 
        $hr = $this->addBlankElement('hr');
11228
 
        $hr->attr['align'] = $align;
11229
 
        $hr->attr['noshade'] = 'Bool#noshade';
11230
 
        $hr->attr['size'] = 'Pixels';
11231
 
        $hr->attr['width'] = 'Length';
11232
 
        
11233
 
        $img = $this->addBlankElement('img');
11234
 
        $img->attr['align'] = 'Enum#top,middle,bottom,left,right';
11235
 
        $img->attr['border'] = 'Pixels';
11236
 
        $img->attr['hspace'] = 'Pixels';
11237
 
        $img->attr['vspace'] = 'Pixels';
11238
 
        
11239
 
        // figure out this integer business
11240
 
        
11241
 
        $li = $this->addBlankElement('li');
11242
 
        $li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
11243
 
        $li->attr['type']  = 'Enum#s:1,i,I,a,A,disc,square,circle';
11244
 
        
11245
 
        $ol = $this->addBlankElement('ol');
11246
 
        $ol->attr['compact'] = 'Bool#compact';
11247
 
        $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
11248
 
        $ol->attr['type'] = 'Enum#s:1,i,I,a,A';
11249
 
        
11250
 
        $p = $this->addBlankElement('p');
11251
 
        $p->attr['align'] = $align;
11252
 
        
11253
 
        $pre = $this->addBlankElement('pre');
11254
 
        $pre->attr['width'] = 'Number';
11255
 
        
11256
 
        // script omitted
11257
 
        
11258
 
        $table = $this->addBlankElement('table');
11259
 
        $table->attr['align'] = 'Enum#left,center,right';
11260
 
        $table->attr['bgcolor'] = 'Color';
11261
 
        
11262
 
        $tr = $this->addBlankElement('tr');
11263
 
        $tr->attr['bgcolor'] = 'Color';
11264
 
        
11265
 
        $th = $this->addBlankElement('th');
11266
 
        $th->attr['bgcolor'] = 'Color';
11267
 
        $th->attr['height'] = 'Length';
11268
 
        $th->attr['nowrap'] = 'Bool#nowrap';
11269
 
        $th->attr['width'] = 'Length';
11270
 
        
11271
 
        $td = $this->addBlankElement('td');
11272
 
        $td->attr['bgcolor'] = 'Color';
11273
 
        $td->attr['height'] = 'Length';
11274
 
        $td->attr['nowrap'] = 'Bool#nowrap';
11275
 
        $td->attr['width'] = 'Length';
11276
 
        
11277
 
        $ul = $this->addBlankElement('ul');
11278
 
        $ul->attr['compact'] = 'Bool#compact';
11279
 
        $ul->attr['type'] = 'Enum#square,disc,circle';
11280
 
        
11281
 
    }
11282
 
    
11283
 
}
11284
 
 
11285
 
 
11286
 
 
11287
 
 
11288
 
/**
11289
 
 * XHTML 1.1 List Module, defines list-oriented elements. Core Module.
11290
 
 */
11291
 
class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule
11292
 
{
11293
 
    
11294
 
    public $name = 'List';
11295
 
    
11296
 
    // According to the abstract schema, the List content set is a fully formed
11297
 
    // one or more expr, but it invariably occurs in an optional declaration
11298
 
    // so we're not going to do that subtlety. It might cause trouble
11299
 
    // if a user defines "List" and expects that multiple lists are
11300
 
    // allowed to be specified, but then again, that's not very intuitive.
11301
 
    // Furthermore, the actual XML Schema may disagree. Regardless,
11302
 
    // we don't have support for such nested expressions without using
11303
 
    // the incredibly inefficient and draconic Custom ChildDef.
11304
 
    
11305
 
    public $content_sets = array('Flow' => 'List');
11306
 
    
11307
 
    public function setup($config) {
11308
 
        $this->addElement('ol', 'List', 'Required: li', 'Common');
11309
 
        $this->addElement('ul', 'List', 'Required: li', 'Common');
11310
 
        $this->addElement('dl', 'List', 'Required: dt | dd', 'Common');
11311
 
        
11312
 
        $this->addElement('li', false, 'Flow', 'Common');
11313
 
        
11314
 
        $this->addElement('dd', false, 'Flow', 'Common');
11315
 
        $this->addElement('dt', false, 'Inline', 'Common');
11316
 
    }
11317
 
    
11318
 
}
11319
 
 
11320
 
 
11321
 
 
11322
 
 
11323
 
class HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule
11324
 
{
11325
 
    
11326
 
    public $name = 'Name';
11327
 
    
11328
 
    public function setup($config) {
11329
 
        $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map');
11330
 
        foreach ($elements as $name) {
11331
 
            $element = $this->addBlankElement($name);
11332
 
            $element->attr['name'] = 'ID';
11333
 
        }
11334
 
    }
11335
 
    
11336
 
}
11337
 
 
11338
 
 
11339
 
 
11340
 
class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule
11341
 
{
11342
 
    public $name = 'NonXMLCommonAttributes';
11343
 
    
11344
 
    public $attr_collections = array(
11345
 
        'Lang' => array(
11346
 
            'lang' => 'LanguageCode',
11347
 
        )
11348
 
    );
11349
 
}
11350
 
 
11351
 
 
11352
 
 
11353
 
 
11354
 
/**
11355
 
 * XHTML 1.1 Object Module, defines elements for generic object inclusion
11356
 
 * @warning Users will commonly use <embed> to cater to legacy browsers: this
11357
 
 *      module does not allow this sort of behavior
11358
 
 */
11359
 
class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
11360
 
{
11361
 
    
11362
 
    public $name = 'Object';
11363
 
    public $safe = false;
11364
 
    
11365
 
    public function setup($config) {
11366
 
        
11367
 
        $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common', 
11368
 
            array(
11369
 
                'archive' => 'URI',
11370
 
                'classid' => 'URI',
11371
 
                'codebase' => 'URI',
11372
 
                'codetype' => 'Text',
11373
 
                'data' => 'URI',
11374
 
                'declare' => 'Bool#declare',
11375
 
                'height' => 'Length',
11376
 
                'name' => 'CDATA',
11377
 
                'standby' => 'Text',
11378
 
                'tabindex' => 'Number',
11379
 
                'type' => 'ContentType',
11380
 
                'width' => 'Length'
11381
 
            )
11382
 
        );
11383
 
 
11384
 
        $this->addElement('param', false, 'Empty', false,
11385
 
            array(
11386
 
                'id' => 'ID',
11387
 
                'name*' => 'Text',
11388
 
                'type' => 'Text',
11389
 
                'value' => 'Text',
11390
 
                'valuetype' => 'Enum#data,ref,object'
11391
 
           )
11392
 
        );
11393
 
    
11394
 
    }
11395
 
    
11396
 
}
11397
 
 
11398
 
 
11399
 
 
11400
 
 
11401
 
/**
11402
 
 * XHTML 1.1 Presentation Module, defines simple presentation-related
11403
 
 * markup. Text Extension Module.
11404
 
 * @note The official XML Schema and DTD specs further divide this into
11405
 
 *       two modules:
11406
 
 *          - Block Presentation (hr)
11407
 
 *          - Inline Presentation (b, big, i, small, sub, sup, tt)
11408
 
 *       We have chosen not to heed this distinction, as content_sets
11409
 
 *       provides satisfactory disambiguation.
11410
 
 */
11411
 
class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule
11412
 
{
11413
 
    
11414
 
    public $name = 'Presentation';
11415
 
    
11416
 
    public function setup($config) {
11417
 
        $this->addElement('b',      'Inline', 'Inline', 'Common');
11418
 
        $this->addElement('big',    'Inline', 'Inline', 'Common');
11419
 
        $this->addElement('hr',     'Block',  'Empty',  'Common');
11420
 
        $this->addElement('i',      'Inline', 'Inline', 'Common');
11421
 
        $this->addElement('small',  'Inline', 'Inline', 'Common');
11422
 
        $this->addElement('sub',    'Inline', 'Inline', 'Common');
11423
 
        $this->addElement('sup',    'Inline', 'Inline', 'Common');
11424
 
        $this->addElement('tt',     'Inline', 'Inline', 'Common');
11425
 
    }
11426
 
    
11427
 
}
11428
 
 
11429
 
 
11430
 
 
11431
 
 
11432
 
/**
11433
 
 * Module defines proprietary tags and attributes in HTML.
11434
 
 * @warning If this module is enabled, standards-compliance is off!
11435
 
 */
11436
 
class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule
11437
 
{
11438
 
    
11439
 
    public $name = 'Proprietary';
11440
 
    
11441
 
    public function setup($config) {
11442
 
        
11443
 
        $this->addElement('marquee', 'Inline', 'Flow', 'Common', 
11444
 
            array(
11445
 
                'direction' => 'Enum#left,right,up,down',
11446
 
                'behavior' => 'Enum#alternate',
11447
 
                'width' => 'Length',
11448
 
                'height' => 'Length',
11449
 
                'scrolldelay' => 'Number',
11450
 
                'scrollamount' => 'Number',
11451
 
                'loop' => 'Number',
11452
 
                'bgcolor' => 'Color',
11453
 
                'hspace' => 'Pixels',
11454
 
                'vspace' => 'Pixels',
11455
 
            )
11456
 
        );
11457
 
    
11458
 
    }
11459
 
    
11460
 
}
11461
 
 
11462
 
 
11463
 
 
11464
 
 
11465
 
/**
11466
 
 * XHTML 1.1 Ruby Annotation Module, defines elements that indicate
11467
 
 * short runs of text alongside base text for annotation or pronounciation.
11468
 
 */
11469
 
class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule
11470
 
{
11471
 
    
11472
 
    public $name = 'Ruby';
11473
 
    
11474
 
    public function setup($config) {
11475
 
        $this->addElement('ruby', 'Inline',
11476
 
            'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
11477
 
            'Common');
11478
 
        $this->addElement('rbc', false, 'Required: rb', 'Common');
11479
 
        $this->addElement('rtc', false, 'Required: rt', 'Common');
11480
 
        $rb = $this->addElement('rb', false, 'Inline', 'Common');
11481
 
        $rb->excludes = array('ruby' => true);
11482
 
        $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number'));
11483
 
        $rt->excludes = array('ruby' => true);
11484
 
        $this->addElement('rp', false, 'Optional: #PCDATA', 'Common');
11485
 
    }
11486
 
    
11487
 
}
11488
 
 
11489
 
 
11490
 
 
11491
 
 
11492
 
/**
11493
 
 * A "safe" embed module. See SafeObject. This is a proprietary element.
11494
 
 */
11495
 
class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
11496
 
{
11497
 
    
11498
 
    public $name = 'SafeEmbed';
11499
 
    
11500
 
    public function setup($config) {
11501
 
        
11502
 
        $max = $config->get('HTML', 'MaxImgLength');
11503
 
        $embed = $this->addElement(
11504
 
            'embed', 'Inline', 'Empty', 'Common',
11505
 
            array(
11506
 
                'src*' => 'URI#embedded',
11507
 
                'type' => 'Enum#application/x-shockwave-flash',
11508
 
                'width' => 'Pixels#' . $max,
11509
 
                'height' => 'Pixels#' . $max,
11510
 
                'allowscriptaccess' => 'Enum#never',
11511
 
                'allownetworking' => 'Enum#internal',
11512
 
                'wmode' => 'Enum#window',
11513
 
                'name' => 'ID',
11514
 
            )
11515
 
        );
11516
 
        $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
11517
 
        
11518
 
    }
11519
 
    
11520
 
}
11521
 
 
11522
 
 
11523
 
 
11524
 
/**
11525
 
 * A "safe" object module. In theory, objects permitted by this module will
11526
 
 * be safe, and untrusted users can be allowed to embed arbitrary flash objects
11527
 
 * (maybe other types too, but only Flash is supported as of right now).
11528
 
 * Highly experimental.
11529
 
 */
11530
 
class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
11531
 
{
11532
 
    
11533
 
    public $name = 'SafeObject';
11534
 
    
11535
 
    public function setup($config) {
11536
 
        
11537
 
        // These definitions are not intrinsically safe: the attribute transforms
11538
 
        // are a vital part of ensuring safety.
11539
 
        
11540
 
        $max = $config->get('HTML', 'MaxImgLength');
11541
 
        $object = $this->addElement(
11542
 
            'object',
11543
 
            'Inline',
11544
 
            'Optional: param | Flow | #PCDATA',
11545
 
            'Common',
11546
 
            array(
11547
 
                // While technically not required by the spec, we're forcing
11548
 
                // it to this value.
11549
 
                'type'   => 'Enum#application/x-shockwave-flash',
11550
 
                'width'  => 'Pixels#' . $max,
11551
 
                'height' => 'Pixels#' . $max,
11552
 
                'data'   => 'URI#embedded'
11553
 
            )
11554
 
        );
11555
 
        $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();
11556
 
 
11557
 
        $param = $this->addElement('param', false, 'Empty', false,
11558
 
            array(
11559
 
                'id' => 'ID',
11560
 
                'name*' => 'Text', 
11561
 
                'value' => 'Text'
11562
 
            )
11563
 
        );
11564
 
        $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
11565
 
        $this->info_injector[] = 'SafeObject';
11566
 
    
11567
 
    }
11568
 
    
11569
 
}
11570
 
 
11571
 
 
11572
 
 
11573
 
/*
11574
 
 
11575
 
WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
11576
 
INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!
11577
 
 
11578
 
*/
11579
 
 
11580
 
/**
11581
 
 * XHTML 1.1 Scripting module, defines elements that are used to contain
11582
 
 * information pertaining to executable scripts or the lack of support
11583
 
 * for executable scripts.
11584
 
 * @note This module does not contain inline scripting elements
11585
 
 */
11586
 
class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule
11587
 
{
11588
 
    public $name = 'Scripting';
11589
 
    public $elements = array('script', 'noscript');
11590
 
    public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript');
11591
 
    public $safe = false;
11592
 
    
11593
 
    public function setup($config) {
11594
 
        // TODO: create custom child-definition for noscript that
11595
 
        // auto-wraps stray #PCDATA in a similar manner to 
11596
 
        // blockquote's custom definition (we would use it but
11597
 
        // blockquote's contents are optional while noscript's contents
11598
 
        // are required)
11599
 
        
11600
 
        // TODO: convert this to new syntax, main problem is getting
11601
 
        // both content sets working
11602
 
        
11603
 
        // In theory, this could be safe, but I don't see any reason to
11604
 
        // allow it.
11605
 
        $this->info['noscript'] = new HTMLPurifier_ElementDef();
11606
 
        $this->info['noscript']->attr = array( 0 => array('Common') );
11607
 
        $this->info['noscript']->content_model = 'Heading | List | Block';
11608
 
        $this->info['noscript']->content_model_type = 'required';
11609
 
        
11610
 
        $this->info['script'] = new HTMLPurifier_ElementDef();
11611
 
        $this->info['script']->attr = array(
11612
 
            'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
11613
 
            'src'   => new HTMLPurifier_AttrDef_URI(true),
11614
 
            'type'  => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
11615
 
        );
11616
 
        $this->info['script']->content_model = '#PCDATA';
11617
 
        $this->info['script']->content_model_type = 'optional';
11618
 
        $this->info['script']->attr_transform_pre['type'] =
11619
 
        $this->info['script']->attr_transform_post['type'] =
11620
 
            new HTMLPurifier_AttrTransform_ScriptRequired();
11621
 
    }
11622
 
}
11623
 
 
11624
 
 
11625
 
 
11626
 
 
11627
 
/**
11628
 
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
11629
 
 * Module.
11630
 
 */
11631
 
class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule
11632
 
{
11633
 
    
11634
 
    public $name = 'StyleAttribute';
11635
 
    public $attr_collections = array(
11636
 
        // The inclusion routine differs from the Abstract Modules but
11637
 
        // is in line with the DTD and XML Schemas.
11638
 
        'Style' => array('style' => false), // see constructor
11639
 
        'Core' => array(0 => array('Style'))
11640
 
    );
11641
 
    
11642
 
    public function setup($config) {
11643
 
        $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
11644
 
    }
11645
 
    
11646
 
}
11647
 
 
11648
 
 
11649
 
 
11650
 
 
11651
 
/**
11652
 
 * XHTML 1.1 Tables Module, fully defines accessible table elements.
11653
 
 */
11654
 
class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule
11655
 
{
11656
 
    
11657
 
    public $name = 'Tables';
11658
 
    
11659
 
    public function setup($config) {
11660
 
        
11661
 
        $this->addElement('caption', false, 'Inline', 'Common');
11662
 
        
11663
 
        $this->addElement('table', 'Block', 
11664
 
            new HTMLPurifier_ChildDef_Table(),  'Common', 
11665
 
            array(
11666
 
                'border' => 'Pixels',
11667
 
                'cellpadding' => 'Length',
11668
 
                'cellspacing' => 'Length',
11669
 
                'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
11670
 
                'rules' => 'Enum#none,groups,rows,cols,all',
11671
 
                'summary' => 'Text',
11672
 
                'width' => 'Length'
11673
 
            )
11674
 
        );
11675
 
        
11676
 
        // common attributes
11677
 
        $cell_align = array(
11678
 
            'align' => 'Enum#left,center,right,justify,char',
11679
 
            'charoff' => 'Length',
11680
 
            'valign' => 'Enum#top,middle,bottom,baseline',
11681
 
        );
11682
 
        
11683
 
        $cell_t = array_merge(
11684
 
            array(
11685
 
                'abbr'    => 'Text',
11686
 
                'colspan' => 'Number',
11687
 
                'rowspan' => 'Number',
11688
 
            ),
11689
 
            $cell_align
11690
 
        );
11691
 
        $this->addElement('td', false, 'Flow', 'Common', $cell_t);
11692
 
        $this->addElement('th', false, 'Flow', 'Common', $cell_t);
11693
 
        
11694
 
        $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align);
11695
 
        
11696
 
        $cell_col = array_merge(
11697
 
            array(
11698
 
                'span'  => 'Number',
11699
 
                'width' => 'MultiLength',
11700
 
            ),
11701
 
            $cell_align
11702
 
        );
11703
 
        $this->addElement('col',      false, 'Empty',         'Common', $cell_col);
11704
 
        $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col);
11705
 
        
11706
 
        $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align);
11707
 
        $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align);
11708
 
        $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align);
11709
 
        
11710
 
    }
11711
 
    
11712
 
}
11713
 
 
11714
 
 
11715
 
 
11716
 
 
11717
 
/**
11718
 
 * XHTML 1.1 Target Module, defines target attribute in link elements.
11719
 
 */
11720
 
class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule
11721
 
{
11722
 
    
11723
 
    public $name = 'Target';
11724
 
    
11725
 
    public function setup($config) {
11726
 
        $elements = array('a');
11727
 
        foreach ($elements as $name) {
11728
 
            $e = $this->addBlankElement($name);
11729
 
            $e->attr = array(
11730
 
                'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
11731
 
            );
11732
 
        }
11733
 
    }
11734
 
    
11735
 
}
11736
 
 
11737
 
 
11738
 
 
11739
 
 
11740
 
/**
11741
 
 * XHTML 1.1 Text Module, defines basic text containers. Core Module.
11742
 
 * @note In the normative XML Schema specification, this module
11743
 
 *       is further abstracted into the following modules:
11744
 
 *          - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6)
11745
 
 *          - Block Structural (div, p)
11746
 
 *          - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var)
11747
 
 *          - Inline Structural (br, span)
11748
 
 *       This module, functionally, does not distinguish between these
11749
 
 *       sub-modules, but the code is internally structured to reflect
11750
 
 *       these distinctions.
11751
 
 */
11752
 
class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule
11753
 
{
11754
 
    
11755
 
    public $name = 'Text';
11756
 
    public $content_sets = array(
11757
 
        'Flow' => 'Heading | Block | Inline'
11758
 
    );
11759
 
    
11760
 
    public function setup($config) {
11761
 
        
11762
 
        // Inline Phrasal -------------------------------------------------
11763
 
        $this->addElement('abbr',    'Inline', 'Inline', 'Common');
11764
 
        $this->addElement('acronym', 'Inline', 'Inline', 'Common');
11765
 
        $this->addElement('cite',    'Inline', 'Inline', 'Common');
11766
 
        $this->addElement('code',    'Inline', 'Inline', 'Common');
11767
 
        $this->addElement('dfn',     'Inline', 'Inline', 'Common');
11768
 
        $this->addElement('em',      'Inline', 'Inline', 'Common');
11769
 
        $this->addElement('kbd',     'Inline', 'Inline', 'Common');
11770
 
        $this->addElement('q',       'Inline', 'Inline', 'Common', array('cite' => 'URI'));
11771
 
        $this->addElement('samp',    'Inline', 'Inline', 'Common');
11772
 
        $this->addElement('strong',  'Inline', 'Inline', 'Common');
11773
 
        $this->addElement('var',     'Inline', 'Inline', 'Common');
11774
 
        
11775
 
        // Inline Structural ----------------------------------------------
11776
 
        $this->addElement('span', 'Inline', 'Inline', 'Common');
11777
 
        $this->addElement('br',   'Inline', 'Empty',  'Core');
11778
 
        
11779
 
        // Block Phrasal --------------------------------------------------
11780
 
        $this->addElement('address',     'Block', 'Inline', 'Common');
11781
 
        $this->addElement('blockquote',  'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') );
11782
 
        $pre = $this->addElement('pre', 'Block', 'Inline', 'Common');
11783
 
        $pre->excludes = $this->makeLookup(
11784
 
            'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' );
11785
 
        $this->addElement('h1', 'Heading', 'Inline', 'Common');
11786
 
        $this->addElement('h2', 'Heading', 'Inline', 'Common');
11787
 
        $this->addElement('h3', 'Heading', 'Inline', 'Common');
11788
 
        $this->addElement('h4', 'Heading', 'Inline', 'Common');
11789
 
        $this->addElement('h5', 'Heading', 'Inline', 'Common');
11790
 
        $this->addElement('h6', 'Heading', 'Inline', 'Common');
11791
 
        
11792
 
        // Block Structural -----------------------------------------------
11793
 
        $this->addElement('p', 'Block', 'Inline', 'Common');
11794
 
        $this->addElement('div', 'Block', 'Flow', 'Common');
11795
 
        
11796
 
    }
11797
 
    
11798
 
}
11799
 
 
11800
 
 
11801
 
 
11802
 
 
11803
 
/**
11804
 
 * Abstract class for a set of proprietary modules that clean up (tidy)
11805
 
 * poorly written HTML.
11806
 
 * @todo Figure out how to protect some of these methods/properties
11807
 
 */
11808
 
class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
11809
 
{
11810
 
    
11811
 
    /**
11812
 
     * List of supported levels. Index zero is a special case "no fixes"
11813
 
     * level.
11814
 
     */
11815
 
    public $levels = array(0 => 'none', 'light', 'medium', 'heavy');
11816
 
    
11817
 
    /**
11818
 
     * Default level to place all fixes in. Disabled by default
11819
 
     */
11820
 
    public $defaultLevel = null;
11821
 
    
11822
 
    /**
11823
 
     * Lists of fixes used by getFixesForLevel(). Format is:
11824
 
     *      HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
11825
 
     */
11826
 
    public $fixesForLevel = array(
11827
 
        'light'  => array(),
11828
 
        'medium' => array(),
11829
 
        'heavy'  => array()
11830
 
    );
11831
 
    
11832
 
    /**
11833
 
     * Lazy load constructs the module by determining the necessary
11834
 
     * fixes to create and then delegating to the populate() function.
11835
 
     * @todo Wildcard matching and error reporting when an added or
11836
 
     *       subtracted fix has no effect.
11837
 
     */
11838
 
    public function setup($config) {
11839
 
        
11840
 
        // create fixes, initialize fixesForLevel
11841
 
        $fixes = $this->makeFixes();
11842
 
        $this->makeFixesForLevel($fixes);
11843
 
        
11844
 
        // figure out which fixes to use
11845
 
        $level = $config->get('HTML', 'TidyLevel');
11846
 
        $fixes_lookup = $this->getFixesForLevel($level);
11847
 
        
11848
 
        // get custom fix declarations: these need namespace processing
11849
 
        $add_fixes    = $config->get('HTML', 'TidyAdd');
11850
 
        $remove_fixes = $config->get('HTML', 'TidyRemove');
11851
 
        
11852
 
        foreach ($fixes as $name => $fix) {
11853
 
            // needs to be refactored a little to implement globbing
11854
 
            if (
11855
 
                isset($remove_fixes[$name]) ||
11856
 
                (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))
11857
 
            ) {
11858
 
                unset($fixes[$name]);
11859
 
            }
11860
 
        }
11861
 
        
11862
 
        // populate this module with necessary fixes
11863
 
        $this->populate($fixes);
11864
 
        
11865
 
    }
11866
 
    
11867
 
    /**
11868
 
     * Retrieves all fixes per a level, returning fixes for that specific
11869
 
     * level as well as all levels below it.
11870
 
     * @param $level String level identifier, see $levels for valid values
11871
 
     * @return Lookup up table of fixes
11872
 
     */
11873
 
    public function getFixesForLevel($level) {
11874
 
        if ($level == $this->levels[0]) {
11875
 
            return array();
11876
 
        }
11877
 
        $activated_levels = array();
11878
 
        for ($i = 1, $c = count($this->levels); $i < $c; $i++) {
11879
 
            $activated_levels[] = $this->levels[$i];
11880
 
            if ($this->levels[$i] == $level) break;
11881
 
        }
11882
 
        if ($i == $c) {
11883
 
            trigger_error(
11884
 
                'Tidy level ' . htmlspecialchars($level) . ' not recognized',
11885
 
                E_USER_WARNING
11886
 
            );
11887
 
            return array();
11888
 
        }
11889
 
        $ret = array();
11890
 
        foreach ($activated_levels as $level) {
11891
 
            foreach ($this->fixesForLevel[$level] as $fix) {
11892
 
                $ret[$fix] = true;
11893
 
            }
11894
 
        }
11895
 
        return $ret;
11896
 
    }
11897
 
    
11898
 
    /**
11899
 
     * Dynamically populates the $fixesForLevel member variable using
11900
 
     * the fixes array. It may be custom overloaded, used in conjunction
11901
 
     * with $defaultLevel, or not used at all.
11902
 
     */
11903
 
    public function makeFixesForLevel($fixes) {
11904
 
        if (!isset($this->defaultLevel)) return;
11905
 
        if (!isset($this->fixesForLevel[$this->defaultLevel])) {
11906
 
            trigger_error(
11907
 
                'Default level ' . $this->defaultLevel . ' does not exist',
11908
 
                E_USER_ERROR
11909
 
            );
11910
 
            return;
11911
 
        }
11912
 
        $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
11913
 
    }
11914
 
    
11915
 
    /**
11916
 
     * Populates the module with transforms and other special-case code
11917
 
     * based on a list of fixes passed to it
11918
 
     * @param $lookup Lookup table of fixes to activate
11919
 
     */
11920
 
    public function populate($fixes) {
11921
 
        foreach ($fixes as $name => $fix) {
11922
 
            // determine what the fix is for
11923
 
            list($type, $params) = $this->getFixType($name);
11924
 
            switch ($type) {
11925
 
                case 'attr_transform_pre':
11926
 
                case 'attr_transform_post':
11927
 
                    $attr = $params['attr'];
11928
 
                    if (isset($params['element'])) {
11929
 
                        $element = $params['element'];
11930
 
                        if (empty($this->info[$element])) {
11931
 
                            $e = $this->addBlankElement($element);
11932
 
                        } else {
11933
 
                            $e = $this->info[$element];
11934
 
                        }
11935
 
                    } else {
11936
 
                        $type = "info_$type";
11937
 
                        $e = $this;
11938
 
                    }
11939
 
                    // PHP does some weird parsing when I do
11940
 
                    // $e->$type[$attr], so I have to assign a ref.
11941
 
                    $f =& $e->$type;
11942
 
                    $f[$attr] = $fix;
11943
 
                    break;
11944
 
                case 'tag_transform':
11945
 
                    $this->info_tag_transform[$params['element']] = $fix;
11946
 
                    break;
11947
 
                case 'child':
11948
 
                case 'content_model_type':
11949
 
                    $element = $params['element'];
11950
 
                    if (empty($this->info[$element])) {
11951
 
                        $e = $this->addBlankElement($element);
11952
 
                    } else {
11953
 
                        $e = $this->info[$element];
11954
 
                    }
11955
 
                    $e->$type = $fix;
11956
 
                    break;
11957
 
                default:
11958
 
                    trigger_error("Fix type $type not supported", E_USER_ERROR);
11959
 
                    break;
11960
 
            }
11961
 
        }
11962
 
    }
11963
 
    
11964
 
    /**
11965
 
     * Parses a fix name and determines what kind of fix it is, as well
11966
 
     * as other information defined by the fix
11967
 
     * @param $name String name of fix
11968
 
     * @return array(string $fix_type, array $fix_parameters)
11969
 
     * @note $fix_parameters is type dependant, see populate() for usage
11970
 
     *       of these parameters
11971
 
     */
11972
 
    public function getFixType($name) {
11973
 
        // parse it
11974
 
        $property = $attr = null;
11975
 
        if (strpos($name, '#') !== false) list($name, $property) = explode('#', $name);
11976
 
        if (strpos($name, '@') !== false) list($name, $attr)     = explode('@', $name);
11977
 
        
11978
 
        // figure out the parameters
11979
 
        $params = array();
11980
 
        if ($name !== '')    $params['element'] = $name;
11981
 
        if (!is_null($attr)) $params['attr'] = $attr;
11982
 
        
11983
 
        // special case: attribute transform
11984
 
        if (!is_null($attr)) {
11985
 
            if (is_null($property)) $property = 'pre';
11986
 
            $type = 'attr_transform_' . $property;
11987
 
            return array($type, $params);
11988
 
        }
11989
 
        
11990
 
        // special case: tag transform
11991
 
        if (is_null($property)) {
11992
 
            return array('tag_transform', $params);
11993
 
        }
11994
 
        
11995
 
        return array($property, $params);
11996
 
        
11997
 
    }
11998
 
    
11999
 
    /**
12000
 
     * Defines all fixes the module will perform in a compact
12001
 
     * associative array of fix name to fix implementation.
12002
 
     */
12003
 
    public function makeFixes() {}
12004
 
    
12005
 
}
12006
 
 
12007
 
 
12008
 
 
12009
 
 
12010
 
 
12011
 
class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule
12012
 
{
12013
 
    public $name = 'XMLCommonAttributes';
12014
 
    
12015
 
    public $attr_collections = array(
12016
 
        'Lang' => array(
12017
 
            'xml:lang' => 'LanguageCode',
12018
 
        )
12019
 
    );
12020
 
}
12021
 
 
12022
 
 
12023
 
 
12024
 
 
12025
 
/**
12026
 
 * Name is deprecated, but allowed in strict doctypes, so onl
12027
 
 */
12028
 
class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy
12029
 
{
12030
 
    public $name = 'Tidy_Name';
12031
 
    public $defaultLevel = 'heavy';
12032
 
    public function makeFixes() {
12033
 
        
12034
 
        $r = array();
12035
 
        
12036
 
        // @name for img, a -----------------------------------------------
12037
 
        // Technically, it's allowed even on strict, so we allow authors to use
12038
 
        // it. However, it's deprecated in future versions of XHTML.
12039
 
        $r['img@name'] = 
12040
 
        $r['a@name'] = new HTMLPurifier_AttrTransform_Name();
12041
 
        
12042
 
        return $r;
12043
 
    }
12044
 
}
12045
 
 
12046
 
 
12047
 
 
12048
 
 
12049
 
class HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy
12050
 
{
12051
 
    
12052
 
    public $name = 'Tidy_Proprietary';
12053
 
    public $defaultLevel = 'light';
12054
 
    
12055
 
    public function makeFixes() {
12056
 
        $r = array();
12057
 
        $r['table@background'] = new HTMLPurifier_AttrTransform_Background();
12058
 
        $r['td@background']    = new HTMLPurifier_AttrTransform_Background();
12059
 
        $r['th@background']    = new HTMLPurifier_AttrTransform_Background();
12060
 
        $r['tr@background']    = new HTMLPurifier_AttrTransform_Background();
12061
 
        $r['thead@background'] = new HTMLPurifier_AttrTransform_Background();
12062
 
        $r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background();
12063
 
        $r['tbody@background'] = new HTMLPurifier_AttrTransform_Background();
12064
 
        return $r;
12065
 
    }
12066
 
    
12067
 
}
12068
 
 
12069
 
 
12070
 
 
12071
 
 
12072
 
class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy
12073
 
{
12074
 
    
12075
 
    public function makeFixes() {
12076
 
        
12077
 
        $r = array();
12078
 
        
12079
 
        // == deprecated tag transforms ===================================
12080
 
        
12081
 
        $r['font']   = new HTMLPurifier_TagTransform_Font();
12082
 
        $r['menu']   = new HTMLPurifier_TagTransform_Simple('ul');
12083
 
        $r['dir']    = new HTMLPurifier_TagTransform_Simple('ul');
12084
 
        $r['center'] = new HTMLPurifier_TagTransform_Simple('div',  'text-align:center;');
12085
 
        $r['u']      = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:underline;');
12086
 
        $r['s']      = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
12087
 
        $r['strike'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
12088
 
        
12089
 
        // == deprecated attribute transforms =============================
12090
 
        
12091
 
        $r['caption@align'] = 
12092
 
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
12093
 
                // we're following IE's behavior, not Firefox's, due
12094
 
                // to the fact that no one supports caption-side:right,
12095
 
                // W3C included (with CSS 2.1). This is a slightly
12096
 
                // unreasonable attribute!
12097
 
                'left'   => 'text-align:left;',
12098
 
                'right'  => 'text-align:right;',
12099
 
                'top'    => 'caption-side:top;',
12100
 
                'bottom' => 'caption-side:bottom;' // not supported by IE
12101
 
            ));
12102
 
        
12103
 
        // @align for img -------------------------------------------------
12104
 
        $r['img@align'] =
12105
 
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
12106
 
                'left'   => 'float:left;',
12107
 
                'right'  => 'float:right;',
12108
 
                'top'    => 'vertical-align:top;',
12109
 
                'middle' => 'vertical-align:middle;',
12110
 
                'bottom' => 'vertical-align:baseline;',
12111
 
            ));
12112
 
        
12113
 
        // @align for table -----------------------------------------------
12114
 
        $r['table@align'] =
12115
 
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
12116
 
                'left'   => 'float:left;',
12117
 
                'center' => 'margin-left:auto;margin-right:auto;',
12118
 
                'right'  => 'float:right;'
12119
 
            ));
12120
 
        
12121
 
        // @align for hr -----------------------------------------------
12122
 
        $r['hr@align'] =
12123
 
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
12124
 
                // we use both text-align and margin because these work
12125
 
                // for different browsers (IE and Firefox, respectively)
12126
 
                // and the melange makes for a pretty cross-compatible
12127
 
                // solution
12128
 
                'left'   => 'margin-left:0;margin-right:auto;text-align:left;',
12129
 
                'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
12130
 
                'right'  => 'margin-left:auto;margin-right:0;text-align:right;'
12131
 
            ));
12132
 
        
12133
 
        // @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
12134
 
        // {{{
12135
 
            $align_lookup = array();
12136
 
            $align_values = array('left', 'right', 'center', 'justify');
12137
 
            foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;";
12138
 
        // }}}
12139
 
        $r['h1@align'] =
12140
 
        $r['h2@align'] =
12141
 
        $r['h3@align'] =
12142
 
        $r['h4@align'] =
12143
 
        $r['h5@align'] =
12144
 
        $r['h6@align'] =
12145
 
        $r['p@align']  =
12146
 
        $r['div@align'] = 
12147
 
            new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup);
12148
 
        
12149
 
        // @bgcolor for table, tr, td, th ---------------------------------
12150
 
        $r['table@bgcolor'] =
12151
 
        $r['td@bgcolor'] =
12152
 
        $r['th@bgcolor'] =
12153
 
            new HTMLPurifier_AttrTransform_BgColor();
12154
 
        
12155
 
        // @border for img ------------------------------------------------
12156
 
        $r['img@border'] = new HTMLPurifier_AttrTransform_Border();
12157
 
        
12158
 
        // @clear for br --------------------------------------------------
12159
 
        $r['br@clear'] =
12160
 
            new HTMLPurifier_AttrTransform_EnumToCSS('clear', array(
12161
 
                'left'  => 'clear:left;',
12162
 
                'right' => 'clear:right;',
12163
 
                'all'   => 'clear:both;',
12164
 
                'none'  => 'clear:none;',
12165
 
            ));
12166
 
        
12167
 
        // @height for td, th ---------------------------------------------
12168
 
        $r['td@height'] = 
12169
 
        $r['th@height'] =
12170
 
            new HTMLPurifier_AttrTransform_Length('height');
12171
 
        
12172
 
        // @hspace for img ------------------------------------------------
12173
 
        $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');
12174
 
        
12175
 
        // @noshade for hr ------------------------------------------------
12176
 
        // this transformation is not precise but often good enough.
12177
 
        // different browsers use different styles to designate noshade
12178
 
        $r['hr@noshade'] =
12179
 
            new HTMLPurifier_AttrTransform_BoolToCSS(
12180
 
                'noshade',
12181
 
                'color:#808080;background-color:#808080;border:0;'
12182
 
            );
12183
 
        
12184
 
        // @nowrap for td, th ---------------------------------------------
12185
 
        $r['td@nowrap'] = 
12186
 
        $r['th@nowrap'] =
12187
 
            new HTMLPurifier_AttrTransform_BoolToCSS(
12188
 
                'nowrap',
12189
 
                'white-space:nowrap;'
12190
 
            );
12191
 
        
12192
 
        // @size for hr  --------------------------------------------------
12193
 
        $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height');
12194
 
        
12195
 
        // @type for li, ol, ul -------------------------------------------
12196
 
        // {{{
12197
 
            $ul_types = array(
12198
 
                'disc'   => 'list-style-type:disc;',
12199
 
                'square' => 'list-style-type:square;',
12200
 
                'circle' => 'list-style-type:circle;'
12201
 
            );
12202
 
            $ol_types = array(
12203
 
                '1'   => 'list-style-type:decimal;',
12204
 
                'i'   => 'list-style-type:lower-roman;',
12205
 
                'I'   => 'list-style-type:upper-roman;',
12206
 
                'a'   => 'list-style-type:lower-alpha;',
12207
 
                'A'   => 'list-style-type:upper-alpha;'
12208
 
            );
12209
 
            $li_types = $ul_types + $ol_types;
12210
 
        // }}}
12211
 
        
12212
 
        $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types);
12213
 
        $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true);
12214
 
        $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true);
12215
 
        
12216
 
        // @vspace for img ------------------------------------------------
12217
 
        $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
12218
 
        
12219
 
        // @width for hr, td, th ------------------------------------------
12220
 
        $r['td@width'] =
12221
 
        $r['th@width'] = 
12222
 
        $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
12223
 
        
12224
 
        return $r;
12225
 
        
12226
 
    }
12227
 
    
12228
 
}
12229
 
 
12230
 
 
12231
 
 
12232
 
 
12233
 
class HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
12234
 
{
12235
 
    public $name = 'Tidy_Strict';
12236
 
    public $defaultLevel = 'light';
12237
 
    
12238
 
    public function makeFixes() {
12239
 
        $r = parent::makeFixes();
12240
 
        $r['blockquote#content_model_type'] = 'strictblockquote';
12241
 
        return $r;
12242
 
    }
12243
 
    
12244
 
    public $defines_child_def = true;
12245
 
    public function getChildDef($def) {
12246
 
        if ($def->content_model_type != 'strictblockquote') return parent::getChildDef($def);
12247
 
        return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
12248
 
    }
12249
 
}
12250
 
 
12251
 
 
12252
 
 
12253
 
class HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
12254
 
{
12255
 
    public $name = 'Tidy_Transitional';
12256
 
    public $defaultLevel = 'heavy';
12257
 
}
12258
 
 
12259
 
 
12260
 
 
12261
 
 
12262
 
class HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy
12263
 
{
12264
 
    
12265
 
    public $name = 'Tidy_XHTML';
12266
 
    public $defaultLevel = 'medium';
12267
 
    
12268
 
    public function makeFixes() {
12269
 
        $r = array();
12270
 
        $r['@lang'] = new HTMLPurifier_AttrTransform_Lang();
12271
 
        return $r;
12272
 
    }
12273
 
    
12274
 
}
12275
 
 
12276
 
 
12277
 
 
12278
 
 
12279
 
/**
12280
 
 * Injector that auto paragraphs text in the root node based on
12281
 
 * double-spacing.
12282
 
 * @todo Ensure all states are unit tested, including variations as well.
12283
 
 * @todo Make a graph of the flow control for this Injector.
12284
 
 */
12285
 
class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector
12286
 
{
12287
 
    
12288
 
    public $name = 'AutoParagraph';
12289
 
    public $needed = array('p');
12290
 
    
12291
 
    private function _pStart() {
12292
 
        $par = new HTMLPurifier_Token_Start('p');
12293
 
        $par->armor['MakeWellFormed_TagClosedError'] = true;
12294
 
        return $par;
12295
 
    }
12296
 
    
12297
 
    public function handleText(&$token) {
12298
 
        $text = $token->data;
12299
 
        // Does the current parent allow <p> tags?
12300
 
        if ($this->allowsElement('p')) {
12301
 
            if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) {
12302
 
                // Note that we have differing behavior when dealing with text
12303
 
                // in the anonymous root node, or a node inside the document.
12304
 
                // If the text as a double-newline, the treatment is the same;
12305
 
                // if it doesn't, see the next if-block if you're in the document.
12306
 
                
12307
 
                $i = $nesting = null;
12308
 
                if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) {
12309
 
                    // State 1.1: ...    ^ (whitespace, then document end)
12310
 
                    //               ----
12311
 
                    // This is a degenerate case
12312
 
                } else {
12313
 
                    // State 1.2: PAR1
12314
 
                    //            ----
12315
 
                    
12316
 
                    // State 1.3: PAR1\n\nPAR2
12317
 
                    //            ------------
12318
 
                    
12319
 
                    // State 1.4: <div>PAR1\n\nPAR2 (see State 2)
12320
 
                    //                 ------------
12321
 
                    $token = array($this->_pStart());
12322
 
                    $this->_splitText($text, $token);
12323
 
                }
12324
 
            } else {
12325
 
                // State 2:   <div>PAR1... (similar to 1.4)
12326
 
                //                 ----
12327
 
                
12328
 
                // We're in an element that allows paragraph tags, but we're not
12329
 
                // sure if we're going to need them.
12330
 
                if ($this->_pLookAhead()) {
12331
 
                    // State 2.1: <div>PAR1<b>PAR1\n\nPAR2
12332
 
                    //                 ----
12333
 
                    // Note: This will always be the first child, since any
12334
 
                    // previous inline element would have triggered this very
12335
 
                    // same routine, and found the double newline. One possible
12336
 
                    // exception would be a comment.
12337
 
                    $token = array($this->_pStart(), $token);
12338
 
                } else {
12339
 
                    // State 2.2.1: <div>PAR1<div>
12340
 
                    //                   ----
12341
 
                    
12342
 
                    // State 2.2.2: <div>PAR1<b>PAR1</b></div>
12343
 
                    //                   ----
12344
 
                }
12345
 
            }
12346
 
        // Is the current parent a <p> tag?
12347
 
        } elseif (
12348
 
            !empty($this->currentNesting) &&
12349
 
            $this->currentNesting[count($this->currentNesting)-1]->name == 'p'
12350
 
        ) {
12351
 
            // State 3.1: ...<p>PAR1
12352
 
            //                  ----
12353
 
            
12354
 
            // State 3.2: ...<p>PAR1\n\nPAR2
12355
 
            //                  ------------
12356
 
            $token = array();
12357
 
            $this->_splitText($text, $token);
12358
 
        // Abort!
12359
 
        } else {
12360
 
            // State 4.1: ...<b>PAR1
12361
 
            //                  ----
12362
 
            
12363
 
            // State 4.2: ...<b>PAR1\n\nPAR2
12364
 
            //                  ------------
12365
 
        }
12366
 
    }
12367
 
    
12368
 
    public function handleElement(&$token) {
12369
 
        // We don't have to check if we're already in a <p> tag for block
12370
 
        // tokens, because the tag would have been autoclosed by MakeWellFormed.
12371
 
        if ($this->allowsElement('p')) {
12372
 
            if (!empty($this->currentNesting)) {
12373
 
                if ($this->_isInline($token)) {
12374
 
                    // State 1: <div>...<b>
12375
 
                    //                  ---
12376
 
                    
12377
 
                    // Check if this token is adjacent to the parent token
12378
 
                    // (seek backwards until token isn't whitespace)
12379
 
                    $i = null;
12380
 
                    $this->backward($i, $prev);
12381
 
                    
12382
 
                    if (!$prev instanceof HTMLPurifier_Token_Start) {
12383
 
                        // Token wasn't adjacent
12384
 
                        
12385
 
                        if (
12386
 
                            $prev instanceof HTMLPurifier_Token_Text &&
12387
 
                            substr($prev->data, -2) === "\n\n"
12388
 
                        ) {
12389
 
                            // State 1.1.4: <div><p>PAR1</p>\n\n<b>
12390
 
                            //                                  ---
12391
 
                            
12392
 
                            // Quite frankly, this should be handled by splitText
12393
 
                            $token = array($this->_pStart(), $token);
12394
 
                        } else {
12395
 
                            // State 1.1.1: <div><p>PAR1</p><b>
12396
 
                            //                              ---
12397
 
                            
12398
 
                            // State 1.1.2: <div><br /><b>
12399
 
                            //                         ---
12400
 
                            
12401
 
                            // State 1.1.3: <div>PAR<b>
12402
 
                            //                      ---
12403
 
                        }
12404
 
                        
12405
 
                    } else {
12406
 
                        // State 1.2.1: <div><b>
12407
 
                        //                   ---
12408
 
                        
12409
 
                        // Lookahead to see if <p> is needed.
12410
 
                        if ($this->_pLookAhead()) {
12411
 
                            // State 1.3.1: <div><b>PAR1\n\nPAR2
12412
 
                            //                   ---
12413
 
                            $token = array($this->_pStart(), $token);
12414
 
                        } else {
12415
 
                            // State 1.3.2: <div><b>PAR1</b></div>
12416
 
                            //                   ---
12417
 
                            
12418
 
                            // State 1.3.3: <div><b>PAR1</b><div></div>\n\n</div>
12419
 
                            //                   ---
12420
 
                        }
12421
 
                    }
12422
 
                } else {
12423
 
                    // State 2.3: ...<div>
12424
 
                    //               -----
12425
 
                }
12426
 
            } else {
12427
 
                if ($this->_isInline($token)) {
12428
 
                    // State 3.1: <b>
12429
 
                    //            ---
12430
 
                    // This is where the {p} tag is inserted, not reflected in
12431
 
                    // inputTokens yet, however.
12432
 
                    $token = array($this->_pStart(), $token);
12433
 
                } else {
12434
 
                    // State 3.2: <div>
12435
 
                    //            -----
12436
 
                }
12437
 
                
12438
 
                $i = null;
12439
 
                if ($this->backward($i, $prev)) {
12440
 
                    if (
12441
 
                        !$prev instanceof HTMLPurifier_Token_Text
12442
 
                    ) {
12443
 
                        // State 3.1.1: ...</p>{p}<b>
12444
 
                        //                        ---
12445
 
                        
12446
 
                        // State 3.2.1: ...</p><div>
12447
 
                        //                     -----
12448
 
                        
12449
 
                        if (!is_array($token)) $token = array($token);
12450
 
                        array_unshift($token, new HTMLPurifier_Token_Text("\n\n"));
12451
 
                    } else {
12452
 
                        // State 3.1.2: ...</p>\n\n{p}<b>
12453
 
                        //                            ---
12454
 
                        
12455
 
                        // State 3.2.2: ...</p>\n\n<div>
12456
 
                        //                         -----
12457
 
                        
12458
 
                        // Note: PAR<ELEM> cannot occur because PAR would have been
12459
 
                        // wrapped in <p> tags.
12460
 
                    }
12461
 
                }
12462
 
            }
12463
 
        } else {
12464
 
            // State 2.2: <ul><li>
12465
 
            //                ----
12466
 
            
12467
 
            // State 2.4: <p><b>
12468
 
            //               ---
12469
 
        }
12470
 
    }
12471
 
    
12472
 
    /**
12473
 
     * Splits up a text in paragraph tokens and appends them
12474
 
     * to the result stream that will replace the original
12475
 
     * @param $data String text data that will be processed
12476
 
     *    into paragraphs
12477
 
     * @param $result Reference to array of tokens that the
12478
 
     *    tags will be appended onto
12479
 
     * @param $config Instance of HTMLPurifier_Config
12480
 
     * @param $context Instance of HTMLPurifier_Context
12481
 
     */
12482
 
    private function _splitText($data, &$result) {
12483
 
        $raw_paragraphs = explode("\n\n", $data);
12484
 
        $paragraphs  = array(); // without empty paragraphs
12485
 
        $needs_start = false;
12486
 
        $needs_end   = false;
12487
 
        
12488
 
        $c = count($raw_paragraphs);
12489
 
        if ($c == 1) {
12490
 
            // There were no double-newlines, abort quickly. In theory this
12491
 
            // should never happen.
12492
 
            $result[] = new HTMLPurifier_Token_Text($data);
12493
 
            return;
12494
 
        }
12495
 
        for ($i = 0; $i < $c; $i++) {
12496
 
            $par = $raw_paragraphs[$i];
12497
 
            if (trim($par) !== '') {
12498
 
                $paragraphs[] = $par;
12499
 
            } else {
12500
 
                if ($i == 0) {
12501
 
                    // Double newline at the front
12502
 
                    if (empty($result)) {
12503
 
                        // The empty result indicates that the AutoParagraph
12504
 
                        // injector did not add any start paragraph tokens.
12505
 
                        // This means that we have been in a paragraph for
12506
 
                        // a while, and the newline means we should start a new one.
12507
 
                        $result[] = new HTMLPurifier_Token_End('p');
12508
 
                        $result[] = new HTMLPurifier_Token_Text("\n\n");
12509
 
                        // However, the start token should only be added if 
12510
 
                        // there is more processing to be done (i.e. there are
12511
 
                        // real paragraphs in here). If there are none, the
12512
 
                        // next start paragraph tag will be handled by the
12513
 
                        // next call to the injector
12514
 
                        $needs_start = true;
12515
 
                    } else {
12516
 
                        // We just started a new paragraph!
12517
 
                        // Reinstate a double-newline for presentation's sake, since
12518
 
                        // it was in the source code.
12519
 
                        array_unshift($result, new HTMLPurifier_Token_Text("\n\n"));
12520
 
                    }
12521
 
                } elseif ($i + 1 == $c) {
12522
 
                    // Double newline at the end
12523
 
                    // There should be a trailing </p> when we're finally done.
12524
 
                    $needs_end = true;
12525
 
                }
12526
 
            }
12527
 
        }
12528
 
        
12529
 
        // Check if this was just a giant blob of whitespace. Move this earlier,
12530
 
        // perhaps?
12531
 
        if (empty($paragraphs)) {
12532
 
            return;
12533
 
        }
12534
 
        
12535
 
        // Add the start tag indicated by \n\n at the beginning of $data
12536
 
        if ($needs_start) {
12537
 
            $result[] = $this->_pStart();
12538
 
        }
12539
 
        
12540
 
        // Append the paragraphs onto the result
12541
 
        foreach ($paragraphs as $par) {
12542
 
            $result[] = new HTMLPurifier_Token_Text($par);
12543
 
            $result[] = new HTMLPurifier_Token_End('p');
12544
 
            $result[] = new HTMLPurifier_Token_Text("\n\n");
12545
 
            $result[] = $this->_pStart();
12546
 
        }
12547
 
        
12548
 
        // Remove trailing start token; Injector will handle this later if
12549
 
        // it was indeed needed. This prevents from needing to do a lookahead,
12550
 
        // at the cost of a lookbehind later.
12551
 
        array_pop($result);
12552
 
        
12553
 
        // If there is no need for an end tag, remove all of it and let 
12554
 
        // MakeWellFormed close it later.
12555
 
        if (!$needs_end) {
12556
 
            array_pop($result); // removes \n\n
12557
 
            array_pop($result); // removes </p>
12558
 
        }
12559
 
        
12560
 
    }
12561
 
    
12562
 
    /**
12563
 
     * Returns true if passed token is inline (and, ergo, allowed in
12564
 
     * paragraph tags)
12565
 
     */
12566
 
    private function _isInline($token) {
12567
 
        return isset($this->htmlDefinition->info['p']->child->elements[$token->name]);
12568
 
    }
12569
 
    
12570
 
    /**
12571
 
     * Looks ahead in the token list and determines whether or not we need
12572
 
     * to insert a <p> tag.
12573
 
     */
12574
 
    private function _pLookAhead() {
12575
 
        $this->current($i, $current);
12576
 
        if ($current instanceof HTMLPurifier_Token_Start) $nesting = 1;
12577
 
        else $nesting = 0;
12578
 
        $ok = false;
12579
 
        while ($this->forwardUntilEndToken($i, $current, $nesting)) {
12580
 
            $result = $this->_checkNeedsP($current);
12581
 
            if ($result !== null) {
12582
 
                $ok = $result;
12583
 
                break;
12584
 
            }
12585
 
        }
12586
 
        return $ok;
12587
 
    }
12588
 
    
12589
 
    /**
12590
 
     * Determines if a particular token requires an earlier inline token
12591
 
     * to get a paragraph. This should be used with _forwardUntilEndToken
12592
 
     */
12593
 
    private function _checkNeedsP($current) {
12594
 
        if ($current instanceof HTMLPurifier_Token_Start){
12595
 
            if (!$this->_isInline($current)) {
12596
 
                // <div>PAR1<div>
12597
 
                //      ----
12598
 
                // Terminate early, since we hit a block element
12599
 
                return false;
12600
 
            }
12601
 
        } elseif ($current instanceof HTMLPurifier_Token_Text) {
12602
 
            if (strpos($current->data, "\n\n") !== false) {
12603
 
                // <div>PAR1<b>PAR1\n\nPAR2
12604
 
                //      ----
12605
 
                return true;
12606
 
            } else {
12607
 
                // <div>PAR1<b>PAR1...
12608
 
                //      ----
12609
 
            }
12610
 
        }
12611
 
        return null;
12612
 
    }
12613
 
    
12614
 
}
12615
 
 
12616
 
 
12617
 
 
12618
 
 
12619
 
/**
12620
 
 * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link.
12621
 
 */
12622
 
class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector
12623
 
{
12624
 
    
12625
 
    public $name = 'DisplayLinkURI';
12626
 
    public $needed = array('a');
12627
 
    
12628
 
    public function handleElement(&$token) {
12629
 
    }
12630
 
    
12631
 
    public function handleEnd(&$token) {
12632
 
        if (isset($token->start->attr['href'])){
12633
 
            $url = $token->start->attr['href'];
12634
 
            unset($token->start->attr['href']);
12635
 
            $token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
12636
 
        } else {
12637
 
            // nothing to display
12638
 
        }
12639
 
    }
12640
 
}
12641
 
 
12642
 
 
12643
 
/**
12644
 
 * Injector that converts http, https and ftp text URLs to actual links.
12645
 
 */
12646
 
class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
12647
 
{
12648
 
    
12649
 
    public $name = 'Linkify';
12650
 
    public $needed = array('a' => array('href'));
12651
 
    
12652
 
    public function handleText(&$token) {
12653
 
        if (!$this->allowsElement('a')) return;
12654
 
        
12655
 
        if (strpos($token->data, '://') === false) {
12656
 
            // our really quick heuristic failed, abort
12657
 
            // this may not work so well if we want to match things like
12658
 
            // "google.com", but then again, most people don't
12659
 
            return;
12660
 
        }
12661
 
        
12662
 
        // there is/are URL(s). Let's split the string:
12663
 
        // Note: this regex is extremely permissive
12664
 
        $bits = preg_split('#((?:https?|ftp)://[^\s\'"<>()]+)#S', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
12665
 
        
12666
 
        $token = array();
12667
 
        
12668
 
        // $i = index
12669
 
        // $c = count
12670
 
        // $l = is link
12671
 
        for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
12672
 
            if (!$l) {
12673
 
                if ($bits[$i] === '') continue;
12674
 
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
12675
 
            } else {
12676
 
                $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
12677
 
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
12678
 
                $token[] = new HTMLPurifier_Token_End('a');
12679
 
            }
12680
 
        }
12681
 
        
12682
 
    }
12683
 
    
12684
 
}
12685
 
 
12686
 
 
12687
 
 
12688
 
 
12689
 
/**
12690
 
 * Injector that converts configuration directive syntax %Namespace.Directive
12691
 
 * to links
12692
 
 */
12693
 
class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector
12694
 
{
12695
 
    
12696
 
    public $name = 'PurifierLinkify';
12697
 
    public $docURL;
12698
 
    public $needed = array('a' => array('href'));
12699
 
    
12700
 
    public function prepare($config, $context) {
12701
 
        $this->docURL = $config->get('AutoFormatParam', 'PurifierLinkifyDocURL');
12702
 
        return parent::prepare($config, $context);
12703
 
    }
12704
 
    
12705
 
    public function handleText(&$token) {
12706
 
        if (!$this->allowsElement('a')) return;
12707
 
        if (strpos($token->data, '%') === false) return;
12708
 
        
12709
 
        $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
12710
 
        $token = array();
12711
 
        
12712
 
        // $i = index
12713
 
        // $c = count
12714
 
        // $l = is link
12715
 
        for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
12716
 
            if (!$l) {
12717
 
                if ($bits[$i] === '') continue;
12718
 
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
12719
 
            } else {
12720
 
                $token[] = new HTMLPurifier_Token_Start('a',
12721
 
                    array('href' => str_replace('%s', $bits[$i], $this->docURL)));
12722
 
                $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
12723
 
                $token[] = new HTMLPurifier_Token_End('a');
12724
 
            }
12725
 
        }
12726
 
        
12727
 
    }
12728
 
    
12729
 
}
12730
 
 
12731
 
 
12732
 
 
12733
 
 
12734
 
class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector
12735
 
{
12736
 
    
12737
 
    private $context, $config;
12738
 
    
12739
 
    public function prepare($config, $context) {
12740
 
        parent::prepare($config, $context);
12741
 
        $this->config = $config;
12742
 
        $this->context = $context;
12743
 
        $this->attrValidator = new HTMLPurifier_AttrValidator();
12744
 
    }
12745
 
    
12746
 
    public function handleElement(&$token) {
12747
 
        if (!$token instanceof HTMLPurifier_Token_Start) return;
12748
 
        $next = false;
12749
 
        for ($i = $this->inputIndex + 1, $c = count($this->inputTokens); $i < $c; $i++) {
12750
 
            $next = $this->inputTokens[$i];
12751
 
            if ($next instanceof HTMLPurifier_Token_Text && $next->is_whitespace) continue;
12752
 
            break;
12753
 
        }
12754
 
        if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
12755
 
            if ($token->name == 'colgroup') return;
12756
 
            $this->attrValidator->validateToken($token, $this->config, $this->context);
12757
 
            $token->armor['ValidateAttributes'] = true;
12758
 
            if (isset($token->attr['id']) || isset($token->attr['name'])) return;
12759
 
            $token = $i - $this->inputIndex + 1;
12760
 
            for ($b = $this->inputIndex - 1; $b > 0; $b--) {
12761
 
                $prev = $this->inputTokens[$b];
12762
 
                if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) continue;
12763
 
                break;
12764
 
            }
12765
 
            // This is safe because we removed the token that triggered this.
12766
 
            $this->rewind($b - 1);
12767
 
            return;
12768
 
        }
12769
 
    }
12770
 
    
12771
 
}
12772
 
 
12773
 
 
12774
 
 
12775
 
/**
12776
 
 * Adds important param elements to inside of object in order to make
12777
 
 * things safe.
12778
 
 */
12779
 
class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector
12780
 
{
12781
 
    public $name = 'SafeObject';
12782
 
    public $needed = array('object', 'param');
12783
 
    
12784
 
    protected $objectStack = array();
12785
 
    protected $paramStack  = array();
12786
 
    
12787
 
    // Keep this synchronized with AttrTransform/SafeParam.php
12788
 
    protected $addParam = array(
12789
 
        'allowScriptAccess' => 'never',
12790
 
        'allowNetworking' => 'internal',
12791
 
    );
12792
 
    protected $allowedParam = array(
12793
 
        'wmode' => true,
12794
 
        'movie' => true,
12795
 
    );
12796
 
    
12797
 
    public function prepare($config, $context) {
12798
 
        parent::prepare($config, $context);
12799
 
    }
12800
 
    
12801
 
    public function handleElement(&$token) {
12802
 
        if ($token->name == 'object') {
12803
 
            $this->objectStack[] = $token;
12804
 
            $this->paramStack[] = array();
12805
 
            $new = array($token);
12806
 
            foreach ($this->addParam as $name => $value) {
12807
 
                $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
12808
 
            }
12809
 
            $token = $new;
12810
 
        } elseif ($token->name == 'param') {
12811
 
            $nest = count($this->currentNesting) - 1;
12812
 
            if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
12813
 
                $i = count($this->objectStack) - 1;
12814
 
                if (!isset($token->attr['name'])) {
12815
 
                    $token = false;
12816
 
                    return;
12817
 
                }
12818
 
                $n = $token->attr['name'];
12819
 
                // We need this fix because YouTube doesn't supply a data
12820
 
                // attribute, which we need if a type is specified. This is
12821
 
                // *very* Flash specific.
12822
 
                if (!isset($this->objectStack[$i]->attr['data']) && $token->attr['name'] == 'movie') {
12823
 
                    $this->objectStack[$i]->attr['data'] = $token->attr['value'];
12824
 
                }
12825
 
                // Check if the parameter is the correct value but has not
12826
 
                // already been added
12827
 
                if (
12828
 
                    !isset($this->paramStack[$i][$n]) &&
12829
 
                    isset($this->addParam[$n]) &&
12830
 
                    $token->attr['name'] === $this->addParam[$n]
12831
 
                ) {
12832
 
                    // keep token, and add to param stack
12833
 
                    $this->paramStack[$i][$n] = true;
12834
 
                } elseif (isset($this->allowedParam[$n])) {
12835
 
                    // keep token, don't do anything to it
12836
 
                    // (could possibly check for duplicates here)
12837
 
                } else {
12838
 
                    $token = false;
12839
 
                }
12840
 
            } else {
12841
 
                // not directly inside an object, DENY!
12842
 
                $token = false;
12843
 
            }
12844
 
        }
12845
 
    }
12846
 
    
12847
 
    public function handleEnd(&$token) {
12848
 
        // This is the WRONG way of handling the object and param stacks;
12849
 
        // we should be inserting them directly on the relevant object tokens
12850
 
        // so that the global stack handling handles it.
12851
 
        if ($token->name == 'object') {
12852
 
            array_pop($this->objectStack);
12853
 
            array_pop($this->paramStack);
12854
 
        }
12855
 
    }
12856
 
    
12857
 
}
12858
 
 
12859
 
 
12860
 
 
12861
 
 
12862
 
/**
12863
 
 * Parser that uses PHP 5's DOM extension (part of the core).
12864
 
 * 
12865
 
 * In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
12866
 
 * It gives us a forgiving HTML parser, which we use to transform the HTML
12867
 
 * into a DOM, and then into the tokens.  It is blazingly fast (for large
12868
 
 * documents, it performs twenty times faster than
12869
 
 * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5. 
12870
 
 * 
12871
 
 * @note Any empty elements will have empty tokens associated with them, even if
12872
 
 * this is prohibited by the spec. This is cannot be fixed until the spec
12873
 
 * comes into play.
12874
 
 * 
12875
 
 * @note PHP's DOM extension does not actually parse any entities, we use
12876
 
 *       our own function to do that.
12877
 
 * 
12878
 
 * @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
12879
 
 *          If this is a huge problem, due to the fact that HTML is hand
12880
 
 *          edited and you are unable to get a parser cache that caches the
12881
 
 *          the output of HTML Purifier while keeping the original HTML lying
12882
 
 *          around, you may want to run Tidy on the resulting output or use
12883
 
 *          HTMLPurifier_DirectLex
12884
 
 */
12885
 
 
12886
 
class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
12887
 
{
12888
 
    
12889
 
    private $factory;
12890
 
    
12891
 
    public function __construct() {
12892
 
        // setup the factory
12893
 
        parent::__construct();
12894
 
        $this->factory = new HTMLPurifier_TokenFactory();
12895
 
    }
12896
 
    
12897
 
    public function tokenizeHTML($html, $config, $context) {
12898
 
        
12899
 
        $html = $this->normalize($html, $config, $context);
12900
 
        
12901
 
        // attempt to armor stray angled brackets that cannot possibly
12902
 
        // form tags and thus are probably being used as emoticons
12903
 
        if ($config->get('Core', 'AggressivelyFixLt')) {
12904
 
            $char = '[^a-z!\/]';
12905
 
            $comment = "/<!--(.*?)(-->|\z)/is";
12906
 
            $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
12907
 
            do {
12908
 
                $old = $html;
12909
 
                $html = preg_replace("/<($char)/i", '&lt;\\1', $html);
12910
 
            } while ($html !== $old);
12911
 
            $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
12912
 
        }
12913
 
        
12914
 
        // preprocess html, essential for UTF-8
12915
 
        $html = $this->wrapHTML($html, $config, $context);
12916
 
        
12917
 
        $doc = new DOMDocument();
12918
 
        $doc->encoding = 'UTF-8'; // theoretically, the above has this covered
12919
 
        
12920
 
        set_error_handler(array($this, 'muteErrorHandler'));
12921
 
        $doc->loadHTML($html);
12922
 
        restore_error_handler();
12923
 
        
12924
 
        $tokens = array();
12925
 
        $this->tokenizeDOM(
12926
 
            $doc->getElementsByTagName('html')->item(0)-> // <html>
12927
 
                  getElementsByTagName('body')->item(0)-> //   <body>
12928
 
                  getElementsByTagName('div')->item(0)    //     <div>
12929
 
            , $tokens);
12930
 
        return $tokens;
12931
 
    }
12932
 
    
12933
 
    /**
12934
 
     * Recursive function that tokenizes a node, putting it into an accumulator.
12935
 
     * 
12936
 
     * @param $node     DOMNode to be tokenized.
12937
 
     * @param $tokens   Array-list of already tokenized tokens.
12938
 
     * @param $collect  Says whether or start and close are collected, set to
12939
 
     *                  false at first recursion because it's the implicit DIV
12940
 
     *                  tag you're dealing with.
12941
 
     * @returns Tokens of node appended to previously passed tokens.
12942
 
     */
12943
 
    protected function tokenizeDOM($node, &$tokens, $collect = false) {
12944
 
        
12945
 
        // intercept non element nodes. WE MUST catch all of them,
12946
 
        // but we're not getting the character reference nodes because
12947
 
        // those should have been preprocessed
12948
 
        if ($node->nodeType === XML_TEXT_NODE) {
12949
 
            $tokens[] = $this->factory->createText($node->data);
12950
 
            return;
12951
 
        } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
12952
 
            // undo libxml's special treatment of <script> and <style> tags
12953
 
            $last = end($tokens);
12954
 
            $data = $node->data;
12955
 
            // (note $node->tagname is already normalized)
12956
 
            if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
12957
 
                $new_data = trim($data);
12958
 
                if (substr($new_data, 0, 4) === '<!--') {
12959
 
                    $data = substr($new_data, 4);
12960
 
                    if (substr($data, -3) === '-->') {
12961
 
                        $data = substr($data, 0, -3);
12962
 
                    } else {
12963
 
                        // Highly suspicious! Not sure what to do...
12964
 
                    }
12965
 
                }
12966
 
            }
12967
 
            $tokens[] = $this->factory->createText($this->parseData($data));
12968
 
            return;
12969
 
        } elseif ($node->nodeType === XML_COMMENT_NODE) {
12970
 
            // this is code is only invoked for comments in script/style in versions
12971
 
            // of libxml pre-2.6.28 (regular comments, of course, are still
12972
 
            // handled regularly)
12973
 
            $tokens[] = $this->factory->createComment($node->data);
12974
 
            return;
12975
 
        } elseif (
12976
 
            // not-well tested: there may be other nodes we have to grab
12977
 
            $node->nodeType !== XML_ELEMENT_NODE
12978
 
        ) {
12979
 
            return;
12980
 
        }
12981
 
        
12982
 
        $attr = $node->hasAttributes() ?
12983
 
            $this->transformAttrToAssoc($node->attributes) :
12984
 
            array();
12985
 
        
12986
 
        // We still have to make sure that the element actually IS empty
12987
 
        if (!$node->childNodes->length) {
12988
 
            if ($collect) {
12989
 
                $tokens[] = $this->factory->createEmpty($node->tagName, $attr);
12990
 
            }
12991
 
        } else {
12992
 
            if ($collect) { // don't wrap on first iteration
12993
 
                $tokens[] = $this->factory->createStart(
12994
 
                    $tag_name = $node->tagName, // somehow, it get's dropped
12995
 
                    $attr
12996
 
                );
12997
 
            }
12998
 
            foreach ($node->childNodes as $node) {
12999
 
                // remember, it's an accumulator. Otherwise, we'd have
13000
 
                // to use array_merge
13001
 
                $this->tokenizeDOM($node, $tokens, true);
13002
 
            }
13003
 
            if ($collect) {
13004
 
                $tokens[] = $this->factory->createEnd($tag_name);
13005
 
            }
13006
 
        }
13007
 
        
13008
 
    }
13009
 
    
13010
 
    /**
13011
 
     * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
13012
 
     * 
13013
 
     * @param $attribute_list DOMNamedNodeMap of DOMAttr objects.
13014
 
     * @returns Associative array of attributes.
13015
 
     */
13016
 
    protected function transformAttrToAssoc($node_map) {
13017
 
        // NamedNodeMap is documented very well, so we're using undocumented
13018
 
        // features, namely, the fact that it implements Iterator and
13019
 
        // has a ->length attribute
13020
 
        if ($node_map->length === 0) return array();
13021
 
        $array = array();
13022
 
        foreach ($node_map as $attr) {
13023
 
            $array[$attr->name] = $attr->value;
13024
 
        }
13025
 
        return $array;
13026
 
    }
13027
 
    
13028
 
    /**
13029
 
     * An error handler that mutes all errors
13030
 
     */
13031
 
    public function muteErrorHandler($errno, $errstr) {}
13032
 
    
13033
 
    /**
13034
 
     * Callback function for undoing escaping of stray angled brackets
13035
 
     * in comments
13036
 
     */
13037
 
    public function callbackUndoCommentSubst($matches) {
13038
 
        return '<!--' . strtr($matches[1], array('&amp;'=>'&','&lt;'=>'<')) . $matches[2];
13039
 
    }
13040
 
    
13041
 
    /**
13042
 
     * Callback function that entity-izes ampersands in comments so that
13043
 
     * callbackUndoCommentSubst doesn't clobber them
13044
 
     */
13045
 
    public function callbackArmorCommentEntities($matches) {
13046
 
        return '<!--' . str_replace('&', '&amp;', $matches[1]) . $matches[2];
13047
 
    }
13048
 
    
13049
 
    /**
13050
 
     * Wraps an HTML fragment in the necessary HTML
13051
 
     */
13052
 
    protected function wrapHTML($html, $config, $context) {
13053
 
        $def = $config->getDefinition('HTML');
13054
 
        $ret = '';
13055
 
        
13056
 
        if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
13057
 
            $ret .= '<!DOCTYPE html ';
13058
 
            if (!empty($def->doctype->dtdPublic)) $ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
13059
 
            if (!empty($def->doctype->dtdSystem)) $ret .= '"' . $def->doctype->dtdSystem . '" ';
13060
 
            $ret .= '>';
13061
 
        }
13062
 
        
13063
 
        $ret .= '<html><head>';
13064
 
        $ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
13065
 
        // No protection if $html contains a stray </div>!
13066
 
        $ret .= '</head><body><div>'.$html.'</div></body></html>';
13067
 
        return $ret;
13068
 
    }
13069
 
    
13070
 
}
13071
 
 
13072
 
 
13073
 
 
13074
 
 
13075
 
/**
13076
 
 * Our in-house implementation of a parser.
13077
 
 * 
13078
 
 * A pure PHP parser, DirectLex has absolutely no dependencies, making
13079
 
 * it a reasonably good default for PHP4.  Written with efficiency in mind,
13080
 
 * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
13081
 
 * pales in comparison to HTMLPurifier_Lexer_DOMLex.
13082
 
 * 
13083
 
 * @todo Reread XML spec and document differences.
13084
 
 */
13085
 
class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
13086
 
{
13087
 
    
13088
 
    public $tracksLineNumbers = true;
13089
 
    
13090
 
    /**
13091
 
     * Whitespace characters for str(c)spn.
13092
 
     */
13093
 
    protected $_whitespace = "\x20\x09\x0D\x0A";
13094
 
    
13095
 
    /**
13096
 
     * Callback function for script CDATA fudge
13097
 
     * @param $matches, in form of array(opening tag, contents, closing tag)
13098
 
     */
13099
 
    protected function scriptCallback($matches) {
13100
 
        return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
13101
 
    }
13102
 
    
13103
 
    public function tokenizeHTML($html, $config, $context) {
13104
 
        
13105
 
        // special normalization for script tags without any armor
13106
 
        // our "armor" heurstic is a < sign any number of whitespaces after
13107
 
        // the first script tag
13108
 
        if ($config->get('HTML', 'Trusted')) {
13109
 
            $html = preg_replace_callback('#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
13110
 
                array($this, 'scriptCallback'), $html);
13111
 
        }
13112
 
        
13113
 
        $html = $this->normalize($html, $config, $context);
13114
 
        
13115
 
        $cursor = 0; // our location in the text
13116
 
        $inside_tag = false; // whether or not we're parsing the inside of a tag
13117
 
        $array = array(); // result array
13118
 
        
13119
 
        // This is also treated to mean maintain *column* numbers too
13120
 
        $maintain_line_numbers = $config->get('Core', 'MaintainLineNumbers');
13121
 
        
13122
 
        if ($maintain_line_numbers === null) {
13123
 
            // automatically determine line numbering by checking
13124
 
            // if error collection is on
13125
 
            $maintain_line_numbers = $config->get('Core', 'CollectErrors');
13126
 
        }
13127
 
        
13128
 
        if ($maintain_line_numbers) {
13129
 
            $current_line = 1;
13130
 
            $current_col  = 0; 
13131
 
            $length = strlen($html);
13132
 
        } else {
13133
 
            $current_line = false;
13134
 
            $current_col  = false;
13135
 
            $length = false;
13136
 
        }
13137
 
        $context->register('CurrentLine', $current_line);
13138
 
        $context->register('CurrentCol',  $current_col);
13139
 
        $nl = "\n";
13140
 
        // how often to manually recalculate. This will ALWAYS be right,
13141
 
        // but it's pretty wasteful. Set to 0 to turn off
13142
 
        $synchronize_interval = $config->get('Core', 'DirectLexLineNumberSyncInterval'); 
13143
 
        
13144
 
        $e = false;
13145
 
        if ($config->get('Core', 'CollectErrors')) {
13146
 
            $e =& $context->get('ErrorCollector');
13147
 
        }
13148
 
        
13149
 
        // for testing synchronization
13150
 
        $loops = 0;
13151
 
        
13152
 
        while(++$loops) {
13153
 
            
13154
 
            // $cursor is either at the start of a token, or inside of
13155
 
            // a tag (i.e. there was a < immediately before it), as indicated
13156
 
            // by $inside_tag
13157
 
            
13158
 
            if ($maintain_line_numbers) {
13159
 
                
13160
 
                // $rcursor, however, is always at the start of a token.
13161
 
                $rcursor = $cursor - (int) $inside_tag;
13162
 
                
13163
 
                // Column number is cheap, so we calculate it every round.
13164
 
                // We're interested at the *end* of the newline string, so 
13165
 
                // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
13166
 
                // from our "rcursor" position.
13167
 
                $nl_pos = strrpos($html, $nl, $rcursor - $length);
13168
 
                $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
13169
 
                
13170
 
                // recalculate lines
13171
 
                if (
13172
 
                    $synchronize_interval &&  // synchronization is on
13173
 
                    $cursor > 0 &&            // cursor is further than zero
13174
 
                    $loops % $synchronize_interval === 0 // time to synchronize!
13175
 
                ) {
13176
 
                    $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
13177
 
                }
13178
 
                
13179
 
            }
13180
 
            
13181
 
            $position_next_lt = strpos($html, '<', $cursor);
13182
 
            $position_next_gt = strpos($html, '>', $cursor);
13183
 
            
13184
 
            // triggers on "<b>asdf</b>" but not "asdf <b></b>"
13185
 
            // special case to set up context
13186
 
            if ($position_next_lt === $cursor) {
13187
 
                $inside_tag = true;
13188
 
                $cursor++;
13189
 
            }
13190
 
            
13191
 
            if (!$inside_tag && $position_next_lt !== false) {
13192
 
                // We are not inside tag and there still is another tag to parse
13193
 
                $token = new
13194
 
                    HTMLPurifier_Token_Text(
13195
 
                        $this->parseData(
13196
 
                            substr(
13197
 
                                $html, $cursor, $position_next_lt - $cursor
13198
 
                            )
13199
 
                        )
13200
 
                    );
13201
 
                if ($maintain_line_numbers) {
13202
 
                    $token->rawPosition($current_line, $current_col);
13203
 
                    $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
13204
 
                }
13205
 
                $array[] = $token;
13206
 
                $cursor  = $position_next_lt + 1;
13207
 
                $inside_tag = true;
13208
 
                continue;
13209
 
            } elseif (!$inside_tag) {
13210
 
                // We are not inside tag but there are no more tags
13211
 
                // If we're already at the end, break
13212
 
                if ($cursor === strlen($html)) break;
13213
 
                // Create Text of rest of string
13214
 
                $token = new
13215
 
                    HTMLPurifier_Token_Text(
13216
 
                        $this->parseData(
13217
 
                            substr(
13218
 
                                $html, $cursor
13219
 
                            )
13220
 
                        )
13221
 
                    );
13222
 
                if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
13223
 
                $array[] = $token;
13224
 
                break;
13225
 
            } elseif ($inside_tag && $position_next_gt !== false) {
13226
 
                // We are in tag and it is well formed
13227
 
                // Grab the internals of the tag
13228
 
                $strlen_segment = $position_next_gt - $cursor;
13229
 
                
13230
 
                if ($strlen_segment < 1) {
13231
 
                    // there's nothing to process!
13232
 
                    $token = new HTMLPurifier_Token_Text('<');
13233
 
                    $cursor++;
13234
 
                    continue;
13235
 
                }
13236
 
                
13237
 
                $segment = substr($html, $cursor, $strlen_segment);
13238
 
                
13239
 
                if ($segment === false) {
13240
 
                    // somehow, we attempted to access beyond the end of
13241
 
                    // the string, defense-in-depth, reported by Nate Abele
13242
 
                    break;
13243
 
                }
13244
 
                
13245
 
                // Check if it's a comment
13246
 
                if (
13247
 
                    substr($segment, 0, 3) === '!--'
13248
 
                ) {
13249
 
                    // re-determine segment length, looking for -->
13250
 
                    $position_comment_end = strpos($html, '-->', $cursor);
13251
 
                    if ($position_comment_end === false) {
13252
 
                        // uh oh, we have a comment that extends to
13253
 
                        // infinity. Can't be helped: set comment
13254
 
                        // end position to end of string
13255
 
                        if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
13256
 
                        $position_comment_end = strlen($html);
13257
 
                        $end = true;
13258
 
                    } else {
13259
 
                        $end = false;
13260
 
                    }
13261
 
                    $strlen_segment = $position_comment_end - $cursor;
13262
 
                    $segment = substr($html, $cursor, $strlen_segment);
13263
 
                    $token = new
13264
 
                        HTMLPurifier_Token_Comment(
13265
 
                            substr(
13266
 
                                $segment, 3, $strlen_segment - 3
13267
 
                            )
13268
 
                        );
13269
 
                    if ($maintain_line_numbers) {
13270
 
                        $token->rawPosition($current_line, $current_col);
13271
 
                        $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
13272
 
                    }
13273
 
                    $array[] = $token;
13274
 
                    $cursor = $end ? $position_comment_end : $position_comment_end + 3;
13275
 
                    $inside_tag = false;
13276
 
                    continue;
13277
 
                }
13278
 
                
13279
 
                // Check if it's an end tag
13280
 
                $is_end_tag = (strpos($segment,'/') === 0);
13281
 
                if ($is_end_tag) {
13282
 
                    $type = substr($segment, 1);
13283
 
                    $token = new HTMLPurifier_Token_End($type);
13284
 
                    if ($maintain_line_numbers) {
13285
 
                        $token->rawPosition($current_line, $current_col);
13286
 
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
13287
 
                    }
13288
 
                    $array[] = $token;
13289
 
                    $inside_tag = false;
13290
 
                    $cursor = $position_next_gt + 1;
13291
 
                    continue;
13292
 
                }
13293
 
                
13294
 
                // Check leading character is alnum, if not, we may
13295
 
                // have accidently grabbed an emoticon. Translate into
13296
 
                // text and go our merry way
13297
 
                if (!ctype_alpha($segment[0])) {
13298
 
                    // XML:  $segment[0] !== '_' && $segment[0] !== ':'
13299
 
                    if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt');
13300
 
                    $token = new HTMLPurifier_Token_Text('<');
13301
 
                    if ($maintain_line_numbers) {
13302
 
                        $token->rawPosition($current_line, $current_col);
13303
 
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
13304
 
                    }
13305
 
                    $array[] = $token;
13306
 
                    $inside_tag = false;
13307
 
                    continue;
13308
 
                }
13309
 
                
13310
 
                // Check if it is explicitly self closing, if so, remove
13311
 
                // trailing slash. Remember, we could have a tag like <br>, so
13312
 
                // any later token processing scripts must convert improperly
13313
 
                // classified EmptyTags from StartTags.
13314
 
                $is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
13315
 
                if ($is_self_closing) {
13316
 
                    $strlen_segment--;
13317
 
                    $segment = substr($segment, 0, $strlen_segment);
13318
 
                }
13319
 
                
13320
 
                // Check if there are any attributes
13321
 
                $position_first_space = strcspn($segment, $this->_whitespace);
13322
 
                
13323
 
                if ($position_first_space >= $strlen_segment) {
13324
 
                    if ($is_self_closing) {
13325
 
                        $token = new HTMLPurifier_Token_Empty($segment);
13326
 
                    } else {
13327
 
                        $token = new HTMLPurifier_Token_Start($segment);
13328
 
                    }
13329
 
                    if ($maintain_line_numbers) {
13330
 
                        $token->rawPosition($current_line, $current_col);
13331
 
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
13332
 
                    }
13333
 
                    $array[] = $token;
13334
 
                    $inside_tag = false;
13335
 
                    $cursor = $position_next_gt + 1;
13336
 
                    continue;
13337
 
                }
13338
 
                
13339
 
                // Grab out all the data
13340
 
                $type = substr($segment, 0, $position_first_space);
13341
 
                $attribute_string =
13342
 
                    trim(
13343
 
                        substr(
13344
 
                            $segment, $position_first_space
13345
 
                        )
13346
 
                    );
13347
 
                if ($attribute_string) {
13348
 
                    $attr = $this->parseAttributeString(
13349
 
                                    $attribute_string
13350
 
                                  , $config, $context
13351
 
                              );
13352
 
                } else {
13353
 
                    $attr = array();
13354
 
                }
13355
 
                
13356
 
                if ($is_self_closing) {
13357
 
                    $token = new HTMLPurifier_Token_Empty($type, $attr);
13358
 
                } else {
13359
 
                    $token = new HTMLPurifier_Token_Start($type, $attr);
13360
 
                }
13361
 
                if ($maintain_line_numbers) {
13362
 
                    $token->rawPosition($current_line, $current_col);
13363
 
                    $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
13364
 
                }
13365
 
                $array[] = $token;
13366
 
                $cursor = $position_next_gt + 1;
13367
 
                $inside_tag = false;
13368
 
                continue;
13369
 
            } else {
13370
 
                // inside tag, but there's no ending > sign
13371
 
                if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
13372
 
                $token = new
13373
 
                    HTMLPurifier_Token_Text(
13374
 
                        '<' .
13375
 
                        $this->parseData(
13376
 
                            substr($html, $cursor)
13377
 
                        )
13378
 
                    );
13379
 
                if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
13380
 
                // no cursor scroll? Hmm...
13381
 
                $array[] = $token;
13382
 
                break;
13383
 
            }
13384
 
            break;
13385
 
        }
13386
 
        
13387
 
        $context->destroy('CurrentLine');
13388
 
        $context->destroy('CurrentCol');
13389
 
        return $array;
13390
 
    }
13391
 
    
13392
 
    /**
13393
 
     * PHP 5.0.x compatible substr_count that implements offset and length
13394
 
     */
13395
 
    protected function substrCount($haystack, $needle, $offset, $length) {
13396
 
        static $oldVersion;
13397
 
        if ($oldVersion === null) {
13398
 
            $oldVersion = version_compare(PHP_VERSION, '5.1', '<');
13399
 
        }
13400
 
        if ($oldVersion) {
13401
 
            $haystack = substr($haystack, $offset, $length);
13402
 
            return substr_count($haystack, $needle);
13403
 
        } else {
13404
 
            return substr_count($haystack, $needle, $offset, $length);
13405
 
        }
13406
 
    }
13407
 
    
13408
 
    /**
13409
 
     * Takes the inside of an HTML tag and makes an assoc array of attributes.
13410
 
     * 
13411
 
     * @param $string Inside of tag excluding name.
13412
 
     * @returns Assoc array of attributes.
13413
 
     */
13414
 
    public function parseAttributeString($string, $config, $context) {
13415
 
        $string = (string) $string; // quick typecast
13416
 
        
13417
 
        if ($string == '') return array(); // no attributes
13418
 
        
13419
 
        $e = false;
13420
 
        if ($config->get('Core', 'CollectErrors')) {
13421
 
            $e =& $context->get('ErrorCollector');
13422
 
        }
13423
 
        
13424
 
        // let's see if we can abort as quickly as possible
13425
 
        // one equal sign, no spaces => one attribute
13426
 
        $num_equal = substr_count($string, '=');
13427
 
        $has_space = strpos($string, ' ');
13428
 
        if ($num_equal === 0 && !$has_space) {
13429
 
            // bool attribute
13430
 
            return array($string => $string);
13431
 
        } elseif ($num_equal === 1 && !$has_space) {
13432
 
            // only one attribute
13433
 
            list($key, $quoted_value) = explode('=', $string);
13434
 
            $quoted_value = trim($quoted_value);
13435
 
            if (!$key) {
13436
 
                if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
13437
 
                return array();
13438
 
            }
13439
 
            if (!$quoted_value) return array($key => '');
13440
 
            $first_char = @$quoted_value[0];
13441
 
            $last_char  = @$quoted_value[strlen($quoted_value)-1];
13442
 
            
13443
 
            $same_quote = ($first_char == $last_char);
13444
 
            $open_quote = ($first_char == '"' || $first_char == "'");
13445
 
            
13446
 
            if ( $same_quote && $open_quote) {
13447
 
                // well behaved
13448
 
                $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
13449
 
            } else {
13450
 
                // not well behaved
13451
 
                if ($open_quote) {
13452
 
                    if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
13453
 
                    $value = substr($quoted_value, 1);
13454
 
                } else {
13455
 
                    $value = $quoted_value;
13456
 
                }
13457
 
            }
13458
 
            if ($value === false) $value = '';
13459
 
            return array($key => $value);
13460
 
        }
13461
 
        
13462
 
        // setup loop environment
13463
 
        $array  = array(); // return assoc array of attributes
13464
 
        $cursor = 0; // current position in string (moves forward)
13465
 
        $size   = strlen($string); // size of the string (stays the same)
13466
 
        
13467
 
        // if we have unquoted attributes, the parser expects a terminating
13468
 
        // space, so let's guarantee that there's always a terminating space.
13469
 
        $string .= ' ';
13470
 
        
13471
 
        while(true) {
13472
 
            
13473
 
            if ($cursor >= $size) {
13474
 
                break;
13475
 
            }
13476
 
            
13477
 
            $cursor += ($value = strspn($string, $this->_whitespace, $cursor));
13478
 
            // grab the key
13479
 
            
13480
 
            $key_begin = $cursor; //we're currently at the start of the key
13481
 
            
13482
 
            // scroll past all characters that are the key (not whitespace or =)
13483
 
            $cursor += strcspn($string, $this->_whitespace . '=', $cursor);
13484
 
            
13485
 
            $key_end = $cursor; // now at the end of the key
13486
 
            
13487
 
            $key = substr($string, $key_begin, $key_end - $key_begin);
13488
 
            
13489
 
            if (!$key) {
13490
 
                if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
13491
 
                $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
13492
 
                continue; // empty key
13493
 
            }
13494
 
            
13495
 
            // scroll past all whitespace
13496
 
            $cursor += strspn($string, $this->_whitespace, $cursor);
13497
 
            
13498
 
            if ($cursor >= $size) {
13499
 
                $array[$key] = $key;
13500
 
                break;
13501
 
            }
13502
 
            
13503
 
            // if the next character is an equal sign, we've got a regular
13504
 
            // pair, otherwise, it's a bool attribute
13505
 
            $first_char = @$string[$cursor];
13506
 
            
13507
 
            if ($first_char == '=') {
13508
 
                // key="value"
13509
 
                
13510
 
                $cursor++;
13511
 
                $cursor += strspn($string, $this->_whitespace, $cursor);
13512
 
                
13513
 
                if ($cursor === false) {
13514
 
                    $array[$key] = '';
13515
 
                    break;
13516
 
                }
13517
 
                
13518
 
                // we might be in front of a quote right now
13519
 
                
13520
 
                $char = @$string[$cursor];
13521
 
                
13522
 
                if ($char == '"' || $char == "'") {
13523
 
                    // it's quoted, end bound is $char
13524
 
                    $cursor++;
13525
 
                    $value_begin = $cursor;
13526
 
                    $cursor = strpos($string, $char, $cursor);
13527
 
                    $value_end = $cursor;
13528
 
                } else {
13529
 
                    // it's not quoted, end bound is whitespace
13530
 
                    $value_begin = $cursor;
13531
 
                    $cursor += strcspn($string, $this->_whitespace, $cursor);
13532
 
                    $value_end = $cursor;
13533
 
                }
13534
 
                
13535
 
                // we reached a premature end
13536
 
                if ($cursor === false) {
13537
 
                    $cursor = $size;
13538
 
                    $value_end = $cursor;
13539
 
                }
13540
 
                
13541
 
                $value = substr($string, $value_begin, $value_end - $value_begin);
13542
 
                if ($value === false) $value = '';
13543
 
                $array[$key] = $this->parseData($value);
13544
 
                $cursor++;
13545
 
                
13546
 
            } else {
13547
 
                // boolattr
13548
 
                if ($key !== '') {
13549
 
                    $array[$key] = $key;
13550
 
                } else {
13551
 
                    // purely theoretical
13552
 
                    if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
13553
 
                }
13554
 
                
13555
 
            }
13556
 
        }
13557
 
        return $array;
13558
 
    }
13559
 
    
13560
 
}
13561
 
 
13562
 
 
13563
 
 
13564
 
 
13565
 
/**
13566
 
 * Composite strategy that runs multiple strategies on tokens.
13567
 
 */
13568
 
abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
13569
 
{
13570
 
    
13571
 
    /**
13572
 
     * List of strategies to run tokens through.
13573
 
     */
13574
 
    protected $strategies = array();
13575
 
    
13576
 
    abstract public function __construct();
13577
 
    
13578
 
    public function execute($tokens, $config, $context) {
13579
 
        foreach ($this->strategies as $strategy) {
13580
 
            $tokens = $strategy->execute($tokens, $config, $context);
13581
 
        }
13582
 
        return $tokens;
13583
 
    }
13584
 
    
13585
 
}
13586
 
 
13587
 
 
13588
 
 
13589
 
 
13590
 
/**
13591
 
 * Core strategy composed of the big four strategies.
13592
 
 */
13593
 
class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite
13594
 
{
13595
 
    
13596
 
    public function __construct() {
13597
 
        $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
13598
 
        $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
13599
 
        $this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
13600
 
        $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
13601
 
    }
13602
 
    
13603
 
}
13604
 
 
13605
 
 
13606
 
 
13607
 
 
13608
 
/**
13609
 
 * Takes a well formed list of tokens and fixes their nesting.
13610
 
 * 
13611
 
 * HTML elements dictate which elements are allowed to be their children,
13612
 
 * for example, you can't have a p tag in a span tag.  Other elements have
13613
 
 * much more rigorous definitions: tables, for instance, require a specific
13614
 
 * order for their elements.  There are also constraints not expressible by
13615
 
 * document type definitions, such as the chameleon nature of ins/del
13616
 
 * tags and global child exclusions.
13617
 
 * 
13618
 
 * The first major objective of this strategy is to iterate through all the
13619
 
 * nodes (not tokens) of the list of tokens and determine whether or not
13620
 
 * their children conform to the element's definition.  If they do not, the
13621
 
 * child definition may optionally supply an amended list of elements that
13622
 
 * is valid or require that the entire node be deleted (and the previous
13623
 
 * node rescanned).
13624
 
 * 
13625
 
 * The second objective is to ensure that explicitly excluded elements of
13626
 
 * an element do not appear in its children.  Code that accomplishes this
13627
 
 * task is pervasive through the strategy, though the two are distinct tasks
13628
 
 * and could, theoretically, be seperated (although it's not recommended).
13629
 
 * 
13630
 
 * @note Whether or not unrecognized children are silently dropped or
13631
 
 *       translated into text depends on the child definitions.
13632
 
 * 
13633
 
 * @todo Enable nodes to be bubbled out of the structure.
13634
 
 */
13635
 
 
13636
 
class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
13637
 
{
13638
 
    
13639
 
    public function execute($tokens, $config, $context) {
13640
 
        //####################################################################//
13641
 
        // Pre-processing
13642
 
        
13643
 
        // get a copy of the HTML definition
13644
 
        $definition = $config->getHTMLDefinition();
13645
 
        
13646
 
        // insert implicit "parent" node, will be removed at end.
13647
 
        // DEFINITION CALL
13648
 
        $parent_name = $definition->info_parent;
13649
 
        array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name));
13650
 
        $tokens[] = new HTMLPurifier_Token_End($parent_name);
13651
 
        
13652
 
        // setup the context variable 'IsInline', for chameleon processing
13653
 
        // is 'false' when we are not inline, 'true' when it must always
13654
 
        // be inline, and an integer when it is inline for a certain
13655
 
        // branch of the document tree
13656
 
        $is_inline = $definition->info_parent_def->descendants_are_inline;
13657
 
        $context->register('IsInline', $is_inline);
13658
 
        
13659
 
        // setup error collector
13660
 
        $e =& $context->get('ErrorCollector', true);
13661
 
        
13662
 
        //####################################################################//
13663
 
        // Loop initialization
13664
 
        
13665
 
        // stack that contains the indexes of all parents,
13666
 
        // $stack[count($stack)-1] being the current parent
13667
 
        $stack = array();
13668
 
        
13669
 
        // stack that contains all elements that are excluded
13670
 
        // it is organized by parent elements, similar to $stack, 
13671
 
        // but it is only populated when an element with exclusions is
13672
 
        // processed, i.e. there won't be empty exclusions.
13673
 
        $exclude_stack = array();
13674
 
        
13675
 
        // variable that contains the start token while we are processing
13676
 
        // nodes. This enables error reporting to do its job
13677
 
        $start_token = false;
13678
 
        $context->register('CurrentToken', $start_token);
13679
 
        
13680
 
        //####################################################################//
13681
 
        // Loop
13682
 
        
13683
 
        // iterate through all start nodes. Determining the start node
13684
 
        // is complicated so it has been omitted from the loop construct
13685
 
        for ($i = 0, $size = count($tokens) ; $i < $size; ) {
13686
 
            
13687
 
            //################################################################//
13688
 
            // Gather information on children
13689
 
            
13690
 
            // child token accumulator
13691
 
            $child_tokens = array();
13692
 
            
13693
 
            // scroll to the end of this node, report number, and collect
13694
 
            // all children
13695
 
            for ($j = $i, $depth = 0; ; $j++) {
13696
 
                if ($tokens[$j] instanceof HTMLPurifier_Token_Start) {
13697
 
                    $depth++;
13698
 
                    // skip token assignment on first iteration, this is the
13699
 
                    // token we currently are on
13700
 
                    if ($depth == 1) continue;
13701
 
                } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) {
13702
 
                    $depth--;
13703
 
                    // skip token assignment on last iteration, this is the
13704
 
                    // end token of the token we're currently on
13705
 
                    if ($depth == 0) break;
13706
 
                }
13707
 
                $child_tokens[] = $tokens[$j];
13708
 
            }
13709
 
            
13710
 
            // $i is index of start token
13711
 
            // $j is index of end token
13712
 
            
13713
 
            $start_token = $tokens[$i]; // to make token available via CurrentToken
13714
 
            
13715
 
            //################################################################//
13716
 
            // Gather information on parent
13717
 
            
13718
 
            // calculate parent information
13719
 
            if ($count = count($stack)) {
13720
 
                $parent_index = $stack[$count-1];
13721
 
                $parent_name  = $tokens[$parent_index]->name;
13722
 
                if ($parent_index == 0) {
13723
 
                    $parent_def   = $definition->info_parent_def;
13724
 
                } else {
13725
 
                    $parent_def   = $definition->info[$parent_name];
13726
 
                }
13727
 
            } else {
13728
 
                // processing as if the parent were the "root" node
13729
 
                // unknown info, it won't be used anyway, in the future,
13730
 
                // we may want to enforce one element only (this is 
13731
 
                // necessary for HTML Purifier to clean entire documents
13732
 
                $parent_index = $parent_name = $parent_def = null;
13733
 
            }
13734
 
            
13735
 
            // calculate context
13736
 
            if ($is_inline === false) {
13737
 
                // check if conditions make it inline
13738
 
                if (!empty($parent_def) && $parent_def->descendants_are_inline) {
13739
 
                    $is_inline = $count - 1;
13740
 
                }
13741
 
            } else {
13742
 
                // check if we're out of inline
13743
 
                if ($count === $is_inline) {
13744
 
                    $is_inline = false;
13745
 
                }
13746
 
            }
13747
 
            
13748
 
            //################################################################//
13749
 
            // Determine whether element is explicitly excluded SGML-style
13750
 
            
13751
 
            // determine whether or not element is excluded by checking all
13752
 
            // parent exclusions. The array should not be very large, two
13753
 
            // elements at most.
13754
 
            $excluded = false;
13755
 
            if (!empty($exclude_stack)) {
13756
 
                foreach ($exclude_stack as $lookup) {
13757
 
                    if (isset($lookup[$tokens[$i]->name])) {
13758
 
                        $excluded = true;
13759
 
                        // no need to continue processing
13760
 
                        break;
13761
 
                    }
13762
 
                }
13763
 
            }
13764
 
            
13765
 
            //################################################################//
13766
 
            // Perform child validation
13767
 
            
13768
 
            if ($excluded) {
13769
 
                // there is an exclusion, remove the entire node
13770
 
                $result = false;
13771
 
                $excludes = array(); // not used, but good to initialize anyway
13772
 
            } else {
13773
 
                // DEFINITION CALL
13774
 
                if ($i === 0) {
13775
 
                    // special processing for the first node
13776
 
                    $def = $definition->info_parent_def;
13777
 
                } else {
13778
 
                    $def = $definition->info[$tokens[$i]->name];
13779
 
                    
13780
 
                }
13781
 
                
13782
 
                if (!empty($def->child)) {
13783
 
                    // have DTD child def validate children
13784
 
                    $result = $def->child->validateChildren(
13785
 
                        $child_tokens, $config, $context);
13786
 
                } else {
13787
 
                    // weird, no child definition, get rid of everything
13788
 
                    $result = false;
13789
 
                }
13790
 
                
13791
 
                // determine whether or not this element has any exclusions
13792
 
                $excludes = $def->excludes;
13793
 
            }
13794
 
            
13795
 
            // $result is now a bool or array
13796
 
            
13797
 
            //################################################################//
13798
 
            // Process result by interpreting $result
13799
 
            
13800
 
            if ($result === true || $child_tokens === $result) {
13801
 
                // leave the node as is
13802
 
                
13803
 
                // register start token as a parental node start
13804
 
                $stack[] = $i;
13805
 
                
13806
 
                // register exclusions if there are any
13807
 
                if (!empty($excludes)) $exclude_stack[] = $excludes;
13808
 
                
13809
 
                // move cursor to next possible start node
13810
 
                $i++;
13811
 
                
13812
 
            } elseif($result === false) {
13813
 
                // remove entire node
13814
 
                
13815
 
                if ($e) {
13816
 
                    if ($excluded) {
13817
 
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
13818
 
                    } else {
13819
 
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
13820
 
                    }
13821
 
                }
13822
 
                
13823
 
                // calculate length of inner tokens and current tokens
13824
 
                $length = $j - $i + 1;
13825
 
                
13826
 
                // perform removal
13827
 
                array_splice($tokens, $i, $length);
13828
 
                
13829
 
                // update size
13830
 
                $size -= $length;
13831
 
                
13832
 
                // there is no start token to register,
13833
 
                // current node is now the next possible start node
13834
 
                // unless it turns out that we need to do a double-check
13835
 
                
13836
 
                // this is a rought heuristic that covers 100% of HTML's
13837
 
                // cases and 99% of all other cases. A child definition
13838
 
                // that would be tricked by this would be something like:
13839
 
                // ( | a b c) where it's all or nothing. Fortunately,
13840
 
                // our current implementation claims that that case would
13841
 
                // not allow empty, even if it did
13842
 
                if (!$parent_def->child->allow_empty) {
13843
 
                    // we need to do a double-check
13844
 
                    $i = $parent_index;
13845
 
                    array_pop($stack);
13846
 
                }
13847
 
                
13848
 
                // PROJECTED OPTIMIZATION: Process all children elements before
13849
 
                // reprocessing parent node.
13850
 
                
13851
 
            } else {
13852
 
                // replace node with $result
13853
 
                
13854
 
                // calculate length of inner tokens
13855
 
                $length = $j - $i - 1;
13856
 
                
13857
 
                if ($e) {
13858
 
                    if (empty($result) && $length) {
13859
 
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
13860
 
                    } else {
13861
 
                        $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
13862
 
                    }
13863
 
                }
13864
 
                
13865
 
                // perform replacement
13866
 
                array_splice($tokens, $i + 1, $length, $result);
13867
 
                
13868
 
                // update size
13869
 
                $size -= $length;
13870
 
                $size += count($result);
13871
 
                
13872
 
                // register start token as a parental node start
13873
 
                $stack[] = $i;
13874
 
                
13875
 
                // register exclusions if there are any
13876
 
                if (!empty($excludes)) $exclude_stack[] = $excludes;
13877
 
                
13878
 
                // move cursor to next possible start node
13879
 
                $i++;
13880
 
                
13881
 
            }
13882
 
            
13883
 
            //################################################################//
13884
 
            // Scroll to next start node
13885
 
            
13886
 
            // We assume, at this point, that $i is the index of the token
13887
 
            // that is the first possible new start point for a node.
13888
 
            
13889
 
            // Test if the token indeed is a start tag, if not, move forward
13890
 
            // and test again.
13891
 
            $size = count($tokens);
13892
 
            while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) {
13893
 
                if ($tokens[$i] instanceof HTMLPurifier_Token_End) {
13894
 
                    // pop a token index off the stack if we ended a node
13895
 
                    array_pop($stack);
13896
 
                    // pop an exclusion lookup off exclusion stack if
13897
 
                    // we ended node and that node had exclusions
13898
 
                    if ($i == 0 || $i == $size - 1) {
13899
 
                        // use specialized var if it's the super-parent
13900
 
                        $s_excludes = $definition->info_parent_def->excludes;
13901
 
                    } else {
13902
 
                        $s_excludes = $definition->info[$tokens[$i]->name]->excludes;
13903
 
                    }
13904
 
                    if ($s_excludes) {
13905
 
                        array_pop($exclude_stack);
13906
 
                    }
13907
 
                }
13908
 
                $i++;
13909
 
            }
13910
 
            
13911
 
        }
13912
 
        
13913
 
        //####################################################################//
13914
 
        // Post-processing
13915
 
        
13916
 
        // remove implicit parent tokens at the beginning and end
13917
 
        array_shift($tokens);
13918
 
        array_pop($tokens);
13919
 
        
13920
 
        // remove context variables
13921
 
        $context->destroy('IsInline');
13922
 
        $context->destroy('CurrentToken');
13923
 
        
13924
 
        //####################################################################//
13925
 
        // Return
13926
 
        
13927
 
        return $tokens;
13928
 
        
13929
 
    }
13930
 
    
13931
 
}
13932
 
 
13933
 
 
13934
 
 
13935
 
 
13936
 
 
13937
 
/**
13938
 
 * Takes tokens makes them well-formed (balance end tags, etc.)
13939
 
 */
13940
 
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
13941
 
{
13942
 
    
13943
 
    /**
13944
 
     * Array stream of tokens being processed.
13945
 
     */
13946
 
    protected $tokens;
13947
 
    
13948
 
    /**
13949
 
     * Current index in $tokens.
13950
 
     */
13951
 
    protected $t;
13952
 
    
13953
 
    /**
13954
 
     * Current nesting of elements.
13955
 
     */
13956
 
    protected $stack;
13957
 
    
13958
 
    /**
13959
 
     * Injectors active in this stream processing.
13960
 
     */
13961
 
    protected $injectors;
13962
 
    
13963
 
    /**
13964
 
     * Current instance of HTMLPurifier_Config.
13965
 
     */
13966
 
    protected $config;
13967
 
    
13968
 
    /**
13969
 
     * Current instance of HTMLPurifier_Context.
13970
 
     */
13971
 
    protected $context;
13972
 
    
13973
 
    public function execute($tokens, $config, $context) {
13974
 
        
13975
 
        $definition = $config->getHTMLDefinition();
13976
 
        
13977
 
        // local variables
13978
 
        $generator = new HTMLPurifier_Generator($config, $context);
13979
 
        $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags');
13980
 
        $e = $context->get('ErrorCollector', true);
13981
 
        $t = false; // token index
13982
 
        $i = false; // injector index
13983
 
        $token      = false; // the current token
13984
 
        $reprocess  = false; // whether or not to reprocess the same token
13985
 
        $stack = array();
13986
 
        
13987
 
        // member variables
13988
 
        $this->stack   =& $stack;
13989
 
        $this->t       =& $t;
13990
 
        $this->tokens  =& $tokens;
13991
 
        $this->config  = $config;
13992
 
        $this->context = $context;
13993
 
        
13994
 
        // context variables
13995
 
        $context->register('CurrentNesting', $stack);
13996
 
        $context->register('InputIndex',     $t);
13997
 
        $context->register('InputTokens',    $tokens);
13998
 
        $context->register('CurrentToken',   $token);
13999
 
        
14000
 
        // -- begin INJECTOR --
14001
 
        
14002
 
        $this->injectors = array();
14003
 
        
14004
 
        $injectors = $config->getBatch('AutoFormat');
14005
 
        $def_injectors = $definition->info_injector;
14006
 
        $custom_injectors = $injectors['Custom'];
14007
 
        unset($injectors['Custom']); // special case
14008
 
        foreach ($injectors as $injector => $b) {
14009
 
            $injector = "HTMLPurifier_Injector_$injector";
14010
 
            if (!$b) continue;
14011
 
            $this->injectors[] = new $injector;
14012
 
        }
14013
 
        foreach ($def_injectors as $injector) {
14014
 
            // assumed to be objects
14015
 
            $this->injectors[] = $injector;
14016
 
        }
14017
 
        foreach ($custom_injectors as $injector) {
14018
 
            if (is_string($injector)) {
14019
 
                $injector = "HTMLPurifier_Injector_$injector";
14020
 
                $injector = new $injector;
14021
 
            }
14022
 
            $this->injectors[] = $injector;
14023
 
        }
14024
 
        
14025
 
        // give the injectors references to the definition and context
14026
 
        // variables for performance reasons
14027
 
        foreach ($this->injectors as $ix => $injector) {
14028
 
            $error = $injector->prepare($config, $context);
14029
 
            if (!$error) continue;
14030
 
            array_splice($this->injectors, $ix, 1); // rm the injector
14031
 
            trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
14032
 
        }
14033
 
        
14034
 
        // -- end INJECTOR --
14035
 
        
14036
 
        // a note on punting:
14037
 
        //      In order to reduce code duplication, whenever some code needs
14038
 
        //      to make HTML changes in order to make things "correct", the
14039
 
        //      new HTML gets sent through the purifier, regardless of its
14040
 
        //      status. This means that if we add a start token, because it
14041
 
        //      was totally necessary, we don't have to update nesting; we just
14042
 
        //      punt ($reprocess = true; continue;) and it does that for us.
14043
 
        
14044
 
        // isset is in loop because $tokens size changes during loop exec
14045
 
        for (
14046
 
            $t = 0;
14047
 
            $t == 0 || isset($tokens[$t - 1]);
14048
 
            // only increment if we don't need to reprocess
14049
 
            $reprocess ? $reprocess = false : $t++
14050
 
        ) {
14051
 
            
14052
 
            // check for a rewind
14053
 
            if (is_int($i) && $i >= 0) {
14054
 
                // possibility: disable rewinding if the current token has a
14055
 
                // rewind set on it already. This would offer protection from
14056
 
                // infinite loop, but might hinder some advanced rewinding.
14057
 
                $rewind_to = $this->injectors[$i]->getRewind();
14058
 
                if (is_int($rewind_to) && $rewind_to < $t) {
14059
 
                    if ($rewind_to < 0) $rewind_to = 0;
14060
 
                    while ($t > $rewind_to) {
14061
 
                        $t--;
14062
 
                        $prev = $tokens[$t];
14063
 
                        // indicate that other injectors should not process this token,
14064
 
                        // but we need to reprocess it
14065
 
                        unset($prev->skip[$i]);
14066
 
                        $prev->rewind = $i;
14067
 
                        if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
14068
 
                        elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
14069
 
                    }
14070
 
                }
14071
 
                $i = false;
14072
 
            }
14073
 
            
14074
 
            // handle case of document end
14075
 
            if (!isset($tokens[$t])) {
14076
 
                // kill processing if stack is empty
14077
 
                if (empty($this->stack)) break;
14078
 
                
14079
 
                // peek
14080
 
                $top_nesting = array_pop($this->stack);
14081
 
                $this->stack[] = $top_nesting;
14082
 
                
14083
 
                // send error
14084
 
                if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
14085
 
                    $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
14086
 
                }
14087
 
                
14088
 
                // append, don't splice, since this is the end
14089
 
                $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
14090
 
                
14091
 
                // punt!
14092
 
                $reprocess = true;
14093
 
                continue;
14094
 
            }
14095
 
            
14096
 
            // if all goes well, this token will be passed through unharmed
14097
 
            $token = $tokens[$t];
14098
 
            
14099
 
            //echo '<hr>';
14100
 
            //printTokens($tokens, $t);
14101
 
            //var_dump($this->stack);
14102
 
            
14103
 
            // quick-check: if it's not a tag, no need to process
14104
 
            if (empty($token->is_tag)) {
14105
 
                if ($token instanceof HTMLPurifier_Token_Text) {
14106
 
                    foreach ($this->injectors as $i => $injector) {
14107
 
                        if (isset($token->skip[$i])) continue;
14108
 
                        if ($token->rewind !== null && $token->rewind !== $i) continue;
14109
 
                        $injector->handleText($token);
14110
 
                        $this->processToken($token, $i);
14111
 
                        $reprocess = true;
14112
 
                        break;
14113
 
                    }
14114
 
                }
14115
 
                // another possibility is a comment
14116
 
                continue;
14117
 
            }
14118
 
            
14119
 
            if (isset($definition->info[$token->name])) {
14120
 
                $type = $definition->info[$token->name]->child->type;
14121
 
            } else {
14122
 
                $type = false; // Type is unknown, treat accordingly
14123
 
            }
14124
 
            
14125
 
            // quick tag checks: anything that's *not* an end tag
14126
 
            $ok = false;
14127
 
            if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
14128
 
                // claims to be a start tag but is empty
14129
 
                $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
14130
 
                $ok = true;
14131
 
            } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
14132
 
                // claims to be empty but really is a start tag
14133
 
                $this->swap(new HTMLPurifier_Token_End($token->name));
14134
 
                $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
14135
 
                // punt (since we had to modify the input stream in a non-trivial way)
14136
 
                $reprocess = true;
14137
 
                continue;
14138
 
            } elseif ($token instanceof HTMLPurifier_Token_Empty) {
14139
 
                // real empty token
14140
 
                $ok = true;
14141
 
            } elseif ($token instanceof HTMLPurifier_Token_Start) {
14142
 
                // start tag
14143
 
                
14144
 
                // ...unless they also have to close their parent
14145
 
                if (!empty($this->stack)) {
14146
 
                    
14147
 
                    $parent = array_pop($this->stack);
14148
 
                    $this->stack[] = $parent;
14149
 
                    
14150
 
                    if (isset($definition->info[$parent->name])) {
14151
 
                        $elements = $definition->info[$parent->name]->child->getNonAutoCloseElements($config);
14152
 
                        $autoclose = !isset($elements[$token->name]);
14153
 
                    } else {
14154
 
                        $autoclose = false;
14155
 
                    }
14156
 
                    
14157
 
                    if ($autoclose) {
14158
 
                        if ($e) $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
14159
 
                        // insert parent end tag before this tag
14160
 
                        $new_token = new HTMLPurifier_Token_End($parent->name);
14161
 
                        $new_token->start = $parent;
14162
 
                        $this->insertBefore($new_token);
14163
 
                        $reprocess = true;
14164
 
                        continue;
14165
 
                    }
14166
 
                    
14167
 
                }
14168
 
                $ok = true;
14169
 
            }
14170
 
            
14171
 
            if ($ok) {
14172
 
                foreach ($this->injectors as $i => $injector) {
14173
 
                    if (isset($token->skip[$i])) continue;
14174
 
                    if ($token->rewind !== null && $token->rewind !== $i) continue;
14175
 
                    $injector->handleElement($token);
14176
 
                    $this->processToken($token, $i);
14177
 
                    $reprocess = true;
14178
 
                    break;
14179
 
                }
14180
 
                if (!$reprocess) {
14181
 
                    // ah, nothing interesting happened; do normal processing
14182
 
                    $this->swap($token);
14183
 
                    if ($token instanceof HTMLPurifier_Token_Start) {
14184
 
                        $this->stack[] = $token;
14185
 
                    } elseif ($token instanceof HTMLPurifier_Token_End) {
14186
 
                        throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
14187
 
                    }
14188
 
                }
14189
 
                continue;
14190
 
            }
14191
 
            
14192
 
            // sanity check: we should be dealing with a closing tag
14193
 
            if (!$token instanceof HTMLPurifier_Token_End) {
14194
 
                throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
14195
 
            }
14196
 
            
14197
 
            // make sure that we have something open
14198
 
            if (empty($this->stack)) {
14199
 
                if ($escape_invalid_tags) {
14200
 
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
14201
 
                    $this->swap(new HTMLPurifier_Token_Text(
14202
 
                        $generator->generateFromToken($token)
14203
 
                    ));
14204
 
                } else {
14205
 
                    $this->remove();
14206
 
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
14207
 
                }
14208
 
                $reprocess = true;
14209
 
                continue;
14210
 
            }
14211
 
            
14212
 
            // first, check for the simplest case: everything closes neatly.
14213
 
            // Eventually, everything passes through here; if there are problems
14214
 
            // we modify the input stream accordingly and then punt, so that
14215
 
            // the tokens get processed again.
14216
 
            $current_parent = array_pop($this->stack);
14217
 
            if ($current_parent->name == $token->name) {
14218
 
                $token->start = $current_parent;
14219
 
                foreach ($this->injectors as $i => $injector) {
14220
 
                    if (isset($token->skip[$i])) continue;
14221
 
                    if ($token->rewind !== null && $token->rewind !== $i) continue;
14222
 
                    $injector->handleEnd($token);
14223
 
                    $this->processToken($token, $i);
14224
 
                    $this->stack[] = $current_parent;
14225
 
                    $reprocess = true;
14226
 
                    break;
14227
 
                }
14228
 
                continue;
14229
 
            }
14230
 
            
14231
 
            // okay, so we're trying to close the wrong tag
14232
 
            
14233
 
            // undo the pop previous pop
14234
 
            $this->stack[] = $current_parent;
14235
 
            
14236
 
            // scroll back the entire nest, trying to find our tag.
14237
 
            // (feature could be to specify how far you'd like to go)
14238
 
            $size = count($this->stack);
14239
 
            // -2 because -1 is the last element, but we already checked that
14240
 
            $skipped_tags = false;
14241
 
            for ($j = $size - 2; $j >= 0; $j--) {
14242
 
                if ($this->stack[$j]->name == $token->name) {
14243
 
                    $skipped_tags = array_slice($this->stack, $j);
14244
 
                    break;
14245
 
                }
14246
 
            }
14247
 
            
14248
 
            // we didn't find the tag, so remove
14249
 
            if ($skipped_tags === false) {
14250
 
                if ($escape_invalid_tags) {
14251
 
                    $this->swap(new HTMLPurifier_Token_Text(
14252
 
                        $generator->generateFromToken($token)
14253
 
                    ));
14254
 
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
14255
 
                } else {
14256
 
                    $this->remove();
14257
 
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
14258
 
                }
14259
 
                $reprocess = true;
14260
 
                continue;
14261
 
            }
14262
 
            
14263
 
            // do errors, in REVERSE $j order: a,b,c with </a></b></c>
14264
 
            $c = count($skipped_tags);
14265
 
            if ($e) {
14266
 
                for ($j = $c - 1; $j > 0; $j--) {
14267
 
                    // notice we exclude $j == 0, i.e. the current ending tag, from
14268
 
                    // the errors...
14269
 
                    if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
14270
 
                        $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
14271
 
                    }
14272
 
                }
14273
 
            }
14274
 
            
14275
 
            // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
14276
 
            for ($j = 1; $j < $c; $j++) {
14277
 
                // ...as well as from the insertions
14278
 
                $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
14279
 
                $new_token->start = $skipped_tags[$j];
14280
 
                $this->insertBefore($new_token);
14281
 
            }
14282
 
            $reprocess = true;
14283
 
            continue;
14284
 
        }
14285
 
        
14286
 
        $context->destroy('CurrentNesting');
14287
 
        $context->destroy('InputTokens');
14288
 
        $context->destroy('InputIndex');
14289
 
        $context->destroy('CurrentToken');
14290
 
        
14291
 
        unset($this->injectors, $this->stack, $this->tokens, $this->t);
14292
 
        return $tokens;
14293
 
    }
14294
 
    
14295
 
    /**
14296
 
     * Processes arbitrary token values for complicated substitution patterns.
14297
 
     * In general:
14298
 
     * 
14299
 
     * If $token is an array, it is a list of tokens to substitute for the
14300
 
     * current token. These tokens then get individually processed. If there
14301
 
     * is a leading integer in the list, that integer determines how many
14302
 
     * tokens from the stream should be removed.
14303
 
     * 
14304
 
     * If $token is a regular token, it is swapped with the current token.
14305
 
     * 
14306
 
     * If $token is false, the current token is deleted.
14307
 
     * 
14308
 
     * If $token is an integer, that number of tokens (with the first token
14309
 
     * being the current one) will be deleted.
14310
 
     * 
14311
 
     * @param $token Token substitution value
14312
 
     * @param $injector Injector that performed the substitution; default is if
14313
 
     *        this is not an injector related operation.
14314
 
     */
14315
 
    protected function processToken($token, $injector = -1) {
14316
 
        
14317
 
        // normalize forms of token
14318
 
        if (is_object($token)) $token = array(1, $token);
14319
 
        if (is_int($token))    $token = array($token);
14320
 
        if ($token === false)  $token = array(1);
14321
 
        if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
14322
 
        if (!is_int($token[0])) array_unshift($token, 1);
14323
 
        if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
14324
 
        
14325
 
        // $token is now an array with the following form:
14326
 
        // array(number nodes to delete, new node 1, new node 2, ...)
14327
 
        
14328
 
        $delete = array_shift($token);
14329
 
        $old = array_splice($this->tokens, $this->t, $delete, $token);
14330
 
        
14331
 
        if ($injector > -1) {
14332
 
            // determine appropriate skips
14333
 
            $oldskip = isset($old[0]) ? $old[0]->skip : array();
14334
 
            foreach ($token as $object) {
14335
 
                $object->skip = $oldskip;
14336
 
                $object->skip[$injector] = true;
14337
 
            }
14338
 
        }
14339
 
        
14340
 
    }
14341
 
    
14342
 
    /**
14343
 
     * Inserts a token before the current token. Cursor now points to this token
14344
 
     */
14345
 
    private function insertBefore($token) {
14346
 
        array_splice($this->tokens, $this->t, 0, array($token));
14347
 
    }
14348
 
 
14349
 
    /**
14350
 
     * Removes current token. Cursor now points to new token occupying previously
14351
 
     * occupied space.
14352
 
     */
14353
 
    private function remove() {
14354
 
        array_splice($this->tokens, $this->t, 1);
14355
 
    }
14356
 
    
14357
 
    /**
14358
 
     * Swap current token with new token. Cursor points to new token (no change).
14359
 
     */
14360
 
    private function swap($token) {
14361
 
        $this->tokens[$this->t] = $token;
14362
 
    }
14363
 
    
14364
 
}
14365
 
 
14366
 
 
14367
 
 
14368
 
 
14369
 
/**
14370
 
 * Removes all unrecognized tags from the list of tokens.
14371
 
 * 
14372
 
 * This strategy iterates through all the tokens and removes unrecognized
14373
 
 * tokens. If a token is not recognized but a TagTransform is defined for
14374
 
 * that element, the element will be transformed accordingly.
14375
 
 */
14376
 
 
14377
 
class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
14378
 
{
14379
 
    
14380
 
    public function execute($tokens, $config, $context) {
14381
 
        $definition = $config->getHTMLDefinition();
14382
 
        $generator = new HTMLPurifier_Generator($config, $context);
14383
 
        $result = array();
14384
 
        
14385
 
        $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags');
14386
 
        $remove_invalid_img  = $config->get('Core', 'RemoveInvalidImg');
14387
 
        
14388
 
        // currently only used to determine if comments should be kept
14389
 
        $trusted = $config->get('HTML', 'Trusted');
14390
 
        
14391
 
        $remove_script_contents = $config->get('Core', 'RemoveScriptContents');
14392
 
        $hidden_elements     = $config->get('Core', 'HiddenElements');
14393
 
        
14394
 
        // remove script contents compatibility
14395
 
        if ($remove_script_contents === true) {
14396
 
            $hidden_elements['script'] = true;
14397
 
        } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) {
14398
 
            unset($hidden_elements['script']);
14399
 
        }
14400
 
        
14401
 
        $attr_validator = new HTMLPurifier_AttrValidator();
14402
 
        
14403
 
        // removes tokens until it reaches a closing tag with its value
14404
 
        $remove_until = false;
14405
 
        
14406
 
        // converts comments into text tokens when this is equal to a tag name
14407
 
        $textify_comments = false;
14408
 
        
14409
 
        $token = false;
14410
 
        $context->register('CurrentToken', $token);
14411
 
        
14412
 
        $e = false;
14413
 
        if ($config->get('Core', 'CollectErrors')) {
14414
 
            $e =& $context->get('ErrorCollector');
14415
 
        }
14416
 
        
14417
 
        foreach($tokens as $token) {
14418
 
            if ($remove_until) {
14419
 
                if (empty($token->is_tag) || $token->name !== $remove_until) {
14420
 
                    continue;
14421
 
                }
14422
 
            }
14423
 
            if (!empty( $token->is_tag )) {
14424
 
                // DEFINITION CALL
14425
 
                
14426
 
                // before any processing, try to transform the element
14427
 
                if (
14428
 
                    isset($definition->info_tag_transform[$token->name])
14429
 
                ) {
14430
 
                    $original_name = $token->name;
14431
 
                    // there is a transformation for this tag
14432
 
                    // DEFINITION CALL
14433
 
                    $token = $definition->
14434
 
                                info_tag_transform[$token->name]->
14435
 
                                    transform($token, $config, $context);
14436
 
                    if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name);
14437
 
                }
14438
 
                
14439
 
                if (isset($definition->info[$token->name])) {
14440
 
                    
14441
 
                    // mostly everything's good, but
14442
 
                    // we need to make sure required attributes are in order
14443
 
                    if (
14444
 
                        ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) &&
14445
 
                        $definition->info[$token->name]->required_attr &&
14446
 
                        ($token->name != 'img' || $remove_invalid_img) // ensure config option still works
14447
 
                    ) {
14448
 
                        $attr_validator->validateToken($token, $config, $context);
14449
 
                        $ok = true;
14450
 
                        foreach ($definition->info[$token->name]->required_attr as $name) {
14451
 
                            if (!isset($token->attr[$name])) {
14452
 
                                $ok = false;
14453
 
                                break;
14454
 
                            }
14455
 
                        }
14456
 
                        if (!$ok) {
14457
 
                            if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name);
14458
 
                            continue;
14459
 
                        }
14460
 
                        $token->armor['ValidateAttributes'] = true;
14461
 
                    }
14462
 
                    
14463
 
                    if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
14464
 
                        $textify_comments = $token->name;
14465
 
                    } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) {
14466
 
                        $textify_comments = false;
14467
 
                    }
14468
 
                    
14469
 
                } elseif ($escape_invalid_tags) {
14470
 
                    // invalid tag, generate HTML representation and insert in
14471
 
                    if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text');
14472
 
                    $token = new HTMLPurifier_Token_Text(
14473
 
                        $generator->generateFromToken($token)
14474
 
                    );
14475
 
                } else {
14476
 
                    // check if we need to destroy all of the tag's children
14477
 
                    // CAN BE GENERICIZED
14478
 
                    if (isset($hidden_elements[$token->name])) {
14479
 
                        if ($token instanceof HTMLPurifier_Token_Start) {
14480
 
                            $remove_until = $token->name;
14481
 
                        } elseif ($token instanceof HTMLPurifier_Token_Empty) {
14482
 
                            // do nothing: we're still looking
14483
 
                        } else {
14484
 
                            $remove_until = false;
14485
 
                        }
14486
 
                        if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed');
14487
 
                    } else {
14488
 
                        if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed');
14489
 
                    }
14490
 
                    continue;
14491
 
                }
14492
 
            } elseif ($token instanceof HTMLPurifier_Token_Comment) {
14493
 
                // textify comments in script tags when they are allowed
14494
 
                if ($textify_comments !== false) {
14495
 
                    $data = $token->data;
14496
 
                    $token = new HTMLPurifier_Token_Text($data);
14497
 
                } elseif ($trusted) {
14498
 
                    // keep, but perform comment cleaning
14499
 
                    if ($e) {
14500
 
                        // perform check whether or not there's a trailing hyphen
14501
 
                        if (substr($token->data, -1) == '-') {
14502
 
                            $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
14503
 
                        }
14504
 
                    }
14505
 
                    $token->data = rtrim($token->data, '-');
14506
 
                    $found_double_hyphen = false;
14507
 
                    while (strpos($token->data, '--') !== false) {
14508
 
                        if ($e && !$found_double_hyphen) {
14509
 
                            $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
14510
 
                        }
14511
 
                        $found_double_hyphen = true; // prevent double-erroring
14512
 
                        $token->data = str_replace('--', '-', $token->data);
14513
 
                    }
14514
 
                } else {
14515
 
                    // strip comments
14516
 
                    if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
14517
 
                    continue;
14518
 
                }
14519
 
            } elseif ($token instanceof HTMLPurifier_Token_Text) {
14520
 
            } else {
14521
 
                continue;
14522
 
            }
14523
 
            $result[] = $token;
14524
 
        }
14525
 
        if ($remove_until && $e) {
14526
 
            // we removed tokens until the end, throw error
14527
 
            $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until);
14528
 
        }
14529
 
        
14530
 
        $context->destroy('CurrentToken');
14531
 
        
14532
 
        return $result;
14533
 
    }
14534
 
    
14535
 
}
14536
 
 
14537
 
 
14538
 
 
14539
 
 
14540
 
/**
14541
 
 * Validate all attributes in the tokens.
14542
 
 */
14543
 
 
14544
 
class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
14545
 
{
14546
 
    
14547
 
    public function execute($tokens, $config, $context) {
14548
 
        
14549
 
        // setup validator
14550
 
        $validator = new HTMLPurifier_AttrValidator();
14551
 
        
14552
 
        $token = false;
14553
 
        $context->register('CurrentToken', $token);
14554
 
        
14555
 
        foreach ($tokens as $key => $token) {
14556
 
            
14557
 
            // only process tokens that have attributes,
14558
 
            //   namely start and empty tags
14559
 
            if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue;
14560
 
            
14561
 
            // skip tokens that are armored
14562
 
            if (!empty($token->armor['ValidateAttributes'])) continue;
14563
 
            
14564
 
            // note that we have no facilities here for removing tokens
14565
 
            $validator->validateToken($token, $config, $context);
14566
 
            
14567
 
            $tokens[$key] = $token; // for PHP 4
14568
 
        }
14569
 
        $context->destroy('CurrentToken');
14570
 
        
14571
 
        return $tokens;
14572
 
    }
14573
 
    
14574
 
}
14575
 
 
14576
 
 
14577
 
 
14578
 
 
14579
 
/**
14580
 
 * Transforms FONT tags to the proper form (SPAN with CSS styling)
14581
 
 * 
14582
 
 * This transformation takes the three proprietary attributes of FONT and
14583
 
 * transforms them into their corresponding CSS attributes.  These are color,
14584
 
 * face, and size.
14585
 
 * 
14586
 
 * @note Size is an interesting case because it doesn't map cleanly to CSS.
14587
 
 *       Thanks to
14588
 
 *       http://style.cleverchimp.com/font_size_intervals/altintervals.html
14589
 
 *       for reasonable mappings.
14590
 
 */
14591
 
class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
14592
 
{
14593
 
    
14594
 
    public $transform_to = 'span';
14595
 
    
14596
 
    protected $_size_lookup = array(
14597
 
        '0' => 'xx-small',
14598
 
        '1' => 'xx-small',
14599
 
        '2' => 'small',
14600
 
        '3' => 'medium',
14601
 
        '4' => 'large',
14602
 
        '5' => 'x-large',
14603
 
        '6' => 'xx-large',
14604
 
        '7' => '300%',
14605
 
        '-1' => 'smaller',
14606
 
        '-2' => '60%',
14607
 
        '+1' => 'larger',
14608
 
        '+2' => '150%',
14609
 
        '+3' => '200%',
14610
 
        '+4' => '300%'
14611
 
    );
14612
 
    
14613
 
    public function transform($tag, $config, $context) {
14614
 
        
14615
 
        if ($tag instanceof HTMLPurifier_Token_End) {
14616
 
            $new_tag = clone $tag;
14617
 
            $new_tag->name = $this->transform_to;
14618
 
            return $new_tag;
14619
 
        }
14620
 
        
14621
 
        $attr = $tag->attr;
14622
 
        $prepend_style = '';
14623
 
        
14624
 
        // handle color transform
14625
 
        if (isset($attr['color'])) {
14626
 
            $prepend_style .= 'color:' . $attr['color'] . ';';
14627
 
            unset($attr['color']);
14628
 
        }
14629
 
        
14630
 
        // handle face transform
14631
 
        if (isset($attr['face'])) {
14632
 
            $prepend_style .= 'font-family:' . $attr['face'] . ';';
14633
 
            unset($attr['face']);
14634
 
        }
14635
 
        
14636
 
        // handle size transform
14637
 
        if (isset($attr['size'])) {
14638
 
            // normalize large numbers
14639
 
            if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
14640
 
                $size = (int) $attr['size'];
14641
 
                if ($size < -2) $attr['size'] = '-2';
14642
 
                if ($size > 4)  $attr['size'] = '+4';
14643
 
            } else {
14644
 
                $size = (int) $attr['size'];
14645
 
                if ($size > 7) $attr['size'] = '7';
14646
 
            }
14647
 
            if (isset($this->_size_lookup[$attr['size']])) {
14648
 
                $prepend_style .= 'font-size:' .
14649
 
                  $this->_size_lookup[$attr['size']] . ';';
14650
 
            }
14651
 
            unset($attr['size']);
14652
 
        }
14653
 
        
14654
 
        if ($prepend_style) {
14655
 
            $attr['style'] = isset($attr['style']) ?
14656
 
                $prepend_style . $attr['style'] :
14657
 
                $prepend_style;
14658
 
        }
14659
 
        
14660
 
        $new_tag = clone $tag;
14661
 
        $new_tag->name = $this->transform_to;
14662
 
        $new_tag->attr = $attr;
14663
 
        
14664
 
        return $new_tag;
14665
 
        
14666
 
    }
14667
 
}
14668
 
 
14669
 
 
14670
 
 
14671
 
 
14672
 
/**
14673
 
 * Simple transformation, just change tag name to something else,
14674
 
 * and possibly add some styling. This will cover most of the deprecated
14675
 
 * tag cases.
14676
 
 */
14677
 
class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform
14678
 
{
14679
 
    
14680
 
    protected $style;
14681
 
    
14682
 
    /**
14683
 
     * @param $transform_to Tag name to transform to.
14684
 
     * @param $style CSS style to add to the tag
14685
 
     */
14686
 
    public function __construct($transform_to, $style = null) {
14687
 
        $this->transform_to = $transform_to;
14688
 
        $this->style = $style;
14689
 
    }
14690
 
    
14691
 
    public function transform($tag, $config, $context) {
14692
 
        $new_tag = clone $tag;
14693
 
        $new_tag->name = $this->transform_to;
14694
 
        if (!is_null($this->style) &&
14695
 
            ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
14696
 
        ) {
14697
 
            $this->prependCSS($new_tag->attr, $this->style);
14698
 
        }
14699
 
        return $new_tag;
14700
 
    }
14701
 
    
14702
 
}
14703
 
 
14704
 
 
14705
 
 
14706
 
 
14707
 
/**
14708
 
 * Concrete comment token class. Generally will be ignored.
14709
 
 */
14710
 
class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
14711
 
{
14712
 
    public $data; /**< Character data within comment. */
14713
 
    /**
14714
 
     * Transparent constructor.
14715
 
     * 
14716
 
     * @param $data String comment data.
14717
 
     */
14718
 
    public function __construct($data, $line = null, $col = null) {
14719
 
        $this->data = $data;
14720
 
        $this->line = $line;
14721
 
        $this->col  = $col;
14722
 
    }
14723
 
}
14724
 
 
14725
 
 
14726
 
 
14727
 
 
14728
 
/**
14729
 
 * Abstract class of a tag token (start, end or empty), and its behavior.
14730
 
 */
14731
 
class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
14732
 
{
14733
 
    /**
14734
 
     * Static bool marker that indicates the class is a tag.
14735
 
     * 
14736
 
     * This allows us to check objects with <tt>!empty($obj->is_tag)</tt>
14737
 
     * without having to use a function call <tt>is_a()</tt>.
14738
 
     */
14739
 
    public $is_tag = true;
14740
 
    
14741
 
    /**
14742
 
     * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
14743
 
     * 
14744
 
     * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
14745
 
     * be lower-casing them, but these tokens cater to HTML tags, which are
14746
 
     * insensitive.
14747
 
     */
14748
 
    public $name;
14749
 
    
14750
 
    /**
14751
 
     * Associative array of the tag's attributes.
14752
 
     */
14753
 
    public $attr = array();
14754
 
    
14755
 
    /**
14756
 
     * Non-overloaded constructor, which lower-cases passed tag name.
14757
 
     * 
14758
 
     * @param $name String name.
14759
 
     * @param $attr Associative array of attributes.
14760
 
     */
14761
 
    public function __construct($name, $attr = array(), $line = null, $col = null) {
14762
 
        $this->name = ctype_lower($name) ? $name : strtolower($name);
14763
 
        foreach ($attr as $key => $value) {
14764
 
            // normalization only necessary when key is not lowercase
14765
 
            if (!ctype_lower($key)) {
14766
 
                $new_key = strtolower($key);
14767
 
                if (!isset($attr[$new_key])) {
14768
 
                    $attr[$new_key] = $attr[$key];
14769
 
                }
14770
 
                if ($new_key !== $key) {
14771
 
                    unset($attr[$key]);
14772
 
                }
14773
 
            }
14774
 
        }
14775
 
        $this->attr = $attr;
14776
 
        $this->line = $line;
14777
 
        $this->col  = $col;
14778
 
    }
14779
 
}
14780
 
 
14781
 
 
14782
 
 
14783
 
/**
14784
 
 * Concrete empty token class.
14785
 
 */
14786
 
class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
14787
 
{
14788
 
    
14789
 
}
14790
 
 
14791
 
 
14792
 
 
14793
 
/**
14794
 
 * Concrete end token class.
14795
 
 * 
14796
 
 * @warning This class accepts attributes even though end tags cannot. This
14797
 
 * is for optimization reasons, as under normal circumstances, the Lexers
14798
 
 * do not pass attributes.
14799
 
 */
14800
 
class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag
14801
 
{
14802
 
    /**
14803
 
     * Token that started this node. Added by MakeWellFormed. Please
14804
 
     * do not edit this!
14805
 
     */
14806
 
    public $start;
14807
 
}
14808
 
 
14809
 
 
14810
 
 
14811
 
/**
14812
 
 * Concrete start token class.
14813
 
 */
14814
 
class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
14815
 
{
14816
 
    
14817
 
}
14818
 
 
14819
 
 
14820
 
 
14821
 
/**
14822
 
 * Concrete text token class.
14823
 
 * 
14824
 
 * Text tokens comprise of regular parsed character data (PCDATA) and raw
14825
 
 * character data (from the CDATA sections). Internally, their
14826
 
 * data is parsed with all entities expanded. Surprisingly, the text token
14827
 
 * does have a "tag name" called #PCDATA, which is how the DTD represents it
14828
 
 * in permissible child nodes.
14829
 
 */
14830
 
class HTMLPurifier_Token_Text extends HTMLPurifier_Token
14831
 
{
14832
 
    
14833
 
    public $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. */
14834
 
    public $data; /**< Parsed character data of text. */
14835
 
    public $is_whitespace; /**< Bool indicating if node is whitespace. */
14836
 
    
14837
 
    /**
14838
 
     * Constructor, accepts data and determines if it is whitespace.
14839
 
     * 
14840
 
     * @param $data String parsed character data.
14841
 
     */
14842
 
    public function __construct($data, $line = null, $col = null) {
14843
 
        $this->data = $data;
14844
 
        $this->is_whitespace = ctype_space($data);
14845
 
        $this->line = $line;
14846
 
        $this->col  = $col;
14847
 
    }
14848
 
    
14849
 
}
14850
 
 
14851
 
 
14852
 
 
14853
 
class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
14854
 
{
14855
 
    public $name = 'DisableExternal';
14856
 
    protected $ourHostParts = false;
14857
 
    public function prepare($config) {
14858
 
        $our_host = $config->getDefinition('URI')->host;
14859
 
        if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host));
14860
 
    }
14861
 
    public function filter(&$uri, $config, $context) {
14862
 
        if (is_null($uri->host)) return true;
14863
 
        if ($this->ourHostParts === false) return false;
14864
 
        $host_parts = array_reverse(explode('.', $uri->host));
14865
 
        foreach ($this->ourHostParts as $i => $x) {
14866
 
            if (!isset($host_parts[$i])) return false;
14867
 
            if ($host_parts[$i] != $this->ourHostParts[$i]) return false;
14868
 
        }
14869
 
        return true;
14870
 
    }
14871
 
}
14872
 
 
14873
 
 
14874
 
 
14875
 
 
14876
 
class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal
14877
 
{
14878
 
    public $name = 'DisableExternalResources';
14879
 
    public function filter(&$uri, $config, $context) {
14880
 
        if (!$context->get('EmbeddedURI', true)) return true;
14881
 
        return parent::filter($uri, $config, $context);
14882
 
    }
14883
 
}
14884
 
 
14885
 
 
14886
 
 
14887
 
 
14888
 
class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
14889
 
{
14890
 
    public $name = 'HostBlacklist';
14891
 
    protected $blacklist = array();
14892
 
    public function prepare($config) {
14893
 
        $this->blacklist = $config->get('URI', 'HostBlacklist');
14894
 
        return true;
14895
 
    }
14896
 
    public function filter(&$uri, $config, $context) {
14897
 
        foreach($this->blacklist as $blacklisted_host_fragment) {
14898
 
            if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
14899
 
                return false;
14900
 
            }
14901
 
        }
14902
 
        return true;
14903
 
    }
14904
 
}
14905
 
 
14906
 
 
14907
 
 
14908
 
// does not support network paths
14909
 
 
14910
 
class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
14911
 
{
14912
 
    public $name = 'MakeAbsolute';
14913
 
    protected $base;
14914
 
    protected $basePathStack = array();
14915
 
    public function prepare($config) {
14916
 
        $def = $config->getDefinition('URI');
14917
 
        $this->base = $def->base;
14918
 
        if (is_null($this->base)) {
14919
 
            trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
14920
 
            return false;
14921
 
        }
14922
 
        $this->base->fragment = null; // fragment is invalid for base URI
14923
 
        $stack = explode('/', $this->base->path);
14924
 
        array_pop($stack); // discard last segment
14925
 
        $stack = $this->_collapseStack($stack); // do pre-parsing
14926
 
        $this->basePathStack = $stack;
14927
 
        return true;
14928
 
    }
14929
 
    public function filter(&$uri, $config, $context) {
14930
 
        if (is_null($this->base)) return true; // abort early
14931
 
        if (
14932
 
            $uri->path === '' && is_null($uri->scheme) &&
14933
 
            is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
14934
 
        ) {
14935
 
            // reference to current document
14936
 
            $uri = clone $this->base;
14937
 
            return true;
14938
 
        }
14939
 
        if (!is_null($uri->scheme)) {
14940
 
            // absolute URI already: don't change
14941
 
            if (!is_null($uri->host)) return true;
14942
 
            $scheme_obj = $uri->getSchemeObj($config, $context);
14943
 
            if (!$scheme_obj) {
14944
 
                // scheme not recognized
14945
 
                return false;
14946
 
            }
14947
 
            if (!$scheme_obj->hierarchical) {
14948
 
                // non-hierarchal URI with explicit scheme, don't change
14949
 
                return true;
14950
 
            }
14951
 
            // special case: had a scheme but always is hierarchical and had no authority
14952
 
        }
14953
 
        if (!is_null($uri->host)) {
14954
 
            // network path, don't bother
14955
 
            return true;
14956
 
        }
14957
 
        if ($uri->path === '') {
14958
 
            $uri->path = $this->base->path;
14959
 
        } elseif ($uri->path[0] !== '/') {
14960
 
            // relative path, needs more complicated processing
14961
 
            $stack = explode('/', $uri->path);
14962
 
            $new_stack = array_merge($this->basePathStack, $stack);
14963
 
            if ($new_stack[0] !== '' && !is_null($this->base->host)) {
14964
 
                array_unshift($new_stack, '');
14965
 
            }
14966
 
            $new_stack = $this->_collapseStack($new_stack);
14967
 
            $uri->path = implode('/', $new_stack);
14968
 
        } else {
14969
 
            // absolute path, but still we should collapse
14970
 
            $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path)));
14971
 
        }
14972
 
        // re-combine
14973
 
        $uri->scheme = $this->base->scheme;
14974
 
        if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo;
14975
 
        if (is_null($uri->host))     $uri->host     = $this->base->host;
14976
 
        if (is_null($uri->port))     $uri->port     = $this->base->port;
14977
 
        return true;
14978
 
    }
14979
 
    
14980
 
    /**
14981
 
     * Resolve dots and double-dots in a path stack
14982
 
     */
14983
 
    private function _collapseStack($stack) {
14984
 
        $result = array();
14985
 
        $is_folder = false;
14986
 
        for ($i = 0; isset($stack[$i]); $i++) {
14987
 
            $is_folder = false;
14988
 
            // absorb an internally duplicated slash
14989
 
            if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue;
14990
 
            if ($stack[$i] == '..') {
14991
 
                if (!empty($result)) {
14992
 
                    $segment = array_pop($result);
14993
 
                    if ($segment === '' && empty($result)) {
14994
 
                        // error case: attempted to back out too far:
14995
 
                        // restore the leading slash
14996
 
                        $result[] = '';
14997
 
                    } elseif ($segment === '..') {
14998
 
                        $result[] = '..'; // cannot remove .. with ..
14999
 
                    }
15000
 
                } else {
15001
 
                    // relative path, preserve the double-dots
15002
 
                    $result[] = '..';
15003
 
                }
15004
 
                $is_folder = true;
15005
 
                continue;
15006
 
            }
15007
 
            if ($stack[$i] == '.') {
15008
 
                // silently absorb
15009
 
                $is_folder = true;
15010
 
                continue;
15011
 
            }
15012
 
            $result[] = $stack[$i];
15013
 
        }
15014
 
        if ($is_folder) $result[] = '';
15015
 
        return $result;
15016
 
    }
15017
 
}
15018
 
 
15019
 
 
15020
 
 
15021
 
 
15022
 
class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
15023
 
{
15024
 
    public $name = 'Munge';
15025
 
    public $post = true;
15026
 
    private $target, $parser, $doEmbed, $secretKey;
15027
 
    
15028
 
    protected $replace = array();
15029
 
    
15030
 
    public function prepare($config) {
15031
 
        $this->target    = $config->get('URI', $this->name);
15032
 
        $this->parser    = new HTMLPurifier_URIParser();
15033
 
        $this->doEmbed   = $config->get('URI', 'MungeResources');
15034
 
        $this->secretKey = $config->get('URI', 'MungeSecretKey');
15035
 
        return true;
15036
 
    }
15037
 
    public function filter(&$uri, $config, $context) {
15038
 
        if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true;
15039
 
        
15040
 
        $scheme_obj = $uri->getSchemeObj($config, $context);
15041
 
        if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
15042
 
        if (is_null($uri->host) || empty($scheme_obj->browsable)) {
15043
 
            return true;
15044
 
        }
15045
 
        
15046
 
        $this->makeReplace($uri, $config, $context);
15047
 
        $this->replace = array_map('rawurlencode', $this->replace);
15048
 
        
15049
 
        $new_uri = strtr($this->target, $this->replace);
15050
 
        $new_uri = $this->parser->parse($new_uri);
15051
 
        // don't redirect if the target host is the same as the 
15052
 
        // starting host
15053
 
        if ($uri->host === $new_uri->host) return true;
15054
 
        $uri = $new_uri; // overwrite
15055
 
        return true;
15056
 
    }
15057
 
    
15058
 
    protected function makeReplace($uri, $config, $context) {
15059
 
        $string = $uri->toString();
15060
 
        // always available
15061
 
        $this->replace['%s'] = $string;
15062
 
        $this->replace['%r'] = $context->get('EmbeddedURI', true);
15063
 
        $token = $context->get('CurrentToken', true);
15064
 
        $this->replace['%n'] = $token ? $token->name : null;
15065
 
        $this->replace['%m'] = $context->get('CurrentAttr', true);
15066
 
        $this->replace['%p'] = $context->get('CurrentCSSProperty', true);
15067
 
        // not always available
15068
 
        if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
15069
 
    }
15070
 
    
15071
 
}
15072
 
 
15073
 
 
15074
 
 
15075
 
/**
15076
 
 * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738.
15077
 
 */
15078
 
class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
15079
 
    
15080
 
    public $default_port = 21;
15081
 
    public $browsable = true; // usually
15082
 
    public $hierarchical = true;
15083
 
    
15084
 
    public function validate(&$uri, $config, $context) {
15085
 
        parent::validate($uri, $config, $context);
15086
 
        $uri->query    = null;
15087
 
        
15088
 
        // typecode check
15089
 
        $semicolon_pos = strrpos($uri->path, ';'); // reverse
15090
 
        if ($semicolon_pos !== false) {
15091
 
            $type = substr($uri->path, $semicolon_pos + 1); // no semicolon
15092
 
            $uri->path = substr($uri->path, 0, $semicolon_pos);
15093
 
            $type_ret = '';
15094
 
            if (strpos($type, '=') !== false) {
15095
 
                // figure out whether or not the declaration is correct
15096
 
                list($key, $typecode) = explode('=', $type, 2);
15097
 
                if ($key !== 'type') {
15098
 
                    // invalid key, tack it back on encoded
15099
 
                    $uri->path .= '%3B' . $type;
15100
 
                } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') {
15101
 
                    $type_ret = ";type=$typecode";
15102
 
                }
15103
 
            } else {
15104
 
                $uri->path .= '%3B' . $type;
15105
 
            }
15106
 
            $uri->path = str_replace(';', '%3B', $uri->path);
15107
 
            $uri->path .= $type_ret;
15108
 
        }
15109
 
        
15110
 
        return true;
15111
 
    }
15112
 
    
15113
 
}
15114
 
 
15115
 
 
15116
 
 
15117
 
 
15118
 
/**
15119
 
 * Validates http (HyperText Transfer Protocol) as defined by RFC 2616
15120
 
 */
15121
 
class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
15122
 
    
15123
 
    public $default_port = 80;
15124
 
    public $browsable = true;
15125
 
    public $hierarchical = true;
15126
 
    
15127
 
    public function validate(&$uri, $config, $context) {
15128
 
        parent::validate($uri, $config, $context);
15129
 
        $uri->userinfo = null;
15130
 
        return true;
15131
 
    }
15132
 
    
15133
 
}
15134
 
 
15135
 
 
15136
 
 
15137
 
 
15138
 
/**
15139
 
 * Validates https (Secure HTTP) according to http scheme.
15140
 
 */
15141
 
class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http {
15142
 
    
15143
 
    public $default_port = 443;
15144
 
    
15145
 
}
15146
 
 
15147
 
 
15148
 
 
15149
 
 
15150
 
// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the
15151
 
// email is valid, but be careful!
15152
 
 
15153
 
/**
15154
 
 * Validates mailto (for E-mail) according to RFC 2368
15155
 
 * @todo Validate the email address
15156
 
 * @todo Filter allowed query parameters
15157
 
 */
15158
 
 
15159
 
class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
15160
 
    
15161
 
    public $browsable = false;
15162
 
    
15163
 
    public function validate(&$uri, $config, $context) {
15164
 
        parent::validate($uri, $config, $context);
15165
 
        $uri->userinfo = null;
15166
 
        $uri->host     = null;
15167
 
        $uri->port     = null;
15168
 
        // we need to validate path against RFC 2368's addr-spec
15169
 
        return true;
15170
 
    }
15171
 
    
15172
 
}
15173
 
 
15174
 
 
15175
 
 
15176
 
 
15177
 
/**
15178
 
 * Validates news (Usenet) as defined by generic RFC 1738
15179
 
 */
15180
 
class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
15181
 
    
15182
 
    public $browsable = false;
15183
 
    
15184
 
    public function validate(&$uri, $config, $context) {
15185
 
        parent::validate($uri, $config, $context);
15186
 
        $uri->userinfo = null;
15187
 
        $uri->host     = null;
15188
 
        $uri->port     = null;
15189
 
        $uri->query    = null;
15190
 
        // typecode check needed on path
15191
 
        return true;
15192
 
    }
15193
 
    
15194
 
}
15195
 
 
15196
 
 
15197
 
 
15198
 
 
15199
 
/**
15200
 
 * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738
15201
 
 */
15202
 
class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
15203
 
    
15204
 
    public $default_port = 119;
15205
 
    public $browsable = false;
15206
 
    
15207
 
    public function validate(&$uri, $config, $context) {
15208
 
        parent::validate($uri, $config, $context);
15209
 
        $uri->userinfo = null;
15210
 
        $uri->query    = null;
15211
 
        return true;
15212
 
    }
15213
 
    
15214
 
}
15215
 
 
15216
 
 
15217
 
 
15218
 
 
15219
 
/**
15220
 
 * Performs safe variable parsing based on types which can be used by
15221
 
 * users. This may not be able to represent all possible data inputs,
15222
 
 * however.
15223
 
 */
15224
 
class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser
15225
 
{
15226
 
    
15227
 
    protected function parseImplementation($var, $type, $allow_null) {
15228
 
        if ($allow_null && $var === null) return null;
15229
 
        switch ($type) {
15230
 
            // Note: if code "breaks" from the switch, it triggers a generic
15231
 
            // exception to be thrown. Specific errors can be specifically
15232
 
            // done here.
15233
 
            case self::MIXED :
15234
 
            case self::ISTRING :
15235
 
            case self::STRING :
15236
 
            case self::TEXT :
15237
 
            case self::ITEXT :
15238
 
                return $var;
15239
 
            case self::INT :
15240
 
                if (is_string($var) && ctype_digit($var)) $var = (int) $var;
15241
 
                return $var;
15242
 
            case self::FLOAT :
15243
 
                if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var;
15244
 
                return $var;
15245
 
            case self::BOOL :
15246
 
                if (is_int($var) && ($var === 0 || $var === 1)) {
15247
 
                    $var = (bool) $var;
15248
 
                } elseif (is_string($var)) {
15249
 
                    if ($var == 'on' || $var == 'true' || $var == '1') {
15250
 
                        $var = true;
15251
 
                    } elseif ($var == 'off' || $var == 'false' || $var == '0') {
15252
 
                        $var = false;
15253
 
                    } else {
15254
 
                        throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type");
15255
 
                    }
15256
 
                }
15257
 
                return $var;
15258
 
            case self::ALIST :
15259
 
            case self::HASH :
15260
 
            case self::LOOKUP :
15261
 
                if (is_string($var)) {
15262
 
                    // special case: technically, this is an array with
15263
 
                    // a single empty string item, but having an empty
15264
 
                    // array is more intuitive
15265
 
                    if ($var == '') return array();
15266
 
                    if (strpos($var, "\n") === false && strpos($var, "\r") === false) {
15267
 
                        // simplistic string to array method that only works
15268
 
                        // for simple lists of tag names or alphanumeric characters
15269
 
                        $var = explode(',',$var);
15270
 
                    } else {
15271
 
                        $var = preg_split('/(,|[\n\r]+)/', $var);
15272
 
                    }
15273
 
                    // remove spaces
15274
 
                    foreach ($var as $i => $j) $var[$i] = trim($j);
15275
 
                    if ($type === self::HASH) {
15276
 
                        // key:value,key2:value2
15277
 
                        $nvar = array();
15278
 
                        foreach ($var as $keypair) {
15279
 
                            $c = explode(':', $keypair, 2);
15280
 
                            if (!isset($c[1])) continue;
15281
 
                            $nvar[$c[0]] = $c[1];
15282
 
                        }
15283
 
                        $var = $nvar;
15284
 
                    }
15285
 
                }
15286
 
                if (!is_array($var)) break;
15287
 
                $keys = array_keys($var);
15288
 
                if ($keys === array_keys($keys)) {
15289
 
                    if ($type == self::ALIST) return $var;
15290
 
                    elseif ($type == self::LOOKUP) {
15291
 
                        $new = array();
15292
 
                        foreach ($var as $key) {
15293
 
                            $new[$key] = true;
15294
 
                        }
15295
 
                        return $new;
15296
 
                    } else break;
15297
 
                }
15298
 
                if ($type === self::LOOKUP) {
15299
 
                    foreach ($var as $key => $value) {
15300
 
                        $var[$key] = true;
15301
 
                    }
15302
 
                }
15303
 
                return $var;
15304
 
            default:
15305
 
                $this->errorInconsistent(__CLASS__, $type);
15306
 
        }
15307
 
        $this->errorGeneric($var, $type);
15308
 
    }
15309
 
    
15310
 
}
15311
 
 
15312
 
 
15313
 
 
15314
 
/**
15315
 
 * This variable parser uses PHP's internal code engine. Because it does
15316
 
 * this, it can represent all inputs; however, it is dangerous and cannot
15317
 
 * be used by users.
15318
 
 */
15319
 
class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser
15320
 
{
15321
 
    
15322
 
    protected function parseImplementation($var, $type, $allow_null) {
15323
 
        return $this->evalExpression($var);
15324
 
    }
15325
 
    
15326
 
    protected function evalExpression($expr) {
15327
 
        $var = null;
15328
 
        $result = eval("\$var = $expr;");
15329
 
        if ($result === false) {
15330
 
            throw new HTMLPurifier_VarParserException("Fatal error in evaluated code");
15331
 
        }
15332
 
        return $var;
15333
 
    }
15334
 
    
15335
 
}
 
1
<?php
 
2
 
 
3
/**
 
4
 * @file
 
5
 * This file was auto-generated by generate-includes.php and includes all of
 
6
 * the core files required by HTML Purifier. Use this if performance is a
 
7
 * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
 
8
 * FILE, changes will be overwritten the next time the script is run.
 
9
 *
 
10
 * @version 4.0.0
 
11
 *
 
12
 * @warning
 
13
 *      You must *not* include any other HTML Purifier files before this file,
 
14
 *      because 'require' not 'require_once' is used.
 
15
 *
 
16
 * @warning
 
17
 *      This file requires that the include path contains the HTML Purifier
 
18
 *      library directory; this is not auto-set.
 
19
 */
 
20
 
 
21
 
 
22
 
 
23
/*! @mainpage
 
24
 *
 
25
 * HTML Purifier is an HTML filter that will take an arbitrary snippet of
 
26
 * HTML and rigorously test, validate and filter it into a version that
 
27
 * is safe for output onto webpages. It achieves this by:
 
28
 *
 
29
 *  -# Lexing (parsing into tokens) the document,
 
30
 *  -# Executing various strategies on the tokens:
 
31
 *      -# Removing all elements not in the whitelist,
 
32
 *      -# Making the tokens well-formed,
 
33
 *      -# Fixing the nesting of the nodes, and
 
34
 *      -# Validating attributes of the nodes; and
 
35
 *  -# Generating HTML from the purified tokens.
 
36
 *
 
37
 * However, most users will only need to interface with the HTMLPurifier
 
38
 * and HTMLPurifier_Config.
 
39
 */
 
40
 
 
41
/*
 
42
    HTML Purifier 4.0.0 - Standards Compliant HTML Filtering
 
43
    Copyright (C) 2006-2008 Edward Z. Yang
 
44
 
 
45
    This library is free software; you can redistribute it and/or
 
46
    modify it under the terms of the GNU Lesser General Public
 
47
    License as published by the Free Software Foundation; either
 
48
    version 2.1 of the License, or (at your option) any later version.
 
49
 
 
50
    This library is distributed in the hope that it will be useful,
 
51
    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
52
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
53
    Lesser General Public License for more details.
 
54
 
 
55
    You should have received a copy of the GNU Lesser General Public
 
56
    License along with this library; if not, write to the Free Software
 
57
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 
58
 */
 
59
 
 
60
/**
 
61
 * Facade that coordinates HTML Purifier's subsystems in order to purify HTML.
 
62
 *
 
63
 * @note There are several points in which configuration can be specified
 
64
 *       for HTML Purifier.  The precedence of these (from lowest to
 
65
 *       highest) is as follows:
 
66
 *          -# Instance: new HTMLPurifier($config)
 
67
 *          -# Invocation: purify($html, $config)
 
68
 *       These configurations are entirely independent of each other and
 
69
 *       are *not* merged (this behavior may change in the future).
 
70
 *
 
71
 * @todo We need an easier way to inject strategies using the configuration
 
72
 *       object.
 
73
 */
 
74
class HTMLPurifier
 
75
{
 
76
 
 
77
    /** Version of HTML Purifier */
 
78
    public $version = '4.0.0';
 
79
 
 
80
    /** Constant with version of HTML Purifier */
 
81
    const VERSION = '4.0.0';
 
82
 
 
83
    /** Global configuration object */
 
84
    public $config;
 
85
 
 
86
    /** Array of extra HTMLPurifier_Filter objects to run on HTML, for backwards compatibility */
 
87
    private $filters = array();
 
88
 
 
89
    /** Single instance of HTML Purifier */
 
90
    private static $instance;
 
91
 
 
92
    protected $strategy, $generator;
 
93
 
 
94
    /**
 
95
     * Resultant HTMLPurifier_Context of last run purification. Is an array
 
96
     * of contexts if the last called method was purifyArray().
 
97
     */
 
98
    public $context;
 
99
 
 
100
    /**
 
101
     * Initializes the purifier.
 
102
     * @param $config Optional HTMLPurifier_Config object for all instances of
 
103
     *                the purifier, if omitted, a default configuration is
 
104
     *                supplied (which can be overridden on a per-use basis).
 
105
     *                The parameter can also be any type that
 
106
     *                HTMLPurifier_Config::create() supports.
 
107
     */
 
108
    public function __construct($config = null) {
 
109
 
 
110
        $this->config = HTMLPurifier_Config::create($config);
 
111
 
 
112
        $this->strategy     = new HTMLPurifier_Strategy_Core();
 
113
 
 
114
    }
 
115
 
 
116
    /**
 
117
     * Adds a filter to process the output. First come first serve
 
118
     * @param $filter HTMLPurifier_Filter object
 
119
     */
 
120
    public function addFilter($filter) {
 
121
        trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING);
 
122
        $this->filters[] = $filter;
 
123
    }
 
124
 
 
125
    /**
 
126
     * Filters an HTML snippet/document to be XSS-free and standards-compliant.
 
127
     *
 
128
     * @param $html String of HTML to purify
 
129
     * @param $config HTMLPurifier_Config object for this operation, if omitted,
 
130
     *                defaults to the config object specified during this
 
131
     *                object's construction. The parameter can also be any type
 
132
     *                that HTMLPurifier_Config::create() supports.
 
133
     * @return Purified HTML
 
134
     */
 
135
    public function purify($html, $config = null) {
 
136
 
 
137
        // :TODO: make the config merge in, instead of replace
 
138
        $config = $config ? HTMLPurifier_Config::create($config) : $this->config;
 
139
 
 
140
        // implementation is partially environment dependant, partially
 
141
        // configuration dependant
 
142
        $lexer = HTMLPurifier_Lexer::create($config);
 
143
 
 
144
        $context = new HTMLPurifier_Context();
 
145
 
 
146
        // setup HTML generator
 
147
        $this->generator = new HTMLPurifier_Generator($config, $context);
 
148
        $context->register('Generator', $this->generator);
 
149
 
 
150
        // set up global context variables
 
151
        if ($config->get('Core.CollectErrors')) {
 
152
            // may get moved out if other facilities use it
 
153
            $language_factory = HTMLPurifier_LanguageFactory::instance();
 
154
            $language = $language_factory->create($config, $context);
 
155
            $context->register('Locale', $language);
 
156
 
 
157
            $error_collector = new HTMLPurifier_ErrorCollector($context);
 
158
            $context->register('ErrorCollector', $error_collector);
 
159
        }
 
160
 
 
161
        // setup id_accumulator context, necessary due to the fact that
 
162
        // AttrValidator can be called from many places
 
163
        $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
 
164
        $context->register('IDAccumulator', $id_accumulator);
 
165
 
 
166
        $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context);
 
167
 
 
168
        // setup filters
 
169
        $filter_flags = $config->getBatch('Filter');
 
170
        $custom_filters = $filter_flags['Custom'];
 
171
        unset($filter_flags['Custom']);
 
172
        $filters = array();
 
173
        foreach ($filter_flags as $filter => $flag) {
 
174
            if (!$flag) continue;
 
175
            if (strpos($filter, '.') !== false) continue;
 
176
            $class = "HTMLPurifier_Filter_$filter";
 
177
            $filters[] = new $class;
 
178
        }
 
179
        foreach ($custom_filters as $filter) {
 
180
            // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
 
181
            $filters[] = $filter;
 
182
        }
 
183
        $filters = array_merge($filters, $this->filters);
 
184
        // maybe prepare(), but later
 
185
 
 
186
        for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) {
 
187
            $html = $filters[$i]->preFilter($html, $config, $context);
 
188
        }
 
189
 
 
190
        // purified HTML
 
191
        $html =
 
192
            $this->generator->generateFromTokens(
 
193
                // list of tokens
 
194
                $this->strategy->execute(
 
195
                    // list of un-purified tokens
 
196
                    $lexer->tokenizeHTML(
 
197
                        // un-purified HTML
 
198
                        $html, $config, $context
 
199
                    ),
 
200
                    $config, $context
 
201
                )
 
202
            );
 
203
 
 
204
        for ($i = $filter_size - 1; $i >= 0; $i--) {
 
205
            $html = $filters[$i]->postFilter($html, $config, $context);
 
206
        }
 
207
 
 
208
        $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context);
 
209
        $this->context =& $context;
 
210
        return $html;
 
211
    }
 
212
 
 
213
    /**
 
214
     * Filters an array of HTML snippets
 
215
     * @param $config Optional HTMLPurifier_Config object for this operation.
 
216
     *                See HTMLPurifier::purify() for more details.
 
217
     * @return Array of purified HTML
 
218
     */
 
219
    public function purifyArray($array_of_html, $config = null) {
 
220
        $context_array = array();
 
221
        foreach ($array_of_html as $key => $html) {
 
222
            $array_of_html[$key] = $this->purify($html, $config);
 
223
            $context_array[$key] = $this->context;
 
224
        }
 
225
        $this->context = $context_array;
 
226
        return $array_of_html;
 
227
    }
 
228
 
 
229
    /**
 
230
     * Singleton for enforcing just one HTML Purifier in your system
 
231
     * @param $prototype Optional prototype HTMLPurifier instance to
 
232
     *                   overload singleton with, or HTMLPurifier_Config
 
233
     *                   instance to configure the generated version with.
 
234
     */
 
235
    public static function instance($prototype = null) {
 
236
        if (!self::$instance || $prototype) {
 
237
            if ($prototype instanceof HTMLPurifier) {
 
238
                self::$instance = $prototype;
 
239
            } elseif ($prototype) {
 
240
                self::$instance = new HTMLPurifier($prototype);
 
241
            } else {
 
242
                self::$instance = new HTMLPurifier();
 
243
            }
 
244
        }
 
245
        return self::$instance;
 
246
    }
 
247
 
 
248
    /**
 
249
     * @note Backwards compatibility, see instance()
 
250
     */
 
251
    public static function getInstance($prototype = null) {
 
252
        return HTMLPurifier::instance($prototype);
 
253
    }
 
254
 
 
255
}
 
256
 
 
257
 
 
258
 
 
259
 
 
260
 
 
261
/**
 
262
 * Defines common attribute collections that modules reference
 
263
 */
 
264
 
 
265
class HTMLPurifier_AttrCollections
 
266
{
 
267
 
 
268
    /**
 
269
     * Associative array of attribute collections, indexed by name
 
270
     */
 
271
    public $info = array();
 
272
 
 
273
    /**
 
274
     * Performs all expansions on internal data for use by other inclusions
 
275
     * It also collects all attribute collection extensions from
 
276
     * modules
 
277
     * @param $attr_types HTMLPurifier_AttrTypes instance
 
278
     * @param $modules Hash array of HTMLPurifier_HTMLModule members
 
279
     */
 
280
    public function __construct($attr_types, $modules) {
 
281
        // load extensions from the modules
 
282
        foreach ($modules as $module) {
 
283
            foreach ($module->attr_collections as $coll_i => $coll) {
 
284
                if (!isset($this->info[$coll_i])) {
 
285
                    $this->info[$coll_i] = array();
 
286
                }
 
287
                foreach ($coll as $attr_i => $attr) {
 
288
                    if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) {
 
289
                        // merge in includes
 
290
                        $this->info[$coll_i][$attr_i] = array_merge(
 
291
                            $this->info[$coll_i][$attr_i], $attr);
 
292
                        continue;
 
293
                    }
 
294
                    $this->info[$coll_i][$attr_i] = $attr;
 
295
                }
 
296
            }
 
297
        }
 
298
        // perform internal expansions and inclusions
 
299
        foreach ($this->info as $name => $attr) {
 
300
            // merge attribute collections that include others
 
301
            $this->performInclusions($this->info[$name]);
 
302
            // replace string identifiers with actual attribute objects
 
303
            $this->expandIdentifiers($this->info[$name], $attr_types);
 
304
        }
 
305
    }
 
306
 
 
307
    /**
 
308
     * Takes a reference to an attribute associative array and performs
 
309
     * all inclusions specified by the zero index.
 
310
     * @param &$attr Reference to attribute array
 
311
     */
 
312
    public function performInclusions(&$attr) {
 
313
        if (!isset($attr[0])) return;
 
314
        $merge = $attr[0];
 
315
        $seen  = array(); // recursion guard
 
316
        // loop through all the inclusions
 
317
        for ($i = 0; isset($merge[$i]); $i++) {
 
318
            if (isset($seen[$merge[$i]])) continue;
 
319
            $seen[$merge[$i]] = true;
 
320
            // foreach attribute of the inclusion, copy it over
 
321
            if (!isset($this->info[$merge[$i]])) continue;
 
322
            foreach ($this->info[$merge[$i]] as $key => $value) {
 
323
                if (isset($attr[$key])) continue; // also catches more inclusions
 
324
                $attr[$key] = $value;
 
325
            }
 
326
            if (isset($this->info[$merge[$i]][0])) {
 
327
                // recursion
 
328
                $merge = array_merge($merge, $this->info[$merge[$i]][0]);
 
329
            }
 
330
        }
 
331
        unset($attr[0]);
 
332
    }
 
333
 
 
334
    /**
 
335
     * Expands all string identifiers in an attribute array by replacing
 
336
     * them with the appropriate values inside HTMLPurifier_AttrTypes
 
337
     * @param &$attr Reference to attribute array
 
338
     * @param $attr_types HTMLPurifier_AttrTypes instance
 
339
     */
 
340
    public function expandIdentifiers(&$attr, $attr_types) {
 
341
 
 
342
        // because foreach will process new elements we add, make sure we
 
343
        // skip duplicates
 
344
        $processed = array();
 
345
 
 
346
        foreach ($attr as $def_i => $def) {
 
347
            // skip inclusions
 
348
            if ($def_i === 0) continue;
 
349
 
 
350
            if (isset($processed[$def_i])) continue;
 
351
 
 
352
            // determine whether or not attribute is required
 
353
            if ($required = (strpos($def_i, '*') !== false)) {
 
354
                // rename the definition
 
355
                unset($attr[$def_i]);
 
356
                $def_i = trim($def_i, '*');
 
357
                $attr[$def_i] = $def;
 
358
            }
 
359
 
 
360
            $processed[$def_i] = true;
 
361
 
 
362
            // if we've already got a literal object, move on
 
363
            if (is_object($def)) {
 
364
                // preserve previous required
 
365
                $attr[$def_i]->required = ($required || $attr[$def_i]->required);
 
366
                continue;
 
367
            }
 
368
 
 
369
            if ($def === false) {
 
370
                unset($attr[$def_i]);
 
371
                continue;
 
372
            }
 
373
 
 
374
            if ($t = $attr_types->get($def)) {
 
375
                $attr[$def_i] = $t;
 
376
                $attr[$def_i]->required = $required;
 
377
            } else {
 
378
                unset($attr[$def_i]);
 
379
            }
 
380
        }
 
381
 
 
382
    }
 
383
 
 
384
}
 
385
 
 
386
 
 
387
 
 
388
 
 
389
 
 
390
/**
 
391
 * Base class for all validating attribute definitions.
 
392
 *
 
393
 * This family of classes forms the core for not only HTML attribute validation,
 
394
 * but also any sort of string that needs to be validated or cleaned (which
 
395
 * means CSS properties and composite definitions are defined here too).
 
396
 * Besides defining (through code) what precisely makes the string valid,
 
397
 * subclasses are also responsible for cleaning the code if possible.
 
398
 */
 
399
 
 
400
abstract class HTMLPurifier_AttrDef
 
401
{
 
402
 
 
403
    /**
 
404
     * Tells us whether or not an HTML attribute is minimized. Has no
 
405
     * meaning in other contexts.
 
406
     */
 
407
    public $minimized = false;
 
408
 
 
409
    /**
 
410
     * Tells us whether or not an HTML attribute is required. Has no
 
411
     * meaning in other contexts
 
412
     */
 
413
    public $required = false;
 
414
 
 
415
    /**
 
416
     * Validates and cleans passed string according to a definition.
 
417
     *
 
418
     * @param $string String to be validated and cleaned.
 
419
     * @param $config Mandatory HTMLPurifier_Config object.
 
420
     * @param $context Mandatory HTMLPurifier_AttrContext object.
 
421
     */
 
422
    abstract public function validate($string, $config, $context);
 
423
 
 
424
    /**
 
425
     * Convenience method that parses a string as if it were CDATA.
 
426
     *
 
427
     * This method process a string in the manner specified at
 
428
     * <http://www.w3.org/TR/html4/types.html#h-6.2> by removing
 
429
     * leading and trailing whitespace, ignoring line feeds, and replacing
 
430
     * carriage returns and tabs with spaces.  While most useful for HTML
 
431
     * attributes specified as CDATA, it can also be applied to most CSS
 
432
     * values.
 
433
     *
 
434
     * @note This method is not entirely standards compliant, as trim() removes
 
435
     *       more types of whitespace than specified in the spec. In practice,
 
436
     *       this is rarely a problem, as those extra characters usually have
 
437
     *       already been removed by HTMLPurifier_Encoder.
 
438
     *
 
439
     * @warning This processing is inconsistent with XML's whitespace handling
 
440
     *          as specified by section 3.3.3 and referenced XHTML 1.0 section
 
441
     *          4.7.  However, note that we are NOT necessarily
 
442
     *          parsing XML, thus, this behavior may still be correct. We
 
443
     *          assume that newlines have been normalized.
 
444
     */
 
445
    public function parseCDATA($string) {
 
446
        $string = trim($string);
 
447
        $string = str_replace(array("\n", "\t", "\r"), ' ', $string);
 
448
        return $string;
 
449
    }
 
450
 
 
451
    /**
 
452
     * Factory method for creating this class from a string.
 
453
     * @param $string String construction info
 
454
     * @return Created AttrDef object corresponding to $string
 
455
     */
 
456
    public function make($string) {
 
457
        // default implementation, return a flyweight of this object.
 
458
        // If $string has an effect on the returned object (i.e. you
 
459
        // need to overload this method), it is best
 
460
        // to clone or instantiate new copies. (Instantiation is safer.)
 
461
        return $this;
 
462
    }
 
463
 
 
464
    /**
 
465
     * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work
 
466
     * properly. THIS IS A HACK!
 
467
     */
 
468
    protected function mungeRgb($string) {
 
469
        return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
 
470
    }
 
471
 
 
472
}
 
473
 
 
474
 
 
475
 
 
476
 
 
477
 
 
478
/**
 
479
 * Processes an entire attribute array for corrections needing multiple values.
 
480
 *
 
481
 * Occasionally, a certain attribute will need to be removed and popped onto
 
482
 * another value.  Instead of creating a complex return syntax for
 
483
 * HTMLPurifier_AttrDef, we just pass the whole attribute array to a
 
484
 * specialized object and have that do the special work.  That is the
 
485
 * family of HTMLPurifier_AttrTransform.
 
486
 *
 
487
 * An attribute transformation can be assigned to run before or after
 
488
 * HTMLPurifier_AttrDef validation.  See HTMLPurifier_HTMLDefinition for
 
489
 * more details.
 
490
 */
 
491
 
 
492
abstract class HTMLPurifier_AttrTransform
 
493
{
 
494
 
 
495
    /**
 
496
     * Abstract: makes changes to the attributes dependent on multiple values.
 
497
     *
 
498
     * @param $attr Assoc array of attributes, usually from
 
499
     *              HTMLPurifier_Token_Tag::$attr
 
500
     * @param $config Mandatory HTMLPurifier_Config object.
 
501
     * @param $context Mandatory HTMLPurifier_Context object
 
502
     * @returns Processed attribute array.
 
503
     */
 
504
    abstract public function transform($attr, $config, $context);
 
505
 
 
506
    /**
 
507
     * Prepends CSS properties to the style attribute, creating the
 
508
     * attribute if it doesn't exist.
 
509
     * @param $attr Attribute array to process (passed by reference)
 
510
     * @param $css CSS to prepend
 
511
     */
 
512
    public function prependCSS(&$attr, $css) {
 
513
        $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
 
514
        $attr['style'] = $css . $attr['style'];
 
515
    }
 
516
 
 
517
    /**
 
518
     * Retrieves and removes an attribute
 
519
     * @param $attr Attribute array to process (passed by reference)
 
520
     * @param $key Key of attribute to confiscate
 
521
     */
 
522
    public function confiscateAttr(&$attr, $key) {
 
523
        if (!isset($attr[$key])) return null;
 
524
        $value = $attr[$key];
 
525
        unset($attr[$key]);
 
526
        return $value;
 
527
    }
 
528
 
 
529
}
 
530
 
 
531
 
 
532
 
 
533
 
 
534
 
 
535
/**
 
536
 * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects
 
537
 */
 
538
class HTMLPurifier_AttrTypes
 
539
{
 
540
    /**
 
541
     * Lookup array of attribute string identifiers to concrete implementations
 
542
     */
 
543
    protected $info = array();
 
544
 
 
545
    /**
 
546
     * Constructs the info array, supplying default implementations for attribute
 
547
     * types.
 
548
     */
 
549
    public function __construct() {
 
550
        // pseudo-types, must be instantiated via shorthand
 
551
        $this->info['Enum']    = new HTMLPurifier_AttrDef_Enum();
 
552
        $this->info['Bool']    = new HTMLPurifier_AttrDef_HTML_Bool();
 
553
 
 
554
        $this->info['CDATA']    = new HTMLPurifier_AttrDef_Text();
 
555
        $this->info['ID']       = new HTMLPurifier_AttrDef_HTML_ID();
 
556
        $this->info['Length']   = new HTMLPurifier_AttrDef_HTML_Length();
 
557
        $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
 
558
        $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
 
559
        $this->info['Pixels']   = new HTMLPurifier_AttrDef_HTML_Pixels();
 
560
        $this->info['Text']     = new HTMLPurifier_AttrDef_Text();
 
561
        $this->info['URI']      = new HTMLPurifier_AttrDef_URI();
 
562
        $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
 
563
        $this->info['Color']    = new HTMLPurifier_AttrDef_HTML_Color();
 
564
 
 
565
        // unimplemented aliases
 
566
        $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
 
567
        $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text();
 
568
        $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text();
 
569
        $this->info['Character'] = new HTMLPurifier_AttrDef_Text();
 
570
 
 
571
        // "proprietary" types
 
572
        $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class();
 
573
 
 
574
        // number is really a positive integer (one or more digits)
 
575
        // FIXME: ^^ not always, see start and value of list items
 
576
        $this->info['Number']   = new HTMLPurifier_AttrDef_Integer(false, false, true);
 
577
    }
 
578
 
 
579
    /**
 
580
     * Retrieves a type
 
581
     * @param $type String type name
 
582
     * @return Object AttrDef for type
 
583
     */
 
584
    public function get($type) {
 
585
 
 
586
        // determine if there is any extra info tacked on
 
587
        if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2);
 
588
        else $string = '';
 
589
 
 
590
        if (!isset($this->info[$type])) {
 
591
            trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR);
 
592
            return;
 
593
        }
 
594
 
 
595
        return $this->info[$type]->make($string);
 
596
 
 
597
    }
 
598
 
 
599
    /**
 
600
     * Sets a new implementation for a type
 
601
     * @param $type String type name
 
602
     * @param $impl Object AttrDef for type
 
603
     */
 
604
    public function set($type, $impl) {
 
605
        $this->info[$type] = $impl;
 
606
    }
 
607
}
 
608
 
 
609
 
 
610
 
 
611
 
 
612
 
 
613
/**
 
614
 * Validates the attributes of a token. Doesn't manage required attributes
 
615
 * very well. The only reason we factored this out was because RemoveForeignElements
 
616
 * also needed it besides ValidateAttributes.
 
617
 */
 
618
class HTMLPurifier_AttrValidator
 
619
{
 
620
 
 
621
    /**
 
622
     * Validates the attributes of a token, returning a modified token
 
623
     * that has valid tokens
 
624
     * @param $token Reference to token to validate. We require a reference
 
625
     *     because the operation this class performs on the token are
 
626
     *     not atomic, so the context CurrentToken to be updated
 
627
     *     throughout
 
628
     * @param $config Instance of HTMLPurifier_Config
 
629
     * @param $context Instance of HTMLPurifier_Context
 
630
     */
 
631
    public function validateToken(&$token, &$config, $context) {
 
632
 
 
633
        $definition = $config->getHTMLDefinition();
 
634
        $e =& $context->get('ErrorCollector', true);
 
635
 
 
636
        // initialize IDAccumulator if necessary
 
637
        $ok =& $context->get('IDAccumulator', true);
 
638
        if (!$ok) {
 
639
            $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
 
640
            $context->register('IDAccumulator', $id_accumulator);
 
641
        }
 
642
 
 
643
        // initialize CurrentToken if necessary
 
644
        $current_token =& $context->get('CurrentToken', true);
 
645
        if (!$current_token) $context->register('CurrentToken', $token);
 
646
 
 
647
        if (
 
648
            !$token instanceof HTMLPurifier_Token_Start &&
 
649
            !$token instanceof HTMLPurifier_Token_Empty
 
650
        ) return $token;
 
651
 
 
652
        // create alias to global definition array, see also $defs
 
653
        // DEFINITION CALL
 
654
        $d_defs = $definition->info_global_attr;
 
655
 
 
656
        // don't update token until the very end, to ensure an atomic update
 
657
        $attr = $token->attr;
 
658
 
 
659
        // do global transformations (pre)
 
660
        // nothing currently utilizes this
 
661
        foreach ($definition->info_attr_transform_pre as $transform) {
 
662
            $attr = $transform->transform($o = $attr, $config, $context);
 
663
            if ($e) {
 
664
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
 
665
            }
 
666
        }
 
667
 
 
668
        // do local transformations only applicable to this element (pre)
 
669
        // ex. <p align="right"> to <p style="text-align:right;">
 
670
        foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
 
671
            $attr = $transform->transform($o = $attr, $config, $context);
 
672
            if ($e) {
 
673
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
 
674
            }
 
675
        }
 
676
 
 
677
        // create alias to this element's attribute definition array, see
 
678
        // also $d_defs (global attribute definition array)
 
679
        // DEFINITION CALL
 
680
        $defs = $definition->info[$token->name]->attr;
 
681
 
 
682
        $attr_key = false;
 
683
        $context->register('CurrentAttr', $attr_key);
 
684
 
 
685
        // iterate through all the attribute keypairs
 
686
        // Watch out for name collisions: $key has previously been used
 
687
        foreach ($attr as $attr_key => $value) {
 
688
 
 
689
            // call the definition
 
690
            if ( isset($defs[$attr_key]) ) {
 
691
                // there is a local definition defined
 
692
                if ($defs[$attr_key] === false) {
 
693
                    // We've explicitly been told not to allow this element.
 
694
                    // This is usually when there's a global definition
 
695
                    // that must be overridden.
 
696
                    // Theoretically speaking, we could have a
 
697
                    // AttrDef_DenyAll, but this is faster!
 
698
                    $result = false;
 
699
                } else {
 
700
                    // validate according to the element's definition
 
701
                    $result = $defs[$attr_key]->validate(
 
702
                                    $value, $config, $context
 
703
                               );
 
704
                }
 
705
            } elseif ( isset($d_defs[$attr_key]) ) {
 
706
                // there is a global definition defined, validate according
 
707
                // to the global definition
 
708
                $result = $d_defs[$attr_key]->validate(
 
709
                                $value, $config, $context
 
710
                           );
 
711
            } else {
 
712
                // system never heard of the attribute? DELETE!
 
713
                $result = false;
 
714
            }
 
715
 
 
716
            // put the results into effect
 
717
            if ($result === false || $result === null) {
 
718
                // this is a generic error message that should replaced
 
719
                // with more specific ones when possible
 
720
                if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed');
 
721
 
 
722
                // remove the attribute
 
723
                unset($attr[$attr_key]);
 
724
            } elseif (is_string($result)) {
 
725
                // generally, if a substitution is happening, there
 
726
                // was some sort of implicit correction going on. We'll
 
727
                // delegate it to the attribute classes to say exactly what.
 
728
 
 
729
                // simple substitution
 
730
                $attr[$attr_key] = $result;
 
731
            } else {
 
732
                // nothing happens
 
733
            }
 
734
 
 
735
            // we'd also want slightly more complicated substitution
 
736
            // involving an array as the return value,
 
737
            // although we're not sure how colliding attributes would
 
738
            // resolve (certain ones would be completely overriden,
 
739
            // others would prepend themselves).
 
740
        }
 
741
 
 
742
        $context->destroy('CurrentAttr');
 
743
 
 
744
        // post transforms
 
745
 
 
746
        // global (error reporting untested)
 
747
        foreach ($definition->info_attr_transform_post as $transform) {
 
748
            $attr = $transform->transform($o = $attr, $config, $context);
 
749
            if ($e) {
 
750
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
 
751
            }
 
752
        }
 
753
 
 
754
        // local (error reporting untested)
 
755
        foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
 
756
            $attr = $transform->transform($o = $attr, $config, $context);
 
757
            if ($e) {
 
758
                if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
 
759
            }
 
760
        }
 
761
 
 
762
        $token->attr = $attr;
 
763
 
 
764
        // destroy CurrentToken if we made it ourselves
 
765
        if (!$current_token) $context->destroy('CurrentToken');
 
766
 
 
767
    }
 
768
 
 
769
 
 
770
}
 
771
 
 
772
 
 
773
 
 
774
 
 
775
 
 
776
// constants are slow, so we use as few as possible
 
777
if (!defined('HTMLPURIFIER_PREFIX')) {
 
778
    define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone');
 
779
    set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path());
 
780
}
 
781
 
 
782
// accomodations for versions earlier than 5.0.2
 
783
// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net>
 
784
if (!defined('PHP_EOL')) {
 
785
    switch (strtoupper(substr(PHP_OS, 0, 3))) {
 
786
        case 'WIN':
 
787
            define('PHP_EOL', "\r\n");
 
788
            break;
 
789
        case 'DAR':
 
790
            define('PHP_EOL', "\r");
 
791
            break;
 
792
        default:
 
793
            define('PHP_EOL', "\n");
 
794
    }
 
795
}
 
796
 
 
797
/**
 
798
 * Bootstrap class that contains meta-functionality for HTML Purifier such as
 
799
 * the autoload function.
 
800
 *
 
801
 * @note
 
802
 *      This class may be used without any other files from HTML Purifier.
 
803
 */
 
804
class HTMLPurifier_Bootstrap
 
805
{
 
806
 
 
807
    /**
 
808
     * Autoload function for HTML Purifier
 
809
     * @param $class Class to load
 
810
     */
 
811
    public static function autoload($class) {
 
812
        $file = HTMLPurifier_Bootstrap::getPath($class);
 
813
        if (!$file) return false;
 
814
        require HTMLPURIFIER_PREFIX . '/' . $file;
 
815
        return true;
 
816
    }
 
817
 
 
818
    /**
 
819
     * Returns the path for a specific class.
 
820
     */
 
821
    public static function getPath($class) {
 
822
        if (strncmp('HTMLPurifier', $class, 12) !== 0) return false;
 
823
        // Custom implementations
 
824
        if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) {
 
825
            $code = str_replace('_', '-', substr($class, 22));
 
826
            $file = 'HTMLPurifier/Language/classes/' . $code . '.php';
 
827
        } else {
 
828
            $file = str_replace('_', '/', $class) . '.php';
 
829
        }
 
830
        if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false;
 
831
        return $file;
 
832
    }
 
833
 
 
834
    /**
 
835
     * "Pre-registers" our autoloader on the SPL stack.
 
836
     */
 
837
    public static function registerAutoload() {
 
838
        $autoload = array('HTMLPurifier_Bootstrap', 'autoload');
 
839
        if ( ($funcs = spl_autoload_functions()) === false ) {
 
840
            spl_autoload_register($autoload);
 
841
        } elseif (function_exists('spl_autoload_unregister')) {
 
842
            $compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
 
843
                      version_compare(PHP_VERSION, '5.1.0', '>=');
 
844
            foreach ($funcs as $func) {
 
845
                if (is_array($func)) {
 
846
                    // :TRICKY: There are some compatibility issues and some
 
847
                    // places where we need to error out
 
848
                    $reflector = new ReflectionMethod($func[0], $func[1]);
 
849
                    if (!$reflector->isStatic()) {
 
850
                        throw new Exception('
 
851
                            HTML Purifier autoloader registrar is not compatible
 
852
                            with non-static object methods due to PHP Bug #44144;
 
853
                            Please do not use HTMLPurifier.autoload.php (or any
 
854
                            file that includes this file); instead, place the code:
 
855
                            spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\'))
 
856
                            after your own autoloaders.
 
857
                        ');
 
858
                    }
 
859
                    // Suprisingly, spl_autoload_register supports the
 
860
                    // Class::staticMethod callback format, although call_user_func doesn't
 
861
                    if ($compat) $func = implode('::', $func);
 
862
                }
 
863
                spl_autoload_unregister($func);
 
864
            }
 
865
            spl_autoload_register($autoload);
 
866
            foreach ($funcs as $func) spl_autoload_register($func);
 
867
        }
 
868
    }
 
869
 
 
870
}
 
871
 
 
872
 
 
873
 
 
874
 
 
875
 
 
876
/**
 
877
 * Super-class for definition datatype objects, implements serialization
 
878
 * functions for the class.
 
879
 */
 
880
abstract class HTMLPurifier_Definition
 
881
{
 
882
 
 
883
    /**
 
884
     * Has setup() been called yet?
 
885
     */
 
886
    public $setup = false;
 
887
 
 
888
    /**
 
889
     * What type of definition is it?
 
890
     */
 
891
    public $type;
 
892
 
 
893
    /**
 
894
     * Sets up the definition object into the final form, something
 
895
     * not done by the constructor
 
896
     * @param $config HTMLPurifier_Config instance
 
897
     */
 
898
    abstract protected function doSetup($config);
 
899
 
 
900
    /**
 
901
     * Setup function that aborts if already setup
 
902
     * @param $config HTMLPurifier_Config instance
 
903
     */
 
904
    public function setup($config) {
 
905
        if ($this->setup) return;
 
906
        $this->setup = true;
 
907
        $this->doSetup($config);
 
908
    }
 
909
 
 
910
}
 
911
 
 
912
 
 
913
 
 
914
 
 
915
 
 
916
/**
 
917
 * Defines allowed CSS attributes and what their values are.
 
918
 * @see HTMLPurifier_HTMLDefinition
 
919
 */
 
920
class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
 
921
{
 
922
 
 
923
    public $type = 'CSS';
 
924
 
 
925
    /**
 
926
     * Assoc array of attribute name to definition object.
 
927
     */
 
928
    public $info = array();
 
929
 
 
930
    /**
 
931
     * Constructs the info array.  The meat of this class.
 
932
     */
 
933
    protected function doSetup($config) {
 
934
 
 
935
        $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum(
 
936
            array('left', 'right', 'center', 'justify'), false);
 
937
 
 
938
        $border_style =
 
939
        $this->info['border-bottom-style'] =
 
940
        $this->info['border-right-style'] =
 
941
        $this->info['border-left-style'] =
 
942
        $this->info['border-top-style'] =  new HTMLPurifier_AttrDef_Enum(
 
943
            array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double',
 
944
            'groove', 'ridge', 'inset', 'outset'), false);
 
945
 
 
946
        $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style);
 
947
 
 
948
        $this->info['clear'] = new HTMLPurifier_AttrDef_Enum(
 
949
            array('none', 'left', 'right', 'both'), false);
 
950
        $this->info['float'] = new HTMLPurifier_AttrDef_Enum(
 
951
            array('none', 'left', 'right'), false);
 
952
        $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
 
953
            array('normal', 'italic', 'oblique'), false);
 
954
        $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum(
 
955
            array('normal', 'small-caps'), false);
 
956
 
 
957
        $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite(
 
958
            array(
 
959
                new HTMLPurifier_AttrDef_Enum(array('none')),
 
960
                new HTMLPurifier_AttrDef_CSS_URI()
 
961
            )
 
962
        );
 
963
 
 
964
        $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum(
 
965
            array('inside', 'outside'), false);
 
966
        $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum(
 
967
            array('disc', 'circle', 'square', 'decimal', 'lower-roman',
 
968
            'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false);
 
969
        $this->info['list-style-image'] = $uri_or_none;
 
970
 
 
971
        $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config);
 
972
 
 
973
        $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum(
 
974
            array('capitalize', 'uppercase', 'lowercase', 'none'), false);
 
975
        $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color();
 
976
 
 
977
        $this->info['background-image'] = $uri_or_none;
 
978
        $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum(
 
979
            array('repeat', 'repeat-x', 'repeat-y', 'no-repeat')
 
980
        );
 
981
        $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
 
982
            array('scroll', 'fixed')
 
983
        );
 
984
        $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
 
985
 
 
986
        $border_color =
 
987
        $this->info['border-top-color'] =
 
988
        $this->info['border-bottom-color'] =
 
989
        $this->info['border-left-color'] =
 
990
        $this->info['border-right-color'] =
 
991
        $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
992
            new HTMLPurifier_AttrDef_Enum(array('transparent')),
 
993
            new HTMLPurifier_AttrDef_CSS_Color()
 
994
        ));
 
995
 
 
996
        $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config);
 
997
 
 
998
        $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color);
 
999
 
 
1000
        $border_width =
 
1001
        $this->info['border-top-width'] =
 
1002
        $this->info['border-bottom-width'] =
 
1003
        $this->info['border-left-width'] =
 
1004
        $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1005
            new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')),
 
1006
            new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative
 
1007
        ));
 
1008
 
 
1009
        $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width);
 
1010
 
 
1011
        $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1012
            new HTMLPurifier_AttrDef_Enum(array('normal')),
 
1013
            new HTMLPurifier_AttrDef_CSS_Length()
 
1014
        ));
 
1015
 
 
1016
        $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1017
            new HTMLPurifier_AttrDef_Enum(array('normal')),
 
1018
            new HTMLPurifier_AttrDef_CSS_Length()
 
1019
        ));
 
1020
 
 
1021
        $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1022
            new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small',
 
1023
                'small', 'medium', 'large', 'x-large', 'xx-large',
 
1024
                'larger', 'smaller')),
 
1025
            new HTMLPurifier_AttrDef_CSS_Percentage(),
 
1026
            new HTMLPurifier_AttrDef_CSS_Length()
 
1027
        ));
 
1028
 
 
1029
        $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1030
            new HTMLPurifier_AttrDef_Enum(array('normal')),
 
1031
            new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives
 
1032
            new HTMLPurifier_AttrDef_CSS_Length('0'),
 
1033
            new HTMLPurifier_AttrDef_CSS_Percentage(true)
 
1034
        ));
 
1035
 
 
1036
        $margin =
 
1037
        $this->info['margin-top'] =
 
1038
        $this->info['margin-bottom'] =
 
1039
        $this->info['margin-left'] =
 
1040
        $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1041
            new HTMLPurifier_AttrDef_CSS_Length(),
 
1042
            new HTMLPurifier_AttrDef_CSS_Percentage(),
 
1043
            new HTMLPurifier_AttrDef_Enum(array('auto'))
 
1044
        ));
 
1045
 
 
1046
        $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin);
 
1047
 
 
1048
        // non-negative
 
1049
        $padding =
 
1050
        $this->info['padding-top'] =
 
1051
        $this->info['padding-bottom'] =
 
1052
        $this->info['padding-left'] =
 
1053
        $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1054
            new HTMLPurifier_AttrDef_CSS_Length('0'),
 
1055
            new HTMLPurifier_AttrDef_CSS_Percentage(true)
 
1056
        ));
 
1057
 
 
1058
        $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding);
 
1059
 
 
1060
        $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1061
            new HTMLPurifier_AttrDef_CSS_Length(),
 
1062
            new HTMLPurifier_AttrDef_CSS_Percentage()
 
1063
        ));
 
1064
 
 
1065
        $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1066
            new HTMLPurifier_AttrDef_CSS_Length('0'),
 
1067
            new HTMLPurifier_AttrDef_CSS_Percentage(true),
 
1068
            new HTMLPurifier_AttrDef_Enum(array('auto'))
 
1069
        ));
 
1070
        $max = $config->get('CSS.MaxImgLength');
 
1071
 
 
1072
        $this->info['width'] =
 
1073
        $this->info['height'] =
 
1074
            $max === null ?
 
1075
            $trusted_wh :
 
1076
            new HTMLPurifier_AttrDef_Switch('img',
 
1077
                // For img tags:
 
1078
                new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1079
                    new HTMLPurifier_AttrDef_CSS_Length('0', $max),
 
1080
                    new HTMLPurifier_AttrDef_Enum(array('auto'))
 
1081
                )),
 
1082
                // For everyone else:
 
1083
                $trusted_wh
 
1084
            );
 
1085
 
 
1086
        $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration();
 
1087
 
 
1088
        $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily();
 
1089
 
 
1090
        // this could use specialized code
 
1091
        $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum(
 
1092
            array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300',
 
1093
            '400', '500', '600', '700', '800', '900'), false);
 
1094
 
 
1095
        // MUST be called after other font properties, as it references
 
1096
        // a CSSDefinition object
 
1097
        $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config);
 
1098
 
 
1099
        // same here
 
1100
        $this->info['border'] =
 
1101
        $this->info['border-bottom'] =
 
1102
        $this->info['border-top'] =
 
1103
        $this->info['border-left'] =
 
1104
        $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config);
 
1105
 
 
1106
        $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array(
 
1107
            'collapse', 'separate'));
 
1108
 
 
1109
        $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array(
 
1110
            'top', 'bottom'));
 
1111
 
 
1112
        $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array(
 
1113
            'auto', 'fixed'));
 
1114
 
 
1115
        $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
 
1116
            new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super',
 
1117
                'top', 'text-top', 'middle', 'bottom', 'text-bottom')),
 
1118
            new HTMLPurifier_AttrDef_CSS_Length(),
 
1119
            new HTMLPurifier_AttrDef_CSS_Percentage()
 
1120
        ));
 
1121
 
 
1122
        $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2);
 
1123
 
 
1124
        // partial support
 
1125
        $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap'));
 
1126
 
 
1127
        if ($config->get('CSS.Proprietary')) {
 
1128
            $this->doSetupProprietary($config);
 
1129
        }
 
1130
 
 
1131
        if ($config->get('CSS.AllowTricky')) {
 
1132
            $this->doSetupTricky($config);
 
1133
        }
 
1134
 
 
1135
        $allow_important = $config->get('CSS.AllowImportant');
 
1136
        // wrap all attr-defs with decorator that handles !important
 
1137
        foreach ($this->info as $k => $v) {
 
1138
            $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important);
 
1139
        }
 
1140
 
 
1141
        $this->setupConfigStuff($config);
 
1142
    }
 
1143
 
 
1144
    protected function doSetupProprietary($config) {
 
1145
        // Internet Explorer only scrollbar colors
 
1146
        $this->info['scrollbar-arrow-color']        = new HTMLPurifier_AttrDef_CSS_Color();
 
1147
        $this->info['scrollbar-base-color']         = new HTMLPurifier_AttrDef_CSS_Color();
 
1148
        $this->info['scrollbar-darkshadow-color']   = new HTMLPurifier_AttrDef_CSS_Color();
 
1149
        $this->info['scrollbar-face-color']         = new HTMLPurifier_AttrDef_CSS_Color();
 
1150
        $this->info['scrollbar-highlight-color']    = new HTMLPurifier_AttrDef_CSS_Color();
 
1151
        $this->info['scrollbar-shadow-color']       = new HTMLPurifier_AttrDef_CSS_Color();
 
1152
 
 
1153
        // technically not proprietary, but CSS3, and no one supports it
 
1154
        $this->info['opacity']          = new HTMLPurifier_AttrDef_CSS_AlphaValue();
 
1155
        $this->info['-moz-opacity']     = new HTMLPurifier_AttrDef_CSS_AlphaValue();
 
1156
        $this->info['-khtml-opacity']   = new HTMLPurifier_AttrDef_CSS_AlphaValue();
 
1157
 
 
1158
        // only opacity, for now
 
1159
        $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter();
 
1160
 
 
1161
    }
 
1162
 
 
1163
    protected function doSetupTricky($config) {
 
1164
        $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array(
 
1165
            'inline', 'block', 'list-item', 'run-in', 'compact',
 
1166
            'marker', 'table', 'inline-table', 'table-row-group',
 
1167
            'table-header-group', 'table-footer-group', 'table-row',
 
1168
            'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none'
 
1169
        ));
 
1170
        $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array(
 
1171
            'visible', 'hidden', 'collapse'
 
1172
        ));
 
1173
        $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll'));
 
1174
    }
 
1175
 
 
1176
 
 
1177
    /**
 
1178
     * Performs extra config-based processing. Based off of
 
1179
     * HTMLPurifier_HTMLDefinition.
 
1180
     * @todo Refactor duplicate elements into common class (probably using
 
1181
     *       composition, not inheritance).
 
1182
     */
 
1183
    protected function setupConfigStuff($config) {
 
1184
 
 
1185
        // setup allowed elements
 
1186
        $support = "(for information on implementing this, see the ".
 
1187
                   "support forums) ";
 
1188
        $allowed_attributes = $config->get('CSS.AllowedProperties');
 
1189
        if ($allowed_attributes !== null) {
 
1190
            foreach ($this->info as $name => $d) {
 
1191
                if(!isset($allowed_attributes[$name])) unset($this->info[$name]);
 
1192
                unset($allowed_attributes[$name]);
 
1193
            }
 
1194
            // emit errors
 
1195
            foreach ($allowed_attributes as $name => $d) {
 
1196
                // :TODO: Is this htmlspecialchars() call really necessary?
 
1197
                $name = htmlspecialchars($name);
 
1198
                trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING);
 
1199
            }
 
1200
        }
 
1201
 
 
1202
    }
 
1203
}
 
1204
 
 
1205
 
 
1206
 
 
1207
 
 
1208
 
 
1209
/**
 
1210
 * Defines allowed child nodes and validates tokens against it.
 
1211
 */
 
1212
abstract class HTMLPurifier_ChildDef
 
1213
{
 
1214
    /**
 
1215
     * Type of child definition, usually right-most part of class name lowercase.
 
1216
     * Used occasionally in terms of context.
 
1217
     */
 
1218
    public $type;
 
1219
 
 
1220
    /**
 
1221
     * Bool that indicates whether or not an empty array of children is okay
 
1222
     *
 
1223
     * This is necessary for redundant checking when changes affecting
 
1224
     * a child node may cause a parent node to now be disallowed.
 
1225
     */
 
1226
    public $allow_empty;
 
1227
 
 
1228
    /**
 
1229
     * Lookup array of all elements that this definition could possibly allow
 
1230
     */
 
1231
    public $elements = array();
 
1232
 
 
1233
    /**
 
1234
     * Get lookup of tag names that should not close this element automatically.
 
1235
     * All other elements will do so.
 
1236
     */
 
1237
    public function getAllowedElements($config) {
 
1238
        return $this->elements;
 
1239
    }
 
1240
 
 
1241
    /**
 
1242
     * Validates nodes according to definition and returns modification.
 
1243
     *
 
1244
     * @param $tokens_of_children Array of HTMLPurifier_Token
 
1245
     * @param $config HTMLPurifier_Config object
 
1246
     * @param $context HTMLPurifier_Context object
 
1247
     * @return bool true to leave nodes as is
 
1248
     * @return bool false to remove parent node
 
1249
     * @return array of replacement child tokens
 
1250
     */
 
1251
    abstract public function validateChildren($tokens_of_children, $config, $context);
 
1252
}
 
1253
 
 
1254
 
 
1255
 
 
1256
 
 
1257
 
 
1258
/**
 
1259
 * Configuration object that triggers customizable behavior.
 
1260
 *
 
1261
 * @warning This class is strongly defined: that means that the class
 
1262
 *          will fail if an undefined directive is retrieved or set.
 
1263
 *
 
1264
 * @note Many classes that could (although many times don't) use the
 
1265
 *       configuration object make it a mandatory parameter.  This is
 
1266
 *       because a configuration object should always be forwarded,
 
1267
 *       otherwise, you run the risk of missing a parameter and then
 
1268
 *       being stumped when a configuration directive doesn't work.
 
1269
 *
 
1270
 * @todo Reconsider some of the public member variables
 
1271
 */
 
1272
class HTMLPurifier_Config
 
1273
{
 
1274
 
 
1275
    /**
 
1276
     * HTML Purifier's version
 
1277
     */
 
1278
    public $version = '4.0.0';
 
1279
 
 
1280
    /**
 
1281
     * Bool indicator whether or not to automatically finalize
 
1282
     * the object if a read operation is done
 
1283
     */
 
1284
    public $autoFinalize = true;
 
1285
 
 
1286
    // protected member variables
 
1287
 
 
1288
    /**
 
1289
     * Namespace indexed array of serials for specific namespaces (see
 
1290
     * getSerial() for more info).
 
1291
     */
 
1292
    protected $serials = array();
 
1293
 
 
1294
    /**
 
1295
     * Serial for entire configuration object
 
1296
     */
 
1297
    protected $serial;
 
1298
 
 
1299
    /**
 
1300
     * Parser for variables
 
1301
     */
 
1302
    protected $parser;
 
1303
 
 
1304
    /**
 
1305
     * Reference HTMLPurifier_ConfigSchema for value checking
 
1306
     * @note This is public for introspective purposes. Please don't
 
1307
     *       abuse!
 
1308
     */
 
1309
    public $def;
 
1310
 
 
1311
    /**
 
1312
     * Indexed array of definitions
 
1313
     */
 
1314
    protected $definitions;
 
1315
 
 
1316
    /**
 
1317
     * Bool indicator whether or not config is finalized
 
1318
     */
 
1319
    protected $finalized = false;
 
1320
 
 
1321
    /**
 
1322
     * Property list containing configuration directives.
 
1323
     */
 
1324
    protected $plist;
 
1325
 
 
1326
    /**
 
1327
     * Whether or not a set is taking place due to an
 
1328
     * alias lookup.
 
1329
     */
 
1330
    private $aliasMode;
 
1331
 
 
1332
    /**
 
1333
     * Set to false if you do not want line and file numbers in errors
 
1334
     * (useful when unit testing)
 
1335
     */
 
1336
    public $chatty = true;
 
1337
 
 
1338
    /**
 
1339
     * Current lock; only gets to this namespace are allowed.
 
1340
     */
 
1341
    private $lock;
 
1342
 
 
1343
    /**
 
1344
     * @param $definition HTMLPurifier_ConfigSchema that defines what directives
 
1345
     *                    are allowed.
 
1346
     */
 
1347
    public function __construct($definition, $parent = null) {
 
1348
        $parent = $parent ? $parent : $definition->defaultPlist;
 
1349
        $this->plist = new HTMLPurifier_PropertyList($parent);
 
1350
        $this->def = $definition; // keep a copy around for checking
 
1351
        $this->parser = new HTMLPurifier_VarParser_Flexible();
 
1352
    }
 
1353
 
 
1354
    /**
 
1355
     * Convenience constructor that creates a config object based on a mixed var
 
1356
     * @param mixed $config Variable that defines the state of the config
 
1357
     *                      object. Can be: a HTMLPurifier_Config() object,
 
1358
     *                      an array of directives based on loadArray(),
 
1359
     *                      or a string filename of an ini file.
 
1360
     * @param HTMLPurifier_ConfigSchema Schema object
 
1361
     * @return Configured HTMLPurifier_Config object
 
1362
     */
 
1363
    public static function create($config, $schema = null) {
 
1364
        if ($config instanceof HTMLPurifier_Config) {
 
1365
            // pass-through
 
1366
            return $config;
 
1367
        }
 
1368
        if (!$schema) {
 
1369
            $ret = HTMLPurifier_Config::createDefault();
 
1370
        } else {
 
1371
            $ret = new HTMLPurifier_Config($schema);
 
1372
        }
 
1373
        if (is_string($config)) $ret->loadIni($config);
 
1374
        elseif (is_array($config)) $ret->loadArray($config);
 
1375
        return $ret;
 
1376
    }
 
1377
 
 
1378
    /**
 
1379
     * Creates a new config object that inherits from a previous one.
 
1380
     * @param HTMLPurifier_Config $config Configuration object to inherit
 
1381
     *        from.
 
1382
     * @return HTMLPurifier_Config object with $config as its parent.
 
1383
     */
 
1384
    public static function inherit(HTMLPurifier_Config $config) {
 
1385
        return new HTMLPurifier_Config($config->def, $config->plist);
 
1386
    }
 
1387
 
 
1388
    /**
 
1389
     * Convenience constructor that creates a default configuration object.
 
1390
     * @return Default HTMLPurifier_Config object.
 
1391
     */
 
1392
    public static function createDefault() {
 
1393
        $definition = HTMLPurifier_ConfigSchema::instance();
 
1394
        $config = new HTMLPurifier_Config($definition);
 
1395
        return $config;
 
1396
    }
 
1397
 
 
1398
    /**
 
1399
     * Retreives a value from the configuration.
 
1400
     * @param $key String key
 
1401
     */
 
1402
    public function get($key, $a = null) {
 
1403
        if ($a !== null) {
 
1404
            $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING);
 
1405
            $key = "$key.$a";
 
1406
        }
 
1407
        if (!$this->finalized) $this->autoFinalize();
 
1408
        if (!isset($this->def->info[$key])) {
 
1409
            // can't add % due to SimpleTest bug
 
1410
            $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
 
1411
                E_USER_WARNING);
 
1412
            return;
 
1413
        }
 
1414
        if (isset($this->def->info[$key]->isAlias)) {
 
1415
            $d = $this->def->info[$key];
 
1416
            $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key,
 
1417
                E_USER_ERROR);
 
1418
            return;
 
1419
        }
 
1420
        if ($this->lock) {
 
1421
            list($ns) = explode('.', $key);
 
1422
            if ($ns !== $this->lock) {
 
1423
                $this->triggerError('Cannot get value of namespace ' . $ns . ' when lock for ' . $this->lock . ' is active, this probably indicates a Definition setup method is accessing directives that are not within its namespace', E_USER_ERROR);
 
1424
                return;
 
1425
            }
 
1426
        }
 
1427
        return $this->plist->get($key);
 
1428
    }
 
1429
 
 
1430
    /**
 
1431
     * Retreives an array of directives to values from a given namespace
 
1432
     * @param $namespace String namespace
 
1433
     */
 
1434
    public function getBatch($namespace) {
 
1435
        if (!$this->finalized) $this->autoFinalize();
 
1436
        $full = $this->getAll();
 
1437
        if (!isset($full[$namespace])) {
 
1438
            $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace),
 
1439
                E_USER_WARNING);
 
1440
            return;
 
1441
        }
 
1442
        return $full[$namespace];
 
1443
    }
 
1444
 
 
1445
    /**
 
1446
     * Returns a md5 signature of a segment of the configuration object
 
1447
     * that uniquely identifies that particular configuration
 
1448
     * @note Revision is handled specially and is removed from the batch
 
1449
     *       before processing!
 
1450
     * @param $namespace Namespace to get serial for
 
1451
     */
 
1452
    public function getBatchSerial($namespace) {
 
1453
        if (empty($this->serials[$namespace])) {
 
1454
            $batch = $this->getBatch($namespace);
 
1455
            unset($batch['DefinitionRev']);
 
1456
            $this->serials[$namespace] = md5(serialize($batch));
 
1457
        }
 
1458
        return $this->serials[$namespace];
 
1459
    }
 
1460
 
 
1461
    /**
 
1462
     * Returns a md5 signature for the entire configuration object
 
1463
     * that uniquely identifies that particular configuration
 
1464
     */
 
1465
    public function getSerial() {
 
1466
        if (empty($this->serial)) {
 
1467
            $this->serial = md5(serialize($this->getAll()));
 
1468
        }
 
1469
        return $this->serial;
 
1470
    }
 
1471
 
 
1472
    /**
 
1473
     * Retrieves all directives, organized by namespace
 
1474
     * @warning This is a pretty inefficient function, avoid if you can
 
1475
     */
 
1476
    public function getAll() {
 
1477
        if (!$this->finalized) $this->autoFinalize();
 
1478
        $ret = array();
 
1479
        foreach ($this->plist->squash() as $name => $value) {
 
1480
            list($ns, $key) = explode('.', $name, 2);
 
1481
            $ret[$ns][$key] = $value;
 
1482
        }
 
1483
        return $ret;
 
1484
    }
 
1485
 
 
1486
    /**
 
1487
     * Sets a value to configuration.
 
1488
     * @param $key String key
 
1489
     * @param $value Mixed value
 
1490
     */
 
1491
    public function set($key, $value, $a = null) {
 
1492
        if (strpos($key, '.') === false) {
 
1493
            $namespace = $key;
 
1494
            $directive = $value;
 
1495
            $value = $a;
 
1496
            $key = "$key.$directive";
 
1497
            $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE);
 
1498
        } else {
 
1499
            list($namespace) = explode('.', $key);
 
1500
        }
 
1501
        if ($this->isFinalized('Cannot set directive after finalization')) return;
 
1502
        if (!isset($this->def->info[$key])) {
 
1503
            $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
 
1504
                E_USER_WARNING);
 
1505
            return;
 
1506
        }
 
1507
        $def = $this->def->info[$key];
 
1508
 
 
1509
        if (isset($def->isAlias)) {
 
1510
            if ($this->aliasMode) {
 
1511
                $this->triggerError('Double-aliases not allowed, please fix '.
 
1512
                    'ConfigSchema bug with' . $key, E_USER_ERROR);
 
1513
                return;
 
1514
            }
 
1515
            $this->aliasMode = true;
 
1516
            $this->set($def->key, $value);
 
1517
            $this->aliasMode = false;
 
1518
            $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE);
 
1519
            return;
 
1520
        }
 
1521
 
 
1522
        // Raw type might be negative when using the fully optimized form
 
1523
        // of stdclass, which indicates allow_null == true
 
1524
        $rtype = is_int($def) ? $def : $def->type;
 
1525
        if ($rtype < 0) {
 
1526
            $type = -$rtype;
 
1527
            $allow_null = true;
 
1528
        } else {
 
1529
            $type = $rtype;
 
1530
            $allow_null = isset($def->allow_null);
 
1531
        }
 
1532
 
 
1533
        try {
 
1534
            $value = $this->parser->parse($value, $type, $allow_null);
 
1535
        } catch (HTMLPurifier_VarParserException $e) {
 
1536
            $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING);
 
1537
            return;
 
1538
        }
 
1539
        if (is_string($value) && is_object($def)) {
 
1540
            // resolve value alias if defined
 
1541
            if (isset($def->aliases[$value])) {
 
1542
                $value = $def->aliases[$value];
 
1543
            }
 
1544
            // check to see if the value is allowed
 
1545
            if (isset($def->allowed) && !isset($def->allowed[$value])) {
 
1546
                $this->triggerError('Value not supported, valid values are: ' .
 
1547
                    $this->_listify($def->allowed), E_USER_WARNING);
 
1548
                return;
 
1549
            }
 
1550
        }
 
1551
        $this->plist->set($key, $value);
 
1552
 
 
1553
        // reset definitions if the directives they depend on changed
 
1554
        // this is a very costly process, so it's discouraged
 
1555
        // with finalization
 
1556
        if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
 
1557
            $this->definitions[$namespace] = null;
 
1558
        }
 
1559
 
 
1560
        $this->serials[$namespace] = false;
 
1561
    }
 
1562
 
 
1563
    /**
 
1564
     * Convenience function for error reporting
 
1565
     */
 
1566
    private function _listify($lookup) {
 
1567
        $list = array();
 
1568
        foreach ($lookup as $name => $b) $list[] = $name;
 
1569
        return implode(', ', $list);
 
1570
    }
 
1571
 
 
1572
    /**
 
1573
     * Retrieves object reference to the HTML definition.
 
1574
     * @param $raw Return a copy that has not been setup yet. Must be
 
1575
     *             called before it's been setup, otherwise won't work.
 
1576
     */
 
1577
    public function getHTMLDefinition($raw = false) {
 
1578
        return $this->getDefinition('HTML', $raw);
 
1579
    }
 
1580
 
 
1581
    /**
 
1582
     * Retrieves object reference to the CSS definition
 
1583
     * @param $raw Return a copy that has not been setup yet. Must be
 
1584
     *             called before it's been setup, otherwise won't work.
 
1585
     */
 
1586
    public function getCSSDefinition($raw = false) {
 
1587
        return $this->getDefinition('CSS', $raw);
 
1588
    }
 
1589
 
 
1590
    /**
 
1591
     * Retrieves a definition
 
1592
     * @param $type Type of definition: HTML, CSS, etc
 
1593
     * @param $raw  Whether or not definition should be returned raw
 
1594
     */
 
1595
    public function getDefinition($type, $raw = false) {
 
1596
        if (!$this->finalized) $this->autoFinalize();
 
1597
        // temporarily suspend locks, so we can handle recursive definition calls
 
1598
        $lock = $this->lock;
 
1599
        $this->lock = null;
 
1600
        $factory = HTMLPurifier_DefinitionCacheFactory::instance();
 
1601
        $cache = $factory->create($type, $this);
 
1602
        $this->lock = $lock;
 
1603
        if (!$raw) {
 
1604
            // see if we can quickly supply a definition
 
1605
            if (!empty($this->definitions[$type])) {
 
1606
                if (!$this->definitions[$type]->setup) {
 
1607
                    $this->definitions[$type]->setup($this);
 
1608
                    $cache->set($this->definitions[$type], $this);
 
1609
                }
 
1610
                return $this->definitions[$type];
 
1611
            }
 
1612
            // memory check missed, try cache
 
1613
            $this->definitions[$type] = $cache->get($this);
 
1614
            if ($this->definitions[$type]) {
 
1615
                // definition in cache, return it
 
1616
                return $this->definitions[$type];
 
1617
            }
 
1618
        } elseif (
 
1619
            !empty($this->definitions[$type]) &&
 
1620
            !$this->definitions[$type]->setup
 
1621
        ) {
 
1622
            // raw requested, raw in memory, quick return
 
1623
            return $this->definitions[$type];
 
1624
        }
 
1625
        // quick checks failed, let's create the object
 
1626
        if ($type == 'HTML') {
 
1627
            $this->definitions[$type] = new HTMLPurifier_HTMLDefinition();
 
1628
        } elseif ($type == 'CSS') {
 
1629
            $this->definitions[$type] = new HTMLPurifier_CSSDefinition();
 
1630
        } elseif ($type == 'URI') {
 
1631
            $this->definitions[$type] = new HTMLPurifier_URIDefinition();
 
1632
        } else {
 
1633
            throw new HTMLPurifier_Exception("Definition of $type type not supported");
 
1634
        }
 
1635
        // quick abort if raw
 
1636
        if ($raw) {
 
1637
            if (is_null($this->get($type . '.DefinitionID'))) {
 
1638
                // fatally error out if definition ID not set
 
1639
                throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
 
1640
            }
 
1641
            return $this->definitions[$type];
 
1642
        }
 
1643
        // set it up
 
1644
        $this->lock = $type;
 
1645
        $this->definitions[$type]->setup($this);
 
1646
        $this->lock = null;
 
1647
        // save in cache
 
1648
        $cache->set($this->definitions[$type], $this);
 
1649
        return $this->definitions[$type];
 
1650
    }
 
1651
 
 
1652
    /**
 
1653
     * Loads configuration values from an array with the following structure:
 
1654
     * Namespace.Directive => Value
 
1655
     * @param $config_array Configuration associative array
 
1656
     */
 
1657
    public function loadArray($config_array) {
 
1658
        if ($this->isFinalized('Cannot load directives after finalization')) return;
 
1659
        foreach ($config_array as $key => $value) {
 
1660
            $key = str_replace('_', '.', $key);
 
1661
            if (strpos($key, '.') !== false) {
 
1662
                $this->set($key, $value);
 
1663
            } else {
 
1664
                $namespace = $key;
 
1665
                $namespace_values = $value;
 
1666
                foreach ($namespace_values as $directive => $value) {
 
1667
                    $this->set($namespace .'.'. $directive, $value);
 
1668
                }
 
1669
            }
 
1670
        }
 
1671
    }
 
1672
 
 
1673
    /**
 
1674
     * Returns a list of array(namespace, directive) for all directives
 
1675
     * that are allowed in a web-form context as per an allowed
 
1676
     * namespaces/directives list.
 
1677
     * @param $allowed List of allowed namespaces/directives
 
1678
     */
 
1679
    public static function getAllowedDirectivesForForm($allowed, $schema = null) {
 
1680
        if (!$schema) {
 
1681
            $schema = HTMLPurifier_ConfigSchema::instance();
 
1682
        }
 
1683
        if ($allowed !== true) {
 
1684
             if (is_string($allowed)) $allowed = array($allowed);
 
1685
             $allowed_ns = array();
 
1686
             $allowed_directives = array();
 
1687
             $blacklisted_directives = array();
 
1688
             foreach ($allowed as $ns_or_directive) {
 
1689
                 if (strpos($ns_or_directive, '.') !== false) {
 
1690
                     // directive
 
1691
                     if ($ns_or_directive[0] == '-') {
 
1692
                         $blacklisted_directives[substr($ns_or_directive, 1)] = true;
 
1693
                     } else {
 
1694
                         $allowed_directives[$ns_or_directive] = true;
 
1695
                     }
 
1696
                 } else {
 
1697
                     // namespace
 
1698
                     $allowed_ns[$ns_or_directive] = true;
 
1699
                 }
 
1700
             }
 
1701
        }
 
1702
        $ret = array();
 
1703
        foreach ($schema->info as $key => $def) {
 
1704
            list($ns, $directive) = explode('.', $key, 2);
 
1705
            if ($allowed !== true) {
 
1706
                if (isset($blacklisted_directives["$ns.$directive"])) continue;
 
1707
                if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue;
 
1708
            }
 
1709
            if (isset($def->isAlias)) continue;
 
1710
            if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue;
 
1711
            $ret[] = array($ns, $directive);
 
1712
        }
 
1713
        return $ret;
 
1714
    }
 
1715
 
 
1716
    /**
 
1717
     * Loads configuration values from $_GET/$_POST that were posted
 
1718
     * via ConfigForm
 
1719
     * @param $array $_GET or $_POST array to import
 
1720
     * @param $index Index/name that the config variables are in
 
1721
     * @param $allowed List of allowed namespaces/directives
 
1722
     * @param $mq_fix Boolean whether or not to enable magic quotes fix
 
1723
     * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy
 
1724
     */
 
1725
    public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
 
1726
        $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
 
1727
        $config = HTMLPurifier_Config::create($ret, $schema);
 
1728
        return $config;
 
1729
    }
 
1730
 
 
1731
    /**
 
1732
     * Merges in configuration values from $_GET/$_POST to object. NOT STATIC.
 
1733
     * @note Same parameters as loadArrayFromForm
 
1734
     */
 
1735
    public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) {
 
1736
         $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
 
1737
         $this->loadArray($ret);
 
1738
    }
 
1739
 
 
1740
    /**
 
1741
     * Prepares an array from a form into something usable for the more
 
1742
     * strict parts of HTMLPurifier_Config
 
1743
     */
 
1744
    public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
 
1745
        if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
 
1746
        $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
 
1747
 
 
1748
        $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
 
1749
        $ret = array();
 
1750
        foreach ($allowed as $key) {
 
1751
            list($ns, $directive) = $key;
 
1752
            $skey = "$ns.$directive";
 
1753
            if (!empty($array["Null_$skey"])) {
 
1754
                $ret[$ns][$directive] = null;
 
1755
                continue;
 
1756
            }
 
1757
            if (!isset($array[$skey])) continue;
 
1758
            $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
 
1759
            $ret[$ns][$directive] = $value;
 
1760
        }
 
1761
        return $ret;
 
1762
    }
 
1763
 
 
1764
    /**
 
1765
     * Loads configuration values from an ini file
 
1766
     * @param $filename Name of ini file
 
1767
     */
 
1768
    public function loadIni($filename) {
 
1769
        if ($this->isFinalized('Cannot load directives after finalization')) return;
 
1770
        $array = parse_ini_file($filename, true);
 
1771
        $this->loadArray($array);
 
1772
    }
 
1773
 
 
1774
    /**
 
1775
     * Checks whether or not the configuration object is finalized.
 
1776
     * @param $error String error message, or false for no error
 
1777
     */
 
1778
    public function isFinalized($error = false) {
 
1779
        if ($this->finalized && $error) {
 
1780
            $this->triggerError($error, E_USER_ERROR);
 
1781
        }
 
1782
        return $this->finalized;
 
1783
    }
 
1784
 
 
1785
    /**
 
1786
     * Finalizes configuration only if auto finalize is on and not
 
1787
     * already finalized
 
1788
     */
 
1789
    public function autoFinalize() {
 
1790
        if ($this->autoFinalize) {
 
1791
            $this->finalize();
 
1792
        } else {
 
1793
            $this->plist->squash(true);
 
1794
        }
 
1795
    }
 
1796
 
 
1797
    /**
 
1798
     * Finalizes a configuration object, prohibiting further change
 
1799
     */
 
1800
    public function finalize() {
 
1801
        $this->finalized = true;
 
1802
        unset($this->parser);
 
1803
    }
 
1804
 
 
1805
    /**
 
1806
     * Produces a nicely formatted error message by supplying the
 
1807
     * stack frame information from two levels up and OUTSIDE of
 
1808
     * HTMLPurifier_Config.
 
1809
     */
 
1810
    protected function triggerError($msg, $no) {
 
1811
        // determine previous stack frame
 
1812
        $backtrace = debug_backtrace();
 
1813
        if ($this->chatty && isset($backtrace[1])) {
 
1814
            $frame = $backtrace[1];
 
1815
            $extra = " on line {$frame['line']} in file {$frame['file']}";
 
1816
        } else {
 
1817
            $extra = '';
 
1818
        }
 
1819
        trigger_error($msg . $extra, $no);
 
1820
    }
 
1821
 
 
1822
    /**
 
1823
     * Returns a serialized form of the configuration object that can
 
1824
     * be reconstituted.
 
1825
     */
 
1826
    public function serialize() {
 
1827
        $this->getDefinition('HTML');
 
1828
        $this->getDefinition('CSS');
 
1829
        $this->getDefinition('URI');
 
1830
        return serialize($this);
 
1831
    }
 
1832
 
 
1833
}
 
1834
 
 
1835
 
 
1836
 
 
1837
 
 
1838
 
 
1839
/**
 
1840
 * Configuration definition, defines directives and their defaults.
 
1841
 */
 
1842
class HTMLPurifier_ConfigSchema {
 
1843
 
 
1844
    /**
 
1845
     * Defaults of the directives and namespaces.
 
1846
     * @note This shares the exact same structure as HTMLPurifier_Config::$conf
 
1847
     */
 
1848
    public $defaults = array();
 
1849
 
 
1850
    /**
 
1851
     * The default property list. Do not edit this property list.
 
1852
     */
 
1853
    public $defaultPlist;
 
1854
 
 
1855
    /**
 
1856
     * Definition of the directives. The structure of this is:
 
1857
     *
 
1858
     *  array(
 
1859
     *      'Namespace' => array(
 
1860
     *          'Directive' => new stdclass(),
 
1861
     *      )
 
1862
     *  )
 
1863
     *
 
1864
     * The stdclass may have the following properties:
 
1865
     *
 
1866
     *  - If isAlias isn't set:
 
1867
     *      - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
 
1868
     *      - allow_null: If set, this directive allows null values
 
1869
     *      - aliases: If set, an associative array of value aliases to real values
 
1870
     *      - allowed: If set, a lookup array of allowed (string) values
 
1871
     *  - If isAlias is set:
 
1872
     *      - namespace: Namespace this directive aliases to
 
1873
     *      - name: Directive name this directive aliases to
 
1874
     *
 
1875
     * In certain degenerate cases, stdclass will actually be an integer. In
 
1876
     * that case, the value is equivalent to an stdclass with the type
 
1877
     * property set to the integer. If the integer is negative, type is
 
1878
     * equal to the absolute value of integer, and allow_null is true.
 
1879
     *
 
1880
     * This class is friendly with HTMLPurifier_Config. If you need introspection
 
1881
     * about the schema, you're better of using the ConfigSchema_Interchange,
 
1882
     * which uses more memory but has much richer information.
 
1883
     */
 
1884
    public $info = array();
 
1885
 
 
1886
    /**
 
1887
     * Application-wide singleton
 
1888
     */
 
1889
    static protected $singleton;
 
1890
 
 
1891
    public function __construct() {
 
1892
        $this->defaultPlist = new HTMLPurifier_PropertyList();
 
1893
    }
 
1894
 
 
1895
    /**
 
1896
     * Unserializes the default ConfigSchema.
 
1897
     */
 
1898
    public static function makeFromSerial() {
 
1899
        return unserialize(file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'));
 
1900
    }
 
1901
 
 
1902
    /**
 
1903
     * Retrieves an instance of the application-wide configuration definition.
 
1904
     */
 
1905
    public static function instance($prototype = null) {
 
1906
        if ($prototype !== null) {
 
1907
            HTMLPurifier_ConfigSchema::$singleton = $prototype;
 
1908
        } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
 
1909
            HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
 
1910
        }
 
1911
        return HTMLPurifier_ConfigSchema::$singleton;
 
1912
    }
 
1913
 
 
1914
    /**
 
1915
     * Defines a directive for configuration
 
1916
     * @warning Will fail of directive's namespace is defined.
 
1917
     * @warning This method's signature is slightly different from the legacy
 
1918
     *          define() static method! Beware!
 
1919
     * @param $namespace Namespace the directive is in
 
1920
     * @param $name Key of directive
 
1921
     * @param $default Default value of directive
 
1922
     * @param $type Allowed type of the directive. See
 
1923
     *      HTMLPurifier_DirectiveDef::$type for allowed values
 
1924
     * @param $allow_null Whether or not to allow null values
 
1925
     */
 
1926
    public function add($key, $default, $type, $allow_null) {
 
1927
        $obj = new stdclass();
 
1928
        $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
 
1929
        if ($allow_null) $obj->allow_null = true;
 
1930
        $this->info[$key] = $obj;
 
1931
        $this->defaults[$key] = $default;
 
1932
        $this->defaultPlist->set($key, $default);
 
1933
    }
 
1934
 
 
1935
    /**
 
1936
     * Defines a directive value alias.
 
1937
     *
 
1938
     * Directive value aliases are convenient for developers because it lets
 
1939
     * them set a directive to several values and get the same result.
 
1940
     * @param $namespace Directive's namespace
 
1941
     * @param $name Name of Directive
 
1942
     * @param $aliases Hash of aliased values to the real alias
 
1943
     */
 
1944
    public function addValueAliases($key, $aliases) {
 
1945
        if (!isset($this->info[$key]->aliases)) {
 
1946
            $this->info[$key]->aliases = array();
 
1947
        }
 
1948
        foreach ($aliases as $alias => $real) {
 
1949
            $this->info[$key]->aliases[$alias] = $real;
 
1950
        }
 
1951
    }
 
1952
 
 
1953
    /**
 
1954
     * Defines a set of allowed values for a directive.
 
1955
     * @warning This is slightly different from the corresponding static
 
1956
     *          method definition.
 
1957
     * @param $namespace Namespace of directive
 
1958
     * @param $name Name of directive
 
1959
     * @param $allowed Lookup array of allowed values
 
1960
     */
 
1961
    public function addAllowedValues($key, $allowed) {
 
1962
        $this->info[$key]->allowed = $allowed;
 
1963
    }
 
1964
 
 
1965
    /**
 
1966
     * Defines a directive alias for backwards compatibility
 
1967
     * @param $namespace
 
1968
     * @param $name Directive that will be aliased
 
1969
     * @param $new_namespace
 
1970
     * @param $new_name Directive that the alias will be to
 
1971
     */
 
1972
    public function addAlias($key, $new_key) {
 
1973
        $obj = new stdclass;
 
1974
        $obj->key = $new_key;
 
1975
        $obj->isAlias = true;
 
1976
        $this->info[$key] = $obj;
 
1977
    }
 
1978
 
 
1979
    /**
 
1980
     * Replaces any stdclass that only has the type property with type integer.
 
1981
     */
 
1982
    public function postProcess() {
 
1983
        foreach ($this->info as $key => $v) {
 
1984
            if (count((array) $v) == 1) {
 
1985
                $this->info[$key] = $v->type;
 
1986
            } elseif (count((array) $v) == 2 && isset($v->allow_null)) {
 
1987
                $this->info[$key] = -$v->type;
 
1988
            }
 
1989
        }
 
1990
    }
 
1991
 
 
1992
}
 
1993
 
 
1994
 
 
1995
 
 
1996
 
 
1997
 
 
1998
/**
 
1999
 * @todo Unit test
 
2000
 */
 
2001
class HTMLPurifier_ContentSets
 
2002
{
 
2003
 
 
2004
    /**
 
2005
     * List of content set strings (pipe seperators) indexed by name.
 
2006
     */
 
2007
    public $info = array();
 
2008
 
 
2009
    /**
 
2010
     * List of content set lookups (element => true) indexed by name.
 
2011
     * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
 
2012
     */
 
2013
    public $lookup = array();
 
2014
 
 
2015
    /**
 
2016
     * Synchronized list of defined content sets (keys of info)
 
2017
     */
 
2018
    protected $keys = array();
 
2019
    /**
 
2020
     * Synchronized list of defined content values (values of info)
 
2021
     */
 
2022
    protected $values = array();
 
2023
 
 
2024
    /**
 
2025
     * Merges in module's content sets, expands identifiers in the content
 
2026
     * sets and populates the keys, values and lookup member variables.
 
2027
     * @param $modules List of HTMLPurifier_HTMLModule
 
2028
     */
 
2029
    public function __construct($modules) {
 
2030
        if (!is_array($modules)) $modules = array($modules);
 
2031
        // populate content_sets based on module hints
 
2032
        // sorry, no way of overloading
 
2033
        foreach ($modules as $module_i => $module) {
 
2034
            foreach ($module->content_sets as $key => $value) {
 
2035
                $temp = $this->convertToLookup($value);
 
2036
                if (isset($this->lookup[$key])) {
 
2037
                    // add it into the existing content set
 
2038
                    $this->lookup[$key] = array_merge($this->lookup[$key], $temp);
 
2039
                } else {
 
2040
                    $this->lookup[$key] = $temp;
 
2041
                }
 
2042
            }
 
2043
        }
 
2044
        $old_lookup = false;
 
2045
        while ($old_lookup !== $this->lookup) {
 
2046
            $old_lookup = $this->lookup;
 
2047
            foreach ($this->lookup as $i => $set) {
 
2048
                $add = array();
 
2049
                foreach ($set as $element => $x) {
 
2050
                    if (isset($this->lookup[$element])) {
 
2051
                        $add += $this->lookup[$element];
 
2052
                        unset($this->lookup[$i][$element]);
 
2053
                    }
 
2054
                }
 
2055
                $this->lookup[$i] += $add;
 
2056
            }
 
2057
        }
 
2058
 
 
2059
        foreach ($this->lookup as $key => $lookup) {
 
2060
            $this->info[$key] = implode(' | ', array_keys($lookup));
 
2061
        }
 
2062
        $this->keys   = array_keys($this->info);
 
2063
        $this->values = array_values($this->info);
 
2064
    }
 
2065
 
 
2066
    /**
 
2067
     * Accepts a definition; generates and assigns a ChildDef for it
 
2068
     * @param $def HTMLPurifier_ElementDef reference
 
2069
     * @param $module Module that defined the ElementDef
 
2070
     */
 
2071
    public function generateChildDef(&$def, $module) {
 
2072
        if (!empty($def->child)) return; // already done!
 
2073
        $content_model = $def->content_model;
 
2074
        if (is_string($content_model)) {
 
2075
            // Assume that $this->keys is alphanumeric
 
2076
            $def->content_model = preg_replace_callback(
 
2077
                '/\b(' . implode('|', $this->keys) . ')\b/',
 
2078
                array($this, 'generateChildDefCallback'),
 
2079
                $content_model
 
2080
            );
 
2081
            //$def->content_model = str_replace(
 
2082
            //    $this->keys, $this->values, $content_model);
 
2083
        }
 
2084
        $def->child = $this->getChildDef($def, $module);
 
2085
    }
 
2086
 
 
2087
    public function generateChildDefCallback($matches) {
 
2088
        return $this->info[$matches[0]];
 
2089
    }
 
2090
 
 
2091
    /**
 
2092
     * Instantiates a ChildDef based on content_model and content_model_type
 
2093
     * member variables in HTMLPurifier_ElementDef
 
2094
     * @note This will also defer to modules for custom HTMLPurifier_ChildDef
 
2095
     *       subclasses that need content set expansion
 
2096
     * @param $def HTMLPurifier_ElementDef to have ChildDef extracted
 
2097
     * @return HTMLPurifier_ChildDef corresponding to ElementDef
 
2098
     */
 
2099
    public function getChildDef($def, $module) {
 
2100
        $value = $def->content_model;
 
2101
        if (is_object($value)) {
 
2102
            trigger_error(
 
2103
                'Literal object child definitions should be stored in '.
 
2104
                'ElementDef->child not ElementDef->content_model',
 
2105
                E_USER_NOTICE
 
2106
            );
 
2107
            return $value;
 
2108
        }
 
2109
        switch ($def->content_model_type) {
 
2110
            case 'required':
 
2111
                return new HTMLPurifier_ChildDef_Required($value);
 
2112
            case 'optional':
 
2113
                return new HTMLPurifier_ChildDef_Optional($value);
 
2114
            case 'empty':
 
2115
                return new HTMLPurifier_ChildDef_Empty();
 
2116
            case 'custom':
 
2117
                return new HTMLPurifier_ChildDef_Custom($value);
 
2118
        }
 
2119
        // defer to its module
 
2120
        $return = false;
 
2121
        if ($module->defines_child_def) { // save a func call
 
2122
            $return = $module->getChildDef($def);
 
2123
        }
 
2124
        if ($return !== false) return $return;
 
2125
        // error-out
 
2126
        trigger_error(
 
2127
            'Could not determine which ChildDef class to instantiate',
 
2128
            E_USER_ERROR
 
2129
        );
 
2130
        return false;
 
2131
    }
 
2132
 
 
2133
    /**
 
2134
     * Converts a string list of elements separated by pipes into
 
2135
     * a lookup array.
 
2136
     * @param $string List of elements
 
2137
     * @return Lookup array of elements
 
2138
     */
 
2139
    protected function convertToLookup($string) {
 
2140
        $array = explode('|', str_replace(' ', '', $string));
 
2141
        $ret = array();
 
2142
        foreach ($array as $i => $k) {
 
2143
            $ret[$k] = true;
 
2144
        }
 
2145
        return $ret;
 
2146
    }
 
2147
 
 
2148
}
 
2149
 
 
2150
 
 
2151
 
 
2152
 
 
2153
 
 
2154
/**
 
2155
 * Registry object that contains information about the current context.
 
2156
 * @warning Is a bit buggy when variables are set to null: it thinks
 
2157
 *          they don't exist! So use false instead, please.
 
2158
 * @note Since the variables Context deals with may not be objects,
 
2159
 *       references are very important here! Do not remove!
 
2160
 */
 
2161
class HTMLPurifier_Context
 
2162
{
 
2163
 
 
2164
    /**
 
2165
     * Private array that stores the references.
 
2166
     */
 
2167
    private $_storage = array();
 
2168
 
 
2169
    /**
 
2170
     * Registers a variable into the context.
 
2171
     * @param $name String name
 
2172
     * @param $ref Reference to variable to be registered
 
2173
     */
 
2174
    public function register($name, &$ref) {
 
2175
        if (isset($this->_storage[$name])) {
 
2176
            trigger_error("Name $name produces collision, cannot re-register",
 
2177
                          E_USER_ERROR);
 
2178
            return;
 
2179
        }
 
2180
        $this->_storage[$name] =& $ref;
 
2181
    }
 
2182
 
 
2183
    /**
 
2184
     * Retrieves a variable reference from the context.
 
2185
     * @param $name String name
 
2186
     * @param $ignore_error Boolean whether or not to ignore error
 
2187
     */
 
2188
    public function &get($name, $ignore_error = false) {
 
2189
        if (!isset($this->_storage[$name])) {
 
2190
            if (!$ignore_error) {
 
2191
                trigger_error("Attempted to retrieve non-existent variable $name",
 
2192
                              E_USER_ERROR);
 
2193
            }
 
2194
            $var = null; // so we can return by reference
 
2195
            return $var;
 
2196
        }
 
2197
        return $this->_storage[$name];
 
2198
    }
 
2199
 
 
2200
    /**
 
2201
     * Destorys a variable in the context.
 
2202
     * @param $name String name
 
2203
     */
 
2204
    public function destroy($name) {
 
2205
        if (!isset($this->_storage[$name])) {
 
2206
            trigger_error("Attempted to destroy non-existent variable $name",
 
2207
                          E_USER_ERROR);
 
2208
            return;
 
2209
        }
 
2210
        unset($this->_storage[$name]);
 
2211
    }
 
2212
 
 
2213
    /**
 
2214
     * Checks whether or not the variable exists.
 
2215
     * @param $name String name
 
2216
     */
 
2217
    public function exists($name) {
 
2218
        return isset($this->_storage[$name]);
 
2219
    }
 
2220
 
 
2221
    /**
 
2222
     * Loads a series of variables from an associative array
 
2223
     * @param $context_array Assoc array of variables to load
 
2224
     */
 
2225
    public function loadArray($context_array) {
 
2226
        foreach ($context_array as $key => $discard) {
 
2227
            $this->register($key, $context_array[$key]);
 
2228
        }
 
2229
    }
 
2230
 
 
2231
}
 
2232
 
 
2233
 
 
2234
 
 
2235
 
 
2236
 
 
2237
/**
 
2238
 * Abstract class representing Definition cache managers that implements
 
2239
 * useful common methods and is a factory.
 
2240
 * @todo Create a separate maintenance file advanced users can use to
 
2241
 *       cache their custom HTMLDefinition, which can be loaded
 
2242
 *       via a configuration directive
 
2243
 * @todo Implement memcached
 
2244
 */
 
2245
abstract class HTMLPurifier_DefinitionCache
 
2246
{
 
2247
 
 
2248
    public $type;
 
2249
 
 
2250
    /**
 
2251
     * @param $name Type of definition objects this instance of the
 
2252
     *      cache will handle.
 
2253
     */
 
2254
    public function __construct($type) {
 
2255
        $this->type = $type;
 
2256
    }
 
2257
 
 
2258
    /**
 
2259
     * Generates a unique identifier for a particular configuration
 
2260
     * @param Instance of HTMLPurifier_Config
 
2261
     */
 
2262
    public function generateKey($config) {
 
2263
        return $config->version . ',' . // possibly replace with function calls
 
2264
               $config->getBatchSerial($this->type) . ',' .
 
2265
               $config->get($this->type . '.DefinitionRev');
 
2266
    }
 
2267
 
 
2268
    /**
 
2269
     * Tests whether or not a key is old with respect to the configuration's
 
2270
     * version and revision number.
 
2271
     * @param $key Key to test
 
2272
     * @param $config Instance of HTMLPurifier_Config to test against
 
2273
     */
 
2274
    public function isOld($key, $config) {
 
2275
        if (substr_count($key, ',') < 2) return true;
 
2276
        list($version, $hash, $revision) = explode(',', $key, 3);
 
2277
        $compare = version_compare($version, $config->version);
 
2278
        // version mismatch, is always old
 
2279
        if ($compare != 0) return true;
 
2280
        // versions match, ids match, check revision number
 
2281
        if (
 
2282
            $hash == $config->getBatchSerial($this->type) &&
 
2283
            $revision < $config->get($this->type . '.DefinitionRev')
 
2284
        ) return true;
 
2285
        return false;
 
2286
    }
 
2287
 
 
2288
    /**
 
2289
     * Checks if a definition's type jives with the cache's type
 
2290
     * @note Throws an error on failure
 
2291
     * @param $def Definition object to check
 
2292
     * @return Boolean true if good, false if not
 
2293
     */
 
2294
    public function checkDefType($def) {
 
2295
        if ($def->type !== $this->type) {
 
2296
            trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}");
 
2297
            return false;
 
2298
        }
 
2299
        return true;
 
2300
    }
 
2301
 
 
2302
    /**
 
2303
     * Adds a definition object to the cache
 
2304
     */
 
2305
    abstract public function add($def, $config);
 
2306
 
 
2307
    /**
 
2308
     * Unconditionally saves a definition object to the cache
 
2309
     */
 
2310
    abstract public function set($def, $config);
 
2311
 
 
2312
    /**
 
2313
     * Replace an object in the cache
 
2314
     */
 
2315
    abstract public function replace($def, $config);
 
2316
 
 
2317
    /**
 
2318
     * Retrieves a definition object from the cache
 
2319
     */
 
2320
    abstract public function get($config);
 
2321
 
 
2322
    /**
 
2323
     * Removes a definition object to the cache
 
2324
     */
 
2325
    abstract public function remove($config);
 
2326
 
 
2327
    /**
 
2328
     * Clears all objects from cache
 
2329
     */
 
2330
    abstract public function flush($config);
 
2331
 
 
2332
    /**
 
2333
     * Clears all expired (older version or revision) objects from cache
 
2334
     * @note Be carefuly implementing this method as flush. Flush must
 
2335
     *       not interfere with other Definition types, and cleanup()
 
2336
     *       should not be repeatedly called by userland code.
 
2337
     */
 
2338
    abstract public function cleanup($config);
 
2339
 
 
2340
}
 
2341
 
 
2342
 
 
2343
 
 
2344
 
 
2345
 
 
2346
/**
 
2347
 * Responsible for creating definition caches.
 
2348
 */
 
2349
class HTMLPurifier_DefinitionCacheFactory
 
2350
{
 
2351
 
 
2352
    protected $caches = array('Serializer' => array());
 
2353
    protected $implementations = array();
 
2354
    protected $decorators = array();
 
2355
 
 
2356
    /**
 
2357
     * Initialize default decorators
 
2358
     */
 
2359
    public function setup() {
 
2360
        $this->addDecorator('Cleanup');
 
2361
    }
 
2362
 
 
2363
    /**
 
2364
     * Retrieves an instance of global definition cache factory.
 
2365
     */
 
2366
    public static function instance($prototype = null) {
 
2367
        static $instance;
 
2368
        if ($prototype !== null) {
 
2369
            $instance = $prototype;
 
2370
        } elseif ($instance === null || $prototype === true) {
 
2371
            $instance = new HTMLPurifier_DefinitionCacheFactory();
 
2372
            $instance->setup();
 
2373
        }
 
2374
        return $instance;
 
2375
    }
 
2376
 
 
2377
    /**
 
2378
     * Registers a new definition cache object
 
2379
     * @param $short Short name of cache object, for reference
 
2380
     * @param $long Full class name of cache object, for construction
 
2381
     */
 
2382
    public function register($short, $long) {
 
2383
        $this->implementations[$short] = $long;
 
2384
    }
 
2385
 
 
2386
    /**
 
2387
     * Factory method that creates a cache object based on configuration
 
2388
     * @param $name Name of definitions handled by cache
 
2389
     * @param $config Instance of HTMLPurifier_Config
 
2390
     */
 
2391
    public function create($type, $config) {
 
2392
        $method = $config->get('Cache.DefinitionImpl');
 
2393
        if ($method === null) {
 
2394
            return new HTMLPurifier_DefinitionCache_Null($type);
 
2395
        }
 
2396
        if (!empty($this->caches[$method][$type])) {
 
2397
            return $this->caches[$method][$type];
 
2398
        }
 
2399
        if (
 
2400
          isset($this->implementations[$method]) &&
 
2401
          class_exists($class = $this->implementations[$method], false)
 
2402
        ) {
 
2403
            $cache = new $class($type);
 
2404
        } else {
 
2405
            if ($method != 'Serializer') {
 
2406
                trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING);
 
2407
            }
 
2408
            $cache = new HTMLPurifier_DefinitionCache_Serializer($type);
 
2409
        }
 
2410
        foreach ($this->decorators as $decorator) {
 
2411
            $new_cache = $decorator->decorate($cache);
 
2412
            // prevent infinite recursion in PHP 4
 
2413
            unset($cache);
 
2414
            $cache = $new_cache;
 
2415
        }
 
2416
        $this->caches[$method][$type] = $cache;
 
2417
        return $this->caches[$method][$type];
 
2418
    }
 
2419
 
 
2420
    /**
 
2421
     * Registers a decorator to add to all new cache objects
 
2422
     * @param
 
2423
     */
 
2424
    public function addDecorator($decorator) {
 
2425
        if (is_string($decorator)) {
 
2426
            $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator";
 
2427
            $decorator = new $class;
 
2428
        }
 
2429
        $this->decorators[$decorator->name] = $decorator;
 
2430
    }
 
2431
 
 
2432
}
 
2433
 
 
2434
 
 
2435
 
 
2436
 
 
2437
 
 
2438
/**
 
2439
 * Represents a document type, contains information on which modules
 
2440
 * need to be loaded.
 
2441
 * @note This class is inspected by Printer_HTMLDefinition->renderDoctype.
 
2442
 *       If structure changes, please update that function.
 
2443
 */
 
2444
class HTMLPurifier_Doctype
 
2445
{
 
2446
    /**
 
2447
     * Full name of doctype
 
2448
     */
 
2449
    public $name;
 
2450
 
 
2451
    /**
 
2452
     * List of standard modules (string identifiers or literal objects)
 
2453
     * that this doctype uses
 
2454
     */
 
2455
    public $modules = array();
 
2456
 
 
2457
    /**
 
2458
     * List of modules to use for tidying up code
 
2459
     */
 
2460
    public $tidyModules = array();
 
2461
 
 
2462
    /**
 
2463
     * Is the language derived from XML (i.e. XHTML)?
 
2464
     */
 
2465
    public $xml = true;
 
2466
 
 
2467
    /**
 
2468
     * List of aliases for this doctype
 
2469
     */
 
2470
    public $aliases = array();
 
2471
 
 
2472
    /**
 
2473
     * Public DTD identifier
 
2474
     */
 
2475
    public $dtdPublic;
 
2476
 
 
2477
    /**
 
2478
     * System DTD identifier
 
2479
     */
 
2480
    public $dtdSystem;
 
2481
 
 
2482
    public function __construct($name = null, $xml = true, $modules = array(),
 
2483
        $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
 
2484
    ) {
 
2485
        $this->name         = $name;
 
2486
        $this->xml          = $xml;
 
2487
        $this->modules      = $modules;
 
2488
        $this->tidyModules  = $tidyModules;
 
2489
        $this->aliases      = $aliases;
 
2490
        $this->dtdPublic    = $dtd_public;
 
2491
        $this->dtdSystem    = $dtd_system;
 
2492
    }
 
2493
}
 
2494
 
 
2495
 
 
2496
 
 
2497
 
 
2498
 
 
2499
class HTMLPurifier_DoctypeRegistry
 
2500
{
 
2501
 
 
2502
    /**
 
2503
     * Hash of doctype names to doctype objects
 
2504
     */
 
2505
    protected $doctypes;
 
2506
 
 
2507
    /**
 
2508
     * Lookup table of aliases to real doctype names
 
2509
     */
 
2510
    protected $aliases;
 
2511
 
 
2512
    /**
 
2513
     * Registers a doctype to the registry
 
2514
     * @note Accepts a fully-formed doctype object, or the
 
2515
     *       parameters for constructing a doctype object
 
2516
     * @param $doctype Name of doctype or literal doctype object
 
2517
     * @param $modules Modules doctype will load
 
2518
     * @param $modules_for_modes Modules doctype will load for certain modes
 
2519
     * @param $aliases Alias names for doctype
 
2520
     * @return Editable registered doctype
 
2521
     */
 
2522
    public function register($doctype, $xml = true, $modules = array(),
 
2523
        $tidy_modules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
 
2524
    ) {
 
2525
        if (!is_array($modules)) $modules = array($modules);
 
2526
        if (!is_array($tidy_modules)) $tidy_modules = array($tidy_modules);
 
2527
        if (!is_array($aliases)) $aliases = array($aliases);
 
2528
        if (!is_object($doctype)) {
 
2529
            $doctype = new HTMLPurifier_Doctype(
 
2530
                $doctype, $xml, $modules, $tidy_modules, $aliases, $dtd_public, $dtd_system
 
2531
            );
 
2532
        }
 
2533
        $this->doctypes[$doctype->name] = $doctype;
 
2534
        $name = $doctype->name;
 
2535
        // hookup aliases
 
2536
        foreach ($doctype->aliases as $alias) {
 
2537
            if (isset($this->doctypes[$alias])) continue;
 
2538
            $this->aliases[$alias] = $name;
 
2539
        }
 
2540
        // remove old aliases
 
2541
        if (isset($this->aliases[$name])) unset($this->aliases[$name]);
 
2542
        return $doctype;
 
2543
    }
 
2544
 
 
2545
    /**
 
2546
     * Retrieves reference to a doctype of a certain name
 
2547
     * @note This function resolves aliases
 
2548
     * @note When possible, use the more fully-featured make()
 
2549
     * @param $doctype Name of doctype
 
2550
     * @return Editable doctype object
 
2551
     */
 
2552
    public function get($doctype) {
 
2553
        if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype];
 
2554
        if (!isset($this->doctypes[$doctype])) {
 
2555
            trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR);
 
2556
            $anon = new HTMLPurifier_Doctype($doctype);
 
2557
            return $anon;
 
2558
        }
 
2559
        return $this->doctypes[$doctype];
 
2560
    }
 
2561
 
 
2562
    /**
 
2563
     * Creates a doctype based on a configuration object,
 
2564
     * will perform initialization on the doctype
 
2565
     * @note Use this function to get a copy of doctype that config
 
2566
     *       can hold on to (this is necessary in order to tell
 
2567
     *       Generator whether or not the current document is XML
 
2568
     *       based or not).
 
2569
     */
 
2570
    public function make($config) {
 
2571
        return clone $this->get($this->getDoctypeFromConfig($config));
 
2572
    }
 
2573
 
 
2574
    /**
 
2575
     * Retrieves the doctype from the configuration object
 
2576
     */
 
2577
    public function getDoctypeFromConfig($config) {
 
2578
        // recommended test
 
2579
        $doctype = $config->get('HTML.Doctype');
 
2580
        if (!empty($doctype)) return $doctype;
 
2581
        $doctype = $config->get('HTML.CustomDoctype');
 
2582
        if (!empty($doctype)) return $doctype;
 
2583
        // backwards-compatibility
 
2584
        if ($config->get('HTML.XHTML')) {
 
2585
            $doctype = 'XHTML 1.0';
 
2586
        } else {
 
2587
            $doctype = 'HTML 4.01';
 
2588
        }
 
2589
        if ($config->get('HTML.Strict')) {
 
2590
            $doctype .= ' Strict';
 
2591
        } else {
 
2592
            $doctype .= ' Transitional';
 
2593
        }
 
2594
        return $doctype;
 
2595
    }
 
2596
 
 
2597
}
 
2598
 
 
2599
 
 
2600
 
 
2601
 
 
2602
 
 
2603
/**
 
2604
 * Structure that stores an HTML element definition. Used by
 
2605
 * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule.
 
2606
 * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition.
 
2607
 *       Please update that class too.
 
2608
 * @warning If you add new properties to this class, you MUST update
 
2609
 *          the mergeIn() method.
 
2610
 */
 
2611
class HTMLPurifier_ElementDef
 
2612
{
 
2613
 
 
2614
    /**
 
2615
     * Does the definition work by itself, or is it created solely
 
2616
     * for the purpose of merging into another definition?
 
2617
     */
 
2618
    public $standalone = true;
 
2619
 
 
2620
    /**
 
2621
     * Associative array of attribute name to HTMLPurifier_AttrDef
 
2622
     * @note Before being processed by HTMLPurifier_AttrCollections
 
2623
     *       when modules are finalized during
 
2624
     *       HTMLPurifier_HTMLDefinition->setup(), this array may also
 
2625
     *       contain an array at index 0 that indicates which attribute
 
2626
     *       collections to load into the full array. It may also
 
2627
     *       contain string indentifiers in lieu of HTMLPurifier_AttrDef,
 
2628
     *       see HTMLPurifier_AttrTypes on how they are expanded during
 
2629
     *       HTMLPurifier_HTMLDefinition->setup() processing.
 
2630
     */
 
2631
    public $attr = array();
 
2632
 
 
2633
    /**
 
2634
     * Indexed list of tag's HTMLPurifier_AttrTransform to be done before validation
 
2635
     */
 
2636
    public $attr_transform_pre = array();
 
2637
 
 
2638
    /**
 
2639
     * Indexed list of tag's HTMLPurifier_AttrTransform to be done after validation
 
2640
     */
 
2641
    public $attr_transform_post = array();
 
2642
 
 
2643
    /**
 
2644
     * HTMLPurifier_ChildDef of this tag.
 
2645
     */
 
2646
    public $child;
 
2647
 
 
2648
    /**
 
2649
     * Abstract string representation of internal ChildDef rules. See
 
2650
     * HTMLPurifier_ContentSets for how this is parsed and then transformed
 
2651
     * into an HTMLPurifier_ChildDef.
 
2652
     * @warning This is a temporary variable that is not available after
 
2653
     *      being processed by HTMLDefinition
 
2654
     */
 
2655
    public $content_model;
 
2656
 
 
2657
    /**
 
2658
     * Value of $child->type, used to determine which ChildDef to use,
 
2659
     * used in combination with $content_model.
 
2660
     * @warning This must be lowercase
 
2661
     * @warning This is a temporary variable that is not available after
 
2662
     *      being processed by HTMLDefinition
 
2663
     */
 
2664
    public $content_model_type;
 
2665
 
 
2666
 
 
2667
 
 
2668
    /**
 
2669
     * Does the element have a content model (#PCDATA | Inline)*? This
 
2670
     * is important for chameleon ins and del processing in
 
2671
     * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't
 
2672
     * have to worry about this one.
 
2673
     */
 
2674
    public $descendants_are_inline = false;
 
2675
 
 
2676
    /**
 
2677
     * List of the names of required attributes this element has. Dynamically
 
2678
     * populated by HTMLPurifier_HTMLDefinition::getElement
 
2679
     */
 
2680
    public $required_attr = array();
 
2681
 
 
2682
    /**
 
2683
     * Lookup table of tags excluded from all descendants of this tag.
 
2684
     * @note SGML permits exclusions for all descendants, but this is
 
2685
     *       not possible with DTDs or XML Schemas. W3C has elected to
 
2686
     *       use complicated compositions of content_models to simulate
 
2687
     *       exclusion for children, but we go the simpler, SGML-style
 
2688
     *       route of flat-out exclusions, which correctly apply to
 
2689
     *       all descendants and not just children. Note that the XHTML
 
2690
     *       Modularization Abstract Modules are blithely unaware of such
 
2691
     *       distinctions.
 
2692
     */
 
2693
    public $excludes = array();
 
2694
 
 
2695
    /**
 
2696
     * This tag is explicitly auto-closed by the following tags.
 
2697
     */
 
2698
    public $autoclose = array();
 
2699
 
 
2700
    /**
 
2701
     * Whether or not this is a formatting element affected by the
 
2702
     * "Active Formatting Elements" algorithm.
 
2703
     */
 
2704
    public $formatting;
 
2705
 
 
2706
    /**
 
2707
     * Low-level factory constructor for creating new standalone element defs
 
2708
     */
 
2709
    public static function create($content_model, $content_model_type, $attr) {
 
2710
        $def = new HTMLPurifier_ElementDef();
 
2711
        $def->content_model = $content_model;
 
2712
        $def->content_model_type = $content_model_type;
 
2713
        $def->attr = $attr;
 
2714
        return $def;
 
2715
    }
 
2716
 
 
2717
    /**
 
2718
     * Merges the values of another element definition into this one.
 
2719
     * Values from the new element def take precedence if a value is
 
2720
     * not mergeable.
 
2721
     */
 
2722
    public function mergeIn($def) {
 
2723
 
 
2724
        // later keys takes precedence
 
2725
        foreach($def->attr as $k => $v) {
 
2726
            if ($k === 0) {
 
2727
                // merge in the includes
 
2728
                // sorry, no way to override an include
 
2729
                foreach ($v as $v2) {
 
2730
                    $this->attr[0][] = $v2;
 
2731
                }
 
2732
                continue;
 
2733
            }
 
2734
            if ($v === false) {
 
2735
                if (isset($this->attr[$k])) unset($this->attr[$k]);
 
2736
                continue;
 
2737
            }
 
2738
            $this->attr[$k] = $v;
 
2739
        }
 
2740
        $this->_mergeAssocArray($this->attr_transform_pre, $def->attr_transform_pre);
 
2741
        $this->_mergeAssocArray($this->attr_transform_post, $def->attr_transform_post);
 
2742
        $this->_mergeAssocArray($this->excludes, $def->excludes);
 
2743
 
 
2744
        if(!empty($def->content_model)) {
 
2745
            $this->content_model =
 
2746
                str_replace("#SUPER", $this->content_model, $def->content_model);
 
2747
            $this->child = false;
 
2748
        }
 
2749
        if(!empty($def->content_model_type)) {
 
2750
            $this->content_model_type = $def->content_model_type;
 
2751
            $this->child = false;
 
2752
        }
 
2753
        if(!is_null($def->child)) $this->child = $def->child;
 
2754
        if(!is_null($def->formatting)) $this->formatting = $def->formatting;
 
2755
        if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline;
 
2756
 
 
2757
    }
 
2758
 
 
2759
    /**
 
2760
     * Merges one array into another, removes values which equal false
 
2761
     * @param $a1 Array by reference that is merged into
 
2762
     * @param $a2 Array that merges into $a1
 
2763
     */
 
2764
    private function _mergeAssocArray(&$a1, $a2) {
 
2765
        foreach ($a2 as $k => $v) {
 
2766
            if ($v === false) {
 
2767
                if (isset($a1[$k])) unset($a1[$k]);
 
2768
                continue;
 
2769
            }
 
2770
            $a1[$k] = $v;
 
2771
        }
 
2772
    }
 
2773
 
 
2774
}
 
2775
 
 
2776
 
 
2777
 
 
2778
 
 
2779
 
 
2780
/**
 
2781
 * A UTF-8 specific character encoder that handles cleaning and transforming.
 
2782
 * @note All functions in this class should be static.
 
2783
 */
 
2784
class HTMLPurifier_Encoder
 
2785
{
 
2786
 
 
2787
    /**
 
2788
     * Constructor throws fatal error if you attempt to instantiate class
 
2789
     */
 
2790
    private function __construct() {
 
2791
        trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
 
2792
    }
 
2793
 
 
2794
    /**
 
2795
     * Error-handler that mutes errors, alternative to shut-up operator.
 
2796
     */
 
2797
    public static function muteErrorHandler() {}
 
2798
 
 
2799
    /**
 
2800
     * Cleans a UTF-8 string for well-formedness and SGML validity
 
2801
     *
 
2802
     * It will parse according to UTF-8 and return a valid UTF8 string, with
 
2803
     * non-SGML codepoints excluded.
 
2804
     *
 
2805
     * @note Just for reference, the non-SGML code points are 0 to 31 and
 
2806
     *       127 to 159, inclusive.  However, we allow code points 9, 10
 
2807
     *       and 13, which are the tab, line feed and carriage return
 
2808
     *       respectively. 128 and above the code points map to multibyte
 
2809
     *       UTF-8 representations.
 
2810
     *
 
2811
     * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
 
2812
     *       hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
 
2813
     *       LGPL license.  Notes on what changed are inside, but in general,
 
2814
     *       the original code transformed UTF-8 text into an array of integer
 
2815
     *       Unicode codepoints. Understandably, transforming that back to
 
2816
     *       a string would be somewhat expensive, so the function was modded to
 
2817
     *       directly operate on the string.  However, this discourages code
 
2818
     *       reuse, and the logic enumerated here would be useful for any
 
2819
     *       function that needs to be able to understand UTF-8 characters.
 
2820
     *       As of right now, only smart lossless character encoding converters
 
2821
     *       would need that, and I'm probably not going to implement them.
 
2822
     *       Once again, PHP 6 should solve all our problems.
 
2823
     */
 
2824
    public static function cleanUTF8($str, $force_php = false) {
 
2825
 
 
2826
        // UTF-8 validity is checked since PHP 4.3.5
 
2827
        // This is an optimization: if the string is already valid UTF-8, no
 
2828
        // need to do PHP stuff. 99% of the time, this will be the case.
 
2829
        // The regexp matches the XML char production, as well as well as excluding
 
2830
        // non-SGML codepoints U+007F to U+009F
 
2831
        if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) {
 
2832
            return $str;
 
2833
        }
 
2834
 
 
2835
        $mState = 0; // cached expected number of octets after the current octet
 
2836
                     // until the beginning of the next UTF8 character sequence
 
2837
        $mUcs4  = 0; // cached Unicode character
 
2838
        $mBytes = 1; // cached expected number of octets in the current sequence
 
2839
 
 
2840
        // original code involved an $out that was an array of Unicode
 
2841
        // codepoints.  Instead of having to convert back into UTF-8, we've
 
2842
        // decided to directly append valid UTF-8 characters onto a string
 
2843
        // $out once they're done.  $char accumulates raw bytes, while $mUcs4
 
2844
        // turns into the Unicode code point, so there's some redundancy.
 
2845
 
 
2846
        $out = '';
 
2847
        $char = '';
 
2848
 
 
2849
        $len = strlen($str);
 
2850
        for($i = 0; $i < $len; $i++) {
 
2851
            $in = ord($str{$i});
 
2852
            $char .= $str[$i]; // append byte to char
 
2853
            if (0 == $mState) {
 
2854
                // When mState is zero we expect either a US-ASCII character
 
2855
                // or a multi-octet sequence.
 
2856
                if (0 == (0x80 & ($in))) {
 
2857
                    // US-ASCII, pass straight through.
 
2858
                    if (($in <= 31 || $in == 127) &&
 
2859
                        !($in == 9 || $in == 13 || $in == 10) // save \r\t\n
 
2860
                    ) {
 
2861
                        // control characters, remove
 
2862
                    } else {
 
2863
                        $out .= $char;
 
2864
                    }
 
2865
                    // reset
 
2866
                    $char = '';
 
2867
                    $mBytes = 1;
 
2868
                } elseif (0xC0 == (0xE0 & ($in))) {
 
2869
                    // First octet of 2 octet sequence
 
2870
                    $mUcs4 = ($in);
 
2871
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
 
2872
                    $mState = 1;
 
2873
                    $mBytes = 2;
 
2874
                } elseif (0xE0 == (0xF0 & ($in))) {
 
2875
                    // First octet of 3 octet sequence
 
2876
                    $mUcs4 = ($in);
 
2877
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
 
2878
                    $mState = 2;
 
2879
                    $mBytes = 3;
 
2880
                } elseif (0xF0 == (0xF8 & ($in))) {
 
2881
                    // First octet of 4 octet sequence
 
2882
                    $mUcs4 = ($in);
 
2883
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
 
2884
                    $mState = 3;
 
2885
                    $mBytes = 4;
 
2886
                } elseif (0xF8 == (0xFC & ($in))) {
 
2887
                    // First octet of 5 octet sequence.
 
2888
                    //
 
2889
                    // This is illegal because the encoded codepoint must be
 
2890
                    // either:
 
2891
                    // (a) not the shortest form or
 
2892
                    // (b) outside the Unicode range of 0-0x10FFFF.
 
2893
                    // Rather than trying to resynchronize, we will carry on
 
2894
                    // until the end of the sequence and let the later error
 
2895
                    // handling code catch it.
 
2896
                    $mUcs4 = ($in);
 
2897
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
 
2898
                    $mState = 4;
 
2899
                    $mBytes = 5;
 
2900
                } elseif (0xFC == (0xFE & ($in))) {
 
2901
                    // First octet of 6 octet sequence, see comments for 5
 
2902
                    // octet sequence.
 
2903
                    $mUcs4 = ($in);
 
2904
                    $mUcs4 = ($mUcs4 & 1) << 30;
 
2905
                    $mState = 5;
 
2906
                    $mBytes = 6;
 
2907
                } else {
 
2908
                    // Current octet is neither in the US-ASCII range nor a
 
2909
                    // legal first octet of a multi-octet sequence.
 
2910
                    $mState = 0;
 
2911
                    $mUcs4  = 0;
 
2912
                    $mBytes = 1;
 
2913
                    $char = '';
 
2914
                }
 
2915
            } else {
 
2916
                // When mState is non-zero, we expect a continuation of the
 
2917
                // multi-octet sequence
 
2918
                if (0x80 == (0xC0 & ($in))) {
 
2919
                    // Legal continuation.
 
2920
                    $shift = ($mState - 1) * 6;
 
2921
                    $tmp = $in;
 
2922
                    $tmp = ($tmp & 0x0000003F) << $shift;
 
2923
                    $mUcs4 |= $tmp;
 
2924
 
 
2925
                    if (0 == --$mState) {
 
2926
                        // End of the multi-octet sequence. mUcs4 now contains
 
2927
                        // the final Unicode codepoint to be output
 
2928
 
 
2929
                        // Check for illegal sequences and codepoints.
 
2930
 
 
2931
                        // From Unicode 3.1, non-shortest form is illegal
 
2932
                        if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
 
2933
                            ((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
 
2934
                            ((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
 
2935
                            (4 < $mBytes) ||
 
2936
                            // From Unicode 3.2, surrogate characters = illegal
 
2937
                            (($mUcs4 & 0xFFFFF800) == 0xD800) ||
 
2938
                            // Codepoints outside the Unicode range are illegal
 
2939
                            ($mUcs4 > 0x10FFFF)
 
2940
                        ) {
 
2941
 
 
2942
                        } elseif (0xFEFF != $mUcs4 && // omit BOM
 
2943
                            // check for valid Char unicode codepoints
 
2944
                            (
 
2945
                                0x9 == $mUcs4 ||
 
2946
                                0xA == $mUcs4 ||
 
2947
                                0xD == $mUcs4 ||
 
2948
                                (0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
 
2949
                                // 7F-9F is not strictly prohibited by XML,
 
2950
                                // but it is non-SGML, and thus we don't allow it
 
2951
                                (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
 
2952
                                (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
 
2953
                            )
 
2954
                        ) {
 
2955
                            $out .= $char;
 
2956
                        }
 
2957
                        // initialize UTF8 cache (reset)
 
2958
                        $mState = 0;
 
2959
                        $mUcs4  = 0;
 
2960
                        $mBytes = 1;
 
2961
                        $char = '';
 
2962
                    }
 
2963
                } else {
 
2964
                    // ((0xC0 & (*in) != 0x80) && (mState != 0))
 
2965
                    // Incomplete multi-octet sequence.
 
2966
                    // used to result in complete fail, but we'll reset
 
2967
                    $mState = 0;
 
2968
                    $mUcs4  = 0;
 
2969
                    $mBytes = 1;
 
2970
                    $char ='';
 
2971
                }
 
2972
            }
 
2973
        }
 
2974
        return $out;
 
2975
    }
 
2976
 
 
2977
    /**
 
2978
     * Translates a Unicode codepoint into its corresponding UTF-8 character.
 
2979
     * @note Based on Feyd's function at
 
2980
     *       <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
 
2981
     *       which is in public domain.
 
2982
     * @note While we're going to do code point parsing anyway, a good
 
2983
     *       optimization would be to refuse to translate code points that
 
2984
     *       are non-SGML characters.  However, this could lead to duplication.
 
2985
     * @note This is very similar to the unichr function in
 
2986
     *       maintenance/generate-entity-file.php (although this is superior,
 
2987
     *       due to its sanity checks).
 
2988
     */
 
2989
 
 
2990
    // +----------+----------+----------+----------+
 
2991
    // | 33222222 | 22221111 | 111111   |          |
 
2992
    // | 10987654 | 32109876 | 54321098 | 76543210 | bit
 
2993
    // +----------+----------+----------+----------+
 
2994
    // |          |          |          | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
 
2995
    // |          |          | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
 
2996
    // |          | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
 
2997
    // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
 
2998
    // +----------+----------+----------+----------+
 
2999
    // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
 
3000
    // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
 
3001
    // +----------+----------+----------+----------+
 
3002
 
 
3003
    public static function unichr($code) {
 
3004
        if($code > 1114111 or $code < 0 or
 
3005
          ($code >= 55296 and $code <= 57343) ) {
 
3006
            // bits are set outside the "valid" range as defined
 
3007
            // by UNICODE 4.1.0
 
3008
            return '';
 
3009
        }
 
3010
 
 
3011
        $x = $y = $z = $w = 0;
 
3012
        if ($code < 128) {
 
3013
            // regular ASCII character
 
3014
            $x = $code;
 
3015
        } else {
 
3016
            // set up bits for UTF-8
 
3017
            $x = ($code & 63) | 128;
 
3018
            if ($code < 2048) {
 
3019
                $y = (($code & 2047) >> 6) | 192;
 
3020
            } else {
 
3021
                $y = (($code & 4032) >> 6) | 128;
 
3022
                if($code < 65536) {
 
3023
                    $z = (($code >> 12) & 15) | 224;
 
3024
                } else {
 
3025
                    $z = (($code >> 12) & 63) | 128;
 
3026
                    $w = (($code >> 18) & 7)  | 240;
 
3027
                }
 
3028
            }
 
3029
        }
 
3030
        // set up the actual character
 
3031
        $ret = '';
 
3032
        if($w) $ret .= chr($w);
 
3033
        if($z) $ret .= chr($z);
 
3034
        if($y) $ret .= chr($y);
 
3035
        $ret .= chr($x);
 
3036
 
 
3037
        return $ret;
 
3038
    }
 
3039
 
 
3040
    /**
 
3041
     * Converts a string to UTF-8 based on configuration.
 
3042
     */
 
3043
    public static function convertToUTF8($str, $config, $context) {
 
3044
        $encoding = $config->get('Core.Encoding');
 
3045
        if ($encoding === 'utf-8') return $str;
 
3046
        static $iconv = null;
 
3047
        if ($iconv === null) $iconv = function_exists('iconv');
 
3048
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
 
3049
        if ($iconv && !$config->get('Test.ForceNoIconv')) {
 
3050
            $str = iconv($encoding, 'utf-8//IGNORE', $str);
 
3051
            if ($str === false) {
 
3052
                // $encoding is not a valid encoding
 
3053
                restore_error_handler();
 
3054
                trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
 
3055
                return '';
 
3056
            }
 
3057
            // If the string is bjorked by Shift_JIS or a similar encoding
 
3058
            // that doesn't support all of ASCII, convert the naughty
 
3059
            // characters to their true byte-wise ASCII/UTF-8 equivalents.
 
3060
            $str = strtr($str, HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding));
 
3061
            restore_error_handler();
 
3062
            return $str;
 
3063
        } elseif ($encoding === 'iso-8859-1') {
 
3064
            $str = utf8_encode($str);
 
3065
            restore_error_handler();
 
3066
            return $str;
 
3067
        }
 
3068
        trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
 
3069
    }
 
3070
 
 
3071
    /**
 
3072
     * Converts a string from UTF-8 based on configuration.
 
3073
     * @note Currently, this is a lossy conversion, with unexpressable
 
3074
     *       characters being omitted.
 
3075
     */
 
3076
    public static function convertFromUTF8($str, $config, $context) {
 
3077
        $encoding = $config->get('Core.Encoding');
 
3078
        if ($encoding === 'utf-8') return $str;
 
3079
        static $iconv = null;
 
3080
        if ($iconv === null) $iconv = function_exists('iconv');
 
3081
        if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
 
3082
            $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str);
 
3083
        }
 
3084
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
 
3085
        if ($iconv && !$config->get('Test.ForceNoIconv')) {
 
3086
            // Undo our previous fix in convertToUTF8, otherwise iconv will barf
 
3087
            $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding);
 
3088
            if (!$escape && !empty($ascii_fix)) {
 
3089
                $clear_fix = array();
 
3090
                foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
 
3091
                $str = strtr($str, $clear_fix);
 
3092
            }
 
3093
            $str = strtr($str, array_flip($ascii_fix));
 
3094
            // Normal stuff
 
3095
            $str = iconv('utf-8', $encoding . '//IGNORE', $str);
 
3096
            restore_error_handler();
 
3097
            return $str;
 
3098
        } elseif ($encoding === 'iso-8859-1') {
 
3099
            $str = utf8_decode($str);
 
3100
            restore_error_handler();
 
3101
            return $str;
 
3102
        }
 
3103
        trigger_error('Encoding not supported', E_USER_ERROR);
 
3104
    }
 
3105
 
 
3106
    /**
 
3107
     * Lossless (character-wise) conversion of HTML to ASCII
 
3108
     * @param $str UTF-8 string to be converted to ASCII
 
3109
     * @returns ASCII encoded string with non-ASCII character entity-ized
 
3110
     * @warning Adapted from MediaWiki, claiming fair use: this is a common
 
3111
     *       algorithm. If you disagree with this license fudgery,
 
3112
     *       implement it yourself.
 
3113
     * @note Uses decimal numeric entities since they are best supported.
 
3114
     * @note This is a DUMB function: it has no concept of keeping
 
3115
     *       character entities that the projected character encoding
 
3116
     *       can allow. We could possibly implement a smart version
 
3117
     *       but that would require it to also know which Unicode
 
3118
     *       codepoints the charset supported (not an easy task).
 
3119
     * @note Sort of with cleanUTF8() but it assumes that $str is
 
3120
     *       well-formed UTF-8
 
3121
     */
 
3122
    public static function convertToASCIIDumbLossless($str) {
 
3123
        $bytesleft = 0;
 
3124
        $result = '';
 
3125
        $working = 0;
 
3126
        $len = strlen($str);
 
3127
        for( $i = 0; $i < $len; $i++ ) {
 
3128
            $bytevalue = ord( $str[$i] );
 
3129
            if( $bytevalue <= 0x7F ) { //0xxx xxxx
 
3130
                $result .= chr( $bytevalue );
 
3131
                $bytesleft = 0;
 
3132
            } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
 
3133
                $working = $working << 6;
 
3134
                $working += ($bytevalue & 0x3F);
 
3135
                $bytesleft--;
 
3136
                if( $bytesleft <= 0 ) {
 
3137
                    $result .= "&#" . $working . ";";
 
3138
                }
 
3139
            } elseif( $bytevalue <= 0xDF ) { //110x xxxx
 
3140
                $working = $bytevalue & 0x1F;
 
3141
                $bytesleft = 1;
 
3142
            } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
 
3143
                $working = $bytevalue & 0x0F;
 
3144
                $bytesleft = 2;
 
3145
            } else { //1111 0xxx
 
3146
                $working = $bytevalue & 0x07;
 
3147
                $bytesleft = 3;
 
3148
            }
 
3149
        }
 
3150
        return $result;
 
3151
    }
 
3152
 
 
3153
    /**
 
3154
     * This expensive function tests whether or not a given character
 
3155
     * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
 
3156
     * fail this test, and require special processing. Variable width
 
3157
     * encodings shouldn't ever fail.
 
3158
     *
 
3159
     * @param string $encoding Encoding name to test, as per iconv format
 
3160
     * @param bool $bypass Whether or not to bypass the precompiled arrays.
 
3161
     * @return Array of UTF-8 characters to their corresponding ASCII,
 
3162
     *      which can be used to "undo" any overzealous iconv action.
 
3163
     */
 
3164
    public static function testEncodingSupportsASCII($encoding, $bypass = false) {
 
3165
        static $encodings = array();
 
3166
        if (!$bypass) {
 
3167
            if (isset($encodings[$encoding])) return $encodings[$encoding];
 
3168
            $lenc = strtolower($encoding);
 
3169
            switch ($lenc) {
 
3170
                case 'shift_jis':
 
3171
                    return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
 
3172
                case 'johab':
 
3173
                    return array("\xE2\x82\xA9" => '\\');
 
3174
            }
 
3175
            if (strpos($lenc, 'iso-8859-') === 0) return array();
 
3176
        }
 
3177
        $ret = array();
 
3178
        set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
 
3179
        if (iconv('UTF-8', $encoding, 'a') === false) return false;
 
3180
        for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
 
3181
            $c = chr($i); // UTF-8 char
 
3182
            $r = iconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion
 
3183
            if (
 
3184
                $r === '' ||
 
3185
                // This line is needed for iconv implementations that do not
 
3186
                // omit characters that do not exist in the target character set
 
3187
                ($r === $c && iconv($encoding, 'UTF-8//IGNORE', $r) !== $c)
 
3188
            ) {
 
3189
                // Reverse engineer: what's the UTF-8 equiv of this byte
 
3190
                // sequence? This assumes that there's no variable width
 
3191
                // encoding that doesn't support ASCII.
 
3192
                $ret[iconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
 
3193
            }
 
3194
        }
 
3195
        restore_error_handler();
 
3196
        $encodings[$encoding] = $ret;
 
3197
        return $ret;
 
3198
    }
 
3199
 
 
3200
 
 
3201
}
 
3202
 
 
3203
 
 
3204
 
 
3205
 
 
3206
 
 
3207
/**
 
3208
 * Object that provides entity lookup table from entity name to character
 
3209
 */
 
3210
class HTMLPurifier_EntityLookup {
 
3211
 
 
3212
    /**
 
3213
     * Assoc array of entity name to character represented.
 
3214
     */
 
3215
    public $table;
 
3216
 
 
3217
    /**
 
3218
     * Sets up the entity lookup table from the serialized file contents.
 
3219
     * @note The serialized contents are versioned, but were generated
 
3220
     *       using the maintenance script generate_entity_file.php
 
3221
     * @warning This is not in constructor to help enforce the Singleton
 
3222
     */
 
3223
    public function setup($file = false) {
 
3224
        if (!$file) {
 
3225
            $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser';
 
3226
        }
 
3227
        $this->table = unserialize(file_get_contents($file));
 
3228
    }
 
3229
 
 
3230
    /**
 
3231
     * Retrieves sole instance of the object.
 
3232
     * @param Optional prototype of custom lookup table to overload with.
 
3233
     */
 
3234
    public static function instance($prototype = false) {
 
3235
        // no references, since PHP doesn't copy unless modified
 
3236
        static $instance = null;
 
3237
        if ($prototype) {
 
3238
            $instance = $prototype;
 
3239
        } elseif (!$instance) {
 
3240
            $instance = new HTMLPurifier_EntityLookup();
 
3241
            $instance->setup();
 
3242
        }
 
3243
        return $instance;
 
3244
    }
 
3245
 
 
3246
}
 
3247
 
 
3248
 
 
3249
 
 
3250
 
 
3251
 
 
3252
// if want to implement error collecting here, we'll need to use some sort
 
3253
// of global data (probably trigger_error) because it's impossible to pass
 
3254
// $config or $context to the callback functions.
 
3255
 
 
3256
/**
 
3257
 * Handles referencing and derefencing character entities
 
3258
 */
 
3259
class HTMLPurifier_EntityParser
 
3260
{
 
3261
 
 
3262
    /**
 
3263
     * Reference to entity lookup table.
 
3264
     */
 
3265
    protected $_entity_lookup;
 
3266
 
 
3267
    /**
 
3268
     * Callback regex string for parsing entities.
 
3269
     */
 
3270
    protected $_substituteEntitiesRegex =
 
3271
'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/';
 
3272
//     1. hex             2. dec      3. string (XML style)
 
3273
 
 
3274
 
 
3275
    /**
 
3276
     * Decimal to parsed string conversion table for special entities.
 
3277
     */
 
3278
    protected $_special_dec2str =
 
3279
            array(
 
3280
                    34 => '"',
 
3281
                    38 => '&',
 
3282
                    39 => "'",
 
3283
                    60 => '<',
 
3284
                    62 => '>'
 
3285
            );
 
3286
 
 
3287
    /**
 
3288
     * Stripped entity names to decimal conversion table for special entities.
 
3289
     */
 
3290
    protected $_special_ent2dec =
 
3291
            array(
 
3292
                    'quot' => 34,
 
3293
                    'amp'  => 38,
 
3294
                    'lt'   => 60,
 
3295
                    'gt'   => 62
 
3296
            );
 
3297
 
 
3298
    /**
 
3299
     * Substitutes non-special entities with their parsed equivalents. Since
 
3300
     * running this whenever you have parsed character is t3h 5uck, we run
 
3301
     * it before everything else.
 
3302
     *
 
3303
     * @param $string String to have non-special entities parsed.
 
3304
     * @returns Parsed string.
 
3305
     */
 
3306
    public function substituteNonSpecialEntities($string) {
 
3307
        // it will try to detect missing semicolons, but don't rely on it
 
3308
        return preg_replace_callback(
 
3309
            $this->_substituteEntitiesRegex,
 
3310
            array($this, 'nonSpecialEntityCallback'),
 
3311
            $string
 
3312
            );
 
3313
    }
 
3314
 
 
3315
    /**
 
3316
     * Callback function for substituteNonSpecialEntities() that does the work.
 
3317
     *
 
3318
     * @param $matches  PCRE matches array, with 0 the entire match, and
 
3319
     *                  either index 1, 2 or 3 set with a hex value, dec value,
 
3320
     *                  or string (respectively).
 
3321
     * @returns Replacement string.
 
3322
     */
 
3323
 
 
3324
    protected function nonSpecialEntityCallback($matches) {
 
3325
        // replaces all but big five
 
3326
        $entity = $matches[0];
 
3327
        $is_num = (@$matches[0][1] === '#');
 
3328
        if ($is_num) {
 
3329
            $is_hex = (@$entity[2] === 'x');
 
3330
            $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
 
3331
 
 
3332
            // abort for special characters
 
3333
            if (isset($this->_special_dec2str[$code]))  return $entity;
 
3334
 
 
3335
            return HTMLPurifier_Encoder::unichr($code);
 
3336
        } else {
 
3337
            if (isset($this->_special_ent2dec[$matches[3]])) return $entity;
 
3338
            if (!$this->_entity_lookup) {
 
3339
                $this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
 
3340
            }
 
3341
            if (isset($this->_entity_lookup->table[$matches[3]])) {
 
3342
                return $this->_entity_lookup->table[$matches[3]];
 
3343
            } else {
 
3344
                return $entity;
 
3345
            }
 
3346
        }
 
3347
    }
 
3348
 
 
3349
    /**
 
3350
     * Substitutes only special entities with their parsed equivalents.
 
3351
     *
 
3352
     * @notice We try to avoid calling this function because otherwise, it
 
3353
     * would have to be called a lot (for every parsed section).
 
3354
     *
 
3355
     * @param $string String to have non-special entities parsed.
 
3356
     * @returns Parsed string.
 
3357
     */
 
3358
    public function substituteSpecialEntities($string) {
 
3359
        return preg_replace_callback(
 
3360
            $this->_substituteEntitiesRegex,
 
3361
            array($this, 'specialEntityCallback'),
 
3362
            $string);
 
3363
    }
 
3364
 
 
3365
    /**
 
3366
     * Callback function for substituteSpecialEntities() that does the work.
 
3367
     *
 
3368
     * This callback has same syntax as nonSpecialEntityCallback().
 
3369
     *
 
3370
     * @param $matches  PCRE-style matches array, with 0 the entire match, and
 
3371
     *                  either index 1, 2 or 3 set with a hex value, dec value,
 
3372
     *                  or string (respectively).
 
3373
     * @returns Replacement string.
 
3374
     */
 
3375
    protected function specialEntityCallback($matches) {
 
3376
        $entity = $matches[0];
 
3377
        $is_num = (@$matches[0][1] === '#');
 
3378
        if ($is_num) {
 
3379
            $is_hex = (@$entity[2] === 'x');
 
3380
            $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
 
3381
            return isset($this->_special_dec2str[$int]) ?
 
3382
                $this->_special_dec2str[$int] :
 
3383
                $entity;
 
3384
        } else {
 
3385
            return isset($this->_special_ent2dec[$matches[3]]) ?
 
3386
                $this->_special_ent2dec[$matches[3]] :
 
3387
                $entity;
 
3388
        }
 
3389
    }
 
3390
 
 
3391
}
 
3392
 
 
3393
 
 
3394
 
 
3395
 
 
3396
 
 
3397
/**
 
3398
 * Error collection class that enables HTML Purifier to report HTML
 
3399
 * problems back to the user
 
3400
 */
 
3401
class HTMLPurifier_ErrorCollector
 
3402
{
 
3403
 
 
3404
    /**
 
3405
     * Identifiers for the returned error array. These are purposely numeric
 
3406
     * so list() can be used.
 
3407
     */
 
3408
    const LINENO   = 0;
 
3409
    const SEVERITY = 1;
 
3410
    const MESSAGE  = 2;
 
3411
    const CHILDREN = 3;
 
3412
 
 
3413
    protected $errors;
 
3414
    protected $_current;
 
3415
    protected $_stacks = array(array());
 
3416
    protected $locale;
 
3417
    protected $generator;
 
3418
    protected $context;
 
3419
 
 
3420
    protected $lines = array();
 
3421
 
 
3422
    public function __construct($context) {
 
3423
        $this->locale    =& $context->get('Locale');
 
3424
        $this->context   = $context;
 
3425
        $this->_current  =& $this->_stacks[0];
 
3426
        $this->errors    =& $this->_stacks[0];
 
3427
    }
 
3428
 
 
3429
    /**
 
3430
     * Sends an error message to the collector for later use
 
3431
     * @param $severity int Error severity, PHP error style (don't use E_USER_)
 
3432
     * @param $msg string Error message text
 
3433
     * @param $subst1 string First substitution for $msg
 
3434
     * @param $subst2 string ...
 
3435
     */
 
3436
    public function send($severity, $msg) {
 
3437
 
 
3438
        $args = array();
 
3439
        if (func_num_args() > 2) {
 
3440
            $args = func_get_args();
 
3441
            array_shift($args);
 
3442
            unset($args[0]);
 
3443
        }
 
3444
 
 
3445
        $token = $this->context->get('CurrentToken', true);
 
3446
        $line  = $token ? $token->line : $this->context->get('CurrentLine', true);
 
3447
        $col   = $token ? $token->col  : $this->context->get('CurrentCol',  true);
 
3448
        $attr  = $this->context->get('CurrentAttr', true);
 
3449
 
 
3450
        // perform special substitutions, also add custom parameters
 
3451
        $subst = array();
 
3452
        if (!is_null($token)) {
 
3453
            $args['CurrentToken'] = $token;
 
3454
        }
 
3455
        if (!is_null($attr)) {
 
3456
            $subst['$CurrentAttr.Name'] = $attr;
 
3457
            if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr];
 
3458
        }
 
3459
 
 
3460
        if (empty($args)) {
 
3461
            $msg = $this->locale->getMessage($msg);
 
3462
        } else {
 
3463
            $msg = $this->locale->formatMessage($msg, $args);
 
3464
        }
 
3465
 
 
3466
        if (!empty($subst)) $msg = strtr($msg, $subst);
 
3467
 
 
3468
        // (numerically indexed)
 
3469
        $error = array(
 
3470
            self::LINENO   => $line,
 
3471
            self::SEVERITY => $severity,
 
3472
            self::MESSAGE  => $msg,
 
3473
            self::CHILDREN => array()
 
3474
        );
 
3475
        $this->_current[] = $error;
 
3476
 
 
3477
 
 
3478
        // NEW CODE BELOW ...
 
3479
 
 
3480
        $struct = null;
 
3481
        // Top-level errors are either:
 
3482
        //  TOKEN type, if $value is set appropriately, or
 
3483
        //  "syntax" type, if $value is null
 
3484
        $new_struct = new HTMLPurifier_ErrorStruct();
 
3485
        $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN;
 
3486
        if ($token) $new_struct->value = clone $token;
 
3487
        if (is_int($line) && is_int($col)) {
 
3488
            if (isset($this->lines[$line][$col])) {
 
3489
                $struct = $this->lines[$line][$col];
 
3490
            } else {
 
3491
                $struct = $this->lines[$line][$col] = $new_struct;
 
3492
            }
 
3493
            // These ksorts may present a performance problem
 
3494
            ksort($this->lines[$line], SORT_NUMERIC);
 
3495
        } else {
 
3496
            if (isset($this->lines[-1])) {
 
3497
                $struct = $this->lines[-1];
 
3498
            } else {
 
3499
                $struct = $this->lines[-1] = $new_struct;
 
3500
            }
 
3501
        }
 
3502
        ksort($this->lines, SORT_NUMERIC);
 
3503
 
 
3504
        // Now, check if we need to operate on a lower structure
 
3505
        if (!empty($attr)) {
 
3506
            $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr);
 
3507
            if (!$struct->value) {
 
3508
                $struct->value = array($attr, 'PUT VALUE HERE');
 
3509
            }
 
3510
        }
 
3511
        if (!empty($cssprop)) {
 
3512
            $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop);
 
3513
            if (!$struct->value) {
 
3514
                // if we tokenize CSS this might be a little more difficult to do
 
3515
                $struct->value = array($cssprop, 'PUT VALUE HERE');
 
3516
            }
 
3517
        }
 
3518
 
 
3519
        // Ok, structs are all setup, now time to register the error
 
3520
        $struct->addError($severity, $msg);
 
3521
    }
 
3522
 
 
3523
    /**
 
3524
     * Retrieves raw error data for custom formatter to use
 
3525
     * @param List of arrays in format of array(line of error,
 
3526
     *        error severity, error message,
 
3527
     *        recursive sub-errors array)
 
3528
     */
 
3529
    public function getRaw() {
 
3530
        return $this->errors;
 
3531
    }
 
3532
 
 
3533
    /**
 
3534
     * Default HTML formatting implementation for error messages
 
3535
     * @param $config Configuration array, vital for HTML output nature
 
3536
     * @param $errors Errors array to display; used for recursion.
 
3537
     */
 
3538
    public function getHTMLFormatted($config, $errors = null) {
 
3539
        $ret = array();
 
3540
 
 
3541
        $this->generator = new HTMLPurifier_Generator($config, $this->context);
 
3542
        if ($errors === null) $errors = $this->errors;
 
3543
 
 
3544
        // 'At line' message needs to be removed
 
3545
 
 
3546
        // generation code for new structure goes here. It needs to be recursive.
 
3547
        foreach ($this->lines as $line => $col_array) {
 
3548
            if ($line == -1) continue;
 
3549
            foreach ($col_array as $col => $struct) {
 
3550
                $this->_renderStruct($ret, $struct, $line, $col);
 
3551
            }
 
3552
        }
 
3553
        if (isset($this->lines[-1])) {
 
3554
            $this->_renderStruct($ret, $this->lines[-1]);
 
3555
        }
 
3556
 
 
3557
        if (empty($errors)) {
 
3558
            return '<p>' . $this->locale->getMessage('ErrorCollector: No errors') . '</p>';
 
3559
        } else {
 
3560
            return '<ul><li>' . implode('</li><li>', $ret) . '</li></ul>';
 
3561
        }
 
3562
 
 
3563
    }
 
3564
 
 
3565
    private function _renderStruct(&$ret, $struct, $line = null, $col = null) {
 
3566
        $stack = array($struct);
 
3567
        $context_stack = array(array());
 
3568
        while ($current = array_pop($stack)) {
 
3569
            $context = array_pop($context_stack);
 
3570
            foreach ($current->errors as $error) {
 
3571
                list($severity, $msg) = $error;
 
3572
                $string = '';
 
3573
                $string .= '<div>';
 
3574
                // W3C uses an icon to indicate the severity of the error.
 
3575
                $error = $this->locale->getErrorName($severity);
 
3576
                $string .= "<span class=\"error e$severity\"><strong>$error</strong></span> ";
 
3577
                if (!is_null($line) && !is_null($col)) {
 
3578
                    $string .= "<em class=\"location\">Line $line, Column $col: </em> ";
 
3579
                } else {
 
3580
                    $string .= '<em class="location">End of Document: </em> ';
 
3581
                }
 
3582
                $string .= '<strong class="description">' . $this->generator->escape($msg) . '</strong> ';
 
3583
                $string .= '</div>';
 
3584
                // Here, have a marker for the character on the column appropriate.
 
3585
                // Be sure to clip extremely long lines.
 
3586
                //$string .= '<pre>';
 
3587
                //$string .= '';
 
3588
                //$string .= '</pre>';
 
3589
                $ret[] = $string;
 
3590
            }
 
3591
            foreach ($current->children as $type => $array) {
 
3592
                $context[] = $current;
 
3593
                $stack = array_merge($stack, array_reverse($array, true));
 
3594
                for ($i = count($array); $i > 0; $i--) {
 
3595
                    $context_stack[] = $context;
 
3596
                }
 
3597
            }
 
3598
        }
 
3599
    }
 
3600
 
 
3601
}
 
3602
 
 
3603
 
 
3604
 
 
3605
 
 
3606
 
 
3607
/**
 
3608
 * Records errors for particular segments of an HTML document such as tokens,
 
3609
 * attributes or CSS properties. They can contain error structs (which apply
 
3610
 * to components of what they represent), but their main purpose is to hold
 
3611
 * errors applying to whatever struct is being used.
 
3612
 */
 
3613
class HTMLPurifier_ErrorStruct
 
3614
{
 
3615
 
 
3616
    /**
 
3617
     * Possible values for $children first-key. Note that top-level structures
 
3618
     * are automatically token-level.
 
3619
     */
 
3620
    const TOKEN     = 0;
 
3621
    const ATTR      = 1;
 
3622
    const CSSPROP   = 2;
 
3623
 
 
3624
    /**
 
3625
     * Type of this struct.
 
3626
     */
 
3627
    public $type;
 
3628
 
 
3629
    /**
 
3630
     * Value of the struct we are recording errors for. There are various
 
3631
     * values for this:
 
3632
     *  - TOKEN: Instance of HTMLPurifier_Token
 
3633
     *  - ATTR: array('attr-name', 'value')
 
3634
     *  - CSSPROP: array('prop-name', 'value')
 
3635
     */
 
3636
    public $value;
 
3637
 
 
3638
    /**
 
3639
     * Errors registered for this structure.
 
3640
     */
 
3641
    public $errors = array();
 
3642
 
 
3643
    /**
 
3644
     * Child ErrorStructs that are from this structure. For example, a TOKEN
 
3645
     * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional
 
3646
     * array in structure: [TYPE]['identifier']
 
3647
     */
 
3648
    public $children = array();
 
3649
 
 
3650
    public function getChild($type, $id) {
 
3651
        if (!isset($this->children[$type][$id])) {
 
3652
            $this->children[$type][$id] = new HTMLPurifier_ErrorStruct();
 
3653
            $this->children[$type][$id]->type = $type;
 
3654
        }
 
3655
        return $this->children[$type][$id];
 
3656
    }
 
3657
 
 
3658
    public function addError($severity, $message) {
 
3659
        $this->errors[] = array($severity, $message);
 
3660
    }
 
3661
 
 
3662
}
 
3663
 
 
3664
 
 
3665
 
 
3666
 
 
3667
 
 
3668
/**
 
3669
 * Global exception class for HTML Purifier; any exceptions we throw
 
3670
 * are from here.
 
3671
 */
 
3672
class HTMLPurifier_Exception extends Exception
 
3673
{
 
3674
 
 
3675
}
 
3676
 
 
3677
 
 
3678
 
 
3679
 
 
3680
 
 
3681
/**
 
3682
 * Represents a pre or post processing filter on HTML Purifier's output
 
3683
 *
 
3684
 * Sometimes, a little ad-hoc fixing of HTML has to be done before
 
3685
 * it gets sent through HTML Purifier: you can use filters to acheive
 
3686
 * this effect. For instance, YouTube videos can be preserved using
 
3687
 * this manner. You could have used a decorator for this task, but
 
3688
 * PHP's support for them is not terribly robust, so we're going
 
3689
 * to just loop through the filters.
 
3690
 *
 
3691
 * Filters should be exited first in, last out. If there are three filters,
 
3692
 * named 1, 2 and 3, the order of execution should go 1->preFilter,
 
3693
 * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
 
3694
 * 1->postFilter.
 
3695
 *
 
3696
 * @note Methods are not declared abstract as it is perfectly legitimate
 
3697
 *       for an implementation not to want anything to happen on a step
 
3698
 */
 
3699
 
 
3700
class HTMLPurifier_Filter
 
3701
{
 
3702
 
 
3703
    /**
 
3704
     * Name of the filter for identification purposes
 
3705
     */
 
3706
    public $name;
 
3707
 
 
3708
    /**
 
3709
     * Pre-processor function, handles HTML before HTML Purifier
 
3710
     */
 
3711
    public function preFilter($html, $config, $context) {
 
3712
        return $html;
 
3713
    }
 
3714
 
 
3715
    /**
 
3716
     * Post-processor function, handles HTML after HTML Purifier
 
3717
     */
 
3718
    public function postFilter($html, $config, $context) {
 
3719
        return $html;
 
3720
    }
 
3721
 
 
3722
}
 
3723
 
 
3724
 
 
3725
 
 
3726
 
 
3727
 
 
3728
/**
 
3729
 * Generates HTML from tokens.
 
3730
 * @todo Refactor interface so that configuration/context is determined
 
3731
 *       upon instantiation, no need for messy generateFromTokens() calls
 
3732
 * @todo Make some of the more internal functions protected, and have
 
3733
 *       unit tests work around that
 
3734
 */
 
3735
class HTMLPurifier_Generator
 
3736
{
 
3737
 
 
3738
    /**
 
3739
     * Whether or not generator should produce XML output
 
3740
     */
 
3741
    private $_xhtml = true;
 
3742
 
 
3743
    /**
 
3744
     * :HACK: Whether or not generator should comment the insides of <script> tags
 
3745
     */
 
3746
    private $_scriptFix = false;
 
3747
 
 
3748
    /**
 
3749
     * Cache of HTMLDefinition during HTML output to determine whether or
 
3750
     * not attributes should be minimized.
 
3751
     */
 
3752
    private $_def;
 
3753
 
 
3754
    /**
 
3755
     * Cache of %Output.SortAttr
 
3756
     */
 
3757
    private $_sortAttr;
 
3758
 
 
3759
    /**
 
3760
     * Configuration for the generator
 
3761
     */
 
3762
    protected $config;
 
3763
 
 
3764
    /**
 
3765
     * @param $config Instance of HTMLPurifier_Config
 
3766
     * @param $context Instance of HTMLPurifier_Context
 
3767
     */
 
3768
    public function __construct($config, $context) {
 
3769
        $this->config = $config;
 
3770
        $this->_scriptFix = $config->get('Output.CommentScriptContents');
 
3771
        $this->_sortAttr = $config->get('Output.SortAttr');
 
3772
        $this->_def = $config->getHTMLDefinition();
 
3773
        $this->_xhtml = $this->_def->doctype->xml;
 
3774
    }
 
3775
 
 
3776
    /**
 
3777
     * Generates HTML from an array of tokens.
 
3778
     * @param $tokens Array of HTMLPurifier_Token
 
3779
     * @param $config HTMLPurifier_Config object
 
3780
     * @return Generated HTML
 
3781
     */
 
3782
    public function generateFromTokens($tokens) {
 
3783
        if (!$tokens) return '';
 
3784
 
 
3785
        // Basic algorithm
 
3786
        $html = '';
 
3787
        for ($i = 0, $size = count($tokens); $i < $size; $i++) {
 
3788
            if ($this->_scriptFix && $tokens[$i]->name === 'script'
 
3789
                && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
 
3790
                // script special case
 
3791
                // the contents of the script block must be ONE token
 
3792
                // for this to work.
 
3793
                $html .= $this->generateFromToken($tokens[$i++]);
 
3794
                $html .= $this->generateScriptFromToken($tokens[$i++]);
 
3795
            }
 
3796
            $html .= $this->generateFromToken($tokens[$i]);
 
3797
        }
 
3798
 
 
3799
        // Tidy cleanup
 
3800
        if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) {
 
3801
            $tidy = new Tidy;
 
3802
            $tidy->parseString($html, array(
 
3803
               'indent'=> true,
 
3804
               'output-xhtml' => $this->_xhtml,
 
3805
               'show-body-only' => true,
 
3806
               'indent-spaces' => 2,
 
3807
               'wrap' => 68,
 
3808
            ), 'utf8');
 
3809
            $tidy->cleanRepair();
 
3810
            $html = (string) $tidy; // explicit cast necessary
 
3811
        }
 
3812
 
 
3813
        // Normalize newlines to system defined value
 
3814
        $nl = $this->config->get('Output.Newline');
 
3815
        if ($nl === null) $nl = PHP_EOL;
 
3816
        if ($nl !== "\n") $html = str_replace("\n", $nl, $html);
 
3817
        return $html;
 
3818
    }
 
3819
 
 
3820
    /**
 
3821
     * Generates HTML from a single token.
 
3822
     * @param $token HTMLPurifier_Token object.
 
3823
     * @return Generated HTML
 
3824
     */
 
3825
    public function generateFromToken($token) {
 
3826
        if (!$token instanceof HTMLPurifier_Token) {
 
3827
            trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING);
 
3828
            return '';
 
3829
 
 
3830
        } elseif ($token instanceof HTMLPurifier_Token_Start) {
 
3831
            $attr = $this->generateAttributes($token->attr, $token->name);
 
3832
            return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>';
 
3833
 
 
3834
        } elseif ($token instanceof HTMLPurifier_Token_End) {
 
3835
            return '</' . $token->name . '>';
 
3836
 
 
3837
        } elseif ($token instanceof HTMLPurifier_Token_Empty) {
 
3838
            $attr = $this->generateAttributes($token->attr, $token->name);
 
3839
             return '<' . $token->name . ($attr ? ' ' : '') . $attr .
 
3840
                ( $this->_xhtml ? ' /': '' ) // <br /> v. <br>
 
3841
                . '>';
 
3842
 
 
3843
        } elseif ($token instanceof HTMLPurifier_Token_Text) {
 
3844
            return $this->escape($token->data, ENT_NOQUOTES);
 
3845
 
 
3846
        } elseif ($token instanceof HTMLPurifier_Token_Comment) {
 
3847
            return '<!--' . $token->data . '-->';
 
3848
        } else {
 
3849
            return '';
 
3850
 
 
3851
        }
 
3852
    }
 
3853
 
 
3854
    /**
 
3855
     * Special case processor for the contents of script tags
 
3856
     * @warning This runs into problems if there's already a literal
 
3857
     *          --> somewhere inside the script contents.
 
3858
     */
 
3859
    public function generateScriptFromToken($token) {
 
3860
        if (!$token instanceof HTMLPurifier_Token_Text) return $this->generateFromToken($token);
 
3861
        // Thanks <http://lachy.id.au/log/2005/05/script-comments>
 
3862
        $data = preg_replace('#//\s*$#', '', $token->data);
 
3863
        return '<!--//--><![CDATA[//><!--' . "\n" . trim($data) . "\n" . '//--><!]]>';
 
3864
    }
 
3865
 
 
3866
    /**
 
3867
     * Generates attribute declarations from attribute array.
 
3868
     * @note This does not include the leading or trailing space.
 
3869
     * @param $assoc_array_of_attributes Attribute array
 
3870
     * @param $element Name of element attributes are for, used to check
 
3871
     *        attribute minimization.
 
3872
     * @return Generate HTML fragment for insertion.
 
3873
     */
 
3874
    public function generateAttributes($assoc_array_of_attributes, $element = false) {
 
3875
        $html = '';
 
3876
        if ($this->_sortAttr) ksort($assoc_array_of_attributes);
 
3877
        foreach ($assoc_array_of_attributes as $key => $value) {
 
3878
            if (!$this->_xhtml) {
 
3879
                // Remove namespaced attributes
 
3880
                if (strpos($key, ':') !== false) continue;
 
3881
                // Check if we should minimize the attribute: val="val" -> val
 
3882
                if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
 
3883
                    $html .= $key . ' ';
 
3884
                    continue;
 
3885
                }
 
3886
            }
 
3887
            $html .= $key.'="'.$this->escape($value).'" ';
 
3888
        }
 
3889
        return rtrim($html);
 
3890
    }
 
3891
 
 
3892
    /**
 
3893
     * Escapes raw text data.
 
3894
     * @todo This really ought to be protected, but until we have a facility
 
3895
     *       for properly generating HTML here w/o using tokens, it stays
 
3896
     *       public.
 
3897
     * @param $string String data to escape for HTML.
 
3898
     * @param $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
 
3899
     *               permissible for non-attribute output.
 
3900
     * @return String escaped data.
 
3901
     */
 
3902
    public function escape($string, $quote = ENT_COMPAT) {
 
3903
        return htmlspecialchars($string, $quote, 'UTF-8');
 
3904
    }
 
3905
 
 
3906
}
 
3907
 
 
3908
 
 
3909
 
 
3910
 
 
3911
 
 
3912
/**
 
3913
 * Definition of the purified HTML that describes allowed children,
 
3914
 * attributes, and many other things.
 
3915
 *
 
3916
 * Conventions:
 
3917
 *
 
3918
 * All member variables that are prefixed with info
 
3919
 * (including the main $info array) are used by HTML Purifier internals
 
3920
 * and should not be directly edited when customizing the HTMLDefinition.
 
3921
 * They can usually be set via configuration directives or custom
 
3922
 * modules.
 
3923
 *
 
3924
 * On the other hand, member variables without the info prefix are used
 
3925
 * internally by the HTMLDefinition and MUST NOT be used by other HTML
 
3926
 * Purifier internals. Many of them, however, are public, and may be
 
3927
 * edited by userspace code to tweak the behavior of HTMLDefinition.
 
3928
 *
 
3929
 * @note This class is inspected by Printer_HTMLDefinition; please
 
3930
 *       update that class if things here change.
 
3931
 *
 
3932
 * @warning Directives that change this object's structure must be in
 
3933
 *          the HTML or Attr namespace!
 
3934
 */
 
3935
class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition
 
3936
{
 
3937
 
 
3938
    // FULLY-PUBLIC VARIABLES ---------------------------------------------
 
3939
 
 
3940
    /**
 
3941
     * Associative array of element names to HTMLPurifier_ElementDef
 
3942
     */
 
3943
    public $info = array();
 
3944
 
 
3945
    /**
 
3946
     * Associative array of global attribute name to attribute definition.
 
3947
     */
 
3948
    public $info_global_attr = array();
 
3949
 
 
3950
    /**
 
3951
     * String name of parent element HTML will be going into.
 
3952
     */
 
3953
    public $info_parent = 'div';
 
3954
 
 
3955
    /**
 
3956
     * Definition for parent element, allows parent element to be a
 
3957
     * tag that's not allowed inside the HTML fragment.
 
3958
     */
 
3959
    public $info_parent_def;
 
3960
 
 
3961
    /**
 
3962
     * String name of element used to wrap inline elements in block context
 
3963
     * @note This is rarely used except for BLOCKQUOTEs in strict mode
 
3964
     */
 
3965
    public $info_block_wrapper = 'p';
 
3966
 
 
3967
    /**
 
3968
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform
 
3969
     */
 
3970
    public $info_tag_transform = array();
 
3971
 
 
3972
    /**
 
3973
     * Indexed list of HTMLPurifier_AttrTransform to be performed before validation.
 
3974
     */
 
3975
    public $info_attr_transform_pre = array();
 
3976
 
 
3977
    /**
 
3978
     * Indexed list of HTMLPurifier_AttrTransform to be performed after validation.
 
3979
     */
 
3980
    public $info_attr_transform_post = array();
 
3981
 
 
3982
    /**
 
3983
     * Nested lookup array of content set name (Block, Inline) to
 
3984
     * element name to whether or not it belongs in that content set.
 
3985
     */
 
3986
    public $info_content_sets = array();
 
3987
 
 
3988
    /**
 
3989
     * Indexed list of HTMLPurifier_Injector to be used.
 
3990
     */
 
3991
    public $info_injector = array();
 
3992
 
 
3993
    /**
 
3994
     * Doctype object
 
3995
     */
 
3996
    public $doctype;
 
3997
 
 
3998
 
 
3999
 
 
4000
    // RAW CUSTOMIZATION STUFF --------------------------------------------
 
4001
 
 
4002
    /**
 
4003
     * Adds a custom attribute to a pre-existing element
 
4004
     * @note This is strictly convenience, and does not have a corresponding
 
4005
     *       method in HTMLPurifier_HTMLModule
 
4006
     * @param $element_name String element name to add attribute to
 
4007
     * @param $attr_name String name of attribute
 
4008
     * @param $def Attribute definition, can be string or object, see
 
4009
     *             HTMLPurifier_AttrTypes for details
 
4010
     */
 
4011
    public function addAttribute($element_name, $attr_name, $def) {
 
4012
        $module = $this->getAnonymousModule();
 
4013
        if (!isset($module->info[$element_name])) {
 
4014
            $element = $module->addBlankElement($element_name);
 
4015
        } else {
 
4016
            $element = $module->info[$element_name];
 
4017
        }
 
4018
        $element->attr[$attr_name] = $def;
 
4019
    }
 
4020
 
 
4021
    /**
 
4022
     * Adds a custom element to your HTML definition
 
4023
     * @note See HTMLPurifier_HTMLModule::addElement for detailed
 
4024
     *       parameter and return value descriptions.
 
4025
     */
 
4026
    public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) {
 
4027
        $module = $this->getAnonymousModule();
 
4028
        // assume that if the user is calling this, the element
 
4029
        // is safe. This may not be a good idea
 
4030
        $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes);
 
4031
        return $element;
 
4032
    }
 
4033
 
 
4034
    /**
 
4035
     * Adds a blank element to your HTML definition, for overriding
 
4036
     * existing behavior
 
4037
     * @note See HTMLPurifier_HTMLModule::addBlankElement for detailed
 
4038
     *       parameter and return value descriptions.
 
4039
     */
 
4040
    public function addBlankElement($element_name) {
 
4041
        $module  = $this->getAnonymousModule();
 
4042
        $element = $module->addBlankElement($element_name);
 
4043
        return $element;
 
4044
    }
 
4045
 
 
4046
    /**
 
4047
     * Retrieves a reference to the anonymous module, so you can
 
4048
     * bust out advanced features without having to make your own
 
4049
     * module.
 
4050
     */
 
4051
    public function getAnonymousModule() {
 
4052
        if (!$this->_anonModule) {
 
4053
            $this->_anonModule = new HTMLPurifier_HTMLModule();
 
4054
            $this->_anonModule->name = 'Anonymous';
 
4055
        }
 
4056
        return $this->_anonModule;
 
4057
    }
 
4058
 
 
4059
    private $_anonModule;
 
4060
 
 
4061
 
 
4062
    // PUBLIC BUT INTERNAL VARIABLES --------------------------------------
 
4063
 
 
4064
    public $type = 'HTML';
 
4065
    public $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */
 
4066
 
 
4067
    /**
 
4068
     * Performs low-cost, preliminary initialization.
 
4069
     */
 
4070
    public function __construct() {
 
4071
        $this->manager = new HTMLPurifier_HTMLModuleManager();
 
4072
    }
 
4073
 
 
4074
    protected function doSetup($config) {
 
4075
        $this->processModules($config);
 
4076
        $this->setupConfigStuff($config);
 
4077
        unset($this->manager);
 
4078
 
 
4079
        // cleanup some of the element definitions
 
4080
        foreach ($this->info as $k => $v) {
 
4081
            unset($this->info[$k]->content_model);
 
4082
            unset($this->info[$k]->content_model_type);
 
4083
        }
 
4084
    }
 
4085
 
 
4086
    /**
 
4087
     * Extract out the information from the manager
 
4088
     */
 
4089
    protected function processModules($config) {
 
4090
 
 
4091
        if ($this->_anonModule) {
 
4092
            // for user specific changes
 
4093
            // this is late-loaded so we don't have to deal with PHP4
 
4094
            // reference wonky-ness
 
4095
            $this->manager->addModule($this->_anonModule);
 
4096
            unset($this->_anonModule);
 
4097
        }
 
4098
 
 
4099
        $this->manager->setup($config);
 
4100
        $this->doctype = $this->manager->doctype;
 
4101
 
 
4102
        foreach ($this->manager->modules as $module) {
 
4103
            foreach($module->info_tag_transform as $k => $v) {
 
4104
                if ($v === false) unset($this->info_tag_transform[$k]);
 
4105
                else $this->info_tag_transform[$k] = $v;
 
4106
            }
 
4107
            foreach($module->info_attr_transform_pre as $k => $v) {
 
4108
                if ($v === false) unset($this->info_attr_transform_pre[$k]);
 
4109
                else $this->info_attr_transform_pre[$k] = $v;
 
4110
            }
 
4111
            foreach($module->info_attr_transform_post as $k => $v) {
 
4112
                if ($v === false) unset($this->info_attr_transform_post[$k]);
 
4113
                else $this->info_attr_transform_post[$k] = $v;
 
4114
            }
 
4115
            foreach ($module->info_injector as $k => $v) {
 
4116
                if ($v === false) unset($this->info_injector[$k]);
 
4117
                else $this->info_injector[$k] = $v;
 
4118
            }
 
4119
        }
 
4120
 
 
4121
        $this->info = $this->manager->getElements();
 
4122
        $this->info_content_sets = $this->manager->contentSets->lookup;
 
4123
 
 
4124
    }
 
4125
 
 
4126
    /**
 
4127
     * Sets up stuff based on config. We need a better way of doing this.
 
4128
     */
 
4129
    protected function setupConfigStuff($config) {
 
4130
 
 
4131
        $block_wrapper = $config->get('HTML.BlockWrapper');
 
4132
        if (isset($this->info_content_sets['Block'][$block_wrapper])) {
 
4133
            $this->info_block_wrapper = $block_wrapper;
 
4134
        } else {
 
4135
            trigger_error('Cannot use non-block element as block wrapper',
 
4136
                E_USER_ERROR);
 
4137
        }
 
4138
 
 
4139
        $parent = $config->get('HTML.Parent');
 
4140
        $def = $this->manager->getElement($parent, true);
 
4141
        if ($def) {
 
4142
            $this->info_parent = $parent;
 
4143
            $this->info_parent_def = $def;
 
4144
        } else {
 
4145
            trigger_error('Cannot use unrecognized element as parent',
 
4146
                E_USER_ERROR);
 
4147
            $this->info_parent_def = $this->manager->getElement($this->info_parent, true);
 
4148
        }
 
4149
 
 
4150
        // support template text
 
4151
        $support = "(for information on implementing this, see the ".
 
4152
                   "support forums) ";
 
4153
 
 
4154
        // setup allowed elements -----------------------------------------
 
4155
 
 
4156
        $allowed_elements = $config->get('HTML.AllowedElements');
 
4157
        $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early
 
4158
 
 
4159
        if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
 
4160
            $allowed = $config->get('HTML.Allowed');
 
4161
            if (is_string($allowed)) {
 
4162
                list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
 
4163
            }
 
4164
        }
 
4165
 
 
4166
        if (is_array($allowed_elements)) {
 
4167
            foreach ($this->info as $name => $d) {
 
4168
                if(!isset($allowed_elements[$name])) unset($this->info[$name]);
 
4169
                unset($allowed_elements[$name]);
 
4170
            }
 
4171
            // emit errors
 
4172
            foreach ($allowed_elements as $element => $d) {
 
4173
                $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful!
 
4174
                trigger_error("Element '$element' is not supported $support", E_USER_WARNING);
 
4175
            }
 
4176
        }
 
4177
 
 
4178
        // setup allowed attributes ---------------------------------------
 
4179
 
 
4180
        $allowed_attributes_mutable = $allowed_attributes; // by copy!
 
4181
        if (is_array($allowed_attributes)) {
 
4182
 
 
4183
            // This actually doesn't do anything, since we went away from
 
4184
            // global attributes. It's possible that userland code uses
 
4185
            // it, but HTMLModuleManager doesn't!
 
4186
            foreach ($this->info_global_attr as $attr => $x) {
 
4187
                $keys = array($attr, "*@$attr", "*.$attr");
 
4188
                $delete = true;
 
4189
                foreach ($keys as $key) {
 
4190
                    if ($delete && isset($allowed_attributes[$key])) {
 
4191
                        $delete = false;
 
4192
                    }
 
4193
                    if (isset($allowed_attributes_mutable[$key])) {
 
4194
                        unset($allowed_attributes_mutable[$key]);
 
4195
                    }
 
4196
                }
 
4197
                if ($delete) unset($this->info_global_attr[$attr]);
 
4198
            }
 
4199
 
 
4200
            foreach ($this->info as $tag => $info) {
 
4201
                foreach ($info->attr as $attr => $x) {
 
4202
                    $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr");
 
4203
                    $delete = true;
 
4204
                    foreach ($keys as $key) {
 
4205
                        if ($delete && isset($allowed_attributes[$key])) {
 
4206
                            $delete = false;
 
4207
                        }
 
4208
                        if (isset($allowed_attributes_mutable[$key])) {
 
4209
                            unset($allowed_attributes_mutable[$key]);
 
4210
                        }
 
4211
                    }
 
4212
                    if ($delete) unset($this->info[$tag]->attr[$attr]);
 
4213
                }
 
4214
            }
 
4215
            // emit errors
 
4216
            foreach ($allowed_attributes_mutable as $elattr => $d) {
 
4217
                $bits = preg_split('/[.@]/', $elattr, 2);
 
4218
                $c = count($bits);
 
4219
                switch ($c) {
 
4220
                    case 2:
 
4221
                        if ($bits[0] !== '*') {
 
4222
                            $element = htmlspecialchars($bits[0]);
 
4223
                            $attribute = htmlspecialchars($bits[1]);
 
4224
                            if (!isset($this->info[$element])) {
 
4225
                                trigger_error("Cannot allow attribute '$attribute' if element '$element' is not allowed/supported $support");
 
4226
                            } else {
 
4227
                                trigger_error("Attribute '$attribute' in element '$element' not supported $support",
 
4228
                                    E_USER_WARNING);
 
4229
                            }
 
4230
                            break;
 
4231
                        }
 
4232
                        // otherwise fall through
 
4233
                    case 1:
 
4234
                        $attribute = htmlspecialchars($bits[0]);
 
4235
                        trigger_error("Global attribute '$attribute' is not ".
 
4236
                            "supported in any elements $support",
 
4237
                            E_USER_WARNING);
 
4238
                        break;
 
4239
                }
 
4240
            }
 
4241
 
 
4242
        }
 
4243
 
 
4244
        // setup forbidden elements ---------------------------------------
 
4245
 
 
4246
        $forbidden_elements   = $config->get('HTML.ForbiddenElements');
 
4247
        $forbidden_attributes = $config->get('HTML.ForbiddenAttributes');
 
4248
 
 
4249
        foreach ($this->info as $tag => $info) {
 
4250
            if (isset($forbidden_elements[$tag])) {
 
4251
                unset($this->info[$tag]);
 
4252
                continue;
 
4253
            }
 
4254
            foreach ($info->attr as $attr => $x) {
 
4255
                if (
 
4256
                    isset($forbidden_attributes["$tag@$attr"]) ||
 
4257
                    isset($forbidden_attributes["*@$attr"]) ||
 
4258
                    isset($forbidden_attributes[$attr])
 
4259
                ) {
 
4260
                    unset($this->info[$tag]->attr[$attr]);
 
4261
                    continue;
 
4262
                } // this segment might get removed eventually
 
4263
                elseif (isset($forbidden_attributes["$tag.$attr"])) {
 
4264
                    // $tag.$attr are not user supplied, so no worries!
 
4265
                    trigger_error("Error with $tag.$attr: tag.attr syntax not supported for HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING);
 
4266
                }
 
4267
            }
 
4268
        }
 
4269
        foreach ($forbidden_attributes as $key => $v) {
 
4270
            if (strlen($key) < 2) continue;
 
4271
            if ($key[0] != '*') continue;
 
4272
            if ($key[1] == '.') {
 
4273
                trigger_error("Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING);
 
4274
            }
 
4275
        }
 
4276
 
 
4277
        // setup injectors -----------------------------------------------------
 
4278
        foreach ($this->info_injector as $i => $injector) {
 
4279
            if ($injector->checkNeeded($config) !== false) {
 
4280
                // remove injector that does not have it's required
 
4281
                // elements/attributes present, and is thus not needed.
 
4282
                unset($this->info_injector[$i]);
 
4283
            }
 
4284
        }
 
4285
    }
 
4286
 
 
4287
    /**
 
4288
     * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
 
4289
     * separate lists for processing. Format is element[attr1|attr2],element2...
 
4290
     * @warning Although it's largely drawn from TinyMCE's implementation,
 
4291
     *      it is different, and you'll probably have to modify your lists
 
4292
     * @param $list String list to parse
 
4293
     * @param array($allowed_elements, $allowed_attributes)
 
4294
     * @todo Give this its own class, probably static interface
 
4295
     */
 
4296
    public function parseTinyMCEAllowedList($list) {
 
4297
 
 
4298
        $list = str_replace(array(' ', "\t"), '', $list);
 
4299
 
 
4300
        $elements = array();
 
4301
        $attributes = array();
 
4302
 
 
4303
        $chunks = preg_split('/(,|[\n\r]+)/', $list);
 
4304
        foreach ($chunks as $chunk) {
 
4305
            if (empty($chunk)) continue;
 
4306
            // remove TinyMCE element control characters
 
4307
            if (!strpos($chunk, '[')) {
 
4308
                $element = $chunk;
 
4309
                $attr = false;
 
4310
            } else {
 
4311
                list($element, $attr) = explode('[', $chunk);
 
4312
            }
 
4313
            if ($element !== '*') $elements[$element] = true;
 
4314
            if (!$attr) continue;
 
4315
            $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ]
 
4316
            $attr = explode('|', $attr);
 
4317
            foreach ($attr as $key) {
 
4318
                $attributes["$element.$key"] = true;
 
4319
            }
 
4320
        }
 
4321
 
 
4322
        return array($elements, $attributes);
 
4323
 
 
4324
    }
 
4325
 
 
4326
 
 
4327
}
 
4328
 
 
4329
 
 
4330
 
 
4331
 
 
4332
 
 
4333
/**
 
4334
 * Represents an XHTML 1.1 module, with information on elements, tags
 
4335
 * and attributes.
 
4336
 * @note Even though this is technically XHTML 1.1, it is also used for
 
4337
 *       regular HTML parsing. We are using modulization as a convenient
 
4338
 *       way to represent the internals of HTMLDefinition, and our
 
4339
 *       implementation is by no means conforming and does not directly
 
4340
 *       use the normative DTDs or XML schemas.
 
4341
 * @note The public variables in a module should almost directly
 
4342
 *       correspond to the variables in HTMLPurifier_HTMLDefinition.
 
4343
 *       However, the prefix info carries no special meaning in these
 
4344
 *       objects (include it anyway if that's the correspondence though).
 
4345
 * @todo Consider making some member functions protected
 
4346
 */
 
4347
 
 
4348
class HTMLPurifier_HTMLModule
 
4349
{
 
4350
 
 
4351
    // -- Overloadable ----------------------------------------------------
 
4352
 
 
4353
    /**
 
4354
     * Short unique string identifier of the module
 
4355
     */
 
4356
    public $name;
 
4357
 
 
4358
    /**
 
4359
     * Informally, a list of elements this module changes. Not used in
 
4360
     * any significant way.
 
4361
     */
 
4362
    public $elements = array();
 
4363
 
 
4364
    /**
 
4365
     * Associative array of element names to element definitions.
 
4366
     * Some definitions may be incomplete, to be merged in later
 
4367
     * with the full definition.
 
4368
     */
 
4369
    public $info = array();
 
4370
 
 
4371
    /**
 
4372
     * Associative array of content set names to content set additions.
 
4373
     * This is commonly used to, say, add an A element to the Inline
 
4374
     * content set. This corresponds to an internal variable $content_sets
 
4375
     * and NOT info_content_sets member variable of HTMLDefinition.
 
4376
     */
 
4377
    public $content_sets = array();
 
4378
 
 
4379
    /**
 
4380
     * Associative array of attribute collection names to attribute
 
4381
     * collection additions. More rarely used for adding attributes to
 
4382
     * the global collections. Example is the StyleAttribute module adding
 
4383
     * the style attribute to the Core. Corresponds to HTMLDefinition's
 
4384
     * attr_collections->info, since the object's data is only info,
 
4385
     * with extra behavior associated with it.
 
4386
     */
 
4387
    public $attr_collections = array();
 
4388
 
 
4389
    /**
 
4390
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform
 
4391
     */
 
4392
    public $info_tag_transform = array();
 
4393
 
 
4394
    /**
 
4395
     * List of HTMLPurifier_AttrTransform to be performed before validation.
 
4396
     */
 
4397
    public $info_attr_transform_pre = array();
 
4398
 
 
4399
    /**
 
4400
     * List of HTMLPurifier_AttrTransform to be performed after validation.
 
4401
     */
 
4402
    public $info_attr_transform_post = array();
 
4403
 
 
4404
    /**
 
4405
     * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
 
4406
     * An injector will only be invoked if all of it's pre-requisites are met;
 
4407
     * if an injector fails setup, there will be no error; it will simply be
 
4408
     * silently disabled.
 
4409
     */
 
4410
    public $info_injector = array();
 
4411
 
 
4412
    /**
 
4413
     * Boolean flag that indicates whether or not getChildDef is implemented.
 
4414
     * For optimization reasons: may save a call to a function. Be sure
 
4415
     * to set it if you do implement getChildDef(), otherwise it will have
 
4416
     * no effect!
 
4417
     */
 
4418
    public $defines_child_def = false;
 
4419
 
 
4420
    /**
 
4421
     * Boolean flag whether or not this module is safe. If it is not safe, all
 
4422
     * of its members are unsafe. Modules are safe by default (this might be
 
4423
     * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
 
4424
     * which is based off of safe HTML, to explicitly say, "This is safe," even
 
4425
     * though there are modules which are "unsafe")
 
4426
     *
 
4427
     * @note Previously, safety could be applied at an element level granularity.
 
4428
     *       We've removed this ability, so in order to add "unsafe" elements
 
4429
     *       or attributes, a dedicated module with this property set to false
 
4430
     *       must be used.
 
4431
     */
 
4432
    public $safe = true;
 
4433
 
 
4434
    /**
 
4435
     * Retrieves a proper HTMLPurifier_ChildDef subclass based on
 
4436
     * content_model and content_model_type member variables of
 
4437
     * the HTMLPurifier_ElementDef class. There is a similar function
 
4438
     * in HTMLPurifier_HTMLDefinition.
 
4439
     * @param $def HTMLPurifier_ElementDef instance
 
4440
     * @return HTMLPurifier_ChildDef subclass
 
4441
     */
 
4442
    public function getChildDef($def) {return false;}
 
4443
 
 
4444
    // -- Convenience -----------------------------------------------------
 
4445
 
 
4446
    /**
 
4447
     * Convenience function that sets up a new element
 
4448
     * @param $element Name of element to add
 
4449
     * @param $type What content set should element be registered to?
 
4450
     *              Set as false to skip this step.
 
4451
     * @param $contents Allowed children in form of:
 
4452
     *              "$content_model_type: $content_model"
 
4453
     * @param $attr_includes What attribute collections to register to
 
4454
     *              element?
 
4455
     * @param $attr What unique attributes does the element define?
 
4456
     * @note See ElementDef for in-depth descriptions of these parameters.
 
4457
     * @return Created element definition object, so you
 
4458
     *         can set advanced parameters
 
4459
     */
 
4460
    public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) {
 
4461
        $this->elements[] = $element;
 
4462
        // parse content_model
 
4463
        list($content_model_type, $content_model) = $this->parseContents($contents);
 
4464
        // merge in attribute inclusions
 
4465
        $this->mergeInAttrIncludes($attr, $attr_includes);
 
4466
        // add element to content sets
 
4467
        if ($type) $this->addElementToContentSet($element, $type);
 
4468
        // create element
 
4469
        $this->info[$element] = HTMLPurifier_ElementDef::create(
 
4470
            $content_model, $content_model_type, $attr
 
4471
        );
 
4472
        // literal object $contents means direct child manipulation
 
4473
        if (!is_string($contents)) $this->info[$element]->child = $contents;
 
4474
        return $this->info[$element];
 
4475
    }
 
4476
 
 
4477
    /**
 
4478
     * Convenience function that creates a totally blank, non-standalone
 
4479
     * element.
 
4480
     * @param $element Name of element to create
 
4481
     * @return Created element
 
4482
     */
 
4483
    public function addBlankElement($element) {
 
4484
        if (!isset($this->info[$element])) {
 
4485
            $this->elements[] = $element;
 
4486
            $this->info[$element] = new HTMLPurifier_ElementDef();
 
4487
            $this->info[$element]->standalone = false;
 
4488
        } else {
 
4489
            trigger_error("Definition for $element already exists in module, cannot redefine");
 
4490
        }
 
4491
        return $this->info[$element];
 
4492
    }
 
4493
 
 
4494
    /**
 
4495
     * Convenience function that registers an element to a content set
 
4496
     * @param Element to register
 
4497
     * @param Name content set (warning: case sensitive, usually upper-case
 
4498
     *        first letter)
 
4499
     */
 
4500
    public function addElementToContentSet($element, $type) {
 
4501
        if (!isset($this->content_sets[$type])) $this->content_sets[$type] = '';
 
4502
        else $this->content_sets[$type] .= ' | ';
 
4503
        $this->content_sets[$type] .= $element;
 
4504
    }
 
4505
 
 
4506
    /**
 
4507
     * Convenience function that transforms single-string contents
 
4508
     * into separate content model and content model type
 
4509
     * @param $contents Allowed children in form of:
 
4510
     *                  "$content_model_type: $content_model"
 
4511
     * @note If contents is an object, an array of two nulls will be
 
4512
     *       returned, and the callee needs to take the original $contents
 
4513
     *       and use it directly.
 
4514
     */
 
4515
    public function parseContents($contents) {
 
4516
        if (!is_string($contents)) return array(null, null); // defer
 
4517
        switch ($contents) {
 
4518
            // check for shorthand content model forms
 
4519
            case 'Empty':
 
4520
                return array('empty', '');
 
4521
            case 'Inline':
 
4522
                return array('optional', 'Inline | #PCDATA');
 
4523
            case 'Flow':
 
4524
                return array('optional', 'Flow | #PCDATA');
 
4525
        }
 
4526
        list($content_model_type, $content_model) = explode(':', $contents);
 
4527
        $content_model_type = strtolower(trim($content_model_type));
 
4528
        $content_model = trim($content_model);
 
4529
        return array($content_model_type, $content_model);
 
4530
    }
 
4531
 
 
4532
    /**
 
4533
     * Convenience function that merges a list of attribute includes into
 
4534
     * an attribute array.
 
4535
     * @param $attr Reference to attr array to modify
 
4536
     * @param $attr_includes Array of includes / string include to merge in
 
4537
     */
 
4538
    public function mergeInAttrIncludes(&$attr, $attr_includes) {
 
4539
        if (!is_array($attr_includes)) {
 
4540
            if (empty($attr_includes)) $attr_includes = array();
 
4541
            else $attr_includes = array($attr_includes);
 
4542
        }
 
4543
        $attr[0] = $attr_includes;
 
4544
    }
 
4545
 
 
4546
    /**
 
4547
     * Convenience function that generates a lookup table with boolean
 
4548
     * true as value.
 
4549
     * @param $list List of values to turn into a lookup
 
4550
     * @note You can also pass an arbitrary number of arguments in
 
4551
     *       place of the regular argument
 
4552
     * @return Lookup array equivalent of list
 
4553
     */
 
4554
    public function makeLookup($list) {
 
4555
        if (is_string($list)) $list = func_get_args();
 
4556
        $ret = array();
 
4557
        foreach ($list as $value) {
 
4558
            if (is_null($value)) continue;
 
4559
            $ret[$value] = true;
 
4560
        }
 
4561
        return $ret;
 
4562
    }
 
4563
 
 
4564
    /**
 
4565
     * Lazy load construction of the module after determining whether
 
4566
     * or not it's needed, and also when a finalized configuration object
 
4567
     * is available.
 
4568
     * @param $config Instance of HTMLPurifier_Config
 
4569
     */
 
4570
    public function setup($config) {}
 
4571
 
 
4572
}
 
4573
 
 
4574
 
 
4575
 
 
4576
 
 
4577
 
 
4578
class HTMLPurifier_HTMLModuleManager
 
4579
{
 
4580
 
 
4581
    /**
 
4582
     * Instance of HTMLPurifier_DoctypeRegistry
 
4583
     */
 
4584
    public $doctypes;
 
4585
 
 
4586
    /**
 
4587
     * Instance of current doctype
 
4588
     */
 
4589
    public $doctype;
 
4590
 
 
4591
    /**
 
4592
     * Instance of HTMLPurifier_AttrTypes
 
4593
     */
 
4594
    public $attrTypes;
 
4595
 
 
4596
    /**
 
4597
     * Active instances of modules for the specified doctype are
 
4598
     * indexed, by name, in this array.
 
4599
     */
 
4600
    public $modules = array();
 
4601
 
 
4602
    /**
 
4603
     * Array of recognized HTMLPurifier_Module instances, indexed by
 
4604
     * module's class name. This array is usually lazy loaded, but a
 
4605
     * user can overload a module by pre-emptively registering it.
 
4606
     */
 
4607
    public $registeredModules = array();
 
4608
 
 
4609
    /**
 
4610
     * List of extra modules that were added by the user using addModule().
 
4611
     * These get unconditionally merged into the current doctype, whatever
 
4612
     * it may be.
 
4613
     */
 
4614
    public $userModules = array();
 
4615
 
 
4616
    /**
 
4617
     * Associative array of element name to list of modules that have
 
4618
     * definitions for the element; this array is dynamically filled.
 
4619
     */
 
4620
    public $elementLookup = array();
 
4621
 
 
4622
    /** List of prefixes we should use for registering small names */
 
4623
    public $prefixes = array('HTMLPurifier_HTMLModule_');
 
4624
 
 
4625
    public $contentSets;     /**< Instance of HTMLPurifier_ContentSets */
 
4626
    public $attrCollections; /**< Instance of HTMLPurifier_AttrCollections */
 
4627
 
 
4628
    /** If set to true, unsafe elements and attributes will be allowed */
 
4629
    public $trusted = false;
 
4630
 
 
4631
    public function __construct() {
 
4632
 
 
4633
        // editable internal objects
 
4634
        $this->attrTypes = new HTMLPurifier_AttrTypes();
 
4635
        $this->doctypes  = new HTMLPurifier_DoctypeRegistry();
 
4636
 
 
4637
        // setup basic modules
 
4638
        $common = array(
 
4639
            'CommonAttributes', 'Text', 'Hypertext', 'List',
 
4640
            'Presentation', 'Edit', 'Bdo', 'Tables', 'Image',
 
4641
            'StyleAttribute',
 
4642
            // Unsafe:
 
4643
            'Scripting', 'Object',  'Forms',
 
4644
            // Sorta legacy, but present in strict:
 
4645
            'Name',
 
4646
        );
 
4647
        $transitional = array('Legacy', 'Target');
 
4648
        $xml = array('XMLCommonAttributes');
 
4649
        $non_xml = array('NonXMLCommonAttributes');
 
4650
 
 
4651
        // setup basic doctypes
 
4652
        $this->doctypes->register(
 
4653
            'HTML 4.01 Transitional', false,
 
4654
            array_merge($common, $transitional, $non_xml),
 
4655
            array('Tidy_Transitional', 'Tidy_Proprietary'),
 
4656
            array(),
 
4657
            '-//W3C//DTD HTML 4.01 Transitional//EN',
 
4658
            'http://www.w3.org/TR/html4/loose.dtd'
 
4659
        );
 
4660
 
 
4661
        $this->doctypes->register(
 
4662
            'HTML 4.01 Strict', false,
 
4663
            array_merge($common, $non_xml),
 
4664
            array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
 
4665
            array(),
 
4666
            '-//W3C//DTD HTML 4.01//EN',
 
4667
            'http://www.w3.org/TR/html4/strict.dtd'
 
4668
        );
 
4669
 
 
4670
        $this->doctypes->register(
 
4671
            'XHTML 1.0 Transitional', true,
 
4672
            array_merge($common, $transitional, $xml, $non_xml),
 
4673
            array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'),
 
4674
            array(),
 
4675
            '-//W3C//DTD XHTML 1.0 Transitional//EN',
 
4676
            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
 
4677
        );
 
4678
 
 
4679
        $this->doctypes->register(
 
4680
            'XHTML 1.0 Strict', true,
 
4681
            array_merge($common, $xml, $non_xml),
 
4682
            array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
 
4683
            array(),
 
4684
            '-//W3C//DTD XHTML 1.0 Strict//EN',
 
4685
            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
 
4686
        );
 
4687
 
 
4688
        $this->doctypes->register(
 
4689
            'XHTML 1.1', true,
 
4690
            array_merge($common, $xml, array('Ruby')),
 
4691
            array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1
 
4692
            array(),
 
4693
            '-//W3C//DTD XHTML 1.1//EN',
 
4694
            'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
 
4695
        );
 
4696
 
 
4697
    }
 
4698
 
 
4699
    /**
 
4700
     * Registers a module to the recognized module list, useful for
 
4701
     * overloading pre-existing modules.
 
4702
     * @param $module Mixed: string module name, with or without
 
4703
     *                HTMLPurifier_HTMLModule prefix, or instance of
 
4704
     *                subclass of HTMLPurifier_HTMLModule.
 
4705
     * @param $overload Boolean whether or not to overload previous modules.
 
4706
     *                  If this is not set, and you do overload a module,
 
4707
     *                  HTML Purifier will complain with a warning.
 
4708
     * @note This function will not call autoload, you must instantiate
 
4709
     *       (and thus invoke) autoload outside the method.
 
4710
     * @note If a string is passed as a module name, different variants
 
4711
     *       will be tested in this order:
 
4712
     *          - Check for HTMLPurifier_HTMLModule_$name
 
4713
     *          - Check all prefixes with $name in order they were added
 
4714
     *          - Check for literal object name
 
4715
     *          - Throw fatal error
 
4716
     *       If your object name collides with an internal class, specify
 
4717
     *       your module manually. All modules must have been included
 
4718
     *       externally: registerModule will not perform inclusions for you!
 
4719
     */
 
4720
    public function registerModule($module, $overload = false) {
 
4721
        if (is_string($module)) {
 
4722
            // attempt to load the module
 
4723
            $original_module = $module;
 
4724
            $ok = false;
 
4725
            foreach ($this->prefixes as $prefix) {
 
4726
                $module = $prefix . $original_module;
 
4727
                if (class_exists($module)) {
 
4728
                    $ok = true;
 
4729
                    break;
 
4730
                }
 
4731
            }
 
4732
            if (!$ok) {
 
4733
                $module = $original_module;
 
4734
                if (!class_exists($module)) {
 
4735
                    trigger_error($original_module . ' module does not exist',
 
4736
                        E_USER_ERROR);
 
4737
                    return;
 
4738
                }
 
4739
            }
 
4740
            $module = new $module();
 
4741
        }
 
4742
        if (empty($module->name)) {
 
4743
            trigger_error('Module instance of ' . get_class($module) . ' must have name');
 
4744
            return;
 
4745
        }
 
4746
        if (!$overload && isset($this->registeredModules[$module->name])) {
 
4747
            trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING);
 
4748
        }
 
4749
        $this->registeredModules[$module->name] = $module;
 
4750
    }
 
4751
 
 
4752
    /**
 
4753
     * Adds a module to the current doctype by first registering it,
 
4754
     * and then tacking it on to the active doctype
 
4755
     */
 
4756
    public function addModule($module) {
 
4757
        $this->registerModule($module);
 
4758
        if (is_object($module)) $module = $module->name;
 
4759
        $this->userModules[] = $module;
 
4760
    }
 
4761
 
 
4762
    /**
 
4763
     * Adds a class prefix that registerModule() will use to resolve a
 
4764
     * string name to a concrete class
 
4765
     */
 
4766
    public function addPrefix($prefix) {
 
4767
        $this->prefixes[] = $prefix;
 
4768
    }
 
4769
 
 
4770
    /**
 
4771
     * Performs processing on modules, after being called you may
 
4772
     * use getElement() and getElements()
 
4773
     * @param $config Instance of HTMLPurifier_Config
 
4774
     */
 
4775
    public function setup($config) {
 
4776
 
 
4777
        $this->trusted = $config->get('HTML.Trusted');
 
4778
 
 
4779
        // generate
 
4780
        $this->doctype = $this->doctypes->make($config);
 
4781
        $modules = $this->doctype->modules;
 
4782
 
 
4783
        // take out the default modules that aren't allowed
 
4784
        $lookup = $config->get('HTML.AllowedModules');
 
4785
        $special_cases = $config->get('HTML.CoreModules');
 
4786
 
 
4787
        if (is_array($lookup)) {
 
4788
            foreach ($modules as $k => $m) {
 
4789
                if (isset($special_cases[$m])) continue;
 
4790
                if (!isset($lookup[$m])) unset($modules[$k]);
 
4791
            }
 
4792
        }
 
4793
 
 
4794
        // add proprietary module (this gets special treatment because
 
4795
        // it is completely removed from doctypes, etc.)
 
4796
        if ($config->get('HTML.Proprietary')) {
 
4797
            $modules[] = 'Proprietary';
 
4798
        }
 
4799
 
 
4800
        // add SafeObject/Safeembed modules
 
4801
        if ($config->get('HTML.SafeObject')) {
 
4802
            $modules[] = 'SafeObject';
 
4803
        }
 
4804
        if ($config->get('HTML.SafeEmbed')) {
 
4805
            $modules[] = 'SafeEmbed';
 
4806
        }
 
4807
 
 
4808
        // merge in custom modules
 
4809
        $modules = array_merge($modules, $this->userModules);
 
4810
 
 
4811
        foreach ($modules as $module) {
 
4812
            $this->processModule($module);
 
4813
            $this->modules[$module]->setup($config);
 
4814
        }
 
4815
 
 
4816
        foreach ($this->doctype->tidyModules as $module) {
 
4817
            $this->processModule($module);
 
4818
            $this->modules[$module]->setup($config);
 
4819
        }
 
4820
 
 
4821
        // prepare any injectors
 
4822
        foreach ($this->modules as $module) {
 
4823
            $n = array();
 
4824
            foreach ($module->info_injector as $i => $injector) {
 
4825
                if (!is_object($injector)) {
 
4826
                    $class = "HTMLPurifier_Injector_$injector";
 
4827
                    $injector = new $class;
 
4828
                }
 
4829
                $n[$injector->name] = $injector;
 
4830
            }
 
4831
            $module->info_injector = $n;
 
4832
        }
 
4833
 
 
4834
        // setup lookup table based on all valid modules
 
4835
        foreach ($this->modules as $module) {
 
4836
            foreach ($module->info as $name => $def) {
 
4837
                if (!isset($this->elementLookup[$name])) {
 
4838
                    $this->elementLookup[$name] = array();
 
4839
                }
 
4840
                $this->elementLookup[$name][] = $module->name;
 
4841
            }
 
4842
        }
 
4843
 
 
4844
        // note the different choice
 
4845
        $this->contentSets = new HTMLPurifier_ContentSets(
 
4846
            // content set assembly deals with all possible modules,
 
4847
            // not just ones deemed to be "safe"
 
4848
            $this->modules
 
4849
        );
 
4850
        $this->attrCollections = new HTMLPurifier_AttrCollections(
 
4851
            $this->attrTypes,
 
4852
            // there is no way to directly disable a global attribute,
 
4853
            // but using AllowedAttributes or simply not including
 
4854
            // the module in your custom doctype should be sufficient
 
4855
            $this->modules
 
4856
        );
 
4857
    }
 
4858
 
 
4859
    /**
 
4860
     * Takes a module and adds it to the active module collection,
 
4861
     * registering it if necessary.
 
4862
     */
 
4863
    public function processModule($module) {
 
4864
        if (!isset($this->registeredModules[$module]) || is_object($module)) {
 
4865
            $this->registerModule($module);
 
4866
        }
 
4867
        $this->modules[$module] = $this->registeredModules[$module];
 
4868
    }
 
4869
 
 
4870
    /**
 
4871
     * Retrieves merged element definitions.
 
4872
     * @return Array of HTMLPurifier_ElementDef
 
4873
     */
 
4874
    public function getElements() {
 
4875
 
 
4876
        $elements = array();
 
4877
        foreach ($this->modules as $module) {
 
4878
            if (!$this->trusted && !$module->safe) continue;
 
4879
            foreach ($module->info as $name => $v) {
 
4880
                if (isset($elements[$name])) continue;
 
4881
                $elements[$name] = $this->getElement($name);
 
4882
            }
 
4883
        }
 
4884
 
 
4885
        // remove dud elements, this happens when an element that
 
4886
        // appeared to be safe actually wasn't
 
4887
        foreach ($elements as $n => $v) {
 
4888
            if ($v === false) unset($elements[$n]);
 
4889
        }
 
4890
 
 
4891
        return $elements;
 
4892
 
 
4893
    }
 
4894
 
 
4895
    /**
 
4896
     * Retrieves a single merged element definition
 
4897
     * @param $name Name of element
 
4898
     * @param $trusted Boolean trusted overriding parameter: set to true
 
4899
     *                 if you want the full version of an element
 
4900
     * @return Merged HTMLPurifier_ElementDef
 
4901
     * @note You may notice that modules are getting iterated over twice (once
 
4902
     *       in getElements() and once here). This
 
4903
     *       is because
 
4904
     */
 
4905
    public function getElement($name, $trusted = null) {
 
4906
 
 
4907
        if (!isset($this->elementLookup[$name])) {
 
4908
            return false;
 
4909
        }
 
4910
 
 
4911
        // setup global state variables
 
4912
        $def = false;
 
4913
        if ($trusted === null) $trusted = $this->trusted;
 
4914
 
 
4915
        // iterate through each module that has registered itself to this
 
4916
        // element
 
4917
        foreach($this->elementLookup[$name] as $module_name) {
 
4918
 
 
4919
            $module = $this->modules[$module_name];
 
4920
 
 
4921
            // refuse to create/merge from a module that is deemed unsafe--
 
4922
            // pretend the module doesn't exist--when trusted mode is not on.
 
4923
            if (!$trusted && !$module->safe) {
 
4924
                continue;
 
4925
            }
 
4926
 
 
4927
            // clone is used because, ideally speaking, the original
 
4928
            // definition should not be modified. Usually, this will
 
4929
            // make no difference, but for consistency's sake
 
4930
            $new_def = clone $module->info[$name];
 
4931
 
 
4932
            if (!$def && $new_def->standalone) {
 
4933
                $def = $new_def;
 
4934
            } elseif ($def) {
 
4935
                // This will occur even if $new_def is standalone. In practice,
 
4936
                // this will usually result in a full replacement.
 
4937
                $def->mergeIn($new_def);
 
4938
            } else {
 
4939
                // :TODO:
 
4940
                // non-standalone definitions that don't have a standalone
 
4941
                // to merge into could be deferred to the end
 
4942
                continue;
 
4943
            }
 
4944
 
 
4945
            // attribute value expansions
 
4946
            $this->attrCollections->performInclusions($def->attr);
 
4947
            $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes);
 
4948
 
 
4949
            // descendants_are_inline, for ChildDef_Chameleon
 
4950
            if (is_string($def->content_model) &&
 
4951
                strpos($def->content_model, 'Inline') !== false) {
 
4952
                if ($name != 'del' && $name != 'ins') {
 
4953
                    // this is for you, ins/del
 
4954
                    $def->descendants_are_inline = true;
 
4955
                }
 
4956
            }
 
4957
 
 
4958
            $this->contentSets->generateChildDef($def, $module);
 
4959
        }
 
4960
 
 
4961
        // This can occur if there is a blank definition, but no base to
 
4962
        // mix it in with
 
4963
        if (!$def) return false;
 
4964
 
 
4965
        // add information on required attributes
 
4966
        foreach ($def->attr as $attr_name => $attr_def) {
 
4967
            if ($attr_def->required) {
 
4968
                $def->required_attr[] = $attr_name;
 
4969
            }
 
4970
        }
 
4971
 
 
4972
        return $def;
 
4973
 
 
4974
    }
 
4975
 
 
4976
}
 
4977
 
 
4978
 
 
4979
 
 
4980
 
 
4981
 
 
4982
/**
 
4983
 * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes
 
4984
 * @note In Slashdot-speak, dupe means duplicate.
 
4985
 * @note The default constructor does not accept $config or $context objects:
 
4986
 *       use must use the static build() factory method to perform initialization.
 
4987
 */
 
4988
class HTMLPurifier_IDAccumulator
 
4989
{
 
4990
 
 
4991
    /**
 
4992
     * Lookup table of IDs we've accumulated.
 
4993
     * @public
 
4994
     */
 
4995
    public $ids = array();
 
4996
 
 
4997
    /**
 
4998
     * Builds an IDAccumulator, also initializing the default blacklist
 
4999
     * @param $config Instance of HTMLPurifier_Config
 
5000
     * @param $context Instance of HTMLPurifier_Context
 
5001
     * @return Fully initialized HTMLPurifier_IDAccumulator
 
5002
     */
 
5003
    public static function build($config, $context) {
 
5004
        $id_accumulator = new HTMLPurifier_IDAccumulator();
 
5005
        $id_accumulator->load($config->get('Attr.IDBlacklist'));
 
5006
        return $id_accumulator;
 
5007
    }
 
5008
 
 
5009
    /**
 
5010
     * Add an ID to the lookup table.
 
5011
     * @param $id ID to be added.
 
5012
     * @return Bool status, true if success, false if there's a dupe
 
5013
     */
 
5014
    public function add($id) {
 
5015
        if (isset($this->ids[$id])) return false;
 
5016
        return $this->ids[$id] = true;
 
5017
    }
 
5018
 
 
5019
    /**
 
5020
     * Load a list of IDs into the lookup table
 
5021
     * @param $array_of_ids Array of IDs to load
 
5022
     * @note This function doesn't care about duplicates
 
5023
     */
 
5024
    public function load($array_of_ids) {
 
5025
        foreach ($array_of_ids as $id) {
 
5026
            $this->ids[$id] = true;
 
5027
        }
 
5028
    }
 
5029
 
 
5030
}
 
5031
 
 
5032
 
 
5033
 
 
5034
 
 
5035
 
 
5036
/**
 
5037
 * Injects tokens into the document while parsing for well-formedness.
 
5038
 * This enables "formatter-like" functionality such as auto-paragraphing,
 
5039
 * smiley-ification and linkification to take place.
 
5040
 *
 
5041
 * A note on how handlers create changes; this is done by assigning a new
 
5042
 * value to the $token reference. These values can take a variety of forms and
 
5043
 * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken()
 
5044
 * documentation.
 
5045
 *
 
5046
 * @todo Allow injectors to request a re-run on their output. This
 
5047
 *       would help if an operation is recursive.
 
5048
 */
 
5049
abstract class HTMLPurifier_Injector
 
5050
{
 
5051
 
 
5052
    /**
 
5053
     * Advisory name of injector, this is for friendly error messages
 
5054
     */
 
5055
    public $name;
 
5056
 
 
5057
    /**
 
5058
     * Instance of HTMLPurifier_HTMLDefinition
 
5059
     */
 
5060
    protected $htmlDefinition;
 
5061
 
 
5062
    /**
 
5063
     * Reference to CurrentNesting variable in Context. This is an array
 
5064
     * list of tokens that we are currently "inside"
 
5065
     */
 
5066
    protected $currentNesting;
 
5067
 
 
5068
    /**
 
5069
     * Reference to InputTokens variable in Context. This is an array
 
5070
     * list of the input tokens that are being processed.
 
5071
     */
 
5072
    protected $inputTokens;
 
5073
 
 
5074
    /**
 
5075
     * Reference to InputIndex variable in Context. This is an integer
 
5076
     * array index for $this->inputTokens that indicates what token
 
5077
     * is currently being processed.
 
5078
     */
 
5079
    protected $inputIndex;
 
5080
 
 
5081
    /**
 
5082
     * Array of elements and attributes this injector creates and therefore
 
5083
     * need to be allowed by the definition. Takes form of
 
5084
     * array('element' => array('attr', 'attr2'), 'element2')
 
5085
     */
 
5086
    public $needed = array();
 
5087
 
 
5088
    /**
 
5089
     * Index of inputTokens to rewind to.
 
5090
     */
 
5091
    protected $rewind = false;
 
5092
 
 
5093
    /**
 
5094
     * Rewind to a spot to re-perform processing. This is useful if you
 
5095
     * deleted a node, and now need to see if this change affected any
 
5096
     * earlier nodes. Rewinding does not affect other injectors, and can
 
5097
     * result in infinite loops if not used carefully.
 
5098
     * @warning HTML Purifier will prevent you from fast-forwarding with this
 
5099
     *          function.
 
5100
     */
 
5101
    public function rewind($index) {
 
5102
        $this->rewind = $index;
 
5103
    }
 
5104
 
 
5105
    /**
 
5106
     * Retrieves rewind, and then unsets it.
 
5107
     */
 
5108
    public function getRewind() {
 
5109
        $r = $this->rewind;
 
5110
        $this->rewind = false;
 
5111
        return $r;
 
5112
    }
 
5113
 
 
5114
    /**
 
5115
     * Prepares the injector by giving it the config and context objects:
 
5116
     * this allows references to important variables to be made within
 
5117
     * the injector. This function also checks if the HTML environment
 
5118
     * will work with the Injector (see checkNeeded()).
 
5119
     * @param $config Instance of HTMLPurifier_Config
 
5120
     * @param $context Instance of HTMLPurifier_Context
 
5121
     * @return Boolean false if success, string of missing needed element/attribute if failure
 
5122
     */
 
5123
    public function prepare($config, $context) {
 
5124
        $this->htmlDefinition = $config->getHTMLDefinition();
 
5125
        // Even though this might fail, some unit tests ignore this and
 
5126
        // still test checkNeeded, so be careful. Maybe get rid of that
 
5127
        // dependency.
 
5128
        $result = $this->checkNeeded($config);
 
5129
        if ($result !== false) return $result;
 
5130
        $this->currentNesting =& $context->get('CurrentNesting');
 
5131
        $this->inputTokens    =& $context->get('InputTokens');
 
5132
        $this->inputIndex     =& $context->get('InputIndex');
 
5133
        return false;
 
5134
    }
 
5135
 
 
5136
    /**
 
5137
     * This function checks if the HTML environment
 
5138
     * will work with the Injector: if p tags are not allowed, the
 
5139
     * Auto-Paragraphing injector should not be enabled.
 
5140
     * @param $config Instance of HTMLPurifier_Config
 
5141
     * @param $context Instance of HTMLPurifier_Context
 
5142
     * @return Boolean false if success, string of missing needed element/attribute if failure
 
5143
     */
 
5144
    public function checkNeeded($config) {
 
5145
        $def = $config->getHTMLDefinition();
 
5146
        foreach ($this->needed as $element => $attributes) {
 
5147
            if (is_int($element)) $element = $attributes;
 
5148
            if (!isset($def->info[$element])) return $element;
 
5149
            if (!is_array($attributes)) continue;
 
5150
            foreach ($attributes as $name) {
 
5151
                if (!isset($def->info[$element]->attr[$name])) return "$element.$name";
 
5152
            }
 
5153
        }
 
5154
        return false;
 
5155
    }
 
5156
 
 
5157
    /**
 
5158
     * Tests if the context node allows a certain element
 
5159
     * @param $name Name of element to test for
 
5160
     * @return True if element is allowed, false if it is not
 
5161
     */
 
5162
    public function allowsElement($name) {
 
5163
        if (!empty($this->currentNesting)) {
 
5164
            $parent_token = array_pop($this->currentNesting);
 
5165
            $this->currentNesting[] = $parent_token;
 
5166
            $parent = $this->htmlDefinition->info[$parent_token->name];
 
5167
        } else {
 
5168
            $parent = $this->htmlDefinition->info_parent_def;
 
5169
        }
 
5170
        if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) {
 
5171
            return false;
 
5172
        }
 
5173
        // check for exclusion
 
5174
        for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) {
 
5175
            $node = $this->currentNesting[$i];
 
5176
            $def  = $this->htmlDefinition->info[$node->name];
 
5177
            if (isset($def->excludes[$name])) return false;
 
5178
        }
 
5179
        return true;
 
5180
    }
 
5181
 
 
5182
    /**
 
5183
     * Iterator function, which starts with the next token and continues until
 
5184
     * you reach the end of the input tokens.
 
5185
     * @warning Please prevent previous references from interfering with this
 
5186
     *          functions by setting $i = null beforehand!
 
5187
     * @param &$i Current integer index variable for inputTokens
 
5188
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
 
5189
     */
 
5190
    protected function forward(&$i, &$current) {
 
5191
        if ($i === null) $i = $this->inputIndex + 1;
 
5192
        else $i++;
 
5193
        if (!isset($this->inputTokens[$i])) return false;
 
5194
        $current = $this->inputTokens[$i];
 
5195
        return true;
 
5196
    }
 
5197
 
 
5198
    /**
 
5199
     * Similar to _forward, but accepts a third parameter $nesting (which
 
5200
     * should be initialized at 0) and stops when we hit the end tag
 
5201
     * for the node $this->inputIndex starts in.
 
5202
     */
 
5203
    protected function forwardUntilEndToken(&$i, &$current, &$nesting) {
 
5204
        $result = $this->forward($i, $current);
 
5205
        if (!$result) return false;
 
5206
        if ($nesting === null) $nesting = 0;
 
5207
        if     ($current instanceof HTMLPurifier_Token_Start) $nesting++;
 
5208
        elseif ($current instanceof HTMLPurifier_Token_End) {
 
5209
            if ($nesting <= 0) return false;
 
5210
            $nesting--;
 
5211
        }
 
5212
        return true;
 
5213
    }
 
5214
 
 
5215
    /**
 
5216
     * Iterator function, starts with the previous token and continues until
 
5217
     * you reach the beginning of input tokens.
 
5218
     * @warning Please prevent previous references from interfering with this
 
5219
     *          functions by setting $i = null beforehand!
 
5220
     * @param &$i Current integer index variable for inputTokens
 
5221
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
 
5222
     */
 
5223
    protected function backward(&$i, &$current) {
 
5224
        if ($i === null) $i = $this->inputIndex - 1;
 
5225
        else $i--;
 
5226
        if ($i < 0) return false;
 
5227
        $current = $this->inputTokens[$i];
 
5228
        return true;
 
5229
    }
 
5230
 
 
5231
    /**
 
5232
     * Initializes the iterator at the current position. Use in a do {} while;
 
5233
     * loop to force the _forward and _backward functions to start at the
 
5234
     * current location.
 
5235
     * @warning Please prevent previous references from interfering with this
 
5236
     *          functions by setting $i = null beforehand!
 
5237
     * @param &$i Current integer index variable for inputTokens
 
5238
     * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
 
5239
     */
 
5240
    protected function current(&$i, &$current) {
 
5241
        if ($i === null) $i = $this->inputIndex;
 
5242
        $current = $this->inputTokens[$i];
 
5243
    }
 
5244
 
 
5245
    /**
 
5246
     * Handler that is called when a text token is processed
 
5247
     */
 
5248
    public function handleText(&$token) {}
 
5249
 
 
5250
    /**
 
5251
     * Handler that is called when a start or empty token is processed
 
5252
     */
 
5253
    public function handleElement(&$token) {}
 
5254
 
 
5255
    /**
 
5256
     * Handler that is called when an end token is processed
 
5257
     */
 
5258
    public function handleEnd(&$token) {
 
5259
        $this->notifyEnd($token);
 
5260
    }
 
5261
 
 
5262
    /**
 
5263
     * Notifier that is called when an end token is processed
 
5264
     * @note This differs from handlers in that the token is read-only
 
5265
     * @deprecated
 
5266
     */
 
5267
    public function notifyEnd($token) {}
 
5268
 
 
5269
 
 
5270
}
 
5271
 
 
5272
 
 
5273
 
 
5274
 
 
5275
 
 
5276
/**
 
5277
 * Represents a language and defines localizable string formatting and
 
5278
 * other functions, as well as the localized messages for HTML Purifier.
 
5279
 */
 
5280
class HTMLPurifier_Language
 
5281
{
 
5282
 
 
5283
    /**
 
5284
     * ISO 639 language code of language. Prefers shortest possible version
 
5285
     */
 
5286
    public $code = 'en';
 
5287
 
 
5288
    /**
 
5289
     * Fallback language code
 
5290
     */
 
5291
    public $fallback = false;
 
5292
 
 
5293
    /**
 
5294
     * Array of localizable messages
 
5295
     */
 
5296
    public $messages = array();
 
5297
 
 
5298
    /**
 
5299
     * Array of localizable error codes
 
5300
     */
 
5301
    public $errorNames = array();
 
5302
 
 
5303
    /**
 
5304
     * True if no message file was found for this language, so English
 
5305
     * is being used instead. Check this if you'd like to notify the
 
5306
     * user that they've used a non-supported language.
 
5307
     */
 
5308
    public $error = false;
 
5309
 
 
5310
    /**
 
5311
     * Has the language object been loaded yet?
 
5312
     * @todo Make it private, fix usage in HTMLPurifier_LanguageTest
 
5313
     */
 
5314
    public $_loaded = false;
 
5315
 
 
5316
    /**
 
5317
     * Instances of HTMLPurifier_Config and HTMLPurifier_Context
 
5318
     */
 
5319
    protected $config, $context;
 
5320
 
 
5321
    public function __construct($config, $context) {
 
5322
        $this->config  = $config;
 
5323
        $this->context = $context;
 
5324
    }
 
5325
 
 
5326
    /**
 
5327
     * Loads language object with necessary info from factory cache
 
5328
     * @note This is a lazy loader
 
5329
     */
 
5330
    public function load() {
 
5331
        if ($this->_loaded) return;
 
5332
        $factory = HTMLPurifier_LanguageFactory::instance();
 
5333
        $factory->loadLanguage($this->code);
 
5334
        foreach ($factory->keys as $key) {
 
5335
            $this->$key = $factory->cache[$this->code][$key];
 
5336
        }
 
5337
        $this->_loaded = true;
 
5338
    }
 
5339
 
 
5340
    /**
 
5341
     * Retrieves a localised message.
 
5342
     * @param $key string identifier of message
 
5343
     * @return string localised message
 
5344
     */
 
5345
    public function getMessage($key) {
 
5346
        if (!$this->_loaded) $this->load();
 
5347
        if (!isset($this->messages[$key])) return "[$key]";
 
5348
        return $this->messages[$key];
 
5349
    }
 
5350
 
 
5351
    /**
 
5352
     * Retrieves a localised error name.
 
5353
     * @param $int integer error number, corresponding to PHP's error
 
5354
     *             reporting
 
5355
     * @return string localised message
 
5356
     */
 
5357
    public function getErrorName($int) {
 
5358
        if (!$this->_loaded) $this->load();
 
5359
        if (!isset($this->errorNames[$int])) return "[Error: $int]";
 
5360
        return $this->errorNames[$int];
 
5361
    }
 
5362
 
 
5363
    /**
 
5364
     * Converts an array list into a string readable representation
 
5365
     */
 
5366
    public function listify($array) {
 
5367
        $sep      = $this->getMessage('Item separator');
 
5368
        $sep_last = $this->getMessage('Item separator last');
 
5369
        $ret = '';
 
5370
        for ($i = 0, $c = count($array); $i < $c; $i++) {
 
5371
            if ($i == 0) {
 
5372
            } elseif ($i + 1 < $c) {
 
5373
                $ret .= $sep;
 
5374
            } else {
 
5375
                $ret .= $sep_last;
 
5376
            }
 
5377
            $ret .= $array[$i];
 
5378
        }
 
5379
        return $ret;
 
5380
    }
 
5381
 
 
5382
    /**
 
5383
     * Formats a localised message with passed parameters
 
5384
     * @param $key string identifier of message
 
5385
     * @param $args Parameters to substitute in
 
5386
     * @return string localised message
 
5387
     * @todo Implement conditionals? Right now, some messages make
 
5388
     *     reference to line numbers, but those aren't always available
 
5389
     */
 
5390
    public function formatMessage($key, $args = array()) {
 
5391
        if (!$this->_loaded) $this->load();
 
5392
        if (!isset($this->messages[$key])) return "[$key]";
 
5393
        $raw = $this->messages[$key];
 
5394
        $subst = array();
 
5395
        $generator = false;
 
5396
        foreach ($args as $i => $value) {
 
5397
            if (is_object($value)) {
 
5398
                if ($value instanceof HTMLPurifier_Token) {
 
5399
                    // factor this out some time
 
5400
                    if (!$generator) $generator = $this->context->get('Generator');
 
5401
                    if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
 
5402
                    if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
 
5403
                    $subst['$'.$i.'.Compact'] =
 
5404
                    $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
 
5405
                    // a more complex algorithm for compact representation
 
5406
                    // could be introduced for all types of tokens. This
 
5407
                    // may need to be factored out into a dedicated class
 
5408
                    if (!empty($value->attr)) {
 
5409
                        $stripped_token = clone $value;
 
5410
                        $stripped_token->attr = array();
 
5411
                        $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
 
5412
                    }
 
5413
                    $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
 
5414
                }
 
5415
                continue;
 
5416
            } elseif (is_array($value)) {
 
5417
                $keys = array_keys($value);
 
5418
                if (array_keys($keys) === $keys) {
 
5419
                    // list
 
5420
                    $subst['$'.$i] = $this->listify($value);
 
5421
                } else {
 
5422
                    // associative array
 
5423
                    // no $i implementation yet, sorry
 
5424
                    $subst['$'.$i.'.Keys'] = $this->listify($keys);
 
5425
                    $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
 
5426
                }
 
5427
                continue;
 
5428
            }
 
5429
            $subst['$' . $i] = $value;
 
5430
        }
 
5431
        return strtr($raw, $subst);
 
5432
    }
 
5433
 
 
5434
}
 
5435
 
 
5436
 
 
5437
 
 
5438
 
 
5439
 
 
5440
/**
 
5441
 * Class responsible for generating HTMLPurifier_Language objects, managing
 
5442
 * caching and fallbacks.
 
5443
 * @note Thanks to MediaWiki for the general logic, although this version
 
5444
 *       has been entirely rewritten
 
5445
 * @todo Serialized cache for languages
 
5446
 */
 
5447
class HTMLPurifier_LanguageFactory
 
5448
{
 
5449
 
 
5450
    /**
 
5451
     * Cache of language code information used to load HTMLPurifier_Language objects
 
5452
     * Structure is: $factory->cache[$language_code][$key] = $value
 
5453
     * @value array map
 
5454
     */
 
5455
    public $cache;
 
5456
 
 
5457
    /**
 
5458
     * Valid keys in the HTMLPurifier_Language object. Designates which
 
5459
     * variables to slurp out of a message file.
 
5460
     * @value array list
 
5461
     */
 
5462
    public $keys = array('fallback', 'messages', 'errorNames');
 
5463
 
 
5464
    /**
 
5465
     * Instance of HTMLPurifier_AttrDef_Lang to validate language codes
 
5466
     * @value object HTMLPurifier_AttrDef_Lang
 
5467
     */
 
5468
    protected $validator;
 
5469
 
 
5470
    /**
 
5471
     * Cached copy of dirname(__FILE__), directory of current file without
 
5472
     * trailing slash
 
5473
     * @value string filename
 
5474
     */
 
5475
    protected $dir;
 
5476
 
 
5477
    /**
 
5478
     * Keys whose contents are a hash map and can be merged
 
5479
     * @value array lookup
 
5480
     */
 
5481
    protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true);
 
5482
 
 
5483
    /**
 
5484
     * Keys whose contents are a list and can be merged
 
5485
     * @value array lookup
 
5486
     */
 
5487
    protected $mergeable_keys_list = array();
 
5488
 
 
5489
    /**
 
5490
     * Retrieve sole instance of the factory.
 
5491
     * @param $prototype Optional prototype to overload sole instance with,
 
5492
     *                   or bool true to reset to default factory.
 
5493
     */
 
5494
    public static function instance($prototype = null) {
 
5495
        static $instance = null;
 
5496
        if ($prototype !== null) {
 
5497
            $instance = $prototype;
 
5498
        } elseif ($instance === null || $prototype == true) {
 
5499
            $instance = new HTMLPurifier_LanguageFactory();
 
5500
            $instance->setup();
 
5501
        }
 
5502
        return $instance;
 
5503
    }
 
5504
 
 
5505
    /**
 
5506
     * Sets up the singleton, much like a constructor
 
5507
     * @note Prevents people from getting this outside of the singleton
 
5508
     */
 
5509
    public function setup() {
 
5510
        $this->validator = new HTMLPurifier_AttrDef_Lang();
 
5511
        $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier';
 
5512
    }
 
5513
 
 
5514
    /**
 
5515
     * Creates a language object, handles class fallbacks
 
5516
     * @param $config Instance of HTMLPurifier_Config
 
5517
     * @param $context Instance of HTMLPurifier_Context
 
5518
     * @param $code Code to override configuration with. Private parameter.
 
5519
     */
 
5520
    public function create($config, $context, $code = false) {
 
5521
 
 
5522
        // validate language code
 
5523
        if ($code === false) {
 
5524
            $code = $this->validator->validate(
 
5525
              $config->get('Core.Language'), $config, $context
 
5526
            );
 
5527
        } else {
 
5528
            $code = $this->validator->validate($code, $config, $context);
 
5529
        }
 
5530
        if ($code === false) $code = 'en'; // malformed code becomes English
 
5531
 
 
5532
        $pcode = str_replace('-', '_', $code); // make valid PHP classname
 
5533
        static $depth = 0; // recursion protection
 
5534
 
 
5535
        if ($code == 'en') {
 
5536
            $lang = new HTMLPurifier_Language($config, $context);
 
5537
        } else {
 
5538
            $class = 'HTMLPurifier_Language_' . $pcode;
 
5539
            $file  = $this->dir . '/Language/classes/' . $code . '.php';
 
5540
            if (file_exists($file) || class_exists($class, false)) {
 
5541
                $lang = new $class($config, $context);
 
5542
            } else {
 
5543
                // Go fallback
 
5544
                $raw_fallback = $this->getFallbackFor($code);
 
5545
                $fallback = $raw_fallback ? $raw_fallback : 'en';
 
5546
                $depth++;
 
5547
                $lang = $this->create($config, $context, $fallback);
 
5548
                if (!$raw_fallback) {
 
5549
                    $lang->error = true;
 
5550
                }
 
5551
                $depth--;
 
5552
            }
 
5553
        }
 
5554
 
 
5555
        $lang->code = $code;
 
5556
 
 
5557
        return $lang;
 
5558
 
 
5559
    }
 
5560
 
 
5561
    /**
 
5562
     * Returns the fallback language for language
 
5563
     * @note Loads the original language into cache
 
5564
     * @param $code string language code
 
5565
     */
 
5566
    public function getFallbackFor($code) {
 
5567
        $this->loadLanguage($code);
 
5568
        return $this->cache[$code]['fallback'];
 
5569
    }
 
5570
 
 
5571
    /**
 
5572
     * Loads language into the cache, handles message file and fallbacks
 
5573
     * @param $code string language code
 
5574
     */
 
5575
    public function loadLanguage($code) {
 
5576
        static $languages_seen = array(); // recursion guard
 
5577
 
 
5578
        // abort if we've already loaded it
 
5579
        if (isset($this->cache[$code])) return;
 
5580
 
 
5581
        // generate filename
 
5582
        $filename = $this->dir . '/Language/messages/' . $code . '.php';
 
5583
 
 
5584
        // default fallback : may be overwritten by the ensuing include
 
5585
        $fallback = ($code != 'en') ? 'en' : false;
 
5586
 
 
5587
        // load primary localisation
 
5588
        if (!file_exists($filename)) {
 
5589
            // skip the include: will rely solely on fallback
 
5590
            $filename = $this->dir . '/Language/messages/en.php';
 
5591
            $cache = array();
 
5592
        } else {
 
5593
            include $filename;
 
5594
            $cache = compact($this->keys);
 
5595
        }
 
5596
 
 
5597
        // load fallback localisation
 
5598
        if (!empty($fallback)) {
 
5599
 
 
5600
            // infinite recursion guard
 
5601
            if (isset($languages_seen[$code])) {
 
5602
                trigger_error('Circular fallback reference in language ' .
 
5603
                    $code, E_USER_ERROR);
 
5604
                $fallback = 'en';
 
5605
            }
 
5606
            $language_seen[$code] = true;
 
5607
 
 
5608
            // load the fallback recursively
 
5609
            $this->loadLanguage($fallback);
 
5610
            $fallback_cache = $this->cache[$fallback];
 
5611
 
 
5612
            // merge fallback with current language
 
5613
            foreach ( $this->keys as $key ) {
 
5614
                if (isset($cache[$key]) && isset($fallback_cache[$key])) {
 
5615
                    if (isset($this->mergeable_keys_map[$key])) {
 
5616
                        $cache[$key] = $cache[$key] + $fallback_cache[$key];
 
5617
                    } elseif (isset($this->mergeable_keys_list[$key])) {
 
5618
                        $cache[$key] = array_merge( $fallback_cache[$key], $cache[$key] );
 
5619
                    }
 
5620
                } else {
 
5621
                    $cache[$key] = $fallback_cache[$key];
 
5622
                }
 
5623
            }
 
5624
 
 
5625
        }
 
5626
 
 
5627
        // save to cache for later retrieval
 
5628
        $this->cache[$code] = $cache;
 
5629
 
 
5630
        return;
 
5631
    }
 
5632
 
 
5633
}
 
5634
 
 
5635
 
 
5636
 
 
5637
 
 
5638
 
 
5639
/**
 
5640
 * Represents a measurable length, with a string numeric magnitude
 
5641
 * and a unit. This object is immutable.
 
5642
 */
 
5643
class HTMLPurifier_Length
 
5644
{
 
5645
 
 
5646
    /**
 
5647
     * String numeric magnitude.
 
5648
     */
 
5649
    protected $n;
 
5650
 
 
5651
    /**
 
5652
     * String unit. False is permitted if $n = 0.
 
5653
     */
 
5654
    protected $unit;
 
5655
 
 
5656
    /**
 
5657
     * Whether or not this length is valid. Null if not calculated yet.
 
5658
     */
 
5659
    protected $isValid;
 
5660
 
 
5661
    /**
 
5662
     * Lookup array of units recognized by CSS 2.1
 
5663
     */
 
5664
    protected static $allowedUnits = array(
 
5665
        'em' => true, 'ex' => true, 'px' => true, 'in' => true,
 
5666
        'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true
 
5667
    );
 
5668
 
 
5669
    /**
 
5670
     * @param number $n Magnitude
 
5671
     * @param string $u Unit
 
5672
     */
 
5673
    public function __construct($n = '0', $u = false) {
 
5674
        $this->n = (string) $n;
 
5675
        $this->unit = $u !== false ? (string) $u : false;
 
5676
    }
 
5677
 
 
5678
    /**
 
5679
     * @param string $s Unit string, like '2em' or '3.4in'
 
5680
     * @warning Does not perform validation.
 
5681
     */
 
5682
    static public function make($s) {
 
5683
        if ($s instanceof HTMLPurifier_Length) return $s;
 
5684
        $n_length = strspn($s, '1234567890.+-');
 
5685
        $n = substr($s, 0, $n_length);
 
5686
        $unit = substr($s, $n_length);
 
5687
        if ($unit === '') $unit = false;
 
5688
        return new HTMLPurifier_Length($n, $unit);
 
5689
    }
 
5690
 
 
5691
    /**
 
5692
     * Validates the number and unit.
 
5693
     */
 
5694
    protected function validate() {
 
5695
        // Special case:
 
5696
        if ($this->n === '+0' || $this->n === '-0') $this->n = '0';
 
5697
        if ($this->n === '0' && $this->unit === false) return true;
 
5698
        if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit);
 
5699
        if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false;
 
5700
        // Hack:
 
5701
        $def = new HTMLPurifier_AttrDef_CSS_Number();
 
5702
        $result = $def->validate($this->n, false, false);
 
5703
        if ($result === false) return false;
 
5704
        $this->n = $result;
 
5705
        return true;
 
5706
    }
 
5707
 
 
5708
    /**
 
5709
     * Returns string representation of number.
 
5710
     */
 
5711
    public function toString() {
 
5712
        if (!$this->isValid()) return false;
 
5713
        return $this->n . $this->unit;
 
5714
    }
 
5715
 
 
5716
    /**
 
5717
     * Retrieves string numeric magnitude.
 
5718
     */
 
5719
    public function getN() {return $this->n;}
 
5720
 
 
5721
    /**
 
5722
     * Retrieves string unit.
 
5723
     */
 
5724
    public function getUnit() {return $this->unit;}
 
5725
 
 
5726
    /**
 
5727
     * Returns true if this length unit is valid.
 
5728
     */
 
5729
    public function isValid() {
 
5730
        if ($this->isValid === null) $this->isValid = $this->validate();
 
5731
        return $this->isValid;
 
5732
    }
 
5733
 
 
5734
    /**
 
5735
     * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
 
5736
     * @warning If both values are too large or small, this calculation will
 
5737
     *          not work properly
 
5738
     */
 
5739
    public function compareTo($l) {
 
5740
        if ($l === false) return false;
 
5741
        if ($l->unit !== $this->unit) {
 
5742
            $converter = new HTMLPurifier_UnitConverter();
 
5743
            $l = $converter->convert($l, $this->unit);
 
5744
            if ($l === false) return false;
 
5745
        }
 
5746
        return $this->n - $l->n;
 
5747
    }
 
5748
 
 
5749
}
 
5750
 
 
5751
 
 
5752
 
 
5753
 
 
5754
 
 
5755
/**
 
5756
 * Forgivingly lexes HTML (SGML-style) markup into tokens.
 
5757
 *
 
5758
 * A lexer parses a string of SGML-style markup and converts them into
 
5759
 * corresponding tokens.  It doesn't check for well-formedness, although its
 
5760
 * internal mechanism may make this automatic (such as the case of
 
5761
 * HTMLPurifier_Lexer_DOMLex).  There are several implementations to choose
 
5762
 * from.
 
5763
 *
 
5764
 * A lexer is HTML-oriented: it might work with XML, but it's not
 
5765
 * recommended, as we adhere to a subset of the specification for optimization
 
5766
 * reasons. This might change in the future. Also, most tokenizers are not
 
5767
 * expected to handle DTDs or PIs.
 
5768
 *
 
5769
 * This class should not be directly instantiated, but you may use create() to
 
5770
 * retrieve a default copy of the lexer.  Being a supertype, this class
 
5771
 * does not actually define any implementation, but offers commonly used
 
5772
 * convenience functions for subclasses.
 
5773
 *
 
5774
 * @note The unit tests will instantiate this class for testing purposes, as
 
5775
 *       many of the utility functions require a class to be instantiated.
 
5776
 *       This means that, even though this class is not runnable, it will
 
5777
 *       not be declared abstract.
 
5778
 *
 
5779
 * @par
 
5780
 *
 
5781
 * @note
 
5782
 * We use tokens rather than create a DOM representation because DOM would:
 
5783
 *
 
5784
 * @par
 
5785
 *  -# Require more processing and memory to create,
 
5786
 *  -# Is not streamable, and
 
5787
 *  -# Has the entire document structure (html and body not needed).
 
5788
 *
 
5789
 * @par
 
5790
 * However, DOM is helpful in that it makes it easy to move around nodes
 
5791
 * without a lot of lookaheads to see when a tag is closed. This is a
 
5792
 * limitation of the token system and some workarounds would be nice.
 
5793
 */
 
5794
class HTMLPurifier_Lexer
 
5795
{
 
5796
 
 
5797
    /**
 
5798
     * Whether or not this lexer implements line-number/column-number tracking.
 
5799
     * If it does, set to true.
 
5800
     */
 
5801
    public $tracksLineNumbers = false;
 
5802
 
 
5803
    // -- STATIC ----------------------------------------------------------
 
5804
 
 
5805
    /**
 
5806
     * Retrieves or sets the default Lexer as a Prototype Factory.
 
5807
     *
 
5808
     * By default HTMLPurifier_Lexer_DOMLex will be returned. There are
 
5809
     * a few exceptions involving special features that only DirectLex
 
5810
     * implements.
 
5811
     *
 
5812
     * @note The behavior of this class has changed, rather than accepting
 
5813
     *       a prototype object, it now accepts a configuration object.
 
5814
     *       To specify your own prototype, set %Core.LexerImpl to it.
 
5815
     *       This change in behavior de-singletonizes the lexer object.
 
5816
     *
 
5817
     * @param $config Instance of HTMLPurifier_Config
 
5818
     * @return Concrete lexer.
 
5819
     */
 
5820
    public static function create($config) {
 
5821
 
 
5822
        if (!($config instanceof HTMLPurifier_Config)) {
 
5823
            $lexer = $config;
 
5824
            trigger_error("Passing a prototype to
 
5825
              HTMLPurifier_Lexer::create() is deprecated, please instead
 
5826
              use %Core.LexerImpl", E_USER_WARNING);
 
5827
        } else {
 
5828
            $lexer = $config->get('Core.LexerImpl');
 
5829
        }
 
5830
 
 
5831
        $needs_tracking =
 
5832
            $config->get('Core.MaintainLineNumbers') ||
 
5833
            $config->get('Core.CollectErrors');
 
5834
 
 
5835
        $inst = null;
 
5836
        if (is_object($lexer)) {
 
5837
            $inst = $lexer;
 
5838
        } else {
 
5839
 
 
5840
            if (is_null($lexer)) { do {
 
5841
                // auto-detection algorithm
 
5842
 
 
5843
                if ($needs_tracking) {
 
5844
                    $lexer = 'DirectLex';
 
5845
                    break;
 
5846
                }
 
5847
 
 
5848
                if (
 
5849
                    class_exists('DOMDocument') &&
 
5850
                    method_exists('DOMDocument', 'loadHTML') &&
 
5851
                    !extension_loaded('domxml')
 
5852
                ) {
 
5853
                    // check for DOM support, because while it's part of the
 
5854
                    // core, it can be disabled compile time. Also, the PECL
 
5855
                    // domxml extension overrides the default DOM, and is evil
 
5856
                    // and nasty and we shan't bother to support it
 
5857
                    $lexer = 'DOMLex';
 
5858
                } else {
 
5859
                    $lexer = 'DirectLex';
 
5860
                }
 
5861
 
 
5862
            } while(0); } // do..while so we can break
 
5863
 
 
5864
            // instantiate recognized string names
 
5865
            switch ($lexer) {
 
5866
                case 'DOMLex':
 
5867
                    $inst = new HTMLPurifier_Lexer_DOMLex();
 
5868
                    break;
 
5869
                case 'DirectLex':
 
5870
                    $inst = new HTMLPurifier_Lexer_DirectLex();
 
5871
                    break;
 
5872
                case 'PH5P':
 
5873
                    $inst = new HTMLPurifier_Lexer_PH5P();
 
5874
                    break;
 
5875
                default:
 
5876
                    throw new HTMLPurifier_Exception("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer));
 
5877
            }
 
5878
        }
 
5879
 
 
5880
        if (!$inst) throw new HTMLPurifier_Exception('No lexer was instantiated');
 
5881
 
 
5882
        // once PHP DOM implements native line numbers, or we
 
5883
        // hack out something using XSLT, remove this stipulation
 
5884
        if ($needs_tracking && !$inst->tracksLineNumbers) {
 
5885
            throw new HTMLPurifier_Exception('Cannot use lexer that does not support line numbers with Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)');
 
5886
        }
 
5887
 
 
5888
        return $inst;
 
5889
 
 
5890
    }
 
5891
 
 
5892
    // -- CONVENIENCE MEMBERS ---------------------------------------------
 
5893
 
 
5894
    public function __construct() {
 
5895
        $this->_entity_parser = new HTMLPurifier_EntityParser();
 
5896
    }
 
5897
 
 
5898
    /**
 
5899
     * Most common entity to raw value conversion table for special entities.
 
5900
     */
 
5901
    protected $_special_entity2str =
 
5902
            array(
 
5903
                    '&quot;' => '"',
 
5904
                    '&amp;'  => '&',
 
5905
                    '&lt;'   => '<',
 
5906
                    '&gt;'   => '>',
 
5907
                    '&#39;'  => "'",
 
5908
                    '&#039;' => "'",
 
5909
                    '&#x27;' => "'"
 
5910
            );
 
5911
 
 
5912
    /**
 
5913
     * Parses special entities into the proper characters.
 
5914
     *
 
5915
     * This string will translate escaped versions of the special characters
 
5916
     * into the correct ones.
 
5917
     *
 
5918
     * @warning
 
5919
     * You should be able to treat the output of this function as
 
5920
     * completely parsed, but that's only because all other entities should
 
5921
     * have been handled previously in substituteNonSpecialEntities()
 
5922
     *
 
5923
     * @param $string String character data to be parsed.
 
5924
     * @returns Parsed character data.
 
5925
     */
 
5926
    public function parseData($string) {
 
5927
 
 
5928
        // following functions require at least one character
 
5929
        if ($string === '') return '';
 
5930
 
 
5931
        // subtracts amps that cannot possibly be escaped
 
5932
        $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
 
5933
            ($string[strlen($string)-1] === '&' ? 1 : 0);
 
5934
 
 
5935
        if (!$num_amp) return $string; // abort if no entities
 
5936
        $num_esc_amp = substr_count($string, '&amp;');
 
5937
        $string = strtr($string, $this->_special_entity2str);
 
5938
 
 
5939
        // code duplication for sake of optimization, see above
 
5940
        $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
 
5941
            ($string[strlen($string)-1] === '&' ? 1 : 0);
 
5942
 
 
5943
        if ($num_amp_2 <= $num_esc_amp) return $string;
 
5944
 
 
5945
        // hmm... now we have some uncommon entities. Use the callback.
 
5946
        $string = $this->_entity_parser->substituteSpecialEntities($string);
 
5947
        return $string;
 
5948
    }
 
5949
 
 
5950
    /**
 
5951
     * Lexes an HTML string into tokens.
 
5952
     *
 
5953
     * @param $string String HTML.
 
5954
     * @return HTMLPurifier_Token array representation of HTML.
 
5955
     */
 
5956
    public function tokenizeHTML($string, $config, $context) {
 
5957
        trigger_error('Call to abstract class', E_USER_ERROR);
 
5958
    }
 
5959
 
 
5960
    /**
 
5961
     * Translates CDATA sections into regular sections (through escaping).
 
5962
     *
 
5963
     * @param $string HTML string to process.
 
5964
     * @returns HTML with CDATA sections escaped.
 
5965
     */
 
5966
    protected static function escapeCDATA($string) {
 
5967
        return preg_replace_callback(
 
5968
            '/<!\[CDATA\[(.+?)\]\]>/s',
 
5969
            array('HTMLPurifier_Lexer', 'CDATACallback'),
 
5970
            $string
 
5971
        );
 
5972
    }
 
5973
 
 
5974
    /**
 
5975
     * Special CDATA case that is especially convoluted for <script>
 
5976
     */
 
5977
    protected static function escapeCommentedCDATA($string) {
 
5978
        return preg_replace_callback(
 
5979
            '#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s',
 
5980
            array('HTMLPurifier_Lexer', 'CDATACallback'),
 
5981
            $string
 
5982
        );
 
5983
    }
 
5984
 
 
5985
    /**
 
5986
     * Callback function for escapeCDATA() that does the work.
 
5987
     *
 
5988
     * @warning Though this is public in order to let the callback happen,
 
5989
     *          calling it directly is not recommended.
 
5990
     * @params $matches PCRE matches array, with index 0 the entire match
 
5991
     *                  and 1 the inside of the CDATA section.
 
5992
     * @returns Escaped internals of the CDATA section.
 
5993
     */
 
5994
    protected static function CDATACallback($matches) {
 
5995
        // not exactly sure why the character set is needed, but whatever
 
5996
        return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8');
 
5997
    }
 
5998
 
 
5999
    /**
 
6000
     * Takes a piece of HTML and normalizes it by converting entities, fixing
 
6001
     * encoding, extracting bits, and other good stuff.
 
6002
     * @todo Consider making protected
 
6003
     */
 
6004
    public function normalize($html, $config, $context) {
 
6005
 
 
6006
        // normalize newlines to \n
 
6007
        $html = str_replace("\r\n", "\n", $html);
 
6008
        $html = str_replace("\r", "\n", $html);
 
6009
 
 
6010
        if ($config->get('HTML.Trusted')) {
 
6011
            // escape convoluted CDATA
 
6012
            $html = $this->escapeCommentedCDATA($html);
 
6013
        }
 
6014
 
 
6015
        // escape CDATA
 
6016
        $html = $this->escapeCDATA($html);
 
6017
 
 
6018
        // extract body from document if applicable
 
6019
        if ($config->get('Core.ConvertDocumentToFragment')) {
 
6020
            $html = $this->extractBody($html);
 
6021
        }
 
6022
 
 
6023
        // expand entities that aren't the big five
 
6024
        $html = $this->_entity_parser->substituteNonSpecialEntities($html);
 
6025
 
 
6026
        // clean into wellformed UTF-8 string for an SGML context: this has
 
6027
        // to be done after entity expansion because the entities sometimes
 
6028
        // represent non-SGML characters (horror, horror!)
 
6029
        $html = HTMLPurifier_Encoder::cleanUTF8($html);
 
6030
 
 
6031
        return $html;
 
6032
    }
 
6033
 
 
6034
    /**
 
6035
     * Takes a string of HTML (fragment or document) and returns the content
 
6036
     * @todo Consider making protected
 
6037
     */
 
6038
    public function extractBody($html) {
 
6039
        $matches = array();
 
6040
        $result = preg_match('!<body[^>]*>(.*)</body>!is', $html, $matches);
 
6041
        if ($result) {
 
6042
            return $matches[1];
 
6043
        } else {
 
6044
            return $html;
 
6045
        }
 
6046
    }
 
6047
 
 
6048
}
 
6049
 
 
6050
 
 
6051
 
 
6052
 
 
6053
 
 
6054
/**
 
6055
 * Class that handles operations involving percent-encoding in URIs.
 
6056
 *
 
6057
 * @warning
 
6058
 *      Be careful when reusing instances of PercentEncoder. The object
 
6059
 *      you use for normalize() SHOULD NOT be used for encode(), or
 
6060
 *      vice-versa.
 
6061
 */
 
6062
class HTMLPurifier_PercentEncoder
 
6063
{
 
6064
 
 
6065
    /**
 
6066
     * Reserved characters to preserve when using encode().
 
6067
     */
 
6068
    protected $preserve = array();
 
6069
 
 
6070
    /**
 
6071
     * String of characters that should be preserved while using encode().
 
6072
     */
 
6073
    public function __construct($preserve = false) {
 
6074
        // unreserved letters, ought to const-ify
 
6075
        for ($i = 48; $i <= 57;  $i++) $this->preserve[$i] = true; // digits
 
6076
        for ($i = 65; $i <= 90;  $i++) $this->preserve[$i] = true; // upper-case
 
6077
        for ($i = 97; $i <= 122; $i++) $this->preserve[$i] = true; // lower-case
 
6078
        $this->preserve[45] = true; // Dash         -
 
6079
        $this->preserve[46] = true; // Period       .
 
6080
        $this->preserve[95] = true; // Underscore   _
 
6081
        $this->preserve[126]= true; // Tilde        ~
 
6082
 
 
6083
        // extra letters not to escape
 
6084
        if ($preserve !== false) {
 
6085
            for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
 
6086
                $this->preserve[ord($preserve[$i])] = true;
 
6087
            }
 
6088
        }
 
6089
    }
 
6090
 
 
6091
    /**
 
6092
     * Our replacement for urlencode, it encodes all non-reserved characters,
 
6093
     * as well as any extra characters that were instructed to be preserved.
 
6094
     * @note
 
6095
     *      Assumes that the string has already been normalized, making any
 
6096
     *      and all percent escape sequences valid. Percents will not be
 
6097
     *      re-escaped, regardless of their status in $preserve
 
6098
     * @param $string String to be encoded
 
6099
     * @return Encoded string.
 
6100
     */
 
6101
    public function encode($string) {
 
6102
        $ret = '';
 
6103
        for ($i = 0, $c = strlen($string); $i < $c; $i++) {
 
6104
            if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) {
 
6105
                $ret .= '%' . sprintf('%02X', $int);
 
6106
            } else {
 
6107
                $ret .= $string[$i];
 
6108
            }
 
6109
        }
 
6110
        return $ret;
 
6111
    }
 
6112
 
 
6113
    /**
 
6114
     * Fix up percent-encoding by decoding unreserved characters and normalizing.
 
6115
     * @warning This function is affected by $preserve, even though the
 
6116
     *          usual desired behavior is for this not to preserve those
 
6117
     *          characters. Be careful when reusing instances of PercentEncoder!
 
6118
     * @param $string String to normalize
 
6119
     */
 
6120
    public function normalize($string) {
 
6121
        if ($string == '') return '';
 
6122
        $parts = explode('%', $string);
 
6123
        $ret = array_shift($parts);
 
6124
        foreach ($parts as $part) {
 
6125
            $length = strlen($part);
 
6126
            if ($length < 2) {
 
6127
                $ret .= '%25' . $part;
 
6128
                continue;
 
6129
            }
 
6130
            $encoding = substr($part, 0, 2);
 
6131
            $text     = substr($part, 2);
 
6132
            if (!ctype_xdigit($encoding)) {
 
6133
                $ret .= '%25' . $part;
 
6134
                continue;
 
6135
            }
 
6136
            $int = hexdec($encoding);
 
6137
            if (isset($this->preserve[$int])) {
 
6138
                $ret .= chr($int) . $text;
 
6139
                continue;
 
6140
            }
 
6141
            $encoding = strtoupper($encoding);
 
6142
            $ret .= '%' . $encoding . $text;
 
6143
        }
 
6144
        return $ret;
 
6145
    }
 
6146
 
 
6147
}
 
6148
 
 
6149
 
 
6150
 
 
6151
 
 
6152
 
 
6153
/**
 
6154
 * Generic property list implementation
 
6155
 */
 
6156
class HTMLPurifier_PropertyList
 
6157
{
 
6158
    /**
 
6159
     * Internal data-structure for properties
 
6160
     */
 
6161
    protected $data = array();
 
6162
 
 
6163
    /**
 
6164
     * Parent plist
 
6165
     */
 
6166
    protected $parent;
 
6167
 
 
6168
    protected $cache;
 
6169
 
 
6170
    public function __construct($parent = null) {
 
6171
        $this->parent = $parent;
 
6172
    }
 
6173
 
 
6174
    /**
 
6175
     * Recursively retrieves the value for a key
 
6176
     */
 
6177
    public function get($name) {
 
6178
        if ($this->has($name)) return $this->data[$name];
 
6179
        // possible performance bottleneck, convert to iterative if necessary
 
6180
        if ($this->parent) return $this->parent->get($name);
 
6181
        throw new HTMLPurifier_Exception("Key '$name' not found");
 
6182
    }
 
6183
 
 
6184
    /**
 
6185
     * Sets the value of a key, for this plist
 
6186
     */
 
6187
    public function set($name, $value) {
 
6188
        $this->data[$name] = $value;
 
6189
    }
 
6190
 
 
6191
    /**
 
6192
     * Returns true if a given key exists
 
6193
     */
 
6194
    public function has($name) {
 
6195
        return array_key_exists($name, $this->data);
 
6196
    }
 
6197
 
 
6198
    /**
 
6199
     * Resets a value to the value of it's parent, usually the default. If
 
6200
     * no value is specified, the entire plist is reset.
 
6201
     */
 
6202
    public function reset($name = null) {
 
6203
        if ($name == null) $this->data = array();
 
6204
        else unset($this->data[$name]);
 
6205
    }
 
6206
 
 
6207
    /**
 
6208
     * Squashes this property list and all of its property lists into a single
 
6209
     * array, and returns the array. This value is cached by default.
 
6210
     * @param $force If true, ignores the cache and regenerates the array.
 
6211
     */
 
6212
    public function squash($force = false) {
 
6213
        if ($this->cache !== null && !$force) return $this->cache;
 
6214
        if ($this->parent) {
 
6215
            return $this->cache = array_merge($this->parent->squash($force), $this->data);
 
6216
        } else {
 
6217
            return $this->cache = $this->data;
 
6218
        }
 
6219
    }
 
6220
 
 
6221
    /**
 
6222
     * Returns the parent plist.
 
6223
     */
 
6224
    public function getParent() {
 
6225
        return $this->parent;
 
6226
    }
 
6227
 
 
6228
    /**
 
6229
     * Sets the parent plist.
 
6230
     */
 
6231
    public function setParent($plist) {
 
6232
        $this->parent = $plist;
 
6233
    }
 
6234
}
 
6235
 
 
6236
 
 
6237
 
 
6238
 
 
6239
 
 
6240
/**
 
6241
 * Property list iterator. Do not instantiate this class directly.
 
6242
 */
 
6243
class HTMLPurifier_PropertyListIterator extends FilterIterator
 
6244
{
 
6245
 
 
6246
    protected $l;
 
6247
    protected $filter;
 
6248
 
 
6249
    /**
 
6250
     * @param $data Array of data to iterate over
 
6251
     * @param $filter Optional prefix to only allow values of
 
6252
     */
 
6253
    public function __construct(Iterator $iterator, $filter = null) {
 
6254
        parent::__construct($iterator);
 
6255
        $this->l = strlen($filter);
 
6256
        $this->filter = $filter;
 
6257
    }
 
6258
 
 
6259
    public function accept() {
 
6260
        $key = $this->getInnerIterator()->key();
 
6261
        if( strncmp($key, $this->filter, $this->l) !== 0 ) {
 
6262
            return false;
 
6263
        }
 
6264
        return true;
 
6265
    }
 
6266
 
 
6267
}
 
6268
 
 
6269
 
 
6270
 
 
6271
 
 
6272
 
 
6273
/**
 
6274
 * Supertype for classes that define a strategy for modifying/purifying tokens.
 
6275
 *
 
6276
 * While HTMLPurifier's core purpose is fixing HTML into something proper,
 
6277
 * strategies provide plug points for extra configuration or even extra
 
6278
 * features, such as custom tags, custom parsing of text, etc.
 
6279
 */
 
6280
 
 
6281
 
 
6282
abstract class HTMLPurifier_Strategy
 
6283
{
 
6284
 
 
6285
    /**
 
6286
     * Executes the strategy on the tokens.
 
6287
     *
 
6288
     * @param $tokens Array of HTMLPurifier_Token objects to be operated on.
 
6289
     * @param $config Configuration options
 
6290
     * @returns Processed array of token objects.
 
6291
     */
 
6292
    abstract public function execute($tokens, $config, $context);
 
6293
 
 
6294
}
 
6295
 
 
6296
 
 
6297
 
 
6298
 
 
6299
 
 
6300
/**
 
6301
 * This is in almost every respect equivalent to an array except
 
6302
 * that it keeps track of which keys were accessed.
 
6303
 *
 
6304
 * @warning For the sake of backwards compatibility with early versions
 
6305
 *     of PHP 5, you must not use the $hash[$key] syntax; if you do
 
6306
 *     our version of offsetGet is never called.
 
6307
 */
 
6308
class HTMLPurifier_StringHash extends ArrayObject
 
6309
{
 
6310
    protected $accessed = array();
 
6311
 
 
6312
    /**
 
6313
     * Retrieves a value, and logs the access.
 
6314
     */
 
6315
    public function offsetGet($index) {
 
6316
        $this->accessed[$index] = true;
 
6317
        return parent::offsetGet($index);
 
6318
    }
 
6319
 
 
6320
    /**
 
6321
     * Returns a lookup array of all array indexes that have been accessed.
 
6322
     * @return Array in form array($index => true).
 
6323
     */
 
6324
    public function getAccessed() {
 
6325
        return $this->accessed;
 
6326
    }
 
6327
 
 
6328
    /**
 
6329
     * Resets the access array.
 
6330
     */
 
6331
    public function resetAccessed() {
 
6332
        $this->accessed = array();
 
6333
    }
 
6334
}
 
6335
 
 
6336
 
 
6337
 
 
6338
 
 
6339
 
 
6340
/**
 
6341
 * Parses string hash files. File format is as such:
 
6342
 *
 
6343
 *      DefaultKeyValue
 
6344
 *      KEY: Value
 
6345
 *      KEY2: Value2
 
6346
 *      --MULTILINE-KEY--
 
6347
 *      Multiline
 
6348
 *      value.
 
6349
 *
 
6350
 * Which would output something similar to:
 
6351
 *
 
6352
 *      array(
 
6353
 *          'ID' => 'DefaultKeyValue',
 
6354
 *          'KEY' => 'Value',
 
6355
 *          'KEY2' => 'Value2',
 
6356
 *          'MULTILINE-KEY' => "Multiline\nvalue.\n",
 
6357
 *      )
 
6358
 *
 
6359
 * We use this as an easy to use file-format for configuration schema
 
6360
 * files, but the class itself is usage agnostic.
 
6361
 *
 
6362
 * You can use ---- to forcibly terminate parsing of a single string-hash;
 
6363
 * this marker is used in multi string-hashes to delimit boundaries.
 
6364
 */
 
6365
class HTMLPurifier_StringHashParser
 
6366
{
 
6367
 
 
6368
    public $default = 'ID';
 
6369
 
 
6370
    /**
 
6371
     * Parses a file that contains a single string-hash.
 
6372
     */
 
6373
    public function parseFile($file) {
 
6374
        if (!file_exists($file)) return false;
 
6375
        $fh = fopen($file, 'r');
 
6376
        if (!$fh) return false;
 
6377
        $ret = $this->parseHandle($fh);
 
6378
        fclose($fh);
 
6379
        return $ret;
 
6380
    }
 
6381
 
 
6382
    /**
 
6383
     * Parses a file that contains multiple string-hashes delimited by '----'
 
6384
     */
 
6385
    public function parseMultiFile($file) {
 
6386
        if (!file_exists($file)) return false;
 
6387
        $ret = array();
 
6388
        $fh = fopen($file, 'r');
 
6389
        if (!$fh) return false;
 
6390
        while (!feof($fh)) {
 
6391
            $ret[] = $this->parseHandle($fh);
 
6392
        }
 
6393
        fclose($fh);
 
6394
        return $ret;
 
6395
    }
 
6396
 
 
6397
    /**
 
6398
     * Internal parser that acepts a file handle.
 
6399
     * @note While it's possible to simulate in-memory parsing by using
 
6400
     *       custom stream wrappers, if such a use-case arises we should
 
6401
     *       factor out the file handle into its own class.
 
6402
     * @param $fh File handle with pointer at start of valid string-hash
 
6403
     *            block.
 
6404
     */
 
6405
    protected function parseHandle($fh) {
 
6406
        $state   = false;
 
6407
        $single  = false;
 
6408
        $ret     = array();
 
6409
        do {
 
6410
            $line = fgets($fh);
 
6411
            if ($line === false) break;
 
6412
            $line = rtrim($line, "\n\r");
 
6413
            if (!$state && $line === '') continue;
 
6414
            if ($line === '----') break;
 
6415
            if (strncmp('--#', $line, 3) === 0) {
 
6416
                // Comment
 
6417
                continue;
 
6418
            } elseif (strncmp('--', $line, 2) === 0) {
 
6419
                // Multiline declaration
 
6420
                $state = trim($line, '- ');
 
6421
                if (!isset($ret[$state])) $ret[$state] = '';
 
6422
                continue;
 
6423
            } elseif (!$state) {
 
6424
                $single = true;
 
6425
                if (strpos($line, ':') !== false) {
 
6426
                    // Single-line declaration
 
6427
                    list($state, $line) = explode(':', $line, 2);
 
6428
                    $line = trim($line);
 
6429
                } else {
 
6430
                    // Use default declaration
 
6431
                    $state  = $this->default;
 
6432
                }
 
6433
            }
 
6434
            if ($single) {
 
6435
                $ret[$state] = $line;
 
6436
                $single = false;
 
6437
                $state  = false;
 
6438
            } else {
 
6439
                $ret[$state] .= "$line\n";
 
6440
            }
 
6441
        } while (!feof($fh));
 
6442
        return $ret;
 
6443
    }
 
6444
 
 
6445
}
 
6446
 
 
6447
 
 
6448
 
 
6449
 
 
6450
 
 
6451
/**
 
6452
 * Defines a mutation of an obsolete tag into a valid tag.
 
6453
 */
 
6454
abstract class HTMLPurifier_TagTransform
 
6455
{
 
6456
 
 
6457
    /**
 
6458
     * Tag name to transform the tag to.
 
6459
     */
 
6460
    public $transform_to;
 
6461
 
 
6462
    /**
 
6463
     * Transforms the obsolete tag into the valid tag.
 
6464
     * @param $tag Tag to be transformed.
 
6465
     * @param $config Mandatory HTMLPurifier_Config object
 
6466
     * @param $context Mandatory HTMLPurifier_Context object
 
6467
     */
 
6468
    abstract public function transform($tag, $config, $context);
 
6469
 
 
6470
    /**
 
6471
     * Prepends CSS properties to the style attribute, creating the
 
6472
     * attribute if it doesn't exist.
 
6473
     * @warning Copied over from AttrTransform, be sure to keep in sync
 
6474
     * @param $attr Attribute array to process (passed by reference)
 
6475
     * @param $css CSS to prepend
 
6476
     */
 
6477
    protected function prependCSS(&$attr, $css) {
 
6478
        $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
 
6479
        $attr['style'] = $css . $attr['style'];
 
6480
    }
 
6481
 
 
6482
}
 
6483
 
 
6484
 
 
6485
 
 
6486
 
 
6487
 
 
6488
/**
 
6489
 * Abstract base token class that all others inherit from.
 
6490
 */
 
6491
class HTMLPurifier_Token {
 
6492
    public $line; /**< Line number node was on in source document. Null if unknown. */
 
6493
    public $col;  /**< Column of line node was on in source document. Null if unknown. */
 
6494
 
 
6495
    /**
 
6496
     * Lookup array of processing that this token is exempt from.
 
6497
     * Currently, valid values are "ValidateAttributes" and
 
6498
     * "MakeWellFormed_TagClosedError"
 
6499
     */
 
6500
    public $armor = array();
 
6501
 
 
6502
    /**
 
6503
     * Used during MakeWellFormed.
 
6504
     */
 
6505
    public $skip;
 
6506
    public $rewind;
 
6507
    public $carryover;
 
6508
 
 
6509
    public function __get($n) {
 
6510
      if ($n === 'type') {
 
6511
        trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE);
 
6512
        switch (get_class($this)) {
 
6513
          case 'HTMLPurifier_Token_Start':      return 'start';
 
6514
          case 'HTMLPurifier_Token_Empty':      return 'empty';
 
6515
          case 'HTMLPurifier_Token_End':        return 'end';
 
6516
          case 'HTMLPurifier_Token_Text':       return 'text';
 
6517
          case 'HTMLPurifier_Token_Comment':    return 'comment';
 
6518
          default: return null;
 
6519
        }
 
6520
      }
 
6521
    }
 
6522
 
 
6523
    /**
 
6524
     * Sets the position of the token in the source document.
 
6525
     */
 
6526
    public function position($l = null, $c = null) {
 
6527
        $this->line = $l;
 
6528
        $this->col  = $c;
 
6529
    }
 
6530
 
 
6531
    /**
 
6532
     * Convenience function for DirectLex settings line/col position.
 
6533
     */
 
6534
    public function rawPosition($l, $c) {
 
6535
        if ($c === -1) $l++;
 
6536
        $this->line = $l;
 
6537
        $this->col  = $c;
 
6538
    }
 
6539
 
 
6540
}
 
6541
 
 
6542
 
 
6543
 
 
6544
 
 
6545
 
 
6546
/**
 
6547
 * Factory for token generation.
 
6548
 *
 
6549
 * @note Doing some benchmarking indicates that the new operator is much
 
6550
 *       slower than the clone operator (even discounting the cost of the
 
6551
 *       constructor).  This class is for that optimization.
 
6552
 *       Other then that, there's not much point as we don't
 
6553
 *       maintain parallel HTMLPurifier_Token hierarchies (the main reason why
 
6554
 *       you'd want to use an abstract factory).
 
6555
 * @todo Port DirectLex to use this
 
6556
 */
 
6557
class HTMLPurifier_TokenFactory
 
6558
{
 
6559
 
 
6560
    /**
 
6561
     * Prototypes that will be cloned.
 
6562
     * @private
 
6563
     */
 
6564
    // p stands for prototype
 
6565
    private $p_start, $p_end, $p_empty, $p_text, $p_comment;
 
6566
 
 
6567
    /**
 
6568
     * Generates blank prototypes for cloning.
 
6569
     */
 
6570
    public function __construct() {
 
6571
        $this->p_start  = new HTMLPurifier_Token_Start('', array());
 
6572
        $this->p_end    = new HTMLPurifier_Token_End('');
 
6573
        $this->p_empty  = new HTMLPurifier_Token_Empty('', array());
 
6574
        $this->p_text   = new HTMLPurifier_Token_Text('');
 
6575
        $this->p_comment= new HTMLPurifier_Token_Comment('');
 
6576
    }
 
6577
 
 
6578
    /**
 
6579
     * Creates a HTMLPurifier_Token_Start.
 
6580
     * @param $name Tag name
 
6581
     * @param $attr Associative array of attributes
 
6582
     * @return Generated HTMLPurifier_Token_Start
 
6583
     */
 
6584
    public function createStart($name, $attr = array()) {
 
6585
        $p = clone $this->p_start;
 
6586
        $p->__construct($name, $attr);
 
6587
        return $p;
 
6588
    }
 
6589
 
 
6590
    /**
 
6591
     * Creates a HTMLPurifier_Token_End.
 
6592
     * @param $name Tag name
 
6593
     * @return Generated HTMLPurifier_Token_End
 
6594
     */
 
6595
    public function createEnd($name) {
 
6596
        $p = clone $this->p_end;
 
6597
        $p->__construct($name);
 
6598
        return $p;
 
6599
    }
 
6600
 
 
6601
    /**
 
6602
     * Creates a HTMLPurifier_Token_Empty.
 
6603
     * @param $name Tag name
 
6604
     * @param $attr Associative array of attributes
 
6605
     * @return Generated HTMLPurifier_Token_Empty
 
6606
     */
 
6607
    public function createEmpty($name, $attr = array()) {
 
6608
        $p = clone $this->p_empty;
 
6609
        $p->__construct($name, $attr);
 
6610
        return $p;
 
6611
    }
 
6612
 
 
6613
    /**
 
6614
     * Creates a HTMLPurifier_Token_Text.
 
6615
     * @param $data Data of text token
 
6616
     * @return Generated HTMLPurifier_Token_Text
 
6617
     */
 
6618
    public function createText($data) {
 
6619
        $p = clone $this->p_text;
 
6620
        $p->__construct($data);
 
6621
        return $p;
 
6622
    }
 
6623
 
 
6624
    /**
 
6625
     * Creates a HTMLPurifier_Token_Comment.
 
6626
     * @param $data Data of comment token
 
6627
     * @return Generated HTMLPurifier_Token_Comment
 
6628
     */
 
6629
    public function createComment($data) {
 
6630
        $p = clone $this->p_comment;
 
6631
        $p->__construct($data);
 
6632
        return $p;
 
6633
    }
 
6634
 
 
6635
}
 
6636
 
 
6637
 
 
6638
 
 
6639
 
 
6640
 
 
6641
/**
 
6642
 * HTML Purifier's internal representation of a URI.
 
6643
 * @note
 
6644
 *      Internal data-structures are completely escaped. If the data needs
 
6645
 *      to be used in a non-URI context (which is very unlikely), be sure
 
6646
 *      to decode it first. The URI may not necessarily be well-formed until
 
6647
 *      validate() is called.
 
6648
 */
 
6649
class HTMLPurifier_URI
 
6650
{
 
6651
 
 
6652
    public $scheme, $userinfo, $host, $port, $path, $query, $fragment;
 
6653
 
 
6654
    /**
 
6655
     * @note Automatically normalizes scheme and port
 
6656
     */
 
6657
    public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment) {
 
6658
        $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
 
6659
        $this->userinfo = $userinfo;
 
6660
        $this->host = $host;
 
6661
        $this->port = is_null($port) ? $port : (int) $port;
 
6662
        $this->path = $path;
 
6663
        $this->query = $query;
 
6664
        $this->fragment = $fragment;
 
6665
    }
 
6666
 
 
6667
    /**
 
6668
     * Retrieves a scheme object corresponding to the URI's scheme/default
 
6669
     * @param $config Instance of HTMLPurifier_Config
 
6670
     * @param $context Instance of HTMLPurifier_Context
 
6671
     * @return Scheme object appropriate for validating this URI
 
6672
     */
 
6673
    public function getSchemeObj($config, $context) {
 
6674
        $registry = HTMLPurifier_URISchemeRegistry::instance();
 
6675
        if ($this->scheme !== null) {
 
6676
            $scheme_obj = $registry->getScheme($this->scheme, $config, $context);
 
6677
            if (!$scheme_obj) return false; // invalid scheme, clean it out
 
6678
        } else {
 
6679
            // no scheme: retrieve the default one
 
6680
            $def = $config->getDefinition('URI');
 
6681
            $scheme_obj = $registry->getScheme($def->defaultScheme, $config, $context);
 
6682
            if (!$scheme_obj) {
 
6683
                // something funky happened to the default scheme object
 
6684
                trigger_error(
 
6685
                    'Default scheme object "' . $def->defaultScheme . '" was not readable',
 
6686
                    E_USER_WARNING
 
6687
                );
 
6688
                return false;
 
6689
            }
 
6690
        }
 
6691
        return $scheme_obj;
 
6692
    }
 
6693
 
 
6694
    /**
 
6695
     * Generic validation method applicable for all schemes. May modify
 
6696
     * this URI in order to get it into a compliant form.
 
6697
     * @param $config Instance of HTMLPurifier_Config
 
6698
     * @param $context Instance of HTMLPurifier_Context
 
6699
     * @return True if validation/filtering succeeds, false if failure
 
6700
     */
 
6701
    public function validate($config, $context) {
 
6702
 
 
6703
        // ABNF definitions from RFC 3986
 
6704
        $chars_sub_delims = '!$&\'()*+,;=';
 
6705
        $chars_gen_delims = ':/?#[]@';
 
6706
        $chars_pchar = $chars_sub_delims . ':@';
 
6707
 
 
6708
        // validate scheme (MUST BE FIRST!)
 
6709
        if (!is_null($this->scheme) && is_null($this->host)) {
 
6710
            $def = $config->getDefinition('URI');
 
6711
            if ($def->defaultScheme === $this->scheme) {
 
6712
                $this->scheme = null;
 
6713
            }
 
6714
        }
 
6715
 
 
6716
        // validate host
 
6717
        if (!is_null($this->host)) {
 
6718
            $host_def = new HTMLPurifier_AttrDef_URI_Host();
 
6719
            $this->host = $host_def->validate($this->host, $config, $context);
 
6720
            if ($this->host === false) $this->host = null;
 
6721
        }
 
6722
 
 
6723
        // validate username
 
6724
        if (!is_null($this->userinfo)) {
 
6725
            $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
 
6726
            $this->userinfo = $encoder->encode($this->userinfo);
 
6727
        }
 
6728
 
 
6729
        // validate port
 
6730
        if (!is_null($this->port)) {
 
6731
            if ($this->port < 1 || $this->port > 65535) $this->port = null;
 
6732
        }
 
6733
 
 
6734
        // validate path
 
6735
        $path_parts = array();
 
6736
        $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
 
6737
        if (!is_null($this->host)) {
 
6738
            // path-abempty (hier and relative)
 
6739
            $this->path = $segments_encoder->encode($this->path);
 
6740
        } elseif ($this->path !== '' && $this->path[0] === '/') {
 
6741
            // path-absolute (hier and relative)
 
6742
            if (strlen($this->path) >= 2 && $this->path[1] === '/') {
 
6743
                // This shouldn't ever happen!
 
6744
                $this->path = '';
 
6745
            } else {
 
6746
                $this->path = $segments_encoder->encode($this->path);
 
6747
            }
 
6748
        } elseif (!is_null($this->scheme) && $this->path !== '') {
 
6749
            // path-rootless (hier)
 
6750
            // Short circuit evaluation means we don't need to check nz
 
6751
            $this->path = $segments_encoder->encode($this->path);
 
6752
        } elseif (is_null($this->scheme) && $this->path !== '') {
 
6753
            // path-noscheme (relative)
 
6754
            // (once again, not checking nz)
 
6755
            $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
 
6756
            $c = strpos($this->path, '/');
 
6757
            if ($c !== false) {
 
6758
                $this->path =
 
6759
                    $segment_nc_encoder->encode(substr($this->path, 0, $c)) .
 
6760
                    $segments_encoder->encode(substr($this->path, $c));
 
6761
            } else {
 
6762
                $this->path = $segment_nc_encoder->encode($this->path);
 
6763
            }
 
6764
        } else {
 
6765
            // path-empty (hier and relative)
 
6766
            $this->path = ''; // just to be safe
 
6767
        }
 
6768
 
 
6769
        // qf = query and fragment
 
6770
        $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
 
6771
 
 
6772
        if (!is_null($this->query)) {
 
6773
            $this->query = $qf_encoder->encode($this->query);
 
6774
        }
 
6775
 
 
6776
        if (!is_null($this->fragment)) {
 
6777
            $this->fragment = $qf_encoder->encode($this->fragment);
 
6778
        }
 
6779
 
 
6780
        return true;
 
6781
 
 
6782
    }
 
6783
 
 
6784
    /**
 
6785
     * Convert URI back to string
 
6786
     * @return String URI appropriate for output
 
6787
     */
 
6788
    public function toString() {
 
6789
        // reconstruct authority
 
6790
        $authority = null;
 
6791
        if (!is_null($this->host)) {
 
6792
            $authority = '';
 
6793
            if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@';
 
6794
            $authority .= $this->host;
 
6795
            if(!is_null($this->port))     $authority .= ':' . $this->port;
 
6796
        }
 
6797
 
 
6798
        // reconstruct the result
 
6799
        $result = '';
 
6800
        if (!is_null($this->scheme))    $result .= $this->scheme . ':';
 
6801
        if (!is_null($authority))       $result .=  '//' . $authority;
 
6802
        $result .= $this->path;
 
6803
        if (!is_null($this->query))     $result .= '?' . $this->query;
 
6804
        if (!is_null($this->fragment))  $result .= '#' . $this->fragment;
 
6805
 
 
6806
        return $result;
 
6807
    }
 
6808
 
 
6809
}
 
6810
 
 
6811
 
 
6812
 
 
6813
 
 
6814
 
 
6815
class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
 
6816
{
 
6817
 
 
6818
    public $type = 'URI';
 
6819
    protected $filters = array();
 
6820
    protected $postFilters = array();
 
6821
    protected $registeredFilters = array();
 
6822
 
 
6823
    /**
 
6824
     * HTMLPurifier_URI object of the base specified at %URI.Base
 
6825
     */
 
6826
    public $base;
 
6827
 
 
6828
    /**
 
6829
     * String host to consider "home" base, derived off of $base
 
6830
     */
 
6831
    public $host;
 
6832
 
 
6833
    /**
 
6834
     * Name of default scheme based on %URI.DefaultScheme and %URI.Base
 
6835
     */
 
6836
    public $defaultScheme;
 
6837
 
 
6838
    public function __construct() {
 
6839
        $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
 
6840
        $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
 
6841
        $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
 
6842
        $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
 
6843
        $this->registerFilter(new HTMLPurifier_URIFilter_Munge());
 
6844
    }
 
6845
 
 
6846
    public function registerFilter($filter) {
 
6847
        $this->registeredFilters[$filter->name] = $filter;
 
6848
    }
 
6849
 
 
6850
    public function addFilter($filter, $config) {
 
6851
        $r = $filter->prepare($config);
 
6852
        if ($r === false) return; // null is ok, for backwards compat
 
6853
        if ($filter->post) {
 
6854
            $this->postFilters[$filter->name] = $filter;
 
6855
        } else {
 
6856
            $this->filters[$filter->name] = $filter;
 
6857
        }
 
6858
    }
 
6859
 
 
6860
    protected function doSetup($config) {
 
6861
        $this->setupMemberVariables($config);
 
6862
        $this->setupFilters($config);
 
6863
    }
 
6864
 
 
6865
    protected function setupFilters($config) {
 
6866
        foreach ($this->registeredFilters as $name => $filter) {
 
6867
            $conf = $config->get('URI.' . $name);
 
6868
            if ($conf !== false && $conf !== null) {
 
6869
                $this->addFilter($filter, $config);
 
6870
            }
 
6871
        }
 
6872
        unset($this->registeredFilters);
 
6873
    }
 
6874
 
 
6875
    protected function setupMemberVariables($config) {
 
6876
        $this->host = $config->get('URI.Host');
 
6877
        $base_uri = $config->get('URI.Base');
 
6878
        if (!is_null($base_uri)) {
 
6879
            $parser = new HTMLPurifier_URIParser();
 
6880
            $this->base = $parser->parse($base_uri);
 
6881
            $this->defaultScheme = $this->base->scheme;
 
6882
            if (is_null($this->host)) $this->host = $this->base->host;
 
6883
        }
 
6884
        if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme');
 
6885
    }
 
6886
 
 
6887
    public function filter(&$uri, $config, $context) {
 
6888
        foreach ($this->filters as $name => $f) {
 
6889
            $result = $f->filter($uri, $config, $context);
 
6890
            if (!$result) return false;
 
6891
        }
 
6892
        return true;
 
6893
    }
 
6894
 
 
6895
    public function postFilter(&$uri, $config, $context) {
 
6896
        foreach ($this->postFilters as $name => $f) {
 
6897
            $result = $f->filter($uri, $config, $context);
 
6898
            if (!$result) return false;
 
6899
        }
 
6900
        return true;
 
6901
    }
 
6902
 
 
6903
}
 
6904
 
 
6905
 
 
6906
 
 
6907
 
 
6908
 
 
6909
/**
 
6910
 * Chainable filters for custom URI processing.
 
6911
 *
 
6912
 * These filters can perform custom actions on a URI filter object,
 
6913
 * including transformation or blacklisting.
 
6914
 *
 
6915
 * @warning This filter is called before scheme object validation occurs.
 
6916
 *          Make sure, if you require a specific scheme object, you
 
6917
 *          you check that it exists. This allows filters to convert
 
6918
 *          proprietary URI schemes into regular ones.
 
6919
 */
 
6920
abstract class HTMLPurifier_URIFilter
 
6921
{
 
6922
 
 
6923
    /**
 
6924
     * Unique identifier of filter
 
6925
     */
 
6926
    public $name;
 
6927
 
 
6928
    /**
 
6929
     * True if this filter should be run after scheme validation.
 
6930
     */
 
6931
    public $post = false;
 
6932
 
 
6933
    /**
 
6934
     * Performs initialization for the filter
 
6935
     */
 
6936
    public function prepare($config) {return true;}
 
6937
 
 
6938
    /**
 
6939
     * Filter a URI object
 
6940
     * @param $uri Reference to URI object variable
 
6941
     * @param $config Instance of HTMLPurifier_Config
 
6942
     * @param $context Instance of HTMLPurifier_Context
 
6943
     * @return bool Whether or not to continue processing: false indicates
 
6944
     *         URL is no good, true indicates continue processing. Note that
 
6945
     *         all changes are committed directly on the URI object
 
6946
     */
 
6947
    abstract public function filter(&$uri, $config, $context);
 
6948
 
 
6949
}
 
6950
 
 
6951
 
 
6952
 
 
6953
 
 
6954
 
 
6955
/**
 
6956
 * Parses a URI into the components and fragment identifier as specified
 
6957
 * by RFC 3986.
 
6958
 */
 
6959
class HTMLPurifier_URIParser
 
6960
{
 
6961
 
 
6962
    /**
 
6963
     * Instance of HTMLPurifier_PercentEncoder to do normalization with.
 
6964
     */
 
6965
    protected $percentEncoder;
 
6966
 
 
6967
    public function __construct() {
 
6968
        $this->percentEncoder = new HTMLPurifier_PercentEncoder();
 
6969
    }
 
6970
 
 
6971
    /**
 
6972
     * Parses a URI.
 
6973
     * @param $uri string URI to parse
 
6974
     * @return HTMLPurifier_URI representation of URI. This representation has
 
6975
     *         not been validated yet and may not conform to RFC.
 
6976
     */
 
6977
    public function parse($uri) {
 
6978
 
 
6979
        $uri = $this->percentEncoder->normalize($uri);
 
6980
 
 
6981
        // Regexp is as per Appendix B.
 
6982
        // Note that ["<>] are an addition to the RFC's recommended
 
6983
        // characters, because they represent external delimeters.
 
6984
        $r_URI = '!'.
 
6985
            '(([^:/?#"<>]+):)?'. // 2. Scheme
 
6986
            '(//([^/?#"<>]*))?'. // 4. Authority
 
6987
            '([^?#"<>]*)'.       // 5. Path
 
6988
            '(\?([^#"<>]*))?'.   // 7. Query
 
6989
            '(#([^"<>]*))?'.     // 8. Fragment
 
6990
            '!';
 
6991
 
 
6992
        $matches = array();
 
6993
        $result = preg_match($r_URI, $uri, $matches);
 
6994
 
 
6995
        if (!$result) return false; // *really* invalid URI
 
6996
 
 
6997
        // seperate out parts
 
6998
        $scheme     = !empty($matches[1]) ? $matches[2] : null;
 
6999
        $authority  = !empty($matches[3]) ? $matches[4] : null;
 
7000
        $path       = $matches[5]; // always present, can be empty
 
7001
        $query      = !empty($matches[6]) ? $matches[7] : null;
 
7002
        $fragment   = !empty($matches[8]) ? $matches[9] : null;
 
7003
 
 
7004
        // further parse authority
 
7005
        if ($authority !== null) {
 
7006
            $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/";
 
7007
            $matches = array();
 
7008
            preg_match($r_authority, $authority, $matches);
 
7009
            $userinfo   = !empty($matches[1]) ? $matches[2] : null;
 
7010
            $host       = !empty($matches[3]) ? $matches[3] : '';
 
7011
            $port       = !empty($matches[4]) ? (int) $matches[5] : null;
 
7012
        } else {
 
7013
            $port = $host = $userinfo = null;
 
7014
        }
 
7015
 
 
7016
        return new HTMLPurifier_URI(
 
7017
            $scheme, $userinfo, $host, $port, $path, $query, $fragment);
 
7018
    }
 
7019
 
 
7020
}
 
7021
 
 
7022
 
 
7023
 
 
7024
 
 
7025
 
 
7026
/**
 
7027
 * Validator for the components of a URI for a specific scheme
 
7028
 */
 
7029
class HTMLPurifier_URIScheme
 
7030
{
 
7031
 
 
7032
    /**
 
7033
     * Scheme's default port (integer)
 
7034
     */
 
7035
    public $default_port = null;
 
7036
 
 
7037
    /**
 
7038
     * Whether or not URIs of this schem are locatable by a browser
 
7039
     * http and ftp are accessible, while mailto and news are not.
 
7040
     */
 
7041
    public $browsable = false;
 
7042
 
 
7043
    /**
 
7044
     * Whether or not the URI always uses <hier_part>, resolves edge cases
 
7045
     * with making relative URIs absolute
 
7046
     */
 
7047
    public $hierarchical = false;
 
7048
 
 
7049
    /**
 
7050
     * Validates the components of a URI
 
7051
     * @note This implementation should be called by children if they define
 
7052
     *       a default port, as it does port processing.
 
7053
     * @param $uri Instance of HTMLPurifier_URI
 
7054
     * @param $config HTMLPurifier_Config object
 
7055
     * @param $context HTMLPurifier_Context object
 
7056
     * @return Bool success or failure
 
7057
     */
 
7058
    public function validate(&$uri, $config, $context) {
 
7059
        if ($this->default_port == $uri->port) $uri->port = null;
 
7060
        return true;
 
7061
    }
 
7062
 
 
7063
}
 
7064
 
 
7065
 
 
7066
 
 
7067
 
 
7068
 
 
7069
/**
 
7070
 * Registry for retrieving specific URI scheme validator objects.
 
7071
 */
 
7072
class HTMLPurifier_URISchemeRegistry
 
7073
{
 
7074
 
 
7075
    /**
 
7076
     * Retrieve sole instance of the registry.
 
7077
     * @param $prototype Optional prototype to overload sole instance with,
 
7078
     *                   or bool true to reset to default registry.
 
7079
     * @note Pass a registry object $prototype with a compatible interface and
 
7080
     *       the function will copy it and return it all further times.
 
7081
     */
 
7082
    public static function instance($prototype = null) {
 
7083
        static $instance = null;
 
7084
        if ($prototype !== null) {
 
7085
            $instance = $prototype;
 
7086
        } elseif ($instance === null || $prototype == true) {
 
7087
            $instance = new HTMLPurifier_URISchemeRegistry();
 
7088
        }
 
7089
        return $instance;
 
7090
    }
 
7091
 
 
7092
    /**
 
7093
     * Cache of retrieved schemes.
 
7094
     */
 
7095
    protected $schemes = array();
 
7096
 
 
7097
    /**
 
7098
     * Retrieves a scheme validator object
 
7099
     * @param $scheme String scheme name like http or mailto
 
7100
     * @param $config HTMLPurifier_Config object
 
7101
     * @param $config HTMLPurifier_Context object
 
7102
     */
 
7103
    public function getScheme($scheme, $config, $context) {
 
7104
        if (!$config) $config = HTMLPurifier_Config::createDefault();
 
7105
 
 
7106
        // important, otherwise attacker could include arbitrary file
 
7107
        $allowed_schemes = $config->get('URI.AllowedSchemes');
 
7108
        if (!$config->get('URI.OverrideAllowedSchemes') &&
 
7109
            !isset($allowed_schemes[$scheme])
 
7110
        ) {
 
7111
            return;
 
7112
        }
 
7113
 
 
7114
        if (isset($this->schemes[$scheme])) return $this->schemes[$scheme];
 
7115
        if (!isset($allowed_schemes[$scheme])) return;
 
7116
 
 
7117
        $class = 'HTMLPurifier_URIScheme_' . $scheme;
 
7118
        if (!class_exists($class)) return;
 
7119
        $this->schemes[$scheme] = new $class();
 
7120
        return $this->schemes[$scheme];
 
7121
    }
 
7122
 
 
7123
    /**
 
7124
     * Registers a custom scheme to the cache, bypassing reflection.
 
7125
     * @param $scheme Scheme name
 
7126
     * @param $scheme_obj HTMLPurifier_URIScheme object
 
7127
     */
 
7128
    public function register($scheme, $scheme_obj) {
 
7129
        $this->schemes[$scheme] = $scheme_obj;
 
7130
    }
 
7131
 
 
7132
}
 
7133
 
 
7134
 
 
7135
 
 
7136
 
 
7137
 
 
7138
/**
 
7139
 * Class for converting between different unit-lengths as specified by
 
7140
 * CSS.
 
7141
 */
 
7142
class HTMLPurifier_UnitConverter
 
7143
{
 
7144
 
 
7145
    const ENGLISH = 1;
 
7146
    const METRIC = 2;
 
7147
    const DIGITAL = 3;
 
7148
 
 
7149
    /**
 
7150
     * Units information array. Units are grouped into measuring systems
 
7151
     * (English, Metric), and are assigned an integer representing
 
7152
     * the conversion factor between that unit and the smallest unit in
 
7153
     * the system. Numeric indexes are actually magical constants that
 
7154
     * encode conversion data from one system to the next, with a O(n^2)
 
7155
     * constraint on memory (this is generally not a problem, since
 
7156
     * the number of measuring systems is small.)
 
7157
     */
 
7158
    protected static $units = array(
 
7159
        self::ENGLISH => array(
 
7160
            'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
 
7161
            'pt' => 4,
 
7162
            'pc' => 48,
 
7163
            'in' => 288,
 
7164
            self::METRIC => array('pt', '0.352777778', 'mm'),
 
7165
        ),
 
7166
        self::METRIC => array(
 
7167
            'mm' => 1,
 
7168
            'cm' => 10,
 
7169
            self::ENGLISH => array('mm', '2.83464567', 'pt'),
 
7170
        ),
 
7171
    );
 
7172
 
 
7173
    /**
 
7174
     * Minimum bcmath precision for output.
 
7175
     */
 
7176
    protected $outputPrecision;
 
7177
 
 
7178
    /**
 
7179
     * Bcmath precision for internal calculations.
 
7180
     */
 
7181
    protected $internalPrecision;
 
7182
 
 
7183
    /**
 
7184
     * Whether or not BCMath is available
 
7185
     */
 
7186
    private $bcmath;
 
7187
 
 
7188
    public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
 
7189
        $this->outputPrecision = $output_precision;
 
7190
        $this->internalPrecision = $internal_precision;
 
7191
        $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
 
7192
    }
 
7193
 
 
7194
    /**
 
7195
     * Converts a length object of one unit into another unit.
 
7196
     * @param HTMLPurifier_Length $length
 
7197
     *      Instance of HTMLPurifier_Length to convert. You must validate()
 
7198
     *      it before passing it here!
 
7199
     * @param string $to_unit
 
7200
     *      Unit to convert to.
 
7201
     * @note
 
7202
     *      About precision: This conversion function pays very special
 
7203
     *      attention to the incoming precision of values and attempts
 
7204
     *      to maintain a number of significant figure. Results are
 
7205
     *      fairly accurate up to nine digits. Some caveats:
 
7206
     *          - If a number is zero-padded as a result of this significant
 
7207
     *            figure tracking, the zeroes will be eliminated.
 
7208
     *          - If a number contains less than four sigfigs ($outputPrecision)
 
7209
     *            and this causes some decimals to be excluded, those
 
7210
     *            decimals will be added on.
 
7211
     */
 
7212
    public function convert($length, $to_unit) {
 
7213
 
 
7214
        if (!$length->isValid()) return false;
 
7215
 
 
7216
        $n    = $length->getN();
 
7217
        $unit = $length->getUnit();
 
7218
 
 
7219
        if ($n === '0' || $unit === false) {
 
7220
            return new HTMLPurifier_Length('0', false);
 
7221
        }
 
7222
 
 
7223
        $state = $dest_state = false;
 
7224
        foreach (self::$units as $k => $x) {
 
7225
            if (isset($x[$unit])) $state = $k;
 
7226
            if (isset($x[$to_unit])) $dest_state = $k;
 
7227
        }
 
7228
        if (!$state || !$dest_state) return false;
 
7229
 
 
7230
        // Some calculations about the initial precision of the number;
 
7231
        // this will be useful when we need to do final rounding.
 
7232
        $sigfigs = $this->getSigFigs($n);
 
7233
        if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
 
7234
 
 
7235
        // BCMath's internal precision deals only with decimals. Use
 
7236
        // our default if the initial number has no decimals, or increase
 
7237
        // it by how ever many decimals, thus, the number of guard digits
 
7238
        // will always be greater than or equal to internalPrecision.
 
7239
        $log = (int) floor(log(abs($n), 10));
 
7240
        $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
 
7241
 
 
7242
        for ($i = 0; $i < 2; $i++) {
 
7243
 
 
7244
            // Determine what unit IN THIS SYSTEM we need to convert to
 
7245
            if ($dest_state === $state) {
 
7246
                // Simple conversion
 
7247
                $dest_unit = $to_unit;
 
7248
            } else {
 
7249
                // Convert to the smallest unit, pending a system shift
 
7250
                $dest_unit = self::$units[$state][$dest_state][0];
 
7251
            }
 
7252
 
 
7253
            // Do the conversion if necessary
 
7254
            if ($dest_unit !== $unit) {
 
7255
                $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
 
7256
                $n = $this->mul($n, $factor, $cp);
 
7257
                $unit = $dest_unit;
 
7258
            }
 
7259
 
 
7260
            // Output was zero, so bail out early. Shouldn't ever happen.
 
7261
            if ($n === '') {
 
7262
                $n = '0';
 
7263
                $unit = $to_unit;
 
7264
                break;
 
7265
            }
 
7266
 
 
7267
            // It was a simple conversion, so bail out
 
7268
            if ($dest_state === $state) {
 
7269
                break;
 
7270
            }
 
7271
 
 
7272
            if ($i !== 0) {
 
7273
                // Conversion failed! Apparently, the system we forwarded
 
7274
                // to didn't have this unit. This should never happen!
 
7275
                return false;
 
7276
            }
 
7277
 
 
7278
            // Pre-condition: $i == 0
 
7279
 
 
7280
            // Perform conversion to next system of units
 
7281
            $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
 
7282
            $unit = self::$units[$state][$dest_state][2];
 
7283
            $state = $dest_state;
 
7284
 
 
7285
            // One more loop around to convert the unit in the new system.
 
7286
 
 
7287
        }
 
7288
 
 
7289
        // Post-condition: $unit == $to_unit
 
7290
        if ($unit !== $to_unit) return false;
 
7291
 
 
7292
        // Useful for debugging:
 
7293
        //echo "<pre>n";
 
7294
        //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
 
7295
 
 
7296
        $n = $this->round($n, $sigfigs);
 
7297
        if (strpos($n, '.') !== false) $n = rtrim($n, '0');
 
7298
        $n = rtrim($n, '.');
 
7299
 
 
7300
        return new HTMLPurifier_Length($n, $unit);
 
7301
    }
 
7302
 
 
7303
    /**
 
7304
     * Returns the number of significant figures in a string number.
 
7305
     * @param string $n Decimal number
 
7306
     * @return int number of sigfigs
 
7307
     */
 
7308
    public function getSigFigs($n) {
 
7309
        $n = ltrim($n, '0+-');
 
7310
        $dp = strpos($n, '.'); // decimal position
 
7311
        if ($dp === false) {
 
7312
            $sigfigs = strlen(rtrim($n, '0'));
 
7313
        } else {
 
7314
            $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
 
7315
            if ($dp !== 0) $sigfigs--;
 
7316
        }
 
7317
        return $sigfigs;
 
7318
    }
 
7319
 
 
7320
    /**
 
7321
     * Adds two numbers, using arbitrary precision when available.
 
7322
     */
 
7323
    private function add($s1, $s2, $scale) {
 
7324
        if ($this->bcmath) return bcadd($s1, $s2, $scale);
 
7325
        else return $this->scale($s1 + $s2, $scale);
 
7326
    }
 
7327
 
 
7328
    /**
 
7329
     * Multiples two numbers, using arbitrary precision when available.
 
7330
     */
 
7331
    private function mul($s1, $s2, $scale) {
 
7332
        if ($this->bcmath) return bcmul($s1, $s2, $scale);
 
7333
        else return $this->scale($s1 * $s2, $scale);
 
7334
    }
 
7335
 
 
7336
    /**
 
7337
     * Divides two numbers, using arbitrary precision when available.
 
7338
     */
 
7339
    private function div($s1, $s2, $scale) {
 
7340
        if ($this->bcmath) return bcdiv($s1, $s2, $scale);
 
7341
        else return $this->scale($s1 / $s2, $scale);
 
7342
    }
 
7343
 
 
7344
    /**
 
7345
     * Rounds a number according to the number of sigfigs it should have,
 
7346
     * using arbitrary precision when available.
 
7347
     */
 
7348
    private function round($n, $sigfigs) {
 
7349
        $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1
 
7350
        $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
 
7351
        $neg = $n < 0 ? '-' : ''; // Negative sign
 
7352
        if ($this->bcmath) {
 
7353
            if ($rp >= 0) {
 
7354
                $n = bcadd($n, $neg . '0.' .  str_repeat('0', $rp) . '5', $rp + 1);
 
7355
                $n = bcdiv($n, '1', $rp);
 
7356
            } else {
 
7357
                // This algorithm partially depends on the standardized
 
7358
                // form of numbers that comes out of bcmath.
 
7359
                $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
 
7360
                $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
 
7361
            }
 
7362
            return $n;
 
7363
        } else {
 
7364
            return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
 
7365
        }
 
7366
    }
 
7367
 
 
7368
    /**
 
7369
     * Scales a float to $scale digits right of decimal point, like BCMath.
 
7370
     */
 
7371
    private function scale($r, $scale) {
 
7372
        if ($scale < 0) {
 
7373
            // The f sprintf type doesn't support negative numbers, so we
 
7374
            // need to cludge things manually. First get the string.
 
7375
            $r = sprintf('%.0f', (float) $r);
 
7376
            // Due to floating point precision loss, $r will more than likely
 
7377
            // look something like 4652999999999.9234. We grab one more digit
 
7378
            // than we need to precise from $r and then use that to round
 
7379
            // appropriately.
 
7380
            $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1);
 
7381
            // Now we return it, truncating the zero that was rounded off.
 
7382
            return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
 
7383
        }
 
7384
        return sprintf('%.' . $scale . 'f', (float) $r);
 
7385
    }
 
7386
 
 
7387
}
 
7388
 
 
7389
 
 
7390
 
 
7391
 
 
7392
 
 
7393
/**
 
7394
 * Parses string representations into their corresponding native PHP
 
7395
 * variable type. The base implementation does a simple type-check.
 
7396
 */
 
7397
class HTMLPurifier_VarParser
 
7398
{
 
7399
 
 
7400
    const STRING    = 1;
 
7401
    const ISTRING   = 2;
 
7402
    const TEXT      = 3;
 
7403
    const ITEXT     = 4;
 
7404
    const INT       = 5;
 
7405
    const FLOAT     = 6;
 
7406
    const BOOL      = 7;
 
7407
    const LOOKUP    = 8;
 
7408
    const ALIST     = 9;
 
7409
    const HASH      = 10;
 
7410
    const MIXED     = 11;
 
7411
 
 
7412
    /**
 
7413
     * Lookup table of allowed types. Mainly for backwards compatibility, but
 
7414
     * also convenient for transforming string type names to the integer constants.
 
7415
     */
 
7416
    static public $types = array(
 
7417
        'string'    => self::STRING,
 
7418
        'istring'   => self::ISTRING,
 
7419
        'text'      => self::TEXT,
 
7420
        'itext'     => self::ITEXT,
 
7421
        'int'       => self::INT,
 
7422
        'float'     => self::FLOAT,
 
7423
        'bool'      => self::BOOL,
 
7424
        'lookup'    => self::LOOKUP,
 
7425
        'list'      => self::ALIST,
 
7426
        'hash'      => self::HASH,
 
7427
        'mixed'     => self::MIXED
 
7428
    );
 
7429
 
 
7430
    /**
 
7431
     * Lookup table of types that are string, and can have aliases or
 
7432
     * allowed value lists.
 
7433
     */
 
7434
    static public $stringTypes = array(
 
7435
        self::STRING    => true,
 
7436
        self::ISTRING   => true,
 
7437
        self::TEXT      => true,
 
7438
        self::ITEXT     => true,
 
7439
    );
 
7440
 
 
7441
    /**
 
7442
     * Validate a variable according to type. Throws
 
7443
     * HTMLPurifier_VarParserException if invalid.
 
7444
     * It may return NULL as a valid type if $allow_null is true.
 
7445
     *
 
7446
     * @param $var Variable to validate
 
7447
     * @param $type Type of variable, see HTMLPurifier_VarParser->types
 
7448
     * @param $allow_null Whether or not to permit null as a value
 
7449
     * @return Validated and type-coerced variable
 
7450
     */
 
7451
    final public function parse($var, $type, $allow_null = false) {
 
7452
        if (is_string($type)) {
 
7453
            if (!isset(HTMLPurifier_VarParser::$types[$type])) {
 
7454
                throw new HTMLPurifier_VarParserException("Invalid type '$type'");
 
7455
            } else {
 
7456
                $type = HTMLPurifier_VarParser::$types[$type];
 
7457
            }
 
7458
        }
 
7459
        $var = $this->parseImplementation($var, $type, $allow_null);
 
7460
        if ($allow_null && $var === null) return null;
 
7461
        // These are basic checks, to make sure nothing horribly wrong
 
7462
        // happened in our implementations.
 
7463
        switch ($type) {
 
7464
            case (self::STRING):
 
7465
            case (self::ISTRING):
 
7466
            case (self::TEXT):
 
7467
            case (self::ITEXT):
 
7468
                if (!is_string($var)) break;
 
7469
                if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var);
 
7470
                return $var;
 
7471
            case (self::INT):
 
7472
                if (!is_int($var)) break;
 
7473
                return $var;
 
7474
            case (self::FLOAT):
 
7475
                if (!is_float($var)) break;
 
7476
                return $var;
 
7477
            case (self::BOOL):
 
7478
                if (!is_bool($var)) break;
 
7479
                return $var;
 
7480
            case (self::LOOKUP):
 
7481
            case (self::ALIST):
 
7482
            case (self::HASH):
 
7483
                if (!is_array($var)) break;
 
7484
                if ($type === self::LOOKUP) {
 
7485
                    foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true');
 
7486
                } elseif ($type === self::ALIST) {
 
7487
                    $keys = array_keys($var);
 
7488
                    if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform');
 
7489
                }
 
7490
                return $var;
 
7491
            case (self::MIXED):
 
7492
                return $var;
 
7493
            default:
 
7494
                $this->errorInconsistent(get_class($this), $type);
 
7495
        }
 
7496
        $this->errorGeneric($var, $type);
 
7497
    }
 
7498
 
 
7499
    /**
 
7500
     * Actually implements the parsing. Base implementation is to not
 
7501
     * do anything to $var. Subclasses should overload this!
 
7502
     */
 
7503
    protected function parseImplementation($var, $type, $allow_null) {
 
7504
        return $var;
 
7505
    }
 
7506
 
 
7507
    /**
 
7508
     * Throws an exception.
 
7509
     */
 
7510
    protected function error($msg) {
 
7511
        throw new HTMLPurifier_VarParserException($msg);
 
7512
    }
 
7513
 
 
7514
    /**
 
7515
     * Throws an inconsistency exception.
 
7516
     * @note This should not ever be called. It would be called if we
 
7517
     *       extend the allowed values of HTMLPurifier_VarParser without
 
7518
     *       updating subclasses.
 
7519
     */
 
7520
    protected function errorInconsistent($class, $type) {
 
7521
        throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented");
 
7522
    }
 
7523
 
 
7524
    /**
 
7525
     * Generic error for if a type didn't work.
 
7526
     */
 
7527
    protected function errorGeneric($var, $type) {
 
7528
        $vtype = gettype($var);
 
7529
        $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype");
 
7530
    }
 
7531
 
 
7532
    static public function getTypeName($type) {
 
7533
        static $lookup;
 
7534
        if (!$lookup) {
 
7535
            // Lazy load the alternative lookup table
 
7536
            $lookup = array_flip(HTMLPurifier_VarParser::$types);
 
7537
        }
 
7538
        if (!isset($lookup[$type])) return 'unknown';
 
7539
        return $lookup[$type];
 
7540
    }
 
7541
 
 
7542
}
 
7543
 
 
7544
 
 
7545
 
 
7546
 
 
7547
 
 
7548
/**
 
7549
 * Exception type for HTMLPurifier_VarParser
 
7550
 */
 
7551
class HTMLPurifier_VarParserException extends HTMLPurifier_Exception
 
7552
{
 
7553
 
 
7554
}
 
7555
 
 
7556
 
 
7557
 
 
7558
 
 
7559
 
 
7560
/**
 
7561
 * Validates the HTML attribute style, otherwise known as CSS.
 
7562
 * @note We don't implement the whole CSS specification, so it might be
 
7563
 *       difficult to reuse this component in the context of validating
 
7564
 *       actual stylesheet declarations.
 
7565
 * @note If we were really serious about validating the CSS, we would
 
7566
 *       tokenize the styles and then parse the tokens. Obviously, we
 
7567
 *       are not doing that. Doing that could seriously harm performance,
 
7568
 *       but would make these components a lot more viable for a CSS
 
7569
 *       filtering solution.
 
7570
 */
 
7571
class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
 
7572
{
 
7573
 
 
7574
    public function validate($css, $config, $context) {
 
7575
 
 
7576
        $css = $this->parseCDATA($css);
 
7577
 
 
7578
        $definition = $config->getCSSDefinition();
 
7579
 
 
7580
        // we're going to break the spec and explode by semicolons.
 
7581
        // This is because semicolon rarely appears in escaped form
 
7582
        // Doing this is generally flaky but fast
 
7583
        // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI
 
7584
        // for details
 
7585
 
 
7586
        $declarations = explode(';', $css);
 
7587
        $propvalues = array();
 
7588
 
 
7589
        /**
 
7590
         * Name of the current CSS property being validated.
 
7591
         */
 
7592
        $property = false;
 
7593
        $context->register('CurrentCSSProperty', $property);
 
7594
 
 
7595
        foreach ($declarations as $declaration) {
 
7596
            if (!$declaration) continue;
 
7597
            if (!strpos($declaration, ':')) continue;
 
7598
            list($property, $value) = explode(':', $declaration, 2);
 
7599
            $property = trim($property);
 
7600
            $value    = trim($value);
 
7601
            $ok = false;
 
7602
            do {
 
7603
                if (isset($definition->info[$property])) {
 
7604
                    $ok = true;
 
7605
                    break;
 
7606
                }
 
7607
                if (ctype_lower($property)) break;
 
7608
                $property = strtolower($property);
 
7609
                if (isset($definition->info[$property])) {
 
7610
                    $ok = true;
 
7611
                    break;
 
7612
                }
 
7613
            } while(0);
 
7614
            if (!$ok) continue;
 
7615
            // inefficient call, since the validator will do this again
 
7616
            if (strtolower(trim($value)) !== 'inherit') {
 
7617
                // inherit works for everything (but only on the base property)
 
7618
                $result = $definition->info[$property]->validate(
 
7619
                    $value, $config, $context );
 
7620
            } else {
 
7621
                $result = 'inherit';
 
7622
            }
 
7623
            if ($result === false) continue;
 
7624
            $propvalues[$property] = $result;
 
7625
        }
 
7626
 
 
7627
        $context->destroy('CurrentCSSProperty');
 
7628
 
 
7629
        // procedure does not write the new CSS simultaneously, so it's
 
7630
        // slightly inefficient, but it's the only way of getting rid of
 
7631
        // duplicates. Perhaps config to optimize it, but not now.
 
7632
 
 
7633
        $new_declarations = '';
 
7634
        foreach ($propvalues as $prop => $value) {
 
7635
            $new_declarations .= "$prop:$value;";
 
7636
        }
 
7637
 
 
7638
        return $new_declarations ? $new_declarations : false;
 
7639
 
 
7640
    }
 
7641
 
 
7642
}
 
7643
 
 
7644
 
 
7645
 
 
7646
 
 
7647
 
 
7648
// Enum = Enumerated
 
7649
/**
 
7650
 * Validates a keyword against a list of valid values.
 
7651
 * @warning The case-insensitive compare of this function uses PHP's
 
7652
 *          built-in strtolower and ctype_lower functions, which may
 
7653
 *          cause problems with international comparisons
 
7654
 */
 
7655
class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef
 
7656
{
 
7657
 
 
7658
    /**
 
7659
     * Lookup table of valid values.
 
7660
     * @todo Make protected
 
7661
     */
 
7662
    public $valid_values   = array();
 
7663
 
 
7664
    /**
 
7665
     * Bool indicating whether or not enumeration is case sensitive.
 
7666
     * @note In general this is always case insensitive.
 
7667
     */
 
7668
    protected $case_sensitive = false; // values according to W3C spec
 
7669
 
 
7670
    /**
 
7671
     * @param $valid_values List of valid values
 
7672
     * @param $case_sensitive Bool indicating whether or not case sensitive
 
7673
     */
 
7674
    public function __construct(
 
7675
        $valid_values = array(), $case_sensitive = false
 
7676
    ) {
 
7677
        $this->valid_values = array_flip($valid_values);
 
7678
        $this->case_sensitive = $case_sensitive;
 
7679
    }
 
7680
 
 
7681
    public function validate($string, $config, $context) {
 
7682
        $string = trim($string);
 
7683
        if (!$this->case_sensitive) {
 
7684
            // we may want to do full case-insensitive libraries
 
7685
            $string = ctype_lower($string) ? $string : strtolower($string);
 
7686
        }
 
7687
        $result = isset($this->valid_values[$string]);
 
7688
 
 
7689
        return $result ? $string : false;
 
7690
    }
 
7691
 
 
7692
    /**
 
7693
     * @param $string In form of comma-delimited list of case-insensitive
 
7694
     *      valid values. Example: "foo,bar,baz". Prepend "s:" to make
 
7695
     *      case sensitive
 
7696
     */
 
7697
    public function make($string) {
 
7698
        if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') {
 
7699
            $string = substr($string, 2);
 
7700
            $sensitive = true;
 
7701
        } else {
 
7702
            $sensitive = false;
 
7703
        }
 
7704
        $values = explode(',', $string);
 
7705
        return new HTMLPurifier_AttrDef_Enum($values, $sensitive);
 
7706
    }
 
7707
 
 
7708
}
 
7709
 
 
7710
 
 
7711
 
 
7712
 
 
7713
 
 
7714
/**
 
7715
 * Validates an integer.
 
7716
 * @note While this class was modeled off the CSS definition, no currently
 
7717
 *       allowed CSS uses this type.  The properties that do are: widows,
 
7718
 *       orphans, z-index, counter-increment, counter-reset.  Some of the
 
7719
 *       HTML attributes, however, find use for a non-negative version of this.
 
7720
 */
 
7721
class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef
 
7722
{
 
7723
 
 
7724
    /**
 
7725
     * Bool indicating whether or not negative values are allowed
 
7726
     */
 
7727
    protected $negative = true;
 
7728
 
 
7729
    /**
 
7730
     * Bool indicating whether or not zero is allowed
 
7731
     */
 
7732
    protected $zero = true;
 
7733
 
 
7734
    /**
 
7735
     * Bool indicating whether or not positive values are allowed
 
7736
     */
 
7737
    protected $positive = true;
 
7738
 
 
7739
    /**
 
7740
     * @param $negative Bool indicating whether or not negative values are allowed
 
7741
     * @param $zero Bool indicating whether or not zero is allowed
 
7742
     * @param $positive Bool indicating whether or not positive values are allowed
 
7743
     */
 
7744
    public function __construct(
 
7745
        $negative = true, $zero = true, $positive = true
 
7746
    ) {
 
7747
        $this->negative = $negative;
 
7748
        $this->zero     = $zero;
 
7749
        $this->positive = $positive;
 
7750
    }
 
7751
 
 
7752
    public function validate($integer, $config, $context) {
 
7753
 
 
7754
        $integer = $this->parseCDATA($integer);
 
7755
        if ($integer === '') return false;
 
7756
 
 
7757
        // we could possibly simply typecast it to integer, but there are
 
7758
        // certain fringe cases that must not return an integer.
 
7759
 
 
7760
        // clip leading sign
 
7761
        if ( $this->negative && $integer[0] === '-' ) {
 
7762
            $digits = substr($integer, 1);
 
7763
            if ($digits === '0') $integer = '0'; // rm minus sign for zero
 
7764
        } elseif( $this->positive && $integer[0] === '+' ) {
 
7765
            $digits = $integer = substr($integer, 1); // rm unnecessary plus
 
7766
        } else {
 
7767
            $digits = $integer;
 
7768
        }
 
7769
 
 
7770
        // test if it's numeric
 
7771
        if (!ctype_digit($digits)) return false;
 
7772
 
 
7773
        // perform scope tests
 
7774
        if (!$this->zero     && $integer == 0) return false;
 
7775
        if (!$this->positive && $integer > 0) return false;
 
7776
        if (!$this->negative && $integer < 0) return false;
 
7777
 
 
7778
        return $integer;
 
7779
 
 
7780
    }
 
7781
 
 
7782
}
 
7783
 
 
7784
 
 
7785
 
 
7786
 
 
7787
 
 
7788
/**
 
7789
 * Validates the HTML attribute lang, effectively a language code.
 
7790
 * @note Built according to RFC 3066, which obsoleted RFC 1766
 
7791
 */
 
7792
class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef
 
7793
{
 
7794
 
 
7795
    public function validate($string, $config, $context) {
 
7796
 
 
7797
        $string = trim($string);
 
7798
        if (!$string) return false;
 
7799
 
 
7800
        $subtags = explode('-', $string);
 
7801
        $num_subtags = count($subtags);
 
7802
 
 
7803
        if ($num_subtags == 0) return false; // sanity check
 
7804
 
 
7805
        // process primary subtag : $subtags[0]
 
7806
        $length = strlen($subtags[0]);
 
7807
        switch ($length) {
 
7808
            case 0:
 
7809
                return false;
 
7810
            case 1:
 
7811
                if (! ($subtags[0] == 'x' || $subtags[0] == 'i') ) {
 
7812
                    return false;
 
7813
                }
 
7814
                break;
 
7815
            case 2:
 
7816
            case 3:
 
7817
                if (! ctype_alpha($subtags[0]) ) {
 
7818
                    return false;
 
7819
                } elseif (! ctype_lower($subtags[0]) ) {
 
7820
                    $subtags[0] = strtolower($subtags[0]);
 
7821
                }
 
7822
                break;
 
7823
            default:
 
7824
                return false;
 
7825
        }
 
7826
 
 
7827
        $new_string = $subtags[0];
 
7828
        if ($num_subtags == 1) return $new_string;
 
7829
 
 
7830
        // process second subtag : $subtags[1]
 
7831
        $length = strlen($subtags[1]);
 
7832
        if ($length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) {
 
7833
            return $new_string;
 
7834
        }
 
7835
        if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]);
 
7836
 
 
7837
        $new_string .= '-' . $subtags[1];
 
7838
        if ($num_subtags == 2) return $new_string;
 
7839
 
 
7840
        // process all other subtags, index 2 and up
 
7841
        for ($i = 2; $i < $num_subtags; $i++) {
 
7842
            $length = strlen($subtags[$i]);
 
7843
            if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
 
7844
                return $new_string;
 
7845
            }
 
7846
            if (!ctype_lower($subtags[$i])) {
 
7847
                $subtags[$i] = strtolower($subtags[$i]);
 
7848
            }
 
7849
            $new_string .= '-' . $subtags[$i];
 
7850
        }
 
7851
 
 
7852
        return $new_string;
 
7853
 
 
7854
    }
 
7855
 
 
7856
}
 
7857
 
 
7858
 
 
7859
 
 
7860
 
 
7861
 
 
7862
/**
 
7863
 * Decorator that, depending on a token, switches between two definitions.
 
7864
 */
 
7865
class HTMLPurifier_AttrDef_Switch
 
7866
{
 
7867
 
 
7868
    protected $tag;
 
7869
    protected $withTag, $withoutTag;
 
7870
 
 
7871
    /**
 
7872
     * @param string $tag Tag name to switch upon
 
7873
     * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag
 
7874
     * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token
 
7875
     */
 
7876
    public function __construct($tag, $with_tag, $without_tag) {
 
7877
        $this->tag = $tag;
 
7878
        $this->withTag = $with_tag;
 
7879
        $this->withoutTag = $without_tag;
 
7880
    }
 
7881
 
 
7882
    public function validate($string, $config, $context) {
 
7883
        $token = $context->get('CurrentToken', true);
 
7884
        if (!$token || $token->name !== $this->tag) {
 
7885
            return $this->withoutTag->validate($string, $config, $context);
 
7886
        } else {
 
7887
            return $this->withTag->validate($string, $config, $context);
 
7888
        }
 
7889
    }
 
7890
 
 
7891
}
 
7892
 
 
7893
 
 
7894
 
 
7895
 
 
7896
 
 
7897
/**
 
7898
 * Validates arbitrary text according to the HTML spec.
 
7899
 */
 
7900
class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef
 
7901
{
 
7902
 
 
7903
    public function validate($string, $config, $context) {
 
7904
        return $this->parseCDATA($string);
 
7905
    }
 
7906
 
 
7907
}
 
7908
 
 
7909
 
 
7910
 
 
7911
 
 
7912
 
 
7913
/**
 
7914
 * Validates a URI as defined by RFC 3986.
 
7915
 * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme
 
7916
 */
 
7917
class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
 
7918
{
 
7919
 
 
7920
    protected $parser;
 
7921
    protected $embedsResource;
 
7922
 
 
7923
    /**
 
7924
     * @param $embeds_resource_resource Does the URI here result in an extra HTTP request?
 
7925
     */
 
7926
    public function __construct($embeds_resource = false) {
 
7927
        $this->parser = new HTMLPurifier_URIParser();
 
7928
        $this->embedsResource = (bool) $embeds_resource;
 
7929
    }
 
7930
 
 
7931
    public function make($string) {
 
7932
        $embeds = (bool) $string;
 
7933
        return new HTMLPurifier_AttrDef_URI($embeds);
 
7934
    }
 
7935
 
 
7936
    public function validate($uri, $config, $context) {
 
7937
 
 
7938
        if ($config->get('URI.Disable')) return false;
 
7939
 
 
7940
        $uri = $this->parseCDATA($uri);
 
7941
 
 
7942
        // parse the URI
 
7943
        $uri = $this->parser->parse($uri);
 
7944
        if ($uri === false) return false;
 
7945
 
 
7946
        // add embedded flag to context for validators
 
7947
        $context->register('EmbeddedURI', $this->embedsResource);
 
7948
 
 
7949
        $ok = false;
 
7950
        do {
 
7951
 
 
7952
            // generic validation
 
7953
            $result = $uri->validate($config, $context);
 
7954
            if (!$result) break;
 
7955
 
 
7956
            // chained filtering
 
7957
            $uri_def = $config->getDefinition('URI');
 
7958
            $result = $uri_def->filter($uri, $config, $context);
 
7959
            if (!$result) break;
 
7960
 
 
7961
            // scheme-specific validation
 
7962
            $scheme_obj = $uri->getSchemeObj($config, $context);
 
7963
            if (!$scheme_obj) break;
 
7964
            if ($this->embedsResource && !$scheme_obj->browsable) break;
 
7965
            $result = $scheme_obj->validate($uri, $config, $context);
 
7966
            if (!$result) break;
 
7967
 
 
7968
            // Post chained filtering
 
7969
            $result = $uri_def->postFilter($uri, $config, $context);
 
7970
            if (!$result) break;
 
7971
 
 
7972
            // survived gauntlet
 
7973
            $ok = true;
 
7974
 
 
7975
        } while (false);
 
7976
 
 
7977
        $context->destroy('EmbeddedURI');
 
7978
        if (!$ok) return false;
 
7979
 
 
7980
        // back to string
 
7981
        return $uri->toString();
 
7982
 
 
7983
    }
 
7984
 
 
7985
}
 
7986
 
 
7987
 
 
7988
 
 
7989
 
 
7990
 
 
7991
/**
 
7992
 * Validates a number as defined by the CSS spec.
 
7993
 */
 
7994
class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
 
7995
{
 
7996
 
 
7997
    /**
 
7998
     * Bool indicating whether or not only positive values allowed.
 
7999
     */
 
8000
    protected $non_negative = false;
 
8001
 
 
8002
    /**
 
8003
     * @param $non_negative Bool indicating whether negatives are forbidden
 
8004
     */
 
8005
    public function __construct($non_negative = false) {
 
8006
        $this->non_negative = $non_negative;
 
8007
    }
 
8008
 
 
8009
    /**
 
8010
     * @warning Some contexts do not pass $config, $context. These
 
8011
     *          variables should not be used without checking HTMLPurifier_Length
 
8012
     */
 
8013
    public function validate($number, $config, $context) {
 
8014
 
 
8015
        $number = $this->parseCDATA($number);
 
8016
 
 
8017
        if ($number === '') return false;
 
8018
        if ($number === '0') return '0';
 
8019
 
 
8020
        $sign = '';
 
8021
        switch ($number[0]) {
 
8022
            case '-':
 
8023
                if ($this->non_negative) return false;
 
8024
                $sign = '-';
 
8025
            case '+':
 
8026
                $number = substr($number, 1);
 
8027
        }
 
8028
 
 
8029
        if (ctype_digit($number)) {
 
8030
            $number = ltrim($number, '0');
 
8031
            return $number ? $sign . $number : '0';
 
8032
        }
 
8033
 
 
8034
        // Period is the only non-numeric character allowed
 
8035
        if (strpos($number, '.') === false) return false;
 
8036
 
 
8037
        list($left, $right) = explode('.', $number, 2);
 
8038
 
 
8039
        if ($left === '' && $right === '') return false;
 
8040
        if ($left !== '' && !ctype_digit($left)) return false;
 
8041
 
 
8042
        $left  = ltrim($left,  '0');
 
8043
        $right = rtrim($right, '0');
 
8044
 
 
8045
        if ($right === '') {
 
8046
            return $left ? $sign . $left : '0';
 
8047
        } elseif (!ctype_digit($right)) {
 
8048
            return false;
 
8049
        }
 
8050
 
 
8051
        return $sign . $left . '.' . $right;
 
8052
 
 
8053
    }
 
8054
 
 
8055
}
 
8056
 
 
8057
 
 
8058
 
 
8059
 
 
8060
 
 
8061
class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number
 
8062
{
 
8063
 
 
8064
    public function __construct() {
 
8065
        parent::__construct(false); // opacity is non-negative, but we will clamp it
 
8066
    }
 
8067
 
 
8068
    public function validate($number, $config, $context) {
 
8069
        $result = parent::validate($number, $config, $context);
 
8070
        if ($result === false) return $result;
 
8071
        $float = (float) $result;
 
8072
        if ($float < 0.0) $result = '0';
 
8073
        if ($float > 1.0) $result = '1';
 
8074
        return $result;
 
8075
    }
 
8076
 
 
8077
}
 
8078
 
 
8079
 
 
8080
 
 
8081
 
 
8082
 
 
8083
/**
 
8084
 * Validates shorthand CSS property background.
 
8085
 * @warning Does not support url tokens that have internal spaces.
 
8086
 */
 
8087
class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
 
8088
{
 
8089
 
 
8090
    /**
 
8091
     * Local copy of component validators.
 
8092
     * @note See HTMLPurifier_AttrDef_Font::$info for a similar impl.
 
8093
     */
 
8094
    protected $info;
 
8095
 
 
8096
    public function __construct($config) {
 
8097
        $def = $config->getCSSDefinition();
 
8098
        $this->info['background-color'] = $def->info['background-color'];
 
8099
        $this->info['background-image'] = $def->info['background-image'];
 
8100
        $this->info['background-repeat'] = $def->info['background-repeat'];
 
8101
        $this->info['background-attachment'] = $def->info['background-attachment'];
 
8102
        $this->info['background-position'] = $def->info['background-position'];
 
8103
    }
 
8104
 
 
8105
    public function validate($string, $config, $context) {
 
8106
 
 
8107
        // regular pre-processing
 
8108
        $string = $this->parseCDATA($string);
 
8109
        if ($string === '') return false;
 
8110
 
 
8111
        // munge rgb() decl if necessary
 
8112
        $string = $this->mungeRgb($string);
 
8113
 
 
8114
        // assumes URI doesn't have spaces in it
 
8115
        $bits = explode(' ', strtolower($string)); // bits to process
 
8116
 
 
8117
        $caught = array();
 
8118
        $caught['color']    = false;
 
8119
        $caught['image']    = false;
 
8120
        $caught['repeat']   = false;
 
8121
        $caught['attachment'] = false;
 
8122
        $caught['position'] = false;
 
8123
 
 
8124
        $i = 0; // number of catches
 
8125
        $none = false;
 
8126
 
 
8127
        foreach ($bits as $bit) {
 
8128
            if ($bit === '') continue;
 
8129
            foreach ($caught as $key => $status) {
 
8130
                if ($key != 'position') {
 
8131
                    if ($status !== false) continue;
 
8132
                    $r = $this->info['background-' . $key]->validate($bit, $config, $context);
 
8133
                } else {
 
8134
                    $r = $bit;
 
8135
                }
 
8136
                if ($r === false) continue;
 
8137
                if ($key == 'position') {
 
8138
                    if ($caught[$key] === false) $caught[$key] = '';
 
8139
                    $caught[$key] .= $r . ' ';
 
8140
                } else {
 
8141
                    $caught[$key] = $r;
 
8142
                }
 
8143
                $i++;
 
8144
                break;
 
8145
            }
 
8146
        }
 
8147
 
 
8148
        if (!$i) return false;
 
8149
        if ($caught['position'] !== false) {
 
8150
            $caught['position'] = $this->info['background-position']->
 
8151
                validate($caught['position'], $config, $context);
 
8152
        }
 
8153
 
 
8154
        $ret = array();
 
8155
        foreach ($caught as $value) {
 
8156
            if ($value === false) continue;
 
8157
            $ret[] = $value;
 
8158
        }
 
8159
 
 
8160
        if (empty($ret)) return false;
 
8161
        return implode(' ', $ret);
 
8162
 
 
8163
    }
 
8164
 
 
8165
}
 
8166
 
 
8167
 
 
8168
 
 
8169
 
 
8170
 
 
8171
/* W3C says:
 
8172
    [ // adjective and number must be in correct order, even if
 
8173
      // you could switch them without introducing ambiguity.
 
8174
      // some browsers support that syntax
 
8175
        [
 
8176
            <percentage> | <length> | left | center | right
 
8177
        ]
 
8178
        [
 
8179
            <percentage> | <length> | top | center | bottom
 
8180
        ]?
 
8181
    ] |
 
8182
    [ // this signifies that the vertical and horizontal adjectives
 
8183
      // can be arbitrarily ordered, however, there can only be two,
 
8184
      // one of each, or none at all
 
8185
        [
 
8186
            left | center | right
 
8187
        ] ||
 
8188
        [
 
8189
            top | center | bottom
 
8190
        ]
 
8191
    ]
 
8192
    top, left = 0%
 
8193
    center, (none) = 50%
 
8194
    bottom, right = 100%
 
8195
*/
 
8196
 
 
8197
/* QuirksMode says:
 
8198
    keyword + length/percentage must be ordered correctly, as per W3C
 
8199
 
 
8200
    Internet Explorer and Opera, however, support arbitrary ordering. We
 
8201
    should fix it up.
 
8202
 
 
8203
    Minor issue though, not strictly necessary.
 
8204
*/
 
8205
 
 
8206
// control freaks may appreciate the ability to convert these to
 
8207
// percentages or something, but it's not necessary
 
8208
 
 
8209
/**
 
8210
 * Validates the value of background-position.
 
8211
 */
 
8212
class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
 
8213
{
 
8214
 
 
8215
    protected $length;
 
8216
    protected $percentage;
 
8217
 
 
8218
    public function __construct() {
 
8219
        $this->length     = new HTMLPurifier_AttrDef_CSS_Length();
 
8220
        $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
 
8221
    }
 
8222
 
 
8223
    public function validate($string, $config, $context) {
 
8224
        $string = $this->parseCDATA($string);
 
8225
        $bits = explode(' ', $string);
 
8226
 
 
8227
        $keywords = array();
 
8228
        $keywords['h'] = false; // left, right
 
8229
        $keywords['v'] = false; // top, bottom
 
8230
        $keywords['c'] = false; // center
 
8231
        $measures = array();
 
8232
 
 
8233
        $i = 0;
 
8234
 
 
8235
        $lookup = array(
 
8236
            'top' => 'v',
 
8237
            'bottom' => 'v',
 
8238
            'left' => 'h',
 
8239
            'right' => 'h',
 
8240
            'center' => 'c'
 
8241
        );
 
8242
 
 
8243
        foreach ($bits as $bit) {
 
8244
            if ($bit === '') continue;
 
8245
 
 
8246
            // test for keyword
 
8247
            $lbit = ctype_lower($bit) ? $bit : strtolower($bit);
 
8248
            if (isset($lookup[$lbit])) {
 
8249
                $status = $lookup[$lbit];
 
8250
                $keywords[$status] = $lbit;
 
8251
                $i++;
 
8252
            }
 
8253
 
 
8254
            // test for length
 
8255
            $r = $this->length->validate($bit, $config, $context);
 
8256
            if ($r !== false) {
 
8257
                $measures[] = $r;
 
8258
                $i++;
 
8259
            }
 
8260
 
 
8261
            // test for percentage
 
8262
            $r = $this->percentage->validate($bit, $config, $context);
 
8263
            if ($r !== false) {
 
8264
                $measures[] = $r;
 
8265
                $i++;
 
8266
            }
 
8267
 
 
8268
        }
 
8269
 
 
8270
        if (!$i) return false; // no valid values were caught
 
8271
 
 
8272
 
 
8273
        $ret = array();
 
8274
 
 
8275
        // first keyword
 
8276
        if     ($keywords['h'])     $ret[] = $keywords['h'];
 
8277
        elseif (count($measures))   $ret[] = array_shift($measures);
 
8278
        elseif ($keywords['c']) {
 
8279
            $ret[] = $keywords['c'];
 
8280
            $keywords['c'] = false; // prevent re-use: center = center center
 
8281
        }
 
8282
 
 
8283
        if     ($keywords['v'])     $ret[] = $keywords['v'];
 
8284
        elseif (count($measures))   $ret[] = array_shift($measures);
 
8285
        elseif ($keywords['c'])     $ret[] = $keywords['c'];
 
8286
 
 
8287
        if (empty($ret)) return false;
 
8288
        return implode(' ', $ret);
 
8289
 
 
8290
    }
 
8291
 
 
8292
}
 
8293
 
 
8294
 
 
8295
 
 
8296
 
 
8297
 
 
8298
/**
 
8299
 * Validates the border property as defined by CSS.
 
8300
 */
 
8301
class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef
 
8302
{
 
8303
 
 
8304
    /**
 
8305
     * Local copy of properties this property is shorthand for.
 
8306
     */
 
8307
    protected $info = array();
 
8308
 
 
8309
    public function __construct($config) {
 
8310
        $def = $config->getCSSDefinition();
 
8311
        $this->info['border-width'] = $def->info['border-width'];
 
8312
        $this->info['border-style'] = $def->info['border-style'];
 
8313
        $this->info['border-top-color'] = $def->info['border-top-color'];
 
8314
    }
 
8315
 
 
8316
    public function validate($string, $config, $context) {
 
8317
        $string = $this->parseCDATA($string);
 
8318
        $string = $this->mungeRgb($string);
 
8319
        $bits = explode(' ', $string);
 
8320
        $done = array(); // segments we've finished
 
8321
        $ret = ''; // return value
 
8322
        foreach ($bits as $bit) {
 
8323
            foreach ($this->info as $propname => $validator) {
 
8324
                if (isset($done[$propname])) continue;
 
8325
                $r = $validator->validate($bit, $config, $context);
 
8326
                if ($r !== false) {
 
8327
                    $ret .= $r . ' ';
 
8328
                    $done[$propname] = true;
 
8329
                    break;
 
8330
                }
 
8331
            }
 
8332
        }
 
8333
        return rtrim($ret);
 
8334
    }
 
8335
 
 
8336
}
 
8337
 
 
8338
 
 
8339
 
 
8340
 
 
8341
 
 
8342
/**
 
8343
 * Validates Color as defined by CSS.
 
8344
 */
 
8345
class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
 
8346
{
 
8347
 
 
8348
    public function validate($color, $config, $context) {
 
8349
 
 
8350
        static $colors = null;
 
8351
        if ($colors === null) $colors = $config->get('Core.ColorKeywords');
 
8352
 
 
8353
        $color = trim($color);
 
8354
        if ($color === '') return false;
 
8355
 
 
8356
        $lower = strtolower($color);
 
8357
        if (isset($colors[$lower])) return $colors[$lower];
 
8358
 
 
8359
        if (strpos($color, 'rgb(') !== false) {
 
8360
            // rgb literal handling
 
8361
            $length = strlen($color);
 
8362
            if (strpos($color, ')') !== $length - 1) return false;
 
8363
            $triad = substr($color, 4, $length - 4 - 1);
 
8364
            $parts = explode(',', $triad);
 
8365
            if (count($parts) !== 3) return false;
 
8366
            $type = false; // to ensure that they're all the same type
 
8367
            $new_parts = array();
 
8368
            foreach ($parts as $part) {
 
8369
                $part = trim($part);
 
8370
                if ($part === '') return false;
 
8371
                $length = strlen($part);
 
8372
                if ($part[$length - 1] === '%') {
 
8373
                    // handle percents
 
8374
                    if (!$type) {
 
8375
                        $type = 'percentage';
 
8376
                    } elseif ($type !== 'percentage') {
 
8377
                        return false;
 
8378
                    }
 
8379
                    $num = (float) substr($part, 0, $length - 1);
 
8380
                    if ($num < 0) $num = 0;
 
8381
                    if ($num > 100) $num = 100;
 
8382
                    $new_parts[] = "$num%";
 
8383
                } else {
 
8384
                    // handle integers
 
8385
                    if (!$type) {
 
8386
                        $type = 'integer';
 
8387
                    } elseif ($type !== 'integer') {
 
8388
                        return false;
 
8389
                    }
 
8390
                    $num = (int) $part;
 
8391
                    if ($num < 0) $num = 0;
 
8392
                    if ($num > 255) $num = 255;
 
8393
                    $new_parts[] = (string) $num;
 
8394
                }
 
8395
            }
 
8396
            $new_triad = implode(',', $new_parts);
 
8397
            $color = "rgb($new_triad)";
 
8398
        } else {
 
8399
            // hexadecimal handling
 
8400
            if ($color[0] === '#') {
 
8401
                $hex = substr($color, 1);
 
8402
            } else {
 
8403
                $hex = $color;
 
8404
                $color = '#' . $color;
 
8405
            }
 
8406
            $length = strlen($hex);
 
8407
            if ($length !== 3 && $length !== 6) return false;
 
8408
            if (!ctype_xdigit($hex)) return false;
 
8409
        }
 
8410
 
 
8411
        return $color;
 
8412
 
 
8413
    }
 
8414
 
 
8415
}
 
8416
 
 
8417
 
 
8418
 
 
8419
 
 
8420
 
 
8421
/**
 
8422
 * Allows multiple validators to attempt to validate attribute.
 
8423
 *
 
8424
 * Composite is just what it sounds like: a composite of many validators.
 
8425
 * This means that multiple HTMLPurifier_AttrDef objects will have a whack
 
8426
 * at the string.  If one of them passes, that's what is returned.  This is
 
8427
 * especially useful for CSS values, which often are a choice between
 
8428
 * an enumerated set of predefined values or a flexible data type.
 
8429
 */
 
8430
class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef
 
8431
{
 
8432
 
 
8433
    /**
 
8434
     * List of HTMLPurifier_AttrDef objects that may process strings
 
8435
     * @todo Make protected
 
8436
     */
 
8437
    public $defs;
 
8438
 
 
8439
    /**
 
8440
     * @param $defs List of HTMLPurifier_AttrDef objects
 
8441
     */
 
8442
    public function __construct($defs) {
 
8443
        $this->defs = $defs;
 
8444
    }
 
8445
 
 
8446
    public function validate($string, $config, $context) {
 
8447
        foreach ($this->defs as $i => $def) {
 
8448
            $result = $this->defs[$i]->validate($string, $config, $context);
 
8449
            if ($result !== false) return $result;
 
8450
        }
 
8451
        return false;
 
8452
    }
 
8453
 
 
8454
}
 
8455
 
 
8456
 
 
8457
 
 
8458
 
 
8459
 
 
8460
/**
 
8461
 * Decorator which enables CSS properties to be disabled for specific elements.
 
8462
 */
 
8463
class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef
 
8464
{
 
8465
    public $def, $element;
 
8466
 
 
8467
    /**
 
8468
     * @param $def Definition to wrap
 
8469
     * @param $element Element to deny
 
8470
     */
 
8471
    public function __construct($def, $element) {
 
8472
        $this->def = $def;
 
8473
        $this->element = $element;
 
8474
    }
 
8475
    /**
 
8476
     * Checks if CurrentToken is set and equal to $this->element
 
8477
     */
 
8478
    public function validate($string, $config, $context) {
 
8479
        $token = $context->get('CurrentToken', true);
 
8480
        if ($token && $token->name == $this->element) return false;
 
8481
        return $this->def->validate($string, $config, $context);
 
8482
    }
 
8483
}
 
8484
 
 
8485
 
 
8486
 
 
8487
 
 
8488
 
 
8489
/**
 
8490
 * Microsoft's proprietary filter: CSS property
 
8491
 * @note Currently supports the alpha filter. In the future, this will
 
8492
 *       probably need an extensible framework
 
8493
 */
 
8494
class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef
 
8495
{
 
8496
 
 
8497
    protected $intValidator;
 
8498
 
 
8499
    public function __construct() {
 
8500
        $this->intValidator = new HTMLPurifier_AttrDef_Integer();
 
8501
    }
 
8502
 
 
8503
    public function validate($value, $config, $context) {
 
8504
        $value = $this->parseCDATA($value);
 
8505
        if ($value === 'none') return $value;
 
8506
        // if we looped this we could support multiple filters
 
8507
        $function_length = strcspn($value, '(');
 
8508
        $function = trim(substr($value, 0, $function_length));
 
8509
        if ($function !== 'alpha' &&
 
8510
            $function !== 'Alpha' &&
 
8511
            $function !== 'progid:DXImageTransform.Microsoft.Alpha'
 
8512
            ) return false;
 
8513
        $cursor = $function_length + 1;
 
8514
        $parameters_length = strcspn($value, ')', $cursor);
 
8515
        $parameters = substr($value, $cursor, $parameters_length);
 
8516
        $params = explode(',', $parameters);
 
8517
        $ret_params = array();
 
8518
        $lookup = array();
 
8519
        foreach ($params as $param) {
 
8520
            list($key, $value) = explode('=', $param);
 
8521
            $key   = trim($key);
 
8522
            $value = trim($value);
 
8523
            if (isset($lookup[$key])) continue;
 
8524
            if ($key !== 'opacity') continue;
 
8525
            $value = $this->intValidator->validate($value, $config, $context);
 
8526
            if ($value === false) continue;
 
8527
            $int = (int) $value;
 
8528
            if ($int > 100) $value = '100';
 
8529
            if ($int < 0) $value = '0';
 
8530
            $ret_params[] = "$key=$value";
 
8531
            $lookup[$key] = true;
 
8532
        }
 
8533
        $ret_parameters = implode(',', $ret_params);
 
8534
        $ret_function = "$function($ret_parameters)";
 
8535
        return $ret_function;
 
8536
    }
 
8537
 
 
8538
}
 
8539
 
 
8540
 
 
8541
 
 
8542
 
 
8543
 
 
8544
/**
 
8545
 * Validates shorthand CSS property font.
 
8546
 */
 
8547
class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef
 
8548
{
 
8549
 
 
8550
    /**
 
8551
     * Local copy of component validators.
 
8552
     *
 
8553
     * @note If we moved specific CSS property definitions to their own
 
8554
     *       classes instead of having them be assembled at run time by
 
8555
     *       CSSDefinition, this wouldn't be necessary.  We'd instantiate
 
8556
     *       our own copies.
 
8557
     */
 
8558
    protected $info = array();
 
8559
 
 
8560
    public function __construct($config) {
 
8561
        $def = $config->getCSSDefinition();
 
8562
        $this->info['font-style']   = $def->info['font-style'];
 
8563
        $this->info['font-variant'] = $def->info['font-variant'];
 
8564
        $this->info['font-weight']  = $def->info['font-weight'];
 
8565
        $this->info['font-size']    = $def->info['font-size'];
 
8566
        $this->info['line-height']  = $def->info['line-height'];
 
8567
        $this->info['font-family']  = $def->info['font-family'];
 
8568
    }
 
8569
 
 
8570
    public function validate($string, $config, $context) {
 
8571
 
 
8572
        static $system_fonts = array(
 
8573
            'caption' => true,
 
8574
            'icon' => true,
 
8575
            'menu' => true,
 
8576
            'message-box' => true,
 
8577
            'small-caption' => true,
 
8578
            'status-bar' => true
 
8579
        );
 
8580
 
 
8581
        // regular pre-processing
 
8582
        $string = $this->parseCDATA($string);
 
8583
        if ($string === '') return false;
 
8584
 
 
8585
        // check if it's one of the keywords
 
8586
        $lowercase_string = strtolower($string);
 
8587
        if (isset($system_fonts[$lowercase_string])) {
 
8588
            return $lowercase_string;
 
8589
        }
 
8590
 
 
8591
        $bits = explode(' ', $string); // bits to process
 
8592
        $stage = 0; // this indicates what we're looking for
 
8593
        $caught = array(); // which stage 0 properties have we caught?
 
8594
        $stage_1 = array('font-style', 'font-variant', 'font-weight');
 
8595
        $final = ''; // output
 
8596
 
 
8597
        for ($i = 0, $size = count($bits); $i < $size; $i++) {
 
8598
            if ($bits[$i] === '') continue;
 
8599
            switch ($stage) {
 
8600
 
 
8601
                // attempting to catch font-style, font-variant or font-weight
 
8602
                case 0:
 
8603
                    foreach ($stage_1 as $validator_name) {
 
8604
                        if (isset($caught[$validator_name])) continue;
 
8605
                        $r = $this->info[$validator_name]->validate(
 
8606
                                                $bits[$i], $config, $context);
 
8607
                        if ($r !== false) {
 
8608
                            $final .= $r . ' ';
 
8609
                            $caught[$validator_name] = true;
 
8610
                            break;
 
8611
                        }
 
8612
                    }
 
8613
                    // all three caught, continue on
 
8614
                    if (count($caught) >= 3) $stage = 1;
 
8615
                    if ($r !== false) break;
 
8616
 
 
8617
                // attempting to catch font-size and perhaps line-height
 
8618
                case 1:
 
8619
                    $found_slash = false;
 
8620
                    if (strpos($bits[$i], '/') !== false) {
 
8621
                        list($font_size, $line_height) =
 
8622
                                                    explode('/', $bits[$i]);
 
8623
                        if ($line_height === '') {
 
8624
                            // ooh, there's a space after the slash!
 
8625
                            $line_height = false;
 
8626
                            $found_slash = true;
 
8627
                        }
 
8628
                    } else {
 
8629
                        $font_size = $bits[$i];
 
8630
                        $line_height = false;
 
8631
                    }
 
8632
                    $r = $this->info['font-size']->validate(
 
8633
                                              $font_size, $config, $context);
 
8634
                    if ($r !== false) {
 
8635
                        $final .= $r;
 
8636
                        // attempt to catch line-height
 
8637
                        if ($line_height === false) {
 
8638
                            // we need to scroll forward
 
8639
                            for ($j = $i + 1; $j < $size; $j++) {
 
8640
                                if ($bits[$j] === '') continue;
 
8641
                                if ($bits[$j] === '/') {
 
8642
                                    if ($found_slash) {
 
8643
                                        return false;
 
8644
                                    } else {
 
8645
                                        $found_slash = true;
 
8646
                                        continue;
 
8647
                                    }
 
8648
                                }
 
8649
                                $line_height = $bits[$j];
 
8650
                                break;
 
8651
                            }
 
8652
                        } else {
 
8653
                            // slash already found
 
8654
                            $found_slash = true;
 
8655
                            $j = $i;
 
8656
                        }
 
8657
                        if ($found_slash) {
 
8658
                            $i = $j;
 
8659
                            $r = $this->info['line-height']->validate(
 
8660
                                              $line_height, $config, $context);
 
8661
                            if ($r !== false) {
 
8662
                                $final .= '/' . $r;
 
8663
                            }
 
8664
                        }
 
8665
                        $final .= ' ';
 
8666
                        $stage = 2;
 
8667
                        break;
 
8668
                    }
 
8669
                    return false;
 
8670
 
 
8671
                // attempting to catch font-family
 
8672
                case 2:
 
8673
                    $font_family =
 
8674
                        implode(' ', array_slice($bits, $i, $size - $i));
 
8675
                    $r = $this->info['font-family']->validate(
 
8676
                                              $font_family, $config, $context);
 
8677
                    if ($r !== false) {
 
8678
                        $final .= $r . ' ';
 
8679
                        // processing completed successfully
 
8680
                        return rtrim($final);
 
8681
                    }
 
8682
                    return false;
 
8683
            }
 
8684
        }
 
8685
        return false;
 
8686
    }
 
8687
 
 
8688
}
 
8689
 
 
8690
 
 
8691
 
 
8692
 
 
8693
 
 
8694
/**
 
8695
 * Validates a font family list according to CSS spec
 
8696
 * @todo whitelisting allowed fonts would be nice
 
8697
 */
 
8698
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
 
8699
{
 
8700
 
 
8701
    public function validate($string, $config, $context) {
 
8702
        static $generic_names = array(
 
8703
            'serif' => true,
 
8704
            'sans-serif' => true,
 
8705
            'monospace' => true,
 
8706
            'fantasy' => true,
 
8707
            'cursive' => true
 
8708
        );
 
8709
 
 
8710
        // assume that no font names contain commas in them
 
8711
        $fonts = explode(',', $string);
 
8712
        $final = '';
 
8713
        foreach($fonts as $font) {
 
8714
            $font = trim($font);
 
8715
            if ($font === '') continue;
 
8716
            // match a generic name
 
8717
            if (isset($generic_names[$font])) {
 
8718
                $final .= $font . ', ';
 
8719
                continue;
 
8720
            }
 
8721
            // match a quoted name
 
8722
            if ($font[0] === '"' || $font[0] === "'") {
 
8723
                $length = strlen($font);
 
8724
                if ($length <= 2) continue;
 
8725
                $quote = $font[0];
 
8726
                if ($font[$length - 1] !== $quote) continue;
 
8727
                $font = substr($font, 1, $length - 2);
 
8728
 
 
8729
                $new_font = '';
 
8730
                for ($i = 0, $c = strlen($font); $i < $c; $i++) {
 
8731
                    if ($font[$i] === '\\') {
 
8732
                        $i++;
 
8733
                        if ($i >= $c) {
 
8734
                            $new_font .= '\\';
 
8735
                            break;
 
8736
                        }
 
8737
                        if (ctype_xdigit($font[$i])) {
 
8738
                            $code = $font[$i];
 
8739
                            for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
 
8740
                                if (!ctype_xdigit($font[$i])) break;
 
8741
                                $code .= $font[$i];
 
8742
                            }
 
8743
                            // We have to be extremely careful when adding
 
8744
                            // new characters, to make sure we're not breaking
 
8745
                            // the encoding.
 
8746
                            $char = HTMLPurifier_Encoder::unichr(hexdec($code));
 
8747
                            if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue;
 
8748
                            $new_font .= $char;
 
8749
                            if ($i < $c && trim($font[$i]) !== '') $i--;
 
8750
                            continue;
 
8751
                        }
 
8752
                        if ($font[$i] === "\n") continue;
 
8753
                    }
 
8754
                    $new_font .= $font[$i];
 
8755
                }
 
8756
 
 
8757
                $font = $new_font;
 
8758
            }
 
8759
            // $font is a pure representation of the font name
 
8760
 
 
8761
            if (ctype_alnum($font) && $font !== '') {
 
8762
                // very simple font, allow it in unharmed
 
8763
                $final .= $font . ', ';
 
8764
                continue;
 
8765
            }
 
8766
 
 
8767
            // complicated font, requires quoting
 
8768
 
 
8769
            // armor single quotes and new lines
 
8770
            $font = str_replace("\\", "\\\\", $font);
 
8771
            $font = str_replace("'", "\\'", $font);
 
8772
            $final .= "'$font', ";
 
8773
        }
 
8774
        $final = rtrim($final, ', ');
 
8775
        if ($final === '') return false;
 
8776
        return $final;
 
8777
    }
 
8778
 
 
8779
}
 
8780
 
 
8781
 
 
8782
 
 
8783
 
 
8784
 
 
8785
/**
 
8786
 * Decorator which enables !important to be used in CSS values.
 
8787
 */
 
8788
class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef
 
8789
{
 
8790
    public $def, $allow;
 
8791
 
 
8792
    /**
 
8793
     * @param $def Definition to wrap
 
8794
     * @param $allow Whether or not to allow !important
 
8795
     */
 
8796
    public function __construct($def, $allow = false) {
 
8797
        $this->def = $def;
 
8798
        $this->allow = $allow;
 
8799
    }
 
8800
    /**
 
8801
     * Intercepts and removes !important if necessary
 
8802
     */
 
8803
    public function validate($string, $config, $context) {
 
8804
        // test for ! and important tokens
 
8805
        $string = trim($string);
 
8806
        $is_important = false;
 
8807
        // :TODO: optimization: test directly for !important and ! important
 
8808
        if (strlen($string) >= 9 && substr($string, -9) === 'important') {
 
8809
            $temp = rtrim(substr($string, 0, -9));
 
8810
            // use a temp, because we might want to restore important
 
8811
            if (strlen($temp) >= 1 && substr($temp, -1) === '!') {
 
8812
                $string = rtrim(substr($temp, 0, -1));
 
8813
                $is_important = true;
 
8814
            }
 
8815
        }
 
8816
        $string = $this->def->validate($string, $config, $context);
 
8817
        if ($this->allow && $is_important) $string .= ' !important';
 
8818
        return $string;
 
8819
    }
 
8820
}
 
8821
 
 
8822
 
 
8823
 
 
8824
 
 
8825
 
 
8826
/**
 
8827
 * Represents a Length as defined by CSS.
 
8828
 */
 
8829
class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef
 
8830
{
 
8831
 
 
8832
    protected $min, $max;
 
8833
 
 
8834
    /**
 
8835
     * @param HTMLPurifier_Length $max Minimum length, or null for no bound. String is also acceptable.
 
8836
     * @param HTMLPurifier_Length $max Maximum length, or null for no bound. String is also acceptable.
 
8837
     */
 
8838
    public function __construct($min = null, $max = null) {
 
8839
        $this->min = $min !== null ? HTMLPurifier_Length::make($min) : null;
 
8840
        $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null;
 
8841
    }
 
8842
 
 
8843
    public function validate($string, $config, $context) {
 
8844
        $string = $this->parseCDATA($string);
 
8845
 
 
8846
        // Optimizations
 
8847
        if ($string === '') return false;
 
8848
        if ($string === '0') return '0';
 
8849
        if (strlen($string) === 1) return false;
 
8850
 
 
8851
        $length = HTMLPurifier_Length::make($string);
 
8852
        if (!$length->isValid()) return false;
 
8853
 
 
8854
        if ($this->min) {
 
8855
            $c = $length->compareTo($this->min);
 
8856
            if ($c === false) return false;
 
8857
            if ($c < 0) return false;
 
8858
        }
 
8859
        if ($this->max) {
 
8860
            $c = $length->compareTo($this->max);
 
8861
            if ($c === false) return false;
 
8862
            if ($c > 0) return false;
 
8863
        }
 
8864
 
 
8865
        return $length->toString();
 
8866
    }
 
8867
 
 
8868
}
 
8869
 
 
8870
 
 
8871
 
 
8872
 
 
8873
 
 
8874
/**
 
8875
 * Validates shorthand CSS property list-style.
 
8876
 * @warning Does not support url tokens that have internal spaces.
 
8877
 */
 
8878
class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef
 
8879
{
 
8880
 
 
8881
    /**
 
8882
     * Local copy of component validators.
 
8883
     * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl.
 
8884
     */
 
8885
    protected $info;
 
8886
 
 
8887
    public function __construct($config) {
 
8888
        $def = $config->getCSSDefinition();
 
8889
        $this->info['list-style-type']     = $def->info['list-style-type'];
 
8890
        $this->info['list-style-position'] = $def->info['list-style-position'];
 
8891
        $this->info['list-style-image'] = $def->info['list-style-image'];
 
8892
    }
 
8893
 
 
8894
    public function validate($string, $config, $context) {
 
8895
 
 
8896
        // regular pre-processing
 
8897
        $string = $this->parseCDATA($string);
 
8898
        if ($string === '') return false;
 
8899
 
 
8900
        // assumes URI doesn't have spaces in it
 
8901
        $bits = explode(' ', strtolower($string)); // bits to process
 
8902
 
 
8903
        $caught = array();
 
8904
        $caught['type']     = false;
 
8905
        $caught['position'] = false;
 
8906
        $caught['image']    = false;
 
8907
 
 
8908
        $i = 0; // number of catches
 
8909
        $none = false;
 
8910
 
 
8911
        foreach ($bits as $bit) {
 
8912
            if ($i >= 3) return; // optimization bit
 
8913
            if ($bit === '') continue;
 
8914
            foreach ($caught as $key => $status) {
 
8915
                if ($status !== false) continue;
 
8916
                $r = $this->info['list-style-' . $key]->validate($bit, $config, $context);
 
8917
                if ($r === false) continue;
 
8918
                if ($r === 'none') {
 
8919
                    if ($none) continue;
 
8920
                    else $none = true;
 
8921
                    if ($key == 'image') continue;
 
8922
                }
 
8923
                $caught[$key] = $r;
 
8924
                $i++;
 
8925
                break;
 
8926
            }
 
8927
        }
 
8928
 
 
8929
        if (!$i) return false;
 
8930
 
 
8931
        $ret = array();
 
8932
 
 
8933
        // construct type
 
8934
        if ($caught['type']) $ret[] = $caught['type'];
 
8935
 
 
8936
        // construct image
 
8937
        if ($caught['image']) $ret[] = $caught['image'];
 
8938
 
 
8939
        // construct position
 
8940
        if ($caught['position']) $ret[] = $caught['position'];
 
8941
 
 
8942
        if (empty($ret)) return false;
 
8943
        return implode(' ', $ret);
 
8944
 
 
8945
    }
 
8946
 
 
8947
}
 
8948
 
 
8949
 
 
8950
 
 
8951
 
 
8952
 
 
8953
/**
 
8954
 * Framework class for strings that involve multiple values.
 
8955
 *
 
8956
 * Certain CSS properties such as border-width and margin allow multiple
 
8957
 * lengths to be specified.  This class can take a vanilla border-width
 
8958
 * definition and multiply it, usually into a max of four.
 
8959
 *
 
8960
 * @note Even though the CSS specification isn't clear about it, inherit
 
8961
 *       can only be used alone: it will never manifest as part of a multi
 
8962
 *       shorthand declaration.  Thus, this class does not allow inherit.
 
8963
 */
 
8964
class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef
 
8965
{
 
8966
 
 
8967
    /**
 
8968
     * Instance of component definition to defer validation to.
 
8969
     * @todo Make protected
 
8970
     */
 
8971
    public $single;
 
8972
 
 
8973
    /**
 
8974
     * Max number of values allowed.
 
8975
     * @todo Make protected
 
8976
     */
 
8977
    public $max;
 
8978
 
 
8979
    /**
 
8980
     * @param $single HTMLPurifier_AttrDef to multiply
 
8981
     * @param $max Max number of values allowed (usually four)
 
8982
     */
 
8983
    public function __construct($single, $max = 4) {
 
8984
        $this->single = $single;
 
8985
        $this->max = $max;
 
8986
    }
 
8987
 
 
8988
    public function validate($string, $config, $context) {
 
8989
        $string = $this->parseCDATA($string);
 
8990
        if ($string === '') return false;
 
8991
        $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n
 
8992
        $length = count($parts);
 
8993
        $final = '';
 
8994
        for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) {
 
8995
            if (ctype_space($parts[$i])) continue;
 
8996
            $result = $this->single->validate($parts[$i], $config, $context);
 
8997
            if ($result !== false) {
 
8998
                $final .= $result . ' ';
 
8999
                $num++;
 
9000
            }
 
9001
        }
 
9002
        if ($final === '') return false;
 
9003
        return rtrim($final);
 
9004
    }
 
9005
 
 
9006
}
 
9007
 
 
9008
 
 
9009
 
 
9010
 
 
9011
 
 
9012
/**
 
9013
 * Validates a Percentage as defined by the CSS spec.
 
9014
 */
 
9015
class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef
 
9016
{
 
9017
 
 
9018
    /**
 
9019
     * Instance of HTMLPurifier_AttrDef_CSS_Number to defer number validation
 
9020
     */
 
9021
    protected $number_def;
 
9022
 
 
9023
    /**
 
9024
     * @param Bool indicating whether to forbid negative values
 
9025
     */
 
9026
    public function __construct($non_negative = false) {
 
9027
        $this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
 
9028
    }
 
9029
 
 
9030
    public function validate($string, $config, $context) {
 
9031
 
 
9032
        $string = $this->parseCDATA($string);
 
9033
 
 
9034
        if ($string === '') return false;
 
9035
        $length = strlen($string);
 
9036
        if ($length === 1) return false;
 
9037
        if ($string[$length - 1] !== '%') return false;
 
9038
 
 
9039
        $number = substr($string, 0, $length - 1);
 
9040
        $number = $this->number_def->validate($number, $config, $context);
 
9041
 
 
9042
        if ($number === false) return false;
 
9043
        return "$number%";
 
9044
 
 
9045
    }
 
9046
 
 
9047
}
 
9048
 
 
9049
 
 
9050
 
 
9051
 
 
9052
 
 
9053
/**
 
9054
 * Validates the value for the CSS property text-decoration
 
9055
 * @note This class could be generalized into a version that acts sort of
 
9056
 *       like Enum except you can compound the allowed values.
 
9057
 */
 
9058
class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef
 
9059
{
 
9060
 
 
9061
    public function validate($string, $config, $context) {
 
9062
 
 
9063
        static $allowed_values = array(
 
9064
            'line-through' => true,
 
9065
            'overline' => true,
 
9066
            'underline' => true,
 
9067
        );
 
9068
 
 
9069
        $string = strtolower($this->parseCDATA($string));
 
9070
 
 
9071
        if ($string === 'none') return $string;
 
9072
 
 
9073
        $parts = explode(' ', $string);
 
9074
        $final = '';
 
9075
        foreach ($parts as $part) {
 
9076
            if (isset($allowed_values[$part])) {
 
9077
                $final .= $part . ' ';
 
9078
            }
 
9079
        }
 
9080
        $final = rtrim($final);
 
9081
        if ($final === '') return false;
 
9082
        return $final;
 
9083
 
 
9084
    }
 
9085
 
 
9086
}
 
9087
 
 
9088
 
 
9089
 
 
9090
 
 
9091
 
 
9092
/**
 
9093
 * Validates a URI in CSS syntax, which uses url('http://example.com')
 
9094
 * @note While theoretically speaking a URI in a CSS document could
 
9095
 *       be non-embedded, as of CSS2 there is no such usage so we're
 
9096
 *       generalizing it. This may need to be changed in the future.
 
9097
 * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as
 
9098
 *          the separator, you cannot put a literal semicolon in
 
9099
 *          in the URI. Try percent encoding it, in that case.
 
9100
 */
 
9101
class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
 
9102
{
 
9103
 
 
9104
    public function __construct() {
 
9105
        parent::__construct(true); // always embedded
 
9106
    }
 
9107
 
 
9108
    public function validate($uri_string, $config, $context) {
 
9109
        // parse the URI out of the string and then pass it onto
 
9110
        // the parent object
 
9111
 
 
9112
        $uri_string = $this->parseCDATA($uri_string);
 
9113
        if (strpos($uri_string, 'url(') !== 0) return false;
 
9114
        $uri_string = substr($uri_string, 4);
 
9115
        $new_length = strlen($uri_string) - 1;
 
9116
        if ($uri_string[$new_length] != ')') return false;
 
9117
        $uri = trim(substr($uri_string, 0, $new_length));
 
9118
 
 
9119
        if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) {
 
9120
            $quote = $uri[0];
 
9121
            $new_length = strlen($uri) - 1;
 
9122
            if ($uri[$new_length] !== $quote) return false;
 
9123
            $uri = substr($uri, 1, $new_length - 1);
 
9124
        }
 
9125
 
 
9126
        $keys   = array(  '(',   ')',   ',',   ' ',   '"',   "'");
 
9127
        $values = array('\\(', '\\)', '\\,', '\\ ', '\\"', "\\'");
 
9128
        $uri = str_replace($values, $keys, $uri);
 
9129
 
 
9130
        $result = parent::validate($uri, $config, $context);
 
9131
 
 
9132
        if ($result === false) return false;
 
9133
 
 
9134
        // escape necessary characters according to CSS spec
 
9135
        // except for the comma, none of these should appear in the
 
9136
        // URI at all
 
9137
        $result = str_replace($keys, $values, $result);
 
9138
 
 
9139
        return "url($result)";
 
9140
 
 
9141
    }
 
9142
 
 
9143
}
 
9144
 
 
9145
 
 
9146
 
 
9147
 
 
9148
 
 
9149
/**
 
9150
 * Validates a boolean attribute
 
9151
 */
 
9152
class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef
 
9153
{
 
9154
 
 
9155
    protected $name;
 
9156
    public $minimized = true;
 
9157
 
 
9158
    public function __construct($name = false) {$this->name = $name;}
 
9159
 
 
9160
    public function validate($string, $config, $context) {
 
9161
        if (empty($string)) return false;
 
9162
        return $this->name;
 
9163
    }
 
9164
 
 
9165
    /**
 
9166
     * @param $string Name of attribute
 
9167
     */
 
9168
    public function make($string) {
 
9169
        return new HTMLPurifier_AttrDef_HTML_Bool($string);
 
9170
    }
 
9171
 
 
9172
}
 
9173
 
 
9174
 
 
9175
 
 
9176
 
 
9177
 
 
9178
/**
 
9179
 * Validates contents based on NMTOKENS attribute type.
 
9180
 */
 
9181
class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef
 
9182
{
 
9183
 
 
9184
    public function validate($string, $config, $context) {
 
9185
 
 
9186
        $string = trim($string);
 
9187
 
 
9188
        // early abort: '' and '0' (strings that convert to false) are invalid
 
9189
        if (!$string) return false;
 
9190
 
 
9191
        $tokens = $this->split($string, $config, $context);
 
9192
        $tokens = $this->filter($tokens, $config, $context);
 
9193
        if (empty($tokens)) return false;
 
9194
        return implode(' ', $tokens);
 
9195
 
 
9196
    }
 
9197
 
 
9198
    /**
 
9199
     * Splits a space separated list of tokens into its constituent parts.
 
9200
     */
 
9201
    protected function split($string, $config, $context) {
 
9202
        // OPTIMIZABLE!
 
9203
        // do the preg_match, capture all subpatterns for reformulation
 
9204
 
 
9205
        // we don't support U+00A1 and up codepoints or
 
9206
        // escaping because I don't know how to do that with regexps
 
9207
        // and plus it would complicate optimization efforts (you never
 
9208
        // see that anyway).
 
9209
        $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start
 
9210
                   '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'.
 
9211
                   '(?:(?=\s)|\z)/'; // look ahead for space or string end
 
9212
        preg_match_all($pattern, $string, $matches);
 
9213
        return $matches[1];
 
9214
    }
 
9215
 
 
9216
    /**
 
9217
     * Template method for removing certain tokens based on arbitrary criteria.
 
9218
     * @note If we wanted to be really functional, we'd do an array_filter
 
9219
     *       with a callback. But... we're not.
 
9220
     */
 
9221
    protected function filter($tokens, $config, $context) {
 
9222
        return $tokens;
 
9223
    }
 
9224
 
 
9225
}
 
9226
 
 
9227
 
 
9228
 
 
9229
 
 
9230
 
 
9231
/**
 
9232
 * Implements special behavior for class attribute (normally NMTOKENS)
 
9233
 */
 
9234
class HTMLPurifier_AttrDef_HTML_Class extends HTMLPurifier_AttrDef_HTML_Nmtokens
 
9235
{
 
9236
    protected function split($string, $config, $context) {
 
9237
        // really, this twiddle should be lazy loaded
 
9238
        $name = $config->getDefinition('HTML')->doctype->name;
 
9239
        if ($name == "XHTML 1.1" || $name == "XHTML 2.0") {
 
9240
            return parent::split($string, $config, $context);
 
9241
        } else {
 
9242
            return preg_split('/\s+/', $string);
 
9243
        }
 
9244
    }
 
9245
    protected function filter($tokens, $config, $context) {
 
9246
        $allowed = $config->get('Attr.AllowedClasses');
 
9247
        $forbidden = $config->get('Attr.ForbiddenClasses');
 
9248
        $ret = array();
 
9249
        foreach ($tokens as $token) {
 
9250
            if (
 
9251
                ($allowed === null || isset($allowed[$token])) &&
 
9252
                !isset($forbidden[$token]) &&
 
9253
                // We need this O(n) check because of PHP's array
 
9254
                // implementation that casts -0 to 0.
 
9255
                !in_array($token, $ret, true)
 
9256
            ) {
 
9257
                $ret[] = $token;
 
9258
            }
 
9259
        }
 
9260
        return $ret;
 
9261
    }
 
9262
}
 
9263
 
 
9264
 
 
9265
 
 
9266
/**
 
9267
 * Validates a color according to the HTML spec.
 
9268
 */
 
9269
class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef
 
9270
{
 
9271
 
 
9272
    public function validate($string, $config, $context) {
 
9273
 
 
9274
        static $colors = null;
 
9275
        if ($colors === null) $colors = $config->get('Core.ColorKeywords');
 
9276
 
 
9277
        $string = trim($string);
 
9278
 
 
9279
        if (empty($string)) return false;
 
9280
        if (isset($colors[$string])) return $colors[$string];
 
9281
        if ($string[0] === '#') $hex = substr($string, 1);
 
9282
        else $hex = $string;
 
9283
 
 
9284
        $length = strlen($hex);
 
9285
        if ($length !== 3 && $length !== 6) return false;
 
9286
        if (!ctype_xdigit($hex)) return false;
 
9287
        if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
 
9288
 
 
9289
        return "#$hex";
 
9290
 
 
9291
    }
 
9292
 
 
9293
}
 
9294
 
 
9295
 
 
9296
 
 
9297
 
 
9298
 
 
9299
/**
 
9300
 * Special-case enum attribute definition that lazy loads allowed frame targets
 
9301
 */
 
9302
class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum
 
9303
{
 
9304
 
 
9305
    public $valid_values = false; // uninitialized value
 
9306
    protected $case_sensitive = false;
 
9307
 
 
9308
    public function __construct() {}
 
9309
 
 
9310
    public function validate($string, $config, $context) {
 
9311
        if ($this->valid_values === false) $this->valid_values = $config->get('Attr.AllowedFrameTargets');
 
9312
        return parent::validate($string, $config, $context);
 
9313
    }
 
9314
 
 
9315
}
 
9316
 
 
9317
 
 
9318
 
 
9319
 
 
9320
 
 
9321
/**
 
9322
 * Validates the HTML attribute ID.
 
9323
 * @warning Even though this is the id processor, it
 
9324
 *          will ignore the directive Attr:IDBlacklist, since it will only
 
9325
 *          go according to the ID accumulator. Since the accumulator is
 
9326
 *          automatically generated, it will have already absorbed the
 
9327
 *          blacklist. If you're hacking around, make sure you use load()!
 
9328
 */
 
9329
 
 
9330
class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef
 
9331
{
 
9332
 
 
9333
    // ref functionality disabled, since we also have to verify
 
9334
    // whether or not the ID it refers to exists
 
9335
 
 
9336
    public function validate($id, $config, $context) {
 
9337
 
 
9338
        if (!$config->get('Attr.EnableID')) return false;
 
9339
 
 
9340
        $id = trim($id); // trim it first
 
9341
 
 
9342
        if ($id === '') return false;
 
9343
 
 
9344
        $prefix = $config->get('Attr.IDPrefix');
 
9345
        if ($prefix !== '') {
 
9346
            $prefix .= $config->get('Attr.IDPrefixLocal');
 
9347
            // prevent re-appending the prefix
 
9348
            if (strpos($id, $prefix) !== 0) $id = $prefix . $id;
 
9349
        } elseif ($config->get('Attr.IDPrefixLocal') !== '') {
 
9350
            trigger_error('%Attr.IDPrefixLocal cannot be used unless '.
 
9351
                '%Attr.IDPrefix is set', E_USER_WARNING);
 
9352
        }
 
9353
 
 
9354
        //if (!$this->ref) {
 
9355
            $id_accumulator =& $context->get('IDAccumulator');
 
9356
            if (isset($id_accumulator->ids[$id])) return false;
 
9357
        //}
 
9358
 
 
9359
        // we purposely avoid using regex, hopefully this is faster
 
9360
 
 
9361
        if (ctype_alpha($id)) {
 
9362
            $result = true;
 
9363
        } else {
 
9364
            if (!ctype_alpha(@$id[0])) return false;
 
9365
            $trim = trim( // primitive style of regexps, I suppose
 
9366
                $id,
 
9367
                'A..Za..z0..9:-._'
 
9368
              );
 
9369
            $result = ($trim === '');
 
9370
        }
 
9371
 
 
9372
        $regexp = $config->get('Attr.IDBlacklistRegexp');
 
9373
        if ($regexp && preg_match($regexp, $id)) {
 
9374
            return false;
 
9375
        }
 
9376
 
 
9377
        if (/*!$this->ref && */$result) $id_accumulator->add($id);
 
9378
 
 
9379
        // if no change was made to the ID, return the result
 
9380
        // else, return the new id if stripping whitespace made it
 
9381
        //     valid, or return false.
 
9382
        return $result ? $id : false;
 
9383
 
 
9384
    }
 
9385
 
 
9386
}
 
9387
 
 
9388
 
 
9389
 
 
9390
 
 
9391
 
 
9392
/**
 
9393
 * Validates an integer representation of pixels according to the HTML spec.
 
9394
 */
 
9395
class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef
 
9396
{
 
9397
 
 
9398
    protected $max;
 
9399
 
 
9400
    public function __construct($max = null) {
 
9401
        $this->max = $max;
 
9402
    }
 
9403
 
 
9404
    public function validate($string, $config, $context) {
 
9405
 
 
9406
        $string = trim($string);
 
9407
        if ($string === '0') return $string;
 
9408
        if ($string === '')  return false;
 
9409
        $length = strlen($string);
 
9410
        if (substr($string, $length - 2) == 'px') {
 
9411
            $string = substr($string, 0, $length - 2);
 
9412
        }
 
9413
        if (!is_numeric($string)) return false;
 
9414
        $int = (int) $string;
 
9415
 
 
9416
        if ($int < 0) return '0';
 
9417
 
 
9418
        // upper-bound value, extremely high values can
 
9419
        // crash operating systems, see <http://ha.ckers.org/imagecrash.html>
 
9420
        // WARNING, above link WILL crash you if you're using Windows
 
9421
 
 
9422
        if ($this->max !== null && $int > $this->max) return (string) $this->max;
 
9423
 
 
9424
        return (string) $int;
 
9425
 
 
9426
    }
 
9427
 
 
9428
    public function make($string) {
 
9429
        if ($string === '') $max = null;
 
9430
        else $max = (int) $string;
 
9431
        $class = get_class($this);
 
9432
        return new $class($max);
 
9433
    }
 
9434
 
 
9435
}
 
9436
 
 
9437
 
 
9438
 
 
9439
 
 
9440
 
 
9441
/**
 
9442
 * Validates the HTML type length (not to be confused with CSS's length).
 
9443
 *
 
9444
 * This accepts integer pixels or percentages as lengths for certain
 
9445
 * HTML attributes.
 
9446
 */
 
9447
 
 
9448
class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels
 
9449
{
 
9450
 
 
9451
    public function validate($string, $config, $context) {
 
9452
 
 
9453
        $string = trim($string);
 
9454
        if ($string === '') return false;
 
9455
 
 
9456
        $parent_result = parent::validate($string, $config, $context);
 
9457
        if ($parent_result !== false) return $parent_result;
 
9458
 
 
9459
        $length = strlen($string);
 
9460
        $last_char = $string[$length - 1];
 
9461
 
 
9462
        if ($last_char !== '%') return false;
 
9463
 
 
9464
        $points = substr($string, 0, $length - 1);
 
9465
 
 
9466
        if (!is_numeric($points)) return false;
 
9467
 
 
9468
        $points = (int) $points;
 
9469
 
 
9470
        if ($points < 0) return '0%';
 
9471
        if ($points > 100) return '100%';
 
9472
 
 
9473
        return ((string) $points) . '%';
 
9474
 
 
9475
    }
 
9476
 
 
9477
}
 
9478
 
 
9479
 
 
9480
 
 
9481
 
 
9482
 
 
9483
/**
 
9484
 * Validates a rel/rev link attribute against a directive of allowed values
 
9485
 * @note We cannot use Enum because link types allow multiple
 
9486
 *       values.
 
9487
 * @note Assumes link types are ASCII text
 
9488
 */
 
9489
class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef
 
9490
{
 
9491
 
 
9492
    /** Name config attribute to pull. */
 
9493
    protected $name;
 
9494
 
 
9495
    public function __construct($name) {
 
9496
        $configLookup = array(
 
9497
            'rel' => 'AllowedRel',
 
9498
            'rev' => 'AllowedRev'
 
9499
        );
 
9500
        if (!isset($configLookup[$name])) {
 
9501
            trigger_error('Unrecognized attribute name for link '.
 
9502
                'relationship.', E_USER_ERROR);
 
9503
            return;
 
9504
        }
 
9505
        $this->name = $configLookup[$name];
 
9506
    }
 
9507
 
 
9508
    public function validate($string, $config, $context) {
 
9509
 
 
9510
        $allowed = $config->get('Attr.' . $this->name);
 
9511
        if (empty($allowed)) return false;
 
9512
 
 
9513
        $string = $this->parseCDATA($string);
 
9514
        $parts = explode(' ', $string);
 
9515
 
 
9516
        // lookup to prevent duplicates
 
9517
        $ret_lookup = array();
 
9518
        foreach ($parts as $part) {
 
9519
            $part = strtolower(trim($part));
 
9520
            if (!isset($allowed[$part])) continue;
 
9521
            $ret_lookup[$part] = true;
 
9522
        }
 
9523
 
 
9524
        if (empty($ret_lookup)) return false;
 
9525
        $string = implode(' ', array_keys($ret_lookup));
 
9526
 
 
9527
        return $string;
 
9528
 
 
9529
    }
 
9530
 
 
9531
}
 
9532
 
 
9533
 
 
9534
 
 
9535
 
 
9536
 
 
9537
/**
 
9538
 * Validates a MultiLength as defined by the HTML spec.
 
9539
 *
 
9540
 * A multilength is either a integer (pixel count), a percentage, or
 
9541
 * a relative number.
 
9542
 */
 
9543
class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length
 
9544
{
 
9545
 
 
9546
    public function validate($string, $config, $context) {
 
9547
 
 
9548
        $string = trim($string);
 
9549
        if ($string === '') return false;
 
9550
 
 
9551
        $parent_result = parent::validate($string, $config, $context);
 
9552
        if ($parent_result !== false) return $parent_result;
 
9553
 
 
9554
        $length = strlen($string);
 
9555
        $last_char = $string[$length - 1];
 
9556
 
 
9557
        if ($last_char !== '*') return false;
 
9558
 
 
9559
        $int = substr($string, 0, $length - 1);
 
9560
 
 
9561
        if ($int == '') return '*';
 
9562
        if (!is_numeric($int)) return false;
 
9563
 
 
9564
        $int = (int) $int;
 
9565
 
 
9566
        if ($int < 0) return false;
 
9567
        if ($int == 0) return '0';
 
9568
        if ($int == 1) return '*';
 
9569
        return ((string) $int) . '*';
 
9570
 
 
9571
    }
 
9572
 
 
9573
}
 
9574
 
 
9575
 
 
9576
 
 
9577
 
 
9578
 
 
9579
abstract class HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef
 
9580
{
 
9581
 
 
9582
    /**
 
9583
     * Unpacks a mailbox into its display-name and address
 
9584
     */
 
9585
    function unpack($string) {
 
9586
        // needs to be implemented
 
9587
    }
 
9588
 
 
9589
}
 
9590
 
 
9591
// sub-implementations
 
9592
 
 
9593
 
 
9594
 
 
9595
 
 
9596
 
 
9597
/**
 
9598
 * Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
 
9599
 */
 
9600
class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
 
9601
{
 
9602
 
 
9603
    /**
 
9604
     * Instance of HTMLPurifier_AttrDef_URI_IPv4 sub-validator
 
9605
     */
 
9606
    protected $ipv4;
 
9607
 
 
9608
    /**
 
9609
     * Instance of HTMLPurifier_AttrDef_URI_IPv6 sub-validator
 
9610
     */
 
9611
    protected $ipv6;
 
9612
 
 
9613
    public function __construct() {
 
9614
        $this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
 
9615
        $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
 
9616
    }
 
9617
 
 
9618
    public function validate($string, $config, $context) {
 
9619
        $length = strlen($string);
 
9620
        if ($string === '') return '';
 
9621
        if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') {
 
9622
            //IPv6
 
9623
            $ip = substr($string, 1, $length - 2);
 
9624
            $valid = $this->ipv6->validate($ip, $config, $context);
 
9625
            if ($valid === false) return false;
 
9626
            return '['. $valid . ']';
 
9627
        }
 
9628
 
 
9629
        // need to do checks on unusual encodings too
 
9630
        $ipv4 = $this->ipv4->validate($string, $config, $context);
 
9631
        if ($ipv4 !== false) return $ipv4;
 
9632
 
 
9633
        // A regular domain name.
 
9634
 
 
9635
        // This breaks I18N domain names, but we don't have proper IRI support,
 
9636
        // so force users to insert Punycode. If there's complaining we'll
 
9637
        // try to fix things into an international friendly form.
 
9638
 
 
9639
        // The productions describing this are:
 
9640
        $a   = '[a-z]';     // alpha
 
9641
        $an  = '[a-z0-9]';  // alphanum
 
9642
        $and = '[a-z0-9-]'; // alphanum | "-"
 
9643
        // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
 
9644
        $domainlabel   = "$an($and*$an)?";
 
9645
        // toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
 
9646
        $toplabel      = "$a($and*$an)?";
 
9647
        // hostname    = *( domainlabel "." ) toplabel [ "." ]
 
9648
        $match = preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string);
 
9649
        if (!$match) return false;
 
9650
 
 
9651
        return $string;
 
9652
    }
 
9653
 
 
9654
}
 
9655
 
 
9656
 
 
9657
 
 
9658
 
 
9659
 
 
9660
/**
 
9661
 * Validates an IPv4 address
 
9662
 * @author Feyd @ forums.devnetwork.net (public domain)
 
9663
 */
 
9664
class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef
 
9665
{
 
9666
 
 
9667
    /**
 
9668
     * IPv4 regex, protected so that IPv6 can reuse it
 
9669
     */
 
9670
    protected $ip4;
 
9671
 
 
9672
    public function validate($aIP, $config, $context) {
 
9673
 
 
9674
        if (!$this->ip4) $this->_loadRegex();
 
9675
 
 
9676
        if (preg_match('#^' . $this->ip4 . '$#s', $aIP))
 
9677
        {
 
9678
                return $aIP;
 
9679
        }
 
9680
 
 
9681
        return false;
 
9682
 
 
9683
    }
 
9684
 
 
9685
    /**
 
9686
     * Lazy load function to prevent regex from being stuffed in
 
9687
     * cache.
 
9688
     */
 
9689
    protected function _loadRegex() {
 
9690
        $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255
 
9691
        $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})";
 
9692
    }
 
9693
 
 
9694
}
 
9695
 
 
9696
 
 
9697
 
 
9698
 
 
9699
 
 
9700
/**
 
9701
 * Validates an IPv6 address.
 
9702
 * @author Feyd @ forums.devnetwork.net (public domain)
 
9703
 * @note This function requires brackets to have been removed from address
 
9704
 *       in URI.
 
9705
 */
 
9706
class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4
 
9707
{
 
9708
 
 
9709
    public function validate($aIP, $config, $context) {
 
9710
 
 
9711
        if (!$this->ip4) $this->_loadRegex();
 
9712
 
 
9713
        $original = $aIP;
 
9714
 
 
9715
        $hex = '[0-9a-fA-F]';
 
9716
        $blk = '(?:' . $hex . '{1,4})';
 
9717
        $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))';   // /0 - /128
 
9718
 
 
9719
        //      prefix check
 
9720
        if (strpos($aIP, '/') !== false)
 
9721
        {
 
9722
                if (preg_match('#' . $pre . '$#s', $aIP, $find))
 
9723
                {
 
9724
                        $aIP = substr($aIP, 0, 0-strlen($find[0]));
 
9725
                        unset($find);
 
9726
                }
 
9727
                else
 
9728
                {
 
9729
                        return false;
 
9730
                }
 
9731
        }
 
9732
 
 
9733
        //      IPv4-compatiblity check
 
9734
        if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find))
 
9735
        {
 
9736
                $aIP = substr($aIP, 0, 0-strlen($find[0]));
 
9737
                $ip = explode('.', $find[0]);
 
9738
                $ip = array_map('dechex', $ip);
 
9739
                $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3];
 
9740
                unset($find, $ip);
 
9741
        }
 
9742
 
 
9743
        //      compression check
 
9744
        $aIP = explode('::', $aIP);
 
9745
        $c = count($aIP);
 
9746
        if ($c > 2)
 
9747
        {
 
9748
                return false;
 
9749
        }
 
9750
        elseif ($c == 2)
 
9751
        {
 
9752
                list($first, $second) = $aIP;
 
9753
                $first = explode(':', $first);
 
9754
                $second = explode(':', $second);
 
9755
 
 
9756
                if (count($first) + count($second) > 8)
 
9757
                {
 
9758
                        return false;
 
9759
                }
 
9760
 
 
9761
                while(count($first) < 8)
 
9762
                {
 
9763
                        array_push($first, '0');
 
9764
                }
 
9765
 
 
9766
                array_splice($first, 8 - count($second), 8, $second);
 
9767
                $aIP = $first;
 
9768
                unset($first,$second);
 
9769
        }
 
9770
        else
 
9771
        {
 
9772
                $aIP = explode(':', $aIP[0]);
 
9773
        }
 
9774
        $c = count($aIP);
 
9775
 
 
9776
        if ($c != 8)
 
9777
        {
 
9778
                return false;
 
9779
        }
 
9780
 
 
9781
        //      All the pieces should be 16-bit hex strings. Are they?
 
9782
        foreach ($aIP as $piece)
 
9783
        {
 
9784
                if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece)))
 
9785
                {
 
9786
                        return false;
 
9787
                }
 
9788
        }
 
9789
 
 
9790
        return $original;
 
9791
 
 
9792
    }
 
9793
 
 
9794
}
 
9795
 
 
9796
 
 
9797
 
 
9798
 
 
9799
 
 
9800
/**
 
9801
 * Primitive email validation class based on the regexp found at
 
9802
 * http://www.regular-expressions.info/email.html
 
9803
 */
 
9804
class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email
 
9805
{
 
9806
 
 
9807
    public function validate($string, $config, $context) {
 
9808
        // no support for named mailboxes i.e. "Bob <bob@example.com>"
 
9809
        // that needs more percent encoding to be done
 
9810
        if ($string == '') return false;
 
9811
        $string = trim($string);
 
9812
        $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string);
 
9813
        return $result ? $string : false;
 
9814
    }
 
9815
 
 
9816
}
 
9817
 
 
9818
 
 
9819
 
 
9820
 
 
9821
 
 
9822
/**
 
9823
 * Pre-transform that changes proprietary background attribute to CSS.
 
9824
 */
 
9825
class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform {
 
9826
 
 
9827
    public function transform($attr, $config, $context) {
 
9828
 
 
9829
        if (!isset($attr['background'])) return $attr;
 
9830
 
 
9831
        $background = $this->confiscateAttr($attr, 'background');
 
9832
        // some validation should happen here
 
9833
 
 
9834
        $this->prependCSS($attr, "background-image:url($background);");
 
9835
 
 
9836
        return $attr;
 
9837
 
 
9838
    }
 
9839
 
 
9840
}
 
9841
 
 
9842
 
 
9843
 
 
9844
 
 
9845
 
 
9846
// this MUST be placed in post, as it assumes that any value in dir is valid
 
9847
 
 
9848
/**
 
9849
 * Post-trasnform that ensures that bdo tags have the dir attribute set.
 
9850
 */
 
9851
class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform
 
9852
{
 
9853
 
 
9854
    public function transform($attr, $config, $context) {
 
9855
        if (isset($attr['dir'])) return $attr;
 
9856
        $attr['dir'] = $config->get('Attr.DefaultTextDir');
 
9857
        return $attr;
 
9858
    }
 
9859
 
 
9860
}
 
9861
 
 
9862
 
 
9863
 
 
9864
 
 
9865
 
 
9866
/**
 
9867
 * Pre-transform that changes deprecated bgcolor attribute to CSS.
 
9868
 */
 
9869
class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform {
 
9870
 
 
9871
    public function transform($attr, $config, $context) {
 
9872
 
 
9873
        if (!isset($attr['bgcolor'])) return $attr;
 
9874
 
 
9875
        $bgcolor = $this->confiscateAttr($attr, 'bgcolor');
 
9876
        // some validation should happen here
 
9877
 
 
9878
        $this->prependCSS($attr, "background-color:$bgcolor;");
 
9879
 
 
9880
        return $attr;
 
9881
 
 
9882
    }
 
9883
 
 
9884
}
 
9885
 
 
9886
 
 
9887
 
 
9888
 
 
9889
 
 
9890
/**
 
9891
 * Pre-transform that changes converts a boolean attribute to fixed CSS
 
9892
 */
 
9893
class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform {
 
9894
 
 
9895
    /**
 
9896
     * Name of boolean attribute that is trigger
 
9897
     */
 
9898
    protected $attr;
 
9899
 
 
9900
    /**
 
9901
     * CSS declarations to add to style, needs trailing semicolon
 
9902
     */
 
9903
    protected $css;
 
9904
 
 
9905
    /**
 
9906
     * @param $attr string attribute name to convert from
 
9907
     * @param $css string CSS declarations to add to style (needs semicolon)
 
9908
     */
 
9909
    public function __construct($attr, $css) {
 
9910
        $this->attr = $attr;
 
9911
        $this->css  = $css;
 
9912
    }
 
9913
 
 
9914
    public function transform($attr, $config, $context) {
 
9915
        if (!isset($attr[$this->attr])) return $attr;
 
9916
        unset($attr[$this->attr]);
 
9917
        $this->prependCSS($attr, $this->css);
 
9918
        return $attr;
 
9919
    }
 
9920
 
 
9921
}
 
9922
 
 
9923
 
 
9924
 
 
9925
 
 
9926
 
 
9927
/**
 
9928
 * Pre-transform that changes deprecated border attribute to CSS.
 
9929
 */
 
9930
class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform {
 
9931
 
 
9932
    public function transform($attr, $config, $context) {
 
9933
        if (!isset($attr['border'])) return $attr;
 
9934
        $border_width = $this->confiscateAttr($attr, 'border');
 
9935
        // some validation should happen here
 
9936
        $this->prependCSS($attr, "border:{$border_width}px solid;");
 
9937
        return $attr;
 
9938
    }
 
9939
 
 
9940
}
 
9941
 
 
9942
 
 
9943
 
 
9944
 
 
9945
 
 
9946
/**
 
9947
 * Generic pre-transform that converts an attribute with a fixed number of
 
9948
 * values (enumerated) to CSS.
 
9949
 */
 
9950
class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform {
 
9951
 
 
9952
    /**
 
9953
     * Name of attribute to transform from
 
9954
     */
 
9955
    protected $attr;
 
9956
 
 
9957
    /**
 
9958
     * Lookup array of attribute values to CSS
 
9959
     */
 
9960
    protected $enumToCSS = array();
 
9961
 
 
9962
    /**
 
9963
     * Case sensitivity of the matching
 
9964
     * @warning Currently can only be guaranteed to work with ASCII
 
9965
     *          values.
 
9966
     */
 
9967
    protected $caseSensitive = false;
 
9968
 
 
9969
    /**
 
9970
     * @param $attr String attribute name to transform from
 
9971
     * @param $enumToCSS Lookup array of attribute values to CSS
 
9972
     * @param $case_sensitive Boolean case sensitivity indicator, default false
 
9973
     */
 
9974
    public function __construct($attr, $enum_to_css, $case_sensitive = false) {
 
9975
        $this->attr = $attr;
 
9976
        $this->enumToCSS = $enum_to_css;
 
9977
        $this->caseSensitive = (bool) $case_sensitive;
 
9978
    }
 
9979
 
 
9980
    public function transform($attr, $config, $context) {
 
9981
 
 
9982
        if (!isset($attr[$this->attr])) return $attr;
 
9983
 
 
9984
        $value = trim($attr[$this->attr]);
 
9985
        unset($attr[$this->attr]);
 
9986
 
 
9987
        if (!$this->caseSensitive) $value = strtolower($value);
 
9988
 
 
9989
        if (!isset($this->enumToCSS[$value])) {
 
9990
            return $attr;
 
9991
        }
 
9992
 
 
9993
        $this->prependCSS($attr, $this->enumToCSS[$value]);
 
9994
 
 
9995
        return $attr;
 
9996
 
 
9997
    }
 
9998
 
 
9999
}
 
10000
 
 
10001
 
 
10002
 
 
10003
 
 
10004
 
 
10005
// must be called POST validation
 
10006
 
 
10007
/**
 
10008
 * Transform that supplies default values for the src and alt attributes
 
10009
 * in img tags, as well as prevents the img tag from being removed
 
10010
 * because of a missing alt tag. This needs to be registered as both
 
10011
 * a pre and post attribute transform.
 
10012
 */
 
10013
class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform
 
10014
{
 
10015
 
 
10016
    public function transform($attr, $config, $context) {
 
10017
 
 
10018
        $src = true;
 
10019
        if (!isset($attr['src'])) {
 
10020
            if ($config->get('Core.RemoveInvalidImg')) return $attr;
 
10021
            $attr['src'] = $config->get('Attr.DefaultInvalidImage');
 
10022
            $src = false;
 
10023
        }
 
10024
 
 
10025
        if (!isset($attr['alt'])) {
 
10026
            if ($src) {
 
10027
                $alt = $config->get('Attr.DefaultImageAlt');
 
10028
                if ($alt === null) {
 
10029
                    $attr['alt'] = basename($attr['src']);
 
10030
                } else {
 
10031
                    $attr['alt'] = $alt;
 
10032
                }
 
10033
            } else {
 
10034
                $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt');
 
10035
            }
 
10036
        }
 
10037
 
 
10038
        return $attr;
 
10039
 
 
10040
    }
 
10041
 
 
10042
}
 
10043
 
 
10044
 
 
10045
 
 
10046
 
 
10047
 
 
10048
/**
 
10049
 * Pre-transform that changes deprecated hspace and vspace attributes to CSS
 
10050
 */
 
10051
class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform {
 
10052
 
 
10053
    protected $attr;
 
10054
    protected $css = array(
 
10055
        'hspace' => array('left', 'right'),
 
10056
        'vspace' => array('top', 'bottom')
 
10057
    );
 
10058
 
 
10059
    public function __construct($attr) {
 
10060
        $this->attr = $attr;
 
10061
        if (!isset($this->css[$attr])) {
 
10062
            trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
 
10063
        }
 
10064
    }
 
10065
 
 
10066
    public function transform($attr, $config, $context) {
 
10067
 
 
10068
        if (!isset($attr[$this->attr])) return $attr;
 
10069
 
 
10070
        $width = $this->confiscateAttr($attr, $this->attr);
 
10071
        // some validation could happen here
 
10072
 
 
10073
        if (!isset($this->css[$this->attr])) return $attr;
 
10074
 
 
10075
        $style = '';
 
10076
        foreach ($this->css[$this->attr] as $suffix) {
 
10077
            $property = "margin-$suffix";
 
10078
            $style .= "$property:{$width}px;";
 
10079
        }
 
10080
 
 
10081
        $this->prependCSS($attr, $style);
 
10082
 
 
10083
        return $attr;
 
10084
 
 
10085
    }
 
10086
 
 
10087
}
 
10088
 
 
10089
 
 
10090
 
 
10091
 
 
10092
 
 
10093
/**
 
10094
 * Performs miscellaneous cross attribute validation and filtering for
 
10095
 * input elements. This is meant to be a post-transform.
 
10096
 */
 
10097
class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform {
 
10098
 
 
10099
    protected $pixels;
 
10100
 
 
10101
    public function __construct() {
 
10102
        $this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
 
10103
    }
 
10104
 
 
10105
    public function transform($attr, $config, $context) {
 
10106
        if (!isset($attr['type'])) $t = 'text';
 
10107
        else $t = strtolower($attr['type']);
 
10108
        if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
 
10109
            unset($attr['checked']);
 
10110
        }
 
10111
        if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') {
 
10112
            unset($attr['maxlength']);
 
10113
        }
 
10114
        if (isset($attr['size']) && $t !== 'text' && $t !== 'password') {
 
10115
            $result = $this->pixels->validate($attr['size'], $config, $context);
 
10116
            if ($result === false) unset($attr['size']);
 
10117
            else $attr['size'] = $result;
 
10118
        }
 
10119
        if (isset($attr['src']) && $t !== 'image') {
 
10120
            unset($attr['src']);
 
10121
        }
 
10122
        if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
 
10123
            $attr['value'] = '';
 
10124
        }
 
10125
        return $attr;
 
10126
    }
 
10127
 
 
10128
}
 
10129
 
 
10130
 
 
10131
 
 
10132
 
 
10133
 
 
10134
/**
 
10135
 * Post-transform that copies lang's value to xml:lang (and vice-versa)
 
10136
 * @note Theoretically speaking, this could be a pre-transform, but putting
 
10137
 *       post is more efficient.
 
10138
 */
 
10139
class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform
 
10140
{
 
10141
 
 
10142
    public function transform($attr, $config, $context) {
 
10143
 
 
10144
        $lang     = isset($attr['lang']) ? $attr['lang'] : false;
 
10145
        $xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false;
 
10146
 
 
10147
        if ($lang !== false && $xml_lang === false) {
 
10148
            $attr['xml:lang'] = $lang;
 
10149
        } elseif ($xml_lang !== false) {
 
10150
            $attr['lang'] = $xml_lang;
 
10151
        }
 
10152
 
 
10153
        return $attr;
 
10154
 
 
10155
    }
 
10156
 
 
10157
}
 
10158
 
 
10159
 
 
10160
 
 
10161
 
 
10162
 
 
10163
/**
 
10164
 * Class for handling width/height length attribute transformations to CSS
 
10165
 */
 
10166
class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform
 
10167
{
 
10168
 
 
10169
    protected $name;
 
10170
    protected $cssName;
 
10171
 
 
10172
    public function __construct($name, $css_name = null) {
 
10173
        $this->name = $name;
 
10174
        $this->cssName = $css_name ? $css_name : $name;
 
10175
    }
 
10176
 
 
10177
    public function transform($attr, $config, $context) {
 
10178
        if (!isset($attr[$this->name])) return $attr;
 
10179
        $length = $this->confiscateAttr($attr, $this->name);
 
10180
        if(ctype_digit($length)) $length .= 'px';
 
10181
        $this->prependCSS($attr, $this->cssName . ":$length;");
 
10182
        return $attr;
 
10183
    }
 
10184
 
 
10185
}
 
10186
 
 
10187
 
 
10188
 
 
10189
 
 
10190
 
 
10191
/**
 
10192
 * Pre-transform that changes deprecated name attribute to ID if necessary
 
10193
 */
 
10194
class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform
 
10195
{
 
10196
 
 
10197
    public function transform($attr, $config, $context) {
 
10198
        // Abort early if we're using relaxed definition of name
 
10199
        if ($config->get('HTML.Attr.Name.UseCDATA')) return $attr;
 
10200
        if (!isset($attr['name'])) return $attr;
 
10201
        $id = $this->confiscateAttr($attr, 'name');
 
10202
        if ( isset($attr['id']))   return $attr;
 
10203
        $attr['id'] = $id;
 
10204
        return $attr;
 
10205
    }
 
10206
 
 
10207
}
 
10208
 
 
10209
 
 
10210
 
 
10211
 
 
10212
 
 
10213
/**
 
10214
 * Post-transform that performs validation to the name attribute; if
 
10215
 * it is present with an equivalent id attribute, it is passed through;
 
10216
 * otherwise validation is performed.
 
10217
 */
 
10218
class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
 
10219
{
 
10220
 
 
10221
    public function __construct() {
 
10222
        $this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
 
10223
    }
 
10224
 
 
10225
    public function transform($attr, $config, $context) {
 
10226
        if (!isset($attr['name'])) return $attr;
 
10227
        $name = $attr['name'];
 
10228
        if (isset($attr['id']) && $attr['id'] === $name) return $attr;
 
10229
        $result = $this->idDef->validate($name, $config, $context);
 
10230
        if ($result === false) unset($attr['name']);
 
10231
        else $attr['name'] = $result;
 
10232
        return $attr;
 
10233
    }
 
10234
 
 
10235
}
 
10236
 
 
10237
 
 
10238
 
 
10239
 
 
10240
 
 
10241
class HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform
 
10242
{
 
10243
    public $name = "SafeEmbed";
 
10244
 
 
10245
    public function transform($attr, $config, $context) {
 
10246
        $attr['allowscriptaccess'] = 'never';
 
10247
        $attr['allownetworking'] = 'internal';
 
10248
        $attr['type'] = 'application/x-shockwave-flash';
 
10249
        return $attr;
 
10250
    }
 
10251
}
 
10252
 
 
10253
 
 
10254
 
 
10255
 
 
10256
 
 
10257
/**
 
10258
 * Writes default type for all objects. Currently only supports flash.
 
10259
 */
 
10260
class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform
 
10261
{
 
10262
    public $name = "SafeObject";
 
10263
 
 
10264
    function transform($attr, $config, $context) {
 
10265
        if (!isset($attr['type'])) $attr['type'] = 'application/x-shockwave-flash';
 
10266
        return $attr;
 
10267
    }
 
10268
}
 
10269
 
 
10270
 
 
10271
 
 
10272
 
 
10273
 
 
10274
/**
 
10275
 * Validates name/value pairs in param tags to be used in safe objects. This
 
10276
 * will only allow name values it recognizes, and pre-fill certain attributes
 
10277
 * with required values.
 
10278
 *
 
10279
 * @note
 
10280
 *      This class only supports Flash. In the future, Quicktime support
 
10281
 *      may be added.
 
10282
 *
 
10283
 * @warning
 
10284
 *      This class expects an injector to add the necessary parameters tags.
 
10285
 */
 
10286
class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
 
10287
{
 
10288
    public $name = "SafeParam";
 
10289
    private $uri;
 
10290
 
 
10291
    public function __construct() {
 
10292
        $this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
 
10293
    }
 
10294
 
 
10295
    public function transform($attr, $config, $context) {
 
10296
        // If we add support for other objects, we'll need to alter the
 
10297
        // transforms.
 
10298
        switch ($attr['name']) {
 
10299
            // application/x-shockwave-flash
 
10300
            // Keep this synchronized with Injector/SafeObject.php
 
10301
            case 'allowScriptAccess':
 
10302
                $attr['value'] = 'never';
 
10303
                break;
 
10304
            case 'allowNetworking':
 
10305
                $attr['value'] = 'internal';
 
10306
                break;
 
10307
            case 'wmode':
 
10308
                $attr['value'] = 'window';
 
10309
                break;
 
10310
            case 'movie':
 
10311
                $attr['value'] = $this->uri->validate($attr['value'], $config, $context);
 
10312
                break;
 
10313
            // add other cases to support other param name/value pairs
 
10314
            default:
 
10315
                $attr['name'] = $attr['value'] = null;
 
10316
        }
 
10317
        return $attr;
 
10318
    }
 
10319
}
 
10320
 
 
10321
 
 
10322
 
 
10323
 
 
10324
 
 
10325
/**
 
10326
 * Implements required attribute stipulation for <script>
 
10327
 */
 
10328
class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
 
10329
{
 
10330
    public function transform($attr, $config, $context) {
 
10331
        if (!isset($attr['type'])) {
 
10332
            $attr['type'] = 'text/javascript';
 
10333
        }
 
10334
        return $attr;
 
10335
    }
 
10336
}
 
10337
 
 
10338
 
 
10339
 
 
10340
 
 
10341
 
 
10342
/**
 
10343
 * Sets height/width defaults for <textarea>
 
10344
 */
 
10345
class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
 
10346
{
 
10347
 
 
10348
    public function transform($attr, $config, $context) {
 
10349
        // Calculated from Firefox
 
10350
        if (!isset($attr['cols'])) $attr['cols'] = '22';
 
10351
        if (!isset($attr['rows'])) $attr['rows'] = '3';
 
10352
        return $attr;
 
10353
    }
 
10354
 
 
10355
}
 
10356
 
 
10357
 
 
10358
 
 
10359
 
 
10360
 
 
10361
/**
 
10362
 * Definition that uses different definitions depending on context.
 
10363
 *
 
10364
 * The del and ins tags are notable because they allow different types of
 
10365
 * elements depending on whether or not they're in a block or inline context.
 
10366
 * Chameleon allows this behavior to happen by using two different
 
10367
 * definitions depending on context.  While this somewhat generalized,
 
10368
 * it is specifically intended for those two tags.
 
10369
 */
 
10370
class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef
 
10371
{
 
10372
 
 
10373
    /**
 
10374
     * Instance of the definition object to use when inline. Usually stricter.
 
10375
     */
 
10376
    public $inline;
 
10377
 
 
10378
    /**
 
10379
     * Instance of the definition object to use when block.
 
10380
     */
 
10381
    public $block;
 
10382
 
 
10383
    public $type = 'chameleon';
 
10384
 
 
10385
    /**
 
10386
     * @param $inline List of elements to allow when inline.
 
10387
     * @param $block List of elements to allow when block.
 
10388
     */
 
10389
    public function __construct($inline, $block) {
 
10390
        $this->inline = new HTMLPurifier_ChildDef_Optional($inline);
 
10391
        $this->block  = new HTMLPurifier_ChildDef_Optional($block);
 
10392
        $this->elements = $this->block->elements;
 
10393
    }
 
10394
 
 
10395
    public function validateChildren($tokens_of_children, $config, $context) {
 
10396
        if ($context->get('IsInline') === false) {
 
10397
            return $this->block->validateChildren(
 
10398
                $tokens_of_children, $config, $context);
 
10399
        } else {
 
10400
            return $this->inline->validateChildren(
 
10401
                $tokens_of_children, $config, $context);
 
10402
        }
 
10403
    }
 
10404
}
 
10405
 
 
10406
 
 
10407
 
 
10408
 
 
10409
 
 
10410
/**
 
10411
 * Custom validation class, accepts DTD child definitions
 
10412
 *
 
10413
 * @warning Currently this class is an all or nothing proposition, that is,
 
10414
 *          it will only give a bool return value.
 
10415
 */
 
10416
class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef
 
10417
{
 
10418
    public $type = 'custom';
 
10419
    public $allow_empty = false;
 
10420
    /**
 
10421
     * Allowed child pattern as defined by the DTD
 
10422
     */
 
10423
    public $dtd_regex;
 
10424
    /**
 
10425
     * PCRE regex derived from $dtd_regex
 
10426
     * @private
 
10427
     */
 
10428
    private $_pcre_regex;
 
10429
    /**
 
10430
     * @param $dtd_regex Allowed child pattern from the DTD
 
10431
     */
 
10432
    public function __construct($dtd_regex) {
 
10433
        $this->dtd_regex = $dtd_regex;
 
10434
        $this->_compileRegex();
 
10435
    }
 
10436
    /**
 
10437
     * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
 
10438
     */
 
10439
    protected function _compileRegex() {
 
10440
        $raw = str_replace(' ', '', $this->dtd_regex);
 
10441
        if ($raw{0} != '(') {
 
10442
            $raw = "($raw)";
 
10443
        }
 
10444
        $el = '[#a-zA-Z0-9_.-]+';
 
10445
        $reg = $raw;
 
10446
 
 
10447
        // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
 
10448
        // DOING! Seriously: if there's problems, please report them.
 
10449
 
 
10450
        // collect all elements into the $elements array
 
10451
        preg_match_all("/$el/", $reg, $matches);
 
10452
        foreach ($matches[0] as $match) {
 
10453
            $this->elements[$match] = true;
 
10454
        }
 
10455
 
 
10456
        // setup all elements as parentheticals with leading commas
 
10457
        $reg = preg_replace("/$el/", '(,\\0)', $reg);
 
10458
 
 
10459
        // remove commas when they were not solicited
 
10460
        $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg);
 
10461
 
 
10462
        // remove all non-paranthetical commas: they are handled by first regex
 
10463
        $reg = preg_replace("/,\(/", '(', $reg);
 
10464
 
 
10465
        $this->_pcre_regex = $reg;
 
10466
    }
 
10467
    public function validateChildren($tokens_of_children, $config, $context) {
 
10468
        $list_of_children = '';
 
10469
        $nesting = 0; // depth into the nest
 
10470
        foreach ($tokens_of_children as $token) {
 
10471
            if (!empty($token->is_whitespace)) continue;
 
10472
 
 
10473
            $is_child = ($nesting == 0); // direct
 
10474
 
 
10475
            if ($token instanceof HTMLPurifier_Token_Start) {
 
10476
                $nesting++;
 
10477
            } elseif ($token instanceof HTMLPurifier_Token_End) {
 
10478
                $nesting--;
 
10479
            }
 
10480
 
 
10481
            if ($is_child) {
 
10482
                $list_of_children .= $token->name . ',';
 
10483
            }
 
10484
        }
 
10485
        // add leading comma to deal with stray comma declarations
 
10486
        $list_of_children = ',' . rtrim($list_of_children, ',');
 
10487
        $okay =
 
10488
            preg_match(
 
10489
                '/^,?'.$this->_pcre_regex.'$/',
 
10490
                $list_of_children
 
10491
            );
 
10492
 
 
10493
        return (bool) $okay;
 
10494
    }
 
10495
}
 
10496
 
 
10497
 
 
10498
 
 
10499
 
 
10500
 
 
10501
/**
 
10502
 * Definition that disallows all elements.
 
10503
 * @warning validateChildren() in this class is actually never called, because
 
10504
 *          empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed
 
10505
 *          before child definitions are parsed in earnest by
 
10506
 *          HTMLPurifier_Strategy_FixNesting.
 
10507
 */
 
10508
class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef
 
10509
{
 
10510
    public $allow_empty = true;
 
10511
    public $type = 'empty';
 
10512
    public function __construct() {}
 
10513
    public function validateChildren($tokens_of_children, $config, $context) {
 
10514
        return array();
 
10515
    }
 
10516
}
 
10517
 
 
10518
 
 
10519
 
 
10520
 
 
10521
 
 
10522
/**
 
10523
 * Definition that allows a set of elements, but disallows empty children.
 
10524
 */
 
10525
class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef
 
10526
{
 
10527
    /**
 
10528
     * Lookup table of allowed elements.
 
10529
     * @public
 
10530
     */
 
10531
    public $elements = array();
 
10532
    /**
 
10533
     * Whether or not the last passed node was all whitespace.
 
10534
     */
 
10535
    protected $whitespace = false;
 
10536
    /**
 
10537
     * @param $elements List of allowed element names (lowercase).
 
10538
     */
 
10539
    public function __construct($elements) {
 
10540
        if (is_string($elements)) {
 
10541
            $elements = str_replace(' ', '', $elements);
 
10542
            $elements = explode('|', $elements);
 
10543
        }
 
10544
        $keys = array_keys($elements);
 
10545
        if ($keys == array_keys($keys)) {
 
10546
            $elements = array_flip($elements);
 
10547
            foreach ($elements as $i => $x) {
 
10548
                $elements[$i] = true;
 
10549
                if (empty($i)) unset($elements[$i]); // remove blank
 
10550
            }
 
10551
        }
 
10552
        $this->elements = $elements;
 
10553
    }
 
10554
    public $allow_empty = false;
 
10555
    public $type = 'required';
 
10556
    public function validateChildren($tokens_of_children, $config, $context) {
 
10557
        // Flag for subclasses
 
10558
        $this->whitespace = false;
 
10559
 
 
10560
        // if there are no tokens, delete parent node
 
10561
        if (empty($tokens_of_children)) return false;
 
10562
 
 
10563
        // the new set of children
 
10564
        $result = array();
 
10565
 
 
10566
        // current depth into the nest
 
10567
        $nesting = 0;
 
10568
 
 
10569
        // whether or not we're deleting a node
 
10570
        $is_deleting = false;
 
10571
 
 
10572
        // whether or not parsed character data is allowed
 
10573
        // this controls whether or not we silently drop a tag
 
10574
        // or generate escaped HTML from it
 
10575
        $pcdata_allowed = isset($this->elements['#PCDATA']);
 
10576
 
 
10577
        // a little sanity check to make sure it's not ALL whitespace
 
10578
        $all_whitespace = true;
 
10579
 
 
10580
        // some configuration
 
10581
        $escape_invalid_children = $config->get('Core.EscapeInvalidChildren');
 
10582
 
 
10583
        // generator
 
10584
        $gen = new HTMLPurifier_Generator($config, $context);
 
10585
 
 
10586
        foreach ($tokens_of_children as $token) {
 
10587
            if (!empty($token->is_whitespace)) {
 
10588
                $result[] = $token;
 
10589
                continue;
 
10590
            }
 
10591
            $all_whitespace = false; // phew, we're not talking about whitespace
 
10592
 
 
10593
            $is_child = ($nesting == 0);
 
10594
 
 
10595
            if ($token instanceof HTMLPurifier_Token_Start) {
 
10596
                $nesting++;
 
10597
            } elseif ($token instanceof HTMLPurifier_Token_End) {
 
10598
                $nesting--;
 
10599
            }
 
10600
 
 
10601
            if ($is_child) {
 
10602
                $is_deleting = false;
 
10603
                if (!isset($this->elements[$token->name])) {
 
10604
                    $is_deleting = true;
 
10605
                    if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) {
 
10606
                        $result[] = $token;
 
10607
                    } elseif ($pcdata_allowed && $escape_invalid_children) {
 
10608
                        $result[] = new HTMLPurifier_Token_Text(
 
10609
                            $gen->generateFromToken($token)
 
10610
                        );
 
10611
                    }
 
10612
                    continue;
 
10613
                }
 
10614
            }
 
10615
            if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) {
 
10616
                $result[] = $token;
 
10617
            } elseif ($pcdata_allowed && $escape_invalid_children) {
 
10618
                $result[] =
 
10619
                    new HTMLPurifier_Token_Text(
 
10620
                        $gen->generateFromToken($token)
 
10621
                    );
 
10622
            } else {
 
10623
                // drop silently
 
10624
            }
 
10625
        }
 
10626
        if (empty($result)) return false;
 
10627
        if ($all_whitespace) {
 
10628
            $this->whitespace = true;
 
10629
            return false;
 
10630
        }
 
10631
        if ($tokens_of_children == $result) return true;
 
10632
        return $result;
 
10633
    }
 
10634
}
 
10635
 
 
10636
 
 
10637
 
 
10638
 
 
10639
 
 
10640
/**
 
10641
 * Definition that allows a set of elements, and allows no children.
 
10642
 * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required,
 
10643
 *       really, one shouldn't inherit from the other.  Only altered behavior
 
10644
 *       is to overload a returned false with an array.  Thus, it will never
 
10645
 *       return false.
 
10646
 */
 
10647
class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required
 
10648
{
 
10649
    public $allow_empty = true;
 
10650
    public $type = 'optional';
 
10651
    public function validateChildren($tokens_of_children, $config, $context) {
 
10652
        $result = parent::validateChildren($tokens_of_children, $config, $context);
 
10653
        // we assume that $tokens_of_children is not modified
 
10654
        if ($result === false) {
 
10655
            if (empty($tokens_of_children)) return true;
 
10656
            elseif ($this->whitespace) return $tokens_of_children;
 
10657
            else return array();
 
10658
        }
 
10659
        return $result;
 
10660
    }
 
10661
}
 
10662
 
 
10663
 
 
10664
 
 
10665
 
 
10666
 
 
10667
/**
 
10668
 * Takes the contents of blockquote when in strict and reformats for validation.
 
10669
 */
 
10670
class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required
 
10671
{
 
10672
    protected $real_elements;
 
10673
    protected $fake_elements;
 
10674
    public $allow_empty = true;
 
10675
    public $type = 'strictblockquote';
 
10676
    protected $init = false;
 
10677
 
 
10678
    /**
 
10679
     * @note We don't want MakeWellFormed to auto-close inline elements since
 
10680
     *       they might be allowed.
 
10681
     */
 
10682
    public function getAllowedElements($config) {
 
10683
        $this->init($config);
 
10684
        return $this->fake_elements;
 
10685
    }
 
10686
 
 
10687
    public function validateChildren($tokens_of_children, $config, $context) {
 
10688
 
 
10689
        $this->init($config);
 
10690
 
 
10691
        // trick the parent class into thinking it allows more
 
10692
        $this->elements = $this->fake_elements;
 
10693
        $result = parent::validateChildren($tokens_of_children, $config, $context);
 
10694
        $this->elements = $this->real_elements;
 
10695
 
 
10696
        if ($result === false) return array();
 
10697
        if ($result === true) $result = $tokens_of_children;
 
10698
 
 
10699
        $def = $config->getHTMLDefinition();
 
10700
        $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper);
 
10701
        $block_wrap_end   = new HTMLPurifier_Token_End(  $def->info_block_wrapper);
 
10702
        $is_inline = false;
 
10703
        $depth = 0;
 
10704
        $ret = array();
 
10705
 
 
10706
        // assuming that there are no comment tokens
 
10707
        foreach ($result as $i => $token) {
 
10708
            $token = $result[$i];
 
10709
            // ifs are nested for readability
 
10710
            if (!$is_inline) {
 
10711
                if (!$depth) {
 
10712
                     if (
 
10713
                        ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) ||
 
10714
                        (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name]))
 
10715
                     ) {
 
10716
                        $is_inline = true;
 
10717
                        $ret[] = $block_wrap_start;
 
10718
                     }
 
10719
                }
 
10720
            } else {
 
10721
                if (!$depth) {
 
10722
                    // starting tokens have been inline text / empty
 
10723
                    if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) {
 
10724
                        if (isset($this->elements[$token->name])) {
 
10725
                            // ended
 
10726
                            $ret[] = $block_wrap_end;
 
10727
                            $is_inline = false;
 
10728
                        }
 
10729
                    }
 
10730
                }
 
10731
            }
 
10732
            $ret[] = $token;
 
10733
            if ($token instanceof HTMLPurifier_Token_Start) $depth++;
 
10734
            if ($token instanceof HTMLPurifier_Token_End)   $depth--;
 
10735
        }
 
10736
        if ($is_inline) $ret[] = $block_wrap_end;
 
10737
        return $ret;
 
10738
    }
 
10739
 
 
10740
    private function init($config) {
 
10741
        if (!$this->init) {
 
10742
            $def = $config->getHTMLDefinition();
 
10743
            // allow all inline elements
 
10744
            $this->real_elements = $this->elements;
 
10745
            $this->fake_elements = $def->info_content_sets['Flow'];
 
10746
            $this->fake_elements['#PCDATA'] = true;
 
10747
            $this->init = true;
 
10748
        }
 
10749
    }
 
10750
}
 
10751
 
 
10752
 
 
10753
 
 
10754
 
 
10755
 
 
10756
/**
 
10757
 * Definition for tables
 
10758
 */
 
10759
class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
 
10760
{
 
10761
    public $allow_empty = false;
 
10762
    public $type = 'table';
 
10763
    public $elements = array('tr' => true, 'tbody' => true, 'thead' => true,
 
10764
        'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true);
 
10765
    public function __construct() {}
 
10766
    public function validateChildren($tokens_of_children, $config, $context) {
 
10767
        if (empty($tokens_of_children)) return false;
 
10768
 
 
10769
        // this ensures that the loop gets run one last time before closing
 
10770
        // up. It's a little bit of a hack, but it works! Just make sure you
 
10771
        // get rid of the token later.
 
10772
        $tokens_of_children[] = false;
 
10773
 
 
10774
        // only one of these elements is allowed in a table
 
10775
        $caption = false;
 
10776
        $thead   = false;
 
10777
        $tfoot   = false;
 
10778
 
 
10779
        // as many of these as you want
 
10780
        $cols    = array();
 
10781
        $content = array();
 
10782
 
 
10783
        $nesting = 0; // current depth so we can determine nodes
 
10784
        $is_collecting = false; // are we globbing together tokens to package
 
10785
                                // into one of the collectors?
 
10786
        $collection = array(); // collected nodes
 
10787
        $tag_index = 0; // the first node might be whitespace,
 
10788
                            // so this tells us where the start tag is
 
10789
 
 
10790
        foreach ($tokens_of_children as $token) {
 
10791
            $is_child = ($nesting == 0);
 
10792
 
 
10793
            if ($token === false) {
 
10794
                // terminating sequence started
 
10795
            } elseif ($token instanceof HTMLPurifier_Token_Start) {
 
10796
                $nesting++;
 
10797
            } elseif ($token instanceof HTMLPurifier_Token_End) {
 
10798
                $nesting--;
 
10799
            }
 
10800
 
 
10801
            // handle node collection
 
10802
            if ($is_collecting) {
 
10803
                if ($is_child) {
 
10804
                    // okay, let's stash the tokens away
 
10805
                    // first token tells us the type of the collection
 
10806
                    switch ($collection[$tag_index]->name) {
 
10807
                        case 'tr':
 
10808
                        case 'tbody':
 
10809
                            $content[] = $collection;
 
10810
                            break;
 
10811
                        case 'caption':
 
10812
                            if ($caption !== false) break;
 
10813
                            $caption = $collection;
 
10814
                            break;
 
10815
                        case 'thead':
 
10816
                        case 'tfoot':
 
10817
                            // access the appropriate variable, $thead or $tfoot
 
10818
                            $var = $collection[$tag_index]->name;
 
10819
                            if ($$var === false) {
 
10820
                                $$var = $collection;
 
10821
                            } else {
 
10822
                                // transmutate the first and less entries into
 
10823
                                // tbody tags, and then put into content
 
10824
                                $collection[$tag_index]->name = 'tbody';
 
10825
                                $collection[count($collection)-1]->name = 'tbody';
 
10826
                                $content[] = $collection;
 
10827
                            }
 
10828
                            break;
 
10829
                         case 'colgroup':
 
10830
                            $cols[] = $collection;
 
10831
                            break;
 
10832
                    }
 
10833
                    $collection = array();
 
10834
                    $is_collecting = false;
 
10835
                    $tag_index = 0;
 
10836
                } else {
 
10837
                    // add the node to the collection
 
10838
                    $collection[] = $token;
 
10839
                }
 
10840
            }
 
10841
 
 
10842
            // terminate
 
10843
            if ($token === false) break;
 
10844
 
 
10845
            if ($is_child) {
 
10846
                // determine what we're dealing with
 
10847
                if ($token->name == 'col') {
 
10848
                    // the only empty tag in the possie, we can handle it
 
10849
                    // immediately
 
10850
                    $cols[] = array_merge($collection, array($token));
 
10851
                    $collection = array();
 
10852
                    $tag_index = 0;
 
10853
                    continue;
 
10854
                }
 
10855
                switch($token->name) {
 
10856
                    case 'caption':
 
10857
                    case 'colgroup':
 
10858
                    case 'thead':
 
10859
                    case 'tfoot':
 
10860
                    case 'tbody':
 
10861
                    case 'tr':
 
10862
                        $is_collecting = true;
 
10863
                        $collection[] = $token;
 
10864
                        continue;
 
10865
                    default:
 
10866
                        if (!empty($token->is_whitespace)) {
 
10867
                            $collection[] = $token;
 
10868
                            $tag_index++;
 
10869
                        }
 
10870
                        continue;
 
10871
                }
 
10872
            }
 
10873
        }
 
10874
 
 
10875
        if (empty($content)) return false;
 
10876
 
 
10877
        $ret = array();
 
10878
        if ($caption !== false) $ret = array_merge($ret, $caption);
 
10879
        if ($cols !== false)    foreach ($cols as $token_array) $ret = array_merge($ret, $token_array);
 
10880
        if ($thead !== false)   $ret = array_merge($ret, $thead);
 
10881
        if ($tfoot !== false)   $ret = array_merge($ret, $tfoot);
 
10882
        foreach ($content as $token_array) $ret = array_merge($ret, $token_array);
 
10883
        if (!empty($collection) && $is_collecting == false){
 
10884
            // grab the trailing space
 
10885
            $ret = array_merge($ret, $collection);
 
10886
        }
 
10887
 
 
10888
        array_pop($tokens_of_children); // remove phantom token
 
10889
 
 
10890
        return ($ret === $tokens_of_children) ? true : $ret;
 
10891
 
 
10892
    }
 
10893
}
 
10894
 
 
10895
 
 
10896
 
 
10897
 
 
10898
 
 
10899
class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache
 
10900
{
 
10901
 
 
10902
    /**
 
10903
     * Cache object we are decorating
 
10904
     */
 
10905
    public $cache;
 
10906
 
 
10907
    public function __construct() {}
 
10908
 
 
10909
    /**
 
10910
     * Lazy decorator function
 
10911
     * @param $cache Reference to cache object to decorate
 
10912
     */
 
10913
    public function decorate(&$cache) {
 
10914
        $decorator = $this->copy();
 
10915
        // reference is necessary for mocks in PHP 4
 
10916
        $decorator->cache =& $cache;
 
10917
        $decorator->type  = $cache->type;
 
10918
        return $decorator;
 
10919
    }
 
10920
 
 
10921
    /**
 
10922
     * Cross-compatible clone substitute
 
10923
     */
 
10924
    public function copy() {
 
10925
        return new HTMLPurifier_DefinitionCache_Decorator();
 
10926
    }
 
10927
 
 
10928
    public function add($def, $config) {
 
10929
        return $this->cache->add($def, $config);
 
10930
    }
 
10931
 
 
10932
    public function set($def, $config) {
 
10933
        return $this->cache->set($def, $config);
 
10934
    }
 
10935
 
 
10936
    public function replace($def, $config) {
 
10937
        return $this->cache->replace($def, $config);
 
10938
    }
 
10939
 
 
10940
    public function get($config) {
 
10941
        return $this->cache->get($config);
 
10942
    }
 
10943
 
 
10944
    public function remove($config) {
 
10945
        return $this->cache->remove($config);
 
10946
    }
 
10947
 
 
10948
    public function flush($config) {
 
10949
        return $this->cache->flush($config);
 
10950
    }
 
10951
 
 
10952
    public function cleanup($config) {
 
10953
        return $this->cache->cleanup($config);
 
10954
    }
 
10955
 
 
10956
}
 
10957
 
 
10958
 
 
10959
 
 
10960
 
 
10961
 
 
10962
/**
 
10963
 * Null cache object to use when no caching is on.
 
10964
 */
 
10965
class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache
 
10966
{
 
10967
 
 
10968
    public function add($def, $config) {
 
10969
        return false;
 
10970
    }
 
10971
 
 
10972
    public function set($def, $config) {
 
10973
        return false;
 
10974
    }
 
10975
 
 
10976
    public function replace($def, $config) {
 
10977
        return false;
 
10978
    }
 
10979
 
 
10980
    public function remove($config) {
 
10981
        return false;
 
10982
    }
 
10983
 
 
10984
    public function get($config) {
 
10985
        return false;
 
10986
    }
 
10987
 
 
10988
    public function flush($config) {
 
10989
        return false;
 
10990
    }
 
10991
 
 
10992
    public function cleanup($config) {
 
10993
        return false;
 
10994
    }
 
10995
 
 
10996
}
 
10997
 
 
10998
 
 
10999
 
 
11000
 
 
11001
 
 
11002
class HTMLPurifier_DefinitionCache_Serializer extends
 
11003
      HTMLPurifier_DefinitionCache
 
11004
{
 
11005
 
 
11006
    public function add($def, $config) {
 
11007
        if (!$this->checkDefType($def)) return;
 
11008
        $file = $this->generateFilePath($config);
 
11009
        if (file_exists($file)) return false;
 
11010
        if (!$this->_prepareDir($config)) return false;
 
11011
        return $this->_write($file, serialize($def));
 
11012
    }
 
11013
 
 
11014
    public function set($def, $config) {
 
11015
        if (!$this->checkDefType($def)) return;
 
11016
        $file = $this->generateFilePath($config);
 
11017
        if (!$this->_prepareDir($config)) return false;
 
11018
        return $this->_write($file, serialize($def));
 
11019
    }
 
11020
 
 
11021
    public function replace($def, $config) {
 
11022
        if (!$this->checkDefType($def)) return;
 
11023
        $file = $this->generateFilePath($config);
 
11024
        if (!file_exists($file)) return false;
 
11025
        if (!$this->_prepareDir($config)) return false;
 
11026
        return $this->_write($file, serialize($def));
 
11027
    }
 
11028
 
 
11029
    public function get($config) {
 
11030
        $file = $this->generateFilePath($config);
 
11031
        if (!file_exists($file)) return false;
 
11032
        return unserialize(file_get_contents($file));
 
11033
    }
 
11034
 
 
11035
    public function remove($config) {
 
11036
        $file = $this->generateFilePath($config);
 
11037
        if (!file_exists($file)) return false;
 
11038
        return unlink($file);
 
11039
    }
 
11040
 
 
11041
    public function flush($config) {
 
11042
        if (!$this->_prepareDir($config)) return false;
 
11043
        $dir = $this->generateDirectoryPath($config);
 
11044
        $dh  = opendir($dir);
 
11045
        while (false !== ($filename = readdir($dh))) {
 
11046
            if (empty($filename)) continue;
 
11047
            if ($filename[0] === '.') continue;
 
11048
            unlink($dir . '/' . $filename);
 
11049
        }
 
11050
    }
 
11051
 
 
11052
    public function cleanup($config) {
 
11053
        if (!$this->_prepareDir($config)) return false;
 
11054
        $dir = $this->generateDirectoryPath($config);
 
11055
        $dh  = opendir($dir);
 
11056
        while (false !== ($filename = readdir($dh))) {
 
11057
            if (empty($filename)) continue;
 
11058
            if ($filename[0] === '.') continue;
 
11059
            $key = substr($filename, 0, strlen($filename) - 4);
 
11060
            if ($this->isOld($key, $config)) unlink($dir . '/' . $filename);
 
11061
        }
 
11062
    }
 
11063
 
 
11064
    /**
 
11065
     * Generates the file path to the serial file corresponding to
 
11066
     * the configuration and definition name
 
11067
     * @todo Make protected
 
11068
     */
 
11069
    public function generateFilePath($config) {
 
11070
        $key = $this->generateKey($config);
 
11071
        return $this->generateDirectoryPath($config) . '/' . $key . '.ser';
 
11072
    }
 
11073
 
 
11074
    /**
 
11075
     * Generates the path to the directory contain this cache's serial files
 
11076
     * @note No trailing slash
 
11077
     * @todo Make protected
 
11078
     */
 
11079
    public function generateDirectoryPath($config) {
 
11080
        $base = $this->generateBaseDirectoryPath($config);
 
11081
        return $base . '/' . $this->type;
 
11082
    }
 
11083
 
 
11084
    /**
 
11085
     * Generates path to base directory that contains all definition type
 
11086
     * serials
 
11087
     * @todo Make protected
 
11088
     */
 
11089
    public function generateBaseDirectoryPath($config) {
 
11090
        $base = $config->get('Cache.SerializerPath');
 
11091
        $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base;
 
11092
        return $base;
 
11093
    }
 
11094
 
 
11095
    /**
 
11096
     * Convenience wrapper function for file_put_contents
 
11097
     * @param $file File name to write to
 
11098
     * @param $data Data to write into file
 
11099
     * @return Number of bytes written if success, or false if failure.
 
11100
     */
 
11101
    private function _write($file, $data) {
 
11102
        return file_put_contents($file, $data);
 
11103
    }
 
11104
 
 
11105
    /**
 
11106
     * Prepares the directory that this type stores the serials in
 
11107
     * @return True if successful
 
11108
     */
 
11109
    private function _prepareDir($config) {
 
11110
        $directory = $this->generateDirectoryPath($config);
 
11111
        if (!is_dir($directory)) {
 
11112
            $base = $this->generateBaseDirectoryPath($config);
 
11113
            if (!is_dir($base)) {
 
11114
                trigger_error('Base directory '.$base.' does not exist,
 
11115
                    please create or change using %Cache.SerializerPath',
 
11116
                    E_USER_WARNING);
 
11117
                return false;
 
11118
            } elseif (!$this->_testPermissions($base)) {
 
11119
                return false;
 
11120
            }
 
11121
            $old = umask(0022); // disable group and world writes
 
11122
            mkdir($directory);
 
11123
            umask($old);
 
11124
        } elseif (!$this->_testPermissions($directory)) {
 
11125
            return false;
 
11126
        }
 
11127
        return true;
 
11128
    }
 
11129
 
 
11130
    /**
 
11131
     * Tests permissions on a directory and throws out friendly
 
11132
     * error messages and attempts to chmod it itself if possible
 
11133
     */
 
11134
    private function _testPermissions($dir) {
 
11135
        // early abort, if it is writable, everything is hunky-dory
 
11136
        if (is_writable($dir)) return true;
 
11137
        if (!is_dir($dir)) {
 
11138
            // generally, you'll want to handle this beforehand
 
11139
            // so a more specific error message can be given
 
11140
            trigger_error('Directory '.$dir.' does not exist',
 
11141
                E_USER_WARNING);
 
11142
            return false;
 
11143
        }
 
11144
        if (function_exists('posix_getuid')) {
 
11145
            // POSIX system, we can give more specific advice
 
11146
            if (fileowner($dir) === posix_getuid()) {
 
11147
                // we can chmod it ourselves
 
11148
                chmod($dir, 0755);
 
11149
                return true;
 
11150
            } elseif (filegroup($dir) === posix_getgid()) {
 
11151
                $chmod = '775';
 
11152
            } else {
 
11153
                // PHP's probably running as nobody, so we'll
 
11154
                // need to give global permissions
 
11155
                $chmod = '777';
 
11156
            }
 
11157
            trigger_error('Directory '.$dir.' not writable, '.
 
11158
                'please chmod to ' . $chmod,
 
11159
                E_USER_WARNING);
 
11160
        } else {
 
11161
            // generic error message
 
11162
            trigger_error('Directory '.$dir.' not writable, '.
 
11163
                'please alter file permissions',
 
11164
                E_USER_WARNING);
 
11165
        }
 
11166
        return false;
 
11167
    }
 
11168
 
 
11169
}
 
11170
 
 
11171
 
 
11172
 
 
11173
 
 
11174
 
 
11175
/**
 
11176
 * Definition cache decorator class that cleans up the cache
 
11177
 * whenever there is a cache miss.
 
11178
 */
 
11179
class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends
 
11180
      HTMLPurifier_DefinitionCache_Decorator
 
11181
{
 
11182
 
 
11183
    public $name = 'Cleanup';
 
11184
 
 
11185
    public function copy() {
 
11186
        return new HTMLPurifier_DefinitionCache_Decorator_Cleanup();
 
11187
    }
 
11188
 
 
11189
    public function add($def, $config) {
 
11190
        $status = parent::add($def, $config);
 
11191
        if (!$status) parent::cleanup($config);
 
11192
        return $status;
 
11193
    }
 
11194
 
 
11195
    public function set($def, $config) {
 
11196
        $status = parent::set($def, $config);
 
11197
        if (!$status) parent::cleanup($config);
 
11198
        return $status;
 
11199
    }
 
11200
 
 
11201
    public function replace($def, $config) {
 
11202
        $status = parent::replace($def, $config);
 
11203
        if (!$status) parent::cleanup($config);
 
11204
        return $status;
 
11205
    }
 
11206
 
 
11207
    public function get($config) {
 
11208
        $ret = parent::get($config);
 
11209
        if (!$ret) parent::cleanup($config);
 
11210
        return $ret;
 
11211
    }
 
11212
 
 
11213
}
 
11214
 
 
11215
 
 
11216
 
 
11217
 
 
11218
 
 
11219
/**
 
11220
 * Definition cache decorator class that saves all cache retrievals
 
11221
 * to PHP's memory; good for unit tests or circumstances where
 
11222
 * there are lots of configuration objects floating around.
 
11223
 */
 
11224
class HTMLPurifier_DefinitionCache_Decorator_Memory extends
 
11225
      HTMLPurifier_DefinitionCache_Decorator
 
11226
{
 
11227
 
 
11228
    protected $definitions;
 
11229
    public $name = 'Memory';
 
11230
 
 
11231
    public function copy() {
 
11232
        return new HTMLPurifier_DefinitionCache_Decorator_Memory();
 
11233
    }
 
11234
 
 
11235
    public function add($def, $config) {
 
11236
        $status = parent::add($def, $config);
 
11237
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
 
11238
        return $status;
 
11239
    }
 
11240
 
 
11241
    public function set($def, $config) {
 
11242
        $status = parent::set($def, $config);
 
11243
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
 
11244
        return $status;
 
11245
    }
 
11246
 
 
11247
    public function replace($def, $config) {
 
11248
        $status = parent::replace($def, $config);
 
11249
        if ($status) $this->definitions[$this->generateKey($config)] = $def;
 
11250
        return $status;
 
11251
    }
 
11252
 
 
11253
    public function get($config) {
 
11254
        $key = $this->generateKey($config);
 
11255
        if (isset($this->definitions[$key])) return $this->definitions[$key];
 
11256
        $this->definitions[$key] = parent::get($config);
 
11257
        return $this->definitions[$key];
 
11258
    }
 
11259
 
 
11260
}
 
11261
 
 
11262
 
 
11263
 
 
11264
 
 
11265
 
 
11266
/**
 
11267
 * XHTML 1.1 Bi-directional Text Module, defines elements that
 
11268
 * declare directionality of content. Text Extension Module.
 
11269
 */
 
11270
class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule
 
11271
{
 
11272
 
 
11273
    public $name = 'Bdo';
 
11274
    public $attr_collections = array(
 
11275
        'I18N' => array('dir' => false)
 
11276
    );
 
11277
 
 
11278
    public function setup($config) {
 
11279
        $bdo = $this->addElement(
 
11280
            'bdo', 'Inline', 'Inline', array('Core', 'Lang'),
 
11281
            array(
 
11282
                'dir' => 'Enum#ltr,rtl', // required
 
11283
                // The Abstract Module specification has the attribute
 
11284
                // inclusions wrong for bdo: bdo allows Lang
 
11285
            )
 
11286
        );
 
11287
        $bdo->attr_transform_post['required-dir'] = new HTMLPurifier_AttrTransform_BdoDir();
 
11288
 
 
11289
        $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
 
11290
    }
 
11291
 
 
11292
}
 
11293
 
 
11294
 
 
11295
 
 
11296
 
 
11297
 
 
11298
class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
 
11299
{
 
11300
    public $name = 'CommonAttributes';
 
11301
 
 
11302
    public $attr_collections = array(
 
11303
        'Core' => array(
 
11304
            0 => array('Style'),
 
11305
            // 'xml:space' => false,
 
11306
            'class' => 'Class',
 
11307
            'id' => 'ID',
 
11308
            'title' => 'CDATA',
 
11309
        ),
 
11310
        'Lang' => array(),
 
11311
        'I18N' => array(
 
11312
            0 => array('Lang'), // proprietary, for xml:lang/lang
 
11313
        ),
 
11314
        'Common' => array(
 
11315
            0 => array('Core', 'I18N')
 
11316
        )
 
11317
    );
 
11318
 
 
11319
}
 
11320
 
 
11321
 
 
11322
 
 
11323
 
 
11324
 
 
11325
/**
 
11326
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
 
11327
 * Module.
 
11328
 */
 
11329
class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule
 
11330
{
 
11331
 
 
11332
    public $name = 'Edit';
 
11333
 
 
11334
    public function setup($config) {
 
11335
        $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow';
 
11336
        $attr = array(
 
11337
            'cite' => 'URI',
 
11338
            // 'datetime' => 'Datetime', // not implemented
 
11339
        );
 
11340
        $this->addElement('del', 'Inline', $contents, 'Common', $attr);
 
11341
        $this->addElement('ins', 'Inline', $contents, 'Common', $attr);
 
11342
    }
 
11343
 
 
11344
    // HTML 4.01 specifies that ins/del must not contain block
 
11345
    // elements when used in an inline context, chameleon is
 
11346
    // a complicated workaround to acheive this effect
 
11347
 
 
11348
    // Inline context ! Block context (exclamation mark is
 
11349
    // separator, see getChildDef for parsing)
 
11350
 
 
11351
    public $defines_child_def = true;
 
11352
    public function getChildDef($def) {
 
11353
        if ($def->content_model_type != 'chameleon') return false;
 
11354
        $value = explode('!', $def->content_model);
 
11355
        return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
 
11356
    }
 
11357
 
 
11358
}
 
11359
 
 
11360
 
 
11361
 
 
11362
 
 
11363
 
 
11364
/**
 
11365
 * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4.
 
11366
 */
 
11367
class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
 
11368
{
 
11369
    public $name = 'Forms';
 
11370
    public $safe = false;
 
11371
 
 
11372
    public $content_sets = array(
 
11373
        'Block' => 'Form',
 
11374
        'Inline' => 'Formctrl',
 
11375
    );
 
11376
 
 
11377
    public function setup($config) {
 
11378
        $form = $this->addElement('form', 'Form',
 
11379
          'Required: Heading | List | Block | fieldset', 'Common', array(
 
11380
            'accept' => 'ContentTypes',
 
11381
            'accept-charset' => 'Charsets',
 
11382
            'action*' => 'URI',
 
11383
            'method' => 'Enum#get,post',
 
11384
            // really ContentType, but these two are the only ones used today
 
11385
            'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
 
11386
        ));
 
11387
        $form->excludes = array('form' => true);
 
11388
 
 
11389
        $input = $this->addElement('input', 'Formctrl', 'Empty', 'Common', array(
 
11390
            'accept' => 'ContentTypes',
 
11391
            'accesskey' => 'Character',
 
11392
            'alt' => 'Text',
 
11393
            'checked' => 'Bool#checked',
 
11394
            'disabled' => 'Bool#disabled',
 
11395
            'maxlength' => 'Number',
 
11396
            'name' => 'CDATA',
 
11397
            'readonly' => 'Bool#readonly',
 
11398
            'size' => 'Number',
 
11399
            'src' => 'URI#embeds',
 
11400
            'tabindex' => 'Number',
 
11401
            'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
 
11402
            'value' => 'CDATA',
 
11403
        ));
 
11404
        $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();
 
11405
 
 
11406
        $this->addElement('select', 'Formctrl', 'Required: optgroup | option', 'Common', array(
 
11407
            'disabled' => 'Bool#disabled',
 
11408
            'multiple' => 'Bool#multiple',
 
11409
            'name' => 'CDATA',
 
11410
            'size' => 'Number',
 
11411
            'tabindex' => 'Number',
 
11412
        ));
 
11413
 
 
11414
        $this->addElement('option', false, 'Optional: #PCDATA', 'Common', array(
 
11415
            'disabled' => 'Bool#disabled',
 
11416
            'label' => 'Text',
 
11417
            'selected' => 'Bool#selected',
 
11418
            'value' => 'CDATA',
 
11419
        ));
 
11420
        // It's illegal for there to be more than one selected, but not
 
11421
        // be multiple. Also, no selected means undefined behavior. This might
 
11422
        // be difficult to implement; perhaps an injector, or a context variable.
 
11423
 
 
11424
        $textarea = $this->addElement('textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array(
 
11425
            'accesskey' => 'Character',
 
11426
            'cols*' => 'Number',
 
11427
            'disabled' => 'Bool#disabled',
 
11428
            'name' => 'CDATA',
 
11429
            'readonly' => 'Bool#readonly',
 
11430
            'rows*' => 'Number',
 
11431
            'tabindex' => 'Number',
 
11432
        ));
 
11433
        $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();
 
11434
 
 
11435
        $button = $this->addElement('button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array(
 
11436
            'accesskey' => 'Character',
 
11437
            'disabled' => 'Bool#disabled',
 
11438
            'name' => 'CDATA',
 
11439
            'tabindex' => 'Number',
 
11440
            'type' => 'Enum#button,submit,reset',
 
11441
            'value' => 'CDATA',
 
11442
        ));
 
11443
 
 
11444
        // For exclusions, ideally we'd specify content sets, not literal elements
 
11445
        $button->excludes = $this->makeLookup(
 
11446
            'form', 'fieldset', // Form
 
11447
            'input', 'select', 'textarea', 'label', 'button', // Formctrl
 
11448
            'a' // as per HTML 4.01 spec, this is omitted by modularization
 
11449
        );
 
11450
 
 
11451
        // Extra exclusion: img usemap="" is not permitted within this element.
 
11452
        // We'll omit this for now, since we don't have any good way of
 
11453
        // indicating it yet.
 
11454
 
 
11455
        // This is HIGHLY user-unfriendly; we need a custom child-def for this
 
11456
        $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common');
 
11457
 
 
11458
        $label = $this->addElement('label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array(
 
11459
            'accesskey' => 'Character',
 
11460
            // 'for' => 'IDREF', // IDREF not implemented, cannot allow
 
11461
        ));
 
11462
        $label->excludes = array('label' => true);
 
11463
 
 
11464
        $this->addElement('legend', false, 'Optional: #PCDATA | Inline', 'Common', array(
 
11465
            'accesskey' => 'Character',
 
11466
        ));
 
11467
 
 
11468
        $this->addElement('optgroup', false, 'Required: option', 'Common', array(
 
11469
            'disabled' => 'Bool#disabled',
 
11470
            'label*' => 'Text',
 
11471
        ));
 
11472
 
 
11473
        // Don't forget an injector for <isindex>. This one's a little complex
 
11474
        // because it maps to multiple elements.
 
11475
 
 
11476
    }
 
11477
}
 
11478
 
 
11479
 
 
11480
 
 
11481
 
 
11482
 
 
11483
/**
 
11484
 * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module.
 
11485
 */
 
11486
class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule
 
11487
{
 
11488
 
 
11489
    public $name = 'Hypertext';
 
11490
 
 
11491
    public function setup($config) {
 
11492
        $a = $this->addElement(
 
11493
            'a', 'Inline', 'Inline', 'Common',
 
11494
            array(
 
11495
                // 'accesskey' => 'Character',
 
11496
                // 'charset' => 'Charset',
 
11497
                'href' => 'URI',
 
11498
                // 'hreflang' => 'LanguageCode',
 
11499
                'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
 
11500
                'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
 
11501
                // 'tabindex' => 'Number',
 
11502
                // 'type' => 'ContentType',
 
11503
            )
 
11504
        );
 
11505
        $a->formatting = true;
 
11506
        $a->excludes = array('a' => true);
 
11507
    }
 
11508
 
 
11509
}
 
11510
 
 
11511
 
 
11512
 
 
11513
 
 
11514
 
 
11515
/**
 
11516
 * XHTML 1.1 Image Module provides basic image embedding.
 
11517
 * @note There is specialized code for removing empty images in
 
11518
 *       HTMLPurifier_Strategy_RemoveForeignElements
 
11519
 */
 
11520
class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule
 
11521
{
 
11522
 
 
11523
    public $name = 'Image';
 
11524
 
 
11525
    public function setup($config) {
 
11526
        $max = $config->get('HTML.MaxImgLength');
 
11527
        $img = $this->addElement(
 
11528
            'img', 'Inline', 'Empty', 'Common',
 
11529
            array(
 
11530
                'alt*' => 'Text',
 
11531
                // According to the spec, it's Length, but percents can
 
11532
                // be abused, so we allow only Pixels.
 
11533
                'height' => 'Pixels#' . $max,
 
11534
                'width'  => 'Pixels#' . $max,
 
11535
                'longdesc' => 'URI',
 
11536
                'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
 
11537
            )
 
11538
        );
 
11539
        if ($max === null || $config->get('HTML.Trusted')) {
 
11540
            $img->attr['height'] =
 
11541
            $img->attr['width'] = 'Length';
 
11542
        }
 
11543
 
 
11544
        // kind of strange, but splitting things up would be inefficient
 
11545
        $img->attr_transform_pre[] =
 
11546
        $img->attr_transform_post[] =
 
11547
            new HTMLPurifier_AttrTransform_ImgRequired();
 
11548
    }
 
11549
 
 
11550
}
 
11551
 
 
11552
 
 
11553
 
 
11554
 
 
11555
 
 
11556
/**
 
11557
 * XHTML 1.1 Legacy module defines elements that were previously
 
11558
 * deprecated.
 
11559
 *
 
11560
 * @note Not all legacy elements have been implemented yet, which
 
11561
 *       is a bit of a reverse problem as compared to browsers! In
 
11562
 *       addition, this legacy module may implement a bit more than
 
11563
 *       mandated by XHTML 1.1.
 
11564
 *
 
11565
 * This module can be used in combination with TransformToStrict in order
 
11566
 * to transform as many deprecated elements as possible, but retain
 
11567
 * questionably deprecated elements that do not have good alternatives
 
11568
 * as well as transform elements that don't have an implementation.
 
11569
 * See docs/ref-strictness.txt for more details.
 
11570
 */
 
11571
 
 
11572
class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule
 
11573
{
 
11574
 
 
11575
    public $name = 'Legacy';
 
11576
 
 
11577
    public function setup($config) {
 
11578
 
 
11579
        $this->addElement('basefont', 'Inline', 'Empty', false, array(
 
11580
            'color' => 'Color',
 
11581
            'face' => 'Text', // extremely broad, we should
 
11582
            'size' => 'Text', // tighten it
 
11583
            'id' => 'ID'
 
11584
        ));
 
11585
        $this->addElement('center', 'Block', 'Flow', 'Common');
 
11586
        $this->addElement('dir', 'Block', 'Required: li', 'Common', array(
 
11587
            'compact' => 'Bool#compact'
 
11588
        ));
 
11589
        $this->addElement('font', 'Inline', 'Inline', array('Core', 'I18N'), array(
 
11590
            'color' => 'Color',
 
11591
            'face' => 'Text', // extremely broad, we should
 
11592
            'size' => 'Text', // tighten it
 
11593
        ));
 
11594
        $this->addElement('menu', 'Block', 'Required: li', 'Common', array(
 
11595
            'compact' => 'Bool#compact'
 
11596
        ));
 
11597
 
 
11598
        $s = $this->addElement('s', 'Inline', 'Inline', 'Common');
 
11599
        $s->formatting = true;
 
11600
 
 
11601
        $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common');
 
11602
        $strike->formatting = true;
 
11603
 
 
11604
        $u = $this->addElement('u', 'Inline', 'Inline', 'Common');
 
11605
        $u->formatting = true;
 
11606
 
 
11607
        // setup modifications to old elements
 
11608
 
 
11609
        $align = 'Enum#left,right,center,justify';
 
11610
 
 
11611
        $address = $this->addBlankElement('address');
 
11612
        $address->content_model = 'Inline | #PCDATA | p';
 
11613
        $address->content_model_type = 'optional';
 
11614
        $address->child = false;
 
11615
 
 
11616
        $blockquote = $this->addBlankElement('blockquote');
 
11617
        $blockquote->content_model = 'Flow | #PCDATA';
 
11618
        $blockquote->content_model_type = 'optional';
 
11619
        $blockquote->child = false;
 
11620
 
 
11621
        $br = $this->addBlankElement('br');
 
11622
        $br->attr['clear'] = 'Enum#left,all,right,none';
 
11623
 
 
11624
        $caption = $this->addBlankElement('caption');
 
11625
        $caption->attr['align'] = 'Enum#top,bottom,left,right';
 
11626
 
 
11627
        $div = $this->addBlankElement('div');
 
11628
        $div->attr['align'] = $align;
 
11629
 
 
11630
        $dl = $this->addBlankElement('dl');
 
11631
        $dl->attr['compact'] = 'Bool#compact';
 
11632
 
 
11633
        for ($i = 1; $i <= 6; $i++) {
 
11634
            $h = $this->addBlankElement("h$i");
 
11635
            $h->attr['align'] = $align;
 
11636
        }
 
11637
 
 
11638
        $hr = $this->addBlankElement('hr');
 
11639
        $hr->attr['align'] = $align;
 
11640
        $hr->attr['noshade'] = 'Bool#noshade';
 
11641
        $hr->attr['size'] = 'Pixels';
 
11642
        $hr->attr['width'] = 'Length';
 
11643
 
 
11644
        $img = $this->addBlankElement('img');
 
11645
        $img->attr['align'] = 'Enum#top,middle,bottom,left,right';
 
11646
        $img->attr['border'] = 'Pixels';
 
11647
        $img->attr['hspace'] = 'Pixels';
 
11648
        $img->attr['vspace'] = 'Pixels';
 
11649
 
 
11650
        // figure out this integer business
 
11651
 
 
11652
        $li = $this->addBlankElement('li');
 
11653
        $li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
 
11654
        $li->attr['type']  = 'Enum#s:1,i,I,a,A,disc,square,circle';
 
11655
 
 
11656
        $ol = $this->addBlankElement('ol');
 
11657
        $ol->attr['compact'] = 'Bool#compact';
 
11658
        $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
 
11659
        $ol->attr['type'] = 'Enum#s:1,i,I,a,A';
 
11660
 
 
11661
        $p = $this->addBlankElement('p');
 
11662
        $p->attr['align'] = $align;
 
11663
 
 
11664
        $pre = $this->addBlankElement('pre');
 
11665
        $pre->attr['width'] = 'Number';
 
11666
 
 
11667
        // script omitted
 
11668
 
 
11669
        $table = $this->addBlankElement('table');
 
11670
        $table->attr['align'] = 'Enum#left,center,right';
 
11671
        $table->attr['bgcolor'] = 'Color';
 
11672
 
 
11673
        $tr = $this->addBlankElement('tr');
 
11674
        $tr->attr['bgcolor'] = 'Color';
 
11675
 
 
11676
        $th = $this->addBlankElement('th');
 
11677
        $th->attr['bgcolor'] = 'Color';
 
11678
        $th->attr['height'] = 'Length';
 
11679
        $th->attr['nowrap'] = 'Bool#nowrap';
 
11680
        $th->attr['width'] = 'Length';
 
11681
 
 
11682
        $td = $this->addBlankElement('td');
 
11683
        $td->attr['bgcolor'] = 'Color';
 
11684
        $td->attr['height'] = 'Length';
 
11685
        $td->attr['nowrap'] = 'Bool#nowrap';
 
11686
        $td->attr['width'] = 'Length';
 
11687
 
 
11688
        $ul = $this->addBlankElement('ul');
 
11689
        $ul->attr['compact'] = 'Bool#compact';
 
11690
        $ul->attr['type'] = 'Enum#square,disc,circle';
 
11691
 
 
11692
    }
 
11693
 
 
11694
}
 
11695
 
 
11696
 
 
11697
 
 
11698
 
 
11699
 
 
11700
/**
 
11701
 * XHTML 1.1 List Module, defines list-oriented elements. Core Module.
 
11702
 */
 
11703
class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule
 
11704
{
 
11705
 
 
11706
    public $name = 'List';
 
11707
 
 
11708
    // According to the abstract schema, the List content set is a fully formed
 
11709
    // one or more expr, but it invariably occurs in an optional declaration
 
11710
    // so we're not going to do that subtlety. It might cause trouble
 
11711
    // if a user defines "List" and expects that multiple lists are
 
11712
    // allowed to be specified, but then again, that's not very intuitive.
 
11713
    // Furthermore, the actual XML Schema may disagree. Regardless,
 
11714
    // we don't have support for such nested expressions without using
 
11715
    // the incredibly inefficient and draconic Custom ChildDef.
 
11716
 
 
11717
    public $content_sets = array('Flow' => 'List');
 
11718
 
 
11719
    public function setup($config) {
 
11720
        $this->addElement('ol', 'List', 'Required: li', 'Common');
 
11721
        $this->addElement('ul', 'List', 'Required: li', 'Common');
 
11722
        $this->addElement('dl', 'List', 'Required: dt | dd', 'Common');
 
11723
 
 
11724
        $this->addElement('li', false, 'Flow', 'Common');
 
11725
 
 
11726
        $this->addElement('dd', false, 'Flow', 'Common');
 
11727
        $this->addElement('dt', false, 'Inline', 'Common');
 
11728
    }
 
11729
 
 
11730
}
 
11731
 
 
11732
 
 
11733
 
 
11734
 
 
11735
 
 
11736
class HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule
 
11737
{
 
11738
 
 
11739
    public $name = 'Name';
 
11740
 
 
11741
    public function setup($config) {
 
11742
        $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map');
 
11743
        foreach ($elements as $name) {
 
11744
            $element = $this->addBlankElement($name);
 
11745
            $element->attr['name'] = 'CDATA';
 
11746
            if (!$config->get('HTML.Attr.Name.UseCDATA')) {
 
11747
                $element->attr_transform_post['NameSync'] = new HTMLPurifier_AttrTransform_NameSync();
 
11748
            }
 
11749
        }
 
11750
    }
 
11751
 
 
11752
}
 
11753
 
 
11754
 
 
11755
 
 
11756
 
 
11757
 
 
11758
class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule
 
11759
{
 
11760
    public $name = 'NonXMLCommonAttributes';
 
11761
 
 
11762
    public $attr_collections = array(
 
11763
        'Lang' => array(
 
11764
            'lang' => 'LanguageCode',
 
11765
        )
 
11766
    );
 
11767
}
 
11768
 
 
11769
 
 
11770
 
 
11771
 
 
11772
 
 
11773
/**
 
11774
 * XHTML 1.1 Object Module, defines elements for generic object inclusion
 
11775
 * @warning Users will commonly use <embed> to cater to legacy browsers: this
 
11776
 *      module does not allow this sort of behavior
 
11777
 */
 
11778
class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
 
11779
{
 
11780
 
 
11781
    public $name = 'Object';
 
11782
    public $safe = false;
 
11783
 
 
11784
    public function setup($config) {
 
11785
 
 
11786
        $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common',
 
11787
            array(
 
11788
                'archive' => 'URI',
 
11789
                'classid' => 'URI',
 
11790
                'codebase' => 'URI',
 
11791
                'codetype' => 'Text',
 
11792
                'data' => 'URI',
 
11793
                'declare' => 'Bool#declare',
 
11794
                'height' => 'Length',
 
11795
                'name' => 'CDATA',
 
11796
                'standby' => 'Text',
 
11797
                'tabindex' => 'Number',
 
11798
                'type' => 'ContentType',
 
11799
                'width' => 'Length'
 
11800
            )
 
11801
        );
 
11802
 
 
11803
        $this->addElement('param', false, 'Empty', false,
 
11804
            array(
 
11805
                'id' => 'ID',
 
11806
                'name*' => 'Text',
 
11807
                'type' => 'Text',
 
11808
                'value' => 'Text',
 
11809
                'valuetype' => 'Enum#data,ref,object'
 
11810
           )
 
11811
        );
 
11812
 
 
11813
    }
 
11814
 
 
11815
}
 
11816
 
 
11817
 
 
11818
 
 
11819
 
 
11820
 
 
11821
/**
 
11822
 * XHTML 1.1 Presentation Module, defines simple presentation-related
 
11823
 * markup. Text Extension Module.
 
11824
 * @note The official XML Schema and DTD specs further divide this into
 
11825
 *       two modules:
 
11826
 *          - Block Presentation (hr)
 
11827
 *          - Inline Presentation (b, big, i, small, sub, sup, tt)
 
11828
 *       We have chosen not to heed this distinction, as content_sets
 
11829
 *       provides satisfactory disambiguation.
 
11830
 */
 
11831
class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule
 
11832
{
 
11833
 
 
11834
    public $name = 'Presentation';
 
11835
 
 
11836
    public function setup($config) {
 
11837
        $this->addElement('hr',     'Block',  'Empty',  'Common');
 
11838
        $this->addElement('sub',    'Inline', 'Inline', 'Common');
 
11839
        $this->addElement('sup',    'Inline', 'Inline', 'Common');
 
11840
        $b = $this->addElement('b',      'Inline', 'Inline', 'Common');
 
11841
        $b->formatting = true;
 
11842
        $big = $this->addElement('big',    'Inline', 'Inline', 'Common');
 
11843
        $big->formatting = true;
 
11844
        $i = $this->addElement('i',      'Inline', 'Inline', 'Common');
 
11845
        $i->formatting = true;
 
11846
        $small = $this->addElement('small',  'Inline', 'Inline', 'Common');
 
11847
        $small->formatting = true;
 
11848
        $tt = $this->addElement('tt',     'Inline', 'Inline', 'Common');
 
11849
        $tt->formatting = true;
 
11850
    }
 
11851
 
 
11852
}
 
11853
 
 
11854
 
 
11855
 
 
11856
 
 
11857
 
 
11858
/**
 
11859
 * Module defines proprietary tags and attributes in HTML.
 
11860
 * @warning If this module is enabled, standards-compliance is off!
 
11861
 */
 
11862
class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule
 
11863
{
 
11864
 
 
11865
    public $name = 'Proprietary';
 
11866
 
 
11867
    public function setup($config) {
 
11868
 
 
11869
        $this->addElement('marquee', 'Inline', 'Flow', 'Common',
 
11870
            array(
 
11871
                'direction' => 'Enum#left,right,up,down',
 
11872
                'behavior' => 'Enum#alternate',
 
11873
                'width' => 'Length',
 
11874
                'height' => 'Length',
 
11875
                'scrolldelay' => 'Number',
 
11876
                'scrollamount' => 'Number',
 
11877
                'loop' => 'Number',
 
11878
                'bgcolor' => 'Color',
 
11879
                'hspace' => 'Pixels',
 
11880
                'vspace' => 'Pixels',
 
11881
            )
 
11882
        );
 
11883
 
 
11884
    }
 
11885
 
 
11886
}
 
11887
 
 
11888
 
 
11889
 
 
11890
 
 
11891
 
 
11892
/**
 
11893
 * XHTML 1.1 Ruby Annotation Module, defines elements that indicate
 
11894
 * short runs of text alongside base text for annotation or pronounciation.
 
11895
 */
 
11896
class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule
 
11897
{
 
11898
 
 
11899
    public $name = 'Ruby';
 
11900
 
 
11901
    public function setup($config) {
 
11902
        $this->addElement('ruby', 'Inline',
 
11903
            'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
 
11904
            'Common');
 
11905
        $this->addElement('rbc', false, 'Required: rb', 'Common');
 
11906
        $this->addElement('rtc', false, 'Required: rt', 'Common');
 
11907
        $rb = $this->addElement('rb', false, 'Inline', 'Common');
 
11908
        $rb->excludes = array('ruby' => true);
 
11909
        $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number'));
 
11910
        $rt->excludes = array('ruby' => true);
 
11911
        $this->addElement('rp', false, 'Optional: #PCDATA', 'Common');
 
11912
    }
 
11913
 
 
11914
}
 
11915
 
 
11916
 
 
11917
 
 
11918
 
 
11919
 
 
11920
/**
 
11921
 * A "safe" embed module. See SafeObject. This is a proprietary element.
 
11922
 */
 
11923
class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
 
11924
{
 
11925
 
 
11926
    public $name = 'SafeEmbed';
 
11927
 
 
11928
    public function setup($config) {
 
11929
 
 
11930
        $max = $config->get('HTML.MaxImgLength');
 
11931
        $embed = $this->addElement(
 
11932
            'embed', 'Inline', 'Empty', 'Common',
 
11933
            array(
 
11934
                'src*' => 'URI#embedded',
 
11935
                'type' => 'Enum#application/x-shockwave-flash',
 
11936
                'width' => 'Pixels#' . $max,
 
11937
                'height' => 'Pixels#' . $max,
 
11938
                'allowscriptaccess' => 'Enum#never',
 
11939
                'allownetworking' => 'Enum#internal',
 
11940
                'wmode' => 'Enum#window',
 
11941
                'name' => 'ID',
 
11942
            )
 
11943
        );
 
11944
        $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
 
11945
 
 
11946
    }
 
11947
 
 
11948
}
 
11949
 
 
11950
 
 
11951
 
 
11952
 
 
11953
 
 
11954
/**
 
11955
 * A "safe" object module. In theory, objects permitted by this module will
 
11956
 * be safe, and untrusted users can be allowed to embed arbitrary flash objects
 
11957
 * (maybe other types too, but only Flash is supported as of right now).
 
11958
 * Highly experimental.
 
11959
 */
 
11960
class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
 
11961
{
 
11962
 
 
11963
    public $name = 'SafeObject';
 
11964
 
 
11965
    public function setup($config) {
 
11966
 
 
11967
        // These definitions are not intrinsically safe: the attribute transforms
 
11968
        // are a vital part of ensuring safety.
 
11969
 
 
11970
        $max = $config->get('HTML.MaxImgLength');
 
11971
        $object = $this->addElement(
 
11972
            'object',
 
11973
            'Inline',
 
11974
            'Optional: param | Flow | #PCDATA',
 
11975
            'Common',
 
11976
            array(
 
11977
                // While technically not required by the spec, we're forcing
 
11978
                // it to this value.
 
11979
                'type'   => 'Enum#application/x-shockwave-flash',
 
11980
                'width'  => 'Pixels#' . $max,
 
11981
                'height' => 'Pixels#' . $max,
 
11982
                'data'   => 'URI#embedded'
 
11983
            )
 
11984
        );
 
11985
        $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();
 
11986
 
 
11987
        $param = $this->addElement('param', false, 'Empty', false,
 
11988
            array(
 
11989
                'id' => 'ID',
 
11990
                'name*' => 'Text',
 
11991
                'value' => 'Text'
 
11992
            )
 
11993
        );
 
11994
        $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
 
11995
        $this->info_injector[] = 'SafeObject';
 
11996
 
 
11997
    }
 
11998
 
 
11999
}
 
12000
 
 
12001
 
 
12002
 
 
12003
 
 
12004
 
 
12005
/*
 
12006
 
 
12007
WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
 
12008
INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!
 
12009
 
 
12010
*/
 
12011
 
 
12012
/**
 
12013
 * XHTML 1.1 Scripting module, defines elements that are used to contain
 
12014
 * information pertaining to executable scripts or the lack of support
 
12015
 * for executable scripts.
 
12016
 * @note This module does not contain inline scripting elements
 
12017
 */
 
12018
class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule
 
12019
{
 
12020
    public $name = 'Scripting';
 
12021
    public $elements = array('script', 'noscript');
 
12022
    public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript');
 
12023
    public $safe = false;
 
12024
 
 
12025
    public function setup($config) {
 
12026
        // TODO: create custom child-definition for noscript that
 
12027
        // auto-wraps stray #PCDATA in a similar manner to
 
12028
        // blockquote's custom definition (we would use it but
 
12029
        // blockquote's contents are optional while noscript's contents
 
12030
        // are required)
 
12031
 
 
12032
        // TODO: convert this to new syntax, main problem is getting
 
12033
        // both content sets working
 
12034
 
 
12035
        // In theory, this could be safe, but I don't see any reason to
 
12036
        // allow it.
 
12037
        $this->info['noscript'] = new HTMLPurifier_ElementDef();
 
12038
        $this->info['noscript']->attr = array( 0 => array('Common') );
 
12039
        $this->info['noscript']->content_model = 'Heading | List | Block';
 
12040
        $this->info['noscript']->content_model_type = 'required';
 
12041
 
 
12042
        $this->info['script'] = new HTMLPurifier_ElementDef();
 
12043
        $this->info['script']->attr = array(
 
12044
            'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
 
12045
            'src'   => new HTMLPurifier_AttrDef_URI(true),
 
12046
            'type'  => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
 
12047
        );
 
12048
        $this->info['script']->content_model = '#PCDATA';
 
12049
        $this->info['script']->content_model_type = 'optional';
 
12050
        $this->info['script']->attr_transform_pre['type'] =
 
12051
        $this->info['script']->attr_transform_post['type'] =
 
12052
            new HTMLPurifier_AttrTransform_ScriptRequired();
 
12053
    }
 
12054
}
 
12055
 
 
12056
 
 
12057
 
 
12058
 
 
12059
 
 
12060
/**
 
12061
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
 
12062
 * Module.
 
12063
 */
 
12064
class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule
 
12065
{
 
12066
 
 
12067
    public $name = 'StyleAttribute';
 
12068
    public $attr_collections = array(
 
12069
        // The inclusion routine differs from the Abstract Modules but
 
12070
        // is in line with the DTD and XML Schemas.
 
12071
        'Style' => array('style' => false), // see constructor
 
12072
        'Core' => array(0 => array('Style'))
 
12073
    );
 
12074
 
 
12075
    public function setup($config) {
 
12076
        $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
 
12077
    }
 
12078
 
 
12079
}
 
12080
 
 
12081
 
 
12082
 
 
12083
 
 
12084
 
 
12085
/**
 
12086
 * XHTML 1.1 Tables Module, fully defines accessible table elements.
 
12087
 */
 
12088
class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule
 
12089
{
 
12090
 
 
12091
    public $name = 'Tables';
 
12092
 
 
12093
    public function setup($config) {
 
12094
 
 
12095
        $this->addElement('caption', false, 'Inline', 'Common');
 
12096
 
 
12097
        $this->addElement('table', 'Block',
 
12098
            new HTMLPurifier_ChildDef_Table(),  'Common',
 
12099
            array(
 
12100
                'border' => 'Pixels',
 
12101
                'cellpadding' => 'Length',
 
12102
                'cellspacing' => 'Length',
 
12103
                'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
 
12104
                'rules' => 'Enum#none,groups,rows,cols,all',
 
12105
                'summary' => 'Text',
 
12106
                'width' => 'Length'
 
12107
            )
 
12108
        );
 
12109
 
 
12110
        // common attributes
 
12111
        $cell_align = array(
 
12112
            'align' => 'Enum#left,center,right,justify,char',
 
12113
            'charoff' => 'Length',
 
12114
            'valign' => 'Enum#top,middle,bottom,baseline',
 
12115
        );
 
12116
 
 
12117
        $cell_t = array_merge(
 
12118
            array(
 
12119
                'abbr'    => 'Text',
 
12120
                'colspan' => 'Number',
 
12121
                'rowspan' => 'Number',
 
12122
            ),
 
12123
            $cell_align
 
12124
        );
 
12125
        $this->addElement('td', false, 'Flow', 'Common', $cell_t);
 
12126
        $this->addElement('th', false, 'Flow', 'Common', $cell_t);
 
12127
 
 
12128
        $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align);
 
12129
 
 
12130
        $cell_col = array_merge(
 
12131
            array(
 
12132
                'span'  => 'Number',
 
12133
                'width' => 'MultiLength',
 
12134
            ),
 
12135
            $cell_align
 
12136
        );
 
12137
        $this->addElement('col',      false, 'Empty',         'Common', $cell_col);
 
12138
        $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col);
 
12139
 
 
12140
        $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align);
 
12141
        $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align);
 
12142
        $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align);
 
12143
 
 
12144
    }
 
12145
 
 
12146
}
 
12147
 
 
12148
 
 
12149
 
 
12150
 
 
12151
 
 
12152
/**
 
12153
 * XHTML 1.1 Target Module, defines target attribute in link elements.
 
12154
 */
 
12155
class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule
 
12156
{
 
12157
 
 
12158
    public $name = 'Target';
 
12159
 
 
12160
    public function setup($config) {
 
12161
        $elements = array('a');
 
12162
        foreach ($elements as $name) {
 
12163
            $e = $this->addBlankElement($name);
 
12164
            $e->attr = array(
 
12165
                'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
 
12166
            );
 
12167
        }
 
12168
    }
 
12169
 
 
12170
}
 
12171
 
 
12172
 
 
12173
 
 
12174
 
 
12175
 
 
12176
/**
 
12177
 * XHTML 1.1 Text Module, defines basic text containers. Core Module.
 
12178
 * @note In the normative XML Schema specification, this module
 
12179
 *       is further abstracted into the following modules:
 
12180
 *          - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6)
 
12181
 *          - Block Structural (div, p)
 
12182
 *          - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var)
 
12183
 *          - Inline Structural (br, span)
 
12184
 *       This module, functionally, does not distinguish between these
 
12185
 *       sub-modules, but the code is internally structured to reflect
 
12186
 *       these distinctions.
 
12187
 */
 
12188
class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule
 
12189
{
 
12190
 
 
12191
    public $name = 'Text';
 
12192
    public $content_sets = array(
 
12193
        'Flow' => 'Heading | Block | Inline'
 
12194
    );
 
12195
 
 
12196
    public function setup($config) {
 
12197
 
 
12198
        // Inline Phrasal -------------------------------------------------
 
12199
        $this->addElement('abbr',    'Inline', 'Inline', 'Common');
 
12200
        $this->addElement('acronym', 'Inline', 'Inline', 'Common');
 
12201
        $this->addElement('cite',    'Inline', 'Inline', 'Common');
 
12202
        $this->addElement('dfn',     'Inline', 'Inline', 'Common');
 
12203
        $this->addElement('kbd',     'Inline', 'Inline', 'Common');
 
12204
        $this->addElement('q',       'Inline', 'Inline', 'Common', array('cite' => 'URI'));
 
12205
        $this->addElement('samp',    'Inline', 'Inline', 'Common');
 
12206
        $this->addElement('var',     'Inline', 'Inline', 'Common');
 
12207
 
 
12208
        $em = $this->addElement('em',      'Inline', 'Inline', 'Common');
 
12209
        $em->formatting = true;
 
12210
 
 
12211
        $strong = $this->addElement('strong',  'Inline', 'Inline', 'Common');
 
12212
        $strong->formatting = true;
 
12213
 
 
12214
        $code = $this->addElement('code',    'Inline', 'Inline', 'Common');
 
12215
        $code->formatting = true;
 
12216
 
 
12217
        // Inline Structural ----------------------------------------------
 
12218
        $this->addElement('span', 'Inline', 'Inline', 'Common');
 
12219
        $this->addElement('br',   'Inline', 'Empty',  'Core');
 
12220
 
 
12221
        // Block Phrasal --------------------------------------------------
 
12222
        $this->addElement('address',     'Block', 'Inline', 'Common');
 
12223
        $this->addElement('blockquote',  'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') );
 
12224
        $pre = $this->addElement('pre', 'Block', 'Inline', 'Common');
 
12225
        $pre->excludes = $this->makeLookup(
 
12226
            'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' );
 
12227
        $this->addElement('h1', 'Heading', 'Inline', 'Common');
 
12228
        $this->addElement('h2', 'Heading', 'Inline', 'Common');
 
12229
        $this->addElement('h3', 'Heading', 'Inline', 'Common');
 
12230
        $this->addElement('h4', 'Heading', 'Inline', 'Common');
 
12231
        $this->addElement('h5', 'Heading', 'Inline', 'Common');
 
12232
        $this->addElement('h6', 'Heading', 'Inline', 'Common');
 
12233
 
 
12234
        // Block Structural -----------------------------------------------
 
12235
        $p = $this->addElement('p', 'Block', 'Inline', 'Common');
 
12236
        $p->autoclose = array_flip(array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul"));
 
12237
 
 
12238
        $this->addElement('div', 'Block', 'Flow', 'Common');
 
12239
 
 
12240
    }
 
12241
 
 
12242
}
 
12243
 
 
12244
 
 
12245
 
 
12246
 
 
12247
 
 
12248
/**
 
12249
 * Abstract class for a set of proprietary modules that clean up (tidy)
 
12250
 * poorly written HTML.
 
12251
 * @todo Figure out how to protect some of these methods/properties
 
12252
 */
 
12253
class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
 
12254
{
 
12255
 
 
12256
    /**
 
12257
     * List of supported levels. Index zero is a special case "no fixes"
 
12258
     * level.
 
12259
     */
 
12260
    public $levels = array(0 => 'none', 'light', 'medium', 'heavy');
 
12261
 
 
12262
    /**
 
12263
     * Default level to place all fixes in. Disabled by default
 
12264
     */
 
12265
    public $defaultLevel = null;
 
12266
 
 
12267
    /**
 
12268
     * Lists of fixes used by getFixesForLevel(). Format is:
 
12269
     *      HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
 
12270
     */
 
12271
    public $fixesForLevel = array(
 
12272
        'light'  => array(),
 
12273
        'medium' => array(),
 
12274
        'heavy'  => array()
 
12275
    );
 
12276
 
 
12277
    /**
 
12278
     * Lazy load constructs the module by determining the necessary
 
12279
     * fixes to create and then delegating to the populate() function.
 
12280
     * @todo Wildcard matching and error reporting when an added or
 
12281
     *       subtracted fix has no effect.
 
12282
     */
 
12283
    public function setup($config) {
 
12284
 
 
12285
        // create fixes, initialize fixesForLevel
 
12286
        $fixes = $this->makeFixes();
 
12287
        $this->makeFixesForLevel($fixes);
 
12288
 
 
12289
        // figure out which fixes to use
 
12290
        $level = $config->get('HTML.TidyLevel');
 
12291
        $fixes_lookup = $this->getFixesForLevel($level);
 
12292
 
 
12293
        // get custom fix declarations: these need namespace processing
 
12294
        $add_fixes    = $config->get('HTML.TidyAdd');
 
12295
        $remove_fixes = $config->get('HTML.TidyRemove');
 
12296
 
 
12297
        foreach ($fixes as $name => $fix) {
 
12298
            // needs to be refactored a little to implement globbing
 
12299
            if (
 
12300
                isset($remove_fixes[$name]) ||
 
12301
                (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))
 
12302
            ) {
 
12303
                unset($fixes[$name]);
 
12304
            }
 
12305
        }
 
12306
 
 
12307
        // populate this module with necessary fixes
 
12308
        $this->populate($fixes);
 
12309
 
 
12310
    }
 
12311
 
 
12312
    /**
 
12313
     * Retrieves all fixes per a level, returning fixes for that specific
 
12314
     * level as well as all levels below it.
 
12315
     * @param $level String level identifier, see $levels for valid values
 
12316
     * @return Lookup up table of fixes
 
12317
     */
 
12318
    public function getFixesForLevel($level) {
 
12319
        if ($level == $this->levels[0]) {
 
12320
            return array();
 
12321
        }
 
12322
        $activated_levels = array();
 
12323
        for ($i = 1, $c = count($this->levels); $i < $c; $i++) {
 
12324
            $activated_levels[] = $this->levels[$i];
 
12325
            if ($this->levels[$i] == $level) break;
 
12326
        }
 
12327
        if ($i == $c) {
 
12328
            trigger_error(
 
12329
                'Tidy level ' . htmlspecialchars($level) . ' not recognized',
 
12330
                E_USER_WARNING
 
12331
            );
 
12332
            return array();
 
12333
        }
 
12334
        $ret = array();
 
12335
        foreach ($activated_levels as $level) {
 
12336
            foreach ($this->fixesForLevel[$level] as $fix) {
 
12337
                $ret[$fix] = true;
 
12338
            }
 
12339
        }
 
12340
        return $ret;
 
12341
    }
 
12342
 
 
12343
    /**
 
12344
     * Dynamically populates the $fixesForLevel member variable using
 
12345
     * the fixes array. It may be custom overloaded, used in conjunction
 
12346
     * with $defaultLevel, or not used at all.
 
12347
     */
 
12348
    public function makeFixesForLevel($fixes) {
 
12349
        if (!isset($this->defaultLevel)) return;
 
12350
        if (!isset($this->fixesForLevel[$this->defaultLevel])) {
 
12351
            trigger_error(
 
12352
                'Default level ' . $this->defaultLevel . ' does not exist',
 
12353
                E_USER_ERROR
 
12354
            );
 
12355
            return;
 
12356
        }
 
12357
        $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
 
12358
    }
 
12359
 
 
12360
    /**
 
12361
     * Populates the module with transforms and other special-case code
 
12362
     * based on a list of fixes passed to it
 
12363
     * @param $lookup Lookup table of fixes to activate
 
12364
     */
 
12365
    public function populate($fixes) {
 
12366
        foreach ($fixes as $name => $fix) {
 
12367
            // determine what the fix is for
 
12368
            list($type, $params) = $this->getFixType($name);
 
12369
            switch ($type) {
 
12370
                case 'attr_transform_pre':
 
12371
                case 'attr_transform_post':
 
12372
                    $attr = $params['attr'];
 
12373
                    if (isset($params['element'])) {
 
12374
                        $element = $params['element'];
 
12375
                        if (empty($this->info[$element])) {
 
12376
                            $e = $this->addBlankElement($element);
 
12377
                        } else {
 
12378
                            $e = $this->info[$element];
 
12379
                        }
 
12380
                    } else {
 
12381
                        $type = "info_$type";
 
12382
                        $e = $this;
 
12383
                    }
 
12384
                    // PHP does some weird parsing when I do
 
12385
                    // $e->$type[$attr], so I have to assign a ref.
 
12386
                    $f =& $e->$type;
 
12387
                    $f[$attr] = $fix;
 
12388
                    break;
 
12389
                case 'tag_transform':
 
12390
                    $this->info_tag_transform[$params['element']] = $fix;
 
12391
                    break;
 
12392
                case 'child':
 
12393
                case 'content_model_type':
 
12394
                    $element = $params['element'];
 
12395
                    if (empty($this->info[$element])) {
 
12396
                        $e = $this->addBlankElement($element);
 
12397
                    } else {
 
12398
                        $e = $this->info[$element];
 
12399
                    }
 
12400
                    $e->$type = $fix;
 
12401
                    break;
 
12402
                default:
 
12403
                    trigger_error("Fix type $type not supported", E_USER_ERROR);
 
12404
                    break;
 
12405
            }
 
12406
        }
 
12407
    }
 
12408
 
 
12409
    /**
 
12410
     * Parses a fix name and determines what kind of fix it is, as well
 
12411
     * as other information defined by the fix
 
12412
     * @param $name String name of fix
 
12413
     * @return array(string $fix_type, array $fix_parameters)
 
12414
     * @note $fix_parameters is type dependant, see populate() for usage
 
12415
     *       of these parameters
 
12416
     */
 
12417
    public function getFixType($name) {
 
12418
        // parse it
 
12419
        $property = $attr = null;
 
12420
        if (strpos($name, '#') !== false) list($name, $property) = explode('#', $name);
 
12421
        if (strpos($name, '@') !== false) list($name, $attr)     = explode('@', $name);
 
12422
 
 
12423
        // figure out the parameters
 
12424
        $params = array();
 
12425
        if ($name !== '')    $params['element'] = $name;
 
12426
        if (!is_null($attr)) $params['attr'] = $attr;
 
12427
 
 
12428
        // special case: attribute transform
 
12429
        if (!is_null($attr)) {
 
12430
            if (is_null($property)) $property = 'pre';
 
12431
            $type = 'attr_transform_' . $property;
 
12432
            return array($type, $params);
 
12433
        }
 
12434
 
 
12435
        // special case: tag transform
 
12436
        if (is_null($property)) {
 
12437
            return array('tag_transform', $params);
 
12438
        }
 
12439
 
 
12440
        return array($property, $params);
 
12441
 
 
12442
    }
 
12443
 
 
12444
    /**
 
12445
     * Defines all fixes the module will perform in a compact
 
12446
     * associative array of fix name to fix implementation.
 
12447
     */
 
12448
    public function makeFixes() {}
 
12449
 
 
12450
}
 
12451
 
 
12452
 
 
12453
 
 
12454
 
 
12455
 
 
12456
class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule
 
12457
{
 
12458
    public $name = 'XMLCommonAttributes';
 
12459
 
 
12460
    public $attr_collections = array(
 
12461
        'Lang' => array(
 
12462
            'xml:lang' => 'LanguageCode',
 
12463
        )
 
12464
    );
 
12465
}
 
12466
 
 
12467
 
 
12468
 
 
12469
 
 
12470
 
 
12471
/**
 
12472
 * Name is deprecated, but allowed in strict doctypes, so onl
 
12473
 */
 
12474
class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy
 
12475
{
 
12476
    public $name = 'Tidy_Name';
 
12477
    public $defaultLevel = 'heavy';
 
12478
    public function makeFixes() {
 
12479
 
 
12480
        $r = array();
 
12481
 
 
12482
        // @name for img, a -----------------------------------------------
 
12483
        // Technically, it's allowed even on strict, so we allow authors to use
 
12484
        // it. However, it's deprecated in future versions of XHTML.
 
12485
        $r['img@name'] =
 
12486
        $r['a@name'] = new HTMLPurifier_AttrTransform_Name();
 
12487
 
 
12488
        return $r;
 
12489
    }
 
12490
}
 
12491
 
 
12492
 
 
12493
 
 
12494
 
 
12495
 
 
12496
class HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy
 
12497
{
 
12498
 
 
12499
    public $name = 'Tidy_Proprietary';
 
12500
    public $defaultLevel = 'light';
 
12501
 
 
12502
    public function makeFixes() {
 
12503
        $r = array();
 
12504
        $r['table@background'] = new HTMLPurifier_AttrTransform_Background();
 
12505
        $r['td@background']    = new HTMLPurifier_AttrTransform_Background();
 
12506
        $r['th@background']    = new HTMLPurifier_AttrTransform_Background();
 
12507
        $r['tr@background']    = new HTMLPurifier_AttrTransform_Background();
 
12508
        $r['thead@background'] = new HTMLPurifier_AttrTransform_Background();
 
12509
        $r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background();
 
12510
        $r['tbody@background'] = new HTMLPurifier_AttrTransform_Background();
 
12511
        return $r;
 
12512
    }
 
12513
 
 
12514
}
 
12515
 
 
12516
 
 
12517
 
 
12518
 
 
12519
 
 
12520
class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy
 
12521
{
 
12522
 
 
12523
    public function makeFixes() {
 
12524
 
 
12525
        $r = array();
 
12526
 
 
12527
        // == deprecated tag transforms ===================================
 
12528
 
 
12529
        $r['font']   = new HTMLPurifier_TagTransform_Font();
 
12530
        $r['menu']   = new HTMLPurifier_TagTransform_Simple('ul');
 
12531
        $r['dir']    = new HTMLPurifier_TagTransform_Simple('ul');
 
12532
        $r['center'] = new HTMLPurifier_TagTransform_Simple('div',  'text-align:center;');
 
12533
        $r['u']      = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:underline;');
 
12534
        $r['s']      = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
 
12535
        $r['strike'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
 
12536
 
 
12537
        // == deprecated attribute transforms =============================
 
12538
 
 
12539
        $r['caption@align'] =
 
12540
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
 
12541
                // we're following IE's behavior, not Firefox's, due
 
12542
                // to the fact that no one supports caption-side:right,
 
12543
                // W3C included (with CSS 2.1). This is a slightly
 
12544
                // unreasonable attribute!
 
12545
                'left'   => 'text-align:left;',
 
12546
                'right'  => 'text-align:right;',
 
12547
                'top'    => 'caption-side:top;',
 
12548
                'bottom' => 'caption-side:bottom;' // not supported by IE
 
12549
            ));
 
12550
 
 
12551
        // @align for img -------------------------------------------------
 
12552
        $r['img@align'] =
 
12553
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
 
12554
                'left'   => 'float:left;',
 
12555
                'right'  => 'float:right;',
 
12556
                'top'    => 'vertical-align:top;',
 
12557
                'middle' => 'vertical-align:middle;',
 
12558
                'bottom' => 'vertical-align:baseline;',
 
12559
            ));
 
12560
 
 
12561
        // @align for table -----------------------------------------------
 
12562
        $r['table@align'] =
 
12563
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
 
12564
                'left'   => 'float:left;',
 
12565
                'center' => 'margin-left:auto;margin-right:auto;',
 
12566
                'right'  => 'float:right;'
 
12567
            ));
 
12568
 
 
12569
        // @align for hr -----------------------------------------------
 
12570
        $r['hr@align'] =
 
12571
            new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
 
12572
                // we use both text-align and margin because these work
 
12573
                // for different browsers (IE and Firefox, respectively)
 
12574
                // and the melange makes for a pretty cross-compatible
 
12575
                // solution
 
12576
                'left'   => 'margin-left:0;margin-right:auto;text-align:left;',
 
12577
                'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
 
12578
                'right'  => 'margin-left:auto;margin-right:0;text-align:right;'
 
12579
            ));
 
12580
 
 
12581
        // @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
 
12582
        // {{{
 
12583
            $align_lookup = array();
 
12584
            $align_values = array('left', 'right', 'center', 'justify');
 
12585
            foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;";
 
12586
        // }}}
 
12587
        $r['h1@align'] =
 
12588
        $r['h2@align'] =
 
12589
        $r['h3@align'] =
 
12590
        $r['h4@align'] =
 
12591
        $r['h5@align'] =
 
12592
        $r['h6@align'] =
 
12593
        $r['p@align']  =
 
12594
        $r['div@align'] =
 
12595
            new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup);
 
12596
 
 
12597
        // @bgcolor for table, tr, td, th ---------------------------------
 
12598
        $r['table@bgcolor'] =
 
12599
        $r['td@bgcolor'] =
 
12600
        $r['th@bgcolor'] =
 
12601
            new HTMLPurifier_AttrTransform_BgColor();
 
12602
 
 
12603
        // @border for img ------------------------------------------------
 
12604
        $r['img@border'] = new HTMLPurifier_AttrTransform_Border();
 
12605
 
 
12606
        // @clear for br --------------------------------------------------
 
12607
        $r['br@clear'] =
 
12608
            new HTMLPurifier_AttrTransform_EnumToCSS('clear', array(
 
12609
                'left'  => 'clear:left;',
 
12610
                'right' => 'clear:right;',
 
12611
                'all'   => 'clear:both;',
 
12612
                'none'  => 'clear:none;',
 
12613
            ));
 
12614
 
 
12615
        // @height for td, th ---------------------------------------------
 
12616
        $r['td@height'] =
 
12617
        $r['th@height'] =
 
12618
            new HTMLPurifier_AttrTransform_Length('height');
 
12619
 
 
12620
        // @hspace for img ------------------------------------------------
 
12621
        $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');
 
12622
 
 
12623
        // @noshade for hr ------------------------------------------------
 
12624
        // this transformation is not precise but often good enough.
 
12625
        // different browsers use different styles to designate noshade
 
12626
        $r['hr@noshade'] =
 
12627
            new HTMLPurifier_AttrTransform_BoolToCSS(
 
12628
                'noshade',
 
12629
                'color:#808080;background-color:#808080;border:0;'
 
12630
            );
 
12631
 
 
12632
        // @nowrap for td, th ---------------------------------------------
 
12633
        $r['td@nowrap'] =
 
12634
        $r['th@nowrap'] =
 
12635
            new HTMLPurifier_AttrTransform_BoolToCSS(
 
12636
                'nowrap',
 
12637
                'white-space:nowrap;'
 
12638
            );
 
12639
 
 
12640
        // @size for hr  --------------------------------------------------
 
12641
        $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height');
 
12642
 
 
12643
        // @type for li, ol, ul -------------------------------------------
 
12644
        // {{{
 
12645
            $ul_types = array(
 
12646
                'disc'   => 'list-style-type:disc;',
 
12647
                'square' => 'list-style-type:square;',
 
12648
                'circle' => 'list-style-type:circle;'
 
12649
            );
 
12650
            $ol_types = array(
 
12651
                '1'   => 'list-style-type:decimal;',
 
12652
                'i'   => 'list-style-type:lower-roman;',
 
12653
                'I'   => 'list-style-type:upper-roman;',
 
12654
                'a'   => 'list-style-type:lower-alpha;',
 
12655
                'A'   => 'list-style-type:upper-alpha;'
 
12656
            );
 
12657
            $li_types = $ul_types + $ol_types;
 
12658
        // }}}
 
12659
 
 
12660
        $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types);
 
12661
        $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true);
 
12662
        $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true);
 
12663
 
 
12664
        // @vspace for img ------------------------------------------------
 
12665
        $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
 
12666
 
 
12667
        // @width for hr, td, th ------------------------------------------
 
12668
        $r['td@width'] =
 
12669
        $r['th@width'] =
 
12670
        $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
 
12671
 
 
12672
        return $r;
 
12673
 
 
12674
    }
 
12675
 
 
12676
}
 
12677
 
 
12678
 
 
12679
 
 
12680
 
 
12681
 
 
12682
class HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
 
12683
{
 
12684
    public $name = 'Tidy_Strict';
 
12685
    public $defaultLevel = 'light';
 
12686
 
 
12687
    public function makeFixes() {
 
12688
        $r = parent::makeFixes();
 
12689
        $r['blockquote#content_model_type'] = 'strictblockquote';
 
12690
        return $r;
 
12691
    }
 
12692
 
 
12693
    public $defines_child_def = true;
 
12694
    public function getChildDef($def) {
 
12695
        if ($def->content_model_type != 'strictblockquote') return parent::getChildDef($def);
 
12696
        return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
 
12697
    }
 
12698
}
 
12699
 
 
12700
 
 
12701
 
 
12702
 
 
12703
 
 
12704
class HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
 
12705
{
 
12706
    public $name = 'Tidy_Transitional';
 
12707
    public $defaultLevel = 'heavy';
 
12708
}
 
12709
 
 
12710
 
 
12711
 
 
12712
 
 
12713
 
 
12714
class HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy
 
12715
{
 
12716
 
 
12717
    public $name = 'Tidy_XHTML';
 
12718
    public $defaultLevel = 'medium';
 
12719
 
 
12720
    public function makeFixes() {
 
12721
        $r = array();
 
12722
        $r['@lang'] = new HTMLPurifier_AttrTransform_Lang();
 
12723
        return $r;
 
12724
    }
 
12725
 
 
12726
}
 
12727
 
 
12728
 
 
12729
 
 
12730
 
 
12731
 
 
12732
/**
 
12733
 * Injector that auto paragraphs text in the root node based on
 
12734
 * double-spacing.
 
12735
 * @todo Ensure all states are unit tested, including variations as well.
 
12736
 * @todo Make a graph of the flow control for this Injector.
 
12737
 */
 
12738
class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector
 
12739
{
 
12740
 
 
12741
    public $name = 'AutoParagraph';
 
12742
    public $needed = array('p');
 
12743
 
 
12744
    private function _pStart() {
 
12745
        $par = new HTMLPurifier_Token_Start('p');
 
12746
        $par->armor['MakeWellFormed_TagClosedError'] = true;
 
12747
        return $par;
 
12748
    }
 
12749
 
 
12750
    public function handleText(&$token) {
 
12751
        $text = $token->data;
 
12752
        // Does the current parent allow <p> tags?
 
12753
        if ($this->allowsElement('p')) {
 
12754
            if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) {
 
12755
                // Note that we have differing behavior when dealing with text
 
12756
                // in the anonymous root node, or a node inside the document.
 
12757
                // If the text as a double-newline, the treatment is the same;
 
12758
                // if it doesn't, see the next if-block if you're in the document.
 
12759
 
 
12760
                $i = $nesting = null;
 
12761
                if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) {
 
12762
                    // State 1.1: ...    ^ (whitespace, then document end)
 
12763
                    //               ----
 
12764
                    // This is a degenerate case
 
12765
                } else {
 
12766
                    // State 1.2: PAR1
 
12767
                    //            ----
 
12768
 
 
12769
                    // State 1.3: PAR1\n\nPAR2
 
12770
                    //            ------------
 
12771
 
 
12772
                    // State 1.4: <div>PAR1\n\nPAR2 (see State 2)
 
12773
                    //                 ------------
 
12774
                    $token = array($this->_pStart());
 
12775
                    $this->_splitText($text, $token);
 
12776
                }
 
12777
            } else {
 
12778
                // State 2:   <div>PAR1... (similar to 1.4)
 
12779
                //                 ----
 
12780
 
 
12781
                // We're in an element that allows paragraph tags, but we're not
 
12782
                // sure if we're going to need them.
 
12783
                if ($this->_pLookAhead()) {
 
12784
                    // State 2.1: <div>PAR1<b>PAR1\n\nPAR2
 
12785
                    //                 ----
 
12786
                    // Note: This will always be the first child, since any
 
12787
                    // previous inline element would have triggered this very
 
12788
                    // same routine, and found the double newline. One possible
 
12789
                    // exception would be a comment.
 
12790
                    $token = array($this->_pStart(), $token);
 
12791
                } else {
 
12792
                    // State 2.2.1: <div>PAR1<div>
 
12793
                    //                   ----
 
12794
 
 
12795
                    // State 2.2.2: <div>PAR1<b>PAR1</b></div>
 
12796
                    //                   ----
 
12797
                }
 
12798
            }
 
12799
        // Is the current parent a <p> tag?
 
12800
        } elseif (
 
12801
            !empty($this->currentNesting) &&
 
12802
            $this->currentNesting[count($this->currentNesting)-1]->name == 'p'
 
12803
        ) {
 
12804
            // State 3.1: ...<p>PAR1
 
12805
            //                  ----
 
12806
 
 
12807
            // State 3.2: ...<p>PAR1\n\nPAR2
 
12808
            //                  ------------
 
12809
            $token = array();
 
12810
            $this->_splitText($text, $token);
 
12811
        // Abort!
 
12812
        } else {
 
12813
            // State 4.1: ...<b>PAR1
 
12814
            //                  ----
 
12815
 
 
12816
            // State 4.2: ...<b>PAR1\n\nPAR2
 
12817
            //                  ------------
 
12818
        }
 
12819
    }
 
12820
 
 
12821
    public function handleElement(&$token) {
 
12822
        // We don't have to check if we're already in a <p> tag for block
 
12823
        // tokens, because the tag would have been autoclosed by MakeWellFormed.
 
12824
        if ($this->allowsElement('p')) {
 
12825
            if (!empty($this->currentNesting)) {
 
12826
                if ($this->_isInline($token)) {
 
12827
                    // State 1: <div>...<b>
 
12828
                    //                  ---
 
12829
 
 
12830
                    // Check if this token is adjacent to the parent token
 
12831
                    // (seek backwards until token isn't whitespace)
 
12832
                    $i = null;
 
12833
                    $this->backward($i, $prev);
 
12834
 
 
12835
                    if (!$prev instanceof HTMLPurifier_Token_Start) {
 
12836
                        // Token wasn't adjacent
 
12837
 
 
12838
                        if (
 
12839
                            $prev instanceof HTMLPurifier_Token_Text &&
 
12840
                            substr($prev->data, -2) === "\n\n"
 
12841
                        ) {
 
12842
                            // State 1.1.4: <div><p>PAR1</p>\n\n<b>
 
12843
                            //                                  ---
 
12844
 
 
12845
                            // Quite frankly, this should be handled by splitText
 
12846
                            $token = array($this->_pStart(), $token);
 
12847
                        } else {
 
12848
                            // State 1.1.1: <div><p>PAR1</p><b>
 
12849
                            //                              ---
 
12850
 
 
12851
                            // State 1.1.2: <div><br /><b>
 
12852
                            //                         ---
 
12853
 
 
12854
                            // State 1.1.3: <div>PAR<b>
 
12855
                            //                      ---
 
12856
                        }
 
12857
 
 
12858
                    } else {
 
12859
                        // State 1.2.1: <div><b>
 
12860
                        //                   ---
 
12861
 
 
12862
                        // Lookahead to see if <p> is needed.
 
12863
                        if ($this->_pLookAhead()) {
 
12864
                            // State 1.3.1: <div><b>PAR1\n\nPAR2
 
12865
                            //                   ---
 
12866
                            $token = array($this->_pStart(), $token);
 
12867
                        } else {
 
12868
                            // State 1.3.2: <div><b>PAR1</b></div>
 
12869
                            //                   ---
 
12870
 
 
12871
                            // State 1.3.3: <div><b>PAR1</b><div></div>\n\n</div>
 
12872
                            //                   ---
 
12873
                        }
 
12874
                    }
 
12875
                } else {
 
12876
                    // State 2.3: ...<div>
 
12877
                    //               -----
 
12878
                }
 
12879
            } else {
 
12880
                if ($this->_isInline($token)) {
 
12881
                    // State 3.1: <b>
 
12882
                    //            ---
 
12883
                    // This is where the {p} tag is inserted, not reflected in
 
12884
                    // inputTokens yet, however.
 
12885
                    $token = array($this->_pStart(), $token);
 
12886
                } else {
 
12887
                    // State 3.2: <div>
 
12888
                    //            -----
 
12889
                }
 
12890
 
 
12891
                $i = null;
 
12892
                if ($this->backward($i, $prev)) {
 
12893
                    if (
 
12894
                        !$prev instanceof HTMLPurifier_Token_Text
 
12895
                    ) {
 
12896
                        // State 3.1.1: ...</p>{p}<b>
 
12897
                        //                        ---
 
12898
 
 
12899
                        // State 3.2.1: ...</p><div>
 
12900
                        //                     -----
 
12901
 
 
12902
                        if (!is_array($token)) $token = array($token);
 
12903
                        array_unshift($token, new HTMLPurifier_Token_Text("\n\n"));
 
12904
                    } else {
 
12905
                        // State 3.1.2: ...</p>\n\n{p}<b>
 
12906
                        //                            ---
 
12907
 
 
12908
                        // State 3.2.2: ...</p>\n\n<div>
 
12909
                        //                         -----
 
12910
 
 
12911
                        // Note: PAR<ELEM> cannot occur because PAR would have been
 
12912
                        // wrapped in <p> tags.
 
12913
                    }
 
12914
                }
 
12915
            }
 
12916
        } else {
 
12917
            // State 2.2: <ul><li>
 
12918
            //                ----
 
12919
 
 
12920
            // State 2.4: <p><b>
 
12921
            //               ---
 
12922
        }
 
12923
    }
 
12924
 
 
12925
    /**
 
12926
     * Splits up a text in paragraph tokens and appends them
 
12927
     * to the result stream that will replace the original
 
12928
     * @param $data String text data that will be processed
 
12929
     *    into paragraphs
 
12930
     * @param $result Reference to array of tokens that the
 
12931
     *    tags will be appended onto
 
12932
     * @param $config Instance of HTMLPurifier_Config
 
12933
     * @param $context Instance of HTMLPurifier_Context
 
12934
     */
 
12935
    private function _splitText($data, &$result) {
 
12936
        $raw_paragraphs = explode("\n\n", $data);
 
12937
        $paragraphs  = array(); // without empty paragraphs
 
12938
        $needs_start = false;
 
12939
        $needs_end   = false;
 
12940
 
 
12941
        $c = count($raw_paragraphs);
 
12942
        if ($c == 1) {
 
12943
            // There were no double-newlines, abort quickly. In theory this
 
12944
            // should never happen.
 
12945
            $result[] = new HTMLPurifier_Token_Text($data);
 
12946
            return;
 
12947
        }
 
12948
        for ($i = 0; $i < $c; $i++) {
 
12949
            $par = $raw_paragraphs[$i];
 
12950
            if (trim($par) !== '') {
 
12951
                $paragraphs[] = $par;
 
12952
            } else {
 
12953
                if ($i == 0) {
 
12954
                    // Double newline at the front
 
12955
                    if (empty($result)) {
 
12956
                        // The empty result indicates that the AutoParagraph
 
12957
                        // injector did not add any start paragraph tokens.
 
12958
                        // This means that we have been in a paragraph for
 
12959
                        // a while, and the newline means we should start a new one.
 
12960
                        $result[] = new HTMLPurifier_Token_End('p');
 
12961
                        $result[] = new HTMLPurifier_Token_Text("\n\n");
 
12962
                        // However, the start token should only be added if
 
12963
                        // there is more processing to be done (i.e. there are
 
12964
                        // real paragraphs in here). If there are none, the
 
12965
                        // next start paragraph tag will be handled by the
 
12966
                        // next call to the injector
 
12967
                        $needs_start = true;
 
12968
                    } else {
 
12969
                        // We just started a new paragraph!
 
12970
                        // Reinstate a double-newline for presentation's sake, since
 
12971
                        // it was in the source code.
 
12972
                        array_unshift($result, new HTMLPurifier_Token_Text("\n\n"));
 
12973
                    }
 
12974
                } elseif ($i + 1 == $c) {
 
12975
                    // Double newline at the end
 
12976
                    // There should be a trailing </p> when we're finally done.
 
12977
                    $needs_end = true;
 
12978
                }
 
12979
            }
 
12980
        }
 
12981
 
 
12982
        // Check if this was just a giant blob of whitespace. Move this earlier,
 
12983
        // perhaps?
 
12984
        if (empty($paragraphs)) {
 
12985
            return;
 
12986
        }
 
12987
 
 
12988
        // Add the start tag indicated by \n\n at the beginning of $data
 
12989
        if ($needs_start) {
 
12990
            $result[] = $this->_pStart();
 
12991
        }
 
12992
 
 
12993
        // Append the paragraphs onto the result
 
12994
        foreach ($paragraphs as $par) {
 
12995
            $result[] = new HTMLPurifier_Token_Text($par);
 
12996
            $result[] = new HTMLPurifier_Token_End('p');
 
12997
            $result[] = new HTMLPurifier_Token_Text("\n\n");
 
12998
            $result[] = $this->_pStart();
 
12999
        }
 
13000
 
 
13001
        // Remove trailing start token; Injector will handle this later if
 
13002
        // it was indeed needed. This prevents from needing to do a lookahead,
 
13003
        // at the cost of a lookbehind later.
 
13004
        array_pop($result);
 
13005
 
 
13006
        // If there is no need for an end tag, remove all of it and let
 
13007
        // MakeWellFormed close it later.
 
13008
        if (!$needs_end) {
 
13009
            array_pop($result); // removes \n\n
 
13010
            array_pop($result); // removes </p>
 
13011
        }
 
13012
 
 
13013
    }
 
13014
 
 
13015
    /**
 
13016
     * Returns true if passed token is inline (and, ergo, allowed in
 
13017
     * paragraph tags)
 
13018
     */
 
13019
    private function _isInline($token) {
 
13020
        return isset($this->htmlDefinition->info['p']->child->elements[$token->name]);
 
13021
    }
 
13022
 
 
13023
    /**
 
13024
     * Looks ahead in the token list and determines whether or not we need
 
13025
     * to insert a <p> tag.
 
13026
     */
 
13027
    private function _pLookAhead() {
 
13028
        $this->current($i, $current);
 
13029
        if ($current instanceof HTMLPurifier_Token_Start) $nesting = 1;
 
13030
        else $nesting = 0;
 
13031
        $ok = false;
 
13032
        while ($this->forwardUntilEndToken($i, $current, $nesting)) {
 
13033
            $result = $this->_checkNeedsP($current);
 
13034
            if ($result !== null) {
 
13035
                $ok = $result;
 
13036
                break;
 
13037
            }
 
13038
        }
 
13039
        return $ok;
 
13040
    }
 
13041
 
 
13042
    /**
 
13043
     * Determines if a particular token requires an earlier inline token
 
13044
     * to get a paragraph. This should be used with _forwardUntilEndToken
 
13045
     */
 
13046
    private function _checkNeedsP($current) {
 
13047
        if ($current instanceof HTMLPurifier_Token_Start){
 
13048
            if (!$this->_isInline($current)) {
 
13049
                // <div>PAR1<div>
 
13050
                //      ----
 
13051
                // Terminate early, since we hit a block element
 
13052
                return false;
 
13053
            }
 
13054
        } elseif ($current instanceof HTMLPurifier_Token_Text) {
 
13055
            if (strpos($current->data, "\n\n") !== false) {
 
13056
                // <div>PAR1<b>PAR1\n\nPAR2
 
13057
                //      ----
 
13058
                return true;
 
13059
            } else {
 
13060
                // <div>PAR1<b>PAR1...
 
13061
                //      ----
 
13062
            }
 
13063
        }
 
13064
        return null;
 
13065
    }
 
13066
 
 
13067
}
 
13068
 
 
13069
 
 
13070
 
 
13071
 
 
13072
 
 
13073
/**
 
13074
 * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link.
 
13075
 */
 
13076
class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector
 
13077
{
 
13078
 
 
13079
    public $name = 'DisplayLinkURI';
 
13080
    public $needed = array('a');
 
13081
 
 
13082
    public function handleElement(&$token) {
 
13083
    }
 
13084
 
 
13085
    public function handleEnd(&$token) {
 
13086
        if (isset($token->start->attr['href'])){
 
13087
            $url = $token->start->attr['href'];
 
13088
            unset($token->start->attr['href']);
 
13089
            $token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
 
13090
        } else {
 
13091
            // nothing to display
 
13092
        }
 
13093
    }
 
13094
}
 
13095
 
 
13096
 
 
13097
 
 
13098
 
 
13099
 
 
13100
/**
 
13101
 * Injector that converts http, https and ftp text URLs to actual links.
 
13102
 */
 
13103
class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
 
13104
{
 
13105
 
 
13106
    public $name = 'Linkify';
 
13107
    public $needed = array('a' => array('href'));
 
13108
 
 
13109
    public function handleText(&$token) {
 
13110
        if (!$this->allowsElement('a')) return;
 
13111
 
 
13112
        if (strpos($token->data, '://') === false) {
 
13113
            // our really quick heuristic failed, abort
 
13114
            // this may not work so well if we want to match things like
 
13115
            // "google.com", but then again, most people don't
 
13116
            return;
 
13117
        }
 
13118
 
 
13119
        // there is/are URL(s). Let's split the string:
 
13120
        // Note: this regex is extremely permissive
 
13121
        $bits = preg_split('#((?:https?|ftp)://[^\s\'"<>()]+)#S', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
 
13122
 
 
13123
        $token = array();
 
13124
 
 
13125
        // $i = index
 
13126
        // $c = count
 
13127
        // $l = is link
 
13128
        for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
 
13129
            if (!$l) {
 
13130
                if ($bits[$i] === '') continue;
 
13131
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
 
13132
            } else {
 
13133
                $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
 
13134
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
 
13135
                $token[] = new HTMLPurifier_Token_End('a');
 
13136
            }
 
13137
        }
 
13138
 
 
13139
    }
 
13140
 
 
13141
}
 
13142
 
 
13143
 
 
13144
 
 
13145
 
 
13146
 
 
13147
/**
 
13148
 * Injector that converts configuration directive syntax %Namespace.Directive
 
13149
 * to links
 
13150
 */
 
13151
class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector
 
13152
{
 
13153
 
 
13154
    public $name = 'PurifierLinkify';
 
13155
    public $docURL;
 
13156
    public $needed = array('a' => array('href'));
 
13157
 
 
13158
    public function prepare($config, $context) {
 
13159
        $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL');
 
13160
        return parent::prepare($config, $context);
 
13161
    }
 
13162
 
 
13163
    public function handleText(&$token) {
 
13164
        if (!$this->allowsElement('a')) return;
 
13165
        if (strpos($token->data, '%') === false) return;
 
13166
 
 
13167
        $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
 
13168
        $token = array();
 
13169
 
 
13170
        // $i = index
 
13171
        // $c = count
 
13172
        // $l = is link
 
13173
        for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
 
13174
            if (!$l) {
 
13175
                if ($bits[$i] === '') continue;
 
13176
                $token[] = new HTMLPurifier_Token_Text($bits[$i]);
 
13177
            } else {
 
13178
                $token[] = new HTMLPurifier_Token_Start('a',
 
13179
                    array('href' => str_replace('%s', $bits[$i], $this->docURL)));
 
13180
                $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
 
13181
                $token[] = new HTMLPurifier_Token_End('a');
 
13182
            }
 
13183
        }
 
13184
 
 
13185
    }
 
13186
 
 
13187
}
 
13188
 
 
13189
 
 
13190
 
 
13191
 
 
13192
 
 
13193
class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector
 
13194
{
 
13195
 
 
13196
    private $context, $config, $attrValidator, $removeNbsp, $removeNbspExceptions;
 
13197
 
 
13198
    public function prepare($config, $context) {
 
13199
        parent::prepare($config, $context);
 
13200
        $this->config = $config;
 
13201
        $this->context = $context;
 
13202
        $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp');
 
13203
        $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions');
 
13204
        $this->attrValidator = new HTMLPurifier_AttrValidator();
 
13205
    }
 
13206
 
 
13207
    public function handleElement(&$token) {
 
13208
        if (!$token instanceof HTMLPurifier_Token_Start) return;
 
13209
        $next = false;
 
13210
        for ($i = $this->inputIndex + 1, $c = count($this->inputTokens); $i < $c; $i++) {
 
13211
            $next = $this->inputTokens[$i];
 
13212
            if ($next instanceof HTMLPurifier_Token_Text) {
 
13213
                if ($next->is_whitespace) continue;
 
13214
                if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) {
 
13215
                    $plain = str_replace("\xC2\xA0", "", $next->data);
 
13216
                    $isWsOrNbsp = $plain === '' || ctype_space($plain);
 
13217
                    if ($isWsOrNbsp) continue;
 
13218
                }
 
13219
            }
 
13220
            break;
 
13221
        }
 
13222
        if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
 
13223
            if ($token->name == 'colgroup') return;
 
13224
            $this->attrValidator->validateToken($token, $this->config, $this->context);
 
13225
            $token->armor['ValidateAttributes'] = true;
 
13226
            if (isset($token->attr['id']) || isset($token->attr['name'])) return;
 
13227
            $token = $i - $this->inputIndex + 1;
 
13228
            for ($b = $this->inputIndex - 1; $b > 0; $b--) {
 
13229
                $prev = $this->inputTokens[$b];
 
13230
                if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) continue;
 
13231
                break;
 
13232
            }
 
13233
            // This is safe because we removed the token that triggered this.
 
13234
            $this->rewind($b - 1);
 
13235
            return;
 
13236
        }
 
13237
    }
 
13238
 
 
13239
}
 
13240
 
 
13241
 
 
13242
 
 
13243
 
 
13244
 
 
13245
/**
 
13246
 * Adds important param elements to inside of object in order to make
 
13247
 * things safe.
 
13248
 */
 
13249
class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector
 
13250
{
 
13251
    public $name = 'SafeObject';
 
13252
    public $needed = array('object', 'param');
 
13253
 
 
13254
    protected $objectStack = array();
 
13255
    protected $paramStack  = array();
 
13256
 
 
13257
    // Keep this synchronized with AttrTransform/SafeParam.php
 
13258
    protected $addParam = array(
 
13259
        'allowScriptAccess' => 'never',
 
13260
        'allowNetworking' => 'internal',
 
13261
    );
 
13262
    protected $allowedParam = array(
 
13263
        'wmode' => true,
 
13264
        'movie' => true,
 
13265
    );
 
13266
 
 
13267
    public function prepare($config, $context) {
 
13268
        parent::prepare($config, $context);
 
13269
    }
 
13270
 
 
13271
    public function handleElement(&$token) {
 
13272
        if ($token->name == 'object') {
 
13273
            $this->objectStack[] = $token;
 
13274
            $this->paramStack[] = array();
 
13275
            $new = array($token);
 
13276
            foreach ($this->addParam as $name => $value) {
 
13277
                $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
 
13278
            }
 
13279
            $token = $new;
 
13280
        } elseif ($token->name == 'param') {
 
13281
            $nest = count($this->currentNesting) - 1;
 
13282
            if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
 
13283
                $i = count($this->objectStack) - 1;
 
13284
                if (!isset($token->attr['name'])) {
 
13285
                    $token = false;
 
13286
                    return;
 
13287
                }
 
13288
                $n = $token->attr['name'];
 
13289
                // We need this fix because YouTube doesn't supply a data
 
13290
                // attribute, which we need if a type is specified. This is
 
13291
                // *very* Flash specific.
 
13292
                if (!isset($this->objectStack[$i]->attr['data']) && $token->attr['name'] == 'movie') {
 
13293
                    $this->objectStack[$i]->attr['data'] = $token->attr['value'];
 
13294
                }
 
13295
                // Check if the parameter is the correct value but has not
 
13296
                // already been added
 
13297
                if (
 
13298
                    !isset($this->paramStack[$i][$n]) &&
 
13299
                    isset($this->addParam[$n]) &&
 
13300
                    $token->attr['name'] === $this->addParam[$n]
 
13301
                ) {
 
13302
                    // keep token, and add to param stack
 
13303
                    $this->paramStack[$i][$n] = true;
 
13304
                } elseif (isset($this->allowedParam[$n])) {
 
13305
                    // keep token, don't do anything to it
 
13306
                    // (could possibly check for duplicates here)
 
13307
                } else {
 
13308
                    $token = false;
 
13309
                }
 
13310
            } else {
 
13311
                // not directly inside an object, DENY!
 
13312
                $token = false;
 
13313
            }
 
13314
        }
 
13315
    }
 
13316
 
 
13317
    public function handleEnd(&$token) {
 
13318
        // This is the WRONG way of handling the object and param stacks;
 
13319
        // we should be inserting them directly on the relevant object tokens
 
13320
        // so that the global stack handling handles it.
 
13321
        if ($token->name == 'object') {
 
13322
            array_pop($this->objectStack);
 
13323
            array_pop($this->paramStack);
 
13324
        }
 
13325
    }
 
13326
 
 
13327
}
 
13328
 
 
13329
 
 
13330
 
 
13331
 
 
13332
 
 
13333
/**
 
13334
 * Parser that uses PHP 5's DOM extension (part of the core).
 
13335
 *
 
13336
 * In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
 
13337
 * It gives us a forgiving HTML parser, which we use to transform the HTML
 
13338
 * into a DOM, and then into the tokens.  It is blazingly fast (for large
 
13339
 * documents, it performs twenty times faster than
 
13340
 * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5.
 
13341
 *
 
13342
 * @note Any empty elements will have empty tokens associated with them, even if
 
13343
 * this is prohibited by the spec. This is cannot be fixed until the spec
 
13344
 * comes into play.
 
13345
 *
 
13346
 * @note PHP's DOM extension does not actually parse any entities, we use
 
13347
 *       our own function to do that.
 
13348
 *
 
13349
 * @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
 
13350
 *          If this is a huge problem, due to the fact that HTML is hand
 
13351
 *          edited and you are unable to get a parser cache that caches the
 
13352
 *          the output of HTML Purifier while keeping the original HTML lying
 
13353
 *          around, you may want to run Tidy on the resulting output or use
 
13354
 *          HTMLPurifier_DirectLex
 
13355
 */
 
13356
 
 
13357
class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
 
13358
{
 
13359
 
 
13360
    private $factory;
 
13361
 
 
13362
    public function __construct() {
 
13363
        // setup the factory
 
13364
        parent::__construct();
 
13365
        $this->factory = new HTMLPurifier_TokenFactory();
 
13366
    }
 
13367
 
 
13368
    public function tokenizeHTML($html, $config, $context) {
 
13369
 
 
13370
        $html = $this->normalize($html, $config, $context);
 
13371
 
 
13372
        // attempt to armor stray angled brackets that cannot possibly
 
13373
        // form tags and thus are probably being used as emoticons
 
13374
        if ($config->get('Core.AggressivelyFixLt')) {
 
13375
            $char = '[^a-z!\/]';
 
13376
            $comment = "/<!--(.*?)(-->|\z)/is";
 
13377
            $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
 
13378
            do {
 
13379
                $old = $html;
 
13380
                $html = preg_replace("/<($char)/i", '&lt;\\1', $html);
 
13381
            } while ($html !== $old);
 
13382
            $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
 
13383
        }
 
13384
 
 
13385
        // preprocess html, essential for UTF-8
 
13386
        $html = $this->wrapHTML($html, $config, $context);
 
13387
 
 
13388
        $doc = new DOMDocument();
 
13389
        $doc->encoding = 'UTF-8'; // theoretically, the above has this covered
 
13390
 
 
13391
        set_error_handler(array($this, 'muteErrorHandler'));
 
13392
        $doc->loadHTML($html);
 
13393
        restore_error_handler();
 
13394
 
 
13395
        $tokens = array();
 
13396
        $this->tokenizeDOM(
 
13397
            $doc->getElementsByTagName('html')->item(0)-> // <html>
 
13398
                  getElementsByTagName('body')->item(0)-> //   <body>
 
13399
                  getElementsByTagName('div')->item(0)    //     <div>
 
13400
            , $tokens);
 
13401
        return $tokens;
 
13402
    }
 
13403
 
 
13404
    /**
 
13405
     * Recursive function that tokenizes a node, putting it into an accumulator.
 
13406
     *
 
13407
     * @param $node     DOMNode to be tokenized.
 
13408
     * @param $tokens   Array-list of already tokenized tokens.
 
13409
     * @param $collect  Says whether or start and close are collected, set to
 
13410
     *                  false at first recursion because it's the implicit DIV
 
13411
     *                  tag you're dealing with.
 
13412
     * @returns Tokens of node appended to previously passed tokens.
 
13413
     */
 
13414
    protected function tokenizeDOM($node, &$tokens, $collect = false) {
 
13415
 
 
13416
        // intercept non element nodes. WE MUST catch all of them,
 
13417
        // but we're not getting the character reference nodes because
 
13418
        // those should have been preprocessed
 
13419
        if ($node->nodeType === XML_TEXT_NODE) {
 
13420
            $tokens[] = $this->factory->createText($node->data);
 
13421
            return;
 
13422
        } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
 
13423
            // undo libxml's special treatment of <script> and <style> tags
 
13424
            $last = end($tokens);
 
13425
            $data = $node->data;
 
13426
            // (note $node->tagname is already normalized)
 
13427
            if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
 
13428
                $new_data = trim($data);
 
13429
                if (substr($new_data, 0, 4) === '<!--') {
 
13430
                    $data = substr($new_data, 4);
 
13431
                    if (substr($data, -3) === '-->') {
 
13432
                        $data = substr($data, 0, -3);
 
13433
                    } else {
 
13434
                        // Highly suspicious! Not sure what to do...
 
13435
                    }
 
13436
                }
 
13437
            }
 
13438
            $tokens[] = $this->factory->createText($this->parseData($data));
 
13439
            return;
 
13440
        } elseif ($node->nodeType === XML_COMMENT_NODE) {
 
13441
            // this is code is only invoked for comments in script/style in versions
 
13442
            // of libxml pre-2.6.28 (regular comments, of course, are still
 
13443
            // handled regularly)
 
13444
            $tokens[] = $this->factory->createComment($node->data);
 
13445
            return;
 
13446
        } elseif (
 
13447
            // not-well tested: there may be other nodes we have to grab
 
13448
            $node->nodeType !== XML_ELEMENT_NODE
 
13449
        ) {
 
13450
            return;
 
13451
        }
 
13452
 
 
13453
        $attr = $node->hasAttributes() ?
 
13454
            $this->transformAttrToAssoc($node->attributes) :
 
13455
            array();
 
13456
 
 
13457
        // We still have to make sure that the element actually IS empty
 
13458
        if (!$node->childNodes->length) {
 
13459
            if ($collect) {
 
13460
                $tokens[] = $this->factory->createEmpty($node->tagName, $attr);
 
13461
            }
 
13462
        } else {
 
13463
            if ($collect) { // don't wrap on first iteration
 
13464
                $tokens[] = $this->factory->createStart(
 
13465
                    $tag_name = $node->tagName, // somehow, it get's dropped
 
13466
                    $attr
 
13467
                );
 
13468
            }
 
13469
            foreach ($node->childNodes as $node) {
 
13470
                // remember, it's an accumulator. Otherwise, we'd have
 
13471
                // to use array_merge
 
13472
                $this->tokenizeDOM($node, $tokens, true);
 
13473
            }
 
13474
            if ($collect) {
 
13475
                $tokens[] = $this->factory->createEnd($tag_name);
 
13476
            }
 
13477
        }
 
13478
 
 
13479
    }
 
13480
 
 
13481
    /**
 
13482
     * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
 
13483
     *
 
13484
     * @param $attribute_list DOMNamedNodeMap of DOMAttr objects.
 
13485
     * @returns Associative array of attributes.
 
13486
     */
 
13487
    protected function transformAttrToAssoc($node_map) {
 
13488
        // NamedNodeMap is documented very well, so we're using undocumented
 
13489
        // features, namely, the fact that it implements Iterator and
 
13490
        // has a ->length attribute
 
13491
        if ($node_map->length === 0) return array();
 
13492
        $array = array();
 
13493
        foreach ($node_map as $attr) {
 
13494
            $array[$attr->name] = $attr->value;
 
13495
        }
 
13496
        return $array;
 
13497
    }
 
13498
 
 
13499
    /**
 
13500
     * An error handler that mutes all errors
 
13501
     */
 
13502
    public function muteErrorHandler($errno, $errstr) {}
 
13503
 
 
13504
    /**
 
13505
     * Callback function for undoing escaping of stray angled brackets
 
13506
     * in comments
 
13507
     */
 
13508
    public function callbackUndoCommentSubst($matches) {
 
13509
        return '<!--' . strtr($matches[1], array('&amp;'=>'&','&lt;'=>'<')) . $matches[2];
 
13510
    }
 
13511
 
 
13512
    /**
 
13513
     * Callback function that entity-izes ampersands in comments so that
 
13514
     * callbackUndoCommentSubst doesn't clobber them
 
13515
     */
 
13516
    public function callbackArmorCommentEntities($matches) {
 
13517
        return '<!--' . str_replace('&', '&amp;', $matches[1]) . $matches[2];
 
13518
    }
 
13519
 
 
13520
    /**
 
13521
     * Wraps an HTML fragment in the necessary HTML
 
13522
     */
 
13523
    protected function wrapHTML($html, $config, $context) {
 
13524
        $def = $config->getDefinition('HTML');
 
13525
        $ret = '';
 
13526
 
 
13527
        if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
 
13528
            $ret .= '<!DOCTYPE html ';
 
13529
            if (!empty($def->doctype->dtdPublic)) $ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
 
13530
            if (!empty($def->doctype->dtdSystem)) $ret .= '"' . $def->doctype->dtdSystem . '" ';
 
13531
            $ret .= '>';
 
13532
        }
 
13533
 
 
13534
        $ret .= '<html><head>';
 
13535
        $ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
 
13536
        // No protection if $html contains a stray </div>!
 
13537
        $ret .= '</head><body><div>'.$html.'</div></body></html>';
 
13538
        return $ret;
 
13539
    }
 
13540
 
 
13541
}
 
13542
 
 
13543
 
 
13544
 
 
13545
 
 
13546
 
 
13547
/**
 
13548
 * Our in-house implementation of a parser.
 
13549
 *
 
13550
 * A pure PHP parser, DirectLex has absolutely no dependencies, making
 
13551
 * it a reasonably good default for PHP4.  Written with efficiency in mind,
 
13552
 * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
 
13553
 * pales in comparison to HTMLPurifier_Lexer_DOMLex.
 
13554
 *
 
13555
 * @todo Reread XML spec and document differences.
 
13556
 */
 
13557
class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
 
13558
{
 
13559
 
 
13560
    public $tracksLineNumbers = true;
 
13561
 
 
13562
    /**
 
13563
     * Whitespace characters for str(c)spn.
 
13564
     */
 
13565
    protected $_whitespace = "\x20\x09\x0D\x0A";
 
13566
 
 
13567
    /**
 
13568
     * Callback function for script CDATA fudge
 
13569
     * @param $matches, in form of array(opening tag, contents, closing tag)
 
13570
     */
 
13571
    protected function scriptCallback($matches) {
 
13572
        return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
 
13573
    }
 
13574
 
 
13575
    public function tokenizeHTML($html, $config, $context) {
 
13576
 
 
13577
        // special normalization for script tags without any armor
 
13578
        // our "armor" heurstic is a < sign any number of whitespaces after
 
13579
        // the first script tag
 
13580
        if ($config->get('HTML.Trusted')) {
 
13581
            $html = preg_replace_callback('#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
 
13582
                array($this, 'scriptCallback'), $html);
 
13583
        }
 
13584
 
 
13585
        $html = $this->normalize($html, $config, $context);
 
13586
 
 
13587
        $cursor = 0; // our location in the text
 
13588
        $inside_tag = false; // whether or not we're parsing the inside of a tag
 
13589
        $array = array(); // result array
 
13590
 
 
13591
        // This is also treated to mean maintain *column* numbers too
 
13592
        $maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
 
13593
 
 
13594
        if ($maintain_line_numbers === null) {
 
13595
            // automatically determine line numbering by checking
 
13596
            // if error collection is on
 
13597
            $maintain_line_numbers = $config->get('Core.CollectErrors');
 
13598
        }
 
13599
 
 
13600
        if ($maintain_line_numbers) {
 
13601
            $current_line = 1;
 
13602
            $current_col  = 0;
 
13603
            $length = strlen($html);
 
13604
        } else {
 
13605
            $current_line = false;
 
13606
            $current_col  = false;
 
13607
            $length = false;
 
13608
        }
 
13609
        $context->register('CurrentLine', $current_line);
 
13610
        $context->register('CurrentCol',  $current_col);
 
13611
        $nl = "\n";
 
13612
        // how often to manually recalculate. This will ALWAYS be right,
 
13613
        // but it's pretty wasteful. Set to 0 to turn off
 
13614
        $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
 
13615
 
 
13616
        $e = false;
 
13617
        if ($config->get('Core.CollectErrors')) {
 
13618
            $e =& $context->get('ErrorCollector');
 
13619
        }
 
13620
 
 
13621
        // for testing synchronization
 
13622
        $loops = 0;
 
13623
 
 
13624
        while(++$loops) {
 
13625
 
 
13626
            // $cursor is either at the start of a token, or inside of
 
13627
            // a tag (i.e. there was a < immediately before it), as indicated
 
13628
            // by $inside_tag
 
13629
 
 
13630
            if ($maintain_line_numbers) {
 
13631
 
 
13632
                // $rcursor, however, is always at the start of a token.
 
13633
                $rcursor = $cursor - (int) $inside_tag;
 
13634
 
 
13635
                // Column number is cheap, so we calculate it every round.
 
13636
                // We're interested at the *end* of the newline string, so
 
13637
                // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
 
13638
                // from our "rcursor" position.
 
13639
                $nl_pos = strrpos($html, $nl, $rcursor - $length);
 
13640
                $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
 
13641
 
 
13642
                // recalculate lines
 
13643
                if (
 
13644
                    $synchronize_interval &&  // synchronization is on
 
13645
                    $cursor > 0 &&            // cursor is further than zero
 
13646
                    $loops % $synchronize_interval === 0 // time to synchronize!
 
13647
                ) {
 
13648
                    $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
 
13649
                }
 
13650
 
 
13651
            }
 
13652
 
 
13653
            $position_next_lt = strpos($html, '<', $cursor);
 
13654
            $position_next_gt = strpos($html, '>', $cursor);
 
13655
 
 
13656
            // triggers on "<b>asdf</b>" but not "asdf <b></b>"
 
13657
            // special case to set up context
 
13658
            if ($position_next_lt === $cursor) {
 
13659
                $inside_tag = true;
 
13660
                $cursor++;
 
13661
            }
 
13662
 
 
13663
            if (!$inside_tag && $position_next_lt !== false) {
 
13664
                // We are not inside tag and there still is another tag to parse
 
13665
                $token = new
 
13666
                    HTMLPurifier_Token_Text(
 
13667
                        $this->parseData(
 
13668
                            substr(
 
13669
                                $html, $cursor, $position_next_lt - $cursor
 
13670
                            )
 
13671
                        )
 
13672
                    );
 
13673
                if ($maintain_line_numbers) {
 
13674
                    $token->rawPosition($current_line, $current_col);
 
13675
                    $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
 
13676
                }
 
13677
                $array[] = $token;
 
13678
                $cursor  = $position_next_lt + 1;
 
13679
                $inside_tag = true;
 
13680
                continue;
 
13681
            } elseif (!$inside_tag) {
 
13682
                // We are not inside tag but there are no more tags
 
13683
                // If we're already at the end, break
 
13684
                if ($cursor === strlen($html)) break;
 
13685
                // Create Text of rest of string
 
13686
                $token = new
 
13687
                    HTMLPurifier_Token_Text(
 
13688
                        $this->parseData(
 
13689
                            substr(
 
13690
                                $html, $cursor
 
13691
                            )
 
13692
                        )
 
13693
                    );
 
13694
                if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
 
13695
                $array[] = $token;
 
13696
                break;
 
13697
            } elseif ($inside_tag && $position_next_gt !== false) {
 
13698
                // We are in tag and it is well formed
 
13699
                // Grab the internals of the tag
 
13700
                $strlen_segment = $position_next_gt - $cursor;
 
13701
 
 
13702
                if ($strlen_segment < 1) {
 
13703
                    // there's nothing to process!
 
13704
                    $token = new HTMLPurifier_Token_Text('<');
 
13705
                    $cursor++;
 
13706
                    continue;
 
13707
                }
 
13708
 
 
13709
                $segment = substr($html, $cursor, $strlen_segment);
 
13710
 
 
13711
                if ($segment === false) {
 
13712
                    // somehow, we attempted to access beyond the end of
 
13713
                    // the string, defense-in-depth, reported by Nate Abele
 
13714
                    break;
 
13715
                }
 
13716
 
 
13717
                // Check if it's a comment
 
13718
                if (
 
13719
                    substr($segment, 0, 3) === '!--'
 
13720
                ) {
 
13721
                    // re-determine segment length, looking for -->
 
13722
                    $position_comment_end = strpos($html, '-->', $cursor);
 
13723
                    if ($position_comment_end === false) {
 
13724
                        // uh oh, we have a comment that extends to
 
13725
                        // infinity. Can't be helped: set comment
 
13726
                        // end position to end of string
 
13727
                        if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
 
13728
                        $position_comment_end = strlen($html);
 
13729
                        $end = true;
 
13730
                    } else {
 
13731
                        $end = false;
 
13732
                    }
 
13733
                    $strlen_segment = $position_comment_end - $cursor;
 
13734
                    $segment = substr($html, $cursor, $strlen_segment);
 
13735
                    $token = new
 
13736
                        HTMLPurifier_Token_Comment(
 
13737
                            substr(
 
13738
                                $segment, 3, $strlen_segment - 3
 
13739
                            )
 
13740
                        );
 
13741
                    if ($maintain_line_numbers) {
 
13742
                        $token->rawPosition($current_line, $current_col);
 
13743
                        $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
 
13744
                    }
 
13745
                    $array[] = $token;
 
13746
                    $cursor = $end ? $position_comment_end : $position_comment_end + 3;
 
13747
                    $inside_tag = false;
 
13748
                    continue;
 
13749
                }
 
13750
 
 
13751
                // Check if it's an end tag
 
13752
                $is_end_tag = (strpos($segment,'/') === 0);
 
13753
                if ($is_end_tag) {
 
13754
                    $type = substr($segment, 1);
 
13755
                    $token = new HTMLPurifier_Token_End($type);
 
13756
                    if ($maintain_line_numbers) {
 
13757
                        $token->rawPosition($current_line, $current_col);
 
13758
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
 
13759
                    }
 
13760
                    $array[] = $token;
 
13761
                    $inside_tag = false;
 
13762
                    $cursor = $position_next_gt + 1;
 
13763
                    continue;
 
13764
                }
 
13765
 
 
13766
                // Check leading character is alnum, if not, we may
 
13767
                // have accidently grabbed an emoticon. Translate into
 
13768
                // text and go our merry way
 
13769
                if (!ctype_alpha($segment[0])) {
 
13770
                    // XML:  $segment[0] !== '_' && $segment[0] !== ':'
 
13771
                    if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt');
 
13772
                    $token = new HTMLPurifier_Token_Text('<');
 
13773
                    if ($maintain_line_numbers) {
 
13774
                        $token->rawPosition($current_line, $current_col);
 
13775
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
 
13776
                    }
 
13777
                    $array[] = $token;
 
13778
                    $inside_tag = false;
 
13779
                    continue;
 
13780
                }
 
13781
 
 
13782
                // Check if it is explicitly self closing, if so, remove
 
13783
                // trailing slash. Remember, we could have a tag like <br>, so
 
13784
                // any later token processing scripts must convert improperly
 
13785
                // classified EmptyTags from StartTags.
 
13786
                $is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
 
13787
                if ($is_self_closing) {
 
13788
                    $strlen_segment--;
 
13789
                    $segment = substr($segment, 0, $strlen_segment);
 
13790
                }
 
13791
 
 
13792
                // Check if there are any attributes
 
13793
                $position_first_space = strcspn($segment, $this->_whitespace);
 
13794
 
 
13795
                if ($position_first_space >= $strlen_segment) {
 
13796
                    if ($is_self_closing) {
 
13797
                        $token = new HTMLPurifier_Token_Empty($segment);
 
13798
                    } else {
 
13799
                        $token = new HTMLPurifier_Token_Start($segment);
 
13800
                    }
 
13801
                    if ($maintain_line_numbers) {
 
13802
                        $token->rawPosition($current_line, $current_col);
 
13803
                        $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
 
13804
                    }
 
13805
                    $array[] = $token;
 
13806
                    $inside_tag = false;
 
13807
                    $cursor = $position_next_gt + 1;
 
13808
                    continue;
 
13809
                }
 
13810
 
 
13811
                // Grab out all the data
 
13812
                $type = substr($segment, 0, $position_first_space);
 
13813
                $attribute_string =
 
13814
                    trim(
 
13815
                        substr(
 
13816
                            $segment, $position_first_space
 
13817
                        )
 
13818
                    );
 
13819
                if ($attribute_string) {
 
13820
                    $attr = $this->parseAttributeString(
 
13821
                                    $attribute_string
 
13822
                                  , $config, $context
 
13823
                              );
 
13824
                } else {
 
13825
                    $attr = array();
 
13826
                }
 
13827
 
 
13828
                if ($is_self_closing) {
 
13829
                    $token = new HTMLPurifier_Token_Empty($type, $attr);
 
13830
                } else {
 
13831
                    $token = new HTMLPurifier_Token_Start($type, $attr);
 
13832
                }
 
13833
                if ($maintain_line_numbers) {
 
13834
                    $token->rawPosition($current_line, $current_col);
 
13835
                    $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
 
13836
                }
 
13837
                $array[] = $token;
 
13838
                $cursor = $position_next_gt + 1;
 
13839
                $inside_tag = false;
 
13840
                continue;
 
13841
            } else {
 
13842
                // inside tag, but there's no ending > sign
 
13843
                if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
 
13844
                $token = new
 
13845
                    HTMLPurifier_Token_Text(
 
13846
                        '<' .
 
13847
                        $this->parseData(
 
13848
                            substr($html, $cursor)
 
13849
                        )
 
13850
                    );
 
13851
                if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
 
13852
                // no cursor scroll? Hmm...
 
13853
                $array[] = $token;
 
13854
                break;
 
13855
            }
 
13856
            break;
 
13857
        }
 
13858
 
 
13859
        $context->destroy('CurrentLine');
 
13860
        $context->destroy('CurrentCol');
 
13861
        return $array;
 
13862
    }
 
13863
 
 
13864
    /**
 
13865
     * PHP 5.0.x compatible substr_count that implements offset and length
 
13866
     */
 
13867
    protected function substrCount($haystack, $needle, $offset, $length) {
 
13868
        static $oldVersion;
 
13869
        if ($oldVersion === null) {
 
13870
            $oldVersion = version_compare(PHP_VERSION, '5.1', '<');
 
13871
        }
 
13872
        if ($oldVersion) {
 
13873
            $haystack = substr($haystack, $offset, $length);
 
13874
            return substr_count($haystack, $needle);
 
13875
        } else {
 
13876
            return substr_count($haystack, $needle, $offset, $length);
 
13877
        }
 
13878
    }
 
13879
 
 
13880
    /**
 
13881
     * Takes the inside of an HTML tag and makes an assoc array of attributes.
 
13882
     *
 
13883
     * @param $string Inside of tag excluding name.
 
13884
     * @returns Assoc array of attributes.
 
13885
     */
 
13886
    public function parseAttributeString($string, $config, $context) {
 
13887
        $string = (string) $string; // quick typecast
 
13888
 
 
13889
        if ($string == '') return array(); // no attributes
 
13890
 
 
13891
        $e = false;
 
13892
        if ($config->get('Core.CollectErrors')) {
 
13893
            $e =& $context->get('ErrorCollector');
 
13894
        }
 
13895
 
 
13896
        // let's see if we can abort as quickly as possible
 
13897
        // one equal sign, no spaces => one attribute
 
13898
        $num_equal = substr_count($string, '=');
 
13899
        $has_space = strpos($string, ' ');
 
13900
        if ($num_equal === 0 && !$has_space) {
 
13901
            // bool attribute
 
13902
            return array($string => $string);
 
13903
        } elseif ($num_equal === 1 && !$has_space) {
 
13904
            // only one attribute
 
13905
            list($key, $quoted_value) = explode('=', $string);
 
13906
            $quoted_value = trim($quoted_value);
 
13907
            if (!$key) {
 
13908
                if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
 
13909
                return array();
 
13910
            }
 
13911
            if (!$quoted_value) return array($key => '');
 
13912
            $first_char = @$quoted_value[0];
 
13913
            $last_char  = @$quoted_value[strlen($quoted_value)-1];
 
13914
 
 
13915
            $same_quote = ($first_char == $last_char);
 
13916
            $open_quote = ($first_char == '"' || $first_char == "'");
 
13917
 
 
13918
            if ( $same_quote && $open_quote) {
 
13919
                // well behaved
 
13920
                $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
 
13921
            } else {
 
13922
                // not well behaved
 
13923
                if ($open_quote) {
 
13924
                    if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
 
13925
                    $value = substr($quoted_value, 1);
 
13926
                } else {
 
13927
                    $value = $quoted_value;
 
13928
                }
 
13929
            }
 
13930
            if ($value === false) $value = '';
 
13931
            return array($key => $value);
 
13932
        }
 
13933
 
 
13934
        // setup loop environment
 
13935
        $array  = array(); // return assoc array of attributes
 
13936
        $cursor = 0; // current position in string (moves forward)
 
13937
        $size   = strlen($string); // size of the string (stays the same)
 
13938
 
 
13939
        // if we have unquoted attributes, the parser expects a terminating
 
13940
        // space, so let's guarantee that there's always a terminating space.
 
13941
        $string .= ' ';
 
13942
 
 
13943
        while(true) {
 
13944
 
 
13945
            if ($cursor >= $size) {
 
13946
                break;
 
13947
            }
 
13948
 
 
13949
            $cursor += ($value = strspn($string, $this->_whitespace, $cursor));
 
13950
            // grab the key
 
13951
 
 
13952
            $key_begin = $cursor; //we're currently at the start of the key
 
13953
 
 
13954
            // scroll past all characters that are the key (not whitespace or =)
 
13955
            $cursor += strcspn($string, $this->_whitespace . '=', $cursor);
 
13956
 
 
13957
            $key_end = $cursor; // now at the end of the key
 
13958
 
 
13959
            $key = substr($string, $key_begin, $key_end - $key_begin);
 
13960
 
 
13961
            if (!$key) {
 
13962
                if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
 
13963
                $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
 
13964
                continue; // empty key
 
13965
            }
 
13966
 
 
13967
            // scroll past all whitespace
 
13968
            $cursor += strspn($string, $this->_whitespace, $cursor);
 
13969
 
 
13970
            if ($cursor >= $size) {
 
13971
                $array[$key] = $key;
 
13972
                break;
 
13973
            }
 
13974
 
 
13975
            // if the next character is an equal sign, we've got a regular
 
13976
            // pair, otherwise, it's a bool attribute
 
13977
            $first_char = @$string[$cursor];
 
13978
 
 
13979
            if ($first_char == '=') {
 
13980
                // key="value"
 
13981
 
 
13982
                $cursor++;
 
13983
                $cursor += strspn($string, $this->_whitespace, $cursor);
 
13984
 
 
13985
                if ($cursor === false) {
 
13986
                    $array[$key] = '';
 
13987
                    break;
 
13988
                }
 
13989
 
 
13990
                // we might be in front of a quote right now
 
13991
 
 
13992
                $char = @$string[$cursor];
 
13993
 
 
13994
                if ($char == '"' || $char == "'") {
 
13995
                    // it's quoted, end bound is $char
 
13996
                    $cursor++;
 
13997
                    $value_begin = $cursor;
 
13998
                    $cursor = strpos($string, $char, $cursor);
 
13999
                    $value_end = $cursor;
 
14000
                } else {
 
14001
                    // it's not quoted, end bound is whitespace
 
14002
                    $value_begin = $cursor;
 
14003
                    $cursor += strcspn($string, $this->_whitespace, $cursor);
 
14004
                    $value_end = $cursor;
 
14005
                }
 
14006
 
 
14007
                // we reached a premature end
 
14008
                if ($cursor === false) {
 
14009
                    $cursor = $size;
 
14010
                    $value_end = $cursor;
 
14011
                }
 
14012
 
 
14013
                $value = substr($string, $value_begin, $value_end - $value_begin);
 
14014
                if ($value === false) $value = '';
 
14015
                $array[$key] = $this->parseData($value);
 
14016
                $cursor++;
 
14017
 
 
14018
            } else {
 
14019
                // boolattr
 
14020
                if ($key !== '') {
 
14021
                    $array[$key] = $key;
 
14022
                } else {
 
14023
                    // purely theoretical
 
14024
                    if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
 
14025
                }
 
14026
 
 
14027
            }
 
14028
        }
 
14029
        return $array;
 
14030
    }
 
14031
 
 
14032
}
 
14033
 
 
14034
 
 
14035
 
 
14036
 
 
14037
 
 
14038
/**
 
14039
 * Composite strategy that runs multiple strategies on tokens.
 
14040
 */
 
14041
abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
 
14042
{
 
14043
 
 
14044
    /**
 
14045
     * List of strategies to run tokens through.
 
14046
     */
 
14047
    protected $strategies = array();
 
14048
 
 
14049
    abstract public function __construct();
 
14050
 
 
14051
    public function execute($tokens, $config, $context) {
 
14052
        foreach ($this->strategies as $strategy) {
 
14053
            $tokens = $strategy->execute($tokens, $config, $context);
 
14054
        }
 
14055
        return $tokens;
 
14056
    }
 
14057
 
 
14058
}
 
14059
 
 
14060
 
 
14061
 
 
14062
 
 
14063
 
 
14064
/**
 
14065
 * Core strategy composed of the big four strategies.
 
14066
 */
 
14067
class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite
 
14068
{
 
14069
 
 
14070
    public function __construct() {
 
14071
        $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
 
14072
        $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
 
14073
        $this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
 
14074
        $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
 
14075
    }
 
14076
 
 
14077
}
 
14078
 
 
14079
 
 
14080
 
 
14081
 
 
14082
 
 
14083
/**
 
14084
 * Takes a well formed list of tokens and fixes their nesting.
 
14085
 *
 
14086
 * HTML elements dictate which elements are allowed to be their children,
 
14087
 * for example, you can't have a p tag in a span tag.  Other elements have
 
14088
 * much more rigorous definitions: tables, for instance, require a specific
 
14089
 * order for their elements.  There are also constraints not expressible by
 
14090
 * document type definitions, such as the chameleon nature of ins/del
 
14091
 * tags and global child exclusions.
 
14092
 *
 
14093
 * The first major objective of this strategy is to iterate through all the
 
14094
 * nodes (not tokens) of the list of tokens and determine whether or not
 
14095
 * their children conform to the element's definition.  If they do not, the
 
14096
 * child definition may optionally supply an amended list of elements that
 
14097
 * is valid or require that the entire node be deleted (and the previous
 
14098
 * node rescanned).
 
14099
 *
 
14100
 * The second objective is to ensure that explicitly excluded elements of
 
14101
 * an element do not appear in its children.  Code that accomplishes this
 
14102
 * task is pervasive through the strategy, though the two are distinct tasks
 
14103
 * and could, theoretically, be seperated (although it's not recommended).
 
14104
 *
 
14105
 * @note Whether or not unrecognized children are silently dropped or
 
14106
 *       translated into text depends on the child definitions.
 
14107
 *
 
14108
 * @todo Enable nodes to be bubbled out of the structure.
 
14109
 */
 
14110
 
 
14111
class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
 
14112
{
 
14113
 
 
14114
    public function execute($tokens, $config, $context) {
 
14115
        //####################################################################//
 
14116
        // Pre-processing
 
14117
 
 
14118
        // get a copy of the HTML definition
 
14119
        $definition = $config->getHTMLDefinition();
 
14120
 
 
14121
        // insert implicit "parent" node, will be removed at end.
 
14122
        // DEFINITION CALL
 
14123
        $parent_name = $definition->info_parent;
 
14124
        array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name));
 
14125
        $tokens[] = new HTMLPurifier_Token_End($parent_name);
 
14126
 
 
14127
        // setup the context variable 'IsInline', for chameleon processing
 
14128
        // is 'false' when we are not inline, 'true' when it must always
 
14129
        // be inline, and an integer when it is inline for a certain
 
14130
        // branch of the document tree
 
14131
        $is_inline = $definition->info_parent_def->descendants_are_inline;
 
14132
        $context->register('IsInline', $is_inline);
 
14133
 
 
14134
        // setup error collector
 
14135
        $e =& $context->get('ErrorCollector', true);
 
14136
 
 
14137
        //####################################################################//
 
14138
        // Loop initialization
 
14139
 
 
14140
        // stack that contains the indexes of all parents,
 
14141
        // $stack[count($stack)-1] being the current parent
 
14142
        $stack = array();
 
14143
 
 
14144
        // stack that contains all elements that are excluded
 
14145
        // it is organized by parent elements, similar to $stack,
 
14146
        // but it is only populated when an element with exclusions is
 
14147
        // processed, i.e. there won't be empty exclusions.
 
14148
        $exclude_stack = array();
 
14149
 
 
14150
        // variable that contains the start token while we are processing
 
14151
        // nodes. This enables error reporting to do its job
 
14152
        $start_token = false;
 
14153
        $context->register('CurrentToken', $start_token);
 
14154
 
 
14155
        //####################################################################//
 
14156
        // Loop
 
14157
 
 
14158
        // iterate through all start nodes. Determining the start node
 
14159
        // is complicated so it has been omitted from the loop construct
 
14160
        for ($i = 0, $size = count($tokens) ; $i < $size; ) {
 
14161
 
 
14162
            //################################################################//
 
14163
            // Gather information on children
 
14164
 
 
14165
            // child token accumulator
 
14166
            $child_tokens = array();
 
14167
 
 
14168
            // scroll to the end of this node, report number, and collect
 
14169
            // all children
 
14170
            for ($j = $i, $depth = 0; ; $j++) {
 
14171
                if ($tokens[$j] instanceof HTMLPurifier_Token_Start) {
 
14172
                    $depth++;
 
14173
                    // skip token assignment on first iteration, this is the
 
14174
                    // token we currently are on
 
14175
                    if ($depth == 1) continue;
 
14176
                } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) {
 
14177
                    $depth--;
 
14178
                    // skip token assignment on last iteration, this is the
 
14179
                    // end token of the token we're currently on
 
14180
                    if ($depth == 0) break;
 
14181
                }
 
14182
                $child_tokens[] = $tokens[$j];
 
14183
            }
 
14184
 
 
14185
            // $i is index of start token
 
14186
            // $j is index of end token
 
14187
 
 
14188
            $start_token = $tokens[$i]; // to make token available via CurrentToken
 
14189
 
 
14190
            //################################################################//
 
14191
            // Gather information on parent
 
14192
 
 
14193
            // calculate parent information
 
14194
            if ($count = count($stack)) {
 
14195
                $parent_index = $stack[$count-1];
 
14196
                $parent_name  = $tokens[$parent_index]->name;
 
14197
                if ($parent_index == 0) {
 
14198
                    $parent_def   = $definition->info_parent_def;
 
14199
                } else {
 
14200
                    $parent_def   = $definition->info[$parent_name];
 
14201
                }
 
14202
            } else {
 
14203
                // processing as if the parent were the "root" node
 
14204
                // unknown info, it won't be used anyway, in the future,
 
14205
                // we may want to enforce one element only (this is
 
14206
                // necessary for HTML Purifier to clean entire documents
 
14207
                $parent_index = $parent_name = $parent_def = null;
 
14208
            }
 
14209
 
 
14210
            // calculate context
 
14211
            if ($is_inline === false) {
 
14212
                // check if conditions make it inline
 
14213
                if (!empty($parent_def) && $parent_def->descendants_are_inline) {
 
14214
                    $is_inline = $count - 1;
 
14215
                }
 
14216
            } else {
 
14217
                // check if we're out of inline
 
14218
                if ($count === $is_inline) {
 
14219
                    $is_inline = false;
 
14220
                }
 
14221
            }
 
14222
 
 
14223
            //################################################################//
 
14224
            // Determine whether element is explicitly excluded SGML-style
 
14225
 
 
14226
            // determine whether or not element is excluded by checking all
 
14227
            // parent exclusions. The array should not be very large, two
 
14228
            // elements at most.
 
14229
            $excluded = false;
 
14230
            if (!empty($exclude_stack)) {
 
14231
                foreach ($exclude_stack as $lookup) {
 
14232
                    if (isset($lookup[$tokens[$i]->name])) {
 
14233
                        $excluded = true;
 
14234
                        // no need to continue processing
 
14235
                        break;
 
14236
                    }
 
14237
                }
 
14238
            }
 
14239
 
 
14240
            //################################################################//
 
14241
            // Perform child validation
 
14242
 
 
14243
            if ($excluded) {
 
14244
                // there is an exclusion, remove the entire node
 
14245
                $result = false;
 
14246
                $excludes = array(); // not used, but good to initialize anyway
 
14247
            } else {
 
14248
                // DEFINITION CALL
 
14249
                if ($i === 0) {
 
14250
                    // special processing for the first node
 
14251
                    $def = $definition->info_parent_def;
 
14252
                } else {
 
14253
                    $def = $definition->info[$tokens[$i]->name];
 
14254
 
 
14255
                }
 
14256
 
 
14257
                if (!empty($def->child)) {
 
14258
                    // have DTD child def validate children
 
14259
                    $result = $def->child->validateChildren(
 
14260
                        $child_tokens, $config, $context);
 
14261
                } else {
 
14262
                    // weird, no child definition, get rid of everything
 
14263
                    $result = false;
 
14264
                }
 
14265
 
 
14266
                // determine whether or not this element has any exclusions
 
14267
                $excludes = $def->excludes;
 
14268
            }
 
14269
 
 
14270
            // $result is now a bool or array
 
14271
 
 
14272
            //################################################################//
 
14273
            // Process result by interpreting $result
 
14274
 
 
14275
            if ($result === true || $child_tokens === $result) {
 
14276
                // leave the node as is
 
14277
 
 
14278
                // register start token as a parental node start
 
14279
                $stack[] = $i;
 
14280
 
 
14281
                // register exclusions if there are any
 
14282
                if (!empty($excludes)) $exclude_stack[] = $excludes;
 
14283
 
 
14284
                // move cursor to next possible start node
 
14285
                $i++;
 
14286
 
 
14287
            } elseif($result === false) {
 
14288
                // remove entire node
 
14289
 
 
14290
                if ($e) {
 
14291
                    if ($excluded) {
 
14292
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
 
14293
                    } else {
 
14294
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
 
14295
                    }
 
14296
                }
 
14297
 
 
14298
                // calculate length of inner tokens and current tokens
 
14299
                $length = $j - $i + 1;
 
14300
 
 
14301
                // perform removal
 
14302
                array_splice($tokens, $i, $length);
 
14303
 
 
14304
                // update size
 
14305
                $size -= $length;
 
14306
 
 
14307
                // there is no start token to register,
 
14308
                // current node is now the next possible start node
 
14309
                // unless it turns out that we need to do a double-check
 
14310
 
 
14311
                // this is a rought heuristic that covers 100% of HTML's
 
14312
                // cases and 99% of all other cases. A child definition
 
14313
                // that would be tricked by this would be something like:
 
14314
                // ( | a b c) where it's all or nothing. Fortunately,
 
14315
                // our current implementation claims that that case would
 
14316
                // not allow empty, even if it did
 
14317
                if (!$parent_def->child->allow_empty) {
 
14318
                    // we need to do a double-check
 
14319
                    $i = $parent_index;
 
14320
                    array_pop($stack);
 
14321
                }
 
14322
 
 
14323
                // PROJECTED OPTIMIZATION: Process all children elements before
 
14324
                // reprocessing parent node.
 
14325
 
 
14326
            } else {
 
14327
                // replace node with $result
 
14328
 
 
14329
                // calculate length of inner tokens
 
14330
                $length = $j - $i - 1;
 
14331
 
 
14332
                if ($e) {
 
14333
                    if (empty($result) && $length) {
 
14334
                        $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
 
14335
                    } else {
 
14336
                        $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
 
14337
                    }
 
14338
                }
 
14339
 
 
14340
                // perform replacement
 
14341
                array_splice($tokens, $i + 1, $length, $result);
 
14342
 
 
14343
                // update size
 
14344
                $size -= $length;
 
14345
                $size += count($result);
 
14346
 
 
14347
                // register start token as a parental node start
 
14348
                $stack[] = $i;
 
14349
 
 
14350
                // register exclusions if there are any
 
14351
                if (!empty($excludes)) $exclude_stack[] = $excludes;
 
14352
 
 
14353
                // move cursor to next possible start node
 
14354
                $i++;
 
14355
 
 
14356
            }
 
14357
 
 
14358
            //################################################################//
 
14359
            // Scroll to next start node
 
14360
 
 
14361
            // We assume, at this point, that $i is the index of the token
 
14362
            // that is the first possible new start point for a node.
 
14363
 
 
14364
            // Test if the token indeed is a start tag, if not, move forward
 
14365
            // and test again.
 
14366
            $size = count($tokens);
 
14367
            while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) {
 
14368
                if ($tokens[$i] instanceof HTMLPurifier_Token_End) {
 
14369
                    // pop a token index off the stack if we ended a node
 
14370
                    array_pop($stack);
 
14371
                    // pop an exclusion lookup off exclusion stack if
 
14372
                    // we ended node and that node had exclusions
 
14373
                    if ($i == 0 || $i == $size - 1) {
 
14374
                        // use specialized var if it's the super-parent
 
14375
                        $s_excludes = $definition->info_parent_def->excludes;
 
14376
                    } else {
 
14377
                        $s_excludes = $definition->info[$tokens[$i]->name]->excludes;
 
14378
                    }
 
14379
                    if ($s_excludes) {
 
14380
                        array_pop($exclude_stack);
 
14381
                    }
 
14382
                }
 
14383
                $i++;
 
14384
            }
 
14385
 
 
14386
        }
 
14387
 
 
14388
        //####################################################################//
 
14389
        // Post-processing
 
14390
 
 
14391
        // remove implicit parent tokens at the beginning and end
 
14392
        array_shift($tokens);
 
14393
        array_pop($tokens);
 
14394
 
 
14395
        // remove context variables
 
14396
        $context->destroy('IsInline');
 
14397
        $context->destroy('CurrentToken');
 
14398
 
 
14399
        //####################################################################//
 
14400
        // Return
 
14401
 
 
14402
        return $tokens;
 
14403
 
 
14404
    }
 
14405
 
 
14406
}
 
14407
 
 
14408
 
 
14409
 
 
14410
 
 
14411
 
 
14412
/**
 
14413
 * Takes tokens makes them well-formed (balance end tags, etc.)
 
14414
 */
 
14415
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
 
14416
{
 
14417
 
 
14418
    /**
 
14419
     * Array stream of tokens being processed.
 
14420
     */
 
14421
    protected $tokens;
 
14422
 
 
14423
    /**
 
14424
     * Current index in $tokens.
 
14425
     */
 
14426
    protected $t;
 
14427
 
 
14428
    /**
 
14429
     * Current nesting of elements.
 
14430
     */
 
14431
    protected $stack;
 
14432
 
 
14433
    /**
 
14434
     * Injectors active in this stream processing.
 
14435
     */
 
14436
    protected $injectors;
 
14437
 
 
14438
    /**
 
14439
     * Current instance of HTMLPurifier_Config.
 
14440
     */
 
14441
    protected $config;
 
14442
 
 
14443
    /**
 
14444
     * Current instance of HTMLPurifier_Context.
 
14445
     */
 
14446
    protected $context;
 
14447
 
 
14448
    public function execute($tokens, $config, $context) {
 
14449
 
 
14450
        $definition = $config->getHTMLDefinition();
 
14451
 
 
14452
        // local variables
 
14453
        $generator = new HTMLPurifier_Generator($config, $context);
 
14454
        $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
 
14455
        $e = $context->get('ErrorCollector', true);
 
14456
        $t = false; // token index
 
14457
        $i = false; // injector index
 
14458
        $token      = false; // the current token
 
14459
        $reprocess  = false; // whether or not to reprocess the same token
 
14460
        $stack = array();
 
14461
 
 
14462
        // member variables
 
14463
        $this->stack   =& $stack;
 
14464
        $this->t       =& $t;
 
14465
        $this->tokens  =& $tokens;
 
14466
        $this->config  = $config;
 
14467
        $this->context = $context;
 
14468
 
 
14469
        // context variables
 
14470
        $context->register('CurrentNesting', $stack);
 
14471
        $context->register('InputIndex',     $t);
 
14472
        $context->register('InputTokens',    $tokens);
 
14473
        $context->register('CurrentToken',   $token);
 
14474
 
 
14475
        // -- begin INJECTOR --
 
14476
 
 
14477
        $this->injectors = array();
 
14478
 
 
14479
        $injectors = $config->getBatch('AutoFormat');
 
14480
        $def_injectors = $definition->info_injector;
 
14481
        $custom_injectors = $injectors['Custom'];
 
14482
        unset($injectors['Custom']); // special case
 
14483
        foreach ($injectors as $injector => $b) {
 
14484
            // XXX: Fix with a legitimate lookup table of enabled filters
 
14485
            if (strpos($injector, '.') !== false) continue;
 
14486
            $injector = "HTMLPurifier_Injector_$injector";
 
14487
            if (!$b) continue;
 
14488
            $this->injectors[] = new $injector;
 
14489
        }
 
14490
        foreach ($def_injectors as $injector) {
 
14491
            // assumed to be objects
 
14492
            $this->injectors[] = $injector;
 
14493
        }
 
14494
        foreach ($custom_injectors as $injector) {
 
14495
            if (is_string($injector)) {
 
14496
                $injector = "HTMLPurifier_Injector_$injector";
 
14497
                $injector = new $injector;
 
14498
            }
 
14499
            $this->injectors[] = $injector;
 
14500
        }
 
14501
 
 
14502
        // give the injectors references to the definition and context
 
14503
        // variables for performance reasons
 
14504
        foreach ($this->injectors as $ix => $injector) {
 
14505
            $error = $injector->prepare($config, $context);
 
14506
            if (!$error) continue;
 
14507
            array_splice($this->injectors, $ix, 1); // rm the injector
 
14508
            trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
 
14509
        }
 
14510
 
 
14511
        // -- end INJECTOR --
 
14512
 
 
14513
        // a note on punting:
 
14514
        //      In order to reduce code duplication, whenever some code needs
 
14515
        //      to make HTML changes in order to make things "correct", the
 
14516
        //      new HTML gets sent through the purifier, regardless of its
 
14517
        //      status. This means that if we add a start token, because it
 
14518
        //      was totally necessary, we don't have to update nesting; we just
 
14519
        //      punt ($reprocess = true; continue;) and it does that for us.
 
14520
 
 
14521
        // isset is in loop because $tokens size changes during loop exec
 
14522
        for (
 
14523
            $t = 0;
 
14524
            $t == 0 || isset($tokens[$t - 1]);
 
14525
            // only increment if we don't need to reprocess
 
14526
            $reprocess ? $reprocess = false : $t++
 
14527
        ) {
 
14528
 
 
14529
            // check for a rewind
 
14530
            if (is_int($i) && $i >= 0) {
 
14531
                // possibility: disable rewinding if the current token has a
 
14532
                // rewind set on it already. This would offer protection from
 
14533
                // infinite loop, but might hinder some advanced rewinding.
 
14534
                $rewind_to = $this->injectors[$i]->getRewind();
 
14535
                if (is_int($rewind_to) && $rewind_to < $t) {
 
14536
                    if ($rewind_to < 0) $rewind_to = 0;
 
14537
                    while ($t > $rewind_to) {
 
14538
                        $t--;
 
14539
                        $prev = $tokens[$t];
 
14540
                        // indicate that other injectors should not process this token,
 
14541
                        // but we need to reprocess it
 
14542
                        unset($prev->skip[$i]);
 
14543
                        $prev->rewind = $i;
 
14544
                        if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
 
14545
                        elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
 
14546
                    }
 
14547
                }
 
14548
                $i = false;
 
14549
            }
 
14550
 
 
14551
            // handle case of document end
 
14552
            if (!isset($tokens[$t])) {
 
14553
                // kill processing if stack is empty
 
14554
                if (empty($this->stack)) break;
 
14555
 
 
14556
                // peek
 
14557
                $top_nesting = array_pop($this->stack);
 
14558
                $this->stack[] = $top_nesting;
 
14559
 
 
14560
                // send error
 
14561
                if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
 
14562
                    $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
 
14563
                }
 
14564
 
 
14565
                // append, don't splice, since this is the end
 
14566
                $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
 
14567
 
 
14568
                // punt!
 
14569
                $reprocess = true;
 
14570
                continue;
 
14571
            }
 
14572
 
 
14573
            $token = $tokens[$t];
 
14574
 
 
14575
            //echo '<br>'; printTokens($tokens, $t); printTokens($this->stack);
 
14576
 
 
14577
            // quick-check: if it's not a tag, no need to process
 
14578
            if (empty($token->is_tag)) {
 
14579
                if ($token instanceof HTMLPurifier_Token_Text) {
 
14580
                    foreach ($this->injectors as $i => $injector) {
 
14581
                        if (isset($token->skip[$i])) continue;
 
14582
                        if ($token->rewind !== null && $token->rewind !== $i) continue;
 
14583
                        $injector->handleText($token);
 
14584
                        $this->processToken($token, $i);
 
14585
                        $reprocess = true;
 
14586
                        break;
 
14587
                    }
 
14588
                }
 
14589
                // another possibility is a comment
 
14590
                continue;
 
14591
            }
 
14592
 
 
14593
            if (isset($definition->info[$token->name])) {
 
14594
                $type = $definition->info[$token->name]->child->type;
 
14595
            } else {
 
14596
                $type = false; // Type is unknown, treat accordingly
 
14597
            }
 
14598
 
 
14599
            // quick tag checks: anything that's *not* an end tag
 
14600
            $ok = false;
 
14601
            if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
 
14602
                // claims to be a start tag but is empty
 
14603
                $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
 
14604
                $ok = true;
 
14605
            } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
 
14606
                // claims to be empty but really is a start tag
 
14607
                $this->swap(new HTMLPurifier_Token_End($token->name));
 
14608
                $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
 
14609
                // punt (since we had to modify the input stream in a non-trivial way)
 
14610
                $reprocess = true;
 
14611
                continue;
 
14612
            } elseif ($token instanceof HTMLPurifier_Token_Empty) {
 
14613
                // real empty token
 
14614
                $ok = true;
 
14615
            } elseif ($token instanceof HTMLPurifier_Token_Start) {
 
14616
                // start tag
 
14617
 
 
14618
                // ...unless they also have to close their parent
 
14619
                if (!empty($this->stack)) {
 
14620
 
 
14621
                    $parent = array_pop($this->stack);
 
14622
                    $this->stack[] = $parent;
 
14623
 
 
14624
                    if (isset($definition->info[$parent->name])) {
 
14625
                        $elements = $definition->info[$parent->name]->child->getAllowedElements($config);
 
14626
                        $autoclose = !isset($elements[$token->name]);
 
14627
                    } else {
 
14628
                        $autoclose = false;
 
14629
                    }
 
14630
 
 
14631
                    $carryover = false;
 
14632
                    if ($autoclose && $definition->info[$parent->name]->formatting) {
 
14633
                        $carryover = true;
 
14634
                    }
 
14635
 
 
14636
                    if ($autoclose) {
 
14637
                        // errors need to be updated
 
14638
                        $new_token = new HTMLPurifier_Token_End($parent->name);
 
14639
                        $new_token->start = $parent;
 
14640
                        if ($carryover) {
 
14641
                            $element = clone $parent;
 
14642
                            $element->armor['MakeWellFormed_TagClosedError'] = true;
 
14643
                            $element->carryover = true;
 
14644
                            $this->processToken(array($new_token, $token, $element));
 
14645
                        } else {
 
14646
                            $this->insertBefore($new_token);
 
14647
                        }
 
14648
                        if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
 
14649
                            if (!$carryover) {
 
14650
                                $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
 
14651
                            } else {
 
14652
                                $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
 
14653
                            }
 
14654
                        }
 
14655
                        $reprocess = true;
 
14656
                        continue;
 
14657
                    }
 
14658
 
 
14659
                }
 
14660
                $ok = true;
 
14661
            }
 
14662
 
 
14663
            if ($ok) {
 
14664
                foreach ($this->injectors as $i => $injector) {
 
14665
                    if (isset($token->skip[$i])) continue;
 
14666
                    if ($token->rewind !== null && $token->rewind !== $i) continue;
 
14667
                    $injector->handleElement($token);
 
14668
                    $this->processToken($token, $i);
 
14669
                    $reprocess = true;
 
14670
                    break;
 
14671
                }
 
14672
                if (!$reprocess) {
 
14673
                    // ah, nothing interesting happened; do normal processing
 
14674
                    $this->swap($token);
 
14675
                    if ($token instanceof HTMLPurifier_Token_Start) {
 
14676
                        $this->stack[] = $token;
 
14677
                    } elseif ($token instanceof HTMLPurifier_Token_End) {
 
14678
                        throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
 
14679
                    }
 
14680
                }
 
14681
                continue;
 
14682
            }
 
14683
 
 
14684
            // sanity check: we should be dealing with a closing tag
 
14685
            if (!$token instanceof HTMLPurifier_Token_End) {
 
14686
                throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
 
14687
            }
 
14688
 
 
14689
            // make sure that we have something open
 
14690
            if (empty($this->stack)) {
 
14691
                if ($escape_invalid_tags) {
 
14692
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
 
14693
                    $this->swap(new HTMLPurifier_Token_Text(
 
14694
                        $generator->generateFromToken($token)
 
14695
                    ));
 
14696
                } else {
 
14697
                    $this->remove();
 
14698
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
 
14699
                }
 
14700
                $reprocess = true;
 
14701
                continue;
 
14702
            }
 
14703
 
 
14704
            // first, check for the simplest case: everything closes neatly.
 
14705
            // Eventually, everything passes through here; if there are problems
 
14706
            // we modify the input stream accordingly and then punt, so that
 
14707
            // the tokens get processed again.
 
14708
            $current_parent = array_pop($this->stack);
 
14709
            if ($current_parent->name == $token->name) {
 
14710
                $token->start = $current_parent;
 
14711
                foreach ($this->injectors as $i => $injector) {
 
14712
                    if (isset($token->skip[$i])) continue;
 
14713
                    if ($token->rewind !== null && $token->rewind !== $i) continue;
 
14714
                    $injector->handleEnd($token);
 
14715
                    $this->processToken($token, $i);
 
14716
                    $this->stack[] = $current_parent;
 
14717
                    $reprocess = true;
 
14718
                    break;
 
14719
                }
 
14720
                continue;
 
14721
            }
 
14722
 
 
14723
            // okay, so we're trying to close the wrong tag
 
14724
 
 
14725
            // undo the pop previous pop
 
14726
            $this->stack[] = $current_parent;
 
14727
 
 
14728
            // scroll back the entire nest, trying to find our tag.
 
14729
            // (feature could be to specify how far you'd like to go)
 
14730
            $size = count($this->stack);
 
14731
            // -2 because -1 is the last element, but we already checked that
 
14732
            $skipped_tags = false;
 
14733
            for ($j = $size - 2; $j >= 0; $j--) {
 
14734
                if ($this->stack[$j]->name == $token->name) {
 
14735
                    $skipped_tags = array_slice($this->stack, $j);
 
14736
                    break;
 
14737
                }
 
14738
            }
 
14739
 
 
14740
            // we didn't find the tag, so remove
 
14741
            if ($skipped_tags === false) {
 
14742
                if ($escape_invalid_tags) {
 
14743
                    $this->swap(new HTMLPurifier_Token_Text(
 
14744
                        $generator->generateFromToken($token)
 
14745
                    ));
 
14746
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
 
14747
                } else {
 
14748
                    $this->remove();
 
14749
                    if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
 
14750
                }
 
14751
                $reprocess = true;
 
14752
                continue;
 
14753
            }
 
14754
 
 
14755
            // do errors, in REVERSE $j order: a,b,c with </a></b></c>
 
14756
            $c = count($skipped_tags);
 
14757
            if ($e) {
 
14758
                for ($j = $c - 1; $j > 0; $j--) {
 
14759
                    // notice we exclude $j == 0, i.e. the current ending tag, from
 
14760
                    // the errors...
 
14761
                    if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
 
14762
                        $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
 
14763
                    }
 
14764
                }
 
14765
            }
 
14766
 
 
14767
            // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
 
14768
            $replace = array($token);
 
14769
            for ($j = 1; $j < $c; $j++) {
 
14770
                // ...as well as from the insertions
 
14771
                $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
 
14772
                $new_token->start = $skipped_tags[$j];
 
14773
                array_unshift($replace, $new_token);
 
14774
                if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
 
14775
                    $element = clone $skipped_tags[$j];
 
14776
                    $element->carryover = true;
 
14777
                    $element->armor['MakeWellFormed_TagClosedError'] = true;
 
14778
                    $replace[] = $element;
 
14779
                }
 
14780
            }
 
14781
            $this->processToken($replace);
 
14782
            $reprocess = true;
 
14783
            continue;
 
14784
        }
 
14785
 
 
14786
        $context->destroy('CurrentNesting');
 
14787
        $context->destroy('InputTokens');
 
14788
        $context->destroy('InputIndex');
 
14789
        $context->destroy('CurrentToken');
 
14790
 
 
14791
        unset($this->injectors, $this->stack, $this->tokens, $this->t);
 
14792
        return $tokens;
 
14793
    }
 
14794
 
 
14795
    /**
 
14796
     * Processes arbitrary token values for complicated substitution patterns.
 
14797
     * In general:
 
14798
     *
 
14799
     * If $token is an array, it is a list of tokens to substitute for the
 
14800
     * current token. These tokens then get individually processed. If there
 
14801
     * is a leading integer in the list, that integer determines how many
 
14802
     * tokens from the stream should be removed.
 
14803
     *
 
14804
     * If $token is a regular token, it is swapped with the current token.
 
14805
     *
 
14806
     * If $token is false, the current token is deleted.
 
14807
     *
 
14808
     * If $token is an integer, that number of tokens (with the first token
 
14809
     * being the current one) will be deleted.
 
14810
     *
 
14811
     * @param $token Token substitution value
 
14812
     * @param $injector Injector that performed the substitution; default is if
 
14813
     *        this is not an injector related operation.
 
14814
     */
 
14815
    protected function processToken($token, $injector = -1) {
 
14816
 
 
14817
        // normalize forms of token
 
14818
        if (is_object($token)) $token = array(1, $token);
 
14819
        if (is_int($token))    $token = array($token);
 
14820
        if ($token === false)  $token = array(1);
 
14821
        if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
 
14822
        if (!is_int($token[0])) array_unshift($token, 1);
 
14823
        if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
 
14824
 
 
14825
        // $token is now an array with the following form:
 
14826
        // array(number nodes to delete, new node 1, new node 2, ...)
 
14827
 
 
14828
        $delete = array_shift($token);
 
14829
        $old = array_splice($this->tokens, $this->t, $delete, $token);
 
14830
 
 
14831
        if ($injector > -1) {
 
14832
            // determine appropriate skips
 
14833
            $oldskip = isset($old[0]) ? $old[0]->skip : array();
 
14834
            foreach ($token as $object) {
 
14835
                $object->skip = $oldskip;
 
14836
                $object->skip[$injector] = true;
 
14837
            }
 
14838
        }
 
14839
 
 
14840
    }
 
14841
 
 
14842
    /**
 
14843
     * Inserts a token before the current token. Cursor now points to this token
 
14844
     */
 
14845
    private function insertBefore($token) {
 
14846
        array_splice($this->tokens, $this->t, 0, array($token));
 
14847
    }
 
14848
 
 
14849
    /**
 
14850
     * Removes current token. Cursor now points to new token occupying previously
 
14851
     * occupied space.
 
14852
     */
 
14853
    private function remove() {
 
14854
        array_splice($this->tokens, $this->t, 1);
 
14855
    }
 
14856
 
 
14857
    /**
 
14858
     * Swap current token with new token. Cursor points to new token (no change).
 
14859
     */
 
14860
    private function swap($token) {
 
14861
        $this->tokens[$this->t] = $token;
 
14862
    }
 
14863
 
 
14864
}
 
14865
 
 
14866
 
 
14867
 
 
14868
 
 
14869
 
 
14870
/**
 
14871
 * Removes all unrecognized tags from the list of tokens.
 
14872
 *
 
14873
 * This strategy iterates through all the tokens and removes unrecognized
 
14874
 * tokens. If a token is not recognized but a TagTransform is defined for
 
14875
 * that element, the element will be transformed accordingly.
 
14876
 */
 
14877
 
 
14878
class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
 
14879
{
 
14880
 
 
14881
    public function execute($tokens, $config, $context) {
 
14882
        $definition = $config->getHTMLDefinition();
 
14883
        $generator = new HTMLPurifier_Generator($config, $context);
 
14884
        $result = array();
 
14885
 
 
14886
        $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
 
14887
        $remove_invalid_img  = $config->get('Core.RemoveInvalidImg');
 
14888
 
 
14889
        // currently only used to determine if comments should be kept
 
14890
        $trusted = $config->get('HTML.Trusted');
 
14891
 
 
14892
        $remove_script_contents = $config->get('Core.RemoveScriptContents');
 
14893
        $hidden_elements     = $config->get('Core.HiddenElements');
 
14894
 
 
14895
        // remove script contents compatibility
 
14896
        if ($remove_script_contents === true) {
 
14897
            $hidden_elements['script'] = true;
 
14898
        } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) {
 
14899
            unset($hidden_elements['script']);
 
14900
        }
 
14901
 
 
14902
        $attr_validator = new HTMLPurifier_AttrValidator();
 
14903
 
 
14904
        // removes tokens until it reaches a closing tag with its value
 
14905
        $remove_until = false;
 
14906
 
 
14907
        // converts comments into text tokens when this is equal to a tag name
 
14908
        $textify_comments = false;
 
14909
 
 
14910
        $token = false;
 
14911
        $context->register('CurrentToken', $token);
 
14912
 
 
14913
        $e = false;
 
14914
        if ($config->get('Core.CollectErrors')) {
 
14915
            $e =& $context->get('ErrorCollector');
 
14916
        }
 
14917
 
 
14918
        foreach($tokens as $token) {
 
14919
            if ($remove_until) {
 
14920
                if (empty($token->is_tag) || $token->name !== $remove_until) {
 
14921
                    continue;
 
14922
                }
 
14923
            }
 
14924
            if (!empty( $token->is_tag )) {
 
14925
                // DEFINITION CALL
 
14926
 
 
14927
                // before any processing, try to transform the element
 
14928
                if (
 
14929
                    isset($definition->info_tag_transform[$token->name])
 
14930
                ) {
 
14931
                    $original_name = $token->name;
 
14932
                    // there is a transformation for this tag
 
14933
                    // DEFINITION CALL
 
14934
                    $token = $definition->
 
14935
                                info_tag_transform[$token->name]->
 
14936
                                    transform($token, $config, $context);
 
14937
                    if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name);
 
14938
                }
 
14939
 
 
14940
                if (isset($definition->info[$token->name])) {
 
14941
 
 
14942
                    // mostly everything's good, but
 
14943
                    // we need to make sure required attributes are in order
 
14944
                    if (
 
14945
                        ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) &&
 
14946
                        $definition->info[$token->name]->required_attr &&
 
14947
                        ($token->name != 'img' || $remove_invalid_img) // ensure config option still works
 
14948
                    ) {
 
14949
                        $attr_validator->validateToken($token, $config, $context);
 
14950
                        $ok = true;
 
14951
                        foreach ($definition->info[$token->name]->required_attr as $name) {
 
14952
                            if (!isset($token->attr[$name])) {
 
14953
                                $ok = false;
 
14954
                                break;
 
14955
                            }
 
14956
                        }
 
14957
                        if (!$ok) {
 
14958
                            if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name);
 
14959
                            continue;
 
14960
                        }
 
14961
                        $token->armor['ValidateAttributes'] = true;
 
14962
                    }
 
14963
 
 
14964
                    if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
 
14965
                        $textify_comments = $token->name;
 
14966
                    } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) {
 
14967
                        $textify_comments = false;
 
14968
                    }
 
14969
 
 
14970
                } elseif ($escape_invalid_tags) {
 
14971
                    // invalid tag, generate HTML representation and insert in
 
14972
                    if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text');
 
14973
                    $token = new HTMLPurifier_Token_Text(
 
14974
                        $generator->generateFromToken($token)
 
14975
                    );
 
14976
                } else {
 
14977
                    // check if we need to destroy all of the tag's children
 
14978
                    // CAN BE GENERICIZED
 
14979
                    if (isset($hidden_elements[$token->name])) {
 
14980
                        if ($token instanceof HTMLPurifier_Token_Start) {
 
14981
                            $remove_until = $token->name;
 
14982
                        } elseif ($token instanceof HTMLPurifier_Token_Empty) {
 
14983
                            // do nothing: we're still looking
 
14984
                        } else {
 
14985
                            $remove_until = false;
 
14986
                        }
 
14987
                        if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed');
 
14988
                    } else {
 
14989
                        if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed');
 
14990
                    }
 
14991
                    continue;
 
14992
                }
 
14993
            } elseif ($token instanceof HTMLPurifier_Token_Comment) {
 
14994
                // textify comments in script tags when they are allowed
 
14995
                if ($textify_comments !== false) {
 
14996
                    $data = $token->data;
 
14997
                    $token = new HTMLPurifier_Token_Text($data);
 
14998
                } elseif ($trusted) {
 
14999
                    // keep, but perform comment cleaning
 
15000
                    if ($e) {
 
15001
                        // perform check whether or not there's a trailing hyphen
 
15002
                        if (substr($token->data, -1) == '-') {
 
15003
                            $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
 
15004
                        }
 
15005
                    }
 
15006
                    $token->data = rtrim($token->data, '-');
 
15007
                    $found_double_hyphen = false;
 
15008
                    while (strpos($token->data, '--') !== false) {
 
15009
                        if ($e && !$found_double_hyphen) {
 
15010
                            $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
 
15011
                        }
 
15012
                        $found_double_hyphen = true; // prevent double-erroring
 
15013
                        $token->data = str_replace('--', '-', $token->data);
 
15014
                    }
 
15015
                } else {
 
15016
                    // strip comments
 
15017
                    if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
 
15018
                    continue;
 
15019
                }
 
15020
            } elseif ($token instanceof HTMLPurifier_Token_Text) {
 
15021
            } else {
 
15022
                continue;
 
15023
            }
 
15024
            $result[] = $token;
 
15025
        }
 
15026
        if ($remove_until && $e) {
 
15027
            // we removed tokens until the end, throw error
 
15028
            $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until);
 
15029
        }
 
15030
 
 
15031
        $context->destroy('CurrentToken');
 
15032
 
 
15033
        return $result;
 
15034
    }
 
15035
 
 
15036
}
 
15037
 
 
15038
 
 
15039
 
 
15040
 
 
15041
 
 
15042
/**
 
15043
 * Validate all attributes in the tokens.
 
15044
 */
 
15045
 
 
15046
class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
 
15047
{
 
15048
 
 
15049
    public function execute($tokens, $config, $context) {
 
15050
 
 
15051
        // setup validator
 
15052
        $validator = new HTMLPurifier_AttrValidator();
 
15053
 
 
15054
        $token = false;
 
15055
        $context->register('CurrentToken', $token);
 
15056
 
 
15057
        foreach ($tokens as $key => $token) {
 
15058
 
 
15059
            // only process tokens that have attributes,
 
15060
            //   namely start and empty tags
 
15061
            if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue;
 
15062
 
 
15063
            // skip tokens that are armored
 
15064
            if (!empty($token->armor['ValidateAttributes'])) continue;
 
15065
 
 
15066
            // note that we have no facilities here for removing tokens
 
15067
            $validator->validateToken($token, $config, $context);
 
15068
 
 
15069
            $tokens[$key] = $token; // for PHP 4
 
15070
        }
 
15071
        $context->destroy('CurrentToken');
 
15072
 
 
15073
        return $tokens;
 
15074
    }
 
15075
 
 
15076
}
 
15077
 
 
15078
 
 
15079
 
 
15080
 
 
15081
 
 
15082
/**
 
15083
 * Transforms FONT tags to the proper form (SPAN with CSS styling)
 
15084
 *
 
15085
 * This transformation takes the three proprietary attributes of FONT and
 
15086
 * transforms them into their corresponding CSS attributes.  These are color,
 
15087
 * face, and size.
 
15088
 *
 
15089
 * @note Size is an interesting case because it doesn't map cleanly to CSS.
 
15090
 *       Thanks to
 
15091
 *       http://style.cleverchimp.com/font_size_intervals/altintervals.html
 
15092
 *       for reasonable mappings.
 
15093
 * @warning This doesn't work completely correctly; specifically, this
 
15094
 *          TagTransform operates before well-formedness is enforced, so
 
15095
 *          the "active formatting elements" algorithm doesn't get applied.
 
15096
 */
 
15097
class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
 
15098
{
 
15099
 
 
15100
    public $transform_to = 'span';
 
15101
 
 
15102
    protected $_size_lookup = array(
 
15103
        '0' => 'xx-small',
 
15104
        '1' => 'xx-small',
 
15105
        '2' => 'small',
 
15106
        '3' => 'medium',
 
15107
        '4' => 'large',
 
15108
        '5' => 'x-large',
 
15109
        '6' => 'xx-large',
 
15110
        '7' => '300%',
 
15111
        '-1' => 'smaller',
 
15112
        '-2' => '60%',
 
15113
        '+1' => 'larger',
 
15114
        '+2' => '150%',
 
15115
        '+3' => '200%',
 
15116
        '+4' => '300%'
 
15117
    );
 
15118
 
 
15119
    public function transform($tag, $config, $context) {
 
15120
 
 
15121
        if ($tag instanceof HTMLPurifier_Token_End) {
 
15122
            $new_tag = clone $tag;
 
15123
            $new_tag->name = $this->transform_to;
 
15124
            return $new_tag;
 
15125
        }
 
15126
 
 
15127
        $attr = $tag->attr;
 
15128
        $prepend_style = '';
 
15129
 
 
15130
        // handle color transform
 
15131
        if (isset($attr['color'])) {
 
15132
            $prepend_style .= 'color:' . $attr['color'] . ';';
 
15133
            unset($attr['color']);
 
15134
        }
 
15135
 
 
15136
        // handle face transform
 
15137
        if (isset($attr['face'])) {
 
15138
            $prepend_style .= 'font-family:' . $attr['face'] . ';';
 
15139
            unset($attr['face']);
 
15140
        }
 
15141
 
 
15142
        // handle size transform
 
15143
        if (isset($attr['size'])) {
 
15144
            // normalize large numbers
 
15145
            if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
 
15146
                $size = (int) $attr['size'];
 
15147
                if ($size < -2) $attr['size'] = '-2';
 
15148
                if ($size > 4)  $attr['size'] = '+4';
 
15149
            } else {
 
15150
                $size = (int) $attr['size'];
 
15151
                if ($size > 7) $attr['size'] = '7';
 
15152
            }
 
15153
            if (isset($this->_size_lookup[$attr['size']])) {
 
15154
                $prepend_style .= 'font-size:' .
 
15155
                  $this->_size_lookup[$attr['size']] . ';';
 
15156
            }
 
15157
            unset($attr['size']);
 
15158
        }
 
15159
 
 
15160
        if ($prepend_style) {
 
15161
            $attr['style'] = isset($attr['style']) ?
 
15162
                $prepend_style . $attr['style'] :
 
15163
                $prepend_style;
 
15164
        }
 
15165
 
 
15166
        $new_tag = clone $tag;
 
15167
        $new_tag->name = $this->transform_to;
 
15168
        $new_tag->attr = $attr;
 
15169
 
 
15170
        return $new_tag;
 
15171
 
 
15172
    }
 
15173
}
 
15174
 
 
15175
 
 
15176
 
 
15177
 
 
15178
 
 
15179
/**
 
15180
 * Simple transformation, just change tag name to something else,
 
15181
 * and possibly add some styling. This will cover most of the deprecated
 
15182
 * tag cases.
 
15183
 */
 
15184
class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform
 
15185
{
 
15186
 
 
15187
    protected $style;
 
15188
 
 
15189
    /**
 
15190
     * @param $transform_to Tag name to transform to.
 
15191
     * @param $style CSS style to add to the tag
 
15192
     */
 
15193
    public function __construct($transform_to, $style = null) {
 
15194
        $this->transform_to = $transform_to;
 
15195
        $this->style = $style;
 
15196
    }
 
15197
 
 
15198
    public function transform($tag, $config, $context) {
 
15199
        $new_tag = clone $tag;
 
15200
        $new_tag->name = $this->transform_to;
 
15201
        if (!is_null($this->style) &&
 
15202
            ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
 
15203
        ) {
 
15204
            $this->prependCSS($new_tag->attr, $this->style);
 
15205
        }
 
15206
        return $new_tag;
 
15207
    }
 
15208
 
 
15209
}
 
15210
 
 
15211
 
 
15212
 
 
15213
 
 
15214
 
 
15215
/**
 
15216
 * Concrete comment token class. Generally will be ignored.
 
15217
 */
 
15218
class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
 
15219
{
 
15220
    public $data; /**< Character data within comment. */
 
15221
    public $is_whitespace = true;
 
15222
    /**
 
15223
     * Transparent constructor.
 
15224
     *
 
15225
     * @param $data String comment data.
 
15226
     */
 
15227
    public function __construct($data, $line = null, $col = null) {
 
15228
        $this->data = $data;
 
15229
        $this->line = $line;
 
15230
        $this->col  = $col;
 
15231
    }
 
15232
}
 
15233
 
 
15234
 
 
15235
 
 
15236
 
 
15237
 
 
15238
/**
 
15239
 * Abstract class of a tag token (start, end or empty), and its behavior.
 
15240
 */
 
15241
class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
 
15242
{
 
15243
    /**
 
15244
     * Static bool marker that indicates the class is a tag.
 
15245
     *
 
15246
     * This allows us to check objects with <tt>!empty($obj->is_tag)</tt>
 
15247
     * without having to use a function call <tt>is_a()</tt>.
 
15248
     */
 
15249
    public $is_tag = true;
 
15250
 
 
15251
    /**
 
15252
     * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
 
15253
     *
 
15254
     * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
 
15255
     * be lower-casing them, but these tokens cater to HTML tags, which are
 
15256
     * insensitive.
 
15257
     */
 
15258
    public $name;
 
15259
 
 
15260
    /**
 
15261
     * Associative array of the tag's attributes.
 
15262
     */
 
15263
    public $attr = array();
 
15264
 
 
15265
    /**
 
15266
     * Non-overloaded constructor, which lower-cases passed tag name.
 
15267
     *
 
15268
     * @param $name String name.
 
15269
     * @param $attr Associative array of attributes.
 
15270
     */
 
15271
    public function __construct($name, $attr = array(), $line = null, $col = null) {
 
15272
        $this->name = ctype_lower($name) ? $name : strtolower($name);
 
15273
        foreach ($attr as $key => $value) {
 
15274
            // normalization only necessary when key is not lowercase
 
15275
            if (!ctype_lower($key)) {
 
15276
                $new_key = strtolower($key);
 
15277
                if (!isset($attr[$new_key])) {
 
15278
                    $attr[$new_key] = $attr[$key];
 
15279
                }
 
15280
                if ($new_key !== $key) {
 
15281
                    unset($attr[$key]);
 
15282
                }
 
15283
            }
 
15284
        }
 
15285
        $this->attr = $attr;
 
15286
        $this->line = $line;
 
15287
        $this->col  = $col;
 
15288
    }
 
15289
}
 
15290
 
 
15291
 
 
15292
 
 
15293
 
 
15294
 
 
15295
/**
 
15296
 * Concrete empty token class.
 
15297
 */
 
15298
class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
 
15299
{
 
15300
 
 
15301
}
 
15302
 
 
15303
 
 
15304
 
 
15305
 
 
15306
 
 
15307
/**
 
15308
 * Concrete end token class.
 
15309
 *
 
15310
 * @warning This class accepts attributes even though end tags cannot. This
 
15311
 * is for optimization reasons, as under normal circumstances, the Lexers
 
15312
 * do not pass attributes.
 
15313
 */
 
15314
class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag
 
15315
{
 
15316
    /**
 
15317
     * Token that started this node. Added by MakeWellFormed. Please
 
15318
     * do not edit this!
 
15319
     */
 
15320
    public $start;
 
15321
}
 
15322
 
 
15323
 
 
15324
 
 
15325
 
 
15326
 
 
15327
/**
 
15328
 * Concrete start token class.
 
15329
 */
 
15330
class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
 
15331
{
 
15332
 
 
15333
}
 
15334
 
 
15335
 
 
15336
 
 
15337
 
 
15338
 
 
15339
/**
 
15340
 * Concrete text token class.
 
15341
 *
 
15342
 * Text tokens comprise of regular parsed character data (PCDATA) and raw
 
15343
 * character data (from the CDATA sections). Internally, their
 
15344
 * data is parsed with all entities expanded. Surprisingly, the text token
 
15345
 * does have a "tag name" called #PCDATA, which is how the DTD represents it
 
15346
 * in permissible child nodes.
 
15347
 */
 
15348
class HTMLPurifier_Token_Text extends HTMLPurifier_Token
 
15349
{
 
15350
 
 
15351
    public $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. */
 
15352
    public $data; /**< Parsed character data of text. */
 
15353
    public $is_whitespace; /**< Bool indicating if node is whitespace. */
 
15354
 
 
15355
    /**
 
15356
     * Constructor, accepts data and determines if it is whitespace.
 
15357
     *
 
15358
     * @param $data String parsed character data.
 
15359
     */
 
15360
    public function __construct($data, $line = null, $col = null) {
 
15361
        $this->data = $data;
 
15362
        $this->is_whitespace = ctype_space($data);
 
15363
        $this->line = $line;
 
15364
        $this->col  = $col;
 
15365
    }
 
15366
 
 
15367
}
 
15368
 
 
15369
 
 
15370
 
 
15371
 
 
15372
 
 
15373
class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
 
15374
{
 
15375
    public $name = 'DisableExternal';
 
15376
    protected $ourHostParts = false;
 
15377
    public function prepare($config) {
 
15378
        $our_host = $config->getDefinition('URI')->host;
 
15379
        if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host));
 
15380
    }
 
15381
    public function filter(&$uri, $config, $context) {
 
15382
        if (is_null($uri->host)) return true;
 
15383
        if ($this->ourHostParts === false) return false;
 
15384
        $host_parts = array_reverse(explode('.', $uri->host));
 
15385
        foreach ($this->ourHostParts as $i => $x) {
 
15386
            if (!isset($host_parts[$i])) return false;
 
15387
            if ($host_parts[$i] != $this->ourHostParts[$i]) return false;
 
15388
        }
 
15389
        return true;
 
15390
    }
 
15391
}
 
15392
 
 
15393
 
 
15394
 
 
15395
 
 
15396
 
 
15397
class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal
 
15398
{
 
15399
    public $name = 'DisableExternalResources';
 
15400
    public function filter(&$uri, $config, $context) {
 
15401
        if (!$context->get('EmbeddedURI', true)) return true;
 
15402
        return parent::filter($uri, $config, $context);
 
15403
    }
 
15404
}
 
15405
 
 
15406
 
 
15407
 
 
15408
 
 
15409
 
 
15410
class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
 
15411
{
 
15412
    public $name = 'HostBlacklist';
 
15413
    protected $blacklist = array();
 
15414
    public function prepare($config) {
 
15415
        $this->blacklist = $config->get('URI.HostBlacklist');
 
15416
        return true;
 
15417
    }
 
15418
    public function filter(&$uri, $config, $context) {
 
15419
        foreach($this->blacklist as $blacklisted_host_fragment) {
 
15420
            if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
 
15421
                return false;
 
15422
            }
 
15423
        }
 
15424
        return true;
 
15425
    }
 
15426
}
 
15427
 
 
15428
 
 
15429
 
 
15430
 
 
15431
 
 
15432
// does not support network paths
 
15433
 
 
15434
class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
 
15435
{
 
15436
    public $name = 'MakeAbsolute';
 
15437
    protected $base;
 
15438
    protected $basePathStack = array();
 
15439
    public function prepare($config) {
 
15440
        $def = $config->getDefinition('URI');
 
15441
        $this->base = $def->base;
 
15442
        if (is_null($this->base)) {
 
15443
            trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
 
15444
            return false;
 
15445
        }
 
15446
        $this->base->fragment = null; // fragment is invalid for base URI
 
15447
        $stack = explode('/', $this->base->path);
 
15448
        array_pop($stack); // discard last segment
 
15449
        $stack = $this->_collapseStack($stack); // do pre-parsing
 
15450
        $this->basePathStack = $stack;
 
15451
        return true;
 
15452
    }
 
15453
    public function filter(&$uri, $config, $context) {
 
15454
        if (is_null($this->base)) return true; // abort early
 
15455
        if (
 
15456
            $uri->path === '' && is_null($uri->scheme) &&
 
15457
            is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
 
15458
        ) {
 
15459
            // reference to current document
 
15460
            $uri = clone $this->base;
 
15461
            return true;
 
15462
        }
 
15463
        if (!is_null($uri->scheme)) {
 
15464
            // absolute URI already: don't change
 
15465
            if (!is_null($uri->host)) return true;
 
15466
            $scheme_obj = $uri->getSchemeObj($config, $context);
 
15467
            if (!$scheme_obj) {
 
15468
                // scheme not recognized
 
15469
                return false;
 
15470
            }
 
15471
            if (!$scheme_obj->hierarchical) {
 
15472
                // non-hierarchal URI with explicit scheme, don't change
 
15473
                return true;
 
15474
            }
 
15475
            // special case: had a scheme but always is hierarchical and had no authority
 
15476
        }
 
15477
        if (!is_null($uri->host)) {
 
15478
            // network path, don't bother
 
15479
            return true;
 
15480
        }
 
15481
        if ($uri->path === '') {
 
15482
            $uri->path = $this->base->path;
 
15483
        } elseif ($uri->path[0] !== '/') {
 
15484
            // relative path, needs more complicated processing
 
15485
            $stack = explode('/', $uri->path);
 
15486
            $new_stack = array_merge($this->basePathStack, $stack);
 
15487
            if ($new_stack[0] !== '' && !is_null($this->base->host)) {
 
15488
                array_unshift($new_stack, '');
 
15489
            }
 
15490
            $new_stack = $this->_collapseStack($new_stack);
 
15491
            $uri->path = implode('/', $new_stack);
 
15492
        } else {
 
15493
            // absolute path, but still we should collapse
 
15494
            $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path)));
 
15495
        }
 
15496
        // re-combine
 
15497
        $uri->scheme = $this->base->scheme;
 
15498
        if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo;
 
15499
        if (is_null($uri->host))     $uri->host     = $this->base->host;
 
15500
        if (is_null($uri->port))     $uri->port     = $this->base->port;
 
15501
        return true;
 
15502
    }
 
15503
 
 
15504
    /**
 
15505
     * Resolve dots and double-dots in a path stack
 
15506
     */
 
15507
    private function _collapseStack($stack) {
 
15508
        $result = array();
 
15509
        $is_folder = false;
 
15510
        for ($i = 0; isset($stack[$i]); $i++) {
 
15511
            $is_folder = false;
 
15512
            // absorb an internally duplicated slash
 
15513
            if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue;
 
15514
            if ($stack[$i] == '..') {
 
15515
                if (!empty($result)) {
 
15516
                    $segment = array_pop($result);
 
15517
                    if ($segment === '' && empty($result)) {
 
15518
                        // error case: attempted to back out too far:
 
15519
                        // restore the leading slash
 
15520
                        $result[] = '';
 
15521
                    } elseif ($segment === '..') {
 
15522
                        $result[] = '..'; // cannot remove .. with ..
 
15523
                    }
 
15524
                } else {
 
15525
                    // relative path, preserve the double-dots
 
15526
                    $result[] = '..';
 
15527
                }
 
15528
                $is_folder = true;
 
15529
                continue;
 
15530
            }
 
15531
            if ($stack[$i] == '.') {
 
15532
                // silently absorb
 
15533
                $is_folder = true;
 
15534
                continue;
 
15535
            }
 
15536
            $result[] = $stack[$i];
 
15537
        }
 
15538
        if ($is_folder) $result[] = '';
 
15539
        return $result;
 
15540
    }
 
15541
}
 
15542
 
 
15543
 
 
15544
 
 
15545
 
 
15546
 
 
15547
class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
 
15548
{
 
15549
    public $name = 'Munge';
 
15550
    public $post = true;
 
15551
    private $target, $parser, $doEmbed, $secretKey;
 
15552
 
 
15553
    protected $replace = array();
 
15554
 
 
15555
    public function prepare($config) {
 
15556
        $this->target    = $config->get('URI.' . $this->name);
 
15557
        $this->parser    = new HTMLPurifier_URIParser();
 
15558
        $this->doEmbed   = $config->get('URI.MungeResources');
 
15559
        $this->secretKey = $config->get('URI.MungeSecretKey');
 
15560
        return true;
 
15561
    }
 
15562
    public function filter(&$uri, $config, $context) {
 
15563
        if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true;
 
15564
 
 
15565
        $scheme_obj = $uri->getSchemeObj($config, $context);
 
15566
        if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
 
15567
        if (is_null($uri->host) || empty($scheme_obj->browsable)) {
 
15568
            return true;
 
15569
        }
 
15570
        // don't redirect if target host is our host
 
15571
        if ($uri->host === $config->getDefinition('URI')->host) {
 
15572
            return true;
 
15573
        }
 
15574
 
 
15575
        $this->makeReplace($uri, $config, $context);
 
15576
        $this->replace = array_map('rawurlencode', $this->replace);
 
15577
 
 
15578
        $new_uri = strtr($this->target, $this->replace);
 
15579
        $new_uri = $this->parser->parse($new_uri);
 
15580
        // don't redirect if the target host is the same as the
 
15581
        // starting host
 
15582
        if ($uri->host === $new_uri->host) return true;
 
15583
        $uri = $new_uri; // overwrite
 
15584
        return true;
 
15585
    }
 
15586
 
 
15587
    protected function makeReplace($uri, $config, $context) {
 
15588
        $string = $uri->toString();
 
15589
        // always available
 
15590
        $this->replace['%s'] = $string;
 
15591
        $this->replace['%r'] = $context->get('EmbeddedURI', true);
 
15592
        $token = $context->get('CurrentToken', true);
 
15593
        $this->replace['%n'] = $token ? $token->name : null;
 
15594
        $this->replace['%m'] = $context->get('CurrentAttr', true);
 
15595
        $this->replace['%p'] = $context->get('CurrentCSSProperty', true);
 
15596
        // not always available
 
15597
        if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
 
15598
    }
 
15599
 
 
15600
}
 
15601
 
 
15602
 
 
15603
 
 
15604
 
 
15605
 
 
15606
/**
 
15607
 * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738.
 
15608
 */
 
15609
class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
 
15610
 
 
15611
    public $default_port = 21;
 
15612
    public $browsable = true; // usually
 
15613
    public $hierarchical = true;
 
15614
 
 
15615
    public function validate(&$uri, $config, $context) {
 
15616
        parent::validate($uri, $config, $context);
 
15617
        $uri->query    = null;
 
15618
 
 
15619
        // typecode check
 
15620
        $semicolon_pos = strrpos($uri->path, ';'); // reverse
 
15621
        if ($semicolon_pos !== false) {
 
15622
            $type = substr($uri->path, $semicolon_pos + 1); // no semicolon
 
15623
            $uri->path = substr($uri->path, 0, $semicolon_pos);
 
15624
            $type_ret = '';
 
15625
            if (strpos($type, '=') !== false) {
 
15626
                // figure out whether or not the declaration is correct
 
15627
                list($key, $typecode) = explode('=', $type, 2);
 
15628
                if ($key !== 'type') {
 
15629
                    // invalid key, tack it back on encoded
 
15630
                    $uri->path .= '%3B' . $type;
 
15631
                } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') {
 
15632
                    $type_ret = ";type=$typecode";
 
15633
                }
 
15634
            } else {
 
15635
                $uri->path .= '%3B' . $type;
 
15636
            }
 
15637
            $uri->path = str_replace(';', '%3B', $uri->path);
 
15638
            $uri->path .= $type_ret;
 
15639
        }
 
15640
 
 
15641
        return true;
 
15642
    }
 
15643
 
 
15644
}
 
15645
 
 
15646
 
 
15647
 
 
15648
 
 
15649
 
 
15650
/**
 
15651
 * Validates http (HyperText Transfer Protocol) as defined by RFC 2616
 
15652
 */
 
15653
class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
 
15654
 
 
15655
    public $default_port = 80;
 
15656
    public $browsable = true;
 
15657
    public $hierarchical = true;
 
15658
 
 
15659
    public function validate(&$uri, $config, $context) {
 
15660
        parent::validate($uri, $config, $context);
 
15661
        $uri->userinfo = null;
 
15662
        return true;
 
15663
    }
 
15664
 
 
15665
}
 
15666
 
 
15667
 
 
15668
 
 
15669
 
 
15670
 
 
15671
/**
 
15672
 * Validates https (Secure HTTP) according to http scheme.
 
15673
 */
 
15674
class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http {
 
15675
 
 
15676
    public $default_port = 443;
 
15677
 
 
15678
}
 
15679
 
 
15680
 
 
15681
 
 
15682
 
 
15683
 
 
15684
// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the
 
15685
// email is valid, but be careful!
 
15686
 
 
15687
/**
 
15688
 * Validates mailto (for E-mail) according to RFC 2368
 
15689
 * @todo Validate the email address
 
15690
 * @todo Filter allowed query parameters
 
15691
 */
 
15692
 
 
15693
class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
 
15694
 
 
15695
    public $browsable = false;
 
15696
 
 
15697
    public function validate(&$uri, $config, $context) {
 
15698
        parent::validate($uri, $config, $context);
 
15699
        $uri->userinfo = null;
 
15700
        $uri->host     = null;
 
15701
        $uri->port     = null;
 
15702
        // we need to validate path against RFC 2368's addr-spec
 
15703
        return true;
 
15704
    }
 
15705
 
 
15706
}
 
15707
 
 
15708
 
 
15709
 
 
15710
 
 
15711
 
 
15712
/**
 
15713
 * Validates news (Usenet) as defined by generic RFC 1738
 
15714
 */
 
15715
class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
 
15716
 
 
15717
    public $browsable = false;
 
15718
 
 
15719
    public function validate(&$uri, $config, $context) {
 
15720
        parent::validate($uri, $config, $context);
 
15721
        $uri->userinfo = null;
 
15722
        $uri->host     = null;
 
15723
        $uri->port     = null;
 
15724
        $uri->query    = null;
 
15725
        // typecode check needed on path
 
15726
        return true;
 
15727
    }
 
15728
 
 
15729
}
 
15730
 
 
15731
 
 
15732
 
 
15733
 
 
15734
 
 
15735
/**
 
15736
 * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738
 
15737
 */
 
15738
class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
 
15739
 
 
15740
    public $default_port = 119;
 
15741
    public $browsable = false;
 
15742
 
 
15743
    public function validate(&$uri, $config, $context) {
 
15744
        parent::validate($uri, $config, $context);
 
15745
        $uri->userinfo = null;
 
15746
        $uri->query    = null;
 
15747
        return true;
 
15748
    }
 
15749
 
 
15750
}
 
15751
 
 
15752
 
 
15753
 
 
15754
 
 
15755
 
 
15756
/**
 
15757
 * Performs safe variable parsing based on types which can be used by
 
15758
 * users. This may not be able to represent all possible data inputs,
 
15759
 * however.
 
15760
 */
 
15761
class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser
 
15762
{
 
15763
 
 
15764
    protected function parseImplementation($var, $type, $allow_null) {
 
15765
        if ($allow_null && $var === null) return null;
 
15766
        switch ($type) {
 
15767
            // Note: if code "breaks" from the switch, it triggers a generic
 
15768
            // exception to be thrown. Specific errors can be specifically
 
15769
            // done here.
 
15770
            case self::MIXED :
 
15771
            case self::ISTRING :
 
15772
            case self::STRING :
 
15773
            case self::TEXT :
 
15774
            case self::ITEXT :
 
15775
                return $var;
 
15776
            case self::INT :
 
15777
                if (is_string($var) && ctype_digit($var)) $var = (int) $var;
 
15778
                return $var;
 
15779
            case self::FLOAT :
 
15780
                if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var;
 
15781
                return $var;
 
15782
            case self::BOOL :
 
15783
                if (is_int($var) && ($var === 0 || $var === 1)) {
 
15784
                    $var = (bool) $var;
 
15785
                } elseif (is_string($var)) {
 
15786
                    if ($var == 'on' || $var == 'true' || $var == '1') {
 
15787
                        $var = true;
 
15788
                    } elseif ($var == 'off' || $var == 'false' || $var == '0') {
 
15789
                        $var = false;
 
15790
                    } else {
 
15791
                        throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type");
 
15792
                    }
 
15793
                }
 
15794
                return $var;
 
15795
            case self::ALIST :
 
15796
            case self::HASH :
 
15797
            case self::LOOKUP :
 
15798
                if (is_string($var)) {
 
15799
                    // special case: technically, this is an array with
 
15800
                    // a single empty string item, but having an empty
 
15801
                    // array is more intuitive
 
15802
                    if ($var == '') return array();
 
15803
                    if (strpos($var, "\n") === false && strpos($var, "\r") === false) {
 
15804
                        // simplistic string to array method that only works
 
15805
                        // for simple lists of tag names or alphanumeric characters
 
15806
                        $var = explode(',',$var);
 
15807
                    } else {
 
15808
                        $var = preg_split('/(,|[\n\r]+)/', $var);
 
15809
                    }
 
15810
                    // remove spaces
 
15811
                    foreach ($var as $i => $j) $var[$i] = trim($j);
 
15812
                    if ($type === self::HASH) {
 
15813
                        // key:value,key2:value2
 
15814
                        $nvar = array();
 
15815
                        foreach ($var as $keypair) {
 
15816
                            $c = explode(':', $keypair, 2);
 
15817
                            if (!isset($c[1])) continue;
 
15818
                            $nvar[$c[0]] = $c[1];
 
15819
                        }
 
15820
                        $var = $nvar;
 
15821
                    }
 
15822
                }
 
15823
                if (!is_array($var)) break;
 
15824
                $keys = array_keys($var);
 
15825
                if ($keys === array_keys($keys)) {
 
15826
                    if ($type == self::ALIST) return $var;
 
15827
                    elseif ($type == self::LOOKUP) {
 
15828
                        $new = array();
 
15829
                        foreach ($var as $key) {
 
15830
                            $new[$key] = true;
 
15831
                        }
 
15832
                        return $new;
 
15833
                    } else break;
 
15834
                }
 
15835
                if ($type === self::LOOKUP) {
 
15836
                    foreach ($var as $key => $value) {
 
15837
                        $var[$key] = true;
 
15838
                    }
 
15839
                }
 
15840
                return $var;
 
15841
            default:
 
15842
                $this->errorInconsistent(__CLASS__, $type);
 
15843
        }
 
15844
        $this->errorGeneric($var, $type);
 
15845
    }
 
15846
 
 
15847
}
 
15848
 
 
15849
 
 
15850
 
 
15851
 
 
15852
 
 
15853
/**
 
15854
 * This variable parser uses PHP's internal code engine. Because it does
 
15855
 * this, it can represent all inputs; however, it is dangerous and cannot
 
15856
 * be used by users.
 
15857
 */
 
15858
class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser
 
15859
{
 
15860
 
 
15861
    protected function parseImplementation($var, $type, $allow_null) {
 
15862
        return $this->evalExpression($var);
 
15863
    }
 
15864
 
 
15865
    protected function evalExpression($expr) {
 
15866
        $var = null;
 
15867
        $result = eval("\$var = $expr;");
 
15868
        if ($result === false) {
 
15869
            throw new HTMLPurifier_VarParserException("Fatal error in evaluated code");
 
15870
        }
 
15871
        return $var;
 
15872
    }
 
15873
 
 
15874
}
 
15875
 
15336
15876
 
15337
15877