Version: current

      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 // | license@php.net so we can mail you a copy immediately.               |
     15 // +----------------------------------------------------------------------+
     16 // | Author: Stijn de Reede <sjr@gmx.co.uk>                               |
     17 // +----------------------------------------------------------------------+
     18 //
     19 // $Id: BBCodeParser.php,v 1.17 2007/07/02 18:46:30 cweiske Exp $
     20 //
     21 // Modified by SilverStripe www.silverstripe.com
     22
     23 /**
     24  * @package  sapphire
     25  * @subpackage misc
     26  * @author   Stijn de Reede  <sjr@gmx.co.uk> , 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  <sjr@gmx.co.uk>
    138      */
    139     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     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 <l.alberton@quipo.it>
    198      */
    199     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 <l.alberton@quipo.it>
    209      */
    210     function addFilter($filter)
    211     {
    212
    213         $filter = ucfirst($filter);
    214         if (!array_key_exists($filter, $this->_filters)) {
    215             $class = 'SSHTMLBBCodeParser_Filter_'.$filter;
    216             if (fopen('BBCodeParser/Filter/'.$filter.'.php','r',true)) {
    217                 include_once 'BBCodeParser/Filter/'.$filter.'.php';
    218             }
    219             if (!class_exists($class)) {
    220
    221                 //PEAR::raiseError("Failed to load filter $filter", null, PEAR_ERROR_DIE);
    222             }
    223             else {
    224                 $this->_filters[$filter] = new $class;
    225                 $this->_definedTags = array_merge(
    226                     $this->_definedTags,
    227                     $this->_filters[$filter]->_definedTags
    228                 );
    229             }
    230         }
    231
    232     }
    233
    234     /**
    235      * Remove an existing filter
    236      *
    237      * @param string $filter
    238      * @author Lorenzo Alberton <l.alberton@quipo.it>
    239      */
    240     function removeFilter($filter)
    241     {
    242         $filter = ucfirst(trim($filter));
    243         if (!empty($filter) && array_key_exists($filter, $this->_filters)) {
    244             unset($this->_filters[$filter]);
    245         }
    246         // also remove the related $this->_definedTags for this filter,
    247         // preserving the others
    248         $this->_definedTags = array();
    249         foreach (array_keys($this->_filters) as $filter) {
    250             $this->_definedTags = array_merge(
    251                 $this->_definedTags,
    252                 $this->_filters[$filter]->_definedTags
    253             );
    254         }
    255     }
    256
    257     /**
    258      * Add new filters
    259      *
    260      * @param mixed (array or string)
    261      * @return boolean true if all ok, false if not.
    262      * @author Lorenzo Alberton <l.alberton@quipo.it>
    263      */
    264     function addFilters($filters)
    265     {
    266         if (is_string($filters)) {
    267             //comma-separated list
    268             if (strpos($filters, ',') !== false) {
    269                 $filters = explode(',', $filters);
    270             } else {
    271                 $filters = array($filters);
    272             }
    273         }
    274         if (!is_array($filters)) {
    275             //invalid format
    276             return false;
    277         }
    278         foreach ($filters as $filter) {
    279             if (trim($filter)){
    280                 $this->addFilter($filter);
    281             }
    282         }
    283         return true;
    284     }
    285
    286     /**
    287      * Executes statements before the actual array building starts
    288      *
    289      * This method should be overwritten in a filter if you want to do
    290      * something before the parsing process starts. This can be useful to
    291      * allow certain short alternative tags which then can be converted into
    292      * proper tags with preg_replace() calls.
    293      * The main class walks through all the filters and and calls this
    294      * method. The filters should modify their private $_preparsed
    295      * variable, with input from $_text.
    296      *
    297      * @return   none
    298      * @access   private
    299      * @see      $_text
    300      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    301      */
    302     function _preparse()
    303     {
    304         // default: assign _text to _preparsed, to be overwritten by filters
    305         $this->_preparsed = $this->_text;
    306
    307         // return if this is a subclass
    308         if (is_subclass_of($this, 'SSHTMLBBCodeParser')) {
    309             return;
    310         }
    311
    312         // walk through the filters and execute _preparse
    313         foreach ($this->_filters as $filter) {
    314             $filter->setText($this->_preparsed);
    315             $filter->_preparse();
    316             $this->_preparsed = $filter->getPreparsed();
    317         }
    318     }
    319
    320     /**
    321      * Builds the tag array from the input string $_text
    322      *
    323      * An array consisting of tag and text elements is contructed from the
    324      * $_preparsed variable. The method uses _buildTag() to check if a tag is
    325      * valid and to build the actual tag to be added to the tag array.
    326      *
    327      * @todo - rewrite whole method, as this one is old and probably slow
    328      *       - see if a recursive method would be better than an iterative one
    329      *
    330      * @return   none
    331      * @access   private
    332      * @see      _buildTag()
    333      * @see      $_text
    334      * @see      $_tagArray
    335      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    336      */
    337     function _buildTagArray()
    338     {
    339         $this->_tagArray = array();
    340         $str = $this->_preparsed;
    341         $strPos = 0;
    342         $strLength = strlen($str);
    343
    344         while (($strPos < $strLength)) {
    345             $tag = array();
    346             $openPos = strpos($str, $this->_options['open'], $strPos);
    347             if ($openPos === false) {
    348                 $openPos = $strLength;
    349                 $nextOpenPos = $strLength;
    350             }
    351             if ($openPos + 1 > $strLength) {
    352                 $nextOpenPos = $strLength;
    353             } else {
    354                 $nextOpenPos = strpos($str, $this->_options['open'], $openPos + 1);
    355                 if ($nextOpenPos === false) {
    356                     $nextOpenPos = $strLength;
    357                 }
    358             }
    359             $closePos = strpos($str, $this->_options['close'], $strPos);
    360             if ($closePos === false) {
    361                 $closePos = $strLength + 1;
    362             }
    363
    364             if ($openPos == $strPos) {
    365                 if (($nextOpenPos < $closePos)) {
    366                     // new open tag before closing tag: treat as text
    367                     $newPos = $nextOpenPos;
    368                     $tag['text'] = substr($str, $strPos, $nextOpenPos - $strPos);
    369                     $tag['type'] = 0;
    370                 } else {
    371                     // possible valid tag
    372                     $newPos = $closePos + 1;
    373                     $newTag = $this->_buildTag(substr($str, $strPos, $closePos - $strPos + 1));
    374                     if (($newTag !== false)) {
    375                         $tag = $newTag;
    376                     } else {
    377                         // no valid tag after all
    378                         $tag['text'] = substr($str, $strPos, $closePos - $strPos + 1);
    379                         $tag['type'] = 0;
    380                     }
    381                 }
    382             } else {
    383                 // just text
    384                 $newPos = $openPos;
    385                 $tag['text'] = substr($str, $strPos, $openPos - $strPos);
    386                 $tag['type'] = 0;
    387             }
    388
    389             // join 2 following text elements
    390             if ($tag['type'] === 0 && isset($prev) && $prev['type'] === 0) {
    391                 $tag['text'] = $prev['text'].$tag['text'];
    392                 array_pop($this->_tagArray);
    393             }
    394
    395             $this->_tagArray[] = $tag;
    396             $prev = $tag;
    397             $strPos = $newPos;
    398         }
    399     }
    400
    401     /**
    402      * Builds a tag from the input string
    403      *
    404      * This method builds a tag array based on the string it got as an
    405      * argument. If the tag is invalid, <false> is returned. The tag
    406      * attributes are extracted from the string and stored in the tag
    407      * array as an associative array.
    408      *
    409      * @param    string          string to build tag from
    410      * @return   array           tag in array format
    411      * @access   private
    412      * @see      _buildTagArray()
    413      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    414      */
    415     function _buildTag($str)
    416     {
    417         $tag = array('text' => $str, 'attributes' => array());
    418
    419         if (substr($str, 1, 1) == '/') {        // closing tag
    420
    421             $tag['tag'] = strtolower(substr($str, 2, strlen($str) - 3));
    422             if (!in_array($tag['tag'], array_keys($this->_definedTags))) {
    423                 return false;                   // nope, it's not valid
    424             } else {
    425                 $tag['type'] = 2;
    426                 return $tag;
    427             }
    428         } else {                                // opening tag
    429
    430             $tag['type'] = 1;
    431             if (strpos($str, ' ') && (strpos($str, '=') === false)) {
    432                 return false;                   // nope, it's not valid
    433             }
    434
    435             // tnx to Onno for the regex
    436             // split the tag with arguments and all
    437             $oe = $this->_options['open_esc'];
    438             $ce = $this->_options['close_esc'];
    439             $tagArray = array();
    440             if (preg_match("!$oe([a-z0-9]+)[^$ce]*$ce!i", $str, $tagArray) == 0) {
    441                 return false;
    442             }
    443             $tag['tag'] = strtolower($tagArray[1]);
    444             if (!in_array($tag['tag'], array_keys($this->_definedTags))) {
    445                 return false;                   // nope, it's not valid
    446             }
    447
    448             // tnx to Onno for the regex
    449             // validate the arguments
    450             $attributeArray = array();
    451             $regex = "![\s$oe]([a-z0-9]+)=(\"[^\s$ce]+\"|[^\s$ce]";
    452             if ($tag['tag'] != 'url') {
    453                 $regex .= "[^=]";
    454             }
    455             $regex .= "+)(?=[\s$ce])!i";
    456             preg_match_all($regex, $str, $attributeArray, PREG_SET_ORDER);
    457             foreach ($attributeArray as $attribute) {
    458                 $attNam = strtolower($attribute[1]);
    459                 if (in_array($attNam, array_keys($this->_definedTags[$tag['tag']]['attributes']))) {
    460                     if ($attribute[2][0] == '"' && $attribute[2][strlen($attribute[2])-1] == '"') {
    461                         $tag['attributes'][$attNam] = substr($attribute[2], 1, -1);
    462                     } else {
    463                         $tag['attributes'][$attNam] = $attribute[2];
    464                     }
    465                 }
    466             }
    467             return $tag;
    468         }
    469     }
    470
    471     /**
    472      * Validates the tag array, regarding the allowed tags
    473      *
    474      * While looping through the tag array, two following text tags are
    475      * joined, and it is checked that the tag is allowed inside the
    476      * last opened tag.
    477      * By remembering what tags have been opened it is checked that
    478      * there is correct (xml compliant) nesting.
    479      * In the end all still opened tags are closed.
    480      *
    481      * @return   none
    482      * @access   private
    483      * @see      _isAllowed()
    484      * @see      $_tagArray
    485      * @author   Stijn de Reede  <sjr@gmx.co.uk>, Seth Price <seth@pricepages.org>
    486      */
    487     function _validateTagArray()
    488     {
    489         $newTagArray = array();
    490         $openTags = array();
    491         foreach ($this->_tagArray as $tag) {
    492             $prevTag = end($newTagArray);
    493             switch ($tag['type']) {
    494             case 0:
    495                 if (($child = $this->_childNeeded(end($openTags), 'text')) &&
    496                     $child !== false &&
    497                     /*
    498                      * No idea what to do in this case: A child is needed, but
    499                      * no valid one is returned. We'll ignore it here and live
    500                      * with it until someone reports a valid bug.
    501                      */
    502                     $child !== true )
    503                 {
    504                     if (trim($tag['text']) == '') {
    505                         //just an empty indentation or newline without value?
    506                         continue;
    507                     }
    508                     $newTagArray[] = $child;
    509                     $openTags[] = $child['tag'];
    510                 }
    511                 if ($prevTag['type'] === 0) {
    512                     $tag['text'] = $prevTag['text'].$tag['text'];
    513                     array_pop($newTagArray);
    514                 }
    515                 $newTagArray[] = $tag;
    516                 break;
    517
    518             case 1:
    519                 if (!$this->_isAllowed(end($openTags), $tag['tag']) ||
    520                    ($parent = $this->_parentNeeded(end($openTags), $tag['tag'])) === true ||
    521                    ($child  = $this->_childNeeded(end($openTags),  $tag['tag'])) === true) {
    522                     $tag['type'] = 0;
    523                     if ($prevTag['type'] === 0) {
    524                         $tag['text'] = $prevTag['text'].$tag['text'];
    525                         array_pop($newTagArray);
    526                     }
    527                 } else {
    528                     if ($parent) {
    529                         /*
    530                          * Avoid use of parent if we can help it. If we are
    531                          * trying to insert a new parent, but the current tag is
    532                          * the same as the previous tag, then assume that the
    533                          * previous tag structure is valid, and add this tag as
    534                          * a sibling. To add as a sibling, we need to close the
    535                          * current tag.
    536                          */
    537                         if ($tag['tag'] == end($openTags)){
    538                             $newTagArray[] = $this->_buildTag('[/'.$tag['tag'].']');
    539                             array_pop($openTags);
    540                         } else {
    541                             $newTagArray[] = $parent;
    542                             $openTags[] = $parent['tag'];
    543                         }
    544                     }
    545                     if ($child) {
    546                         $newTagArray[] = $child;
    547                         $openTags[] = $child['tag'];
    548                     }
    549                     $openTags[] = $tag['tag'];
    550                 }
    551                 $newTagArray[] = $tag;
    552                 break;
    553
    554             case 2:
    555                 if (($tag['tag'] == end($openTags) || $this->_isAllowed(end($openTags), $tag['tag']))) {
    556                     if (in_array($tag['tag'], $openTags)) {
    557                         $tmpOpenTags = array();
    558                         while (end($openTags) != $tag['tag']) {
    559                             $newTagArray[] = $this->_buildTag('[/'.end($openTags).']');
    560                             $tmpOpenTags[] = end($openTags);
    561                             array_pop($openTags);
    562                         }
    563                         $newTagArray[] = $tag;
    564                         array_pop($openTags);
    565                         /* why is this here? it just seems to break things
    566                          * (nested lists where closing tags need to be
    567                          * generated)
    568                         while (end($tmpOpenTags)) {
    569                             $tmpTag = $this->_buildTag('['.end($tmpOpenTags).']');
    570                             $newTagArray[] = $tmpTag;
    571                             $openTags[] = $tmpTag['tag'];
    572                             array_pop($tmpOpenTags);
    573                         }*/
    574                     }
    575                 } else {
    576                     $tag['type'] = 0;
    577                     if ($prevTag['type'] === 0) {
    578                         $tag['text'] = $prevTag['text'].$tag['text'];
    579                         array_pop($newTagArray);
    580                     }
    581                     $newTagArray[] = $tag;
    582                 }
    583                 break;
    584             }
    585         }
    586         while (end($openTags)) {
    587             $newTagArray[] = $this->_buildTag('[/'.end($openTags).']');
    588             array_pop($openTags);
    589         }
    590         $this->_tagArray = $newTagArray;
    591     }
    592
    593     /**
    594      * Checks to see if a parent is needed
    595      *
    596      * Checks to see if the current $in tag has an appropriate parent. If it
    597      * does, then it returns false. If a parent is needed, then it returns the
    598      * first tag in the list to add to the stack.
    599      *
    600      * @param    array           tag that is on the outside
    601      * @param    array           tag that is on the inside
    602      * @return   boolean         false if not needed, tag if needed, true if out
    603      *                           of  our minds
    604      * @access   private
    605      * @see      _validateTagArray()
    606      * @author   Seth Price <seth@pricepages.org>
    607      */
    608     function _parentNeeded($out, $in)
    609     {
    610         if (!isset($this->_definedTags[$in]['parent']) ||
    611             ($this->_definedTags[$in]['parent'] == 'all')
    612         ) {
    613             return false;
    614         }
    615
    616         $ar = explode('^', $this->_definedTags[$in]['parent']);
    617         $tags = explode(',', $ar[1]);
    618         if ($ar[0] == 'none'){
    619             if ($out && in_array($out, $tags)) {
    620                 return false;
    621             }
    622             //Create a tag from the first one on the list
    623             return $this->_buildTag('['.$tags[0].']');
    624         }
    625         if ($ar[0] == 'all' && $out && !in_array($out, $tags)) {
    626             return false;
    627         }
    628         // Tag is needed, we don't know which one. We could make something up,
    629         // but it would be so random, I think that it would be worthless.
    630         return true;
    631     }
    632
    633     /**
    634      * Checks to see if a child is needed
    635      *
    636      * Checks to see if the current $out tag has an appropriate child. If it
    637      * does, then it returns false. If a child is needed, then it returns the
    638      * first tag in the list to add to the stack.
    639      *
    640      * @param    array           tag that is on the outside
    641      * @param    array           tag that is on the inside
    642      * @return   boolean         false if not needed, tag if needed, true if out
    643      *                           of our minds
    644      * @access   private
    645      * @see      _validateTagArray()
    646      * @author   Seth Price <seth@pricepages.org>
    647      */
    648     function _childNeeded($out, $in)
    649     {
    650         if (!isset($this->_definedTags[$out]['child']) ||
    651            ($this->_definedTags[$out]['child'] == 'all')
    652         ) {
    653             return false;
    654         }
    655
    656         $ar = explode('^', $this->_definedTags[$out]['child']);
    657         $tags = explode(',', $ar[1]);
    658         if ($ar[0] == 'none'){
    659             if ($in && in_array($in, $tags)) {
    660                 return false;
    661             }
    662             //Create a tag from the first one on the list
    663             return $this->_buildTag('['.$tags[0].']');
    664         }
    665         if ($ar[0] == 'all' && $in && !in_array($in, $tags)) {
    666             return false;
    667         }
    668         // Tag is needed, we don't know which one. We could make something up,
    669         // but it would be so random, I think that it would be worthless.
    670         return true;
    671     }
    672
    673     /**
    674      * Checks to see if a tag is allowed inside another tag
    675      *
    676      * The allowed tags are extracted from the private _definedTags array.
    677      *
    678      * @param    array           tag that is on the outside
    679      * @param    array           tag that is on the inside
    680      * @return   boolean         return true if the tag is allowed, false
    681      *                           otherwise
    682      * @access   private
    683      * @see      _validateTagArray()
    684      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    685      */
    686     function _isAllowed($out, $in)
    687     {
    688         if (!$out || ($this->_definedTags[$out]['allowed'] == 'all')) {
    689             return true;
    690         }
    691         if ($this->_definedTags[$out]['allowed'] == 'none') {
    692             return false;
    693         }
    694
    695         $ar = explode('^', $this->_definedTags[$out]['allowed']);
    696         $tags = explode(',', $ar[1]);
    697         if ($ar[0] == 'none' && in_array($in, $tags)) {
    698             return true;
    699         }
    700         if ($ar[0] == 'all'  && in_array($in, $tags)) {
    701             return false;
    702         }
    703         return false;
    704     }
    705
    706     /**
    707      * Builds a parsed string based on the tag array
    708      *
    709      * The correct html and attribute values are extracted from the private
    710      * _definedTags array.
    711      *
    712      * @return   none
    713      * @access   private
    714      * @see      $_tagArray
    715      * @see      $_parsed
    716      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    717      */
    718     function _buildParsedString()
    719     {
    720         $this->_parsed = '';
    721         foreach ($this->_tagArray as $tag) {
    722             switch ($tag['type']) {
    723
    724             // just text
    725             case 0:
    726                 $this->_parsed .= $tag['text'];
    727                 break;
    728
    729             // opening tag
    730             case 1:
    731                 $this->_parsed .= '<'.$this->_definedTags[$tag['tag']]['htmlopen'];
    732                 if ($this->_options['quotestyle'] == 'single') $q = "'";
    733                 if ($this->_options['quotestyle'] == 'double') $q = '"';
    734                 foreach ($tag['attributes'] as $a => $v) {
    735                     //prevent XSS attacks. IMHO this is not enough, though...
    736                     //@see http://pear.php.net/bugs/bug.php?id=5609
    737                     $v = preg_replace('#(script|about|applet|activex|chrome):#is', "\\1&#058;", $v);
    738                     $v = htmlspecialchars($v);
    739                     $v = str_replace('&amp;amp;', '&amp;', $v);
    740
    741                     if (($this->_options['quotewhat'] == 'nothing') ||
    742                         (($this->_options['quotewhat'] == 'strings') && is_numeric($v))
    743                     ) {
    744                         $this->_parsed .= ' '.sprintf($this->_definedTags[$tag['tag']]['attributes'][$a], $v, '');
    745                     } else {
    746                         $this->_parsed .= ' '.sprintf($this->_definedTags[$tag['tag']]['attributes'][$a], $v, $q);
    747                     }
    748                 }
    749                 if ($this->_definedTags[$tag['tag']]['htmlclose'] == '' && $this->_options['xmlclose']) {
    750                     $this->_parsed .= ' /';
    751                 }
    752                 $this->_parsed .= '>';
    753                 break;
    754
    755             // closing tag
    756             case 2:
    757                 if ($this->_definedTags[$tag['tag']]['htmlclose'] != '') {
    758                     $this->_parsed .= '</'.$this->_definedTags[$tag['tag']]['htmlclose'].'>';
    759                 }
    760                 break;
    761             }
    762         }
    763     }
    764
    765     /**
    766      * Sets text in the object to be parsed
    767      *
    768      * @param    string          the text to set in the object
    769      * @return   none
    770      * @access   public
    771      * @see      getText()
    772      * @see      $_text
    773      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    774      */
    775     function setText($str)
    776     {
    777         $this->_text = $str;
    778     }
    779
    780     /**
    781      * Gets the unparsed text from the object
    782      *
    783      * @return   string          the text set in the object
    784      * @access   public
    785      * @see      setText()
    786      * @see      $_text
    787      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    788      */
    789     function getText()
    790     {
    791         return $this->_text;
    792     }
    793
    794     /**
    795      * Gets the preparsed text from the object
    796      *
    797      * @return   string          the text set in the object
    798      * @access   public
    799      * @see      _preparse()
    800      * @see      $_preparsed
    801      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    802      */
    803     function getPreparsed()
    804     {
    805         return $this->_preparsed;
    806     }
    807
    808     /**
    809      * Gets the parsed text from the object
    810      *
    811      * @return   string          the parsed text set in the object
    812      * @access   public
    813      * @see      parse()
    814      * @see      $_parsed
    815      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    816      */
    817     function getParsed()
    818     {
    819         return $this->_parsed;
    820     }
    821
    822     /**
    823      * Parses the text set in the object
    824      *
    825      * @return   none
    826      * @access   public
    827      * @see      _preparse()
    828      * @see      _buildTagArray()
    829      * @see      _validateTagArray()
    830      * @see      _buildParsedString()
    831      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    832      */
    833     function parse()
    834     {
    835         $this->_preparse();
    836         $this->_buildTagArray();
    837         $this->_validateTagArray();
    838         $this->_buildParsedString();
    839     }
    840
    841     /**
    842      * Quick method to do setText(), parse() and getParsed at once
    843      *
    844      * @return   none
    845      * @access   public
    846      * @see      parse()
    847      * @see      $_text
    848      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    849      */
    850     function qparse($str)
    851     {
    852         $this->_text = $str;
    853         $this->parse();
    854         return $this->_parsed;
    855     }
    856
    857     /**
    858      * Quick static method to do setText(), parse() and getParsed at once
    859      *
    860      * @return   none
    861      * @access   public
    862      * @see      parse()
    863      * @see      $_text
    864      * @author   Stijn de Reede  <sjr@gmx.co.uk>
    865      */
    866     function staticQparse($str)
    867     {
    868         $p = new SSHTMLBBCodeParser();
    869         $str = $p->qparse($str);
    870         unset($p);
    871         return $str;
    872     }
    873 }
    874 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -