Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 47 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 47 |
|
0.00% |
0 / 12 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onMakeGlobalVariablesScript | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isDisambiguationPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isDiffPage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
isReadMoreAllowedOnSkin | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
hasRelatedArticles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
56 | |||
onBeforePageDisplay | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onResourceLoaderGetConfigVars | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onParserFirstCallInit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onFuncRelated | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onOutputPageParserOutput | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onSkinAfterContent | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace RelatedArticles; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use MediaWiki\Config\ConfigFactory; |
7 | use MediaWiki\Context\IContextSource; |
8 | use MediaWiki\Extension\Disambiguator\Lookup; |
9 | use MediaWiki\Hook\ParserFirstCallInitHook; |
10 | use MediaWiki\Hook\SkinAfterContentHook; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
13 | use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook; |
14 | use MediaWiki\Output\Hook\OutputPageParserOutputHook; |
15 | use MediaWiki\Output\OutputPage; |
16 | use MediaWiki\Parser\Parser; |
17 | use MediaWiki\Parser\ParserOutput; |
18 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook; |
19 | use MediaWiki\Title\Title; |
20 | use Skin; |
21 | |
22 | class 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 | } |