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