Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.70% covered (success)
91.70%
994 / 1084
87.50% covered (warning)
87.50%
28 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFFormPrinter
91.70% covered (success)
91.70%
994 / 1084
87.50% covered (warning)
87.50%
28 / 32
528.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
4
 setSemanticTypeHook
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCargoTypeHook
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInputTypeHook
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerInputType
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
13
 getInputType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDefaultInputTypeSMW
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getDefaultInputTypeCargo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getPossibleInputTypesSMW
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getPossibleInputTypesCargo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getAllInputTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 showDeletionLog
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 strReplaceFirst
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 placeholderFormat
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 makePlaceholderInFormHTML
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 multipleTemplateStartHTML
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 multipleTemplateInstanceTableHTML
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 multipleTemplateInstanceHTML
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 multipleTemplateEndHTML
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 tableHTML
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
12
 getSpreadsheetAutocompleteAttributes
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 spreadsheetHTML
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
20
 getStringForCurrentTime
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 getStringFromPassedInArray
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
22
 displayLoadingImage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 formHTML
90.07% covered (success)
90.07%
535 / 594
0.00% covered (danger)
0.00%
0 / 1
327.75
 getCargoBasedMapping
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 formFieldHTML
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
17
 addTranslatableInput
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
24.43
 createFormFieldTranslateTag
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
70.58
 generateUUID
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getParsedValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Handles the creation and running of a user-created form.
4 *
5 * @author Yaron Koren
6 * @author Nils Oppermann
7 * @author Jeffrey Stuckman
8 * @author Harold Solbrig
9 * @author Daniel Hansch
10 * @author Stephan Gambke
11 * @author LY Meng
12 * @file
13 * @ingroup PF
14 */
15
16use MediaWiki\EditPage\EditPage;
17use MediaWiki\Html\Html;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Title\Title;
20
21class PFFormPrinter {
22
23    public const CONTEXT_REGULAR = 0;
24    public const CONTEXT_QUERY = 1;
25    public const CONTEXT_EMBEDDED_QUERY = 2;
26    public const CONTEXT_AUTOEDIT = 3;
27    public const CONTEXT_AUTOCREATE = 4;
28
29    public $mSemanticTypeHooks;
30    public $mCargoTypeHooks;
31    public $mInputTypeHooks;
32    public $standardInputsIncluded;
33    public $mPageTitle;
34
35    private $mInputTypeClasses;
36    private $mDefaultInputForPropType;
37    private $mDefaultInputForPropTypeList;
38    private $mPossibleInputsForPropType;
39    private $mPossibleInputsForPropTypeList;
40    private $mDefaultInputForCargoType;
41    private $mDefaultInputForCargoTypeList;
42    private $mPossibleInputsForCargoType;
43    private $mPossibleInputsForCargoTypeList;
44
45    private static $mParsedValues = [];
46
47    public function __construct() {
48        global $wgPageFormsDisableOutsideServices;
49        // Initialize variables.
50        $this->mSemanticTypeHooks = [];
51        $this->mCargoTypeHooks = [];
52        $this->mInputTypeHooks = [];
53        $this->mInputTypeClasses = [];
54        $this->mDefaultInputForPropType = [];
55        $this->mDefaultInputForPropTypeList = [];
56        $this->mPossibleInputsForPropType = [];
57        $this->mPossibleInputsForPropTypeList = [];
58        $this->mDefaultInputForCargoType = [];
59        $this->mDefaultInputForCargoTypeList = [];
60        $this->mPossibleInputsForCargoType = [];
61        $this->mPossibleInputsForCargoTypeList = [];
62
63        $this->standardInputsIncluded = false;
64
65        $this->registerInputType( 'PFTextInput' );
66        $this->registerInputType( 'PFTextWithAutocompleteInput' );
67        $this->registerInputType( 'PFTextAreaInput' );
68        $this->registerInputType( 'PFTextAreaWithAutocompleteInput' );
69        $this->registerInputType( 'PFDateInput' );
70        $this->registerInputType( 'PFStartDateInput' );
71        $this->registerInputType( 'PFEndDateInput' );
72        $this->registerInputType( 'PFDatePickerInput' );
73        $this->registerInputType( 'PFDateTimePicker' );
74        $this->registerInputType( 'PFDateTimeInput' );
75        $this->registerInputType( 'PFStartDateTimeInput' );
76        $this->registerInputType( 'PFEndDateTimeInput' );
77        $this->registerInputType( 'PFYearInput' );
78        $this->registerInputType( 'PFCheckboxInput' );
79        $this->registerInputType( 'PFDropdownInput' );
80        $this->registerInputType( 'PFRadioButtonInput' );
81        $this->registerInputType( 'PFCheckboxesInput' );
82        $this->registerInputType( 'PFListBoxInput' );
83        $this->registerInputType( 'PFComboBoxInput' );
84        $this->registerInputType( 'PFTreeInput' );
85        $this->registerInputType( 'PFTokensInput' );
86        $this->registerInputType( 'PFRegExpInput' );
87        $this->registerInputType( 'PFRatingInput' );
88        // Add this if the Semantic Maps extension is not
89        // included, or if it's SM (really Maps) v4.0 or higher.
90        if ( !$wgPageFormsDisableOutsideServices ) {
91            if ( !defined( 'SM_VERSION' ) || version_compare( SM_VERSION, '4.0', '>=' ) ) {
92                $this->registerInputType( 'PFGoogleMapsInput' );
93            }
94            $this->registerInputType( 'PFOpenLayersInput' );
95            $this->registerInputType( 'PFLeafletInput' );
96        }
97
98        // All-purpose setup hook.
99        // Avoid PHP 7.1 warning from passing $this by reference.
100        $formPrinterRef = $this;
101        MediaWikiServices::getInstance()->getHookContainer()->run( 'PageForms::FormPrinterSetup', [ &$formPrinterRef ] );
102    }
103
104    public function setSemanticTypeHook( $type, $is_list, $class_name, $default_args ) {
105        $this->mSemanticTypeHooks[$type][$is_list] = [ $class_name, $default_args ];
106    }
107
108    public function setCargoTypeHook( $type, $is_list, $class_name, $default_args ) {
109        $this->mCargoTypeHooks[$type][$is_list] = [ $class_name, $default_args ];
110    }
111
112    public function setInputTypeHook( $input_type, $class_name, $default_args ) {
113        $this->mInputTypeHooks[$input_type] = [ $class_name, $default_args ];
114    }
115
116    /**
117     * Register all information about the passed-in form input class.
118     *
119     * @param string $inputTypeClass The full qualified class name representing the new input.
120     * Must be derived from PFFormInput.
121     */
122    public function registerInputType( $inputTypeClass ) {
123        $inputTypeName = call_user_func( [ $inputTypeClass, 'getName' ] );
124        $this->mInputTypeClasses[$inputTypeName] = $inputTypeClass;
125        $this->setInputTypeHook( $inputTypeName, $inputTypeClass, [] );
126
127        $defaultProperties = call_user_func( [ $inputTypeClass, 'getDefaultPropTypes' ] );
128        foreach ( $defaultProperties as $propertyType => $additionalValues ) {
129            $this->setSemanticTypeHook( $propertyType, false, $inputTypeClass, $additionalValues );
130            $this->mDefaultInputForPropType[$propertyType] = $inputTypeName;
131        }
132        $defaultPropertyLists = call_user_func( [ $inputTypeClass, 'getDefaultPropTypeLists' ] );
133        foreach ( $defaultPropertyLists as $propertyType => $additionalValues ) {
134            $this->setSemanticTypeHook( $propertyType, true, $inputTypeClass, $additionalValues );
135            $this->mDefaultInputForPropTypeList[$propertyType] = $inputTypeName;
136        }
137
138        $defaultCargoTypes = call_user_func( [ $inputTypeClass, 'getDefaultCargoTypes' ] );
139        foreach ( $defaultCargoTypes as $fieldType => $additionalValues ) {
140            $this->setCargoTypeHook( $fieldType, false, $inputTypeClass, $additionalValues );
141            $this->mDefaultInputForCargoType[$fieldType] = $inputTypeName;
142        }
143        $defaultCargoTypeLists = call_user_func( [ $inputTypeClass, 'getDefaultCargoTypeLists' ] );
144        foreach ( $defaultCargoTypeLists as $fieldType => $additionalValues ) {
145            $this->setCargoTypeHook( $fieldType, true, $inputTypeClass, $additionalValues );
146            $this->mDefaultInputForCargoTypeList[$fieldType] = $inputTypeName;
147        }
148
149        $otherProperties = call_user_func( [ $inputTypeClass, 'getOtherPropTypesHandled' ] );
150        foreach ( $otherProperties as $propertyTypeID ) {
151            if ( array_key_exists( $propertyTypeID, $this->mPossibleInputsForPropType ) ) {
152                $this->mPossibleInputsForPropType[$propertyTypeID][] = $inputTypeName;
153            } else {
154                $this->mPossibleInputsForPropType[$propertyTypeID] = [ $inputTypeName ];
155            }
156        }
157        $otherPropertyLists = call_user_func( [ $inputTypeClass, 'getOtherPropTypeListsHandled' ] );
158        foreach ( $otherPropertyLists as $propertyTypeID ) {
159            if ( array_key_exists( $propertyTypeID, $this->mPossibleInputsForPropTypeList ) ) {
160                $this->mPossibleInputsForPropTypeList[$propertyTypeID][] = $inputTypeName;
161            } else {
162                $this->mPossibleInputsForPropTypeList[$propertyTypeID] = [ $inputTypeName ];
163            }
164        }
165
166        $otherCargoTypes = call_user_func( [ $inputTypeClass, 'getOtherCargoTypesHandled' ] );
167        foreach ( $otherCargoTypes as $cargoType ) {
168            if ( array_key_exists( $cargoType, $this->mPossibleInputsForCargoType ) ) {
169                $this->mPossibleInputsForCargoType[$cargoType][] = $inputTypeName;
170            } else {
171                $this->mPossibleInputsForCargoType[$cargoType] = [ $inputTypeName ];
172            }
173        }
174        $otherCargoTypeLists = call_user_func( [ $inputTypeClass, 'getOtherCargoTypeListsHandled' ] );
175        foreach ( $otherCargoTypeLists as $cargoType ) {
176            if ( array_key_exists( $cargoType, $this->mPossibleInputsForCargoTypeList ) ) {
177                $this->mPossibleInputsForCargoTypeList[$cargoType][] = $inputTypeName;
178            } else {
179                $this->mPossibleInputsForCargoTypeList[$cargoType] = [ $inputTypeName ];
180            }
181        }
182
183        // FIXME: No need to register these functions explicitly. Instead
184        // formFieldHTML should call $someInput -> getJsInitFunctionData() and
185        // store its return value. formHTML should at some (late) point use the
186        // stored data.
187        //
188        // $initJSFunction = call_user_func( array( $inputTypeClass, 'getJsInitFunctionData' ) );
189        // if ( !is_null( $initJSFunction ) ) {
190        //     $wgPageFormsInitJSFunctions[] = $initJSFunction;
191        // }
192        //
193        // $validationJSFunctions = call_user_func( array( $inputTypeClass, 'getJsValidationFunctionData' ) );
194        // if ( count( $validationJSFunctions ) > 0 ) {
195        //     $wgPageFormsValidationJSFunctions = array_merge( $wgPageFormsValidationJSFunctions, $initJSFunction );
196        // }
197    }
198
199    public function getInputType( $inputTypeName ) {
200        if ( array_key_exists( $inputTypeName, $this->mInputTypeClasses ) ) {
201            return $this->mInputTypeClasses[$inputTypeName];
202        } else {
203            return null;
204        }
205    }
206
207    public function getDefaultInputTypeSMW( $isList, $propertyType ) {
208        if ( $isList ) {
209            if ( array_key_exists( $propertyType, $this->mDefaultInputForPropTypeList ) ) {
210                return $this->mDefaultInputForPropTypeList[$propertyType];
211            } else {
212                return null;
213            }
214        } else {
215            if ( array_key_exists( $propertyType, $this->mDefaultInputForPropType ) ) {
216                return $this->mDefaultInputForPropType[$propertyType];
217            } else {
218                return null;
219            }
220        }
221    }
222
223    public function getDefaultInputTypeCargo( $isList, $fieldType ) {
224        if ( $isList ) {
225            if ( array_key_exists( $fieldType, $this->mDefaultInputForCargoTypeList ) ) {
226                return $this->mDefaultInputForCargoTypeList[$fieldType];
227            } else {
228                return null;
229            }
230        } else {
231            if ( array_key_exists( $fieldType, $this->mDefaultInputForCargoType ) ) {
232                return $this->mDefaultInputForCargoType[$fieldType];
233            } else {
234                return null;
235            }
236        }
237    }
238
239    public function getPossibleInputTypesSMW( $isList, $propertyType ) {
240        if ( $isList ) {
241            if ( array_key_exists( $propertyType, $this->mPossibleInputsForPropTypeList ) ) {
242                return $this->mPossibleInputsForPropTypeList[$propertyType];
243            } else {
244                return [];
245            }
246        } else {
247            if ( array_key_exists( $propertyType, $this->mPossibleInputsForPropType ) ) {
248                return $this->mPossibleInputsForPropType[$propertyType];
249            } else {
250                return [];
251            }
252        }
253    }
254
255    public function getPossibleInputTypesCargo( $isList, $fieldType ) {
256        if ( $isList ) {
257            if ( array_key_exists( $fieldType, $this->mPossibleInputsForCargoTypeList ) ) {
258                return $this->mPossibleInputsForCargoTypeList[$fieldType];
259            } else {
260                return [];
261            }
262        } else {
263            if ( array_key_exists( $fieldType, $this->mPossibleInputsForCargoType ) ) {
264                return $this->mPossibleInputsForCargoType[$fieldType];
265            } else {
266                return [];
267            }
268        }
269    }
270
271    public function getAllInputTypes() {
272        return array_keys( $this->mInputTypeClasses );
273    }
274
275    /**
276     * Show the set of previous deletions for the page being edited.
277     * @param OutputPage $out
278     * @return true
279     */
280    function showDeletionLog( $out ) {
281        LogEventsList::showLogExtract( $out, 'delete', $this->mPageTitle->getPrefixedText(),
282            '', [ 'lim' => 10,
283                'conds' => [ "log_action != 'revision'" ],
284                'showIfEmpty' => false,
285                'msgKey' => [ 'moveddeleted-notice' ] ]
286        );
287        return true;
288    }
289
290    /**
291     * Like PHP's str_replace(), but only replaces the first found
292     * instance - unfortunately, str_replace() doesn't allow for that.
293     * This code is basically copied directly from
294     * http://www.php.net/manual/en/function.str-replace.php#86177
295     * - this might make sense in the PFUtils class, if it's useful in
296     * other places.
297     * @param string $search
298     * @param string $replace
299     * @param string $subject
300     * @return string
301     */
302    function strReplaceFirst( $search, $replace, $subject ) {
303        $firstChar = strpos( $subject, $search );
304        if ( $firstChar !== false ) {
305            $beforeStr = substr( $subject, 0, $firstChar );
306            $afterStr = substr( $subject, $firstChar + strlen( $search ) );
307            return $beforeStr . $replace . $afterStr;
308        } else {
309            return $subject;
310        }
311    }
312
313    static function placeholderFormat( $templateName, $fieldName ) {
314        $templateName = str_replace( '_', ' ', $templateName );
315        $fieldName = str_replace( '_', ' ', $fieldName );
316        return $templateName . '___' . $fieldName;
317    }
318
319    static function makePlaceholderInFormHTML( $str ) {
320        return '@insert"HTML_' . $str . '@';
321    }
322
323    function multipleTemplateStartHTML( $tif ) {
324        // If placeholder is set, it means we want to insert a
325        // multiple template form's HTML into the main form's HTML.
326        // So, the HTML will be stored in $text.
327        $text = "\t" . '<div class="multipleTemplateWrapper">' . "\n";
328        $attrs = [ 'class' => 'multipleTemplateList' ];
329        if ( $tif->getMinInstancesAllowed() !== null ) {
330            $attrs['minimumInstances'] = $tif->getMinInstancesAllowed();
331        }
332        if ( $tif->getMaxInstancesAllowed() !== null ) {
333            $attrs['maximumInstances'] = $tif->getMaxInstancesAllowed();
334        }
335        if ( $tif->getDisplayedFieldsWhenMinimized() != null ) {
336            $attrs['data-displayed-fields-when-minimized'] = $tif->getDisplayedFieldsWhenMinimized();
337        }
338        $text .= "\t" . Html::openElement( 'div', $attrs ) . "\n";
339        return $text;
340    }
341
342    /**
343     * Creates the HTML for the inner table for every instance of a
344     * multiple-instance template in the form.
345     * @param bool $form_is_disabled
346     * @param string $mainText
347     * @return string
348     */
349    function multipleTemplateInstanceTableHTML( $form_is_disabled, $mainText ) {
350        if ( $form_is_disabled ) {
351            $addAboveButton = $removeButton = '';
352        } else {
353            $addAboveButton = Html::element( 'a', [ 'class' => "addAboveButton", 'title' => wfMessage( 'pf_formedit_addanotherabove' )->text() ] );
354            $removeButton = Html::element( 'a', [ 'class' => "removeButton", 'title' => wfMessage( 'pf_formedit_remove' )->text() ] );
355        }
356
357        $text = <<<END
358            <table class="multipleTemplateInstanceTable">
359            <tr>
360            <td class="instanceRearranger"></td>
361            <td class="instanceMain">$mainText</td>
362            <td class="instanceAddAbove">$addAboveButton</td>
363            <td class="instanceRemove">$removeButton</td>
364            </tr>
365            </table>
366END;
367
368        return $text;
369    }
370
371    /**
372     * Creates the HTML for a single instance of a multiple-instance
373     * template.
374     * @param PFTemplateInForm $template_in_form
375     * @param bool $form_is_disabled
376     * @param string &$section
377     * @return string
378     */
379    function multipleTemplateInstanceHTML( $template_in_form, $form_is_disabled, &$section ) {
380        global $wgPageFormsCalendarHTML;
381
382        $wgPageFormsCalendarHTML[$template_in_form->getTemplateName()] = str_replace( '[num]', "[cf]", $section );
383
384        // Add the character "a" onto the instance number of this input
385        // in the form, to differentiate the inputs the form starts out
386        // with from any inputs added by the Javascript.
387        $section = str_replace( '[num]', "[{$template_in_form->getInstanceNum()}a]", $section );
388        // @TODO - this replacement should be
389        // case- and spacing-insensitive.
390        // Also, keeping the "id=" attribute should not be
391        // necessary; but currently it is, for "show on select".
392        $section = preg_replace_callback(
393            '/ id="(.*?)"/',
394            static function ( $matches ) {
395                $id = htmlspecialchars( $matches[1], ENT_QUOTES );
396                return " id=\"$id\" data-origID=\"$id\" ";
397            },
398            $section
399        );
400
401        $text = "\t\t" . Html::rawElement( 'div',
402                [
403                // The "multipleTemplate" class is there for
404                // backwards-compatibility with any custom CSS on people's
405                // wikis before PF 2.0.9.
406                'class' => "multipleTemplateInstance multipleTemplate"
407            ],
408            $this->multipleTemplateInstanceTableHTML( $form_is_disabled, $section )
409        ) . "\n";
410
411        return $text;
412    }
413
414    /**
415     * Creates the end of the HTML for a multiple-instance template -
416     * including the sections necessary for adding additional instances.
417     * @param PFTemplateInForm $template_in_form
418     * @param bool $form_is_disabled
419     * @param string $section
420     * @return string
421     */
422    function multipleTemplateEndHTML( $template_in_form, $form_is_disabled, $section ) {
423        global $wgPageFormsTabIndex;
424
425        $text = "\t\t" . Html::rawElement( 'div',
426            [
427                'class' => "multipleTemplateStarter",
428                'style' => "display: none",
429            ],
430            $this->multipleTemplateInstanceTableHTML( $form_is_disabled, $section )
431        ) . "\n";
432
433        $attributes = [
434            'tabIndex' => $wgPageFormsTabIndex,
435            'classes' => [ 'multipleTemplateAdder' ],
436            'label' => Sanitizer::decodeCharReferences( $template_in_form->getAddButtonText() ),
437            'icon' => 'add'
438        ];
439        if ( $form_is_disabled ) {
440            $attributes['disabled'] = true;
441            $attributes['classes'] = [];
442        }
443        $button = new OOUI\ButtonWidget( $attributes );
444        $text .= <<<END
445    </div><!-- multipleTemplateList -->
446        <p>$button</p>
447        <div class="pfErrorMessages"></div>
448    </div><!-- multipleTemplateWrapper -->
449</fieldset>
450END;
451        return $text;
452    }
453
454    function tableHTML( $tif, $instanceNum ) {
455        global $wgPageFormsFieldNum;
456
457        $allGridValues = $tif->getGridValues();
458        if ( array_key_exists( $instanceNum, $allGridValues ) ) {
459            $gridValues = $allGridValues[$instanceNum];
460        } else {
461            $gridValues = null;
462        }
463
464        $html = '';
465        foreach ( $tif->getFields() as $formField ) {
466            $fieldName = $formField->getTemplateField()->getFieldName();
467            if ( $gridValues == null ) {
468                $curValue = null;
469            } else {
470                $curValue = $gridValues[$fieldName];
471            }
472
473            if ( $formField->holdsTemplate() ) {
474                $attribs = [];
475                if ( $formField->hasFieldArg( 'class' ) ) {
476                    $attribs['class'] = $formField->getFieldArg( 'class' );
477                }
478                $html .= '</table>' . "\n";
479                $html .= Html::hidden( $formField->getInputName(), $curValue, $attribs );
480                $html .= $formField->additionalHTMLForInput( $curValue, $fieldName, $tif->getTemplateName() );
481                $html .= '<table class="formtable">' . "\n";
482                continue;
483            }
484
485            if ( $formField->isHidden() ) {
486                $attribs = [];
487                if ( $formField->hasFieldArg( 'class' ) ) {
488                    $attribs['class'] = $formField->getFieldArg( 'class' );
489                }
490                $html .= Html::hidden( $formField->getInputName(), $curValue, $attribs );
491                continue;
492            }
493
494            $wgPageFormsFieldNum++;
495            if ( $formField->getLabel() !== null ) {
496                $labelText = $formField->getLabel();
497                // Kind of a @HACK - for a checkbox within
498                // display=table, 'label' is used for two
499                // purposes: the label column, and the text
500                // after the checkbox. Unset the value here so
501                // that it's only used for the first purpose,
502                // and doesn't show up twice.
503                $formField->setFieldArg( 'label', '' );
504            } elseif ( $formField->getLabelMsg() !== null ) {
505                $labelText = wfMessage( $formField->getLabelMsg() )->parse();
506            } elseif ( $formField->getTemplateField()->getLabel() !== null ) {
507                $labelText = $formField->getTemplateField()->getLabel() . ':';
508            } else {
509                $labelText = $fieldName . ': ';
510            }
511            $label = Html::element( 'label',
512                [ 'for' => "input_$wgPageFormsFieldNum" ],
513                $labelText );
514
515            $labelCellAttrs = [];
516            if ( $formField->hasFieldArg( 'tooltip' ) ) {
517                $labelCellAttrs['data-tooltip'] = $formField->getFieldArg( 'tooltip' );
518            }
519
520            $labelCell = Html::rawElement( 'th', $labelCellAttrs, $label );
521            $inputHTML = $this->formFieldHTML( $formField, $curValue );
522            $inputHTML .= $formField->additionalHTMLForInput( $curValue, $fieldName, $tif->getTemplateName() );
523            $inputCell = Html::rawElement( 'td', null, $inputHTML );
524            $html .= Html::rawElement( 'tr', null, $labelCell . $inputCell ) . "\n";
525        }
526
527        $html = Html::rawElement( 'table', [ 'class' => 'formtable' ], $html );
528
529        return $html;
530    }
531
532    function getSpreadsheetAutocompleteAttributes( $formFieldArgs ) {
533        if ( array_key_exists( 'values from category', $formFieldArgs ) ) {
534            return [ 'category', $formFieldArgs[ 'values from category' ] ];
535        } elseif ( array_key_exists( 'cargo table', $formFieldArgs ) ) {
536            $cargo_table = $formFieldArgs[ 'cargo table' ];
537            $cargo_field = $formFieldArgs[ 'cargo field' ];
538            return [ 'cargo field', $cargo_table . '|' . $cargo_field ];
539        } elseif ( array_key_exists( 'values from property', $formFieldArgs ) ) {
540            return [ 'property', $formFieldArgs['values from property'] ];
541        } elseif ( array_key_exists( 'values from concept', $formFieldArgs ) ) {
542            return [ 'concept', $formFieldArgs['values from concept'] ];
543        } elseif ( array_key_exists( 'values dependent on', $formFieldArgs ) ) {
544            return [ 'dep_on', '' ];
545        } elseif ( array_key_exists( 'values from external data', $formFieldArgs ) ) {
546            return [ 'external data', $formFieldArgs['origName'] ];
547        } elseif ( array_key_exists( 'values from wikidata', $formFieldArgs ) ) {
548            return [ 'wikidata', $formFieldArgs['values from wikidata'] ];
549        } else {
550            return [ '', '' ];
551        }
552    }
553
554    function spreadsheetHTML( $tif ) {
555        global $wgOut, $wgPageFormsGridValues, $wgPageFormsGridParams;
556        global $wgPageFormsScriptPath;
557
558        if ( empty( $tif->getFields() ) ) {
559            return;
560        }
561
562        $wgOut->addModules( 'ext.pageforms.spreadsheet' );
563
564        $gridParams = [];
565        foreach ( $tif->getFields() as $formField ) {
566            $templateField = $formField->getTemplateField();
567            $formFieldArgs = $formField->getFieldArgs();
568            $possibleValues = $formField->getPossibleValues();
569
570            $inputType = $formField->getInputType();
571            $gridParamValues = [ 'name' => $templateField->getFieldName() ];
572            [ $autocompletedatatype, $autocompletesettings ] = $this->getSpreadsheetAutocompleteAttributes( $formFieldArgs );
573            if ( $formField->getLabel() !== null ) {
574                $gridParamValues['label'] = $formField->getLabel();
575            }
576            if ( $formField->getDefaultValue() !== null ) {
577                $gridParamValues['default'] = $formField->getDefaultValue();
578            }
579            // currently the spreadsheets in Page Forms doesn't support the tokens input
580            // so it's better to take a default jspreadsheet editor for tokens
581            if ( $formField->isList() || $inputType == 'tokens' ) {
582                $autocompletedatatype = '';
583                $autocompletesettings = '';
584                $gridParamValues['type'] = 'text';
585            } elseif ( !empty( $possibleValues )
586                && $autocompletedatatype != 'category' && $autocompletedatatype != 'cargo field'
587                && $autocompletedatatype != 'concept' && $autocompletedatatype != 'property' ) {
588                $gridParamValues['values'] = $possibleValues;
589                if ( $formField->isList() ) {
590                    $gridParamValues['list'] = true;
591                    $gridParamValues['delimiter'] = $formField->getFieldArg( 'delimiter' );
592                }
593            } elseif ( $inputType == 'textarea' ) {
594                $gridParamValues['type'] = 'textarea';
595            } elseif ( $inputType == 'checkbox' ) {
596                $gridParamValues['type'] = 'checkbox';
597            } elseif ( $inputType == 'date' ) {
598                $gridParamValues['type'] = 'date';
599            } elseif ( $inputType == 'datetime' ) {
600                $gridParamValues['type'] = 'datetime';
601            } elseif ( $possibleValues != null ) {
602                array_unshift( $possibleValues, '' );
603                $completePossibleValues = [];
604                foreach ( $possibleValues as $value ) {
605                    $completePossibleValues[] = [ 'Name' => $value, 'Id' => $value ];
606                }
607                $gridParamValues['type'] = 'select';
608                $gridParamValues['items'] = $completePossibleValues;
609                $gridParamValues['valueField'] = 'Id';
610                $gridParamValues['textField'] = 'Name';
611            } else {
612                $gridParamValues['type'] = 'text';
613            }
614            $gridParamValues['autocompletedatatype'] = $autocompletedatatype;
615            $gridParamValues['autocompletesettings'] = $autocompletesettings;
616            $gridParamValues['inputType'] = $inputType;
617            $gridParams[] = $gridParamValues;
618        }
619
620        $templateName = $tif->getTemplateName();
621        $templateDivID = str_replace( ' ', '', $templateName ) . "Grid";
622        $templateDivAttrs = [
623            'class' => 'pfSpreadsheet',
624            'id' => $templateDivID,
625            'data-template-name' => $templateName
626        ];
627        if ( $tif->getHeight() != null ) {
628            $templateDivAttrs['height'] = $tif->getHeight();
629        }
630
631        $loadingImage = Html::element( 'img', [ 'src' => "$wgPageFormsScriptPath/skins/loading.gif" ] );
632        $loadingImageDiv = '<div class="loadingImage">' . $loadingImage . '</div>';
633        $text = Html::rawElement( 'div', $templateDivAttrs, $loadingImageDiv );
634
635        $wgPageFormsGridParams[$templateName] = $gridParams;
636        $wgPageFormsGridValues[$templateName] = $tif->getGridValues();
637
638        PFFormUtils::setGlobalVarsForSpreadsheet();
639
640        return $text;
641    }
642
643    /**
644     * Get a string representing the current time, for the time zone
645     * specified in the wiki.
646     * @param string $includeTime
647     * @param string $includeTimezone
648     * @return string
649     */
650    function getStringForCurrentTime( $includeTime, $includeTimezone ) {
651        global $wgLocaltimezone, $wgAmericanDates, $wgPageForms24HourTime;
652
653        if ( isset( $wgLocaltimezone ) ) {
654            $serverTimezone = date_default_timezone_get();
655            date_default_timezone_set( $wgLocaltimezone );
656        }
657        $cur_time = time();
658        $year = date( "Y", $cur_time );
659        $month = date( "n", $cur_time );
660        $day = date( "j", $cur_time );
661        if ( $wgAmericanDates == true ) {
662            $month_names = PFFormUtils::getMonthNames();
663            $month_name = $month_names[$month - 1];
664            $curTimeString = "$month_name $day$year";
665        } else {
666            $curTimeString = "$year-$month-$day";
667        }
668        if ( isset( $wgLocaltimezone ) ) {
669            date_default_timezone_set( $serverTimezone );
670        }
671        if ( !$includeTime ) {
672            return $curTimeString;
673        }
674
675        if ( $wgPageForms24HourTime ) {
676            $hour = str_pad( intval( substr( date( "G", $cur_time ), 0, 2 ) ), 2, '0', STR_PAD_LEFT );
677        } else {
678            $hour = str_pad( intval( substr( date( "g", $cur_time ), 0, 2 ) ), 2, '0', STR_PAD_LEFT );
679        }
680        $minute = str_pad( intval( substr( date( "i", $cur_time ), 0, 2 ) ), 2, '0', STR_PAD_LEFT );
681        $second = str_pad( intval( substr( date( "s", $cur_time ), 0, 2 ) ), 2, '0', STR_PAD_LEFT );
682        if ( $wgPageForms24HourTime ) {
683            $curTimeString .= " $hour:$minute:$second";
684        } else {
685            $ampm = date( "A", $cur_time );
686            $curTimeString .= " $hour:$minute:$second $ampm";
687        }
688
689        if ( $includeTimezone ) {
690            $timezone = date( "T", $cur_time );
691            $curTimeString .= " $timezone";
692        }
693
694        return $curTimeString;
695    }
696
697    /**
698     * If the value passed in for a certain field, when a form is
699     * submitted, is an array, then it might be from a checkbox
700     * or date input - in that case, convert it into a string.
701     * @param array $value
702     * @param string $delimiter
703     * @param bool $is_autoedit
704     * @return string
705     */
706    static function getStringFromPassedInArray( $value, $delimiter, $is_autoedit = false ) {
707        // If it's just a regular list, concatenate it.
708        // This is needed due to some strange behavior
709        // in PF, where, if a preload page is passed in
710        // in the query string, the form ends up being
711        // parsed twice.
712        if ( array_key_exists( 'is_list', $value ) ) {
713            unset( $value['is_list'] );
714            return implode( "$delimiter ", $value );
715        }
716
717        // if it has 1 or 2 elements, assume it's a checkbox; if it has
718        // 3 elements, assume it's a date
719        // - this handling will have to get more complex if other
720        // possibilities get added
721        if ( count( $value ) == 1 ) {
722            // If this is part of an internal form created to
723            // do autoedit, treat a blank value as a true null,
724            // rather than as false.
725            // @TODO - it's certainly possible that this function
726            // doesn't need to be called at all, if @is_autoedit
727            // is true - and the value should simply be blank if
728            // it's an array.
729            return $is_autoedit ? '' : PFUtils::getWordForYesOrNo( false );
730        } elseif ( count( $value ) == 2 ) {
731            return PFUtils::getWordForYesOrNo( true );
732        // if it's 3 or greater, assume it's a date or datetime
733        } elseif ( count( $value ) >= 3 ) {
734            $month = $value['month'];
735            $day = $value['day'];
736            if ( $day !== '' ) {
737                global $wgAmericanDates;
738                if ( $wgAmericanDates == false ) {
739                    // pad out day to always be two digits
740                    $day = str_pad( $day, 2, "0", STR_PAD_LEFT );
741                }
742            }
743            $year = $value['year'];
744            $hour = $minute = $second = $ampm24h = $timezone = null;
745            if ( isset( $value['hour'] ) ) {
746                $hour = $value['hour'];
747            }
748            if ( isset( $value['minute'] ) ) {
749                $minute = $value['minute'];
750            }
751            if ( isset( $value['second'] ) ) {
752                $second = $value['second'];
753            }
754            if ( isset( $value['ampm24h'] ) ) {
755                $ampm24h = $value['ampm24h'];
756            }
757            if ( isset( $value['timezone'] ) ) {
758                $timezone = $value['timezone'];
759            }
760            // if ( $month !== '' && $day !== '' && $year !== '' ) {
761            // We can accept either year, or year + month, or year + month + day.
762            // if ( $month !== '' && $day !== '' && $year !== '' ) {
763            if ( $year !== '' ) {
764                // special handling for American dates - otherwise, just
765                // the standard year/month/day (where month is a number)
766                global $wgAmericanDates;
767
768                if ( $month == '' ) {
769                    return $year;
770                } elseif ( $day == '' ) {
771                    if ( !$wgAmericanDates ) {
772                        // The month is a number - we
773                        // need it to be a string, so
774                        // that the date will be parsed
775                        // correctly if strtotime() is
776                        // used.
777                        $monthNames = PFFormUtils::getMonthNames();
778                        $month = $monthNames[$month - 1];
779                    }
780                    return "$month $year";
781                } else {
782                    if ( $wgAmericanDates == true ) {
783                        $new_value = "$month $day$year";
784                    } else {
785                        $new_value = "$year-$month-$day";
786                    }
787                    // If there's a day, include whatever
788                    // time information we have.
789                    if ( $hour !== null ) {
790                        $new_value .= " " . str_pad( intval( substr( $hour, 0, 2 ) ), 2, '0', STR_PAD_LEFT ) . ":" . str_pad( intval( substr( $minute, 0, 2 ) ), 2, '0', STR_PAD_LEFT );
791                    }
792                    if ( $second !== null ) {
793                        $new_value .= ":" . str_pad( intval( substr( $second, 0, 2 ) ), 2, '0', STR_PAD_LEFT );
794                    }
795                    if ( $ampm24h !== null ) {
796                        $new_value .= " $ampm24h";
797                    }
798                    if ( $timezone !== null ) {
799                        $new_value .= " $timezone";
800                    }
801                    return $new_value;
802                }
803            }
804        }
805        return '';
806    }
807
808    static function displayLoadingImage() {
809        global $wgPageFormsScriptPath;
810
811        $text = '<div id="loadingMask"></div>';
812        $loadingBGImage = Html::element( 'img', [ 'src' => "$wgPageFormsScriptPath/skins/loadingbg.png" ] );
813        $text .= '<div style="position: fixed; left: 50%; top: 50%;">' . $loadingBGImage . '</div>';
814        $loadingImage = Html::element( 'img', [ 'src' => "$wgPageFormsScriptPath/skins/loading.gif" ] );
815        $text .= '<div style="position: fixed; left: 50%; top: 50%; padding: 48px;">' . $loadingImage . '</div>';
816
817        return Html::rawElement( 'span', [ 'class' => 'loadingImage' ], $text );
818    }
819
820    /**
821     * This function is the real heart of the entire Page Forms
822     * extension. It handles two main actions: (1) displaying a form on the
823     * screen, given a form definition and possibly page contents (if an
824     * existing page is being edited); and (2) creating actual page
825     * contents, if the form was already submitted by the user.
826     *
827     * It also does some related tasks, like figuring out the page name (if
828     * only a page formula exists).
829     * @param string $form_def
830     * @param bool $form_submitted
831     * @param bool $page_exists
832     * @param string|null $form_id
833     * @param string|null $existing_page_content
834     * @param string|null $page_name
835     * @param string|null $page_name_formula
836     * @param int $form_context
837     * @param array $autocreate_query query parameters from #formredlink
838     * @param User|null $user
839     * @return array
840     * @throws FatalError
841     * @throws MWException
842     */
843    function formHTML(
844        $form_def,
845        $form_submitted,
846        $page_exists,
847        $form_id = null,
848        $existing_page_content = null,
849        $page_name = null,
850        $page_name_formula = null,
851        $form_context = self::CONTEXT_REGULAR,
852        $autocreate_query = [],
853        $user = null
854    ) {
855        global $wgRequest;
856        // used to represent the current tab index in the form
857        global $wgPageFormsTabIndex;
858        // used for setting various HTML IDs
859        global $wgPageFormsFieldNum;
860        global $wgPageFormsShowExpandAllLink;
861
862        // Initialize some variables.
863        $wiki_page = new PFWikiPage();
864        $source_is_page = $page_exists || $existing_page_content != null;
865        $wgPageFormsTabIndex = 0;
866        $wgPageFormsFieldNum = 0;
867        $source_page_matches_this_form = false;
868        $form_page_title = '';
869        $generated_page_name = $page_name_formula ?? '';
870        $new_text = "";
871        $original_page_content = $existing_page_content;
872        $is_query = ( $form_context == self::CONTEXT_QUERY || $form_context == self::CONTEXT_EMBEDDED_QUERY );
873        $is_embedded = $form_context == self::CONTEXT_EMBEDDED_QUERY;
874        $is_autoedit = $form_context == self::CONTEXT_AUTOEDIT;
875        $is_autocreate = $form_context == self::CONTEXT_AUTOCREATE;
876
877        // Disable all form elements if user doesn't have edit
878        // permission - two different checks are needed, because
879        // editing permissions can be set in different ways.
880        // HACK - sometimes we don't know the page name in advance, but
881        // we still need to set a title here for testing permissions.
882        if ( $is_embedded ) {
883            // If this is an embedded form,
884            // just use the name of the actual page we're on.
885            global $wgTitle;
886            $this->mPageTitle = $wgTitle;
887        } elseif ( $is_query ) {
888            // We're in Special:RunQuery - just use that as the
889            // title.
890            global $wgTitle;
891            $this->mPageTitle = $wgTitle;
892        } elseif ( $page_name === '' || $page_name === null ) {
893            $this->mPageTitle = Title::newFromText(
894                $wgRequest->getVal( 'namespace' ) . ":Page Forms permissions test" );
895        } else {
896            $this->mPageTitle = Title::newFromText( $page_name );
897        }
898
899        if ( $user === null ) {
900            $user = RequestContext::getMain()->getUser();
901        }
902
903        global $wgOut;
904        // Show previous set of deletions for this page, if it's been
905        // deleted before.
906        if ( !$form_submitted &&
907            ( $this->mPageTitle && !$this->mPageTitle->exists() &&
908            $page_name_formula === null )
909        ) {
910            $this->showDeletionLog( $wgOut );
911        }
912        $services = MediaWikiServices::getInstance();
913        $hookContainer = $services->getHookContainer();
914        // Unfortunately, we can't just call userCan() or its
915        // equivalent here because it seems to ignore the setting
916        // "$wgEmailConfirmToEdit = true;". Instead, we'll just get the
917        // permission errors from the start, and use those to determine
918        // whether the page is editable.
919        if ( !$is_query ) {
920            $permissionManager = $services->getPermissionManager();
921            $readOnlyMode = $services->getReadOnlyMode();
922            $permissionStatus = $permissionErrors = null;
923
924            if ( method_exists( $permissionManager, 'getPermissionStatus' ) ) {
925                // MW 1.43+
926                $permissionStatus = $permissionManager->getPermissionStatus( 'edit', $user, $this->mPageTitle );
927                if ( $readOnlyMode->isReadOnly() ) {
928                    $permissionStatus->error( 'readonlytext', $readOnlyMode->getReason() );
929                }
930                $userCanEditPage = $permissionStatus->isOK();
931            } else {
932                // MW < 1.43
933                $permissionErrors = $permissionManager->getPermissionErrors( 'edit', $user, $this->mPageTitle );
934                if ( $readOnlyMode->isReadOnly() ) {
935                    $permissionErrors = [ [ 'readonlytext', [ $readOnlyMode->getReason() ] ] ];
936                }
937                $userCanEditPage = count( $permissionErrors ) == 0;
938            }
939
940            $hookContainer->run( 'PageForms::UserCanEditPage', [ $this->mPageTitle, &$userCanEditPage ] );
941        }
942
943        // Start off with a loading spinner - this will be removed by
944        // the JavaScript once everything has finished loading.
945        $form_text = self::displayLoadingImage();
946        if ( $is_query || $userCanEditPage ) {
947            $form_is_disabled = false;
948            // Show "Your IP address will be recorded" warning if
949            // user is anonymous, and it's not a query.
950            if ( $user->isAnon() && !$is_query ) {
951                // Based on code in MediaWiki's EditPage.php.
952                $anonEditWarning = wfMessage( 'anoneditwarning',
953                    // Log-in link
954                    '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
955                    // Sign-up link
956                    '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' )->parse();
957                $form_text .= Html::warningBox( $anonEditWarning, 'mw-anon-edit-warning' );
958            }
959        } else {
960            $form_is_disabled = true;
961            if ( $wgOut->getTitle() != null ) {
962                $wgOut->setPageTitle( wfMessage( 'badaccess' )->text() );
963                if ( $permissionStatus ) {
964                    $wgOut->addWikiTextAsInterface( $wgOut->formatPermissionStatus( $permissionStatus, 'edit' ) );
965                } else {
966                    // MW < 1.43
967                    $wgOut->addWikiTextAsInterface(
968                        $wgOut->formatPermissionsErrorMessage( $permissionErrors, 'edit' )
969                    );
970                }
971                $wgOut->addHTML( "\n<hr />\n" );
972            }
973        }
974
975        if ( $wgPageFormsShowExpandAllLink ) {
976            $form_text .= Html::rawElement( 'p', [ 'id' => 'pf-expand-all' ],
977                // @TODO - add an i18n message for this.
978                Html::element( 'a', [ 'href' => '#' ], 'Expand all collapsed parts of the form' ) ) . "\n";
979        }
980
981        $parser = $services->getParserFactory()->getInstance();
982        if ( !$parser->getOptions() ) {
983            $parser->setOptions( ParserOptions::newFromUser( $user ) );
984        }
985        $parser->setTitle( $this->mPageTitle );
986        // This is needed in order to make sure $parser->mLinkHolders
987        // is set.
988        $parser->clearState();
989        $parser->setOutputType( Parser::OT_HTML );
990
991        $form_def = PFFormUtils::getFormDefinition( $parser, $form_def, $form_id );
992
993        // Turn form definition file into an array of sections, one for
994        // each template definition (plus the first section).
995        $form_def_sections = [];
996        $start_position = 0;
997        $section_start = 0;
998        $free_text_was_included = false;
999        $preloaded_free_text = null;
1000        // @HACK - replace the 'free text' standard input with a
1001        // field declaration to get it to be handled as a field.
1002        $form_def = str_replace( 'standard input|free text', 'field|#freetext#', $form_def );
1003        while ( $brackets_loc = strpos( $form_def, "{{{", $start_position ) ) {
1004            $brackets_end_loc = strpos( $form_def, "}}}", $brackets_loc );
1005            $bracketed_string = substr( $form_def, $brackets_loc + 3, $brackets_end_loc - ( $brackets_loc + 3 ) );
1006            $tag_components = PFUtils::getFormTagComponents( $bracketed_string );
1007            $tag_title = trim( $tag_components[0] );
1008            if ( $tag_title == 'for template' || $tag_title == 'end template' ) {
1009                // Create a section for everything up to here
1010                $section = substr( $form_def, $section_start, $brackets_loc - $section_start );
1011                $form_def_sections[] = $section;
1012                $section_start = $brackets_loc;
1013            }
1014            $start_position = $brackets_loc + 1;
1015        }
1016        // end while
1017        $form_def_sections[] = trim( substr( $form_def, $section_start ) );
1018
1019        // Cycle through the form definition file, and possibly an
1020        // existing article as well, finding template and field
1021        // declarations and replacing them with form elements, either
1022        // blank or pre-populated, as appropriate.
1023        $template_name = null;
1024        $template = null;
1025        $tif = null;
1026        // This array will keep track of all the replaced @<name>@ strings
1027        $placeholderFields = [];
1028
1029        for ( $section_num = 0; $section_num < count( $form_def_sections ); $section_num++ ) {
1030            $start_position = 0;
1031            // the append is there to ensure that the original
1032            // array doesn't get modified; is it necessary?
1033            $section = " " . $form_def_sections[$section_num];
1034
1035            while ( $brackets_loc = strpos( $section, '{{{', $start_position ) ) {
1036                $brackets_end_loc = strpos( $section, "}}}", $brackets_loc );
1037                // For cases with more than 3 ending brackets,
1038                // take the last 3 ones as the tag end.
1039                while ( isset( $section[$brackets_end_loc + 3] ) && $section[$brackets_end_loc + 3] == "}" ) {
1040                    $brackets_end_loc++;
1041                }
1042                $bracketed_string = substr( $section, $brackets_loc + 3, $brackets_end_loc - ( $brackets_loc + 3 ) );
1043                $tag_components = PFUtils::getFormTagComponents( $bracketed_string );
1044                if ( count( $tag_components ) == 0 ) {
1045                    continue;
1046                }
1047                $tag_title = trim( $tag_components[0] );
1048                // Checks for forbidden characters
1049                if ( $tag_title != 'info' ) {
1050                    foreach ( $tag_components as $tag_component ) {
1051                        // Angled brackets could cause a security leak (and should not be necessary).
1052                        // Allow them in "default filename", though.
1053                        $tagParts = explode( '=', $tag_component, 2 );
1054                        if ( count( $tagParts ) == 2 && $tagParts[0] == 'default filename' ) {
1055                            continue;
1056                        }
1057                        if ( strpos( $tag_component, '<' ) !== false && strpos( $tag_component, '>' ) !== false ) {
1058                            throw new MWException(
1059                                '<div class="error">Error in form definition! The following field tag contains forbidden characters:</div>' .
1060                                "\n<pre>" . htmlspecialchars( $tag_component ) . "</pre>"
1061                            );
1062                        }
1063                    }
1064                }
1065                // =====================================================
1066                // for template processing
1067                // =====================================================
1068                if ( $tag_title == 'for template' ) {
1069                    if ( $tif ) {
1070                        $previous_template_name = $tif->getTemplateName();
1071                    } else {
1072                        $previous_template_name = '';
1073                    }
1074                    if ( count( $tag_components ) < 2 ) {
1075                        throw new MWException( 'Error: a template name must be specified in each "for template" tag.' );
1076                    }
1077                    $template_name = str_replace( '_', ' ', self::getParsedValue( $parser, $tag_components[1] ) );
1078                    $is_new_template = ( $template_name != $previous_template_name );
1079                    if ( $is_new_template ) {
1080                        $template = PFTemplate::newFromName( $template_name );
1081                        $tif = PFTemplateInForm::newFromFormTag( $tag_components );
1082                    }
1083                    // Remove template tag.
1084                    $section = substr_replace( $section, '', $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1085                    // If we are editing a page, and this
1086                    // template can be found more than
1087                    // once in that page, and multiple
1088                    // values are allowed, repeat this
1089                    // section.
1090                    if ( $source_is_page ) {
1091                        $tif->setPageRelatedInfo( $existing_page_content );
1092                        // Get the first instance of
1093                        // this template on the page
1094                        // being edited, even if there
1095                        // are more.
1096                        if ( $tif->pageCallsThisTemplate() ) {
1097                            $tif->setFieldValuesFromPage( $existing_page_content );
1098                            $existing_template_text = $tif->getFullTextInPage();
1099                            // Now remove this template from the text being edited.
1100                            $existing_page_content = $this->strReplaceFirst( $existing_template_text, '', $existing_page_content );
1101                            // If we've found a match in the source
1102                            // page, there's a good chance that this
1103                            // page was created with this form - note
1104                            // that, so we don't send the user a warning.
1105                            $source_page_matches_this_form = true;
1106                        }
1107                    }
1108
1109                    // We get values from the request,
1110                    // regardless of whether the source is the
1111                    // page or a form submit, because even if
1112                    // the source is a page, values can still
1113                    // come from a query string.
1114                    // (Unless it's called from #formredlink.)
1115                    if ( !$is_autocreate ) {
1116                        $tif->setFieldValuesFromSubmit();
1117                    }
1118
1119                    $tif->checkIfAllInstancesPrinted( $form_submitted, $source_is_page, $is_autoedit );
1120
1121                    if ( !$tif->allInstancesPrinted() ) {
1122                        $wiki_page->addTemplate( $tif );
1123                    }
1124
1125                // =====================================================
1126                // end template processing
1127                // =====================================================
1128                } elseif ( $tag_title == 'end template' ) {
1129                    if ( count( $tag_components ) > 1 ) {
1130                        throw new MWException( '<div class="error">Error in form definition: \'end template\' tag cannot contain any additional parameters.</div>' );
1131                    }
1132                    if ( $source_is_page ) {
1133                        // Add any unhandled template fields
1134                        // in the page as hidden variables.
1135                        $form_text .= PFFormUtils::unhandledFieldsHTML( $tif, $is_autoedit );
1136                    }
1137                    // Remove this tag from the $section variable.
1138                    $section = substr_replace( $section, '', $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1139                    $template = null;
1140                    $tif = null;
1141                // =====================================================
1142                // field processing
1143                // =====================================================
1144                } elseif ( $tag_title == 'field' ) {
1145                    // If the template is null, that (hopefully)
1146                    // means we're handling the free text field.
1147                    // Make the template a dummy variable.
1148                    if ( $tif == null ) {
1149                        $template = new PFTemplate( null, [] );
1150                        // Get free text from the query string, if it was set.
1151                        if ( $wgRequest->getCheck( 'free_text' ) ) {
1152                            $standard_input = $wgRequest->getArray( 'standard_input', [] );
1153                            $standard_input['#freetext#'] = $wgRequest->getVal( 'free_text' );
1154                            $wgRequest->setVal( 'standard_input', $standard_input );
1155                        }
1156                        $tif = PFTemplateInForm::create( 'standard_input', null, null, null, [] );
1157                        $tif->setFieldValuesFromSubmit();
1158                    }
1159                    // We get the field name both here
1160                    // and in the PFFormField constructor,
1161                    // because PFFormField isn't equipped
1162                    // to deal with the #freetext# hack,
1163                    // among others.
1164                    $field_name = trim( $tag_components[1] );
1165                    $form_field = PFFormField::newFromFormFieldTag( $tag_components, $template, $tif, $form_is_disabled, $user );
1166                    // For special displays, add in the
1167                    // form fields, so we know the data
1168                    // structure.
1169                    if ( ( $tif->getDisplay() == 'table' && ( !$tif->allowsMultiple() || $tif->getInstanceNum() == 0 ) ) ||
1170                        ( $tif->getDisplay() == 'spreadsheet' && $tif->allowsMultiple() && $tif->getInstanceNum() == 0 ) || ( $tif->getDisplay() == 'calendar' && $tif->allowsMultiple() && $tif->getInstanceNum() == 0 ) ) {
1171                        $tif->addField( $form_field );
1172                    }
1173                    $val_modifier = null;
1174                    if ( $is_autocreate ) {
1175                        $values_from_query = $autocreate_query[$tif->getTemplateName()] ?? [];
1176                        $cur_value = $form_field->getCurrentValue( $values_from_query, $form_submitted, $source_is_page, $tif->allInstancesPrinted(), $val_modifier );
1177                    } else {
1178                        $cur_value = $form_field->getCurrentValue( $tif->getValuesFromSubmit(), $form_submitted, $source_is_page, $tif->allInstancesPrinted(), $val_modifier, $is_autoedit );
1179                    }
1180                    $delimiter = $form_field->getFieldArg( 'delimiter' );
1181                    if ( $form_field->holdsTemplate() ) {
1182                        $placeholderFields[] = self::placeholderFormat( $tif->getTemplateName(), $field_name );
1183                    }
1184
1185                    if ( $val_modifier !== null ) {
1186                        $page_value = $tif->getValuesFromPage()[$field_name];
1187                    }
1188                    if ( $val_modifier === '+' ) {
1189                        if ( preg_match( "#(,|\^)\s*$cur_value\s*(,|\$)#", $page_value ) === 0 ) {
1190                            if ( trim( $page_value ) !== '' ) {
1191                                // if page_value is empty, simply don't do anything, because then cur_value
1192                                // is already the value it has to be (no delimiter needed).
1193                                $cur_value = $page_value . $delimiter . $cur_value;
1194                            }
1195                        } else {
1196                            $cur_value = $page_value;
1197                        }
1198                        $tif->changeFieldValues( $field_name, $cur_value, $delimiter );
1199                    } elseif ( $val_modifier === '-' ) {
1200                        // get an array of elements to remove:
1201                        $remove = array_map( 'trim', explode( ",", $cur_value ) );
1202                        // process the current value:
1203                        $val_array = array_map( 'trim', explode( $delimiter, $page_value ) );
1204                        // remove element(s) from list
1205                        foreach ( $remove as $rmv ) {
1206                            // go through each element and remove match(es)
1207                            $key = array_search( $rmv, $val_array );
1208                            if ( $key !== false ) {
1209                                unset( $val_array[$key] );
1210                            }
1211                        }
1212                        // Convert modified array back to a comma-separated string value and modify
1213                        $cur_value = implode( ",", $val_array );
1214                        if ( $cur_value === '' ) {
1215                            // HACK: setting an empty string prevents anything from happening at all.
1216                            // set a dummy string that evaluates to an empty string
1217                            $cur_value = '{{subst:lc: }}';
1218                        }
1219                        $tif->changeFieldValues( $field_name, $cur_value, $delimiter );
1220                    }
1221                    // If the user is editing a page, and that page contains a call to
1222                    // the template being processed, get the current field's value
1223                    // from the template call.
1224                    // Do the same thing if it's a new page but there's a "preload" -
1225                    // unless a value for this field was already set in the query string.
1226                    if ( ( $page_exists || $cur_value == '' ) && ( $tif->getFullTextInPage() != '' ) && !$form_submitted ) {
1227                        if ( $tif->hasValueFromPageForField( $field_name ) ) {
1228                            // Get value, and remove it,
1229                            // so that at the end we
1230                            // can have a list of all
1231                            // the fields that weren't
1232                            // handled by the form.
1233                            $cur_value = $tif->getAndRemoveValueFromPageForField( $field_name );
1234
1235                            // If the field is a placeholder, the contents of this template
1236                            // parameter should be treated as elements parsed by an another
1237                            // multiple template form.
1238                            // By putting that at the very end of the parsed string, we'll
1239                            // have it processed as a regular multiple template form.
1240                            if ( $form_field->holdsTemplate() ) {
1241                                $existing_page_content .= $cur_value;
1242                            }
1243                        } elseif ( isset( $cur_value ) && !empty( $cur_value ) ) {
1244                            // Do nothing.
1245                        } else {
1246                            $cur_value = '';
1247                        }
1248                    }
1249
1250                    // Handle the free text field.
1251                    if ( $field_name == '#freetext#' ) {
1252                        // If there was no preloading, this will just be blank.
1253                        $preloaded_free_text = $cur_value;
1254                        // Add placeholders for the free text in both the form and
1255                        // the page, using <free_text> tags - once all the free text
1256                        // is known (at the end), it will get substituted in.
1257                        if ( $form_field->isHidden() ) {
1258                            $new_text = Html::hidden( 'pf_free_text', '!free_text!' );
1259                        } else {
1260                            $wgPageFormsTabIndex++;
1261                            $wgPageFormsFieldNum++;
1262                            if ( $cur_value === '' || $cur_value === null ) {
1263                                $default_value = '!free_text!';
1264                            } else {
1265                                $default_value = $cur_value;
1266                            }
1267                            $freeTextInput = new PFTextAreaInput( $input_number = null, $default_value, 'pf_free_text', ( $form_is_disabled || $form_field->isRestricted() ), $form_field->getFieldArgs() );
1268                            $freeTextInput->addJavaScript();
1269                            $new_text = $freeTextInput->getHtmlText();
1270                            if ( $form_field->hasFieldArg( 'edittools' ) ) {
1271                                // borrowed from EditPage::showEditTools()
1272                                $edittools_text = self::getParsedValue( $parser, wfMessage( 'edittools', [ 'content' ] )->text() );
1273
1274                                $new_text .= <<<END
1275        <div class="mw-editTools">
1276        $edittools_text
1277        </div>
1278
1279END;
1280                            }
1281                        }
1282                        $free_text_was_included = true;
1283                        $wiki_page->addFreeTextSection();
1284                    }
1285
1286                    if ( $tif->getTemplateName() === '' || $field_name == '#freetext#' ) {
1287                        $section = substr_replace( $section, $new_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1288                    } else {
1289                        if ( is_array( $cur_value ) ) {
1290                            // @TODO - is this code ever called?
1291                            $delimiter = $form_field->getFieldArg( 'is_list' );
1292                            // first, check if it's a list
1293                            if ( array_key_exists( 'is_list', $cur_value ) &&
1294                                    $cur_value['is_list'] == true ) {
1295                                $cur_value_in_template = "";
1296                                foreach ( $cur_value as $key => $val ) {
1297                                    if ( $key !== "is_list" ) {
1298                                        if ( $cur_value_in_template != "" ) {
1299                                            $cur_value_in_template .= $delimiter . " ";
1300                                        }
1301                                        $cur_value_in_template .= $val;
1302                                    }
1303                                }
1304                            } else {
1305                                // If it's not a list, it's probably from a checkbox or date input -
1306                                // convert the values into a string.
1307                                $cur_value_in_template = self::getStringFromPassedInArray( $cur_value, $delimiter );
1308                            }
1309                        } elseif ( $form_field->holdsTemplate() ) {
1310                            // If this field holds an embedded template,
1311                            // and the value is not an array, it means
1312                            // there are no instances of the template -
1313                            // set the value to null to avoid getting
1314                            // whatever is currently on the page.
1315                            $cur_value_in_template = null;
1316                        } else {
1317                            // value is not an array.
1318                            $cur_value_in_template = $cur_value;
1319                        }
1320
1321                        // If we're creating the page name from a formula based on
1322                        // form values, see if the current input is part of that formula,
1323                        // and if so, substitute in the actual value.
1324                        if ( $form_submitted && $generated_page_name !== '' ) {
1325                            // This line appears to be unnecessary.
1326                            // $generated_page_name = str_replace('.', '_', $generated_page_name);
1327                            $generated_page_name = str_replace( ' ', '_', $generated_page_name );
1328                            $escaped_input_name = str_replace( ' ', '_', $form_field->getInputName() );
1329                            $generated_page_name = str_ireplace( "<$escaped_input_name>",
1330                                $cur_value_in_template ?? '', $generated_page_name );
1331                            // Once the substitution is done, replace underlines back
1332                            // with spaces.
1333                            $generated_page_name = str_replace( '_', ' ', $generated_page_name );
1334                        }
1335                        if ( defined( 'CARGO_VERSION' ) && $form_field->hasFieldArg( 'mapping cargo table' ) &&
1336                            $form_field->hasFieldArg( 'mapping cargo field' ) && $form_field->hasFieldArg( 'mapping cargo value field' ) ) {
1337                            $mappingCargoTable = $form_field->getFieldArg( 'mapping cargo table' );
1338                            $mappingCargoField = $form_field->getFieldArg( 'mapping cargo field' );
1339                            $mappingCargoValueField = $form_field->getFieldArg( 'mapping cargo value field' );
1340                            if ( !$form_submitted && $cur_value !== null && $cur_value !== '' ) {
1341                                $cur_value = $this->getCargoBasedMapping( $cur_value, $mappingCargoTable, $mappingCargoField, $mappingCargoValueField, $form_field );
1342                            }
1343                            if ( $form_submitted && $cur_value_in_template !== null && $cur_value_in_template !== '' ) {
1344                                $cur_value_in_template = $this->getCargoBasedMapping( $cur_value_in_template, $mappingCargoTable, $mappingCargoValueField, $mappingCargoField, $form_field );
1345                            }
1346                        }
1347                        if ( $cur_value !== '' &&
1348                            ( $form_field->hasFieldArg( 'mapping template' ) ||
1349                            $form_field->hasFieldArg( 'mapping property' ) ||
1350                            ( $form_field->hasFieldArg( 'mapping cargo table' ) &&
1351                            $form_field->hasFieldArg( 'mapping cargo field' ) ) ||
1352                            $form_field->getUseDisplayTitle() ) ) {
1353                            // If the input type is "tokens', the value is not
1354                            // an array, but the delimiter still needs to be set.
1355                            if ( !is_array( $cur_value ) ) {
1356                                if ( $form_field->isList() ) {
1357                                    $delimiter = $form_field->getFieldArg( 'delimiter' );
1358                                } else {
1359                                    $delimiter = null;
1360                                }
1361                            }
1362                            $cur_value = $form_field->valueStringToLabels( $cur_value, $delimiter, $form_submitted );
1363                        }
1364
1365                        // Call hooks - unfortunately this has to be split into two
1366                        // separate calls, because of the different variable names in
1367                        // each case.
1368                        // @TODO - should it be $cur_value for both cases? Or should the
1369                        // hook perhaps modify both variables?
1370                        if ( $form_submitted ) {
1371                            $hookContainer->run( 'PageForms::CreateFormField', [ &$form_field, &$cur_value_in_template, true ] );
1372                        } else {
1373                            $this->createFormFieldTranslateTag( $template, $tif, $form_field, $cur_value );
1374                            $hookContainer->run( 'PageForms::CreateFormField', [ &$form_field, &$cur_value, false ] );
1375                        }
1376                        // if this is not part of a 'multiple' template, increment the
1377                        // global tab index (used for correct tabbing)
1378                        if ( !$form_field->hasFieldArg( 'part_of_multiple' ) ) {
1379                            $wgPageFormsTabIndex++;
1380                        }
1381                        // increment the global field number regardless
1382                        $wgPageFormsFieldNum++;
1383                        if ( $source_is_page && !$tif->allInstancesPrinted() ) {
1384                            // If the source is a page, don't use the default
1385                            // values - except for newly-added instances of a
1386                            // multiple-instance template.
1387                        // If the field is a date field, and its default value was set
1388                        // to 'now', and it has no current value, set $cur_value to be
1389                        // the current date.
1390                        } elseif ( $form_field->getDefaultValue() == 'now' &&
1391                                // if the date is hidden, cur_value will already be set
1392                                // to the default value
1393                                ( $cur_value == '' || $cur_value == 'now' ) ) {
1394                            $input_type = $form_field->getInputType();
1395                            // We don't handle the 'datepicker' and 'datetimepicker'
1396                            // input types here, because they have their own
1397                            // formatting; instead, they handle 'now' themselves.
1398                            if ( $input_type == 'date' || $input_type == 'datetime' ||
1399                                    $input_type == 'year' ||
1400                                    ( $input_type == '' && $form_field->getTemplateField()->getPropertyType() == '_dat' ) ) {
1401                                $cur_value_in_template = self::getStringForCurrentTime( $input_type == 'datetime', $form_field->hasFieldArg( 'include timezone' ) );
1402                            }
1403                        // If the field is a text field, and its default value was set
1404                        // to 'current user', and it has no current value, set $cur_value
1405                        // to be the current user.
1406                        } elseif ( $form_field->getDefaultValue() == 'current user' &&
1407                            // if the input is hidden, cur_value will already be set
1408                            // to the default value
1409                            ( $cur_value === '' || $cur_value == 'current user' )
1410                        ) {
1411                            $cur_value_in_template = $user->isRegistered() ? $user->getName() : '';
1412                            $cur_value = $cur_value_in_template;
1413                        // UUID is the only default value (so far) that can also be set
1414                        // by the JavaScript, for multiple-instance templates - for the
1415                        // other default values, there's no real need to have a
1416                        // different value for each instance.
1417                        } elseif ( $form_field->getDefaultValue() == 'uuid' &&
1418                            ( $cur_value == '' || $cur_value == 'uuid' )
1419                        ) {
1420                            if ( $tif->allowsMultiple() ) {
1421                                // Will be set by the JS.
1422                                $form_field->setFieldArg( 'class', 'new-uuid' );
1423                            } else {
1424                                $cur_value = $cur_value_in_template = self::generateUUID();
1425                            }
1426                        }
1427
1428                        // If all instances have been
1429                        // printed, that means we're
1430                        // now printing a "starter"
1431                        // div - set the current value
1432                        // to null, unless it's the
1433                        // default value.
1434                        // (Ideally it wouldn't get
1435                        // set at all, but that seems a
1436                        // little harder.)
1437                        if ( $tif->allInstancesPrinted() && $form_field->getDefaultValue() == null ) {
1438                            $cur_value = null;
1439                        }
1440
1441                        $new_text = $this->formFieldHTML( $form_field, $cur_value );
1442                        $new_text .= $form_field->additionalHTMLForInput( $cur_value, $field_name, $tif->getTemplateName() );
1443
1444                        if ( $new_text ) {
1445                            $wiki_page->addTemplateParam( $template_name, $tif->getInstanceNum(), $field_name, $cur_value_in_template );
1446                            $section = substr_replace( $section, $new_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1447                            $start_position = $brackets_loc + strlen( $new_text );
1448                        } else {
1449                            $start_position = $brackets_end_loc;
1450                        }
1451                    }
1452
1453                    if ( $tif->allowsMultiple() && !$tif->allInstancesPrinted() ) {
1454                        $wordForYes = PFUtils::getWordForYesOrNo( true );
1455                        if ( $form_field->getInputType() == 'checkbox' ) {
1456                            if ( strtolower( $cur_value ) == strtolower( $wordForYes ) || strtolower( $cur_value ) == 'yes' || $cur_value == '1' ) {
1457                                $cur_value = true;
1458                            } else {
1459                                $cur_value = false;
1460                            }
1461                        }
1462                    }
1463
1464                    if ( $tif->getDisplay() != null && ( !$tif->allowsMultiple() || !$tif->allInstancesPrinted() ) ) {
1465                        $tif->addGridValue( $field_name, $cur_value );
1466                    }
1467
1468                // =====================================================
1469                // standard input processing
1470                // =====================================================
1471                } elseif ( $tag_title == 'standard input' ) {
1472                    // handle all the possible values
1473                    $input_name = $tag_components[1];
1474                    $input_label = null;
1475                    $attr = [];
1476
1477                    // if it's a query, ignore all standard inputs except run query
1478                    if ( ( $is_query && $input_name != 'run query' ) || ( !$is_query && $input_name == 'run query' ) ) {
1479                        $new_text = "";
1480                        $section = substr_replace( $section, $new_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1481                        continue;
1482                    }
1483                    // set a flag so that the standard 'form bottom' won't get displayed
1484                    $this->standardInputsIncluded = true;
1485                    // cycle through the other components
1486                    $is_checked = false;
1487                    for ( $i = 2; $i < count( $tag_components ); $i++ ) {
1488                        $component = $tag_components[$i];
1489                        $sub_components = array_map( 'trim', explode( '=', $component ) );
1490                        if ( count( $sub_components ) == 1 ) {
1491                            if ( $sub_components[0] == 'checked' ) {
1492                                $is_checked = true;
1493                            }
1494                        } elseif ( count( $sub_components ) == 2 ) {
1495                            switch ( $sub_components[0] ) {
1496                                case 'label':
1497                                    $input_label = self::getParsedValue( $parser, $sub_components[1] );
1498                                    break;
1499                                case 'class':
1500                                    $attr['class'] = $sub_components[1];
1501                                    break;
1502                                case 'style':
1503                                    $attr['style'] = Sanitizer::checkCSS( $sub_components[1] );
1504                                    break;
1505                            }
1506                        }
1507                    }
1508                    if ( $input_name == 'summary' ) {
1509                        $value = $wgRequest->getVal( 'wpSummary' );
1510                        $new_text = PFFormUtils::summaryInputHTML( $form_is_disabled, $input_label, $attr, $value );
1511                    } elseif ( $input_name == 'minor edit' ) {
1512                        $is_checked = $wgRequest->getCheck( 'wpMinoredit' );
1513                        $new_text = PFFormUtils::minorEditInputHTML( $form_submitted, $form_is_disabled, $is_checked, $input_label, $attr );
1514                    } elseif ( $input_name == 'watch' ) {
1515                        $is_checked = $wgRequest->getCheck( 'wpWatchthis' );
1516                        $new_text = PFFormUtils::watchInputHTML( $form_submitted, $form_is_disabled, $is_checked, $input_label, $attr );
1517                    } elseif ( $input_name == 'save' ) {
1518                        $new_text = PFFormUtils::saveButtonHTML( $form_is_disabled, $input_label, $attr );
1519                    } elseif ( $input_name == 'save and continue' ) {
1520                        // Remove save and continue button in one-step-process
1521                        if ( $this->mPageTitle == $page_name ) {
1522                            $new_text = PFFormUtils::saveAndContinueButtonHTML( $form_is_disabled, $input_label, $attr );
1523                        } else {
1524                            $new_text = '';
1525                        }
1526                    } elseif ( $input_name == 'preview' ) {
1527                        $new_text = PFFormUtils::showPreviewButtonHTML( $form_is_disabled, $input_label, $attr );
1528                    } elseif ( $input_name == 'changes' ) {
1529                        $new_text = PFFormUtils::showChangesButtonHTML( $form_is_disabled, $input_label, $attr );
1530                    } elseif ( $input_name == 'cancel' ) {
1531                        $new_text = PFFormUtils::cancelLinkHTML( $form_is_disabled, $input_label, $attr );
1532                    } elseif ( $input_name == 'run query' ) {
1533                        $new_text = PFFormUtils::runQueryButtonHTML( $form_is_disabled, $input_label, $attr );
1534                    }
1535                    $section = substr_replace( $section, $new_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1536                // =====================================================
1537                // for section processing
1538                // =====================================================
1539                } elseif ( $tag_title == 'section' ) {
1540                    $wgPageFormsFieldNum++;
1541                    $wgPageFormsTabIndex++;
1542
1543                    $section_name = trim( $tag_components[1] );
1544                    $page_section_in_form = PFPageSection::newFromFormTag( $tag_components, $user );
1545                    $section_text = null;
1546
1547                    // Split the existing page contents into the textareas in the form.
1548                    $default_value = "";
1549                    $section_start_loc = 0;
1550                    if ( $source_is_page && $existing_page_content !== null ) {
1551                        // For the last section of the page, there is no trailing newline in
1552                        // $existing_page_content, but the code below expects it. This code
1553                        // ensures that there is always trailing newline. T72202
1554                        if ( substr( $existing_page_content, -1 ) !== "\n" ) {
1555                            $existing_page_content .= "\n";
1556                        }
1557
1558                        $equalsSigns = str_repeat( '=', $page_section_in_form->getSectionLevel() );
1559                        $searchStr =
1560                            '/^' .
1561                            preg_quote( $equalsSigns, '/' ) .
1562                            '[ ]*?' .
1563                            preg_quote( $section_name, '/' ) .
1564                            '[ ]*?' .
1565                            preg_quote( $equalsSigns, '/' ) .
1566                            '$/m';
1567                        if ( preg_match( $searchStr, $existing_page_content, $matches, PREG_OFFSET_CAPTURE ) ) {
1568                            $section_start_loc = $matches[0][1];
1569                            $header_text = $matches[0][0];
1570                            $existing_page_content = str_replace( $header_text, '', $existing_page_content );
1571                        } else {
1572                            $section_start_loc = 0;
1573                        }
1574                        $section_end_loc = -1;
1575
1576                        // get the position of the next template or section defined in the form which is not empty and hidden if empty
1577                        $previous_brackets_end_loc = $brackets_end_loc;
1578                        $next_section_found = false;
1579                        // loop until the next section is found
1580                        while ( !$next_section_found ) {
1581                            $next_bracket_start_loc = strpos( $section, '{{{', $previous_brackets_end_loc );
1582                            if ( $next_bracket_start_loc == false ) {
1583                                $section_end_loc = strpos( $existing_page_content, '{{', $section_start_loc );
1584                                $next_section_found = true;
1585                            } else {
1586                                $next_bracket_end_loc = strpos( $section, '}}}', $next_bracket_start_loc );
1587                                $bracketed_string_next_section = substr( $section, $next_bracket_start_loc + 3, $next_bracket_end_loc - ( $next_bracket_start_loc + 3 ) );
1588                                $tag_components_next_section = PFUtils::getFormTagComponents( $bracketed_string_next_section );
1589                                $page_next_section_in_form = PFPageSection::newFromFormTag( $tag_components_next_section, $user );
1590                                $tag_title_next_section = trim( $tag_components_next_section[0] );
1591                                if ( $tag_title_next_section == 'section' ) {
1592                                    // There is no pattern match for the next section if the section is empty and its hideIfEmpty attribute is set
1593                                    if ( preg_match( '/(^={1,6}[ ]*?' . preg_quote( $tag_components_next_section[1], '/' ) . '[ ]*?={1,6}\s*?$)/m', $existing_page_content, $matches, PREG_OFFSET_CAPTURE ) ) {
1594                                        $section_end_loc = $matches[0][1];
1595                                        $next_section_found = true;
1596                                    // Check for the next section if no pattern match
1597                                    } elseif ( $page_next_section_in_form->isHideIfEmpty() ) {
1598                                        $previous_brackets_end_loc = $next_bracket_end_loc;
1599                                    } else {
1600                                        // If none of the above conditions is satisfied, exit the loop.
1601                                        break;
1602                                    }
1603                                } else {
1604                                    $next_section_found = true;
1605                                }
1606                            }
1607                        }
1608
1609                        if ( $section_end_loc === -1 || $section_end_loc === null ) {
1610                            $section_text = substr( $existing_page_content, $section_start_loc );
1611                            $existing_page_content = substr( $existing_page_content, 0, $section_start_loc );
1612                        } else {
1613                            $section_text = substr( $existing_page_content, $section_start_loc, $section_end_loc - $section_start_loc );
1614                            $existing_page_content = substr( $existing_page_content, 0, $section_start_loc ) . substr( $existing_page_content, $section_end_loc );
1615                        }
1616                    }
1617
1618                    // If input is from the form.
1619                    if ( ( !$source_is_page ) && $wgRequest ) {
1620                        $text_per_section = $wgRequest->getArray( '_section' );
1621
1622                        if ( is_array( $text_per_section ) && array_key_exists( $section_name, $text_per_section ) ) {
1623                            $section_text = $text_per_section[$section_name];
1624                        } else {
1625                            $section_text = '';
1626                        }
1627                        // $section_options will allow to pass additional options in the future without breaking backword compatibility
1628                        $section_options = [ 'hideIfEmpty' => $page_section_in_form->isHideIfEmpty() ];
1629                        $wiki_page->addSection( $section_name, $page_section_in_form->getSectionLevel(), $section_text, $section_options );
1630                    }
1631
1632                    $section_text = trim( $section_text );
1633
1634                    // Set input name for query string.
1635                    $input_name = '_section' . '[' . $section_name . ']';
1636                    $other_args = $page_section_in_form->getSectionArgs();
1637                    $other_args['isSection'] = true;
1638                    if ( $page_section_in_form->isMandatory() ) {
1639                        $other_args['mandatory'] = true;
1640                    }
1641
1642                    if ( $page_section_in_form->isHidden() ) {
1643                        $form_section_text = Html::hidden( $input_name, $section_text );
1644                    } else {
1645                        $sectionInput = new PFTextAreaInput( $wgPageFormsFieldNum, $section_text, $input_name, ( $form_is_disabled || $page_section_in_form->isRestricted() ), $other_args );
1646                        $sectionInput->addJavaScript();
1647                        $form_section_text = $sectionInput->getHtmlText();
1648                    }
1649
1650                    $section = substr_replace( $section, $form_section_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1651                // =====================================================
1652                // page info processing
1653                // =====================================================
1654                } elseif ( $tag_title == 'info' ) {
1655                    // TODO: Generate an error message if this is included more than once
1656                    foreach ( array_slice( $tag_components, 1 ) as $component ) {
1657                        $sub_components = array_map( 'trim', explode( '=', $component, 2 ) );
1658                        // Tag names are case-insensitive
1659                        $tag = strtolower( $sub_components[0] );
1660                        if ( $tag == 'create title' || $tag == 'add title' ) {
1661                            // Handle this only if
1662                            // we're adding a page.
1663                            if ( !$is_query && !$this->mPageTitle->exists() ) {
1664                                $form_page_title = $sub_components[1];
1665                            }
1666                        } elseif ( $tag == 'edit title' ) {
1667                            // Handle this only if
1668                            // we're editing a page.
1669                            if ( !$is_query && $this->mPageTitle->exists() ) {
1670                                $form_page_title = $sub_components[1];
1671                            }
1672                        } elseif ( $tag == 'query title' ) {
1673                            // Handle this only if
1674                            // we're in 'RunQuery'.
1675                            if ( $is_query ) {
1676                                $form_page_title = $sub_components[1];
1677                            }
1678                        } elseif ( $tag == 'includeonly free text' || $tag == 'onlyinclude free text' ) {
1679                            $wiki_page->makeFreeTextOnlyInclude();
1680                        } elseif ( $tag == 'query form at top' ) {
1681                            // TODO - this should be made a field of
1682                            // some non-static class that actually
1683                            // prints the form, instead of requiring
1684                            // a global variable.
1685                            global $wgPageFormsRunQueryFormAtTop;
1686                            $wgPageFormsRunQueryFormAtTop = true;
1687                        }
1688                    }
1689                    // Replace the {{{info}}} tag with a hidden span, instead of a blank, to avoid a
1690                    // potential security issue.
1691                    $section = substr_replace( $section, '<span style="visibility: hidden;"></span>', $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1692                // =====================================================
1693                // default outer level processing
1694                // =====================================================
1695                } else {
1696                    // Tag is not one of the allowed values -
1697                    // ignore it, other than to HTML-escape it.
1698                    $form_section_text = htmlspecialchars( substr( $section, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc ) );
1699                    $section = substr_replace( $section, $form_section_text, $brackets_loc, $brackets_end_loc + 3 - $brackets_loc );
1700                    $start_position = $brackets_end_loc;
1701                }
1702                // end if
1703            }
1704            // end while
1705
1706            if ( $tif && ( !$tif->allowsMultiple() || $tif->allInstancesPrinted() ) ) {
1707                $template_text = $wiki_page->createTemplateCallsForTemplateName( $tif->getTemplateName() );
1708                // Escape the '$' characters for the preg_replace() call.
1709                $template_text = str_replace( '$', '\$', $template_text );
1710
1711                // If there is a placeholder in the text, we
1712                // know that we are doing a replace.
1713                if ( $existing_page_content && strpos( $existing_page_content, '{{{insertionpoint}}}', 0 ) !== false ) {
1714                    $existing_page_content = preg_replace( '/\{\{\{insertionpoint\}\}\}(\r?\n?)/',
1715                        preg_replace( '/\}\}/m', '}�',
1716                            preg_replace( '/\{\{/m', '�{', $template_text ) ) .
1717                        "{{{insertionpoint}}}",
1718                        $existing_page_content );
1719                }
1720            }
1721
1722            $multipleTemplateHTML = '';
1723            if ( $tif ) {
1724                if ( $tif->getLabel() != null ) {
1725                    $fieldsetStartHTML = "<fieldset>\n" . Html::element( 'legend', null, $tif->getLabel() ) . "\n";
1726                    $fieldsetStartHTML .= $tif->getIntro();
1727                    if ( !$tif->allowsMultiple() ) {
1728                        $form_text .= $fieldsetStartHTML;
1729                    } elseif ( $tif->allowsMultiple() && $tif->getInstanceNum() == 0 ) {
1730                        $multipleTemplateHTML .= $fieldsetStartHTML;
1731                    }
1732                } else {
1733                    if ( !$tif->allowsMultiple() ) {
1734                        $form_text .= $tif->getIntro();
1735                    }
1736                    if ( $tif->allowsMultiple() && $tif->getInstanceNum() == 0 ) {
1737                        $multipleTemplateHTML .= $tif->getIntro();
1738                    }
1739                }
1740            }
1741            if ( $tif && $tif->allowsMultiple() ) {
1742                if ( $tif->getDisplay() == 'spreadsheet' ) {
1743                    if ( $tif->allInstancesPrinted() ) {
1744                        $multipleTemplateHTML .= $this->spreadsheetHTML( $tif );
1745                        // For spreadsheets, this needs
1746                        // to be specially inserted.
1747                        if ( $tif->getLabel() != null ) {
1748                            $multipleTemplateHTML .= "</fieldset>\n";
1749                        }
1750                    }
1751                } elseif ( $tif->getDisplay() == 'calendar' ) {
1752                    if ( $tif->allInstancesPrinted() ) {
1753                        global $wgPageFormsCalendarParams, $wgPageFormsCalendarValues;
1754                        global $wgPageFormsScriptPath;
1755                        $text = '';
1756                        $params = [];
1757                        foreach ( $tif->getFields() as $formField ) {
1758                            $templateField = $formField->getTemplateField();
1759                            $inputType = $formField->getInputType();
1760                            $values = [ 'name' => $templateField->getFieldName() ];
1761                            if ( $formField->getLabel() !== null ) {
1762                                $values['title'] = $formField->getLabel();
1763                            }
1764                            $possibleValues = $formField->getPossibleValues();
1765                            if ( $inputType == 'textarea' ) {
1766                                $values['type'] = 'textarea';
1767                            } elseif ( $inputType == 'datetime' ) {
1768                                $values['type'] = 'datetime';
1769                            } elseif ( $inputType == 'checkbox' ) {
1770                                $values['type'] = 'checkbox';
1771                            } elseif ( $inputType == 'checkboxes' ) {
1772                                $values['type'] = 'checkboxes';
1773                            } elseif ( $inputType == 'listbox' ) {
1774                                $values['type'] = 'listbox';
1775                            } elseif ( $inputType == 'date' ) {
1776                                $values['type'] = 'date';
1777                            } elseif ( $inputType == 'rating' ) {
1778                                $values['type'] = 'rating';
1779                            } elseif ( $inputType == 'radiobutton' ) {
1780                                $values['type'] = 'radiobutton';
1781                            } elseif ( $inputType == 'tokens' ) {
1782                                $values['type'] = 'tokens';
1783                            } elseif ( $possibleValues != null ) {
1784                                array_unshift( $possibleValues, '' );
1785                                $completePossibleValues = [];
1786                                foreach ( $possibleValues as $value ) {
1787                                    $completePossibleValues[] = [ 'Name' => $value, 'Id' => $value ];
1788                                }
1789                                $values['type'] = 'select';
1790                                $values['items'] = $completePossibleValues;
1791                                $values['valueField'] = 'Id';
1792                                $values['textField'] = 'Name';
1793                            } else {
1794                                $values['type'] = 'text';
1795                            }
1796                            $params[] = $values;
1797                        }
1798                        $templateName = $tif->getTemplateName();
1799                        $templateDivID = str_replace( ' ', '_', $templateName ) . "FullCalendar";
1800                        $templateDivAttrs = [
1801                            'class' => 'pfFullCalendarJS',
1802                            'id' => $templateDivID,
1803                            'template-name' => $templateName,
1804                            'title-field' => $tif->getEventTitleField(),
1805                            'event-date-field' => $tif->getEventDateField(),
1806                            'event-start-date-field' => $tif->getEventStartDateField(),
1807                            'event-end-date-field' => $tif->getEventEndDateField()
1808                        ];
1809                        $loadingImage = Html::element( 'img', [ 'src' => "$wgPageFormsScriptPath/skins/loading.gif" ] );
1810                        $text = "<div id='fullCalendarLoading1' style='display: none;'>" . $loadingImage . "</div>";
1811                        $text .= Html::rawElement( 'div', $templateDivAttrs, $text );
1812                        $wgPageFormsCalendarParams[$templateName] = $params;
1813                        $wgPageFormsCalendarValues[$templateName] = $tif->getGridValues();
1814                        $fullForm = $this->multipleTemplateInstanceHTML( $tif, $form_is_disabled, $section );
1815                        $multipleTemplateHTML .= $text;
1816                        $multipleTemplateHTML .= "</fieldset>\n";
1817                        PFFormUtils::setGlobalVarsForSpreadsheet();
1818                    }
1819                } else {
1820                    if ( $tif->getDisplay() == 'table' ) {
1821                        $section = $this->tableHTML( $tif, $tif->getInstanceNum() );
1822                    }
1823                    if ( $tif->getInstanceNum() == 0 ) {
1824                        $multipleTemplateHTML .= $this->multipleTemplateStartHTML( $tif );
1825                    }
1826                    if ( !$tif->allInstancesPrinted() ) {
1827                        $multipleTemplateHTML .= $this->multipleTemplateInstanceHTML( $tif, $form_is_disabled, $section );
1828                    } else {
1829                        $multipleTemplateHTML .= $this->multipleTemplateEndHTML( $tif, $form_is_disabled, $section );
1830                    }
1831                }
1832                $placeholder = $tif->getPlaceholder();
1833                if ( $placeholder == null ) {
1834                    // The normal process.
1835                    $form_text .= $multipleTemplateHTML;
1836                } else {
1837                    // The template text won't be appended
1838                    // at the end of the template like for
1839                    // usual multiple template forms.
1840                    // The HTML text will instead be stored in
1841                    // the $multipleTemplateHTML variable,
1842                    // and then added in the right
1843                    // @insertHTML_".$placeHolderField."@"; position
1844                    // Optimization: actually, instead of
1845                    // separating the processes, the usual
1846                    // multiple template forms could also be
1847                    // handled this way if a fitting
1848                    // placeholder tag was added.
1849                    // We replace the HTML into the current
1850                    // placeholder tag, but also add another
1851                    // placeholder tag, to keep track of it.
1852                    $multipleTemplateHTML .= self::makePlaceholderInFormHTML( $placeholder );
1853                    $form_text = str_replace( self::makePlaceholderInFormHTML( $placeholder ), $multipleTemplateHTML, $form_text );
1854                }
1855                if ( !$tif->allInstancesPrinted() ) {
1856                    // This will cause the section to be
1857                    // re-parsed on the next go.
1858                    $section_num--;
1859                    $tif->incrementInstanceNum();
1860                }
1861            } elseif ( $tif && $tif->getDisplay() == 'table' ) {
1862                $form_text .= $this->tableHTML( $tif, 0 );
1863            } elseif ( $tif && !$tif->allowsMultiple() && $tif->getLabel() != null ) {
1864                $form_text .= $section . "\n</fieldset>";
1865            } else {
1866                $form_text .= $section;
1867            }
1868        }
1869        // end for
1870
1871        // Cleanup - everything has been browsed.
1872        // Remove all the remaining placeholder
1873        // tags in the HTML and wiki-text.
1874        foreach ( $placeholderFields as $stringToReplace ) {
1875            // Remove the @<insertHTML>@ tags from the generated
1876            // HTML form.
1877            $form_text = str_replace( self::makePlaceholderInFormHTML( $stringToReplace ), '', $form_text );
1878        }
1879
1880        // If it wasn't included in the form definition, add the
1881        // 'free text' input as a hidden field at the bottom.
1882        if ( !$free_text_was_included ) {
1883            $form_text .= Html::hidden( 'pf_free_text', '!free_text!' );
1884        }
1885        // Get free text, and add to page data, as well as retroactively
1886        // inserting it into the form.
1887
1888        if ( $source_is_page ) {
1889            // If the page is the source, free_text will just be
1890            // whatever in the page hasn't already been inserted
1891            // into the form.
1892            $free_text = trim( $existing_page_content );
1893        // ...or get it from the form submission, if it's not called from #formredlink
1894        } elseif ( !$is_autocreate && $wgRequest->getCheck( 'pf_free_text' ) ) {
1895            $free_text = $wgRequest->getVal( 'pf_free_text' );
1896            if ( !$free_text_was_included ) {
1897                $wiki_page->addFreeTextSection();
1898            }
1899        } elseif ( $preloaded_free_text != null ) {
1900            $free_text = $preloaded_free_text;
1901        } else {
1902            $free_text = '';
1903        }
1904
1905        if ( $wiki_page->freeTextOnlyInclude() ) {
1906            $free_text = str_replace( "<onlyinclude>", '', $free_text );
1907            $free_text = str_replace( "</onlyinclude>", '', $free_text );
1908            $free_text = trim( $free_text );
1909        }
1910
1911        $page_text = '';
1912
1913        $hookContainer->run( 'PageForms::BeforeFreeTextSubst',
1914            [ &$free_text, $existing_page_content, &$page_text ] );
1915
1916        // Now that we have the free text, we can create the full page
1917        // text.
1918        // The page text needs to be created whether or not the form
1919        // was submitted, in case this is called from #formredlink.
1920        $wiki_page->setFreeText( $free_text );
1921        $page_text = $wiki_page->createPageText();
1922
1923        // Also substitute the free text into the form.
1924        $escaped_free_text = Sanitizer::safeEncodeAttribute( $free_text );
1925        $form_text = str_replace( '!free_text!', $escaped_free_text, $form_text );
1926
1927        // Add a warning in, if we're editing an existing page and that
1928        // page appears to not have been created with this form.
1929        if ( !$is_query && $page_name_formula === null &&
1930            $this->mPageTitle->exists() && $existing_page_content !== ''
1931            && !$source_page_matches_this_form ) {
1932            // Prepend with a colon in case it's a file or category page.
1933            $wrongFormText = wfMessage( 'pf_formedit_formwarning', ':' . $page_name )->parse();
1934            $form_text = Html::warningBox( $wrongFormText ) .
1935                "\n<br clear=\"both\" />\n" . $form_text;
1936        }
1937
1938        // Add form bottom, if no custom "standard inputs" have been defined.
1939        if ( !$this->standardInputsIncluded ) {
1940            if ( $is_query ) {
1941                $form_text .= PFFormUtils::queryFormBottom();
1942            } else {
1943                $form_text .= PFFormUtils::formBottom( $form_submitted, $form_is_disabled );
1944            }
1945        }
1946
1947        if ( !$is_query ) {
1948            $form_text .= Html::hidden( 'wpStarttime', wfTimestampNow() );
1949            // This variable is called $mwWikiPage and not
1950            // something simpler, to avoid confusion with the
1951            // variable $wiki_page, which is of type PFWikiPage.
1952            $mwWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $this->mPageTitle );
1953            $form_text .= Html::hidden( 'wpEdittime', $mwWikiPage->getTimestamp() );
1954            $form_text .= Html::hidden( 'editRevId', 0 );
1955            $form_text .= Html::hidden( 'wpEditToken', $user->getEditToken() );
1956            $form_text .= Html::hidden( 'wpUnicodeCheck', EditPage::UNICODE_CHECK );
1957            $form_text .= Html::hidden( 'wpUltimateParam', true );
1958        }
1959
1960        $form_text .= "\t</form>\n";
1961        $hookContainer->run( 'PageForms::RenderingEnd', [ &$form_text ] );
1962
1963        // Send the autocomplete values to the browser, along with the
1964        // mappings of which values should apply to which fields.
1965        // If doing a replace, the page text is actually the modified
1966        // original page.
1967        if ( !$is_embedded ) {
1968            $form_page_title = self::getParsedValue( $parser, str_replace( "{{!}}", "|", $form_page_title ) );
1969        } else {
1970            $form_page_title = null;
1971        }
1972
1973        return [ $form_text, $page_text, $form_page_title, $generated_page_name ];
1974    }
1975
1976    /**
1977     * Cargo based mapping compatible with autocompletion
1978     * @param string $currentValue
1979     * @param string $mappingCargoTable
1980     * @param string $mappingCargoField
1981     * @param string $mappingCargoValueField
1982     * @param PFFormField $form_field
1983     * @return string|null $currentValue
1984     */
1985    private function getCargoBasedMapping( $currentValue, $mappingCargoTable, $mappingCargoField, $mappingCargoValueField, $form_field ) {
1986        $cargoValues = [];
1987        $delimiter = $form_field->getFieldArg( 'delimiter' );
1988        $cur_value = str_replace( '"', '\"', $currentValue );
1989        if ( $form_field->isList() ) {
1990            $cargoValues = explode( $delimiter, $currentValue );
1991        } else {
1992            $cargoValues = [ $currentValue ];
1993        }
1994        foreach ( $cargoValues as $key => $value ) {
1995            $cargoValue = PFValuesUtils::getValuesForCargoField( $mappingCargoTable, $mappingCargoField, $mappingCargoValueField . '="' . trim( $value ) . '"' );
1996            if ( !empty( $cargoValue ) ) {
1997                $cargoValues[ $key ] = current( $cargoValue );
1998            }
1999        }
2000        $currentValue = implode( $delimiter, $cargoValues );
2001        return $currentValue;
2002    }
2003
2004    /**
2005     * Create the HTML to display this field within a form.
2006     * @param PFFormField $form_field
2007     * @param string $cur_value
2008     * @return string
2009     */
2010    function formFieldHTML( $form_field, $cur_value ) {
2011        global $wgPageFormsFieldNum;
2012
2013        // Also get the actual field, with all the semantic information
2014        // (type is PFTemplateField, instead of PFFormField)
2015        $template_field = $form_field->getTemplateField();
2016        $class_name = null;
2017
2018        if ( $form_field->isHidden() ) {
2019            $attribs = [];
2020            if ( $form_field->hasFieldArg( 'class' ) ) {
2021                $attribs['class'] = $form_field->getFieldArg( 'class' );
2022            }
2023            $text = Html::hidden( $form_field->getInputName(), $cur_value, $attribs );
2024        } elseif ( $form_field->getInputType() !== '' &&
2025                array_key_exists( $form_field->getInputType(), $this->mInputTypeHooks ) &&
2026                $this->mInputTypeHooks[$form_field->getInputType()] != null ) {
2027            // Last argument to constructor should be a hash,
2028            // merging the default values for this input type with
2029            // all other properties set in the form definition, plus
2030            // some semantic-related arguments.
2031            $hook_values = $this->mInputTypeHooks[$form_field->getInputType()];
2032            $class_name = $hook_values[0];
2033            $other_args = $form_field->getArgumentsForInputCall( $hook_values[1] );
2034        } else {
2035            // The input type is not defined in the form.
2036            $cargo_field_type = $template_field->getFieldType();
2037            $property_type = $template_field->getPropertyType();
2038            $is_list = ( $form_field->isList() || $template_field->isList() );
2039            if ( $cargo_field_type !== '' &&
2040                array_key_exists( $cargo_field_type, $this->mCargoTypeHooks ) &&
2041                isset( $this->mCargoTypeHooks[$cargo_field_type][$is_list] ) ) {
2042                $hook_values = $this->mCargoTypeHooks[$cargo_field_type][$is_list];
2043                $class_name = $hook_values[0];
2044                $other_args = $form_field->getArgumentsForInputCall( $hook_values[1] );
2045            } elseif ( $property_type !== '' &&
2046                array_key_exists( $property_type, $this->mSemanticTypeHooks ) &&
2047                isset( $this->mSemanticTypeHooks[$property_type][$is_list] ) ) {
2048                $hook_values = $this->mSemanticTypeHooks[$property_type][$is_list];
2049                $class_name = $hook_values[0];
2050                $other_args = $form_field->getArgumentsForInputCall( $hook_values[1] );
2051            } else {
2052                // Anything else.
2053                $class_name = 'PFTextInput';
2054                $other_args = $form_field->getArgumentsForInputCall();
2055                // Set default size for list inputs.
2056                if ( $form_field->isList() ) {
2057                    if ( !array_key_exists( 'size', $other_args ) ) {
2058                        $other_args['size'] = 100;
2059                    }
2060                }
2061            }
2062        }
2063
2064        if ( $class_name !== null ) {
2065            $form_input = new $class_name( $wgPageFormsFieldNum, $cur_value, $form_field->getInputName(), $form_field->isDisabled(), $other_args );
2066
2067            // If a regex was defined, make this a "regexp" input that wraps
2068            // around the real one.
2069            if ( $template_field->getRegex() !== null ) {
2070                $other_args['regexp'] = $template_field->getRegex();
2071                $form_input = PFRegExpInput::newFromInput( $form_input );
2072            }
2073            $form_input->addJavaScript();
2074            $text = $form_input->getHtmlText();
2075        }
2076
2077        $this->addTranslatableInput( $form_field, $text );
2078        return $text;
2079    }
2080
2081    /**
2082     * If a field is "translatable", add a hidden input containing the "<!--T:X-->"
2083     * translate tag.
2084     *
2085     * @param PFFormField $form_field
2086     * @param string &$text
2087     */
2088    private function addTranslatableInput( $form_field, &$text ) {
2089        if ( !PFUtils::isTranslateEnabled() || !$form_field->hasFieldArg( 'translatable' ) || !$form_field->getFieldArg( 'translatable' ) ) {
2090            return;
2091        }
2092
2093        if ( $form_field->hasFieldArg( 'translate_number_tag' ) ) {
2094            $inputName = $form_field->getInputName();
2095            $pattern = '/\[([^\\]\\]]+)\]$/';
2096            if ( preg_match( $pattern, $inputName, $matches ) ) {
2097                $inputName = preg_replace( $pattern, '[${1}_translate_number_tag]', $inputName );
2098            } else {
2099                $inputName .= '_translate_number_tag';
2100            }
2101            $translateTag = $form_field->getFieldArg( 'translate_number_tag' );
2102            $text .= "<input type='hidden' name='$inputName' value='$translateTag'/>";
2103        }
2104    }
2105
2106    private function createFormFieldTranslateTag( &$template, &$tif, &$form_field, &$cur_value ) {
2107        if ( !PFUtils::isTranslateEnabled() || !$form_field->hasFieldArg( 'translatable' ) || !$form_field->getFieldArg( 'translatable' ) ) {
2108            return;
2109        }
2110
2111        if ( $cur_value == null ) {
2112            return;
2113        }
2114
2115        // If translatable, add translatable tags when saving, or remove them for displaying form.
2116        if ( preg_match( '#^<translate>(.*)</translate>$#', $cur_value, $matches ) ) {
2117            $cur_value = $matches[1];
2118        } elseif ( substr( $cur_value, 0, strlen( '<translate>' ) ) == '<translate>'
2119                && substr( $cur_value, -1 * strlen( '</translate>' ) ) == '</translate>' ) {
2120            // For unknown reasons, the pregmatch regex does not work every time !! :(
2121            $cur_value = substr( $cur_value, strlen( '<translate>' ), -1 * strlen( '</translate>' ) );
2122        }
2123
2124        if ( substr( $cur_value, 0, 6 ) == '<!--T:' ) {
2125            // hide the tag <!-- T:X --> in another input
2126            // if field does not use VisualEditor?
2127
2128            if ( preg_match( "/<!-- *T:([a-zA-Z0-9]+) *-->( |\n)/", $cur_value, $matches ) ) {
2129                // Remove the tag from this input.
2130                $cur_value = str_replace( $matches[0], '', $cur_value );
2131                // Add a field arg, to add a hidden input in form with the tag.
2132                $form_field->setFieldArg( 'translate_number_tag', $matches[0] );
2133            }
2134        }
2135    }
2136
2137    private static function generateUUID() {
2138        // Copied from https://www.php.net/manual/en/function.uniqid.php#94959
2139        return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
2140            // 32 bits for "time_low"
2141            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
2142            // 16 bits for "time_mid"
2143            mt_rand( 0, 0xffff ),
2144            // 16 bits for "time_hi_and_version",
2145            // four most significant bits holds version number 4
2146            mt_rand( 0, 0x0fff ) | 0x4000,
2147            // 16 bits, 8 bits for "clk_seq_hi_res",
2148            // 8 bits for "clk_seq_low",
2149            // two most significant bits holds zero and one for variant DCE1.1
2150            mt_rand( 0, 0x3fff ) | 0x8000,
2151            // 48 bits for "node"
2152            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
2153        );
2154    }
2155
2156    /**
2157     * Cache parsed values as much as possible, to avoid computing-
2158     * intensive parsing.
2159     *
2160     * @param Parser $parser
2161     * @param string $value
2162     * @return string
2163     */
2164    public static function getParsedValue( $parser, $value ) {
2165        if ( !array_key_exists( $value, self::$mParsedValues ) ) {
2166            self::$mParsedValues[$value] = trim( $parser->recursiveTagParse( $value ) );
2167        }
2168
2169        return self::$mParsedValues[$value];
2170    }
2171
2172}