Completed
Push — 3.5 ( 1a9180...1bec8a )
by Daniel
24s
created

SSHTMLBBCodeParser   D

Complexity

Total Complexity 124

Size/Duplication

Total Lines 817
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 817
rs 4.4444
c 0
b 0
f 0
wmc 124
lcom 1
cbo 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
D SSHTMLBBCodeParser() 0 40 9
A getStaticProperty() 0 11 3
A setOption() 0 4 1
B addFilter() 0 22 4
A removeFilter() 0 16 4
B addFilters() 0 21 6
A _preparse() 0 17 3
C _buildTagArray() 0 63 12
C _buildTag() 0 55 12
D _validateTagArray() 0 105 23
C _parentNeeded() 0 24 9
C _childNeeded() 0 24 9
B _isAllowed() 0 19 8
C _buildParsedString() 0 46 14
A setText() 0 4 1
A getText() 0 4 1
A getPreparsed() 0 4 1
A getParsed() 0 4 1
A parse() 0 7 1
A qparse() 0 6 1
A staticQparse() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like SSHTMLBBCodeParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SSHTMLBBCodeParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/* vim: set expandtab tabstop=4 shiftwidth=4: */
3
// +----------------------------------------------------------------------+
4
// | PHP Version 4                                                        |
5
// +----------------------------------------------------------------------+
6
// | Copyright (c) 1997-2003 The PHP Group                                |
7
// +----------------------------------------------------------------------+
8
// | This source file is subject to version 2.02 of the PHP license,      |
9
// | that is bundled with this package in the file LICENSE, and is        |
10
// | available at through the world-wide-web at                           |
11
// | http://www.php.net/license/2_02.txt.                                 |
12
// | If you did not receive a copy of the PHP license and are unable to   |
13
// | obtain it through the world-wide-web, please send a note to          |
14
// | [email protected] so we can mail you a copy immediately.               |
15
// +----------------------------------------------------------------------+
16
// | Author: Stijn de Reede <[email protected]>                               |
17
// +----------------------------------------------------------------------+
18
//
19
// $Id: BBCodeParser.php,v 1.17 2007/07/02 18:46:30 cweiske Exp $
20
//
21
// Modified by SilverStripe silverstripe.com
22
23
/**
24
 * @package framework
25
 * @subpackage misc
26
 * @author   Stijn de Reede  <[email protected]> , SilverStripe
27
 *
28
 *
29
 * This is a parser to replace UBB style tags with their html equivalents. It
30
 * does not simply do some regex calls, but is complete stack based
31
 * parse engine. This ensures that all tags are properly nested, if not,
32
 * extra tags are added to maintain the nesting. This parser should only produce
33
 * xhtml 1.0 compliant code. All tags are validated and so are all their attributes.
34
 * It should be easy to extend this parser with your own tags, see the _definedTags
35
 * format description below.
36
 *
37
 *
38
 * Usage:
39
 * $parser = new SSHTMLBBCodeParser();
40
 * $parser->setText('normal [b]bold[/b] and normal again');
41
 * $parser->parse();
42
 * echo $parser->getParsed();
43
 * or:
44
 * $parser = new SSHTMLBBCodeParser();
45
 * echo $parser->qparse('normal [b]bold[/b] and normal again');
46
 * or:
47
 * echo SSHTMLBBCodeParser::staticQparse('normal [b]bold[/b] and normal again');
48
 *
49
 *
50
 * Setting the options from the ini file:
51
 * $config = parse_ini_file('BBCodeParser.ini', true);
52
 *  $options = &PEAR::getStaticProperty('SSHTMLBBCodeParser', '_options');
53
 * $options = $config['SSHTMLBBCodeParser'];
54
 * unset($options);
55
 */
