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