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