56
class SSHTMLBBCodeParser
57
{
58
	/**
59
	 * An array of tags parsed by the engine, should be overwritten by filters
60
	 *
61
	 * @access   private
62
	 * @var      array
63
	 */
64
	var $_definedTags  = array();
65
66
	/**
67
	 * A string containing the input
68
	 *
69
	 * @access   private
70
	 * @var      string
71
	 */
72
	var $_text          = '';
73
74
	/**
75
	 * A string containing the preparsed input
76
	 *
77
	 * @access   private
78
	 * @var      string
79
	 */
80
	var $_preparsed     = '';
81
82
	/**
83
	 * An array tags and texts build from the input text
84
	 *
85
	 * @access   private
86
	 * @var      array
87
	 */
88
	var $_tagArray      = array();
89
90
	/**
91
	 * A string containing the parsed version of the text
92
	 *
93
	 * @access   private
94
	 * @var      string
95
	 */
96
	var $_parsed        = '';
97
98
	/**
99
	 * An array of options, filled by an ini file or through the contructor
100
	 *
101
	 * @access   private
102
	 * @var      array
103
	 */
104
	var $_options = array(
105
		'quotestyle'    => 'double',
106
		'quotewhat'     => 'all',
107
		'open'          => '[',
108
		'close'         => ']',
109
		'xmlclose'      => true,
110
		'filters'       => 'Basic'
111
	);
112
113
	/**
114
	 * An array of filters used for parsing
115
	 *
116
	 * @access   private
117
	 * @var      array
118
	 */
119
	var $_filters       = array();
120
121
	/**
122
	 * Constructor, initialises the options and filters
123
	 *
124
	 * Sets the private variable _options with base options defined with
125
	 * &PEAR::getStaticProperty(), overwriting them with (if present)
126
	 * the argument to this method.
127
	 * Then it sets the extra options to properly escape the tag
128
	 * characters in preg_replace() etc. The set options are
129
	 * then stored back with &PEAR::getStaticProperty(), so that the filter
130
	 * classes can use them.
131
	 * All the filters in the options are initialised and their defined tags
132
	 * are copied into the private variable _definedTags.
133
	 *
134
	 * @param    array           options to use, can be left out
135
	 * @return   none
136
	 * @access   public
137
	 * @author   Stijn de Reede  <[email protected]>
138
	 */
139
	public function SSHTMLBBCodeParser($options = array())
140
	{
141
		// set the already set options
142
		$baseoptions = &SSHTMLBBCodeParser::getStaticProperty('SSHTMLBBCodeParser', '_options');
143
		if (is_array($baseoptions)) {
144
			foreach ($baseoptions as  $k => $v)  {
145
				$this->_options[$k] = $v;
146
			}
147
		}
148
149
		// set the options passed as an argument
150
		foreach ($options as $k => $v )  {
151
			$this->_options[$k] = $v;
152
		}
153
154
		// add escape open and close chars to the options for preg escaping
155
		$preg_escape = '\^$.[]|()?*+{}';
156
		if ($this->_options['open'] != '' && strpos($preg_escape, $this->_options['open'])) {
157
			$this->_options['open_esc'] = "\\".$this->_options['open'];
158
		} else {
159
			$this->_options['open_esc'] = $this->_options['open'];
160
		}
161
		if ($this->_options['close'] != '' && strpos($preg_escape, $this->_options['close'])) {
162
			$this->_options['close_esc'] = "\\".$this->_options['close'];
163
		} else {
164
			$this->_options['close_esc'] = $this->_options['close'];
165
		}
166
167
		// set the options back so that child classes can use them */
168
		$baseoptions = $this->_options;
169
		unset($baseoptions);
170
171
		// return if this is a subclass
172
		if (is_subclass_of($this, 'SSHTMLBBCodeParser_Filter')) {
173
			return;
174
		}
175
176
		// extract the definedTags from subclasses */
177
		$this->addFilters($this->_options['filters']);
178
	}
179
180
	static function &getStaticProperty($class, $var)
181
	{
182
		static $properties;
183
		if (!isset($properties[$class])) {
184
			$properties[$class] = array();
185
		}
186
		if (!array_key_exists($var, $properties[$class])) {
187
			$properties[$class][$var] = null;
188
		}
189
		return $properties[$class][$var];
190
	}
191
192
	/**
193
	 * Option setter
194
	 *
195
	 * @param string option name
196
	 * @param mixed  option value
197
	 * @author Lorenzo Alberton <[email protected]>
198
	 */
199
	public function setOption($name, $value)
200
	{
201
		$this->_options[$name] = $value;
202
	}
203
204
	/**
205
	 * Add a new filter
206
	 *
207
	 * @param string filter
208
	 * @author Lorenzo Alberton <[email protected]>
209
	 */
210
	public function addFilter($filter)
211
	{
212
		$filter = ucfirst($filter);
213
		if (!array_key_exists($filter, $this->_filters)) {
214
			$class = 'SSHTMLBBCodeParser_Filter_'.$filter;
215
			if (fopen('BBCodeParser/Filter/'.$filter.'.php','r',true)) {
216
				include_once 'BBCodeParser/Filter/'.$filter.'.php';
217
			}
218
			if (!class_exists($class)) {
219
220
				//PEAR::raiseError("Failed to load filter $filter", null, PEAR_ERROR_DIE);
221
			}
222
			else {
223
				$this->_filters[$filter] = new $class;
224
				$this->_definedTags = array_merge(
225
					$this->_definedTags,
226
					$this->_filters[$filter]->_definedTags
227
				);
228
			}
229
		}
230
231
	}
232
233
	/**
234
	 * Remove an existing filter
235
	 *
236
	 * @param string $filter
237
	 * @author Lorenzo Alberton <[email protected]>
238
	 */
239
	public function removeFilter($filter)
240
	{
241
		$filter = ucfirst(trim($filter));
242
		if (!empty($filter) && array_key_exists($filter, $this->_filters)) {
243
			unset($this->_filters[$filter]);
244
		}
245
		// also remove the related $this->_definedTags for this filter,
246
		// preserving the others
247
		$this->_definedTags = array();
248
		foreach (array_keys($this->_filters) as $filter) {
249
			$this->_definedTags = array_merge(
250
				$this->_definedTags,
251
				$this->_filters[$filter]->_definedTags
252
			);
253
		}
254
	}
255
256
	/**
257
	 * Add new filters
258
	 *
259
	 * @param mixed (array or string)
260
	 * @return boolean true if all ok, false if not.
261
	 * @author Lorenzo Alberton <[email protected]>
262
	 */
263
	public function addFilters($filters)
264
	{
265
		if (is_string($filters)) {
266
			//comma-separated list
267
			if (strpos($filters, ',') !== false) {
268
				$filters = explode(',', $filters);
269
			} else {
270
				$filters = array($filters);
271
			}
272
		}
273
		if (!is_array($filters)) {
274
			//invalid format
275
			return false;
276
		}
277
		foreach ($filters as $filter) {
278
			if (trim($filter)){
279
				$this->addFilter($filter);
280
			}
281
		}
282
		return true;
283
	}
284
285
	/**
286
	 * Executes statements before the actual array building starts
287
	 *
288
	 * This method should be overwritten in a filter if you want to do
289
	 * something before the parsing process starts. This can be useful to
290
	 * allow certain short alternative tags which then can be converted into
291
	 * proper tags with preg_replace() calls.
292
	 * The main class walks through all the filters and and calls this
293
	 * method. The filters should modify their private $_preparsed
294
	 * variable, with input from $_text.
295
	 *
296
	 * @return   none
297
	 * @access   private
298
	 * @see      $_text
299
	 * @author   Stijn de Reede  <[email protected]>
300
	 */
301
	public function _preparse()
302
	{
303
		// default: assign _text to _preparsed, to be overwritten by filters
304
		$this->_preparsed = $this->_text;
305
306
		// return if this is a subclass
307
		if (is_subclass_of($this, 'SSHTMLBBCodeParser')) {
308
			return;
309
		}
310
311
		// walk through the filters and execute _preparse
312
		foreach ($this->_filters as $filter) {
313
			$filter->setText($this->_preparsed);
314
			$filter->_preparse();
315
			$this->_preparsed = $filter->getPreparsed();
316
		}
317
	}
318
319
	/**
320
	 * Builds the tag array from the input string $_text
321
	 *
322
	 * An array consisting of tag and text elements is contructed from the
323
	 * $_preparsed variable. The method uses _buildTag() to check if a tag is
324
	 * valid and to build the actual tag to be added to the tag array.
325
	 *
326
	 * @todo - rewrite whole method, as this one is old and probably slow
327
	 *       - see if a recursive method would be better than an iterative one
328
	 *
329
	 * @return   none
330
	 * @access   private
331
	 * @see      _buildTag()
332
	 * @see      $_text
333
	 * @see      $_tagArray
334
	 * @author   Stijn de Reede  <[email protected]>
335
	 */
336
	public function _buildTagArray()
337
	{
338
		$this->_tagArray = array();
339
		$str = $this->_preparsed;
340
		$strPos = 0;
341
		$strLength = strlen($str);
342
343
		while (($strPos < $strLength)) {
344
			$tag = array();
345
			$openPos = strpos($str, $this->_options['open'], $strPos);
346
			if ($openPos === false) {
347
				$openPos = $strLength;
348
				$nextOpenPos = $strLength;
349
			}
350
			if ($openPos + 1 > $strLength) {
351
				$nextOpenPos = $strLength;
352
			} else {
353
				$nextOpenPos = strpos($str, $this->_options['open'], $openPos + 1);
354
				if ($nextOpenPos === false) {
355
					$nextOpenPos = $strLength;
356
				}
357
			}
358
			$closePos = strpos($str, $this->_options['close'], $strPos);
359
			if ($closePos === false) {
360
				$closePos = $strLength + 1;
361
			}
362
363
			if ($openPos == $strPos) {
364
				if (($nextOpenPos < $closePos)) {
365
					// new open tag before closing tag: treat as text
366
					$newPos = $nextOpenPos;
367
					$tag['text'] = substr($str, $strPos, $nextOpenPos - $strPos);
368
					$tag['type'] = 0;
369
				} else {
370
					// possible valid tag
371
					$newPos = $closePos + 1;
372
					$newTag = $this->_buildTag(substr($str, $strPos, $closePos - $strPos + 1));
373
					if (($newTag !== false)) {
374
						$tag = $newTag;
375
					} else {
376
						// no valid tag after all
377
						$tag['text'] = substr($str, $strPos, $closePos - $strPos + 1);
378
						$tag['type'] = 0;
379
					}
380
				}
381
			} else {
382
				// just text
383
				$newPos = $openPos;
384
				$tag['text'] = substr($str, $strPos, $openPos - $strPos);
385
				$tag['type'] = 0;
386
			}
387
388
			// join 2 following text elements
389
			if ($tag['type'] === 0 && isset($prev) && $prev['type'] === 0) {
390
				$tag['text'] = $prev['text'].$tag['text'];
391
				array_pop($this->_tagArray);
392
			}
393
394
			$this->_tagArray[] = $tag;
395
			$prev = $tag;
396
			$strPos = $newPos;
397
		}
398
	}
399
400
	/**
401
	 * Builds a tag from the input string
402
	 *
403
	 * This method builds a tag array based on the string it got as an
404
	 * argument. If the tag is invalid, <false> is returned. The tag
405
	 * attributes are extracted from the string and stored in the tag
406
	 * array as an associative array.
407
	 *
408
	 * @param    string          string to build tag from
409
	 * @return   array           tag in array format
410
	 * @access   private
411
	 * @see      _buildTagArray()
412
	 * @author   Stijn de Reede  <[email protected]>
413
	 */
414
	public function _buildTag($str)
415
	{
416
		$tag = array('text' => $str, 'attributes' => array());
417
418
		if (substr($str, 1, 1) == '/') {        // closing tag
419
420
			$tag['tag'] = strtolower(substr($str, 2, strlen($str) - 3));
421
			if (!in_array($tag['tag'], array_keys($this->_definedTags))) {
422
				return false;                   // nope, it's not valid
423
			} else {
424
				$tag['type'] = 2;
425
				return $tag;
426
			}
427
		} else {                                // opening tag
428
429
			$tag['type'] = 1;
430
			if (strpos($str, ' ') && (strpos($str, '=') === false)) {
431
				return false;                   // nope, it's not valid
432
			}
433
434
			// tnx to Onno for the regex
435
			// split the tag with arguments and all
436
			$oe = $this->_options['open_esc'];
437
			$ce = $this->_options['close_esc'];
438
			$tagArray = array();
439
			if (preg_match("!$oe([a-z0-9]+)[^$ce]*$ce!i", $str, $tagArray) == 0) {
440
				return false;
441
			}
442
			$tag['tag'] = strtolower($tagArray[1]);
443
			if (!in_array($tag['tag'], array_keys($this->_definedTags))) {
444
				return false;                   // nope, it's not valid
445
			}
446
447
			// tnx to Onno for the regex
448
			// validate the arguments
449
			$attributeArray = array();
450
			$regex = "![\s$oe]([a-z0-9]+)=(\"[^\s$ce]+\"|[^\s$ce]";
451
			if ($tag['tag'] != 'url') {
452
				$regex .= "[^=]";
453
			}
454
			$regex .= "+)(?=[\s$ce])!i";
455
			preg_match_all($regex, $str, $attributeArray, PREG_SET_ORDER);
456
			foreach ($attributeArray as $attribute) {
457
				$attNam = strtolower($attribute[1]);
458
				if (in_array($attNam, array_keys($this->_definedTags[$tag['tag']]['attributes']))) {
459
					if ($attribute[2][0] == '"' && $attribute[2][strlen($attribute[2])-1] == '"') {
460
						$tag['attributes'][$attNam] = substr($attribute[2], 1, -1);
461
					} else {
462
						$tag['attributes'][$attNam] = $attribute[2];
463
					}
464
				}
465
			}
466
			return $tag;
467
		}
468
	}
469
470
	/**
471
	 * Validates the tag array, regarding the allowed tags
472
	 *
473
	 * While looping through the tag array, two following text tags are
474
	 * joined, and it is checked that the tag is allowed inside the
475
	 * last opened tag.
476
	 * By remembering what tags have been opened it is checked that
477
	 * there is correct (xml compliant) nesting.
478
	 * In the end all still opened tags are closed.
479
	 *
480
	 * @return   none
481
	 * @access   private
482
	 * @see      _isAllowed()
483
	 * @see      $_tagArray
484
	 * @author   Stijn de Reede  <[email protected]>, Seth Price <[email protected]>
485
	 */
486
	public function _validateTagArray()
487
	{
488
		$newTagArray = array();
489
		$openTags = array();
490
		foreach ($this->_tagArray as $tag) {
491
			$prevTag = end($newTagArray);
492
			switch ($tag['type']) {
493
			case 0:
494
				if (($child = $this->_childNeeded(end($openTags), 'text')) &&
495
					$child !== false &&
496
					/*
497
					 * No idea what to do in this case: A child is needed, but
498
					 * no valid one is returned. We'll ignore it here and live
499
					 * with it until someone reports a valid bug.
500
					 */
501
					$child !== true )
502
				{
503
					if (trim($tag['text']) == '') {
504
						//just an empty indentation or newline without value?
505
						continue;
506
					}
507
					$newTagArray[] = $child;
508
					$openTags[] = $child['tag'];
509
				}
510
				if ($prevTag['type'] === 0) {
511
					$tag['text'] = $prevTag['text'].$tag['text'];
512
					array_pop($newTagArray);
513
				}
514
				$newTagArray[] = $tag;
515
				break;
516
517
			case 1:
518
				if (!$this->_isAllowed(end($openTags), $tag['tag']) ||
519
					($parent = $this->_parentNeeded(end($openTags), $tag['tag'])) === true ||
520
					($child  = $this->_childNeeded(end($openTags),  $tag['tag'])) === true) {
521
					$tag['type'] = 0;
522
					if ($prevTag['type'] === 0) {
523
						$tag['text'] = $prevTag['text'].$tag['text'];
524
						array_pop($newTagArray);
525
					}
526
				} else {
527
					if ($parent) {
528
						/*
529
						 * Avoid use of parent if we can help it. If we are
530
						 * trying to insert a new parent, but the current tag is
531
						 * the same as the previous tag, then assume that the
532
						 * previous tag structure is valid, and add this tag as
533
						 * a sibling. To add as a sibling, we need to close the
534
						 * current tag.
535
						 */
536
						if ($tag['tag'] == end($openTags)){
537
							$newTagArray[] = $this->_buildTag('[/'.$tag['tag'].']');
538
							array_pop($openTags);
539
						} else {
540
							$newTagArray[] = $parent;
541
							$openTags[] = $parent['tag'];
542
						}
543
					}
544
					if ($child) {
545
						$newTagArray[] = $child;
546
						$openTags[] = $child['tag'];
547
					}
548
					$openTags[] = $tag['tag'];
549
				}
550
				$newTagArray[] = $tag;
551
				break;
552
553
			case 2:
554
				if (($tag['tag'] == end($openTags) || $this->_isAllowed(end($openTags), $tag['tag']))) {
555
					if (in_array($tag['tag'], $openTags)) {
556
						$tmpOpenTags = array();
557
						while (end($openTags) != $tag['tag']) {
558
							$newTagArray[] = $this->_buildTag('[/'.end($openTags).']');
559
							$tmpOpenTags[] = end($openTags);
560
							array_pop($openTags);
561
						}
562
						$newTagArray[] = $tag;
563
						array_pop($openTags);
564
						/* why is this here? it just seems to break things
565
						 * (nested lists where closing tags need to be
566
						 * generated)
567
						while (end($tmpOpenTags)) {
568
							$tmpTag = $this->_buildTag('['.end($tmpOpenTags).']');
569
							$newTagArray[] = $tmpTag;
570
							$openTags[] = $tmpTag['tag'];
571
							array_pop($tmpOpenTags);
572
						}*/
573
					}
574
				} else {
575
					$tag['type'] = 0;
576
					if ($prevTag['type'] === 0) {
577
						$tag['text'] = $prevTag['text'].$tag['text'];
578
						array_pop($newTagArray);
579
					}
580
					$newTagArray[] = $tag;
581
				}
582
				break;
583
			}
584
		}
585
		while (end($openTags)) {
586
			$newTagArray[] = $this->_buildTag('[/'.end($openTags).']');
587
			array_pop($openTags);
588
		}
589
		$this->_tagArray = $newTagArray;
590
	}
591
592
	/**
593
	 * Checks to see if a parent is needed
594
	 *
595
	 * Checks to see if the current $in tag has an appropriate parent. If it
596
	 * does, then it returns false. If a parent is needed, then it returns the
597
	 * first tag in the list to add to the stack.
598
	 *
599
	 * @param    array           tag that is on the outside
600
	 * @param    array           tag that is on the inside
601
	 * @return   boolean         false if not needed, tag if needed, true if out
602
	 *                           of  our minds
603
	 * @access   private
604
	 * @see      _validateTagArray()
605
	 * @author   Seth Price <[email protected]>
606
	 */
607
	public function _parentNeeded($out, $in)
608
	{
609
		if (!isset($this->_definedTags[$in]['parent']) ||
610
			($this->_definedTags[$in]['parent'] == 'all')
611
		) {
612
			return false;
613
		}
614
615
		$ar = explode('^', $this->_definedTags[$in]['parent']);
616
		$tags = explode(',', $ar[1]);
617
		if ($ar[0] == 'none'){
618
			if ($out && in_array($out, $tags)) {
619
				return false;
620
			}
621
			//Create a tag from the first one on the list
622
			return $this->_buildTag('['.$tags[0].']');
623
		}
624
		if ($ar[0] == 'all' && $out && !in_array($out, $tags)) {
625
			return false;
626
		}
627
		// Tag is needed, we don't know which one. We could make something up,
628
		// but it would be so random, I think that it would be worthless.
629
		return true;
630
	}
631
632
	/**
633
	 * Checks to see if a child is needed
634
	 *
635
	 * Checks to see if the current $out tag has an appropriate child. If it
636
	 * does, then it returns false. If a child is needed, then it returns the
637
	 * first tag in the list to add to the stack.
638
	 *
639
	 * @param    array           tag that is on the outside
640
	 * @param    array           tag that is on the inside
641
	 * @return   boolean         false if not needed, tag if needed, true if out
642
	 *                           of our minds
643
	 * @access   private
644
	 * @see      _validateTagArray()
645
	 * @author   Seth Price <[email protected]>
646
	 */
647
	public function _childNeeded($out, $in)
648
	{
649
		if (!isset($this->_definedTags[$out]['child']) ||
650
			($this->_definedTags[$out]['child'] == 'all')
651
		) {
652
			return false;
653
		}
654
655
		$ar = explode('^', $this->_definedTags[$out]['child']);
656
		$tags = explode(',', $ar[1]);
657
		if ($ar[0] == 'none'){
658
			if ($in && in_array($in, $tags)) {
659
				return false;
660
			}
661
			//Create a tag from the first one on the list
662
			return $this->_buildTag('['.$tags[0].']');
663
		}
664
		if ($ar[0] == 'all' && $in && !in_array($in, $tags)) {
665
			return false;
666
		}
667
		// Tag is needed, we don't know which one. We could make something up,
668
		// but it would be so random, I think that it would be worthless.
669
		return true;
670
	}
671
672
	/**
673
	 * Checks to see if a tag is allowed inside another tag
674
	 *
675
	 * The allowed tags are extracted from the private _definedTags array.
676
	 *
677
	 * @param    array           tag that is on the outside
678
	 * @param    array           tag that is on the inside
679
	 * @return   boolean         return true if the tag is allowed, false
680
	 *                           otherwise
681
	 * @access   private
682
	 * @see      _validateTagArray()
683
	 * @author   Stijn de Reede  <[email protected]>
684
	 */
685
	public function _isAllowed($out, $in)
686
	{
687
		if (!$out || ($this->_definedTags[$out]['allowed'] == 'all')) {
688
			return true;
689
		}
690
		if ($this->_definedTags[$out]['allowed'] == 'none') {
691
			return false;
692
		}
693
694
		$ar = explode('^', $this->_definedTags[$out]['allowed']);
695
		$tags = explode(',', $ar[1]);
696
		if ($ar[0] == 'none' && in_array($in, $tags)) {
697
			return true;
698
		}
699
		if ($ar[0] == 'all'  && in_array($in, $tags)) {
700
			return false;
701
		}
702
		return false;
703
	}
704
705
	/**
706
	 * Builds a parsed string based on the tag array
707
	 *
708
	 * The correct html and attribute values are extracted from the private
709
	 * _definedTags array.
710
	 *
711
	 * @return   none
712
	 * @access   private
713
	 * @see      $_tagArray
714
	 * @see      $_parsed
715
	 * @author   Stijn de Reede  <[email protected]>
716
	 */
717
	public function _buildParsedString()
718
	{
719
		$this->_parsed = '';
720
		foreach ($this->_tagArray as $tag) {
721
			switch ($tag['type']) {
722
723
			// just text
724
			case 0:
725
				$this->_parsed .= $tag['text'];
726
				break;
727
728
			// opening tag
729
			case 1:
730
				$this->_parsed .= '<'.$this->_definedTags[$tag['tag']]['htmlopen'];
731
				if ($this->_options['quotestyle'] == 'single') $q = "'";
732
				if ($this->_options['quotestyle'] == 'double') $q = '"';
733
				foreach ($tag['attributes'] as $a => $v) {
734
					//prevent XSS attacks. IMHO this is not enough, though...
735
					//@see http://pear.php.net/bugs/bug.php?id=5609
736
					$v = preg_replace('#(script|about|applet|activex|chrome):#is', "\\1&#058;", $v);
737
					$v = htmlspecialchars($v);
738
					$v = str_replace('&amp;amp;', '&amp;', $v);
739
740
					if (($this->_options['quotewhat'] == 'nothing') ||
741
						(($this->_options['quotewhat'] == 'strings') && is_numeric($v))
742
					) {
743
						$this->_parsed .= ' '.sprintf($this->_definedTags[$tag['tag']]['attributes'][$a], $v, '');
744
					} else {
745
						$this->_parsed .= ' '.sprintf($this->_definedTags[$tag['tag']]['attributes'][$a], $v, $q);
746
					}
747
				}
748
				if ($this->_definedTags[$tag['tag']]['htmlclose'] == '' && $this->_options['xmlclose']) {
749
					$this->_parsed .= ' /';
750
				}
751
				$this->_parsed .= '>';
752
				break;
753
754
			// closing tag
755
			case 2:
756
				if ($this->_definedTags[$tag['tag']]['htmlclose'] != '') {
757
					$this->_parsed .= '</'.$this->_definedTags[$tag['tag']]['htmlclose'].'>';
758
				}
759
				break;
760
			}
761
		}
762
	}
763
764
	/**
765
	 * Sets text in the object to be parsed
766
	 *
767
	 * @param    string          the text to set in the object
768
	 * @return   none
769
	 * @access   public
770
	 * @see      getText()
771
	 * @see      $_text
772
	 * @author   Stijn de Reede  <[email protected]>
773
	 */
774
	public function setText($str)
775
	{
776
		$this->_text = $str;
777
	}
778
779
	/**
780
	 * Gets the unparsed text from the object
781
	 *
782
	 * @return   string          the text set in the object
783
	 * @access   public
784
	 * @see      setText()
785
	 * @see      $_text
786
	 * @author   Stijn de Reede  <[email protected]>
787
	 */
788
	public function getText()
789
	{
790
		return $this->_text;
791
	}
792
793
	/**
794
	 * Gets the preparsed text from the object
795
	 *
796
	 * @return   string          the text set in the object
797
	 * @access   public
798
	 * @see      _preparse()
799
	 * @see      $_preparsed
800
	 * @author   Stijn de Reede  <[email protected]>
801
	 */
802
	public function getPreparsed()
803
	{
804
		return $this->_preparsed;
805
	}
806
807
	/**
808
	 * Gets the parsed text from the object
809
	 *
810
	 * @return   string          the parsed text set in the object
811
	 * @access   public
812
	 * @see      parse()
813
	 * @see      $_parsed
814
	 * @author   Stijn de Reede  <[email protected]>
815
	 */
816
	public function getParsed()
817
	{
818
		return $this->_parsed;
819
	}
820
821
	/**
822
	 * Parses the text set in the object
823
	 *
824
	 * @return   none
825
	 * @access   public
826
	 * @see      _preparse()
827
	 * @see      _buildTagArray()
828
	 * @see      _validateTagArray()
829
	 * @see      _buildParsedString()
830
	 * @author   Stijn de Reede  <[email protected]>
831
	 */
832
	public function parse()
833
	{
834
		$this->_preparse();
835
		$this->_buildTagArray();
836
		$this->_validateTagArray();
837
		$this->_buildParsedString();
838
	}
839
840
	/**
841
	 * Quick method to do setText(), parse() and getParsed at once
842
	 *
843
	 * @return   none
844
	 * @access   public
845
	 * @see      parse()
846
	 * @see      $_text
847
	 * @author   Stijn de Reede  <[email protected]>
848
	 */
849
	public function qparse($str)
850
	{
851
		$this->_text = $str;
852
		$this->parse();
853
		return $this->_parsed;
854
	}
855
856
	/**
857
	 * Quick static method to do setText(), parse() and getParsed at once
858
	 *
859
	 * @return   none
860
	 * @access   public
861
	 * @see      parse()
862
	 * @see      $_text
863
	 * @author   Stijn de Reede  <[email protected]>
864
	 */
865
	public function staticQparse($str)
866
	{
867
		$p = new SSHTMLBBCodeParser();
868
		$str = $p->qparse($str);
869
		unset($p);
870
		return $str;
871
	}
872
}
873