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