Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 356
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 / 356
0.00% covered (danger)
0.00%
0 / 26
9702
0.00% covered (danger)
0.00%
0 / 1
 unhandledFieldsHTML
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 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 / 45
0.00% covered (danger)
0.00%
0 / 1
240
 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 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 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     * @return string
25     */
26    static function unhandledFieldsHTML( $template_in_form ) {
27        // This shouldn't happen, but sometimes this value is null.
28        // @TODO - fix the code that calls this function so the
29        // value is never null.
30        if ( $template_in_form === null ) {
31            return '';
32        }
33
34        // HTML element names shouldn't contain spaces
35        $templateName = str_replace( ' ', '_', $template_in_form->getTemplateName() );
36        $text = "";
37        foreach ( $template_in_form->getValuesFromPage() as $key => $value ) {
38            if ( $key !== null && !is_numeric( $key ) ) {
39                $key = urlencode( $key );
40                $text .= Html::hidden( '_unhandled_' . $templateName . '_' . $key, $value );
41            }
42        }
43        return $text;
44    }
45
46    static function summaryInputHTML( $is_disabled, $label = null, $attr = [], $value = '' ) {
47        global $wgPageFormsTabIndex;
48
49        if ( $label == null ) {
50            $label = wfMessage( 'summary' )->text();
51        }
52
53        $wgPageFormsTabIndex++;
54        $attr += [
55            'tabIndex' => $wgPageFormsTabIndex,
56            'value' => $value,
57            'name' => 'wpSummary',
58            'id' => 'wpSummary',
59            'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
60            'title' => wfMessage( 'tooltip-summary' )->text(),
61            'accessKey' => wfMessage( 'accesskey-summary' )->text()
62        ];
63        if ( $is_disabled ) {
64            $attr['disabled'] = true;
65        }
66        if ( array_key_exists( 'class', $attr ) ) {
67            $attr['classes'] = [ $attr['class'] ];
68        }
69
70        $text = new OOUI\FieldLayout(
71            new OOUI\TextInputWidget( $attr ),
72            [
73                'align' => 'top',
74                'label' => $label
75            ]
76        );
77
78        return $text;
79    }
80
81    static function minorEditInputHTML( $form_submitted, $is_disabled, $is_checked, $label = null, $attrs = [] ) {
82        global $wgPageFormsTabIndex;
83
84        $wgPageFormsTabIndex++;
85        if ( !$form_submitted ) {
86            $user = RequestContext::getMain()->getUser();
87            $is_checked = MediaWikiServices::getInstance()->getUserOptionsLookup()
88                ->getOption( $user, 'minordefault' );
89        }
90
91        if ( $label == null ) {
92            $label = wfMessage( 'minoredit' )->parse();
93        }
94
95        $attrs += [
96            'id' => 'wpMinoredit',
97            'name' => 'wpMinoredit',
98            'accessKey' => wfMessage( 'accesskey-minoredit' )->text(),
99            'tabIndex' => $wgPageFormsTabIndex,
100        ];
101        if ( $is_checked ) {
102            $attrs['selected'] = true;
103        }
104        if ( $is_disabled ) {
105            $attrs['disabled'] = true;
106        }
107        // @phan-suppress-next-line PhanImpossibleTypeComparison
108        if ( array_key_exists( 'class', $attrs ) ) {
109            $attrs['classes'] = [ $attrs['class'] ];
110        }
111
112        // We can't use OOUI\FieldLayout here, because it will make the display too wide.
113        $labelWidget = new OOUI\LabelWidget( [
114            'label' => new OOUI\HtmlSnippet( $label )
115        ] );
116        $text = Html::rawElement(
117            'label',
118            [ 'title' => wfMessage( 'tooltip-minoredit' )->parse() ],
119            new OOUI\CheckboxInputWidget( $attrs ) . $labelWidget
120        );
121        $text = Html::rawElement( 'div', [ 'style' => 'display: inline-block; padding: 12px 16px 12px 0;' ], $text );
122
123        return $text;
124    }
125
126    static function watchInputHTML( $form_submitted, $is_disabled, $is_checked = false, $label = null, $attrs = [] ) {
127        global $wgPageFormsTabIndex, $wgTitle;
128
129        $wgPageFormsTabIndex++;
130        // figure out if the checkbox should be checked -
131        // this code borrowed from /includes/EditPage.php
132        if ( !$form_submitted ) {
133            $user = RequestContext::getMain()->getUser();
134            $services = MediaWikiServices::getInstance();
135            $userOptionsLookup = $services->getUserOptionsLookup();
136            if ( method_exists( \MediaWiki\Watchlist\WatchlistManager::class, 'isWatched' ) ) {
137                // MediaWiki 1.37+
138                $watchlistManager = $services->getWatchlistManager();
139                if ( $userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
140                    # Watch all edits
141                    $is_checked = true;
142                } elseif ( $userOptionsLookup->getOption( $user, 'watchcreations' ) &&
143                    !$wgTitle->exists() ) {
144                    # Watch creations
145                    $is_checked = true;
146                } elseif ( $watchlistManager->isWatched( $user, $wgTitle ) ) {
147                    # Already watched
148                    $is_checked = true;
149                }
150            } else {
151                if ( $userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
152                    # Watch all edits
153                    $is_checked = true;
154                } elseif ( $userOptionsLookup->getOption( $user, 'watchcreations' ) &&
155                    !$wgTitle->exists() ) {
156                    # Watch creations
157                    $is_checked = true;
158                } elseif ( $user->isWatched( $wgTitle ) ) {
159                    # Already watched
160                    $is_checked = true;
161                }
162            }
163        }
164        if ( $label == null ) {
165            $label = wfMessage( 'watchthis' )->parse();
166        }
167        $attrs += [
168            'id' => 'wpWatchthis',
169            'name' => 'wpWatchthis',
170            'accessKey' => wfMessage( 'accesskey-watch' )->text(),
171            'tabIndex' => $wgPageFormsTabIndex,
172        ];
173        if ( $is_checked ) {
174            $attrs['selected'] = true;
175        }
176        if ( $is_disabled ) {
177            $attrs['disabled'] = true;
178        }
179        // @phan-suppress-next-line PhanImpossibleTypeComparison
180        if ( array_key_exists( 'class', $attrs ) ) {
181            $attrs['classes'] = [ $attrs['class'] ];
182        }
183
184        // We can't use OOUI\FieldLayout here, because it will make the display too wide.
185        $labelWidget = new OOUI\LabelWidget( [
186            'label' => new OOUI\HtmlSnippet( $label )
187        ] );
188        $text = Html::rawElement(
189            'label',
190            [ 'title' => wfMessage( 'tooltip-watch' )->parse() ],
191            new OOUI\CheckboxInputWidget( $attrs ) . $labelWidget
192        );
193        $text = Html::rawElement( 'div', [ 'style' => 'display: inline-block; padding: 12px 16px 12px 0;' ], $text );
194
195        return $text;
196    }
197
198    /**
199     * Helper function to display a simple button
200     * @param string $name
201     * @param string $value
202     * @param string $type
203     * @param array $attrs
204     * @return ButtonInputWidget
205     */
206    private static function buttonHTML( $name, $value, $type, $attrs ) {
207        $attrs += [
208            'type' => $type,
209            'name' => $name,
210            'label' => $value
211        ];
212        $button = new ButtonInputWidget( $attrs );
213        // Special handling for 'class'.
214        if ( isset( $attrs['class'] ) ) {
215            // Make sure it's an array.
216            if ( is_string( $attrs['class'] ) ) {
217                $attrs['class'] = [ $attrs['class'] ];
218            }
219            $button->addClasses( $attrs['class'] );
220        }
221        return $button;
222    }
223
224    static function saveButtonHTML( $is_disabled, $label = null, $attr = [] ) {
225        global $wgPageFormsTabIndex;
226
227        $wgPageFormsTabIndex++;
228        if ( $label == null ) {
229            $label = wfMessage( 'savearticle' )->text();
230        }
231        $temp = $attr + [
232            'id'        => 'wpSave',
233            'tabIndex'  => $wgPageFormsTabIndex,
234            'accessKey' => wfMessage( 'accesskey-save' )->text(),
235            'title'     => wfMessage( 'tooltip-save' )->text(),
236            'flags'     => [ 'primary', 'progressive' ]
237        ];
238        if ( $is_disabled ) {
239            $temp['disabled'] = true;
240        }
241        return self::buttonHTML( 'wpSave', $label, 'submit', $temp );
242    }
243
244    static function saveAndContinueButtonHTML( $is_disabled, $label = null, $attr = [] ) {
245        global $wgPageFormsTabIndex;
246
247        $wgPageFormsTabIndex++;
248
249        if ( $label == null ) {
250            $label = wfMessage( 'pf_formedit_saveandcontinueediting' )->text();
251        }
252
253        $temp = $attr + [
254            'id'        => 'wpSaveAndContinue',
255            'tabIndex'  => $wgPageFormsTabIndex,
256            'disabled'  => true,
257            'accessKey' => wfMessage( 'pf_formedit_accesskey_saveandcontinueediting' )->text(),
258            'title'     => wfMessage( 'pf_formedit_tooltip_saveandcontinueediting' )->text(),
259        ];
260
261        if ( $is_disabled ) {
262            $temp['class'] = 'pf-save_and_continue disabled';
263        } else {
264            $temp['class'] = 'pf-save_and_continue';
265        }
266
267        return self::buttonHTML( 'wpSaveAndContinue', $label, 'button', $temp );
268    }
269
270    static function showPreviewButtonHTML( $is_disabled, $label = null, $attr = [] ) {
271        global $wgPageFormsTabIndex;
272
273        $wgPageFormsTabIndex++;
274        if ( $label == null ) {
275            $label = wfMessage( 'showpreview' )->text();
276        }
277        $temp = $attr + [
278            'id'        => 'wpPreview',
279            'tabIndex'  => $wgPageFormsTabIndex,
280            'accessKey' => wfMessage( 'accesskey-preview' )->text(),
281            'title'     => wfMessage( 'tooltip-preview' )->text(),
282        ];
283        if ( $is_disabled ) {
284            $temp['disabled'] = true;
285        }
286        return self::buttonHTML( 'wpPreview', $label, 'submit', $temp );
287    }
288
289    static function showChangesButtonHTML( $is_disabled, $label = null, $attr = [] ) {
290        global $wgPageFormsTabIndex;
291
292        $wgPageFormsTabIndex++;
293        if ( $label == null ) {
294            $label = wfMessage( 'showdiff' )->text();
295        }
296        $temp = $attr + [
297            'id'        => 'wpDiff',
298            'tabIndex'  => $wgPageFormsTabIndex,
299            'accessKey' => wfMessage( 'accesskey-diff' )->text(),
300            'title'     => wfMessage( 'tooltip-diff' )->text(),
301        ];
302        if ( $is_disabled ) {
303            $temp['disabled'] = true;
304        }
305        return self::buttonHTML( 'wpDiff', $label, 'submit', $temp );
306    }
307
308    static function cancelLinkHTML( $is_disabled, $label = null, $attr = [] ) {
309        global $wgTitle;
310
311        if ( $label == null ) {
312            $label = wfMessage( 'cancel' )->parse();
313        }
314        $attr['classes'] = [];
315        if ( $wgTitle == null || $wgTitle->isSpecial( 'FormEdit' ) ) {
316            $attr['classes'][] = 'pfSendBack';
317        } else {
318            $attr['href'] = $wgTitle->getFullURL();
319        }
320        $attr['framed'] = false;
321        $attr['label'] = $label;
322        $attr['flags'] = [ 'destructive' ];
323        if ( array_key_exists( 'class', $attr ) ) {
324            $attr['classes'][] = $attr['class'];
325        }
326
327        return "\t\t" . new OOUI\ButtonWidget( $attr ) . "\n";
328    }
329
330    static function runQueryButtonHTML( $is_disabled = false, $label = null, $attr = [] ) {
331        // is_disabled is currently ignored
332        global $wgPageFormsTabIndex;
333
334        $wgPageFormsTabIndex++;
335        if ( $label == null ) {
336            $label = wfMessage( 'runquery' )->text();
337        }
338        $buttonHTML = self::buttonHTML( 'wpRunQuery', $label, 'submit',
339            $attr + [
340            'id' => 'wpRunQuery',
341            'tabIndex' => $wgPageFormsTabIndex,
342            'title' => $label,
343            'flags' => [ 'primary', 'progressive' ],
344            'icon' => 'search'
345        ] );
346        return new OOUI\FieldLayout( $buttonHTML );
347    }
348
349    /**
350     * Much of this function is based on MediaWiki's EditPage::showEditForm().
351     * @param bool $form_submitted
352     * @param bool $is_disabled
353     * @return string
354     */
355    static function formBottom( $form_submitted, $is_disabled ) {
356        $text = <<<END
357    <br />
358    <div class='editOptions'>
359
360END;
361        $req = RequestContext::getMain()->getRequest();
362        $summary = $req->getVal( 'wpSummary' );
363        $text .= self::summaryInputHTML( $is_disabled, null, [], $summary );
364        $user = RequestContext::getMain()->getUser();
365        if ( $user->isAllowed( 'minoredit' ) ) {
366            $text .= self::minorEditInputHTML( $form_submitted, $is_disabled, false );
367        }
368
369        $userIsRegistered = $user->isRegistered();
370        if ( $userIsRegistered ) {
371            $text .= self::watchInputHTML( $form_submitted, $is_disabled );
372        }
373
374        $text .= <<<END
375    <br />
376    <div class='editButtons'>
377
378END;
379        $text .= self::saveButtonHTML( $is_disabled );
380        $text .= self::showPreviewButtonHTML( $is_disabled );
381        $text .= self::showChangesButtonHTML( $is_disabled );
382        $text .= self::cancelLinkHTML( $is_disabled );
383        $text .= <<<END
384    </div><!-- editButtons -->
385    </div><!-- editOptions -->
386
387END;
388        return $text;
389    }
390
391    /**
392     * Loosely based on MediaWiki's EditPage::getPreloadedContent().
393     *
394     * @param string $preload
395     * @return string
396     */
397    static function getPreloadedText( $preload ) {
398        if ( $preload === '' ) {
399            return '';
400        }
401
402        $preloadTitle = Title::newFromText( $preload );
403        if ( !isset( $preloadTitle ) ) {
404            return '';
405        }
406
407        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
408        $user = RequestContext::getMain()->getUser();
409        if ( !$permissionManager->userCan( 'read', $user, $preloadTitle ) ) {
410            return '';
411        }
412
413        $text = PFUtils::getPageText( $preloadTitle );
414        // Remove <noinclude> sections and <includeonly> tags from text
415        $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text );
416        $text = strtr( $text, [ '<includeonly>' => '', '</includeonly>' => '' ] );
417        return $text;
418    }
419
420    /**
421     * Used by 'RunQuery' page
422     * @return string
423     */
424    static function queryFormBottom() {
425        return self::runQueryButtonHTML( false );
426    }
427
428    static function getMonthNames() {
429        return [
430            wfMessage( 'january' )->inContentLanguage()->text(),
431            wfMessage( 'february' )->inContentLanguage()->text(),
432            wfMessage( 'march' )->inContentLanguage()->text(),
433            wfMessage( 'april' )->inContentLanguage()->text(),
434            // Needed to avoid using 3-letter abbreviation
435            wfMessage( 'may_long' )->inContentLanguage()->text(),
436            wfMessage( 'june' )->inContentLanguage()->text(),
437            wfMessage( 'july' )->inContentLanguage()->text(),
438            wfMessage( 'august' )->inContentLanguage()->text(),
439            wfMessage( 'september' )->inContentLanguage()->text(),
440            wfMessage( 'october' )->inContentLanguage()->text(),
441            wfMessage( 'november' )->inContentLanguage()->text(),
442            wfMessage( 'december' )->inContentLanguage()->text()
443        ];
444    }
445
446    public static function setGlobalVarsForSpreadsheet() {
447        global $wgPageFormsContLangYes, $wgPageFormsContLangNo, $wgPageFormsContLangMonths;
448
449        // JS variables that hold boolean and date values in the wiki's
450        // (as opposed to the user's) language.
451        $wgPageFormsContLangYes = wfMessage( 'htmlform-yes' )->inContentLanguage()->text();
452        $wgPageFormsContLangNo = wfMessage( 'htmlform-no' )->inContentLanguage()->text();
453        $monthMessages = [
454            "january", "february", "march", "april", "may_long", "june",
455            "july", "august", "september", "october", "november", "december"
456        ];
457        $wgPageFormsContLangMonths = [ '' ];
458        foreach ( $monthMessages as $monthMsg ) {
459            $wgPageFormsContLangMonths[] = wfMessage( $monthMsg )->inContentLanguage()->text();
460        }
461    }
462
463    /**
464     * Parse the form definition and return it
465     * @param Parser $parser
466     * @param string|null $form_def
467     * @param string|null $form_id
468     * @return string
469     */
470    public static function getFormDefinition( Parser $parser, $form_def = null, $form_id = null ) {
471        if ( $form_id !== null ) {
472            $cachedDef = self::getFormDefinitionFromCache( $form_id, $parser );
473
474            if ( $cachedDef !== null ) {
475                return $cachedDef;
476            }
477        }
478
479        if ( $form_def !== null ) {
480            // Do nothing.
481        } elseif ( $form_id !== null ) {
482            $form_title = Title::newFromID( $form_id );
483            $form_def = PFUtils::getPageText( $form_title );
484        } else {
485            // No text, no ID -> no form definition.
486            return '';
487        }
488
489        // Remove <noinclude> sections and <includeonly> tags from form definition
490        $form_def = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $form_def );
491        $form_def = strtr( $form_def, [ '<includeonly>' => '', '</includeonly>' => '' ] );
492
493        // We need to replace all PF tags in the form definition by strip items. But we can not just use
494        // the Parser strip state because the Parser would during parsing replace all strip items and then
495        // mangle them into HTML code. So we have to use our own. Which means we also can not just use
496        // Parser::insertStripItem() (see below).
497        // Also include a quotation mark, to help avoid security leaks.
498        $rnd = wfRandomString( 16 ) . '"' . wfRandomString( 15 );
499
500        // This regexp will find any PF triple braced tags (including correct handling of contained braces), i.e.
501        // {{{field|foo|default={{Bar}}}}} is not a problem. When used with preg_match and friends, $matches[0] will
502        // contain the whole PF tag, $matches[1] will contain the tag without the enclosing triple braces.
503        $regexp = '#\{\{\{((?>[^\{\}]+)|(\{((?>[^\{\}]+)|(?-2))*\}))*\}\}\}#';
504        // Needed to restore highlighting in vi - <?
505
506        $items = [];
507
508        // replace all PF tags by strip markers
509        $form_def = preg_replace_callback(
510            $regexp,
511
512            // This is essentially a copy of Parser::insertStripItem().
513            static function ( array $matches ) use ( &$items, $rnd ) {
514                $markerIndex = count( $items );
515                $items[] = $matches[0];
516                return "$rnd-item-$markerIndex-$rnd";
517            },
518
519            $form_def
520        );
521
522        // Parse wiki-text.
523        // @phan-suppress-next-line PhanRedundantCondition for BC with old MW
524        $title = is_object( $parser->getTitle() ) ? $parser->getTitle() : $form_title;
525        // We need to pass "false" in to the parse() $clearState param so that
526        // embedding Special:RunQuery will work.
527        $output = $parser->parse( $form_def, $title, $parser->getOptions(), true, false );
528        $form_def = $output->getText();
529        $form_def = preg_replace_callback(
530            "/{$rnd}-item-(\d+)-{$rnd}/",
531            static function ( array $matches ) use ( $items ) {
532                $markerIndex = (int)$matches[1];
533                return $items[$markerIndex];
534            },
535            $form_def
536        );
537
538        if ( $output->getCacheTime() == -1 ) {
539            $form_article = Article::newFromID( $form_id );
540            self::purgeCache( $form_article );
541            wfDebug( "Caching disabled for form definition $form_id\n" );
542        } elseif ( $form_id !== null ) {
543            self::cacheFormDefinition( $form_id, $form_def, $parser );
544        }
545
546        return $form_def;
547    }
548
549    /**
550     * Get a form definition from cache
551     * @param string $form_id
552     * @param Parser $parser
553     * @return string|null
554     */
555    protected static function getFormDefinitionFromCache( $form_id, Parser $parser ) {
556        global $wgPageFormsCacheFormDefinitions;
557
558        // use cache if allowed
559        if ( !$wgPageFormsCacheFormDefinitions ) {
560            return null;
561        }
562
563        $cache = self::getFormCache();
564
565        // create a cache key consisting of owner name, article id and user options
566        $cacheKeyForForm = self::getCacheKey( $form_id, $parser );
567
568        $cached_def = $cache->get( $cacheKeyForForm );
569
570        // Cache hit?
571        if ( is_string( $cached_def ) ) {
572            wfDebug( "Cache hit: Got form definition $cacheKeyForForm from cache\n" );
573
574            return $cached_def;
575        }
576
577        wfDebug( "Cache miss: Form definition $cacheKeyForForm not found in cache\n" );
578
579        return null;
580    }
581
582    /**
583     * Store a form definition in cache
584     * @param string $form_id
585     * @param string $form_def
586     * @param Parser $parser
587     */
588    protected static function cacheFormDefinition( $form_id, $form_def, Parser $parser ) {
589        global $wgPageFormsCacheFormDefinitions;
590
591        // Store in cache if requested
592        if ( !$wgPageFormsCacheFormDefinitions ) {
593            return;
594        }
595
596        $cache = self::getFormCache();
597        $cacheKeyForForm = self::getCacheKey( $form_id, $parser );
598        $cacheKeyForList = self::getCacheKey( $form_id );
599
600        // Update list of form definitions
601        $listOfFormKeys = $cache->get( $cacheKeyForList );
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}