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