Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddMetaData
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 3
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 updateBodyClasslist
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 95
0.00% covered (danger)
0.00%
0 / 1
306
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\DOM\Processors;
5
6use Closure;
7use DateTime;
8use Wikimedia\Parsoid\Config\Env;
9use Wikimedia\Parsoid\Core\DOMCompat;
10use Wikimedia\Parsoid\DOM\Element;
11use Wikimedia\Parsoid\DOM\Node;
12use Wikimedia\Parsoid\Parsoid;
13use Wikimedia\Parsoid\Utils\DOMUtils;
14use Wikimedia\Parsoid\Utils\PHPUtils;
15use Wikimedia\Parsoid\Utils\Utils;
16use Wikimedia\Parsoid\Wt2Html\DOMProcessorPipeline;
17use 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 */
23class 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}