Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 133 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
| AddMetaData | |
0.00% |
0 / 133 |
|
0.00% |
0 / 3 |
380 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
2 | |||
| updateBodyClasslist | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
| run | |
0.00% |
0 / 95 |
|
0.00% |
0 / 1 |
306 | |||
| 1 | <?php |
| 2 | declare( strict_types = 1 ); |
| 3 | |
| 4 | namespace Wikimedia\Parsoid\Wt2Html\DOM\Processors; |
| 5 | |
| 6 | use Closure; |
| 7 | use DateTime; |
| 8 | use Wikimedia\Parsoid\Config\Env; |
| 9 | use Wikimedia\Parsoid\Core\DOMCompat; |
| 10 | use Wikimedia\Parsoid\DOM\Element; |
| 11 | use Wikimedia\Parsoid\DOM\Node; |
| 12 | use Wikimedia\Parsoid\Parsoid; |
| 13 | use Wikimedia\Parsoid\Utils\DOMUtils; |
| 14 | use Wikimedia\Parsoid\Utils\PHPUtils; |
| 15 | use Wikimedia\Parsoid\Utils\Utils; |
| 16 | use Wikimedia\Parsoid\Wt2Html\DOMProcessorPipeline; |
| 17 | use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor; |
| 18 | |
| 19 | /** |
| 20 | * This stage is moving to core (T393925) and new feature development |
| 21 | * should be done there (PageBundleParserOutputConverter). |
| 22 | */ |
| 23 | class AddMetaData implements Wt2HtmlDOMProcessor { |
| 24 | private array $metadataMap; |
| 25 | private ?DOMProcessorPipeline $parentPipeline; |
| 26 | |
| 27 | public function __construct( ?DOMProcessorPipeline $domPP ) { |
| 28 | $this->parentPipeline = $domPP; |
| 29 | |
| 30 | // map from mediawiki metadata names to RDFa property names |
| 31 | $this->metadataMap = [ |
| 32 | 'ns' => [ |
| 33 | 'property' => 'mw:pageNamespace', |
| 34 | 'content' => '%d', |
| 35 | ], |
| 36 | 'id' => [ |
| 37 | 'property' => 'mw:pageId', |
| 38 | 'content' => '%d', |
| 39 | ], |
| 40 | |
| 41 | // DO NOT ADD rev_user, rev_userid, and rev_comment (See T125266) |
| 42 | |
| 43 | // 'rev_revid' is used to set the overall subject of the document, we don't |
| 44 | // need to add a specific <meta> or <link> element for it. |
| 45 | |
| 46 | 'rev_parentid' => [ |
| 47 | 'rel' => 'dc:replaces', |
| 48 | 'resource' => 'mwr:revision/%d', |
| 49 | ], |
| 50 | 'rev_timestamp' => [ |
| 51 | 'property' => 'dc:modified', |
| 52 | 'content' => static function ( $m ): string { |
| 53 | # Convert from TS_MW ("mediawiki timestamp") format |
| 54 | $dt = DateTime::createFromFormat( 'YmdHis', $m['rev_timestamp'] ); |
| 55 | # Note that DateTime::ISO8601 is not actually ISO8601, alas. |
| 56 | return $dt->format( 'Y-m-d\TH:i:s.000\Z' ); |
| 57 | }, |
| 58 | ], |
| 59 | 'rev_sha1' => [ |
| 60 | 'property' => 'mw:revisionSHA1', |
| 61 | 'content' => '%s', |
| 62 | ] |
| 63 | ]; |
| 64 | } |
| 65 | |
| 66 | private function updateBodyClasslist( Element $body, Env $env ): void { |
| 67 | $dir = $env->getPageConfig()->getPageLanguageDir(); |
| 68 | $bodyCL = DOMCompat::getClassList( $body ); |
| 69 | $bodyCL->add( 'mw-content-' . $dir ); |
| 70 | $bodyCL->add( 'sitedir-' . $dir ); |
| 71 | $bodyCL->add( $dir ); |
| 72 | $body->setAttribute( 'dir', $dir ); |
| 73 | |
| 74 | // Set 'mw-body-content' directly on the body. |
| 75 | // This is the designated successor for #bodyContent in core skins. |
| 76 | $bodyCL->add( 'mw-body-content' ); |
| 77 | // Set 'parsoid-body' to add the desired layout styling from Vector. |
| 78 | $bodyCL->add( 'parsoid-body' ); |
| 79 | // Also, add the 'mediawiki' class. |
| 80 | // Some MediaWiki:Common.css seem to target this selector. |
| 81 | $bodyCL->add( 'mediawiki' ); |
| 82 | // Set 'mw-parser-output' directly on the body. |
| 83 | // Templates target this class as part of the TemplateStyles RFC |
| 84 | // FIXME: This isn't expected to be found on the same element as the |
| 85 | // body class above, since some css targets it as a descendant. |
| 86 | // In visual diff'ing, we migrate the body contents to a wrapper div |
| 87 | // with this class to reduce visual differences. Consider getting |
| 88 | // rid of it. |
| 89 | $bodyCL->add( 'mw-parser-output' ); |
| 90 | |
| 91 | // Set the parsoid version on the body, for consistency with |
| 92 | // the wrapper div. |
| 93 | $body->setAttribute( 'data-mw-parsoid-version', Parsoid::version() ); |
| 94 | $body->setAttribute( 'data-mw-html-version', Parsoid::defaultHTMLVersion() ); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * @inheritDoc |
| 99 | */ |
| 100 | public function run( |
| 101 | Env $env, Node $root, array $options = [], bool $atTopLevel = false |
| 102 | ): void { |
| 103 | $title = $env->getContextTitle(); |
| 104 | $document = $root->ownerDocument; |
| 105 | |
| 106 | // Set the charset in the <head> first. |
| 107 | // This also adds the <head> element if it was missing. |
| 108 | DOMUtils::appendToHead( $document, 'meta', [ 'charset' => 'utf-8' ] ); |
| 109 | |
| 110 | // add mw: and mwr: RDFa prefixes |
| 111 | $prefixes = [ |
| 112 | 'dc: http://purl.org/dc/terms/', |
| 113 | 'mw: http://mediawiki.org/rdf/' |
| 114 | ]; |
| 115 | $document->documentElement->setAttribute( 'prefix', implode( ' ', $prefixes ) ); |
| 116 | |
| 117 | // (From wfParseUrl in core:) |
| 118 | // Protocol-relative URLs are handled really badly by parse_url(). |
| 119 | // It's so bad that the easiest way to handle them is to just prepend |
| 120 | // 'https:' and strip the protocol out later. |
| 121 | $baseURI = $env->getSiteConfig()->baseURI(); |
| 122 | $wasRelative = str_starts_with( $baseURI, '//' ); |
| 123 | if ( $wasRelative ) { |
| 124 | $baseURI = "https:$baseURI"; |
| 125 | } |
| 126 | // add 'https://' to baseURI if it was missing |
| 127 | $pu = parse_url( $baseURI ); |
| 128 | $mwrPrefix = ( !empty( $pu['scheme'] ) ? '' : 'https://' ) . |
| 129 | $baseURI . 'Special:Redirect/'; |
| 130 | |
| 131 | ( DOMCompat::getHead( $document ) )->setAttribute( 'prefix', 'mwr: ' . $mwrPrefix ); |
| 132 | |
| 133 | // add <head> content based on page meta data: |
| 134 | |
| 135 | // Add page / revision metadata to the <head> |
| 136 | // PORT-FIXME: We will need to do some refactoring to eliminate |
| 137 | // this hardcoding. Probably even merge this into metadataMap |
| 138 | $pageConfig = $env->getPageConfig(); |
| 139 | $revProps = [ |
| 140 | 'id' => $pageConfig->getPageId(), |
| 141 | 'ns' => $title->getNamespace(), |
| 142 | 'rev_parentid' => $pageConfig->getParentRevisionId(), |
| 143 | 'rev_revid' => $pageConfig->getRevisionId(), |
| 144 | 'rev_sha1' => $pageConfig->getRevisionSha1(), |
| 145 | 'rev_timestamp' => $pageConfig->getRevisionTimestamp() |
| 146 | ]; |
| 147 | foreach ( $revProps as $key => $value ) { |
| 148 | // generate proper attributes for the <meta> or <link> tag |
| 149 | if ( $value === null || $value === '' || !isset( $this->metadataMap[$key] ) ) { |
| 150 | continue; |
| 151 | } |
| 152 | |
| 153 | $attrs = []; |
| 154 | $mdm = $this->metadataMap[$key]; |
| 155 | |
| 156 | /** FIXME: The JS side has a bunch of other checks here */ |
| 157 | |
| 158 | foreach ( $mdm as $k => $v ) { |
| 159 | // evaluate a function, or perform sprintf-style formatting, or |
| 160 | // use string directly, depending on value in metadataMap |
| 161 | if ( $v instanceof Closure ) { |
| 162 | $v = $v( $revProps ); |
| 163 | } elseif ( str_contains( $v, '%' ) ) { |
| 164 | // @phan-suppress-next-line PhanPluginPrintfVariableFormatString |
| 165 | $v = sprintf( $v, $value ); |
| 166 | } |
| 167 | $attrs[$k] = $v; |
| 168 | } |
| 169 | |
| 170 | // <link> is used if there's a resource or href attribute. |
| 171 | DOMUtils::appendToHead( $document, |
| 172 | isset( $attrs['resource'] ) || isset( $attrs['href'] ) ? 'link' : 'meta', |
| 173 | $attrs |
| 174 | ); |
| 175 | } |
| 176 | |
| 177 | if ( $revProps['rev_revid'] ) { |
| 178 | $document->documentElement->setAttribute( |
| 179 | 'about', $mwrPrefix . 'revision/' . $revProps['rev_revid'] |
| 180 | ); |
| 181 | } |
| 182 | |
| 183 | // Normalize before comparison |
| 184 | if ( $title->isSameLinkAs( $env->getSiteConfig()->mainPageLinkTarget() ) ) { |
| 185 | DOMUtils::appendToHead( $document, 'meta', [ |
| 186 | 'property' => 'isMainPage', |
| 187 | 'content' => 'true' /* HTML attribute values should be strings */ |
| 188 | ] ); |
| 189 | } |
| 190 | |
| 191 | // Set the parsoid content-type strings |
| 192 | // FIXME: Should we be using http-equiv for this? |
| 193 | DOMUtils::appendToHead( $document, 'meta', [ |
| 194 | 'property' => 'mw:htmlVersion', |
| 195 | 'content' => $env->getOutputContentVersion() |
| 196 | ] |
| 197 | ); |
| 198 | // Temporary backward compatibility for clients |
| 199 | // This could be skipped if we support a version downgrade path |
| 200 | // with a major version bump. |
| 201 | DOMUtils::appendToHead( $document, 'meta', [ |
| 202 | 'property' => 'mw:html:version', |
| 203 | 'content' => $env->getOutputContentVersion() |
| 204 | ] |
| 205 | ); |
| 206 | |
| 207 | $expTitle = explode( '/', $title->getPrefixedDBKey() ); |
| 208 | $expTitle = array_map( PHPUtils::encodeURIComponent( ... ), $expTitle ); |
| 209 | |
| 210 | DOMUtils::appendToHead( $document, 'link', [ |
| 211 | 'rel' => 'dc:isVersionOf', |
| 212 | 'href' => $env->getSiteConfig()->baseURI() . implode( '/', $expTitle ) |
| 213 | ] ); |
| 214 | |
| 215 | // Add base href pointing to the wiki root |
| 216 | DOMUtils::appendToHead( $document, 'base', [ |
| 217 | 'href' => $env->getSiteConfig()->baseURI() |
| 218 | ] ); |
| 219 | |
| 220 | // PageConfig guarantees language will always be non-null. |
| 221 | $lang = $env->getPageConfig()->getPageLanguageBcp47(); |
| 222 | $body = DOMCompat::getBody( $document ); |
| 223 | $body->setAttribute( 'lang', $lang->toBcp47Code() ); |
| 224 | $this->updateBodyClasslist( $body, $env ); |
| 225 | // T324431: Note that this is *not* the displaytitle, and that |
| 226 | // the title element contents are plaintext *not* HTML |
| 227 | DOMCompat::setTitle( $document, $title->getPrefixedText() ); |
| 228 | $env->getSiteConfig()->exportMetadataToHeadBcp47( |
| 229 | $document, $env->getMetadata(), |
| 230 | $title->getPrefixedText(), $lang |
| 231 | ); |
| 232 | |
| 233 | // Indicate whether LanguageConverter is enabled, so that downstream |
| 234 | // caches can split on variant (if necessary) |
| 235 | DOMUtils::appendToHead( $document, 'meta', [ |
| 236 | 'http-equiv' => 'content-language', |
| 237 | // Note that this is "wrong": we should be returning |
| 238 | // $env->htmlContentLanguageBcp47()->toBcp47Code() directly |
| 239 | // but for back-compat we'll return the "old" mediawiki-internal |
| 240 | // code for now |
| 241 | 'content' => Utils::bcp47ToMwCode( # T323052: remove this call |
| 242 | $env->htmlContentLanguageBcp47()->toBcp47Code() |
| 243 | ), |
| 244 | ] |
| 245 | ); |
| 246 | DOMUtils::appendToHead( $document, 'meta', [ |
| 247 | 'http-equiv' => 'vary', |
| 248 | 'content' => $env->htmlVary() |
| 249 | ] |
| 250 | ); |
| 251 | |
| 252 | if ( $env->profiling() && $this->parentPipeline ) { |
| 253 | $body = DOMCompat::getBody( $document ); |
| 254 | $body->appendChild( $body->ownerDocument->createTextNode( "\n" ) ); |
| 255 | $body->appendChild( $body->ownerDocument->createComment( $this->parentPipeline->getTimeProfile() ) ); |
| 256 | $body->appendChild( $body->ownerDocument->createTextNode( "\n" ) ); |
| 257 | } |
| 258 | |
| 259 | if ( $env->hasDumpFlag( 'wt2html:limits' ) ) { |
| 260 | /* |
| 261 | * PORT-FIXME: Not yet implemented |
| 262 | $env->printWt2HtmlResourceUsage( [ |
| 263 | 'HTML Size' => strlen( DOMCompat::getOuterHTML( $document->documentElement ) ) |
| 264 | ] ); |
| 265 | */ |
| 266 | } |
| 267 | } |
| 268 | } |