MediaWiki REL1_32
WebInstallerOptions.php
Go to the documentation of this file.
1<?php
23
27 public function execute() {
28 global $wgLang;
29
30 if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
31 $this->submitSkins();
32 return 'skip';
33 }
34 if ( $this->parent->request->wasPosted() ) {
35 if ( $this->submit() ) {
36 return 'continue';
37 }
38 }
39
40 $emailwrapperStyle = $this->getVar( 'wgEnableEmail' ) ? '' : 'display: none';
41 $this->startForm();
42 $this->addHTML(
43 # User Rights
44 // getRadioSet() builds a set of labeled radio buttons.
45 // For grep: The following messages are used as the item labels:
46 // config-profile-wiki, config-profile-no-anon, config-profile-fishbowl, config-profile-private
47 $this->parent->getRadioSet( [
48 'var' => '_RightsProfile',
49 'label' => 'config-profile',
50 'itemLabelPrefix' => 'config-profile-',
51 'values' => array_keys( $this->parent->rightsProfiles ),
52 ] ) .
53 $this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) .
54
55 # Licensing
56 // getRadioSet() builds a set of labeled radio buttons.
57 // For grep: The following messages are used as the item labels:
58 // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
59 // config-license-cc-0, config-license-pd, config-license-gfdl,
60 // config-license-none, config-license-cc-choose
61 $this->parent->getRadioSet( [
62 'var' => '_LicenseCode',
63 'label' => 'config-license',
64 'itemLabelPrefix' => 'config-license-',
65 'values' => array_keys( $this->parent->licenses ),
66 'commonAttribs' => [ 'class' => 'licenseRadio' ],
67 ] ) .
68 $this->getCCChooser() .
69 $this->parent->getHelpBox( 'config-license-help' ) .
70
71 # E-mail
72 $this->getFieldsetStart( 'config-email-settings' ) .
73 $this->parent->getCheckBox( [
74 'var' => 'wgEnableEmail',
75 'label' => 'config-enable-email',
76 'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'emailwrapper' ],
77 ] ) .
78 $this->parent->getHelpBox( 'config-enable-email-help' ) .
79 "<div id=\"emailwrapper\" style=\"$emailwrapperStyle\">" .
80 $this->parent->getTextBox( [
81 'var' => 'wgPasswordSender',
82 'label' => 'config-email-sender'
83 ] ) .
84 $this->parent->getHelpBox( 'config-email-sender-help' ) .
85 $this->parent->getCheckBox( [
86 'var' => 'wgEnableUserEmail',
87 'label' => 'config-email-user',
88 ] ) .
89 $this->parent->getHelpBox( 'config-email-user-help' ) .
90 $this->parent->getCheckBox( [
91 'var' => 'wgEnotifUserTalk',
92 'label' => 'config-email-usertalk',
93 ] ) .
94 $this->parent->getHelpBox( 'config-email-usertalk-help' ) .
95 $this->parent->getCheckBox( [
96 'var' => 'wgEnotifWatchlist',
97 'label' => 'config-email-watchlist',
98 ] ) .
99 $this->parent->getHelpBox( 'config-email-watchlist-help' ) .
100 $this->parent->getCheckBox( [
101 'var' => 'wgEmailAuthentication',
102 'label' => 'config-email-auth',
103 ] ) .
104 $this->parent->getHelpBox( 'config-email-auth-help' ) .
105 "</div>" .
106 $this->getFieldsetEnd()
107 );
108
109 $skins = $this->parent->findExtensions( 'skins' );
110 $skinHtml = $this->getFieldsetStart( 'config-skins' );
111
112 $skinNames = array_map( 'strtolower', array_keys( $skins ) );
113 $chosenSkinName = $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
114
115 if ( $skins ) {
116 $radioButtons = $this->parent->getRadioElements( [
117 'var' => 'wgDefaultSkin',
118 'itemLabels' => array_fill_keys( $skinNames, 'config-skins-use-as-default' ),
119 'values' => $skinNames,
120 'value' => $chosenSkinName,
121 ] );
122
123 foreach ( $skins as $skin => $info ) {
124 if ( isset( $info['screenshots'] ) ) {
125 $screenshotText = $this->makeScreenshotsLink( $skin, $info['screenshots'] );
126 } else {
127 $screenshotText = htmlspecialchars( $skin );
128 }
129 $skinHtml .=
130 '<div class="config-skins-item">' .
131 $this->parent->getCheckBox( [
132 'var' => "skin-$skin",
133 'rawtext' => $screenshotText,
134 'value' => $this->getVar( "skin-$skin", true ), // all found skins enabled by default
135 ] ) .
136 '<div class="config-skins-use-as-default">' . $radioButtons[strtolower( $skin )] . '</div>' .
137 '</div>';
138 }
139 } else {
140 $skinHtml .=
141 $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) .
142 Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
143 }
144
145 $skinHtml .= $this->parent->getHelpBox( 'config-skins-help' ) .
146 $this->getFieldsetEnd();
147 $this->addHTML( $skinHtml );
148
149 $extensions = $this->parent->findExtensions();
150 $dependencyMap = [];
151
152 if ( $extensions ) {
153 $extHtml = $this->getFieldsetStart( 'config-extensions' );
154
155 $extByType = [];
157 // Sort by type first
158 foreach ( $extensions as $ext => $info ) {
159 if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
160 // We let extensions normally define custom types, but
161 // since we aren't loading extensions, we'll have to
162 // categorize them under other
163 $info['type'] = 'other';
164 }
165 $extByType[$info['type']][$ext] = $info;
166 }
167
168 foreach ( $types as $type => $message ) {
169 if ( !isset( $extByType[$type] ) ) {
170 continue;
171 }
172 $extHtml .= Html::element( 'h2', [], $message );
173 foreach ( $extByType[$type] as $ext => $info ) {
174 $urlText = '';
175 if ( isset( $info['url'] ) ) {
176 $urlText = ' ' . Html::element( 'a', [ 'href' => $info['url'] ], '(more information)' );
177 }
178 $attribs = [
179 'data-name' => $ext,
180 'class' => 'config-ext-input'
181 ];
182 $labelAttribs = [];
183 $fullDepList = [];
184 if ( isset( $info['requires']['extensions'] ) ) {
185 $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
186 $labelAttribs['class'] = 'mw-ext-with-dependencies';
187 }
188 if ( isset( $info['requires']['skins'] ) ) {
189 $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
190 $labelAttribs['class'] = 'mw-ext-with-dependencies';
191 }
192 if ( isset( $dependencyMap[$ext] ) ) {
193 $links = [];
194 // For each dependency, link to the checkbox for each
195 // extension/skin that is required
196 if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
197 foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
198 $links[] = Html::element(
199 'a',
200 [ 'href' => "#config_ext-$name" ],
201 $name
202 );
203 }
204 }
205 if ( isset( $dependencyMap[$ext]['skins'] ) ) {
206 foreach ( $dependencyMap[$ext]['skins'] as $name ) {
207 $links[] = Html::element(
208 'a',
209 [ 'href' => "#config_skin-$name" ],
210 $name
211 );
212 }
213 }
214
215 $text = wfMessage( 'config-extensions-requires' )
216 ->rawParams( $ext, $wgLang->commaList( $links ) )
217 ->escaped();
218 } else {
219 $text = $ext;
220 }
221 $extHtml .= $this->parent->getCheckBox( [
222 'var' => "ext-$ext",
223 'rawtext' => $text,
224 'attribs' => $attribs,
225 'labelAttribs' => $labelAttribs,
226 ] );
227 }
228 }
229
230 $extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
231 $this->getFieldsetEnd();
232 $this->addHTML( $extHtml );
233 // Push the dependency map to the client side
234 $this->addHTML( Html::inlineScript(
235 'var extDependencyMap = ' . Xml::encodeJsVar( $dependencyMap )
236 ) );
237 }
238
239 // Having / in paths in Windows looks funny :)
240 $this->setVar( 'wgDeletedDirectory',
241 str_replace(
242 '/', DIRECTORY_SEPARATOR,
243 $this->getVar( 'wgDeletedDirectory' )
244 )
245 );
246
247 $uploadwrapperStyle = $this->getVar( 'wgEnableUploads' ) ? '' : 'display: none';
248 $this->addHTML(
249 # Uploading
250 $this->getFieldsetStart( 'config-upload-settings' ) .
251 $this->parent->getCheckBox( [
252 'var' => 'wgEnableUploads',
253 'label' => 'config-upload-enable',
254 'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'uploadwrapper' ],
255 'help' => $this->parent->getHelpBox( 'config-upload-help' )
256 ] ) .
257 '<div id="uploadwrapper" style="' . $uploadwrapperStyle . '">' .
258 $this->parent->getTextBox( [
259 'var' => 'wgDeletedDirectory',
260 'label' => 'config-upload-deleted',
261 'attribs' => [ 'dir' => 'ltr' ],
262 'help' => $this->parent->getHelpBox( 'config-upload-deleted-help' )
263 ] ) .
264 '</div>' .
265 $this->parent->getTextBox( [
266 'var' => 'wgLogo',
267 'label' => 'config-logo',
268 'attribs' => [ 'dir' => 'ltr' ],
269 'help' => $this->parent->getHelpBox( 'config-logo-help' )
270 ] )
271 );
272 $this->addHTML(
273 $this->parent->getCheckBox( [
274 'var' => 'wgUseInstantCommons',
275 'label' => 'config-instantcommons',
276 'help' => $this->parent->getHelpBox( 'config-instantcommons-help' )
277 ] ) .
278 $this->getFieldsetEnd()
279 );
280
281 $caches = [ 'none' ];
282 $cachevalDefault = 'none';
283
284 if ( count( $this->getVar( '_Caches' ) ) ) {
285 // A CACHE_ACCEL implementation is available
286 $caches[] = 'accel';
287 $cachevalDefault = 'accel';
288 }
289 $caches[] = 'memcached';
290
291 // We'll hide/show this on demand when the value changes, see config.js.
292 $cacheval = $this->getVar( '_MainCacheType' );
293 if ( !$cacheval ) {
294 // We need to set a default here; but don't hardcode it
295 // or we lose it every time we reload the page for validation
296 // or going back!
297 $cacheval = $cachevalDefault;
298 }
299 $hidden = ( $cacheval == 'memcached' ) ? '' : 'display: none';
300 $this->addHTML(
301 # Advanced settings
302 $this->getFieldsetStart( 'config-advanced-settings' ) .
303 # Object cache settings
304 // getRadioSet() builds a set of labeled radio buttons.
305 // For grep: The following messages are used as the item labels:
306 // config-cache-none, config-cache-accel, config-cache-memcached
307 $this->parent->getRadioSet( [
308 'var' => '_MainCacheType',
309 'label' => 'config-cache-options',
310 'itemLabelPrefix' => 'config-cache-',
311 'values' => $caches,
312 'value' => $cacheval,
313 ] ) .
314 $this->parent->getHelpBox( 'config-cache-help' ) .
315 "<div id=\"config-memcachewrapper\" style=\"$hidden\">" .
316 $this->parent->getTextArea( [
317 'var' => '_MemCachedServers',
318 'label' => 'config-memcached-servers',
319 'help' => $this->parent->getHelpBox( 'config-memcached-help' )
320 ] ) .
321 '</div>' .
322 $this->getFieldsetEnd()
323 );
324 $this->endForm();
325
326 return null;
327 }
328
329 private function makeScreenshotsLink( $name, $screenshots ) {
330 global $wgLang;
331 if ( count( $screenshots ) > 1 ) {
332 $links = [];
333 $counter = 1;
334 foreach ( $screenshots as $shot ) {
335 $links[] = Html::element(
336 'a',
337 [ 'href' => $shot, 'target' => '_blank' ],
338 $wgLang->formatNum( $counter++ )
339 );
340 }
341 return wfMessage( 'config-skins-screenshots' )
342 ->rawParams( $name, $wgLang->commaList( $links ) )
343 ->escaped();
344 } else {
345 $link = Html::element(
346 'a',
347 [ 'href' => $screenshots[0], 'target' => '_blank' ],
348 wfMessage( 'config-screenshot' )->text()
349 );
350 return wfMessage( 'config-skins-screenshot', $name )->rawParams( $link )->escaped();
351 }
352 }
353
357 public function getCCPartnerUrl() {
358 $server = $this->getVar( 'wgServer' );
359 $exitUrl = $server . $this->parent->getUrl( [
360 'page' => 'Options',
361 'SubmitCC' => 'indeed',
362 'config__LicenseCode' => 'cc',
363 'config_wgRightsUrl' => '[license_url]',
364 'config_wgRightsText' => '[license_name]',
365 'config_wgRightsIcon' => '[license_button]',
366 ] );
367 $styleUrl = $server . dirname( dirname( $this->parent->getUrl() ) ) .
368 '/mw-config/config-cc.css';
369 $iframeUrl = '//creativecommons.org/license/?' .
370 wfArrayToCgi( [
371 'partner' => 'MediaWiki',
372 'exit_url' => $exitUrl,
373 'lang' => $this->getVar( '_UserLang' ),
374 'stylesheet' => $styleUrl,
375 ] );
376
377 return $iframeUrl;
378 }
379
383 public function getCCChooser() {
384 $iframeAttribs = [
385 'class' => 'config-cc-iframe',
386 'name' => 'config-cc-iframe',
387 'id' => 'config-cc-iframe',
388 'frameborder' => 0,
389 'width' => '100%',
390 'height' => '100%',
391 ];
392 if ( $this->getVar( '_CCDone' ) ) {
393 $iframeAttribs['src'] = $this->parent->getUrl( [ 'ShowCC' => 'yes' ] );
394 } else {
395 $iframeAttribs['src'] = $this->getCCPartnerUrl();
396 }
397 $wrapperStyle = ( $this->getVar( '_LicenseCode' ) == 'cc-choose' ) ? '' : 'display: none';
398
399 return "<div class=\"config-cc-wrapper\" id=\"config-cc-wrapper\" style=\"$wrapperStyle\">\n" .
400 Html::element( 'iframe', $iframeAttribs, '', false /* not short */ ) .
401 "</div>\n";
402 }
403
407 public function getCCDoneBox() {
408 $js = "parent.document.getElementById('config-cc-wrapper').style.height = '$1';";
409 // If you change this height, also change it in config.css
410 $expandJs = str_replace( '$1', '54em', $js );
411 $reduceJs = str_replace( '$1', '70px', $js );
412
413 return '<p>' .
414 Html::element( 'img', [ 'src' => $this->getVar( 'wgRightsIcon' ) ] ) .
415 "\u{00A0}\u{00A0}" .
416 htmlspecialchars( $this->getVar( 'wgRightsText' ) ) .
417 "</p>\n" .
418 "<p style=\"text-align: center;\">" .
419 Html::element( 'a',
420 [
421 'href' => $this->getCCPartnerUrl(),
422 'onclick' => $expandJs,
423 ],
424 wfMessage( 'config-cc-again' )->text()
425 ) .
426 "</p>\n" .
427 "<script>\n" .
428 # Reduce the wrapper div height
429 htmlspecialchars( $reduceJs ) .
430 "\n" .
431 "</script>\n";
432 }
433
434 public function submitCC() {
435 $newValues = $this->parent->setVarsFromRequest(
436 [ 'wgRightsUrl', 'wgRightsText', 'wgRightsIcon' ] );
437 if ( count( $newValues ) != 3 ) {
438 $this->parent->showError( 'config-cc-error' );
439
440 return;
441 }
442 $this->setVar( '_CCDone', true );
443 $this->addHTML( $this->getCCDoneBox() );
444 }
445
452 public function submitSkins() {
453 $skins = array_keys( $this->parent->findExtensions( 'skins' ) );
454 $this->parent->setVar( '_Skins', $skins );
455
456 if ( $skins ) {
457 $skinNames = array_map( 'strtolower', $skins );
458 $this->parent->setVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
459 }
460
461 return true;
462 }
463
467 public function submit() {
468 $this->parent->setVarsFromRequest( [ '_RightsProfile', '_LicenseCode',
469 'wgEnableEmail', 'wgPasswordSender', 'wgEnableUploads', 'wgLogo',
470 'wgEnableUserEmail', 'wgEnotifUserTalk', 'wgEnotifWatchlist',
471 'wgEmailAuthentication', '_MainCacheType', '_MemCachedServers',
472 'wgUseInstantCommons', 'wgDefaultSkin' ] );
473
474 $retVal = true;
475
476 if ( !array_key_exists( $this->getVar( '_RightsProfile' ), $this->parent->rightsProfiles ) ) {
477 reset( $this->parent->rightsProfiles );
478 $this->setVar( '_RightsProfile', key( $this->parent->rightsProfiles ) );
479 }
480
481 $code = $this->getVar( '_LicenseCode' );
482 if ( $code == 'cc-choose' ) {
483 if ( !$this->getVar( '_CCDone' ) ) {
484 $this->parent->showError( 'config-cc-not-chosen' );
485 $retVal = false;
486 }
487 } elseif ( array_key_exists( $code, $this->parent->licenses ) ) {
488 // Messages:
489 // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
490 // config-license-cc-0, config-license-pd, config-license-gfdl, config-license-none,
491 // config-license-cc-choose
492 $entry = $this->parent->licenses[$code];
493 if ( isset( $entry['text'] ) ) {
494 $this->setVar( 'wgRightsText', $entry['text'] );
495 } else {
496 $this->setVar( 'wgRightsText', wfMessage( 'config-license-' . $code )->text() );
497 }
498 $this->setVar( 'wgRightsUrl', $entry['url'] );
499 $this->setVar( 'wgRightsIcon', $entry['icon'] );
500 } else {
501 $this->setVar( 'wgRightsText', '' );
502 $this->setVar( 'wgRightsUrl', '' );
503 $this->setVar( 'wgRightsIcon', '' );
504 }
505
506 $skinsAvailable = array_keys( $this->parent->findExtensions( 'skins' ) );
507 $skinsToInstall = [];
508 foreach ( $skinsAvailable as $skin ) {
509 $this->parent->setVarsFromRequest( [ "skin-$skin" ] );
510 if ( $this->getVar( "skin-$skin" ) ) {
511 $skinsToInstall[] = $skin;
512 }
513 }
514 $this->parent->setVar( '_Skins', $skinsToInstall );
515
516 if ( !$skinsToInstall && $skinsAvailable ) {
517 $this->parent->showError( 'config-skins-must-enable-some' );
518 $retVal = false;
519 }
520 $defaultSkin = $this->getVar( 'wgDefaultSkin' );
521 $skinsToInstallLowercase = array_map( 'strtolower', $skinsToInstall );
522 if ( $skinsToInstall && array_search( $defaultSkin, $skinsToInstallLowercase ) === false ) {
523 $this->parent->showError( 'config-skins-must-enable-default' );
524 $retVal = false;
525 }
526
527 $extsAvailable = array_keys( $this->parent->findExtensions() );
528 $extsToInstall = [];
529 foreach ( $extsAvailable as $ext ) {
530 $this->parent->setVarsFromRequest( [ "ext-$ext" ] );
531 if ( $this->getVar( "ext-$ext" ) ) {
532 $extsToInstall[] = $ext;
533 }
534 }
535 $this->parent->setVar( '_Extensions', $extsToInstall );
536
537 if ( $this->getVar( '_MainCacheType' ) == 'memcached' ) {
538 $memcServers = explode( "\n", $this->getVar( '_MemCachedServers' ) );
539 if ( !$memcServers ) {
540 $this->parent->showError( 'config-memcache-needservers' );
541 $retVal = false;
542 }
543
544 foreach ( $memcServers as $server ) {
545 $memcParts = explode( ":", $server, 2 );
546 if ( !isset( $memcParts[0] )
547 || ( !IP::isValid( $memcParts[0] )
548 && ( gethostbyname( $memcParts[0] ) == $memcParts[0] ) )
549 ) {
550 $this->parent->showError( 'config-memcache-badip', $memcParts[0] );
551 $retVal = false;
552 } elseif ( !isset( $memcParts[1] ) ) {
553 $this->parent->showError( 'config-memcache-noport', $memcParts[0] );
554 $retVal = false;
555 } elseif ( $memcParts[1] < 1 || $memcParts[1] > 65535 ) {
556 $this->parent->showError( 'config-memcache-badport', 1, 65535 );
557 $retVal = false;
558 }
559 }
560 }
561
562 return $retVal;
563 }
564
565}
address of the mail
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
$wgLang
Definition Setup.php:910
static getExtensionTypes()
Returns an array with the base extension types.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
makeScreenshotsLink( $name, $screenshots)
submitSkins()
If the user skips this installer page, we still need to set up the default skins, but ignore everythi...
Abstract class to define pages for the web installer.
getFieldsetEnd()
Get the end tag of a fieldset.
setVar( $name, $value)
endForm( $continue='continue', $back='back')
getVar( $var, $default=null)
getFieldsetStart( $legend)
Get the starting tags of a fieldset.
either a plain
Definition hooks.txt:2105
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message key
Definition hooks.txt:2214
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:895
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3106
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2063
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned $skin
Definition hooks.txt:2060
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration settings
Definition globals.txt:37
you have access to all of the normal MediaWiki so you can get a DB use the cache
if(!is_readable( $file)) $ext
Definition router.php:55