Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 173 |
|
0.00% |
0 / 24 |
CRAP | |
0.00% |
0 / 1 |
PFUtils | |
0.00% |
0 / 173 |
|
0.00% |
0 / 24 |
3782 | |
0.00% |
0 / 1 |
getContLang | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSMWContLang | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getParser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
linkForSpecialPage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
makeLink | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
titleURLString | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getPageText | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getSpecialPage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getSMWStore | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
linkText | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
printRedirectForm | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
addFormRLModules | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
20 | |||
getAllForms | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
convertBackToPipes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
smartSplitFormTag | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
getFormTagComponents | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getWordForYesOrNo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
arrayMergeRecursiveDistinct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
ignoreFormName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
isCapitalized | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCanonicalName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isTranslateEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCargoFieldDescription | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getReadDB | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Helper functions for the Page Forms extension. |
4 | * |
5 | * @author Yaron Koren |
6 | * @file |
7 | * @ingroup PF |
8 | */ |
9 | |
10 | use MediaWiki\Linker\LinkRenderer; |
11 | use MediaWiki\Linker\LinkTarget; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Revision\RevisionRecord; |
14 | use Wikimedia\Rdbms\DBConnRef; |
15 | use Wikimedia\Rdbms\IDatabase; |
16 | |
17 | class PFUtils { |
18 | |
19 | /** |
20 | * Get a content language object. |
21 | * |
22 | * @return Language |
23 | */ |
24 | public static function getContLang() { |
25 | return MediaWikiServices::getInstance()->getContentLanguage(); |
26 | } |
27 | |
28 | public static function getSMWContLang() { |
29 | if ( function_exists( 'smwfContLang' ) ) { |
30 | // SMW 3.2+ |
31 | return smwfContLang(); |
32 | } else { |
33 | global $smwgContLang; |
34 | return $smwgContLang; |
35 | } |
36 | } |
37 | |
38 | /** |
39 | * Get a parser object. |
40 | * |
41 | * @return Parser |
42 | */ |
43 | public static function getParser() { |
44 | return MediaWikiServices::getInstance()->getParser(); |
45 | } |
46 | |
47 | /** |
48 | * Creates a link to a special page, using that page's top-level description as the link text. |
49 | * @param LinkRenderer $linkRenderer |
50 | * @param string $specialPageName |
51 | * @return string |
52 | */ |
53 | public static function linkForSpecialPage( $linkRenderer, $specialPageName ) { |
54 | $specialPage = self::getSpecialPage( $specialPageName ); |
55 | return $linkRenderer->makeKnownLink( $specialPage->getPageTitle(), |
56 | htmlspecialchars( $specialPage->getDescription() ) ); |
57 | } |
58 | |
59 | /** |
60 | * @param LinkRenderer $linkRenderer |
61 | * @param LinkTarget|Title $title |
62 | * @param string|null $msg Must already be HTML escaped |
63 | * @param array $attrs link attributes |
64 | * @param array $params query parameters |
65 | * |
66 | * @return string HTML link |
67 | * |
68 | * Copied from CargoUtils::makeLink(). |
69 | */ |
70 | public static function makeLink( $linkRenderer, $title, $msg = null, $attrs = [], $params = [] ) { |
71 | global $wgTitle; |
72 | |
73 | if ( $title === null ) { |
74 | return null; |
75 | } elseif ( $wgTitle !== null && $title->equals( $wgTitle ) ) { |
76 | // Display bolded text instead of a link. |
77 | return Linker::makeSelfLinkObj( $title, $msg ); |
78 | } else { |
79 | $html = ( $msg == null ) ? null : new HtmlArmor( $msg ); |
80 | return $linkRenderer->makeLink( $title, $html, $attrs, $params ); |
81 | } |
82 | } |
83 | |
84 | /** |
85 | * Creates the name of the page that appears in the URL; |
86 | * this method is necessary because Title::getPartialURL(), for |
87 | * some reason, doesn't include the namespace |
88 | * @param Title $title |
89 | * @return string |
90 | */ |
91 | public static function titleURLString( $title ) { |
92 | $namespace = $title->getNsText(); |
93 | if ( $namespace !== '' ) { |
94 | $namespace .= ':'; |
95 | } |
96 | if ( self::isCapitalized( $title->getNamespace() ) ) { |
97 | return $namespace . self::getContLang()->ucfirst( $title->getPartialURL() ); |
98 | } else { |
99 | return $namespace . $title->getPartialURL(); |
100 | } |
101 | } |
102 | |
103 | /** |
104 | * Gets the text contents of a page with the passed-in Title object. |
105 | * @param Title $title |
106 | * @param int $audience |
107 | * @return string|null |
108 | */ |
109 | public static function getPageText( $title, $audience = RevisionRecord::FOR_PUBLIC ) { |
110 | $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
111 | $content = $wikiPage->getContent( $audience ); |
112 | if ( $content instanceof TextContent ) { |
113 | return $content->getText(); |
114 | } else { |
115 | return null; |
116 | } |
117 | } |
118 | |
119 | public static function getSpecialPage( $pageName ) { |
120 | return MediaWikiServices::getInstance() |
121 | ->getSpecialPageFactory() |
122 | ->getPage( $pageName ); |
123 | } |
124 | |
125 | /** |
126 | * Helper function to get the SMW data store, if SMW is installed. |
127 | * @return Store|null |
128 | */ |
129 | public static function getSMWStore() { |
130 | if ( class_exists( '\SMW\StoreFactory' ) ) { |
131 | return \SMW\StoreFactory::getStore(); |
132 | } else { |
133 | return null; |
134 | } |
135 | } |
136 | |
137 | /** |
138 | * Creates wiki-text for a link to a wiki page |
139 | * @param int $namespace |
140 | * @param string $name |
141 | * @param string|null $text |
142 | * @return string |
143 | */ |
144 | public static function linkText( $namespace, $name, $text = null ) { |
145 | $title = Title::makeTitleSafe( $namespace, $name ); |
146 | if ( $title === null ) { |
147 | // TODO maybe report an error here? |
148 | return $name; |
149 | } |
150 | if ( $text === null ) { |
151 | return '[[:' . $title->getPrefixedText() . '|' . $name . ']]'; |
152 | } else { |
153 | return '[[:' . $title->getPrefixedText() . '|' . $text . ']]'; |
154 | } |
155 | } |
156 | |
157 | /** |
158 | * Returns a hidden mini-form to be printed at the bottom of various helper |
159 | * forms, like Special:CreateForm, so that the main form can either save or |
160 | * preview the resulting page. |
161 | * |
162 | * @param string $title |
163 | * @param string $page_contents |
164 | * @param string $edit_summary |
165 | * @param bool $is_save |
166 | * @param User $user |
167 | * @return string |
168 | */ |
169 | public static function printRedirectForm( |
170 | $title, |
171 | $page_contents, |
172 | $edit_summary, |
173 | $is_save, |
174 | $user |
175 | ) { |
176 | global $wgPageFormsScriptPath; |
177 | |
178 | if ( $is_save ) { |
179 | $action = "wpSave"; |
180 | } else { |
181 | $action = "wpPreview"; |
182 | } |
183 | |
184 | $text = <<<END |
185 | <p style="position: absolute; left: 45%; top: 45%;"><img src="$wgPageFormsScriptPath/skins/loading.gif" /></p> |
186 | |
187 | END; |
188 | $form_body = Html::hidden( 'wpTextbox1', $page_contents ); |
189 | $form_body .= Html::hidden( 'wpUnicodeCheck', 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ' ); |
190 | $form_body .= Html::hidden( 'wpSummary', $edit_summary ); |
191 | // @TODO - add this in at some point. |
192 | //$form_body .= Html::hidden( 'editRevId', $edit_rev_id ); |
193 | |
194 | $userIsRegistered = $user->isRegistered(); |
195 | if ( $userIsRegistered ) { |
196 | $edit_token = $user->getEditToken(); |
197 | } else { |
198 | $edit_token = \MediaWiki\Session\Token::SUFFIX; |
199 | } |
200 | $form_body .= Html::hidden( 'wpEditToken', $edit_token ); |
201 | $form_body .= Html::hidden( $action, null ); |
202 | |
203 | $form_body .= Html::hidden( 'wpUltimateParam', true ); |
204 | |
205 | $text .= Html::rawElement( |
206 | 'form', |
207 | [ |
208 | 'id' => 'editform', |
209 | 'name' => 'editform', |
210 | 'method' => 'post', |
211 | 'action' => $title instanceof Title ? $title->getLocalURL( 'action=submit' ) : $title |
212 | ], |
213 | $form_body |
214 | ); |
215 | |
216 | $script = <<<END |
217 | window.onload = function() { |
218 | document.editform.submit(); |
219 | } |
220 | |
221 | END; |
222 | |
223 | $nonce = RequestContext::getMain()->getOutput()->getCSP()->getNonce(); |
224 | $text .= Html::inlineScript( $script, $nonce ); |
225 | |
226 | // @TODO - remove this hook? It seems useless. |
227 | MediaWikiServices::getInstance()->getHookContainer()->run( 'PageForms::PrintRedirectForm', [ $is_save, !$is_save, false, &$text ] ); |
228 | return $text; |
229 | } |
230 | |
231 | /** |
232 | * Includes the necessary ResourceLoader modules for the form |
233 | * to display and work correctly. |
234 | * |
235 | * Accepts an optional Parser instance, or uses $wgOut if omitted. |
236 | * @param Parser|null $parser |
237 | */ |
238 | public static function addFormRLModules( $parser = null ) { |
239 | global $wgOut, $wgPageFormsSimpleUpload; |
240 | |
241 | // Handling depends on whether or not this form is embedded |
242 | // in another page. |
243 | if ( !$parser ) { |
244 | $wgOut->addMeta( 'robots', 'noindex,nofollow' ); |
245 | $output = $wgOut; |
246 | } else { |
247 | $output = $parser->getOutput(); |
248 | } |
249 | |
250 | $mainModules = [ |
251 | 'ext.pageforms.main', |
252 | 'ext.pageforms.submit', |
253 | 'ext.smw.tooltips', |
254 | // @TODO - the inclusion of modules for specific |
255 | // form inputs is wasteful, and should be removed - |
256 | // it should only be done as needed for each input. |
257 | // Unfortunately the use of multiple-instance |
258 | // templates makes that tricky (every form input needs |
259 | // to re-apply the JS on a new instance) - it can be |
260 | // done via JS hooks, but it hasn't been done yet. |
261 | 'ext.pageforms.jstree', |
262 | 'ext.pageforms.imagepreview', |
263 | 'ext.pageforms.autogrow', |
264 | 'ext.pageforms.checkboxes', |
265 | 'ext.pageforms.select2', |
266 | 'ext.pageforms.rating', |
267 | 'ext.pageforms.popupformedit', |
268 | 'ext.pageforms.fullcalendar', |
269 | 'jquery.makeCollapsible' |
270 | ]; |
271 | |
272 | $mainModuleStyles = [ |
273 | 'ext.pageforms.main.styles', |
274 | 'ext.pageforms.submit.styles', |
275 | "ext.pageforms.checkboxes.styles", |
276 | 'ext.pageforms.select2.styles', |
277 | 'ext.pageforms.rating.styles', |
278 | "ext.pageforms.forminput.styles" |
279 | ]; |
280 | |
281 | if ( $wgPageFormsSimpleUpload ) { |
282 | $mainModules[] = 'ext.pageforms.simpleupload'; |
283 | } |
284 | |
285 | $output->addModules( $mainModules ); |
286 | $output->addModuleStyles( $mainModuleStyles ); |
287 | |
288 | $otherModules = []; |
289 | MediaWikiServices::getInstance()->getHookContainer()->run( 'PageForms::AddRLModules', [ &$otherModules ] ); |
290 | // @phan-suppress-next-line PhanEmptyForeach |
291 | foreach ( $otherModules as $rlModule ) { |
292 | $output->addModules( $rlModule ); |
293 | } |
294 | } |
295 | |
296 | /** |
297 | * Returns an array of all form names on this wiki. |
298 | * @return string[] |
299 | */ |
300 | public static function getAllForms() { |
301 | $dbr = self::getReadDB(); |
302 | $res = $dbr->select( 'page', |
303 | 'page_title', |
304 | [ 'page_namespace' => PF_NS_FORM, |
305 | 'page_is_redirect' => false ], |
306 | __METHOD__, |
307 | [ 'ORDER BY' => 'page_title' ] ); |
308 | $form_names = []; |
309 | while ( $row = $res->fetchRow() ) { |
310 | $form_names[] = str_replace( '_', ' ', $row[0] ); |
311 | } |
312 | $res->free(); |
313 | if ( count( $form_names ) == 0 ) { |
314 | // This case requires special handling in the UI. |
315 | throw new MWException( wfMessage( 'pf-noforms-error' )->parse() ); |
316 | } |
317 | return $form_names; |
318 | } |
319 | |
320 | /** |
321 | * A helper function, used by getFormTagComponents(). |
322 | * @param string $s |
323 | * @return string |
324 | */ |
325 | public static function convertBackToPipes( $s ) { |
326 | return str_replace( "\1", '|', $s ); |
327 | } |
328 | |
329 | /** |
330 | * Splits the contents of a tag in a form definition based on pipes, |
331 | * but does not split on pipes that are contained within additional |
332 | * curly brackets, in case the tag contains any calls to parser |
333 | * functions or templates. |
334 | * @param string $string |
335 | * @return string[] |
336 | */ |
337 | static function smartSplitFormTag( $string ) { |
338 | if ( $string == '' ) { |
339 | return []; |
340 | } |
341 | |
342 | $delimiter = '|'; |
343 | $returnValues = []; |
344 | $numOpenCurlyBrackets = 0; |
345 | $curReturnValue = ''; |
346 | |
347 | for ( $i = 0; $i < strlen( $string ); $i++ ) { |
348 | $curChar = $string[$i]; |
349 | if ( $curChar == '{' ) { |
350 | $numOpenCurlyBrackets++; |
351 | } elseif ( $curChar == '}' ) { |
352 | $numOpenCurlyBrackets--; |
353 | } |
354 | |
355 | if ( $curChar == $delimiter && $numOpenCurlyBrackets == 0 ) { |
356 | $returnValues[] = trim( $curReturnValue ); |
357 | $curReturnValue = ''; |
358 | } else { |
359 | $curReturnValue .= $curChar; |
360 | } |
361 | } |
362 | $returnValues[] = trim( $curReturnValue ); |
363 | |
364 | return $returnValues; |
365 | } |
366 | |
367 | /** |
368 | * This function is basically equivalent to calling |
369 | * explode( '|', $str ), except that it doesn't split on pipes |
370 | * that are within parser function calls - i.e., pipes within |
371 | * double curly brackets. |
372 | * @param string $str |
373 | * @return string[] |
374 | */ |
375 | public static function getFormTagComponents( $str ) { |
376 | // Turn each pipe within double curly brackets into another, |
377 | // unused character (here, "\1"), then do the explode, then |
378 | // convert them back. |
379 | // regex adapted from: |
380 | // https://www.regular-expressions.info/recurse.html |
381 | $pattern = '/{{(?>[^{}]|(?R))*?}}/'; |
382 | // needed to fix highlighting - <? |
383 | // Remove HTML comments |
384 | $str = preg_replace( '/<!--.*?-->/s', '', $str ); |
385 | $str = preg_replace_callback( $pattern, static function ( $match ) { |
386 | $hasPipe = strpos( $match[0], '|' ); |
387 | return $hasPipe ? str_replace( "|", "\1", $match[0] ) : $match[0]; |
388 | }, $str ); |
389 | return array_map( [ 'PFUtils', 'convertBackToPipes' ], self::smartSplitFormTag( $str ) ); |
390 | } |
391 | |
392 | /** |
393 | * Gets the word in the wiki's language for either the value 'yes' or |
394 | * 'no'. |
395 | * @param bool $isYes |
396 | * @return string |
397 | */ |
398 | public static function getWordForYesOrNo( $isYes ) { |
399 | // @TODO - should Page Forms define these messages itself? |
400 | $message = $isYes ? 'htmlform-yes' : 'htmlform-no'; |
401 | return wfMessage( $message )->inContentLanguage()->text(); |
402 | } |
403 | |
404 | /** |
405 | * array_merge_recursive merges arrays, but it converts values with duplicate |
406 | * keys to arrays rather than overwriting the value in the first array with the duplicate |
407 | * value in the second array, as array_merge does. |
408 | * |
409 | * arrayMergeRecursiveDistinct() does not change the datatypes of the values in the arrays. |
410 | * Matching keys' values in the second array overwrite those in the first array. |
411 | * |
412 | * Parameters are passed by reference, though only for performance reasons. They're not |
413 | * altered by this function. |
414 | * |
415 | * See http://www.php.net/manual/en/function.array-merge-recursive.php#92195 |
416 | * |
417 | * @param array &$array1 |
418 | * @param array &$array2 |
419 | * @return array |
420 | * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk> |
421 | * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com> |
422 | */ |
423 | public static function arrayMergeRecursiveDistinct( array &$array1, array &$array2 ) { |
424 | $merged = $array1; |
425 | |
426 | foreach ( $array2 as $key => &$value ) { |
427 | if ( is_array( $value ) && isset( $merged[$key] ) && is_array( $merged[$key] ) ) { |
428 | $merged[$key] = self::arrayMergeRecursiveDistinct( $merged[$key], $value ); |
429 | } else { |
430 | $merged[$key] = $value; |
431 | } |
432 | } |
433 | |
434 | return $merged; |
435 | } |
436 | |
437 | /** |
438 | * Return whether to "ignore" (treat as a non-form) a form with this |
439 | * name, based on whether it matches any of the specified text patterns. |
440 | * |
441 | * @param string $formName |
442 | * @return bool |
443 | */ |
444 | public static function ignoreFormName( $formName ) { |
445 | global $wgPageFormsIgnoreTitlePattern; |
446 | |
447 | if ( !is_array( $wgPageFormsIgnoreTitlePattern ) ) { |
448 | $wgPageFormsIgnoreTitlePattern = [ $wgPageFormsIgnoreTitlePattern ]; |
449 | } |
450 | |
451 | foreach ( $wgPageFormsIgnoreTitlePattern as $pattern ) { |
452 | if ( preg_match( '/' . $pattern . '/', $formName ) ) { |
453 | return true; |
454 | } |
455 | } |
456 | |
457 | return false; |
458 | } |
459 | |
460 | public static function isCapitalized( $index ) { |
461 | return MediaWikiServices::getInstance() |
462 | ->getNamespaceInfo() |
463 | ->isCapitalized( $index ); |
464 | } |
465 | |
466 | public static function getCanonicalName( $index ) { |
467 | return MediaWikiServices::getInstance() |
468 | ->getNamespaceInfo() |
469 | ->getCanonicalName( $index ); |
470 | } |
471 | |
472 | public static function isTranslateEnabled() { |
473 | return ExtensionRegistry::getInstance()->isLoaded( 'Translate' ); |
474 | } |
475 | |
476 | public static function getCargoFieldDescription( $cargoTable, $cargoField ) { |
477 | try { |
478 | $tableSchemas = CargoUtils::getTableSchemas( [ $cargoTable ] ); |
479 | } catch ( MWException $e ) { |
480 | return null; |
481 | } |
482 | if ( !array_key_exists( $cargoTable, $tableSchemas ) ) { |
483 | return null; |
484 | } |
485 | $tableSchema = $tableSchemas[$cargoTable]; |
486 | return $tableSchema->mFieldDescriptions[$cargoField] ?? null; |
487 | } |
488 | |
489 | /** |
490 | * Provides database for read access |
491 | * |
492 | * @return IDatabase|DBConnRef |
493 | */ |
494 | public static function getReadDB() { |
495 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
496 | if ( method_exists( $lbFactory, 'getReplicaDatabase' ) ) { |
497 | // MW 1.40+ |
498 | // The correct type \Wikimedia\Rdbms\IReadableDatabase cannot be used |
499 | // as the return type, as that class only exists since 1.40. |
500 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
501 | return $lbFactory->getReplicaDatabase(); |
502 | } else { |
503 | return $lbFactory->getMainLB()->getConnection( DB_REPLICA ); |
504 | } |
505 | } |
506 | } |