1<?php
2
3/**
4 * Copyright © 2007 Daniel Kinzler
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 */
23use Wikimedia\Rdbms\IDatabase;
24use Wikimedia\WrappedString;
25
26class GadgetHooks {
27	/**
28	 * PageSaveComplete hook handler
29	 *
30	 * Only run in versions of mediawiki begining 1.35; before 1.35, ::onPageContentSaveComplete
31	 * and ::onPageContentInsertComplete are used
32	 *
33	 * @note parameters include classes not available before 1.35, so for those typehints
34	 * are not used. The variable name reflects the class
35	 *
36	 * @param WikiPage $wikiPage
37	 * @param mixed $userIdentity unused
38	 * @param string $summary
39	 * @param int $flags
40	 * @param mixed $revisionRecord unused
41	 * @param mixed $editResult unused
42	 */
43	public static function onPageSaveComplete(
44		WikiPage $wikiPage,
45		$userIdentity,
46		string $summary,
47		int $flags,
48		$revisionRecord,
49		$editResult
50	) {
51		$title = $wikiPage->getTitle();
52		$repo = GadgetRepo::singleton();
53
54		if ( $flags & EDIT_NEW ) {
55			if ( $title->inNamespace( NS_GADGET_DEFINITION ) ) {
56				$repo->handlePageCreation( $title );
57			}
58		}
59
60		$repo->handlePageUpdate( $title );
61	}
62
63	/**
64	 * UserGetDefaultOptions hook handler
65	 * @param array &$defaultOptions Array of default preference keys and values
66	 */
67	public static function userGetDefaultOptions( array &$defaultOptions ) {
68		$gadgets = GadgetRepo::singleton()->getStructuredList();
69		if ( !$gadgets ) {
70			return;
71		}
72
73		/**
74		 * @var $gadget Gadget
75		 */
76		foreach ( $gadgets as $thisSection ) {
77			foreach ( $thisSection as $gadgetId => $gadget ) {
78				if ( $gadget->isOnByDefault() ) {
79					$defaultOptions['gadget-' . $gadgetId] = 1;
80				}
81			}
82		}
83	}
84
85	/**
86	 * GetPreferences hook handler.
87	 * @param User $user
88	 * @param array &$preferences Preference descriptions
89	 */
90	public static function getPreferences( User $user, array &$preferences ) {
91		$gadgets = GadgetRepo::singleton()->getStructuredList();
92		if ( !$gadgets ) {
93			return;
94		}
95
96		$options = [];
97		$default = [];
98		$skin = RequestContext::getMain()->getSkin();
99		foreach ( $gadgets as $section => $thisSection ) {
100			$available = [];
101
102			/**
103			 * @var $gadget Gadget
104			 */
105			foreach ( $thisSection as $gadget ) {
106				if (
107					!$gadget->isHidden()
108					&& $gadget->isAllowed( $user )
109					&& $gadget->isSkinSupported( $skin )
110				) {
111					$gname = $gadget->getName();
112					$available[$gadget->getDescriptionMessageKey()] = $gname;
113					if ( $gadget->isEnabled( $user ) ) {
114						$default[] = $gname;
115					}
116				}
117			}
118
119			if ( $available === [] ) {
120				continue;
121			}
122
123			if ( $section !== '' ) {
124				$options["gadget-section-$section"] = $available;
125			} else {
126				$options = array_merge( $options, $available );
127			}
128		}
129
130		$preferences['gadgets-intro'] =
131			[
132				'type' => 'info',
133				'default' => wfMessage( 'gadgets-prefstext' )->parseAsBlock(),
134				'section' => 'gadgets',
135				'raw' => true,
136			];
137
138		$preferences['gadgets'] =
139			[
140				'type' => 'multiselect',
141				'options-messages' => $options,
142				'options-messages-parse' => true,
143				'section' => 'gadgets',
144				'label' => '&#160;',
145				'prefix' => 'gadget-',
146				'default' => $default,
147				'noglobal' => true,
148			];
149	}
150
151	/**
152	 * ResourceLoaderRegisterModules hook handler.
153	 * @param ResourceLoader &$resourceLoader
154	 */
155	public static function registerModules( ResourceLoader &$resourceLoader ) {
156		$repo = GadgetRepo::singleton();
157		$ids = $repo->getGadgetIds();
158
159		foreach ( $ids as $id ) {
160			$resourceLoader->register( Gadget::getModuleName( $id ), [
161				'class' => GadgetResourceLoaderModule::class,
162				'id' => $id,
163			] );
164		}
165	}
166
167	/**
168	 * BeforePageDisplay hook handler.
169	 * @param OutputPage $out
170	 */
171	public static function beforePageDisplay( OutputPage $out ) {
172		$repo = GadgetRepo::singleton();
173		$ids = $repo->getGadgetIds();
174		if ( !$ids ) {
175			return;
176		}
177
178		$lb = new LinkBatch();
179		$lb->setCaller( __METHOD__ );
180		$enabledLegacyGadgets = [];
181
182		/**
183		 * @var $gadget Gadget
184		 */
185		$user = $out->getUser();
186		foreach ( $ids as $id ) {
187			try {
188				$gadget = $repo->getGadget( $id );
189			} catch ( InvalidArgumentException $e ) {
190				continue;
191			}
192			$peers = [];
193			foreach ( $gadget->getPeers() as $peerName ) {
194				try {
195					$peers[] = $repo->getGadget( $peerName );
196				} catch ( InvalidArgumentException $e ) {
197					// Ignore
198					// @todo: Emit warning for invalid peer?
199				}
200			}
201			if ( $gadget->isEnabled( $user )
202				&& $gadget->isAllowed( $user )
203				&& $gadget->isSkinSupported( $out->getSkin() )
204				&& ( in_array( $out->getTarget() ?? 'desktop', $gadget->getTargets() ) )
205			) {
206				if ( $gadget->hasModule() ) {
207					if ( $gadget->getType() === 'styles' ) {
208						$out->addModuleStyles( Gadget::getModuleName( $gadget->getName() ) );
209					} else {
210						$out->addModules( Gadget::getModuleName( $gadget->getName() ) );
211						// Load peer modules
212						foreach ( $peers as $peer ) {
213							if ( $peer->getType() === 'styles' ) {
214								$out->addModuleStyles( Gadget::getModuleName( $peer->getName() ) );
215							}
216							// Else, if not type=styles: Use dependencies instead.
217							// Note: No need for recursion as styles modules don't support
218							// either of 'dependencies' and 'peers'.
219						}
220					}
221				}
222
223				if ( $gadget->getLegacyScripts() ) {
224					$enabledLegacyGadgets[] = $id;
225				}
226			}
227		}
228
229		$strings = [];
230		foreach ( $enabledLegacyGadgets as $id ) {
231			$strings[] = self::makeLegacyWarning( $id );
232		}
233		$out->addHTML( WrappedString::join( "\n", $strings ) );
234	}
235
236	/**
237	 * @param string $id
238	 * @return string|WrappedString HTML
239	 */
240	private static function makeLegacyWarning( $id ) {
241		$special = SpecialPage::getTitleFor( 'Gadgets' );
242
243		return ResourceLoader::makeInlineScript(
244			Xml::encodeJsCall( 'mw.log.warn', [
245				"Gadget \"$id\" was not loaded. Please migrate it to use ResourceLoader. " .
246				'See <' . $special->getCanonicalURL() . '>.'
247			] )
248		);
249	}
250
251	/**
252	 * Valid gadget definition page after content is modified
253	 *
254	 * @param IContextSource $context
255	 * @param Content $content
256	 * @param Status $status
257	 * @param string $summary
258	 * @throws Exception
259	 * @return bool
260	 */
261	public static function onEditFilterMergedContent( IContextSource $context,
262		Content $content,
263		Status $status,
264		$summary
265	) {
266		$title = $context->getTitle();
267
268		if ( !$title->inNamespace( NS_GADGET_DEFINITION ) ) {
269			return true;
270		}
271
272		if ( !$content instanceof GadgetDefinitionContent ) {
273			// This should not be possible?
274			throw new Exception(
275				"Tried to save non-GadgetDefinitionContent to {$title->getPrefixedText()}"
276			);
277		}
278
279		$validateStatus = $content->validate();
280		if ( !$validateStatus->isGood() ) {
281			$status->merge( $validateStatus );
282			// @todo Remove this line after this extension do not support mediawiki version 1.36 and before
283			$status->value = EditPage::AS_HOOK_ERROR_EXPECTED;
284			return false;
285		}
286
287		return true;
288	}
289
290	/**
291	 * Mark the Title as having a content model of javascript or css for pages
292	 * in the Gadget namespace based on their file extension
293	 *
294	 * @param Title $title
295	 * @param string &$model
296	 * @return bool
297	 */
298	public static function onContentHandlerDefaultModelFor( Title $title, &$model ) {
299		if ( $title->inNamespace( NS_GADGET ) ) {
300			preg_match( '!\.(css|js)$!u', $title->getText(), $ext );
301			$ext = $ext[1] ?? '';
302			switch ( $ext ) {
303				case 'js':
304					$model = 'javascript';
305					return false;
306				case 'css':
307					$model = 'css';
308					return false;
309			}
310		}
311
312		return true;
313	}
314
315	/**
316	 * Set the CodeEditor language for Gadget definition pages. It already
317	 * knows the language for Gadget: namespace pages.
318	 *
319	 * @param Title $title
320	 * @param string &$lang
321	 * @return bool
322	 */
323	public static function onCodeEditorGetPageLanguage( Title $title, &$lang ) {
324		if ( $title->hasContentModel( 'GadgetDefinition' ) ) {
325			$lang = 'json';
326			return false;
327		}
328
329		return true;
330	}
331
332	/**
333	 * Add the GadgetUsage special page to the list of QueryPages.
334	 * @param array &$queryPages
335	 */
336	public static function onwgQueryPages( array &$queryPages ) {
337		$queryPages[] = [ 'SpecialGadgetUsage', 'GadgetUsage' ];
338	}
339
340	/**
341	 * Prevent gadget preferences from being deleted.
342	 * @link https://www.mediawiki.org/wiki/Manual:Hooks/DeleteUnknownPreferences
343	 * @param string[] &$where Array of where clause conditions to add to.
344	 * @param IDatabase $db
345	 */
346	public static function onDeleteUnknownPreferences( array &$where, IDatabase $db ) {
347		$where[] = 'up_property NOT' . $db->buildLike( 'gadget-', $db->anyString() );
348	}
349}
350