Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.78% |
81 / 82 |
|
88.89% |
8 / 9 |
CRAP | |
0.00% |
0 / 1 |
| ParserCoreTagHooks | |
98.78% |
81 / 82 |
|
88.89% |
8 / 9 |
23 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| register | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| pre | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
| html | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
| nowiki | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| gallery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| indicator | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| langconvert | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
| strictlyValidateBcp47Variant | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Tag hooks provided by MediaWiki core |
| 4 | * |
| 5 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | * @ingroup Parser |
| 8 | */ |
| 9 | |
| 10 | namespace MediaWiki\Parser; |
| 11 | |
| 12 | use MediaWiki\Config\ServiceOptions; |
| 13 | use MediaWiki\Html\Html; |
| 14 | use MediaWiki\Language\LanguageCode; |
| 15 | use MediaWiki\Language\LanguageConverterFactory; |
| 16 | use MediaWiki\Language\LanguageFactory; |
| 17 | use MediaWiki\MainConfigNames; |
| 18 | use UnexpectedValueException; |
| 19 | use Wikimedia\StringUtils\StringUtils; |
| 20 | |
| 21 | /** |
| 22 | * Various tag hooks, registered in every Parser |
| 23 | * @ingroup Parser |
| 24 | * @since 1.46 |
| 25 | */ |
| 26 | class ParserCoreTagHooks { |
| 27 | |
| 28 | /** |
| 29 | * @internal For use by ServiceWiring |
| 30 | */ |
| 31 | public const CONSTRUCTOR_OPTIONS = [ |
| 32 | // See documentation for the corresponding config options |
| 33 | MainConfigNames::RawHtml, |
| 34 | ]; |
| 35 | |
| 36 | /** |
| 37 | * @internal |
| 38 | */ |
| 39 | public function __construct( |
| 40 | private readonly ServiceOptions $svcOptions, |
| 41 | private readonly LanguageFactory $languageFactory, |
| 42 | private readonly LanguageConverterFactory $languageConverterFactory, |
| 43 | ) { |
| 44 | $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * @internal |
| 49 | */ |
| 50 | public function register( Parser $parser ): void { |
| 51 | $rawHtml = $this->svcOptions->get( MainConfigNames::RawHtml ); |
| 52 | $parser->setHook( 'pre', $this->pre( ... ) ); |
| 53 | $parser->setHook( 'nowiki', $this->nowiki( ... ) ); |
| 54 | $parser->setHook( 'gallery', $this->gallery( ... ) ); |
| 55 | $parser->setHook( 'indicator', $this->indicator( ... ) ); |
| 56 | $parser->setHook( 'langconvert', $this->langconvert( ... ) ); |
| 57 | if ( $rawHtml ) { |
| 58 | $parser->setHook( 'html', $this->html( ... ) ); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Core parser tag hook function for 'pre'. |
| 64 | * Text is treated roughly as 'nowiki' wrapped in an HTML 'pre' tag; |
| 65 | * valid HTML attributes are passed on. |
| 66 | * |
| 67 | * @param ?string $content |
| 68 | * @param array $attribs |
| 69 | * @param Parser $parser |
| 70 | * @param PPFrame $frame |
| 71 | * @return string HTML |
| 72 | * @internal |
| 73 | */ |
| 74 | public function pre( |
| 75 | ?string $content, array $attribs, Parser $parser, PPFrame $frame |
| 76 | ): string { |
| 77 | $content ??= ''; |
| 78 | |
| 79 | if ( ( $attribs['format'] ?? '' ) === 'wikitext' ) { |
| 80 | // T348722: $frame is omitted here. Editors should |
| 81 | // use the tag parser function if they want access |
| 82 | // to template args, for compatibility with Parsoid |
| 83 | $content = $parser->recursiveTagParse( $content ); |
| 84 | } else { |
| 85 | // Backwards-compatibility hack |
| 86 | $content = StringUtils::delimiterReplace( |
| 87 | '<nowiki>', '</nowiki>', '$1', $content, 'i' |
| 88 | ); |
| 89 | |
| 90 | // We need to let both '"' and '&' through, |
| 91 | // for strip markers and entities respectively. |
| 92 | $content = str_replace( |
| 93 | [ '>', '<' ], |
| 94 | [ '>', '<' ], |
| 95 | $content |
| 96 | ); |
| 97 | } |
| 98 | |
| 99 | $attribs = array_map( $parser->killMarkers( ... ), $attribs ); |
| 100 | $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); |
| 101 | |
| 102 | return Html::rawElement( 'pre', $attribs, $content ); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Core parser tag hook function for 'html', used only when |
| 107 | * $wgRawHtml is enabled. |
| 108 | * |
| 109 | * This is potentially unsafe and should be used only in very careful |
| 110 | * circumstances, as the contents are emitted as raw HTML. |
| 111 | * |
| 112 | * Uses undocumented extended tag hook return values, introduced in r61913. |
| 113 | * |
| 114 | * @suppress SecurityCheck-XSS |
| 115 | * @param ?string $content |
| 116 | * @param array $attributes |
| 117 | * @param Parser $parser |
| 118 | * @return array|string Output of tag hook |
| 119 | * @internal |
| 120 | */ |
| 121 | public function html( ?string $content, array $attributes, Parser $parser ) { |
| 122 | $rawHtml = $this->svcOptions->get( MainConfigNames::RawHtml ); |
| 123 | if ( $rawHtml ) { |
| 124 | if ( $parser->getOptions()->getAllowUnsafeRawHtml() ) { |
| 125 | return [ $content ?? '', 'markerType' => 'nowiki' ]; |
| 126 | } else { |
| 127 | // In a system message where raw html is |
| 128 | // not allowed (but it is allowed in other |
| 129 | // contexts). |
| 130 | return Html::element( 'span', |
| 131 | [ 'class' => 'error' ], |
| 132 | $parser->msg( 'rawhtml-notallowed' )->text() |
| 133 | ); |
| 134 | } |
| 135 | } else { |
| 136 | throw new UnexpectedValueException( '<html> extension tag encountered unexpectedly' ); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * Core parser tag hook function for 'nowiki'. Text within this section |
| 142 | * gets interpreted as a string of text with HTML-compatible character |
| 143 | * references, and wiki markup within it will not be expanded. |
| 144 | * |
| 145 | * Uses undocumented extended tag hook return values, introduced in r61913. |
| 146 | * |
| 147 | * Used outside core by the Scribunto extension. |
| 148 | * |
| 149 | * Uses custom html escaping which phan-taint-check won't recognize |
| 150 | * hence we suppress the error. |
| 151 | * @suppress SecurityCheck-XSS |
| 152 | * |
| 153 | * @param ?string $content |
| 154 | * @param array $attributes |
| 155 | * @param Parser $parser |
| 156 | * @return array |
| 157 | */ |
| 158 | public function nowiki( ?string $content, array $attributes, Parser $parser ): array { |
| 159 | $content = strtr( $content ?? '', [ |
| 160 | // lang converter |
| 161 | '-{' => '-{', |
| 162 | '}-' => '}-', |
| 163 | // html tags |
| 164 | '<' => '<', |
| 165 | '>' => '>' |
| 166 | // Note: Both '"' and '&' are not converted. |
| 167 | // This allows strip markers and entities through. |
| 168 | ] ); |
| 169 | return [ $content, 'markerType' => 'nowiki' ]; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Core parser tag hook function for 'gallery'. |
| 174 | * |
| 175 | * Renders a thumbnail list of the given images, with optional captions. |
| 176 | * Full syntax documented on the wiki: |
| 177 | * |
| 178 | * https://www.mediawiki.org/wiki/Help:Images#Gallery_syntax |
| 179 | * |
| 180 | * @todo break Parser::renderImageGallery out here too. |
| 181 | * |
| 182 | * @param ?string $content |
| 183 | * @param array $attributes |
| 184 | * @param Parser $parser |
| 185 | * @return string HTML |
| 186 | * @internal |
| 187 | */ |
| 188 | public function gallery( ?string $content, array $attributes, Parser $parser ): string { |
| 189 | return $parser->renderImageGallery( $content ?? '', $attributes ); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * XML-style tag for page status indicators: icons (or short text snippets) usually displayed in |
| 194 | * the top-right corner of the page, outside of the main content. |
| 195 | * |
| 196 | * Used outside core by the CommunityRequests extension. |
| 197 | * |
| 198 | * @param ?string $content |
| 199 | * @param array $attributes |
| 200 | * @param Parser $parser |
| 201 | * @param PPFrame $frame |
| 202 | * @return string |
| 203 | */ |
| 204 | public function indicator( ?string $content, array $attributes, Parser $parser, PPFrame $frame ): string { |
| 205 | if ( !isset( $attributes['name'] ) || trim( $attributes['name'] ) === '' ) { |
| 206 | return '<span class="error">' . |
| 207 | $parser->msg( 'invalid-indicator-name' )->parse() . |
| 208 | '</span>'; |
| 209 | } |
| 210 | |
| 211 | $parser->getOutput()->setIndicator( |
| 212 | trim( $parser->killMarkers( $attributes['name'] ) ), |
| 213 | Parser::stripOuterParagraph( $parser->recursiveTagParseFully( $content ?? '', $frame ) ) |
| 214 | ); |
| 215 | |
| 216 | return ''; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Returns content converted into the requested language variant, using LanguageConverter. |
| 221 | * |
| 222 | * @param ?string $content |
| 223 | * @param array $attributes |
| 224 | * @param Parser $parser |
| 225 | * @param PPFrame $frame |
| 226 | * @return array |
| 227 | * @internal |
| 228 | */ |
| 229 | public function langconvert( ?string $content, array $attributes, Parser $parser, PPFrame $frame ): array { |
| 230 | $output = false; |
| 231 | if ( isset( $attributes['from'] ) && isset( $attributes['to'] ) ) { |
| 232 | $from = self::strictlyValidateBcp47Variant( $attributes['from'] ); |
| 233 | $to = self::strictlyValidateBcp47Variant( $attributes['to'] ); |
| 234 | // Verify these are both variants of the same base language! |
| 235 | if ( $from !== null && $to !== null && $from['parent']->equals( $to['parent'] ) ) { |
| 236 | $converter = $from['converter']; |
| 237 | $output = $converter->autoConvert( |
| 238 | $parser->recursiveTagParse( $content ?? '', $frame ), |
| 239 | $to['code'] |
| 240 | ); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | if ( $output === false ) { |
| 245 | $output = Html::rawElement( |
| 246 | 'span', |
| 247 | [ 'class' => 'error' ], |
| 248 | $parser->msg( 'invalid-langconvert-attrs' )->parse() |
| 249 | ); |
| 250 | } |
| 251 | // Protect this from double-conversion. |
| 252 | return [ $output, 'markerType' => 'nowiki' ]; |
| 253 | } |
| 254 | |
| 255 | private function strictlyValidateBcp47Variant( string $arg ): ?array { |
| 256 | // allow whitespace around raw value |
| 257 | $arg = trim( $arg ); |
| 258 | // Validate the language code against the allowed variants in the |
| 259 | // appropriate converter for the base language. |
| 260 | $lang = $this->languageFactory->getLanguage( $arg ); |
| 261 | $baseLang = $this->languageFactory->getParentLanguage( $lang ); |
| 262 | $converter = $this->languageConverterFactory->getLanguageConverter( $baseLang ); |
| 263 | $variant = $converter->validateVariant( $lang->getCode() ); |
| 264 | // Now strictly enforce the fact that the original argument was the |
| 265 | // BCP-47 code in particular (not a compatibility alias) |
| 266 | // This helps ensure that legacy compatibility aliases don't creep |
| 267 | // into new wikitext (since <langconvert> was a new extension). |
| 268 | if ( |
| 269 | $variant === null || |
| 270 | strcasecmp( LanguageCode::bcp47( $variant ), $arg ) !== 0 |
| 271 | ) { |
| 272 | return null; |
| 273 | } |
| 274 | return [ |
| 275 | 'converter' => $converter, |
| 276 | 'language' => $lang, |
| 277 | 'parent' => $baseLang, |
| 278 | 'code' => $variant |
| 279 | ]; |
| 280 | } |
| 281 | } |