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