Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.33% covered (danger)
28.33%
68 / 240
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFHooks
28.33% covered (danger)
28.33%
68 / 240
0.00% covered (danger)
0.00%
0 / 15
1873.63
0.00% covered (danger)
0.00%
0 / 1
 registerExtension
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 initialize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 registerModules
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 registerNamespaces
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 registerFunctions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 setGlobalJSVariables
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 registerPageSchemasClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addToAdminLinks
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 addToCargoTablesColumns
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 addToCargoTablesLinks
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 addToCargoTablesRow
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 showFormPreview
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 setPostEditCookie
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 handleForceReload
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 renamePageBasedOnFormula
82.93% covered (warning)
82.93%
68 / 82
0.00% covered (danger)
0.00%
0 / 1
37.10
1<?php
2/**
3 * Static functions called by various outside hooks, as well as by
4 * extension.json.
5 *
6 * @author Yaron Koren
7 * @file
8 * @ingroup PF
9 */
10
11use MediaWiki\EditPage\EditPage;
12use MediaWiki\Html\Html;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\ResourceLoader\ResourceLoader;
15use MediaWiki\StubObject\StubObject;
16use MediaWiki\Title\Title;
17
18class PFHooks {
19
20    /**
21     * Used for caching by addToCargoTablesLinks().
22     */
23    private static $mMultiPageEditPage = null;
24
25    public static function registerExtension() {
26        if ( defined( 'PF_VERSION' ) ) {
27            // Do not load Page Forms more than once.
28            return 1;
29        }
30
31        define( 'PF_VERSION', '6.0.11' );
32
33        $GLOBALS['wgPageFormsIP'] = dirname( __DIR__ ) . '/../';
34
35        /**
36         * This is a delayed init that makes sure that MediaWiki is set
37         * up properly before we add our stuff.
38         */
39
40        if ( defined( 'SMW_VERSION' ) || ExtensionRegistry::getInstance()->isLoaded( 'SemanticMediaWiki' ) ) {
41            $GLOBALS['wgSpecialPages']['CreateProperty'] = 'PFCreateProperty';
42            $GLOBALS['wgAutoloadClasses']['PFCreateProperty'] = __DIR__ . '/../specials/PFCreateProperty.php';
43            $GLOBALS['smwgEnabledSpecialPage'][] = 'RunQuery';
44        }
45
46        // @TODO - rename this variable to something like $wgPageFormsEDValues.
47        // (This was formerly an External Data global variable.)
48        if ( method_exists( 'EDParserFunctions', 'getAllValues' ) ) {
49            $GLOBALS['edgValues'] = EDParserFunctions::getAllValues();
50        } else {
51            $GLOBALS['edgValues'] = [];
52        }
53
54        // Allow for popup windows for file upload
55        $GLOBALS['wgEditPageFrameOptions'] = 'SAMEORIGIN';
56    }
57
58    public static function initialize() {
59        $GLOBALS['wgPageFormsScriptPath'] = $GLOBALS['wgExtensionAssetsPath'] . '/PageForms';
60
61        // This global variable is needed so that other
62        // extensions can hook into it to add their own
63        // input types.
64        $GLOBALS['wgPageFormsFormPrinter'] = new StubObject( 'wgPageFormsFormPrinter', 'PFFormPrinter' );
65    }
66
67    /**
68     * Called by ResourceLoaderRegisterModules hook.
69     *
70     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
71     *
72     * @param ResourceLoader $resourceLoader The ResourceLoader object
73     */
74    public static function registerModules( ResourceLoader $resourceLoader ) {
75        // These used to use a value of __DIR__ for 'localBasePath',
76        // but apparently in some installations that had a value of
77        // /PageForms/libs and in others just /PageForms, so we'll set
78        // the value here instead.
79        $pageFormsDir = __DIR__ . '/..';
80
81        $mapsModuleAttrs = [
82            'localBasePath' => $pageFormsDir,
83            'remoteExtPath' => 'PageForms',
84            'dependencies' => [ 'oojs-ui.styles.icons-location' ]
85        ];
86
87        if ( ExtensionRegistry::getInstance()->isLoaded( 'OpenLayers' ) ) {
88            $mapsModuleAttrs['scripts'] = '/libs/PF_maps.offline.js';
89            $mapsModuleAttrs['dependencies'][] = 'ext.openlayers.main';
90        } else {
91            $mapsModuleAttrs['scripts'] = '/libs/PF_maps.js';
92        }
93
94        $resourceLoader->register( [ 'ext.pageforms.maps' => $mapsModuleAttrs ] );
95    }
96
97    /**
98     * Register the namespaces for Page Forms.
99     * @see https://www.mediawiki.org/wiki/Manual:Hooks/CanonicalNamespaces
100     *
101     * @since 2.4.1
102     *
103     * @param array &$list
104     */
105    public static function registerNamespaces( array &$list ) {
106        global $wgNamespacesWithSubpages;
107
108        if ( !defined( 'PF_NS_FORM' ) ) {
109            define( 'PF_NS_FORM', 106 );
110            define( 'PF_NS_FORM_TALK', 107 );
111        }
112
113        $list[PF_NS_FORM] = 'Form';
114        $list[PF_NS_FORM_TALK] = 'Form_talk';
115
116        // Support subpages only for talk pages by default
117        $wgNamespacesWithSubpages[PF_NS_FORM_TALK] = true;
118    }
119
120    /**
121     * Called by the ParserFirstCallInit hook.
122     *
123     * @param Parser $parser
124     */
125    static function registerFunctions( Parser $parser ) {
126        $parser->setFunctionHook( 'default_form', [ 'PFDefaultForm', 'run' ] );
127        $parser->setFunctionHook( 'forminput', [ 'PFFormInputParserFunction', 'run' ] );
128        $parser->setFunctionHook( 'formlink', [ 'PFFormLink', 'run' ] );
129        $parser->setFunctionHook( 'formredlink', [ 'PFFormRedLink', 'run' ] );
130        $parser->setFunctionHook( 'queryformlink', [ 'PFQueryFormLink', 'run' ] );
131        $parser->setFunctionHook( 'arraymap', [ 'PFArrayMap', 'run' ], Parser::SFH_OBJECT_ARGS );
132        $parser->setFunctionHook( 'arraymaptemplate', [ 'PFArrayMapTemplate', 'run' ], Parser::SFH_OBJECT_ARGS );
133
134        $parser->setFunctionHook( 'autoedit', [ 'PFAutoEdit', 'run' ] );
135        $parser->setFunctionHook( 'autoedit_rating', [ 'PFAutoEditRating', 'run' ] );
136        $parser->setFunctionHook( 'template_params', [ 'PFTemplateParams', 'run' ] );
137        $parser->setFunctionHook( 'template_display', [ 'PFTemplateDisplay', 'run' ], Parser::SFH_OBJECT_ARGS );
138        $parser->setFunctionHook( 'page_name_formula', [ 'PFPageNameFormula', 'run' ] );
139    }
140
141    /**
142     * Called by the MakeGlobalVariablesScript hook.
143     *
144     * @param array &$vars
145     */
146    static function setGlobalJSVariables( &$vars ) {
147        global $wgPageFormsTargetName;
148        global $wgPageFormsAutocompleteValues, $wgPageFormsAutocompleteOnAllChars;
149        global $wgPageFormsFieldProperties, $wgPageFormsCargoFields, $wgPageFormsDependentFields;
150        global $wgPageFormsGridValues, $wgPageFormsGridParams;
151        global $wgPageFormsCalendarValues, $wgPageFormsCalendarParams, $wgPageFormsCalendarHTML;
152        global $wgPageFormsContLangYes, $wgPageFormsContLangNo, $wgPageFormsContLangMonths;
153        global $wgPageFormsHeightForMinimizingInstances, $wgPageFormsDelayReload;
154        global $wgPageFormsShowOnSelect, $wgPageFormsScriptPath;
155        global $edgValues, $wgPageFormsEDSettings;
156        global $wgAmericanDates;
157
158        $vars['wgPageFormsTargetName'] = $wgPageFormsTargetName;
159        $vars['wgPageFormsAutocompleteValues'] = $wgPageFormsAutocompleteValues;
160        $vars['wgPageFormsAutocompleteOnAllChars'] = $wgPageFormsAutocompleteOnAllChars;
161        $vars['wgPageFormsFieldProperties'] = $wgPageFormsFieldProperties;
162        $vars['wgPageFormsCargoFields'] = $wgPageFormsCargoFields;
163        $vars['wgPageFormsDependentFields'] = $wgPageFormsDependentFields;
164        $vars['wgPageFormsCalendarValues'] = $wgPageFormsCalendarValues;
165        $vars['wgPageFormsCalendarParams'] = $wgPageFormsCalendarParams;
166        $vars['wgPageFormsCalendarHTML'] = $wgPageFormsCalendarHTML;
167        $vars['wgPageFormsGridValues'] = $wgPageFormsGridValues;
168        $vars['wgPageFormsGridParams'] = $wgPageFormsGridParams;
169        $vars['wgPageFormsContLangYes'] = $wgPageFormsContLangYes;
170        $vars['wgPageFormsContLangNo'] = $wgPageFormsContLangNo;
171        $vars['wgPageFormsContLangMonths'] = $wgPageFormsContLangMonths;
172        $vars['wgPageFormsHeightForMinimizingInstances'] = $wgPageFormsHeightForMinimizingInstances;
173        $vars['wgPageFormsDelayReload'] = $wgPageFormsDelayReload;
174        $vars['wgPageFormsShowOnSelect'] = $wgPageFormsShowOnSelect;
175        $vars['wgPageFormsScriptPath'] = $wgPageFormsScriptPath;
176        $vars['edgValues'] = $edgValues;
177        $vars['wgPageFormsEDSettings'] = $wgPageFormsEDSettings;
178        $vars['wgAmericanDates'] = $wgAmericanDates;
179    }
180
181    /**
182     * Called by the PageSchemasRegisterHandlers hook.
183     */
184    public static function registerPageSchemasClass() {
185        global $wgPageSchemasHandlerClasses;
186        $wgPageSchemasHandlerClasses[] = 'PFPageSchemas';
187    }
188
189    /**
190     * Called by the AdminLinks hook.
191     *
192     * @param ALTree &$admin_links_tree
193     */
194    public static function addToAdminLinks( &$admin_links_tree ) {
195        $data_structure_label = wfMessage( 'pf-adminlinks-datastructure' )->escaped();
196        $data_structure_section = $admin_links_tree->getSection( $data_structure_label );
197        if ( $data_structure_section === null ) {
198            $data_structure_section = new ALSection( wfMessage( 'pf-adminlinks-datastructure' )->escaped() );
199        }
200
201        $pf_row = new ALRow( 'pageforms' );
202        $pf_row->addItem( ALItem::newFromSpecialPage( 'Categories' ) );
203        $data_structure_section->addRow( $pf_row );
204        $pf_admin_row = new ALRow( 'pageforms_admin' );
205        $data_structure_section->addRow( $pf_admin_row );
206
207        $admin_links_tree->addSection( $data_structure_section, wfMessage( 'adminlinks_browsesearch' )->escaped() );
208
209        $pf_row->addItem( ALItem::newFromSpecialPage( 'Templates' ), 'Properties' );
210        $pf_row->addItem( ALItem::newFromSpecialPage( 'Forms' ), 'SemanticStatistics' );
211        $pf_row->addItem( ALItem::newFromSpecialPage( 'MultiPageEdit' ) );
212        $pf_admin_row->addItem( ALItem::newFromSpecialPage( 'CreateClass' ), 'SMWAdmin' );
213        if ( class_exists( 'PFCreateProperty' ) ) {
214            $pf_admin_row->addItem( ALItem::newFromSpecialPage( 'CreateProperty' ), 'SMWAdmin' );
215        }
216        $pf_admin_row->addItem( ALItem::newFromSpecialPage( 'CreateTemplate' ), 'SMWAdmin' );
217        $pf_admin_row->addItem( ALItem::newFromSpecialPage( 'CreateForm' ), 'SMWAdmin' );
218        $pf_admin_row->addItem( ALItem::newFromSpecialPage( 'CreateCategory' ), 'SMWAdmin' );
219    }
220
221    /**
222     * Called by the CargoTablesSetAllowedActions hook.
223     *
224     * @param CargoTables $cargoTablesPage
225     * @param array &$allowedActions
226     */
227    public static function addToCargoTablesColumns( $cargoTablesPage, &$allowedActions ) {
228        if ( !$cargoTablesPage->getUser()->isAllowed( 'multipageedit' ) ) {
229            return;
230        }
231
232        $cargoTablesPage->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-editing-core' ] );
233
234        $editColumn = [ 'edit' => [ 'ooui-icon' => 'edit', 'ooui-title' => 'edit' ] ];
235        $indexOfDrilldown = array_search( 'drilldown', array_keys( $allowedActions ) );
236        $pos = $indexOfDrilldown === false ? count( $allowedActions ) : $indexOfDrilldown + 1;
237        $allowedActions = array_merge( array_slice( $allowedActions, 0, $pos ), $editColumn, array_slice( $allowedActions, $pos ) );
238    }
239
240    /**
241     * Called by the CargoTablesActionLinks hook.
242     *
243     * Adds an "Edit" link to Special:CargoTables, pointing to Special:MultiPageEdit.
244     *
245     * @param array &$actionLinks Action links
246     * @param string $tableName Cargo table name
247     * @param bool $isReplacementTable Whether this table is a replacement table
248     * @param bool $hasReplacementTable Whether this table has a replacement table
249     * @param int[][] $templatesThatDeclareTables
250     * @param string[] $templatesThatAttachToTables
251     * @param User|null $user The current user
252     *
253     * @since 4.4
254     */
255    public static function addToCargoTablesLinks( &$actionLinks, $tableName, $isReplacementTable, $hasReplacementTable, $templatesThatDeclareTables, $templatesThatAttachToTables, $user = null ) {
256        // If it has a "replacement table", it's read-only and can't
257        // be edited (though the replacement table can).
258        if ( $hasReplacementTable ) {
259            return;
260        }
261
262        // Check permissions.
263        if ( $user == null ) {
264            // For Cargo versions < 3.1.
265            $user = RequestContext::getMain()->getUser();
266        }
267
268        if ( !$user->isAllowed( 'multipageedit' ) ) {
269            return;
270        }
271        // Only put in an "Edit" link if there's exactly one template
272        // for this Cargo table, and one form for that template.
273        if ( !array_key_exists( $tableName, $templatesThatDeclareTables ) ) {
274            return;
275        }
276        if ( array_key_exists( $tableName, $templatesThatAttachToTables ) ) {
277            return;
278        }
279        $templateIDs = $templatesThatDeclareTables[$tableName];
280        if ( count( $templateIDs ) > 1 ) {
281            return;
282        }
283
284        $templateTitle = Title::newFromID( $templateIDs[0] );
285        $templateName = $templateTitle->getText();
286        if ( self::$mMultiPageEditPage == null ) {
287            self::$mMultiPageEditPage = new PFMultiPageEdit();
288            self::$mMultiPageEditPage->setTemplateList();
289        }
290        $formName = self::$mMultiPageEditPage->getFormForTemplate( $templateName );
291        if ( $formName == null ) {
292            return;
293        }
294
295        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
296        $sp = PFUtils::getSpecialPage( 'MultiPageEdit' );
297        $editMsg = wfMessage( 'edit' )->text();
298        $linkParams = [ 'template' => $templateName, 'form' => $formName ];
299        $text = $linkRenderer->makeKnownLink( $sp->getPageTitle(), $editMsg, [], $linkParams );
300
301        $indexOfDrilldown = array_search( 'drilldown', array_keys( $actionLinks ) );
302        $pos = $indexOfDrilldown === false ? count( $actionLinks ) : $indexOfDrilldown + 1;
303        $actionLinks = array_merge( array_slice( $actionLinks, 0, $pos ), [ 'edit' => $text ], array_slice( $actionLinks, $pos ) );
304    }
305
306    /**
307     * Called by the CargoTablesSetActionLinks hook.
308     *
309     * Adds an "Edit" link to Special:CargoTables, pointing to Special:MultiPageEdit.
310     *
311     * @param CargoTables $cargoTablesPage
312     * @param array &$actionLinks Action links
313     * @param string $tableName Cargo table name
314     * @param bool $isReplacementTable Whether this table iss a replacement table
315     * @param bool $hasReplacementTable Whether this table has a replacement table
316     * @param int[][] $templatesThatDeclareTables
317     * @param string[] $templatesThatAttachToTables
318     * @param string[] $actionList
319     * @param User|null $user The current user
320     *
321     * @since 4.8.1
322     */
323    public static function addToCargoTablesRow( $cargoTablesPage, &$actionLinks, $tableName, $isReplacementTable, $hasReplacementTable, $templatesThatDeclareTables, $templatesThatAttachToTables, $actionList, $user = null ) {
324        $cargoTablesPage->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-editing-core' ] );
325
326        // For the sake of simplicity, this function basically just
327        // wraps around the previous hook function, for Cargo <= 2.4.
328        // That's why there's this awkward behavior of parsing links
329        // to get their URL. Hopefully this won't cause problems.
330        self::addToCargoTablesLinks( $actionLinks, $tableName, $isReplacementTable, $hasReplacementTable, $templatesThatDeclareTables, $templatesThatAttachToTables, $user );
331
332        if ( array_key_exists( 'edit', $actionLinks ) ) {
333            preg_match( '/href="(.*?)"/', $actionLinks['edit'], $matches );
334            $mpeURL = html_entity_decode( $matches[1] );
335            $actionLinks['edit'] = $cargoTablesPage->getActionButton( 'edit', $mpeURL );
336        }
337    }
338
339    /**
340     * Called by the EditPage::importFormData hook.
341     *
342     * @param EditPage $editpage
343     * @param WebRequest $request
344     */
345    public static function showFormPreview( EditPage $editpage, WebRequest $request ) {
346        global $wgPageFormsFormPrinter;
347
348        wfDebug( __METHOD__ . ": enter.\n" );
349
350        // Exit if we're not in preview mode.
351        if ( !$editpage->preview ) {
352            return;
353        }
354        // Exit if we aren't in the "Form" namespace.
355        if ( $editpage->getArticle()->getTitle()->getNamespace() != PF_NS_FORM ) {
356            return;
357        }
358
359        // Needed in case there are any OOUI-based input types in the form.
360        $out = $editpage->getContext()->getOutput();
361        $out->enableOOUI();
362
363        $previewNote = $out->parseAsInterface( wfMessage( 'pf-preview-note' )->text() );
364        // The "pfForm" ID is there so the form JS will be activated.
365        $editpage->previewTextAfterContent .= Html::element( 'h2', [], wfMessage( 'pf-preview-header' )->text() ) . "\n" .
366            '<div id="pfForm" class="previewnote" style="font-weight: bold">' . $previewNote . "</div>\n<hr />\n";
367
368        $form_definition = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $editpage->textbox1 );
369        [ $form_text, $data_text, $form_page_title, $generated_page_name ] =
370            $wgPageFormsFormPrinter->formHTML(
371                $editpage->getContext(), $form_definition, null, false, null, null, "Page Forms form preview dummy title", null );
372
373        $parserOutput = PFUtils::getParser()->getOutput();
374        $out->addParserOutputMetadata( $parserOutput );
375
376        PFUtils::addFormRLModules( $out );
377        $editpage->previewTextAfterContent .=
378            '<div style="margin-top: 15px">' . $form_text . "</div>";
379    }
380
381    /**
382     * Called by the PageSaveComplete hook.
383     *
384     * Set a cookie after the page save so that a "Your edit was saved"
385     * popup will appear after form-based saves, just as it does after
386     * standard saves. This code will be called after all saves, which
387     * means that it will lead to redundant cookie-setting after normal
388     * saves. However, there doesn't appear to be a way to to set the
389     * cookie correctly only after form-based saves, unfortunately.
390     *
391     * @param WikiPage $wikiPage
392     * @param MediaWiki\User\UserIdentity $user
393     * @param string $summary
394     * @param int $flags
395     * @param MediaWiki\Revision\RevisionRecord $revisionRecord
396     * @param MediaWiki\Storage\EditResult $editResult
397     */
398    public static function setPostEditCookie( WikiPage $wikiPage, MediaWiki\User\UserIdentity $user, string $summary, int $flags,
399        MediaWiki\Revision\RevisionRecord $revisionRecord, MediaWiki\Storage\EditResult $editResult
400    ) {
401        // Have this take effect only if the save came from a form -
402        // we need to use a global variable to determine that.
403        global $wgPageFormsFormPrinter;
404        if ( !property_exists( $wgPageFormsFormPrinter, 'mInputTypeHooks' ) ) {
405            return;
406        }
407
408        // Code based loosely on EditPage::setPostEditCookie().
409        $postEditKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $revisionRecord->getID();
410        $response = RequestContext::getMain()->getRequest()->response();
411        $response->setCookie( $postEditKey, 'saved', time() + EditPage::POST_EDIT_COOKIE_DURATION );
412    }
413
414    /**
415     * Called by the BeforePageDisplay hook.
416     *
417     * Reload the page if "forceReload=true" exists in the URL query string.
418     * This is a @hack done so that the $wgPageFormsDelayReload setting
419     * (itself a hack) can take effect - in some cases, having a #formlink
420     * call with "returnto=" and "reload" both set does not actually
421     * refresh the queries on the original page in time, so we use this to
422     * then reload the original page, which does seem to work. There may
423     * well be a better solution for this, though.
424     *
425     * @param MediaWiki\Output\OutputPage $out
426     * @param Skin $skin
427     */
428    public static function handleForceReload( $out, Skin $skin ) {
429        global $wgPageFormsScriptPath;
430
431        if ( $out->getRequest()->getVal( 'forceReload' ) !== 'true' ) {
432            return;
433        }
434
435        $out->clearHTML();
436        $loadingImage = Html::element( 'img', [ 'src' => "$wgPageFormsScriptPath/skins/loading.gif" ] );
437        $text = "\t" . Html::rawElement( 'p', [ 'style' => "position: absolute; left: 45%; top: 45%;" ], $loadingImage );
438        $reloadURL = $out->getTitle()->getFullURL();
439        $text .= Html::element( 'meta', [ 'http-equiv' => 'refresh', 'content' => "0; url=$reloadURL" ] );
440
441        $out->addHTML( $text );
442    }
443
444    public static function renamePageBasedOnFormula( WikiPage $wikiPage, MediaWiki\User\UserIdentity $user, string $summary, int $flags, MediaWiki\Revision\RevisionRecord $revisionRecord, MediaWiki\Storage\EditResult $editResult ) {
445        $parserOptions = \ParserOptions::newFromUser( $user );
446        $parserOutput = $wikiPage->getParserOutput( $parserOptions );
447        if ( !$parserOutput ) {
448            return;
449        }
450        $formula = $parserOutput->getExtensionData( 'PFPageNameFormula' );
451        if ( !$formula ) {
452            return;
453        }
454
455        $title = $wikiPage->getTitle();
456        $content = $revisionRecord->getContent( \MediaWiki\Revision\SlotRecord::MAIN );
457        if ( !$content || !$content instanceof \MediaWiki\Content\TextContent ) {
458            return;
459        }
460        $text = $content->getText();
461        $generated_page_name = $formula;
462
463        preg_match_all( "/<([^\[\]]+)\[([^\[\]]+)\]>/", $formula, $matches, PREG_SET_ORDER );
464        if ( !empty( $matches ) ) {
465            $generated_page_name = str_replace( ' ', '_', $generated_page_name );
466            foreach ( $matches as $match ) {
467                $full_match = $match[0];
468                $template_name = $match[1];
469                $field_name = $match[2];
470
471                $tif = PFTemplateInForm::create( $template_name );
472                $tif->setPageRelatedInfo( $text );
473                $tif->setFieldValuesFromPage( $text );
474
475                $values = $tif->getValuesFromPage();
476                $replacement = '';
477                if ( is_array( $values ) && array_key_exists( $field_name, $values ) ) {
478                    $replacement = $values[$field_name];
479                }
480
481                $escaped_match = str_replace( ' ', '_', $full_match );
482                $generated_page_name = str_ireplace( $escaped_match, $replacement, $generated_page_name );
483            }
484            $generated_page_name = str_replace( '_', ' ', $generated_page_name );
485        }
486
487        $generated_page_name = trim( $generated_page_name );
488        if ( $generated_page_name === '' ) {
489            return;
490        }
491
492        $titleText = $title->getText();
493        $hasUniqueNumber = false;
494
495        if ( preg_match( '/<unique number(.*)>/', $generated_page_name ) ) {
496            $hasUniqueNumber = true;
497
498            // Check if the current title matches the pattern.
499            // Escape special regex characters in the formula, but preserve our placeholder.
500            $pattern = preg_quote( preg_replace( '/<unique number(.*)>/', '___UNIQUE_NUMBER___', $generated_page_name ), '/' );
501            // Replace the placeholder with a pattern that matches a number (or random digits).
502            $pattern = str_replace( '___UNIQUE_NUMBER___', '(.*)', $pattern );
503
504            if ( preg_match( '/^' . $pattern . '$/', $titleText, $numberMatches ) ) {
505                $numberPart = $numberMatches[1];
506                // If the matched part is a valid number (or empty, or random digits),
507                // then the current title is already a valid instance of this formula.
508                if ( $numberPart === '' || is_numeric( $numberPart ) ) {
509                    return;
510                }
511            }
512
513            // If we get here, the current title does not match, so we need to generate a new unique name.
514            // Replace "unique number" tag with {num...} temporarily to match PFAutoeditAPI logic.
515            $targetName = preg_replace( '/<unique number(.*)>/', '{num\1}', $generated_page_name, 1 );
516            $targetName = str_replace( ' ', '_', $targetName );
517
518            $titleNumber = '';
519            $isRandom = false;
520            $randomNumHasPadding = false;
521            $randomNumDigits = 6;
522
523            if ( preg_match( '/{num.*}/', $targetName, $matches ) && strpos( $targetName, '{num' ) !== false ) {
524                if ( preg_match( '/{num;random(;(0)?([1-9][0-9]*))?}/', $targetName, $matches ) ) {
525                    $isRandom = true;
526                    $randomNumHasPadding = array_key_exists( 2, $matches );
527                    $randomNumDigits = ( array_key_exists( 3, $matches ) ? $matches[3] : $randomNumDigits );
528                    $titleNumber = PFAutoeditAPI::makeRandomNumber( $randomNumDigits, $randomNumHasPadding );
529                } elseif ( preg_match( '/{num.*start[_]*=[_]*([^;]*).*}/', $targetName, $matches ) ) {
530                    if ( count( $matches ) == 2 && is_numeric( $matches[1] ) && $matches[1] >= 0 ) {
531                        $titleNumber = $matches[1];
532                    }
533                } elseif ( preg_match( '/^(_?{num.*}?)*$/', $targetName, $matches ) ) {
534                    $titleNumber = '1';
535                }
536
537                $targetTitle = \MediaWiki\Title\Title::newFromText( preg_replace( '/{num.*}/', $titleNumber, $targetName ) );
538
539                if ( $targetTitle ) {
540                    $numAttemptsAtTitle = 0;
541                    // Cycle through numbers until we find one that gives a nonexistent page title.
542                    while ( $targetTitle->getArticleID( \IDBAccessObject::READ_LATEST ) !== 0 ) {
543                        $numAttemptsAtTitle++;
544
545                        if ( $isRandom ) {
546                            if ( $numAttemptsAtTitle > 20 ) {
547                                $randomNumDigits++;
548                            }
549                            $titleNumber = PFAutoeditAPI::makeRandomNumber( $randomNumDigits, $randomNumHasPadding );
550                        } elseif ( $titleNumber == "" ) {
551                            $titleNumber = 2;
552                        } else {
553                            $titleNumber = str_pad( strval( $titleNumber + 1 ), strlen( $titleNumber ), '0', STR_PAD_LEFT );
554                        }
555
556                        $targetTitle = \MediaWiki\Title\Title::newFromText( preg_replace( '/{num.*}/', $titleNumber, $targetName ) );
557                    }
558
559                    $generated_page_name = $targetTitle->getText();
560                }
561            }
562        }
563
564        $newTitle = \MediaWiki\Title\Title::newFromText( $generated_page_name );
565
566        if ( $newTitle && !$newTitle->equals( $title ) && !$newTitle->exists() ) {
567            try {
568                $movePageFactory = \MediaWiki\MediaWikiServices::getInstance()->getMovePageFactory();
569                $movePage = $movePageFactory->newMovePage( $title, $newTitle );
570                $userFactory = \MediaWiki\MediaWikiServices::getInstance()->getUserFactory();
571                $authority = $userFactory->newFromUserIdentity( $user );
572                $movePage->moveIfAllowed( $authority, wfMessage( 'pf-automatic-rename' )->text(), true );
573            } catch ( \Exception ) {
574            }
575        }
576    }
577}