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' => ' ', 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