From 4a418c75107a535b048a55f52f98775786bcfe2c Mon Sep 17 00:00:00 2001 From: ShevArtV Date: Sun, 21 Jun 2026 13:53:00 +0300 Subject: [PATCH] Fix infinite recursion when a static element is loaded during pdoToolsOnFenomInit CoreTools::getFenom() resolves the shared `fenom` service from the container, whose factory builds the Fenom instance. The Fenom constructor fired the pdoToolsOnFenomInit event before the container had memoized the instance, so any plugin attached to that event which re-enters getFenom() caused the factory to run again -> a new Fenom -> the event again -> ... until the PHP memory limit was exhausted. This is reliably triggered with pdotools_fenom_parser=1: loading a static element resolves its file through a media source, and modElement::getSourceFile() parses the source properties with the (pdo) parser, which calls getFenom(). If a static plugin is bound to pdoToolsOnFenomInit, the page dies with a fatal "Allowed memory size exhausted". Fire pdoToolsOnFenomInit once from getFenom(), after the instance is stored in the services container, guarded by a flag. A re-entrant getFenom() then returns the already-registered instance instead of constructing a new one. Co-Authored-By: Claude Opus 4.8 (1M context) --- core/components/pdotools/docs/changelog.txt | 4 ++++ core/components/pdotools/src/CoreTools.php | 19 ++++++++++++++++++- .../pdotools/src/Parsing/Fenom/Fenom.php | 14 +++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/core/components/pdotools/docs/changelog.txt b/core/components/pdotools/docs/changelog.txt index 65ca7a3a..acdb8fb8 100644 --- a/core/components/pdotools/docs/changelog.txt +++ b/core/components/pdotools/docs/changelog.txt @@ -1,5 +1,9 @@ Changelog for pdoTools. +Unreleased +============== +- Fixed infinite recursion (memory exhaustion) when a plugin attached to the pdoToolsOnFenomInit event re-enters CoreTools::getFenom() while Fenom is being constructed (e.g. loading a static element with the pdoParser active). The event is now fired once from getFenom() after the Fenom instance is registered in the services container. + 3.0.2-pl (11.04.2023) ============== - Support for file snippets in the pdoPage class. diff --git a/core/components/pdotools/src/CoreTools.php b/core/components/pdotools/src/CoreTools.php index 98ea5922..3ec5d9d9 100644 --- a/core/components/pdotools/src/CoreTools.php +++ b/core/components/pdotools/src/CoreTools.php @@ -20,6 +20,8 @@ class CoreTools protected $modx; /** @var Fenom $fenom */ protected $fenom; + /** @var bool $fenomInitialized Whether the pdoToolsOnFenomInit event has been fired */ + protected $fenomInitialized = false; /** @var array $config Array with class config */ protected $config = []; /** @var array $store Array for cache elements and user data */ @@ -146,7 +148,22 @@ public function getFenom() $this->modx->services->add('fenom', new $class($this->modx, $this)); } - return $this->modx->services->get('fenom'); + /** @var Fenom $fenom */ + $fenom = $this->modx->services->get('fenom'); + + // Fire pdoToolsOnFenomInit only once, and only AFTER the instance has been + // stored in the services container. If the event were fired from the Fenom + // constructor (i.e. before the container memoizes the instance), a plugin + // attached to pdoToolsOnFenomInit that re-enters getFenom() - for example the + // pdoParser parsing media source properties while a static element is being + // loaded - would build another Fenom and recurse until the memory limit is + // exhausted. + if (!$this->fenomInitialized) { + $this->fenomInitialized = true; + $fenom->invokeInitEvent(); + } + + return $fenom; } /** diff --git a/core/components/pdotools/src/Parsing/Fenom/Fenom.php b/core/components/pdotools/src/Parsing/Fenom/Fenom.php index ae09ed34..a67ec269 100644 --- a/core/components/pdotools/src/Parsing/Fenom/Fenom.php +++ b/core/components/pdotools/src/Parsing/Fenom/Fenom.php @@ -60,12 +60,24 @@ public function __construct(modX $modx, CoreTools $pdoTools) $this->setOptions($options); $this->_addDefaultModifiers(); + } + /** + * Fire the pdoToolsOnFenomInit event so plugins can extend Fenom + * (register custom modifiers, functions, etc). + * + * This is intentionally NOT called from the constructor. It must be invoked + * by CoreTools::getFenom() AFTER the instance has been registered in the + * services container - otherwise a plugin attached to pdoToolsOnFenomInit + * that re-enters getFenom() would build another Fenom and recurse endlessly. + */ + public function invokeInitEvent() + { $this->modx->invokeEvent( 'pdoToolsOnFenomInit', [ 'fenom' => $this, - 'config' => $pdoTools->config(), + 'config' => $this->pdoTools->config(), ] ); }