diff --git a/App/Media.php b/App/Media.php new file mode 100644 index 0000000..e7d3b17 --- /dev/null +++ b/App/Media.php @@ -0,0 +1,234 @@ +snapshotAreaCode(); + $this->writeAreaCode(null); + + try { + $configCacheFile = $this->filesystem + ->getDirectoryRead(DirectoryList::VAR_DIR) + ->getAbsolutePath(self::CONFIG_CACHE_FILENAME); + $mediaDirectory = $this->readCachedMediaDirectory($configCacheFile); + $relativePath = $this->extractRelativePath((string) $this->request->getRequestUri()); + + $media = $this->createMagentoMedia( + mediaDirectory: $mediaDirectory, + configCacheFile: $configCacheFile, + relativeFileName: $relativePath, + ); + + return $media->launch(); + } finally { + // Always restore the area code so concurrent /index.php or /static.php + // requests serviced by the same worker pool see the area BootstrapPool + // set up for them, not the GLOBAL Magento Media transiently selects. + $this->writeAreaCode($savedAreaCode); + } + } + + /** + * @inheritDoc + */ + #[\Override] + public function catchException(Bootstrap $bootstrap, Exception $exception): bool + { + $this->logger->critical($exception->getMessage(), ['exception' => $exception]); + $this->response->setHttpResponseCode(404); + $this->response->sendResponse(); + + return true; + } + + /** + * Construct the stock Magento media app with the request-derived arguments. + * + * The {@see $isAllowed} closure's two-argument signature matches the call + * site inside Magento\MediaStorage\App\Media::launch(): + * `$isAllowed($fileRelativePath, $allowedResources)` + */ + private function createMagentoMedia( + ?string $mediaDirectory, + string $configCacheFile, + string $relativeFileName, + ): MagentoMedia { + $isAllowed = static function (string $resource, array $allowedResources): bool { + foreach ($allowedResources as $allowed) { + if (0 === stripos($resource, (string) $allowed)) { + return true; + } + } + return false; + }; + + /** @var MagentoMedia $media */ + $media = $this->objectManager->create(MagentoMedia::class, [ + 'mediaDirectory' => $mediaDirectory, + 'configCacheFile' => $configCacheFile, + 'isAllowed' => $isAllowed, + 'relativeFileName' => $relativeFileName, + ]); + + return $media; + } + + /** + * Read the cached media_directory value from var/resource_config.json. + * Returns null if absent or malformed — Magento\MediaStorage\App\Media + * tolerates a null mediaDirectory and triggers a config rebuild itself. + */ + private function readCachedMediaDirectory(string $configCacheFile): ?string + { + if (!file_exists($configCacheFile)) { + return null; + } + $cached = json_decode((string) file_get_contents($configCacheFile), true); + if (!is_array($cached)) { + return null; + } + $mediaDirectory = (string) ($cached['media_directory'] ?? ''); + + return $mediaDirectory !== '' ? $mediaDirectory : null; + } + + /** + * Mirror stock pub/get.php's relative-path derivation: strip `../` + * traversal attempts and prune query strings, except for /static/ + * subpaths that legitimately carry version/sourcemap identifiers. + */ + private function extractRelativePath(string $requestUri): string + { + $relativePath = str_replace('../', '', $requestUri); + if (false === stripos($relativePath, '/static/version') + && false === stripos($relativePath, '/static/sourcemaps') + && false === stripos($relativePath, '/static/_cache/merged') + ) { + $relativePath = (string) preg_replace('/\?.*/', '', $relativePath); + } + + return trim($relativePath, '/'); + } + + /** + * Read the current raw `_areaCode` value via reflection. Returns null if + * never set. We bypass {@see State::getAreaCode()} because that throws + * a LocalizedException when the code is unset — undesirable for a snapshot + * that needs to support the "first request after worker boot" case. + */ + private function snapshotAreaCode(): ?string + { + $ref = new ReflectionObject($this->state); + if (!$ref->hasProperty('_areaCode')) { + return null; + } + $prop = $ref->getProperty('_areaCode'); + $prop->setAccessible(true); + + /** @var ?string $value */ + $value = $prop->getValue($this->state); + + return $value; + } + + /** + * Write the raw `_areaCode` value via reflection. Passing null clears it + * so a subsequent setAreaCode() call succeeds. Defensive against framework + * field renames: silently no-op if the property is missing. + */ + private function writeAreaCode(?string $value): void + { + $ref = new ReflectionObject($this->state); + if ($ref->hasProperty('_areaCode')) { + $prop = $ref->getProperty('_areaCode'); + $prop->setAccessible(true); + $prop->setValue($this->state, $value); + } + // Also reset the emulation flag — Media doesn't go through + // emulateAreaCode(), so this flag should always be false in its scope. + if ($ref->hasProperty('_isAreaCodeEmulated')) { + $prop = $ref->getProperty('_isAreaCodeEmulated'); + $prop->setAccessible(true); + $prop->setValue($this->state, false); + } + } +} diff --git a/composer.json b/composer.json index 3945d83..936fe7b 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,9 @@ ], "require": { "php": "^8.3", - "psr/log": "*", - "magento/framework": "*", - "magento/module-media-storage": "*" + "psr/log": "^3.0", + "magento/framework": "^103.0", + "magento/module-media-storage": "^100.4" }, "require-dev": { "magento/magento-coding-standard": "^33",