Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 12
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isDisambiguationPage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isDiffPage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isReadMoreAllowedOnSkin
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 hasRelatedArticles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
56
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onFuncRelated
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onOutputPageParserOutput
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinAfterContent
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace RelatedArticles;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Config\ConfigFactory;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Extension\Disambiguator\Lookup;
9use MediaWiki\Hook\ParserFirstCallInitHook;
10use MediaWiki\Hook\SkinAfterContentHook;
11use MediaWiki\Html\Html;
12use MediaWiki\Output\Hook\BeforePageDisplayHook;
13use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook;
14use MediaWiki\Output\Hook\OutputPageParserOutputHook;
15use MediaWiki\Output\OutputPage;
16use MediaWiki\Parser\Parser;
17use MediaWiki\Parser\ParserOutput;
18use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
19use MediaWiki\Title\Title;
20use Skin;
21
22class Hooks implements
23    ParserFirstCallInitHook,
24    OutputPageParserOutputHook,
25    MakeGlobalVariablesScriptHook,
26    BeforePageDisplayHook,
27    ResourceLoaderGetConfigVarsHook,
28    SkinAfterContentHook
29{
30
31    private Config $relatedArticlesConfig;
32
33    /** Either a Lookup from the Disambiguator extension, or null if that is not installed */
34    private ?Lookup $disambiguatorLookup;
35
36    public function __construct( ConfigFactory $configFactory, ?Lookup $disambiguatorLookup ) {
37        $this->relatedArticlesConfig = $configFactory->makeConfig( 'RelatedArticles' );
38        $this->disambiguatorLookup = $disambiguatorLookup;
39    }
40
41    /**
42     * Handler for the <code>MakeGlobalVariablesScript</code> hook.
43     *
44     * Sets the value of the <code>wgRelatedArticles</code> global variable
45     * to the list of related articles in the cached parser output.
46     *
47     * @param array &$vars variables to be added into the output of OutputPage::headElement.
48     * @param OutputPage $out OutputPage instance calling the hook
49     */
50    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
51        $editorCuratedPages = $out->getProperty( 'RelatedArticles' );
52        if ( $editorCuratedPages ) {
53            $vars['wgRelatedArticles'] = $editorCuratedPages;
54        }
55    }
56
57    /**
58     * Uses the Disambiguator extension to test whether the page is a disambiguation page.
59     *
60     * If the Disambiguator extension isn't installed, then the test always fails, i.e. the page is
61     * never a disambiguation page.
62     *
63     * @param Title $title
64     * @return bool
65     */
66    private function isDisambiguationPage( Title $title ) {
67        return $this->disambiguatorLookup &&
68            $this->disambiguatorLookup->isDisambiguationPage( $title );
69    }
70
71    /**
72     * Check whether the output page is a diff page
73     *
74     * @param IContextSource $context
75     * @return bool
76     */
77    private static function isDiffPage( IContextSource $context ) {
78        $request = $context->getRequest();
79        $type = $request->getRawVal( 'type' );
80        $diff = $request->getCheck( 'diff' );
81        $oldId = $request->getCheck( 'oldid' );
82
83        return $type === 'revision' || $diff || $oldId;
84    }
85
86    /**
87     * Is ReadMore allowed on skin?
88     *
89     * Some wikis may want to only enable the feature on some skins, so we'll only
90     * show it if the allow list (`RelatedArticlesFooterAllowedSkins`
91     * configuration variable) is empty or the skin is listed.
92     *
93     * @param Skin $skin
94     * @return bool
95     */
96    private function isReadMoreAllowedOnSkin( Skin $skin ) {
97        $skins = $this->relatedArticlesConfig->get( 'RelatedArticlesFooterAllowedSkins' );
98        $skinName = $skin->getSkinName();
99        return !$skins || in_array( $skinName, $skins );
100    }
101
102    /**
103     * Can the page show related articles?
104     *
105     * @param Skin $skin
106     * @return bool
107     */
108    private function hasRelatedArticles( Skin $skin ): bool {
109        $title = $skin->getTitle();
110        $action = $skin->getRequest()->getRawVal( 'action', 'view' );
111        return $title->inNamespace( NS_MAIN ) &&
112            // T120735
113            $action === 'view' &&
114            !$title->isMainPage() &&
115            $title->exists() &&
116            !self::isDiffPage( $skin ) &&
117            !$this->isDisambiguationPage( $title ) &&
118            $this->isReadMoreAllowedOnSkin( $skin );
119    }
120
121    /**
122     * Handler for the <code>BeforePageDisplay</code> hook.
123     *
124     * Adds the <code>ext.relatedArticles.readMore.bootstrap</code> module
125     * to the output when:
126     *
127     * <ol>
128     *   <li>On mobile, the output is being rendered with
129     *     <code>SkinMinervaBeta<code></li>
130     *   <li>The page is in mainspace</li>
131     *   <li>The action is 'view'</li>
132     *   <li>The page is not the Main Page</li>
133     *   <li>The page is not a disambiguation page</li>
134     *   <li>The page is not a diff page</li>
135     *   <li>The feature is allowed on the skin (see isReadMoreAllowedOnSkin() above)</li>
136     * </ol>
137     *
138     * @param OutputPage $out The OutputPage object
139     * @param Skin $skin Skin object that will be used to generate the page
140     */
141    public function onBeforePageDisplay( $out, $skin ): void {
142        if ( $this->hasRelatedArticles( $skin ) ) {
143            $out->addModules( [ 'ext.relatedArticles.readMore.bootstrap' ] );
144            $out->addModuleStyles( [ 'ext.relatedArticles.styles' ] );
145        }
146    }
147
148    /**
149     * ResourceLoaderGetConfigVars hook handler for setting a config variable
150     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderGetConfigVars
151     *
152     * @param array &$vars Array of variables to be added into the output of the startup module.
153     * @param string $skin
154     * @param Config $config
155     */
156    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
157        $limit = $this->relatedArticlesConfig->get( 'RelatedArticlesCardLimit' );
158        $vars['wgRelatedArticlesCardLimit'] = $limit;
159        if ( $limit < 1 || $limit > 20 ) {
160            throw new \RuntimeException(
161                'The value of wgRelatedArticlesCardLimit is not valid. It should be between 1 and 20.'
162            );
163        }
164    }
165
166    /**
167     * Handler for the <code>ParserFirstCallInit</code> hook.
168     *
169     * Registers the <code>related</code> parser function (see
170     * {@see Hooks::onFuncRelated}).
171     *
172     * @param Parser $parser Parser object
173     */
174    public function onParserFirstCallInit( $parser ) {
175        $parser->setFunctionHook( 'related', [ self::class, 'onFuncRelated' ] );
176    }
177
178    /**
179     * The <code>related</code> parser function.
180     *
181     * Appends the arguments to the internal list so that it can be used
182     * more that once per page.
183     * We don't use setProperty here is there is no need
184     * to store it as a page prop in the database, only in the cache.
185     *
186     * @todo Test for uniqueness
187     * @param Parser $parser Parser object
188     * @param string ...$args
189     *
190     * @return string Always <code>''</code>
191     */
192    public static function onFuncRelated( Parser $parser, ...$args ) {
193        $parserOutput = $parser->getOutput();
194        $relatedPages = $parserOutput->getExtensionData( 'RelatedArticles' );
195        if ( !$relatedPages ) {
196            $relatedPages = [];
197        }
198
199        // Add all the related pages passed by the parser function
200        // {{#related:Test with read more|Foo|Bar}}
201        foreach ( $args as $relatedPage ) {
202            $relatedPages[] = $relatedPage;
203        }
204        $parserOutput->setExtensionData( 'RelatedArticles', $relatedPages );
205
206        return '';
207    }
208
209    /**
210     * Passes the related pages list from the cached parser output
211     * object to the output page for rendering.
212     *
213     * The list of related pages will be retrieved using
214     * <code>ParserOutput#getExtensionData</code>.
215     *
216     * @param OutputPage $out the OutputPage object
217     * @param ParserOutput $parserOutput ParserOutput object
218     */
219    public function onOutputPageParserOutput( $out, $parserOutput ): void {
220        $related = $parserOutput->getExtensionData( 'RelatedArticles' );
221
222        if ( $related ) {
223            $out->setProperty( 'RelatedArticles', $related );
224        }
225    }
226
227    /**
228     * Create container for ReadMore cards so that they're correctly placed in all skins.
229     *
230     * @param string &$data
231     * @param Skin $skin
232     */
233    public function onSkinAfterContent( &$data, $skin ) {
234        if ( $this->hasRelatedArticles( $skin ) ) {
235            $data .= Html::element( 'div', [ 'class' => 'read-more-container' ] );
236        }
237    }
238}