PHPackages                             hryvinskyi/magento2-external-media-prefetcher - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. hryvinskyi/magento2-external-media-prefetcher

ActiveMagento2-module[File &amp; Storage](/categories/file-storage)

hryvinskyi/magento2-external-media-prefetcher
=============================================

N/A

2.0.1(2mo ago)56241MITPHP

Since Apr 17Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/hryvinskyi/magento2-external-media-prefetcher)[ Packagist](https://packagist.org/packages/hryvinskyi/magento2-external-media-prefetcher)[ RSS](/packages/hryvinskyi-magento2-external-media-prefetcher/feed)WikiDiscussions master Synced today

READMEChangelog (3)Dependencies (4)Versions (6)Used By (0)

External Media Prefetcher for Magento 2
=======================================

[](#external-media-prefetcher-for-magento-2)

A Magento 2 module that automatically fetches missing media files from an external source (typically your production site) with SSRF protection, configurable retries/timeouts, an async queue mode, and placeholder-aware filtering so you never persist a CDN's "image not found" body as a real product image.

Description
-----------

[](#description)

This module solves the common problem of missing media files in development or staging environments. When a media file is requested but not found locally, the module automatically attempts to download it from a configured external URL. It hooks in at two points:

1. **`Magento\MediaStorage\Model\File\Storage\Synchronization::synchronize()`** — catches requests routed through `pub/get.php`.
2. **`Magento\Catalog\Model\Product\Image::setBaseFile()`** — catches catalog image helper calls in templates (`$imageHelper->init($product, $type)->resize(...)`) before Magento falls back to the placeholder.

Requirements
------------

[](#requirements)

- Magento 2.4.x
- PHP 8.1 or higher

Installation
------------

[](#installation)

```
composer require hryvinskyi/magento2-external-media-prefetcher
bin/magento module:enable Hryvinskyi_ExternalMediaPrefetcher
bin/magento setup:upgrade
```

Configuration
-------------

[](#configuration)

All configuration is stored in `app/etc/env.php` and can be managed through a dedicated CLI command — no admin UI is involved.

### CLI command

[](#cli-command)

```
bin/magento hryvinskyi:external-media-prefetcher:configure [options]
```

OptionDescriptionDefault`--show`Print current values and exit.—`--enabled=1|0`Enable/disable the prefetcher.`0``--url=`Base URL of the external media source (e.g. `https://cdn.example.com/media/`).—`--allowed-schemes=https,http`Comma-separated URL schemes the validator will allow.`https``--async=1|0`Dispatch downloads via the message queue instead of running inline.`0``--connect-timeout=`HTTP connect timeout in seconds.`3``--request-timeout=`HTTP request timeout in seconds.`8``--retry-count=`Retries on 5xx / 408 / 429 / transport errors. 4xx does not retry.`2``--retry-delay-ms=`Base delay for exponential backoff between retries.`200``--placeholder-hash=`Manual placeholder SHA-256 (64-char lowercase hex).—`--placeholder-length=`Manual placeholder byte length.—`--auto`Probe the external URL once and auto-fill `placeholder-hash` / `placeholder-length`. Cannot be combined with the manual flags.—Values are validated up-front: boolean flags accept `1/0/true/false/yes/no/on/off`, integer flags must be non-negative, and `--placeholder-hash` must be a 64-char lowercase hex string.

### Example: set everything in one call

[](#example-set-everything-in-one-call)

```
bin/magento hryvinskyi:external-media-prefetcher:configure \
    --enabled=1 \
    --url=https://www.example.com/media/ \
    --async=0 \
    --connect-timeout=3 \
    --request-timeout=8 \
    --retry-count=2 \
    --auto

bin/magento cache:clean config
```

The `--auto` flag probes the URL you just passed (no need to run a separate detect step) and persists the resulting placeholder signature alongside the rest of the options.

### Direct env.php editing (if you prefer)

[](#direct-envphp-editing-if-you-prefer)

```
'system' => [
    'default' => [
        'external_media_prefetcher' => [
            'general' => [
                'enabled'            => '1',
                'external_media_url' => 'https://www.example.com/media/',
                'allowed_schemes'    => 'https',
                'async_enabled'      => '0',
                'connect_timeout'    => '3',
                'request_timeout'    => '8',
                'retry_count'        => '2',
                'retry_delay_ms'     => '200',
            ],
            'placeholder' => [
                'hash'   => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
                'length' => '0',
            ],
        ],
    ],
],
```

How It Works
------------

[](#how-it-works)

### Sync mode (default)

[](#sync-mode-default)

1. A request hits a missing media file.
2. Either plugin fires (`Synchronization` or `Product\Image`), cleans the path, and checks the media directory.
3. `Security\UrlValidator` asserts the resulting URL: scheme allowlist, `FILTER_VALIDATE_URL`, and DNS resolution that rejects loopback / private / link-local / reserved IPs (SSRF protection).
4. `FileDownloader` requests the file using the configured connect/request timeouts, with exponential-backoff retries on 5xx / 408 / 429 / transport errors. 4xx responses are terminal.
5. Before writing, the response body is compared against the cached placeholder signature. If it matches, the file is **not** saved — preventing "placeholder-as-product-image" pollution when the CDN returns a generic fallback image for missing files.
6. The destination directory is `realpath`-checked to guarantee it's inside the Magento media directory (path-traversal protection).
7. The file is written and the request proceeds against the real image.

### Async mode

[](#async-mode)

When `--async=1` is set, both plugins publish the clean relative path onto the `hryvinskyi.external_media_prefetcher.download` queue (db connection) instead of fetching inline. The current request still renders the placeholder, but the next request for the same file gets the real image. Start the consumer with:

```
bin/magento queue:consumers:start hryvinskyi.external_media_prefetcher.download
```

### Placeholder detection

[](#placeholder-detection)

If you don't configure a manual signature, `PlaceholderDetector` probes the external URL once against a guaranteed-missing path (`/catalog/product/__hryvinskyi_emp_probe_.jpg`) and caches the `{hash, length}` result in Magento's app cache (tag `HRYVINSKYI_EMP_PLACEHOLDER`, TTL 1 day). Negative probes (4xx/5xx/empty) are also cached so we don't hammer the source.

Logging
-------

[](#logging)

All module output goes to `var/log/external_media_prefetcher.log` (a dedicated Monolog channel), including probe results, blocked SSRF attempts, rejected placeholder bodies, and retry attempts.

Security Notes
--------------

[](#security-notes)

- URL validation blocks `127.0.0.0/8`, RFC1918 ranges, `169.254.0.0/16`, and reserved blocks — so a compromised / misconfigured `external_media_url` cannot be used to reach internal metadata services (`169.254.169.254`, etc.) or internal hosts.
- Default allowed scheme is `https` only. Plain `http` must be opted into explicitly.
- Both destination-directory preparation and the pre-write destination check use `realpath` + prefix comparison against the media root to block `..` traversal.
- The module never trusts the source CDN's response body on its own: the placeholder signature check protects against silently caching fallback images.

Testing
-------

[](#testing)

```
ddev exec vendor/bin/phpunit \
    -c dev/tests/unit/phpunit.xml.dist \
    vendor/hryvinskyi/magento2-external-media-prefetcher/Test/Unit
```

Unit test coverage: `Config`, `PathResolver`, `FileDownloader` (happy path, SSRF rejection, placeholder skip, retry on 5xx, no-retry on 404, path-traversal), `PlaceholderDetector`(override, cache hit/miss, negative cache, probe), `UrlValidator` (scheme / loopback / private / link-local / public), `SynchronizationPlugin` (sync + async branches).

Why It's Useful
---------------

[](#why-its-useful)

- **Development and staging**: transparently pull media from production on demand.
- **Missing files**: no more broken product images without manual intervention.
- **Safe by default**: SSRF-safe, traversal-safe, placeholder-aware.
- **Tunable**: switch between inline and queue-backed downloads per environment.

License
-------

[](#license)

[MIT](LICENSE)

Author
------

[](#author)

Volodymyr Hryvinskyi Email: GitHub:

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance85

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~91 days

Total

5

Last Release

78d ago

Major Versions

1.0.2 → 2.0.02026-04-14

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/9294098?v=4)[Volodymyr Hryvinskyi](/maintainers/hryvinskyi)[@hryvinskyi](https://github.com/hryvinskyi)

---

Top Contributors

[![hryvinskyi](https://avatars.githubusercontent.com/u/9294098?v=4)](https://github.com/hryvinskyi "hryvinskyi (7 commits)")

### Embed Badge

![Health badge](/badges/hryvinskyi-magento2-external-media-prefetcher/health.svg)

```
[![Health](https://phpackages.com/badges/hryvinskyi-magento2-external-media-prefetcher/health.svg)](https://phpackages.com/packages/hryvinskyi-magento2-external-media-prefetcher)
```

###  Alternatives

[buckaroo/magento2

Buckaroo Magento 2 extension

32420.3k8](/packages/buckaroo-magento2)[baldwin/magento2-module-image-cleanup

Magento 2 module which can cleanup old image files that are no longer being used

82111.7k](/packages/baldwin-magento2-module-image-cleanup)[myparcelnl/magento

A Magento 2 module that creates MyParcel labels

1860.2k](/packages/myparcelnl-magento)[mage-os/module-automatic-translation

Automatic AI content translation for Mage-OS.

3017.9k](/packages/mage-os-module-automatic-translation)[auroraextensions/googlecloudstorage

Google Cloud Storage integration for Magento.

146.6k](/packages/auroraextensions-googlecloudstorage)[loki/magento2-components

Core module for defining Alpine.js components with advanced AJAX features

1011.8k26](/packages/loki-magento2-components)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
