Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.71% covered (warning)
68.71%
101 / 147
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetaTag
68.71% covered (warning)
68.71%
101 / 147
71.43% covered (warning)
71.43%
10 / 14
96.05
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addMetadata
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
6.01
 getAllowedParameterNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addGoogleSiteVerification
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addBingSiteVerification
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addYandexSiteVerification
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addPinterestSiteVerification
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addNortonSiteVerification
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 addNaverSiteVerification
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 addFacebookAppId
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 addFacebookAdmins
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 addHrefLangs
69.57% covered (warning)
69.57%
32 / 46
0.00% covered (danger)
0.00%
0 / 1
9.80
 addNoIndex
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 shouldAddNoIndex
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16 *
17 * @file
18 */
19
20declare( strict_types=1 );
21
22namespace MediaWiki\Extension\WikiSEO\Generator;
23
24use Html;
25use MediaWiki\Extension\WikiSEO\Validator;
26use MediaWiki\Extension\WikiSEO\WikiSEO;
27use MediaWiki\MediaWikiServices;
28use OutputPage;
29
30/**
31 * Basic metadata tag generator
32 * Adds metadata for description, keywords and robots
33 *
34 * @package MediaWiki\Extension\WikiSEO\Generator
35 */
36class MetaTag extends AbstractBaseGenerator implements GeneratorInterface {
37    protected $tags = [ 'description', 'keywords', 'robots', 'googlebot' ];
38
39    /**
40     * Initialize the generator with all metadata and the page to output the metadata onto
41     *
42     * @param array $metadata All metadata
43     * @param OutputPage $out The page to add the metadata to
44     *
45     * @return void
46     */
47    public function init( array $metadata, OutputPage $out ): void {
48        $this->metadata = $metadata;
49        $this->outputPage = $out;
50    }
51
52    /**
53     * Add the metadata to the OutputPage
54     *
55     * @return void
56     */
57    public function addMetadata(): void {
58        $this->addGoogleSiteVerification();
59        $this->addBingSiteVerification();
60        $this->addYandexSiteVerification();
61        $this->addPinterestSiteVerification();
62        $this->addNortonSiteVerification();
63        $this->addNaverSiteVerification();
64        $this->addFacebookAppId();
65        $this->addFacebookAdmins();
66        $this->addHrefLangs();
67        $this->addNoIndex();
68
69        // Meta tags already set in the page
70        $outputMeta = [];
71        foreach ( $this->outputPage->getMetaTags() as $metaTag ) {
72            $outputMeta[$metaTag[0]] = $metaTag[1];
73        }
74
75        foreach ( $this->tags as $tag ) {
76            // Set through addNoIndex
77            if ( $tag === 'robots' ) {
78                continue;
79            }
80
81            // Only add tag if it doesn't already exist in the output page
82            if ( array_key_exists( $tag, $this->metadata ) && !array_key_exists( $tag, $outputMeta ) ) {
83                $this->outputPage->addMeta( $tag, $this->metadata[$tag] );
84            }
85        }
86    }
87
88    /**
89     * @inheritDoc
90     */
91    public function getAllowedParameterNames(): array {
92        return $this->tags;
93    }
94
95    /**
96     * Add $wgGoogleSiteVerificationKey from LocalSettings
97     */
98    private function addGoogleSiteVerification(): void {
99        $googleSiteVerificationKey = $this->getConfigValue( 'GoogleSiteVerificationKey' );
100
101        if ( !empty( $googleSiteVerificationKey ) ) {
102            $this->outputPage->addMeta( 'google-site-verification', $googleSiteVerificationKey );
103        }
104    }
105
106    /**
107     * Add $wgBingSiteVerificationKey from LocalSettings
108     */
109    private function addBingSiteVerification(): void {
110        $bingSiteVerificationKey = $this->getConfigValue( 'BingSiteVerificationKey' );
111
112        if ( !empty( $bingSiteVerificationKey ) ) {
113            $this->outputPage->addMeta( 'msvalidate.01', $bingSiteVerificationKey );
114        }
115    }
116
117    /**
118     * Add $wgYandexSiteVerificationKey from LocalSettings
119     */
120    private function addYandexSiteVerification(): void {
121        $yandexSiteVerificationKey = $this->getConfigValue( 'YandexSiteVerificationKey' );
122
123        if ( !empty( $yandexSiteVerificationKey ) ) {
124            $this->outputPage->addMeta( 'yandex-verification', $yandexSiteVerificationKey );
125        }
126    }
127
128    /**
129     * Add $wgPinterestSiteVerificationKey from LocalSettings
130     */
131    private function addPinterestSiteVerification(): void {
132        $pinterestSiteVerificationKey = $this->getConfigValue( 'PinterestSiteVerificationKey' );
133
134        if ( !empty( $pinterestSiteVerificationKey ) ) {
135            $this->outputPage->addMeta( 'p:domain_verify', $pinterestSiteVerificationKey );
136        }
137    }
138
139    /**
140     * Add $wgNortonSiteVerificationKey from LocalSettings
141     */
142    private function addNortonSiteVerification(): void {
143        $nortonSiteVerificationKey = $this->getConfigValue( 'NortonSiteVerificationKey' );
144
145        if ( !empty( $nortonSiteVerificationKey ) ) {
146            $this->outputPage->addMeta(
147                'norton-safeweb-site-verification',
148                $nortonSiteVerificationKey
149            );
150        }
151    }
152
153    /**
154     * Add $wgNaverSiteVerificationKey from LocalSettings
155     */
156    private function addNaverSiteVerification(): void {
157        $naverSiteVerificationKey = $this->getConfigValue( 'NaverSiteVerificationKey' );
158
159        if ( !empty( $naverSiteVerificationKey ) ) {
160            $this->outputPage->addMeta(
161                'naver-site-verification',
162                $naverSiteVerificationKey
163            );
164        }
165    }
166
167    /**
168     * Add $wgFacebookAppId from LocalSettings
169     */
170    private function addFacebookAppId(): void {
171        $facebookAppId = $this->getConfigValue( 'FacebookAppId' );
172
173        if ( !empty( $facebookAppId ) ) {
174            $this->outputPage->addHeadItem(
175                'fb:app_id', Html::element(
176                    'meta', [
177                    'property' => 'fb:app_id',
178                    'content' => $facebookAppId,
179                    ]
180                )
181            );
182        }
183    }
184
185    /**
186     * Add $wgFacebookAdmins from LocalSettings
187     */
188    private function addFacebookAdmins(): void {
189        $facebookAdmins = $this->getConfigValue( 'FacebookAdmins' );
190
191        if ( !empty( $facebookAdmins ) ) {
192            $this->outputPage->addHeadItem(
193                'fb:admins', Html::element(
194                    'meta', [
195                    'property' => 'fb:admins',
196                    'content' => $facebookAdmins,
197                    ]
198                )
199            );
200        }
201    }
202
203    /**
204     * Sets <link rel="alternate" href="url" hreflang="language-area"> elements
205     * Will add a link element for the current page if $wgWikiSeoDefaultLanguage is set
206     */
207    private function addHrefLangs(): void {
208        $language = $this->getConfigValue( 'WikiSeoDefaultLanguage' );
209
210        $title = $this->outputPage->getTitle();
211        if ( !empty( $language ) && $title !== null && in_array( $language, Validator::$isoLanguageCodes, true ) ) {
212            $subpage = $title->getSubpageText();
213            $languageUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
214            $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
215
216            // Title might be a page containing a translation.
217            // Change the language and add an alternate link To the root page with the defined default language
218            if ( $title->isSubpage() && $languageUtils->isSupportedLanguage( $subpage ) ) {
219                $this->outputPage->addHeadItem(
220                    $language, Html::element(
221                    'link',
222                        [
223                            'rel' => 'alternate',
224                            'href' => WikiSEO::protocolizeUrl(
225                                $title->getBaseTitle()->getFullURL(),
226                                $this->outputPage->getRequest()
227                            ),
228                            'hreflang' => $language,
229                        ]
230                    )
231                );
232
233                $language = $languageFactory->getLanguage( $subpage )->getHtmlCode();
234            }
235
236            $this->outputPage->addHeadItem(
237                $language, Html::element(
238                'link',
239                    [
240                        'rel' => 'alternate',
241                        'href' => WikiSEO::protocolizeUrl(
242                            $title->getFullURL(),
243                            $this->outputPage->getRequest()
244                        ),
245                        'hreflang' => $language,
246                    ]
247                )
248            );
249        }
250
251        foreach ( $this->metadata as $metaKey => $url ) {
252            if ( strpos( $metaKey, 'hreflang' ) === false ) {
253                continue;
254            }
255
256            $this->outputPage->addHeadItem(
257                $metaKey, Html::element(
258                    'link', [
259                    'rel' => 'alternate',
260                    'href' => $url,
261                    'hreflang' => substr( $metaKey, 9 ),
262                    ]
263                )
264            );
265        }
266    }
267
268    /**
269     * Sets the robot policy to noindex
270     */
271    private function addNoIndex(): void {
272        if ( !empty( $this->metadata['robots'] ) ) {
273            // We assume the following order: index policy, follow policy
274            $parts = array_map( 'trim', explode( ',', $this->metadata['robots'] ) );
275
276            $this->outputPage->setIndexPolicy( $parts[0] ?? '' );
277            $this->outputPage->setFollowPolicy( $parts[1] ?? '' );
278        } elseif ( $this->shouldAddNoIndex() ) {
279            $this->outputPage->setIndexPolicy( 'noindex' );
280        }
281    }
282
283    /**
284     * Check a blacklist of URL parameters and values to see if we should add a noindex meta tag
285     * Based on https://gitlab.com/hydrawiki/extensions/seo/blob/master/SEOHooks.php#L84
286     *
287     * Special Pages are 'noindex,nofollow' by default
288     * @see \Article::getRobotPolicy
289     *
290     * @return bool
291     */
292    private function shouldAddNoIndex(): bool {
293        $blockedURLParamKeys = [
294            'curid', 'diff', 'from', 'group', 'mobileaction', 'oldid',
295            'printable', 'profile', 'redirect', 'redlink', 'stableid'
296        ];
297
298        $blockedURLParamKeyValuePairs = [
299            'action' => [
300                'delete', 'edit', 'history', 'info',
301                'pagevalues', 'purge', 'visualeditor', 'watch'
302            ],
303            'feed' => [ 'rss' ],
304            'limit' => [ '500' ],
305            'title' => $this->getConfigValue( 'WikiSeoNoindexPageTitles' ) ?? [],
306            'veaction' => [
307                'edit', 'editsource'
308            ]
309        ];
310
311        if ( $this->outputPage->getTitle() === null ) {
312            // Bail out
313            return false;
314        }
315
316        if ( in_array( $this->outputPage->getTitle()->getText(), $blockedURLParamKeyValuePairs['title'], true ) ) {
317            return true;
318        }
319
320        foreach ( $this->outputPage->getRequest()->getValues() as $key => $value ) {
321            if ( in_array( $key, $blockedURLParamKeys, true ) ) {
322                return true;
323            }
324
325            if ( isset( $blockedURLParamKeyValuePairs[$key] ) && in_array(
326                    $value,
327                    $blockedURLParamKeyValuePairs[$key],
328                    true
329                ) ) {
330                return true;
331            }
332        }
333
334        return false;
335    }
336}