Make WordPress Core


Ignore:
Timestamp:
07/10/2026 05:22:53 PM (2 months ago)
Author:
jonsurrell
Message:

HTML API: Update SELECT element parsing according to specification.

The HTML standard was updated to support customizable SELECT elements. Update HTML API parsing accordingly.

Developed in https://github.com/WordPress/wordpress-develop/pull/9298.

Props jonsurrell, dmsnell.
Fixes #63736.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r62687 r62689  
    136136 * these situations and will bail.
    137137 *
     138 * The parser does not implement the "maybe clone an option into selectedcontent" algorithm.
     139 * SELECTEDCONTENT elements may not reflect the actual selected content.
     140 *
    138141 * @since 6.4.0
    139142 *
    140143 * @see WP_HTML_Tag_Processor
    141  * @see https://html.spec.whatwg.org/
     144 * @link https://html.spec.whatwg.org/
    142145 * @phpstan-consistent-constructor
    143146 */
     
    11431146                                case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
    11441147                                        return $this->step_in_cell();
    1145 
    1146                                 case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
    1147                                         return $this->step_in_select();
    1148 
    1149                                 case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE:
    1150                                         return $this->step_in_select_in_table();
    11511148
    11521149                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
     
    26822679                         * > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset",
    26832680                         * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main",
    2684                          * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
     2681                         * > "menu", "nav", "ol", "pre", "search", "section", "select", "summary", "ul"
    26852682                         */
    26862683                        case '-ADDRESS':
     
    27092706                        case '-SEARCH':
    27102707                        case '-SECTION':
     2708                        case '-SELECT':
    27112709                        case '-SUMMARY':
    27122710                        case '-UL':
     
    30273025                         */
    30283026                        case '+INPUT':
     3027                                /*
     3028                                 * > If the parser was created as part of the HTML fragment parsing algorithm
     3029                                 * > (fragment case) and the context element passed to that algorithm is a
     3030                                 * > select element:
     3031                                 * >   1. Parse error.
     3032                                 * >   2. Ignore the token.
     3033                                 * >   3. Return.
     3034                                 */
     3035                                if ( isset( $this->context_node ) && 'SELECT' === $this->context_node->node_name ) {
     3036                                        return $this->step();
     3037                                }
     3038
     3039                                /*
     3040                                 * > If the stack of open elements has a select element in scope:
     3041                                 * >   1. Parse error.
     3042                                 * >   2. Pop elements from the stack of open elements until a select element
     3043                                 * >      has been popped from the stack.
     3044                                 */
     3045                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
     3046                                        $this->state->stack_of_open_elements->pop_until( 'SELECT' );
     3047                                }
     3048
    30293049                                $this->reconstruct_active_formatting_elements();
    30303050                                $this->insert_html_element( $this->state->current_token );
     
    30583078                                        $this->close_a_p_element();
    30593079                                }
     3080
     3081                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
     3082                                        $this->generate_implied_end_tags();
     3083                                        /*
     3084                                         * > If the stack of open elements has an option element in scope or has
     3085                                         * > an optgroup element in scope, then this is a parse error.
     3086                                         *
     3087                                         * @todo Indicate a parse error once it's possible.
     3088                                         */
     3089                                }
     3090
    30603091                                $this->insert_html_element( $this->state->current_token );
    30613092                                $this->state->frameset_ok = false;
     
    31443175                         */
    31453176                        case '+SELECT':
     3177                                /*
     3178                                 * > If the parser was created as part of the HTML fragment parsing algorithm
     3179                                 * > (fragment case) and the context element passed to that algorithm is a
     3180                                 * > select element:
     3181                                 * >   1. Parse error.
     3182                                 * >   2. Ignore the token.
     3183                                 */
     3184                                if ( isset( $this->context_node ) && 'SELECT' === $this->context_node->node_name ) {
     3185                                        // @todo Indicate a parse error once it's possible.
     3186                                        return $this->step();
     3187                                }
     3188                                /*
     3189                                 * > Otherwise, if the stack of open elements has a select element in scope:
     3190                                 * >   1. Parse error.
     3191                                 * >   2. Ignore the token.
     3192                                 * >   3. Pop elements from the stack of open elements until a select element
     3193                                 * >      has been popped from the stack.
     3194                                 */
     3195                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
     3196                                        // @todo Indicate a parse error once it's possible.
     3197                                        $this->state->stack_of_open_elements->pop_until( 'SELECT' );
     3198                                        return $this->step();
     3199                                }
     3200
    31463201                                $this->reconstruct_active_formatting_elements();
    31473202                                $this->insert_html_element( $this->state->current_token );
    31483203                                $this->state->frameset_ok = false;
    3149 
    3150                                 switch ( $this->state->insertion_mode ) {
     3204                                return true;
     3205
     3206                        /*
     3207                         * > A start tag whose tag name is "option"
     3208                         */
     3209                        case '+OPTION':
     3210                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
     3211                                        $this->generate_implied_end_tags( 'OPTGROUP' );
    31513212                                        /*
    3152                                          * > If the insertion mode is one of "in table", "in caption", "in table body", "in row",
    3153                                          * > or "in cell", then switch the insertion mode to "in select in table".
     3213                                         * > If the stack of open elements has an option element in scope, then this
     3214                                         * > is a parse error.
     3215                                         * @todo Indicate a parse error once it's possible.
    31543216                                         */
    3155                                         case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
    3156                                         case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
    3157                                         case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
    3158                                         case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
    3159                                         case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
    3160                                                 $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE;
    3161                                                 break;
    3162 
     3217                                } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     3218                                        $this->state->stack_of_open_elements->pop();
     3219                                }
     3220
     3221                                $this->reconstruct_active_formatting_elements();
     3222                                $this->insert_html_element( $this->state->current_token );
     3223                                return true;
     3224
     3225                        /*
     3226                         * > A start tag whose tag name is "optgroup"
     3227                         */
     3228                        case '+OPTGROUP':
     3229                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
     3230                                        $this->generate_implied_end_tags();
    31633231                                        /*
    3164                                          * > Otherwise, switch the insertion mode to "in select".
     3232                                         * > If the stack of open elements has an option element in scope or has an
     3233                                         * > optgroup element in scope, then this is a parse error.
     3234                                         * @todo Indicate a parse error once it's possible.
    31653235                                         */
    3166                                         default:
    3167                                                 $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT;
    3168                                                 break;
    3169                                 }
    3170                                 return true;
    3171 
    3172                         /*
    3173                          * > A start tag whose tag name is one of: "optgroup", "option"
    3174                          */
    3175                         case '+OPTGROUP':
    3176                         case '+OPTION':
    3177                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     3236                                } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
    31783237                                        $this->state->stack_of_open_elements->pop();
    31793238                                }
     3239
    31803240                                $this->reconstruct_active_formatting_elements();
    31813241                                $this->insert_html_element( $this->state->current_token );
     
    41334193                 */
    41344194                return $this->step_in_body();
    4135         }
    4136 
    4137         /**
    4138          * Parses next element in the 'in select' insertion mode.
    4139          *
    4140          * This internal function performs the 'in select' insertion mode
    4141          * logic for the generalized WP_HTML_Processor::step() function.
    4142          *
    4143          * @since 6.7.0
    4144          * @ignore
    4145          *
    4146          * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
    4147          *
    4148          * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect
    4149          * @see WP_HTML_Processor::step
    4150          *
    4151          * @return bool Whether an element was found.
    4152          */
    4153         private function step_in_select(): bool {
    4154                 $token_name = $this->get_token_name();
    4155                 $token_type = $this->get_token_type();
    4156                 $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
    4157                 $op         = "{$op_sigil}{$token_name}";
    4158 
    4159                 switch ( $op ) {
    4160                         /*
    4161                          * > Any other character token
    4162                          */
    4163                         case '#text':
    4164                                 /*
    4165                                  * > A character token that is U+0000 NULL
    4166                                  *
    4167                                  * If a text node only comprises null bytes then it should be
    4168                                  * entirely ignored and should not return to calling code.
    4169                                  */
    4170                                 if ( parent::TEXT_IS_NULL_SEQUENCE === $this->text_node_classification ) {
    4171                                         // Parse error: ignore the token.
    4172                                         return $this->step();
    4173                                 }
    4174 
    4175                                 $this->insert_html_element( $this->state->current_token );
    4176                                 return true;
    4177 
    4178                         /*
    4179                          * > A comment token
    4180                          * > A processing instruction token
    4181                          */
    4182                         case '#comment':
    4183                         case '#funky-comment':
    4184                         case '#presumptuous-tag':
    4185                         case '#processing-instruction':
    4186                                 $this->insert_html_element( $this->state->current_token );
    4187                                 return true;
    4188 
    4189                         /*
    4190                          * > A DOCTYPE token
    4191                          */
    4192                         case 'html':
    4193                                 // Parse error: ignore the token.
    4194                                 return $this->step();
    4195 
    4196                         /*
    4197                          * > A start tag whose tag name is "html"
    4198                          */
    4199                         case '+HTML':
    4200                                 return $this->step_in_body();
    4201 
    4202                         /*
    4203                          * > A start tag whose tag name is "option"
    4204                          */
    4205                         case '+OPTION':
    4206                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
    4207                                         $this->state->stack_of_open_elements->pop();
    4208                                 }
    4209                                 $this->insert_html_element( $this->state->current_token );
    4210                                 return true;
    4211 
    4212                         /*
    4213                          * > A start tag whose tag name is "optgroup"
    4214                          * > A start tag whose tag name is "hr"
    4215                          *
    4216                          * These rules are identical except for the treatment of the self-closing flag and
    4217                          * the subsequent pop of the HR void element, all of which is handled elsewhere in the processor.
    4218                          */
    4219                         case '+OPTGROUP':
    4220                         case '+HR':
    4221                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
    4222                                         $this->state->stack_of_open_elements->pop();
    4223                                 }
    4224 
    4225                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) {
    4226                                         $this->state->stack_of_open_elements->pop();
    4227                                 }
    4228 
    4229                                 $this->insert_html_element( $this->state->current_token );
    4230                                 return true;
    4231 
    4232                         /*
    4233                          * > An end tag whose tag name is "optgroup"
    4234                          */
    4235                         case '-OPTGROUP':
    4236                                 $current_node = $this->state->stack_of_open_elements->current_node();
    4237                                 if ( $current_node && 'OPTION' === $current_node->node_name ) {
    4238                                         foreach ( $this->state->stack_of_open_elements->walk_up( $current_node ) as $parent ) {
    4239                                                 break;
    4240                                         }
    4241                                         if ( $parent && 'OPTGROUP' === $parent->node_name ) {
    4242                                                 $this->state->stack_of_open_elements->pop();
    4243                                         }
    4244                                 }
    4245 
    4246                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) {
    4247                                         $this->state->stack_of_open_elements->pop();
    4248                                         return true;
    4249                                 }
    4250 
    4251                                 // Parse error: ignore the token.
    4252                                 return $this->step();
    4253 
    4254                         /*
    4255                          * > An end tag whose tag name is "option"
    4256                          */
    4257                         case '-OPTION':
    4258                                 if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
    4259                                         $this->state->stack_of_open_elements->pop();
    4260                                         return true;
    4261                                 }
    4262 
    4263                                 // Parse error: ignore the token.
    4264                                 return $this->step();
    4265 
    4266                         /*
    4267                          * > An end tag whose tag name is "select"
    4268                          * > A start tag whose tag name is "select"
    4269                          *
    4270                          * > It just gets treated like an end tag.
    4271                          */
    4272                         case '-SELECT':
    4273                         case '+SELECT':
    4274                                 if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) {
    4275                                         // Parse error: ignore the token.
    4276                                         return $this->step();
    4277                                 }
    4278                                 $this->state->stack_of_open_elements->pop_until( 'SELECT' );
    4279                                 $this->reset_insertion_mode_appropriately();
    4280                                 return true;
    4281 
    4282                         /*
    4283                          * > A start tag whose tag name is one of: "input", "keygen", "textarea"
    4284                          *
    4285                          * All three of these tags are considered a parse error when found in this insertion mode.
    4286                          */
    4287                         case '+INPUT':
    4288                         case '+KEYGEN':
    4289                         case '+TEXTAREA':
    4290                                 if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) {
    4291                                         // Ignore the token.
    4292                                         return $this->step();
    4293                                 }
    4294                                 $this->state->stack_of_open_elements->pop_until( 'SELECT' );
    4295                                 $this->reset_insertion_mode_appropriately();
    4296                                 return $this->step( self::REPROCESS_CURRENT_NODE );
    4297 
    4298                         /*
    4299                          * > A start tag whose tag name is one of: "script", "template"
    4300                          * > An end tag whose tag name is "template"
    4301                          */
    4302                         case '+SCRIPT':
    4303                         case '+TEMPLATE':
    4304                         case '-TEMPLATE':
    4305                                 return $this->step_in_head();
    4306                 }
    4307 
    4308                 /*
    4309                  * > Anything else
    4310                  * >   Parse error: ignore the token.
    4311                  */
    4312                 return $this->step();
    4313         }
    4314 
    4315         /**
    4316          * Parses next element in the 'in select in table' insertion mode.
    4317          *
    4318          * This internal function performs the 'in select in table' insertion mode
    4319          * logic for the generalized WP_HTML_Processor::step() function.
    4320          *
    4321          * @since 6.7.0
    4322          * @ignore
    4323          *
    4324          * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
    4325          *
    4326          * @see https://html.spec.whatwg.org/#parsing-main-inselectintable
    4327          * @see WP_HTML_Processor::step
    4328          *
    4329          * @return bool Whether an element was found.
    4330          */
    4331         private function step_in_select_in_table(): bool {
    4332                 $token_name = $this->get_token_name();
    4333                 $token_type = $this->get_token_type();
    4334                 $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
    4335                 $op         = "{$op_sigil}{$token_name}";
    4336 
    4337                 switch ( $op ) {
    4338                         /*
    4339                          * > A start tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"
    4340                          */
    4341                         case '+CAPTION':
    4342                         case '+TABLE':
    4343                         case '+TBODY':
    4344                         case '+TFOOT':
    4345                         case '+THEAD':
    4346                         case '+TR':
    4347                         case '+TD':
    4348                         case '+TH':
    4349                                 // @todo Indicate a parse error once it's possible.
    4350                                 $this->state->stack_of_open_elements->pop_until( 'SELECT' );
    4351                                 $this->reset_insertion_mode_appropriately();
    4352                                 return $this->step( self::REPROCESS_CURRENT_NODE );
    4353 
    4354                         /*
    4355                          * > An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"
    4356                          */
    4357                         case '-CAPTION':
    4358                         case '-TABLE':
    4359                         case '-TBODY':
    4360                         case '-TFOOT':
    4361                         case '-THEAD':
    4362                         case '-TR':
    4363                         case '-TD':
    4364                         case '-TH':
    4365                                 // @todo Indicate a parse error once it's possible.
    4366                                 if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $token_name ) ) {
    4367                                         return $this->step();
    4368                                 }
    4369                                 $this->state->stack_of_open_elements->pop_until( 'SELECT' );
    4370                                 $this->reset_insertion_mode_appropriately();
    4371                                 return $this->step( self::REPROCESS_CURRENT_NODE );
    4372                 }
    4373 
    4374                 /*
    4375                  * > Anything else
    4376                  */
    4377                 return $this->step_in_select();
    43784195        }
    43794196
     
    52365053                                        return $this->step_in_cell();
    52375054
    5238                                 case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
    5239                                         return $this->step_in_select();
    5240 
    5241                                 case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE:
    5242                                         return $this->step_in_select_in_table();
    5243 
    52445055                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
    52455056                                        return $this->step_in_template();
     
    61395950                        switch ( $node->node_name ) {
    61405951                                /*
    6141                                  * > 4. If node is a `select` element, run these substeps:
    6142                                  * >   1. If _last_ is true, jump to the step below labeled done.
    6143                                  * >   2. Let _ancestor_ be _node_.
    6144                                  * >   3. _Loop_: If _ancestor_ is the first node in the stack of open elements,
    6145                                  * >      jump to the step below labeled done.
    6146                                  * >   4. Let ancestor be the node before ancestor in the stack of open elements.
    6147                                  * >   …
    6148                                  * >   7. Jump back to the step labeled _loop_.
    6149                                  * >   8. _Done_: Switch the insertion mode to "in select" and return.
    6150                                  */
    6151                                 case 'SELECT':
    6152                                         if ( ! $last ) {
    6153                                                 foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $ancestor ) {
    6154                                                         if ( 'html' !== $ancestor->namespace ) {
    6155                                                                 continue;
    6156                                                         }
    6157 
    6158                                                         switch ( $ancestor->node_name ) {
    6159                                                                 /*
    6160                                                                  * > 5. If _ancestor_ is a `template` node, jump to the step below
    6161                                                                  * >    labeled _done_.
    6162                                                                  */
    6163                                                                 case 'TEMPLATE':
    6164                                                                         break 2;
    6165 
    6166                                                                 /*
    6167                                                                  * > 6. If _ancestor_ is a `table` node, switch the insertion mode to
    6168                                                                  * >    "in select in table" and return.
    6169                                                                  */
    6170                                                                 case 'TABLE':
    6171                                                                         $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE;
    6172                                                                         return;
    6173                                                         }
    6174                                                 }
    6175                                         }
    6176                                         $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT;
    6177                                         return;
    6178 
    6179                                 /*
    6180                                  * > 5. If _node_ is a `td` or `th` element and _last_ is false, then switch the
     5952                                 * > 4. If _node_ is a `td` or `th` element and _last_ is false, then switch the
    61815953                                 * >    insertion mode to "in cell" and return.
    61825954                                 */
     
    61895961                                        break;
    61905962
    6191                                         /*
    6192                                         * > 6. If _node_ is a `tr` element, then switch the insertion mode to "in row"
    6193                                         * >    and return.
    6194                                         */
     5963                                /*
     5964                                * > 5. If _node_ is a `tr` element, then switch the insertion mode to "in row"
     5965                                * >    and return.
     5966                                */
    61955967                                case 'TR':
    61965968                                        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     
    61985970
    61995971                                /*
    6200                                  * > 7. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the
     5972                                 * > 6. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the
    62015973                                 * >    insertion mode to "in table body" and return.
    62025974                                 */
     
    62085980
    62095981                                /*
    6210                                  * > 8. If _node_ is a `caption` element, then switch the insertion mode to
     5982                                 * > 7. If _node_ is a `caption` element, then switch the insertion mode to
    62115983                                 * >    "in caption" and return.
    62125984                                 */
     
    62165988
    62175989                                /*
    6218                                  * > 9. If _node_ is a `colgroup` element, then switch the insertion mode to
     5990                                 * > 8. If _node_ is a `colgroup` element, then switch the insertion mode to
    62195991                                 * >    "in column group" and return.
    62205992                                 */
     
    62245996
    62255997                                /*
    6226                                  * > 10. If _node_ is a `table` element, then switch the insertion mode to
    6227                                  * >     "in table" and return.
     5998                                 * > 9. If _node_ is a `table` element, then switch the insertion mode to
     5999                                 * >    "in table" and return.
    62286000                                 */
    62296001                                case 'TABLE':
     
    62326004
    62336005                                /*
    6234                                  * > 11. If _node_ is a `template` element, then switch the insertion mode to the
     6006                                 * > 10. If _node_ is a `template` element, then switch the insertion mode to the
    62356007                                 * >     current template insertion mode and return.
    62366008                                 */
     
    62406012
    62416013                                /*
    6242                                  * > 12. If _node_ is a `head` element and _last_ is false, then switch the
     6014                                 * > 11. If _node_ is a `head` element and _last_ is false, then switch the
    62436015                                 * >     insertion mode to "in head" and return.
    62446016                                 */
     
    62516023
    62526024                                /*
    6253                                  * > 13. If _node_ is a `body` element, then switch the insertion mode to "in body"
     6025                                 * > 12. If _node_ is a `body` element, then switch the insertion mode to "in body"
    62546026                                 * >     and return.
    62556027                                 */
     
    62596031
    62606032                                /*
    6261                                  * > 14. If _node_ is a `frameset` element, then switch the insertion mode to
     6033                                 * > 13. If _node_ is a `frameset` element, then switch the insertion mode to
    62626034                                 * >     "in frameset" and return. (fragment case)
    62636035                                 */
     
    62676039
    62686040                                /*
    6269                                  * > 15. If _node_ is an `html` element, run these substeps:
     6041                                 * > 14. If _node_ is an `html` element, run these substeps:
    62706042                                 * >     1. If the head element pointer is null, switch the insertion mode to
    62716043                                 * >        "before head" and return. (fragment case)
     
    62826054
    62836055                /*
    6284                  * > 16. If _last_ is true, then switch the insertion mode to "in body"
     6056                 * > 15. If _last_ is true, then switch the insertion mode to "in body"
    62856057                 * >     and return. (fragment case)
    62866058                 *
Note: See TracChangeset for help on using the changeset viewer.