6 * @author: Jean-Lou Dupont (http://www.bluecortex.com)
8 * Purpose: Provides a toolkit for easier Mediawiki
9 * extension development.
12 * - 'singleton' implementation suited for extensions that require single instance
13 * - 'magic word' helper functionality
14 * - limited pollution of global namespace
16 * Tested Compatibility: MW 1.8.2 (PHP5), 1.9.3, 1.10
19 * v1.0 Initial availability
20 * v1.01 Small enhancement in processArgList
21 * v1.02 Corrected minor bug
22 * v1.1 Added function 'checkPageEditRestriction'
23 * v1.2 Added 'getArticle' function
24 * ---- Moved to SVN management
25 * v1.3 Added wgExtensionCredits updating upon Special:Version viewing
26 * v1.4 Fixed broken singleton functionality
27 * v1.5 Added automatic registration of hook functions based
28 * on the definition of an handler in the derived class
29 * (e.g. if handler 'hArticleSave' exists, then the appropriate
30 * 'ArticleSave' hook is registered)
31 * v1.51 Fixed '$passingStyle' bug (thanks to Joshua C. Lerner)
32 * v1.6 Added 'updateCreditsDescription' helper method.
33 * v1.7 Added 'depth' parameter support: more than 1 class depth can be created.
34 * Added 'setupTags' method (support for parser tags)
35 * Enhancement to 'getParam' method
36 * Added 'formatParams' method
37 * v1.8 Added 'initFirst' parameter
38 * v1.9 Added support for including 'head' scripts and stylesheeets
39 * in a manner compatible with parser caching functionality.
40 * (Original idea from [user:Jimbojw]
41 * v1.91 Added check for screening script duplicates in 'addHeadScript'
42 * v1.92 Added optional removal of parameters not listed in template.
45 $wgExtensionCredits['other'][] = array(
46 'name' => 'ExtensionClass',
47 'version' => 'v1.92 $LastChangedRevision: 174 $',
48 'author' => 'Jean-Lou Dupont',
49 'url' => 'http://www.bluecortex.com',
54 static $gObj; // singleton instance
56 // List up-to-date with MW 1.10 SVN 21828
57 static $hookList = array(
58 'ArticlePageDataBefore',
59 'ArticlePageDataAfter',
60 'ArticleAfterFetchContent',
61 'ArticleViewRedirect',
65 'ArticleInsertComplete',
66 'ArticleSaveComplete',
68 'MarkPatrolledComplete',
70 'WatchArticleComplete',
72 'UnwatchArticleComplete',
74 'ArticleProtectComplete',
76 'ArticleDeleteComplete',
77 'ArticleEditUpdatesDeleteFromRecentchanges',
78 'ArticleEditUpdateNewTalk',
85 'EditFormPreloadText',
86 'EditPage::attemptSave',
88 'EditPage::showEditForm:initial',
89 'EditPage::showEditForm:fields',
94 'MagicWordMagicWords',
95 'MagicWordwgVariableIDs',
99 'OutputPageParserOutput',
100 'OutputPageBeforeHTML',
102 'PageHistoryBeforeList',
103 'PageHistoryLineEnding',
113 'ParserBeforeInternalParse',
114 'InternalParseBeforeLinks',
115 'ParserGetVariableValueVarCache',
116 'ParserGetVariableValueTs',
117 'ParserGetVariableValueSwitch',
120 'RawPageViewBeforeOutput',
130 'SkinTemplateOutputPageBeforeExec',
132 'SkinTemplatePreventOtherActiveTabs',
134 'SkinTemplateBuildContentActionUrlsAfterSpecialPage',
135 'SkinTemplateContentActions',
136 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink',
137 'SkinTemplateSetupPageCss',
141 'SpecialContributionsBeforeMainOutput',
144 'SpecialMovepageAfterMove',
145 'SpecialMovepageAfterMove',
146 'SpecialPage_initList',
147 'SpecialPageExecuteBeforeHeader',
148 'SpecialPageExecuteBeforePage',
149 'SpecialPageExecuteAfterPage',
150 'PreferencesUserInformationPanel',
151 'SpecialSearchNogomatch',
153 'UndeleteShowRevision',
154 'UploadForm:BeforeProcessing',
155 'UploadVerification',
157 'UploadForm:initial',
164 'UserLogoutComplete',
166 /*'SpecialVersionExtensionTypes',*/ // reserved special treatment
178 'UserRetrieveNewTalks',
179 'UserClearNewTalkNotification',
185 /*'LanguageGetMagic', */ // reserved a special treatment in this class
186 'LangugeGetSpecialPageAliases',
187 'MonoBookTemplateToolboxEnd',
188 'SkinTemplateSetupPageCss',
189 'SkinTemplatePreventOtherActiveTabs'
194 var $paramPassingStyle;
197 // Parameter passing style.
201 public static function &singleton( $mwlist=null ,$globalObjName=null,
202 $passingStyle = self::mw_style, $depth = 1,
205 // Let's first extract the callee's classname
206 $trace = debug_backtrace();
207 $cname = $trace[$depth]['class'];
209 // If no globalObjName was given, create a unique one.
210 if ($globalObjName === null)
211 $globalObjName = substr(create_function('',''), 1 );
213 // Since there can only be one extension with a given child class name,
214 // Let's store the $globalObjName in a static array.
215 if (!isset(self::$gObj[$cname]) )
216 self::$gObj[$cname] = $globalObjName;
218 if ( !isset( $GLOBALS[self::$gObj[$cname]] ) )
219 $GLOBALS[self::$gObj[$cname]] = new $cname( $mwlist, $passingStyle, $depth, $initFirst );
221 return $GLOBALS[self::$gObj[$cname]];
223 public function ExtensionClass( $mgwords=null, $passingStyle = self::mw_style,
224 $depth = 1, $initFirst = false )
226 * $mgwords: array of 'magic words' to subscribe to *if* required.
231 if ($passingStyle == null) $passingStyle = self::mw_style; // prevention...
232 $this->paramPassingStyle = $passingStyle;
234 // Let's first extract the callee's classname
235 $trace = debug_backtrace();
236 $this->className= $cname = $trace[$depth]['class'];
237 // And let's retrieve the global object's name
238 $n = self::$gObj[$cname];
240 global $wgExtensionFunctions;
243 $initFnc = create_function('',"global $".$n."; $".$n."->setup();");
245 array_unshift( $wgExtensionFunctions, $initFnc );
246 else $wgExtensionFunctions[] = $initFnc;
248 $this->ext_mgwords = $mgwords;
249 if (is_array($this->ext_mgwords) )
250 $wgHooks['LanguageGetMagic'][] = array($this, 'getMagic');
253 if ( in_array( 'hUpdateExtensionCredits', get_class_methods($this->className) ) )
254 $wgHooks['SpecialVersionExtensionTypes'][] = array( &$this, 'hUpdateExtensionCredits' );
257 foreach (self::$hookList as $index => $hookName)
258 if ( in_array( 'h'.$hookName, get_class_methods($this->className) ) )
259 $wgHooks[$hookName][] = array( &$this, 'h'.$hookName );
261 public function getParamPassingStyle() { return $this->passingStyle; }
262 public function setup()
264 if (is_array($this->ext_mgwords))
267 // ================== MAGIC WORD HELPER FUNCTIONS ===========================
268 public function getMagic( &$magicwords, $langCode )
270 foreach($this->ext_mgwords as $index => $key)
271 $magicwords [$key] = array( 0, $key );
274 public function setupMagic( )
277 foreach($this->ext_mgwords as $index => $key)
278 $wgParser->setFunctionHook( "$key", array( $this, "mg_$key" ) );
280 public function setupTags( $tagList )
283 foreach($tagList as $index => $key)
284 $wgParser->setHook( "$key", array( $this, "tag_$key" ) );
286 // ================== GENERAL PURPOSE HELPER FUNCTIONS ===========================
287 public function processArgList( $list, $getridoffirstparam=false )
289 * The resulting list contains:
290 * - The parameters extracted by 'key=value' whereby (key => value) entries in the list
291 * - The parameters extracted by 'index' whereby ( index = > value) entries in the list
294 if ($getridoffirstparam)
295 array_shift( $list );
297 // the parser sometimes includes a boggie
298 // null parameter. get rid of it.
299 if (count($list) >0 )
300 if (empty( $list[count($list)-1] ))
301 unset( $list[count($list)-1] );
304 foreach ($list as $index => $el )
306 $t = explode("=", $el);
309 $result[ "{$t[0]}" ] = $t[1];
310 unset( $list[$index] );
314 return array_merge( $result, $list );
316 public function getParam( &$alist, $key, $index, $default )
318 * Gets a parameter by 'key' if present
319 * or fallback on getting the value by 'index' and
320 * ultimately fallback on default if both previous attempts fail.
323 if (array_key_exists($key, $alist) )
325 elseif (array_key_exists($index, $alist) && $index!==null )
326 return $alist[$index];
330 public function initParams( &$alist, &$templateElements, $removeNotInTemplate = true )
334 if ($removeNotInTemplate)
335 foreach( $templateElements as $index => &$el )
336 if ( !isset($alist[ $el['key'] ]) )
337 unset( $alist[$el['key']] );
339 foreach( $templateElements as $index => &$el )
340 $alist[$el['key']] = $this->getParam( $alist, $el['key'], $el['index'], $el['default'] );
342 public function formatParams( &$alist , &$template )
343 // look at yuiPanel extension for usage example.
344 // $alist = { 'key' => 'value' ... }
346 foreach ( $alist as $key => $value )
348 $this->formatParam( $key, $value, $template );
350 private function formatParam( &$key, &$value, &$template )
352 $format = $this->getFormat( $key, $template );
353 if ($format !==null )
357 case 'bool': $value = (bool) $value; break;
358 case 'int': $value = (int) $value; break;
360 case 'string': $value = (string) $value; break;
364 public function getFormat( &$key, &$template )
367 foreach( $template as $index => &$el )
368 if ( $el['key'] == $key )
369 $format = $el['format'];
373 public function checkPageEditRestriction( &$title )
375 // where $title is a Mediawiki Title class object instance
379 $state = $title->getRestrictions('edit');
380 foreach ($state as $index => $group )
381 if ( $group == 'sysop' )
386 public function getArticle( $article_title )
388 $title = Title::newFromText( $article_title );
390 // Can't load page if title is invalid.
391 if ($title == null) return null;
392 $article = new Article($title);
397 function isSysop( $user = null ) // v1.5 feature
404 return in_array( 'sysop', $user->getGroups() );
407 function updateCreditsDescription( &$text ) // v1.6 feature.
409 global $wgExtensionCredits;
411 foreach ( $wgExtensionCredits[self::thisType] as $index => &$el )
412 if ($el['name']==self::thisName)
413 $el['description'].=$text;
416 /* Add scripts & stylesheets functionality.
417 This process must be done in two phases:
418 phase 1- encode information related to the required
419 scripts & stylesheets in a 'meta form' in
420 the parser cache text.
421 phase 2- when the page is rendered, extract the meta information
422 and include the information appropriately in the 'head' of the page.
423 ************************************************************************************/
425 static $scriptsAdded;
426 static $scriptsListed;
428 function addHeadScript( $st )
430 if ( !isset($st) ) return;
432 if ( !isset(self::$scriptList) )
433 self::$scriptList[] = $st;
434 elseif (!in_array($st, self::$scriptList))
435 self::$scriptList[] = $st;
437 self::$scriptsAdded = false;
438 self::$scriptsListed = false;
441 function hParserAfterTidy( &$parser, &$text )
442 // set the meta information in the parsed 'wikitext'.
444 if (self::$scriptsListed) return true;
445 self::$scriptsListed = true;
447 if (!empty(self::$scriptList))
448 foreach(self::$scriptList as $sc)
449 $text .= '<!-- META_KEYWORDS '.base64_encode($sc).' -->';
453 function hOutputPageBeforeHTML( &$op, &$text )
454 // This function sifts through 'meta tags' embedded in html comments
455 // and picks out scripts & stylesheet references that need to be put
456 // in the page's HEAD.
458 // some hooks get called more than once...
459 // In this case, since ExtensionClass provides a
460 // base class for numerous extensions, then it is very
461 // likely this method will be called more than once;
462 // so, we want to make sure we include the head scripts just once.
463 if (self::$scriptsAdded) return true;
464 self::$scriptsAdded = true;
467 '/<!-- META_KEYWORDS ([0-9a-zA-Z\\+\\/]+=*) -->/m',
469 $matches)===false) return true;
473 foreach ($data AS $item)
475 $content = @base64_decode($item);
476 if ($content) $op->addScript( $content );
481 } // end class definition.