Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 173
0.00% covered (danger)
0.00%
0 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFUtils
0.00% covered (danger)
0.00%
0 / 173
0.00% covered (danger)
0.00%
0 / 24
3782
0.00% covered (danger)
0.00%
0 / 1
 getContLang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSMWContLang
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getParser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkForSpecialPage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 makeLink
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 titleURLString
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getPageText
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialPage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSMWStore
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 linkText
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 printRedirectForm
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 addFormRLModules
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 getAllForms
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 convertBackToPipes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 smartSplitFormTag
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 getFormTagComponents
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getWordForYesOrNo
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 arrayMergeRecursiveDistinct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 ignoreFormName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 isCapitalized
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCanonicalName
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isTranslateEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCargoFieldDescription
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getReadDB
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
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
10use MediaWiki\Linker\LinkRenderer;
11use MediaWiki\Linker\LinkTarget;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Revision\RevisionRecord;
14use Wikimedia\Rdbms\DBConnRef;
15use Wikimedia\Rdbms\IDatabase;
16
17class 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
187END;
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
221END;
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}