Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.03% covered (warning)
58.03%
206 / 355
50.00% covered (danger)
50.00%
13 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFFormUtils
58.03% covered (warning)
58.03%
206 / 355
50.00% covered (danger)
50.00%
13 / 26
777.42
0.00% covered (danger)
0.00%
0 / 1
 unhandledFieldsHTML
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 summaryInputHTML
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
4
 minorEditInputHTML
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
42
 watchInputHTML
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
110
 buttonHTML
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 saveButtonHTML
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 saveAndContinueButtonHTML
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 showPreviewButtonHTML
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 showChangesButtonHTML
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 cancelLinkHTML
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 runQueryButtonHTML
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 formBottom
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
3.02
 getPreloadedText
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
 queryFormBottom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMonthNames
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 setGlobalVarsForSpreadsheet
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getFormDefinition
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
72
 getFormDefinitionFromCache
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 cacheFormDefinition
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 purgeCache
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
13.69
 purgeCacheOnSave
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getFormCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getCacheKey
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 headerHTML
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getChangedIndex
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 setShowOnSelect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3use MediaWiki\CommentStore\CommentStore;
4use MediaWiki\Html\Html;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Revision\RenderedRevision;
7use MediaWiki\Title\Title;
8use OOUI\ButtonInputWidget;
9
10/**
11 * Utilities for the display and retrieval of forms.
12 *
13 * @author Yaron Koren
14 * @author Jeffrey Stuckman
15 * @author Harold Solbrig
16 * @author Eugene Mednikov
17 * @file
18 * @ingroup PF
19 */
20
21class PFFormUtils {
22
23    /**
24     * Add a hidden input for each field in the template call that's
25     * not handled by the form itself
26     * @param PFTemplateInForm|null $template_in_form
27     * @param bool $is_autoedit
28     * @return string
29     */
30    static function unhandledFieldsHTML( $template_in_form, $is_autoedit = false ) {
31        // This shouldn't happen, but sometimes this value is null.
32        // @TODO - fix the code that calls this function so the
33        // value is never null.
34        if ( $template_in_form === null ) {
35            return '';
36        }
37
38        // HTML element names shouldn't contain spaces
39        $templateName = str_replace( ' ', '_', $template_in_form->getTemplateName() );
40        $text = "";
41        foreach ( $template_in_form->getValuesFromPage() as $key => $value ) {
42            if ( $key === null || is_numeric( $key ) ) {
43                continue;
44            }
45            // Handle the special case of #autoedit - we ignore
46            // blank values, because we don't want a case like
47            // {{#autoedit:...|City={{{City|}}}...}} (within a
48            // template) to blank the value of "City", if the user
49            // didn't enter anything.
50            if ( $is_autoedit && $value === '' ) {
51                continue;
52            }
53
54            $key = urlencode( $key );
55            $text .= Html::hidden( '_unhandled_' . $templateName . '_' . $key, $value );
56        }
57        return $text;
58    }
59
60    static function summaryInputHTML( $is_disabled, $label = null, $attr = [], $value = '' ) {
61        global $wgPageFormsTabIndex;
62
63        if ( $label == null ) {
64            $label = wfMessage( 'summary' )->parse();
65        }
66
67        $wgPageFormsTabIndex++;
68        $attr += [
69            'tabIndex' => $wgPageFormsTabIndex,
70            'value' => $value,
71            'name' => 'wpSummary',
72            'id' => 'wpSummary',
73            'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
74            'title' => wfMessage( 'tooltip-summary' )->text(),
75            'accessKey' => wfMessage( 'accesskey-summary' )->text()
76        ];
77        if ( $is_disabled ) {
78            $attr['disabled'] = true;
79        }
80        if ( array_key_exists( 'class', $attr ) ) {
81            $attr['classes'] = [ $attr['class'] ];
82        }
83
84        $text = new OOUI\FieldLayout(
85            new OOUI\TextInputWidget( $attr ),
86            [
87                'align' => 'top',
88                'label' => new OOUI\HtmlSnippet( $label )
89            ]
90        );
91
92        return $text;
93    }
94
95    static function minorEditInputHTML( $form_submitted, $is_disabled, $is_checked, $label = null, $attrs = [] ) {
96        global $wgPageFormsTabIndex;
97
98        $wgPageFormsTabIndex++;
99        if ( !$form_submitted ) {
100            $user = RequestContext::getMain()->getUser();
101            $is_checked = MediaWikiServices::getInstance()->getUserOptionsLookup()
102                ->getOption( $user, 'minordefault' );
103        }
104
105        if ( $label == null ) {
106            $label = wfMessage( 'minoredit' )->parse();
107        }
108
109        $attrs += [
110            'id' => 'wpMinoredit',
111            'name' => 'wpMinoredit',
112            'accessKey' => wfMessage( 'accesskey-minoredit' )->text(),
113            'tabIndex' => $wgPageFormsTabIndex,
114        ];
115        if ( $is_checked ) {
116            $attrs['selected'] = true;
117        }
118        if ( $is_disabled ) {
119            $attrs['disabled'] = true;
120        }
121        if ( array_key_exists( 'class', $attrs ) ) {
122            $attrs['classes'] = [ $attrs['class'] ];
123        }
124
125        // We can't use OOUI\FieldLayout here, because it will make the display too wide.
126        $labelWidget = new OOUI\LabelWidget( [
127            'label' => new OOUI\HtmlSnippet( $label )
128        ] );
129        $text = Html::rawElement(
130            'label',
131            [ 'title' => wfMessage( 'tooltip-minoredit' )->parse() ],
132            new OOUI\CheckboxInputWidget( $attrs ) . $labelWidget
133        );
134        $text = Html::rawElement( 'div', [ 'style' => 'display: inline-block; padding: 12px 16px 12px 0;' ], $text );
135
136        return $text;
137    }
138
139    static function watchInputHTML( $form_submitted, $is_disabled, $is_checked = false, $label = null, $attrs = [] ) {
140        global $wgPageFormsTabIndex, $wgTitle;
141
142        $wgPageFormsTabIndex++;
143        // figure out if the checkbox should be checked -
144        // this code borrowed from /includes/EditPage.php
145        if ( !$form_submitted ) {
146            $user = RequestContext::getMain()->getUser();
147            $services = MediaWikiServices::getInstance();
148            $userOptionsLookup = $services->getUserOptionsLookup();
149            $watchlistManager = $services->getWatchlistManager();
150            if ( $userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
151                # Watch all edits
152                $is_checked = true;
153            } elseif ( $userOptionsLookup->getOption( $user, 'watchcreations' ) &&
154                !$wgTitle->exists() ) {
155                # Watch creations
156                $is_checked = true;
157            } elseif ( $watchlistManager->isWatched( $user, $wgTitle ) ) {
158                # Already watched
159                $is_checked = true;
160            }
161        }
162        if ( $label == null ) {
163            $label = wfMessage( 'watchthis' )->parse();
164        }
165        $attrs += [
166            'id' => 'wpWatchthis',
167            'name' => 'wpWatchthis',
168            'accessKey' => wfMessage( 'accesskey-watch' )->text(),
169            'tabIndex' => $wgPageFormsTabIndex,
170        ];
171        if ( $is_checked ) {
172            $attrs['selected'] = true;
173        }
174        if ( $is_disabled ) {
175            $attrs['disabled'] = true;
176        }
177        if ( array_key_exists( 'class', $attrs ) ) {
178            $attrs['classes'] = [ $attrs['class'] ];
179        }
180
181        // We can't use OOUI\FieldLayout here, because it will make the display too wide.
182        $labelWidget = new OOUI\LabelWidget( [
183            'label' => new OOUI\HtmlSnippet( $label )
184        ] );
185        $text = Html::rawElement(
186            'label',
187            [ 'title' => wfMessage( 'tooltip-watch' )->parse() ],
188            new OOUI\CheckboxInputWidget( $attrs ) . $labelWidget
189        );
190        $text = Html::rawElement( 'div', [ 'style' => 'display: inline-block; padding: 12px 16px 12px 0;' ], $text );
191
192        return $text;
193    }
194
195    /**
196     * Helper function to display a simple button
197     * @param string $name
198     * @param string $value
199     * @param string $type
200     * @param array $attrs
201     * @return ButtonInputWidget
202     */
203    private static function buttonHTML( $name, $value, $type, $attrs ) {
204        $attrs += [
205            'type' => $type,
206            'name' => $name,
207            'label' => $value
208        ];
209        $button = new ButtonInputWidget( $attrs );
210        // Special handling for 'class'.
211        if ( isset( $attrs['class'] ) ) {
212            // Make sure it's an array.
213            if ( is_string( $attrs['class'] ) ) {
214                $attrs['class'] = [ $attrs['class'] ];
215            }
216            $button->addClasses( $attrs['class'] );
217        }
218        return $button;
219    }
220
221    static function saveButtonHTML( $is_disabled, $label = null, $attr = [] ) {
222        global $wgPageFormsTabIndex;
223
224        $wgPageFormsTabIndex++;
225        if ( $label == null ) {
226            $label = wfMessage( 'savearticle' )->text();
227        }
228        $temp = $attr + [
229            'id'        => 'wpSave',
230            'tabIndex'  => $wgPageFormsTabIndex,
231            'accessKey' => wfMessage( 'accesskey-save' )->text(),
232            'title'     => wfMessage( 'tooltip-save' )->text(),
233            'flags'     => [ 'primary', 'progressive' ]
234        ];
235        if ( $is_disabled ) {
236            $temp['disabled'] = true;
237        }
238        return self::buttonHTML( 'wpSave', $label, 'submit', $temp );
239    }
240
241    static function saveAndContinueButtonHTML( $is_disabled, $label = null, $attr = [] ) {
242        global $wgPageFormsTabIndex;
243
244        $wgPageFormsTabIndex++;
245
246        if ( $label == null ) {
247            $label = wfMessage( 'pf_formedit_saveandcontinueediting' )->text();
248        }
249
250        $temp = $attr + [
251            'id'        => 'wpSaveAndContinue',
252            'tabIndex'  => $wgPageFormsTabIndex,
253            'disabled'  => true,
254            'accessKey' => wfMessage( 'pf_formedit_accesskey_saveandcontinueediting' )->text(),
255            'title'     => wfMessage( 'pf_formedit_tooltip_saveandcontinueediting' )->text(),
256        ];
257
258        if ( $is_disabled ) {
259            $temp['class'] = 'pf-save_and_continue disabled';
260        } else {
261            $temp['class'] = 'pf-save_and_continue';
262        }
263
264        return self::buttonHTML( 'wpSaveAndContinue', $label, 'button', $temp );
265    }
266
267    static function showPreviewButtonHTML( $is_disabled, $label = null, $attr = [] ) {
268        global $wgPageFormsTabIndex;
269
270        $wgPageFormsTabIndex++;
271        if ( $label == null ) {
272            $label = wfMessage( 'showpreview' )->text();
273        }
274        $temp = $attr + [
275            'id'        => 'wpPreview',
276            'tabIndex'  => $wgPageFormsTabIndex,
277            'accessKey' => wfMessage( 'accesskey-preview' )->text(),
278            'title'     => wfMessage( 'tooltip-preview' )->text(),
279        ];
280        if ( $is_disabled ) {
281            $temp['disabled'] = true;
282        }
283        return self::buttonHTML( 'wpPreview', $label, 'submit', $temp );
284    }
285
286    static function showChangesButtonHTML( $is_disabled, $label = null, $attr = [] ) {
287        global $wgPageFormsTabIndex;
288
289        $wgPageFormsTabIndex++;
290        if ( $label == null ) {
291            $label = wfMessage( 'showdiff' )->text();
292        }
293        $temp = $attr + [
294            'id'        => 'wpDiff',
295            'tabIndex'  => $wgPageFormsTabIndex,
296            'accessKey' => wfMessage( 'accesskey-diff' )->text(),
297            'title'     => wfMessage( 'tooltip-diff' )->text(),
298        ];
299        if ( $is_disabled ) {
300            $temp['disabled'] = true;
301        }
302        return self::buttonHTML( 'wpDiff', $label, 'submit', $temp );
303    }
304
305    static function cancelLinkHTML( $is_disabled, $label = null, $attr = [] ) {
306        global $wgTitle;
307
308        if ( $label == null ) {
309            $label = wfMessage( 'cancel' )->parse();
310        }
311        $attr['classes'] = [];
312        if ( $wgTitle == null || $wgTitle->isSpecial( 'FormEdit' ) ) {
313            $attr['classes'][] = 'pfSendBack';
314        } else {
315            $attr['href'] = $wgTitle->getFullURL();
316        }
317        $attr['framed'] = false;
318        $attr['label'] = $label;
319        $attr['flags'] = [ 'destructive' ];
320        if ( array_key_exists( 'class', $attr ) ) {
321            $attr['classes'][] = $attr['class'];
322        }
323
324        return "\t\t" . new OOUI\ButtonWidget( $attr ) . "\n";
325    }
326
327    static function runQueryButtonHTML( $is_disabled = false, $label = null, $attr = [] ) {
328        // is_disabled is currently ignored
329        global $wgPageFormsTabIndex;
330
331        $wgPageFormsTabIndex++;
332        if ( $label == null ) {
333            $label = wfMessage( 'runquery' )->text();
334        }
335        $buttonHTML = self::buttonHTML( 'wpRunQuery', $label, 'submit',
336            $attr + [
337            'id' => 'wpRunQuery',
338            'tabIndex' => $wgPageFormsTabIndex,
339            'title' => $label,
340            'flags' => [ 'primary', 'progressive' ],
341            'icon' => 'search'
342        ] );
343        return new OOUI\FieldLayout( $buttonHTML );
344    }
345
346    /**
347     * Much of this function is based on MediaWiki's EditPage::showEditForm().
348     * @param bool $form_submitted
349     * @param bool $is_disabled
350     * @return string
351     */
352    static function formBottom( $form_submitted, $is_disabled ) {
353        $text = <<<END
354    <br />
355    <div class='editOptions'>
356
357END;
358        $req = RequestContext::getMain()->getRequest();
359        $summary = $req->getVal( 'wpSummary' );
360        $text .= self::summaryInputHTML( $is_disabled, null, [], $summary );
361        $user = RequestContext::getMain()->getUser();
362        if ( $user->isAllowed( 'minoredit' ) ) {
363            $text .= self::minorEditInputHTML( $form_submitted, $is_disabled, false );
364        }
365
366        $userIsRegistered = $user->isRegistered();
367        if ( $userIsRegistered ) {
368            $text .= self::watchInputHTML( $form_submitted, $is_disabled );
369        }
370
371        $text .= <<<END
372    <br />
373    <div class='editButtons'>
374
375END;
376        $text .= self::saveButtonHTML( $is_disabled );
377        $text .= self::showPreviewButtonHTML( $is_disabled );
378        $text .= self::showChangesButtonHTML( $is_disabled );
379        $text .= self::cancelLinkHTML( $is_disabled );
380        $text .= <<<END
381    </div><!-- editButtons -->
382    </div><!-- editOptions -->
383
384END;
385        return $text;
386    }
387
388    /**
389     * Loosely based on MediaWiki's EditPage::getPreloadedContent().
390     *
391     * @param string $preload
392     * @return string
393     */
394    static function getPreloadedText( $preload ) {
395        if ( $preload === '' ) {
396            return '';
397        }
398
399        $preloadTitle = Title::newFromText( $preload );
400        if ( !isset( $preloadTitle ) ) {
401            return '';
402        }
403
404        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
405        $user = RequestContext::getMain()->getUser();
406        if ( !$permissionManager->userCan( 'read', $user, $preloadTitle ) ) {
407            return '';
408        }
409
410        $text = PFUtils::getPageText( $preloadTitle );
411        // Remove <noinclude> sections and <includeonly> tags from text
412        $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text );
413        $text = strtr( $text, [ '<includeonly>' => '', '</includeonly>' => '' ] );
414        return $text;
415    }
416
417    /**
418     * Used by 'RunQuery' page
419     * @return string
420     */
421    static function queryFormBottom() {
422        return self::runQueryButtonHTML( false );
423    }
424
425    static function getMonthNames() {
426        return [
427            wfMessage( 'january' )->inContentLanguage()->text(),
428            wfMessage( 'february' )->inContentLanguage()->text(),
429            wfMessage( 'march' )->inContentLanguage()->text(),
430            wfMessage( 'april' )->inContentLanguage()->text(),
431            // Needed to avoid using 3-letter abbreviation
432            wfMessage( 'may_long' )->inContentLanguage()->text(),
433            wfMessage( 'june' )->inContentLanguage()->text(),
434            wfMessage( 'july' )->inContentLanguage()->text(),
435            wfMessage( 'august' )->inContentLanguage()->text(),
436            wfMessage( 'september' )->inContentLanguage()->text(),
437            wfMessage( 'october' )->inContentLanguage()->text(),
438            wfMessage( 'november' )->inContentLanguage()->text(),
439            wfMessage( 'december' )->inContentLanguage()->text()
440        ];
441    }
442
443    public static function setGlobalVarsForSpreadsheet() {
444        global $wgPageFormsContLangYes, $wgPageFormsContLangNo, $wgPageFormsContLangMonths;
445
446        // JS variables that hold boolean and date values in the wiki's
447        // (as opposed to the user's) language.
448        $wgPageFormsContLangYes = wfMessage( 'htmlform-yes' )->inContentLanguage()->text();
449        $wgPageFormsContLangNo = wfMessage( 'htmlform-no' )->inContentLanguage()->text();
450        $monthMessages = [
451            "january", "february", "march", "april", "may_long", "june",
452            "july", "august", "september", "october", "november", "december"
453        ];
454        $wgPageFormsContLangMonths = [ '' ];
455        foreach ( $monthMessages as $monthMsg ) {
456            $wgPageFormsContLangMonths[] = wfMessage( $monthMsg )->inContentLanguage()->text();
457        }
458    }
459
460    /**
461     * Parse the form definition and return it
462     * @param Parser $parser
463     * @param string|null $form_def
464     * @param string|null $form_id
465     * @return string
466     */
467    public static function getFormDefinition( Parser $parser, $form_def = null, $form_id = null ) {
468        if ( $form_id !== null ) {
469            $cachedDef = self::getFormDefinitionFromCache( $form_id, $parser );
470
471            if ( $cachedDef !== null ) {
472                return $cachedDef;
473            }
474        }
475
476        if ( $form_def !== null ) {
477            // Do nothing.
478        } elseif ( $form_id !== null ) {
479            $form_title = Title::newFromID( $form_id );
480            $form_def = PFUtils::getPageText( $form_title );
481        } else {
482            // No text, no ID -> no form definition.
483            return '';
484        }
485
486        // Remove <noinclude> sections and <includeonly> tags from form definition
487        $form_def = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $form_def );
488        $form_def = strtr( $form_def, [ '<includeonly>' => '', '</includeonly>' => '' ] );
489
490        // We need to replace all PF tags in the form definition by strip items. But we can not just use
491        // the Parser strip state because the Parser would during parsing replace all strip items and then
492        // mangle them into HTML code. So we have to use our own. Which means we also can not just use
493        // Parser::insertStripItem() (see below).
494        // Also include a quotation mark, to help avoid security leaks.
495        $rnd = wfRandomString( 16 ) . '"' . wfRandomString( 15 );
496
497        // This regexp will find any PF triple braced tags (including correct handling of contained braces), i.e.
498        // {{{field|foo|default={{Bar}}}}} is not a problem. When used with preg_match and friends, $matches[0] will
499        // contain the whole PF tag, $matches[1] will contain the tag without the enclosing triple braces.
500        $regexp = '#\{\{\{((?>[^\{\}]+)|(\{((?>[^\{\}]+)|(?-2))*\}))*\}\}\}#';
501        // Needed to restore highlighting in vi - <?
502
503        $items = [];
504
505        // replace all PF tags by strip markers
506        $form_def = preg_replace_callback(
507            $regexp,
508
509            // This is essentially a copy of Parser::insertStripItem().
510            static function ( array $matches ) use ( &$items, $rnd ) {
511                $markerIndex = count( $items );
512                $items[] = $matches[0];
513                return "$rnd-item-$markerIndex-$rnd";
514            },
515
516            $form_def
517        );
518
519        // Parse wiki-text.
520        // @phan-suppress-next-line PhanRedundantCondition for BC with old MW
521        $title = is_object( $parser->getTitle() ) ? $parser->getTitle() : $form_title;
522        // We need to pass "false" in to the parse() $clearState param so that
523        // embedding Special:RunQuery will work.
524        $output = $parser->parse( $form_def, $title, $parser->getOptions(), true, false );
525        $form_def = $output->runOutputPipeline( $parser->getOptions() )->getContentHolderText();
526        $form_def = preg_replace_callback(
527            "/{$rnd}-item-(\d+)-{$rnd}/",
528            static function ( array $matches ) use ( $items ) {
529                $markerIndex = (int)$matches[1];
530                return $items[$markerIndex];
531            },
532            $form_def
533        );
534
535        if ( $output->getCacheTime() == -1 ) {
536            $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $form_id );
537            self::purgeCache( $wikiPage );
538            wfDebug( "Caching disabled for form definition $form_id\n" );
539        } elseif ( $form_id !== null ) {
540            self::cacheFormDefinition( $form_id, $form_def, $parser );
541        }
542
543        return $form_def;
544    }
545
546    /**
547     * Get a form definition from cache
548     * @param string $form_id
549     * @param Parser $parser
550     * @return string|null
551     */
552    protected static function getFormDefinitionFromCache( $form_id, Parser $parser ) {
553        global $wgPageFormsCacheFormDefinitions;
554
555        // use cache if allowed
556        if ( !$wgPageFormsCacheFormDefinitions ) {
557            return null;
558        }
559
560        $cache = self::getFormCache();
561
562        // create a cache key consisting of owner name, article id and user options
563        $cacheKeyForForm = self::getCacheKey( $form_id, $parser );
564
565        $cached_def = $cache->get( $cacheKeyForForm );
566
567        // Cache hit?
568        if ( is_string( $cached_def ) ) {
569            wfDebug( "Cache hit: Got form definition $cacheKeyForForm from cache\n" );
570
571            return $cached_def;
572        }
573
574        wfDebug( "Cache miss: Form definition $cacheKeyForForm not found in cache\n" );
575
576        return null;
577    }
578
579    /**
580     * Store a form definition in cache
581     * @param string $form_id
582     * @param string $form_def
583     * @param Parser $parser
584     */
585    protected static function cacheFormDefinition( $form_id, $form_def, Parser $parser ) {
586        global $wgPageFormsCacheFormDefinitions;
587
588        // Store in cache if requested
589        if ( !$wgPageFormsCacheFormDefinitions ) {
590            return;
591        }
592
593        $cache = self::getFormCache();
594        $cacheKeyForForm = self::getCacheKey( $form_id, $parser );
595        $cacheKeyForList = self::getCacheKey( $form_id );
596
597        // Update list of form definitions
598        $listOfFormKeys = $cache->get( $cacheKeyForList );
599        if ( !is_array( $listOfFormKeys ) ) {
600            $listOfFormKeys = [];
601        }
602        // The list of values is used by self::purge, keys are ignored.
603        // This way we automatically override duplicates.
604        $listOfFormKeys[$cacheKeyForForm] = $cacheKeyForForm;
605
606        // We cache indefinitely ignoring $wgParserCacheExpireTime.
607        // The reasoning is that there really is not point in expiring
608        // rarely changed forms automatically (after one day per
609        // default). Instead the cache is purged on storing/purging a
610        // form definition.
611
612        // Store form definition with current user options
613        $cache->set( $cacheKeyForForm, $form_def );
614
615        // Store updated list of form definitions
616        $cache->set( $cacheKeyForList, $listOfFormKeys );
617        wfDebug( "Cached form definition $cacheKeyForForm\n" );
618    }
619
620    /**
621     * Deletes the form definition associated with the given wiki page
622     * from the main cache.
623     *
624     * Hooks: ArticlePurge
625     *
626     * @param WikiPage $wikipage
627     * @return bool
628     */
629    public static function purgeCache( WikiPage $wikipage ) {
630        if ( !$wikipage->getTitle()->inNamespace( PF_NS_FORM ) ) {
631            return true;
632        }
633
634        $cache = self::getFormCache();
635        $cacheKeyForList = self::getCacheKey( $wikipage->getId() );
636
637        // get references to stored datasets
638        $listOfFormKeys = $cache->get( $cacheKeyForList );
639
640        if ( !is_array( $listOfFormKeys ) ) {
641            return true;
642        }
643
644        // delete stored datasets
645        foreach ( $listOfFormKeys as $key ) {
646            $cache->delete( $key );
647            wfDebug( "Deleted cached form definition $key.\n" );
648        }
649
650        // delete references to datasets
651        $cache->delete( $cacheKeyForList );
652        wfDebug( "Deleted cached form definition references $cacheKeyForList.\n" );
653
654        return true;
655    }
656
657    /**
658     * Deletes the form definition associated with the given wiki page
659     * from the main cache.
660     *
661     * Hook: MultiContentSave
662     *
663     * @param RenderedRevision $renderedRevision
664     * @return bool
665     */
666    public static function purgeCacheOnSave( RenderedRevision $renderedRevision ) {
667        $articleID = $renderedRevision->getRevision()->getPageId();
668        $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $articleID );
669        if ( $wikiPage == null ) {
670            // @TODO - should this ever happen?
671            return true;
672        }
673        return self::purgeCache( $wikiPage );
674    }
675
676    /**
677     * Get the cache object used by the form cache
678     * @return BagOStuff
679     */
680    public static function getFormCache() {
681        global $wgPageFormsFormCacheType, $wgParserCacheType;
682        $ret = ObjectCache::getInstance( ( $wgPageFormsFormCacheType !== null ) ? $wgPageFormsFormCacheType : $wgParserCacheType );
683        return $ret;
684    }
685
686    /**
687     * Get a cache key.
688     *
689     * @param string $formId
690     * @param Parser|null $parser Provide parser to get unique cache key
691     * @return string
692     */
693    public static function getCacheKey( $formId, $parser = null ) {
694        $cache = self::getFormCache();
695
696        return ( $parser === null )
697            ? $cache->makeKey( 'ext.PageForms.formdefinition', $formId )
698            : $cache->makeKey(
699                'ext.PageForms.formdefinition',
700                $formId,
701                $parser->getOptions()->optionsHash( ParserOptions::allCacheVaryingOptions() )
702            );
703    }
704
705    /**
706     * Get section header HTML
707     * @param string $header_name
708     * @param int $header_level
709     * @return string
710     */
711    static function headerHTML( $header_name, $header_level = 2 ) {
712        global $wgPageFormsTabIndex;
713
714        $wgPageFormsTabIndex++;
715        $text = "";
716
717        if ( !is_numeric( $header_level ) ) {
718            // The default header level is set to 2
719            $header_level = 2;
720        }
721
722        $header_level = min( $header_level, 6 );
723        $elementName = 'h' . $header_level;
724        $text = Html::rawElement( $elementName, [], $header_name );
725        return $text;
726    }
727
728    /**
729     * Get the changed index if a new template or section was
730     * inserted before the end, or one was deleted in the form
731     * @param int $i
732     * @param int|null $new_item_loc
733     * @param int|null $deleted_item_loc
734     * @return int
735     */
736    static function getChangedIndex( $i, $new_item_loc, $deleted_item_loc ) {
737        $old_i = $i;
738        if ( $new_item_loc != null ) {
739            if ( $i > $new_item_loc ) {
740                $old_i = $i - 1;
741            } elseif ( $i == $new_item_loc ) {
742                // it's the new template; it shouldn't
743                // get any query-string data
744                $old_i = -1;
745            }
746        } elseif ( $deleted_item_loc != null ) {
747            if ( $i >= $deleted_item_loc ) {
748                $old_i = $i + 1;
749            }
750        }
751        return $old_i;
752    }
753
754    static function setShowOnSelect( $showOnSelectVals, $inputID, $isCheckbox = false ) {
755        global $wgPageFormsShowOnSelect;
756
757        foreach ( $showOnSelectVals as $divID => $options ) {
758            // A checkbox will just have div ID(s).
759            $data = $isCheckbox ? $divID : [ $options, $divID ];
760            if ( array_key_exists( $inputID, $wgPageFormsShowOnSelect ) ) {
761                $wgPageFormsShowOnSelect[$inputID][] = $data;
762            } else {
763                $wgPageFormsShowOnSelect[$inputID] = [ $data ];
764            }
765        }
766    }
767}