Skip to content

Commit 862877a

Browse files
committed
Initial commit
0 parents  commit 862877a

File tree

14 files changed

+667
-0
lines changed

14 files changed

+667
-0
lines changed

Helper/Data.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
namespace Fruitcake\CustomImageUrl\Helper;
4+
5+
use Magento\Catalog\Model\Config\CatalogMediaConfig;
6+
use Fruitcake\CustomImageUrl\Model\Config\CustomConfig;
7+
use Magento\Catalog\Model\View\Asset\Image;
8+
use Magento\Framework\App\Helper\AbstractHelper;
9+
use Magento\Framework\App\Helper\Context;
10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Framework\UrlInterface;
12+
use Magento\Store\Model\ScopeInterface;
13+
use Magento\Store\Model\StoreManagerInterface;
14+
15+
16+
class Data extends AbstractHelper
17+
{
18+
19+
/** @var CatalogMediaConfig */
20+
private $catalogMediaConfig;
21+
22+
/** @var CustomConfig */
23+
private $customConfig;
24+
25+
/** @var StoreManagerInterface */
26+
private $storeManager;
27+
28+
public function __construct(
29+
Context $context,
30+
CatalogMediaConfig $catalogMediaConfig,
31+
CustomConfig $customConfig,
32+
StoreManagerInterface $storeManager
33+
) {
34+
parent::__construct($context);
35+
36+
$this->catalogMediaConfig = $catalogMediaConfig;
37+
$this->customConfig = $customConfig;
38+
$this->storeManager = $storeManager;
39+
}
40+
41+
public function replaceImageUrlWithCustom(Image $image, string $imageUrl)
42+
{
43+
$customType = $this->getCustomUrlType();
44+
45+
// No action required, just use the default URL
46+
if ($customType === CustomConfig::TYPE_DEFAULT) {
47+
return $imageUrl;
48+
}
49+
50+
$params = $image->getImageTransformationParameters();
51+
52+
if ($customType === CustomConfig::TYPE_PATTERN) {
53+
return $this->getCustomUrlFromPattern($imageUrl, $params);
54+
}
55+
56+
if ($customType === CustomConfig::TYPE_IMGPROXY) {
57+
return $this->getImgProxyUrl($imageUrl, $params);
58+
}
59+
60+
throw new LocalizedException(
61+
__("The specified Custom Catalog media URL type '$customType' is not supported.")
62+
);
63+
}
64+
65+
public function getCustomUrlFromPattern(string $imageUrl, array $params): string
66+
{
67+
$customUrl = $this->customConfig->getCustomPattern();
68+
69+
$urlParts = parse_url($imageUrl);
70+
$params = $params + $urlParts;
71+
72+
$params['path'] = ltrim($params['path'], '/');
73+
$params['base_url'] = $this->getBaseUrl();
74+
$params['base_url_media'] = $this->getMediaBaseUrl();
75+
$params['image_url'] = explode('?', $imageUrl)[0];
76+
77+
foreach ($params as $k => $v) {
78+
$k = str_replace('-', '_', $k);
79+
if (strpos($customUrl, '{{'.$k) !== false) {
80+
$customUrl = str_replace('{{'.$k.'}}', $v, $customUrl);
81+
$customUrl = str_replace('{{'.$k.'|urlencode}}', urlencode($v), $customUrl);
82+
$customUrl = str_replace('{{'.$k.'|rawurlencode}}', rawurlencode($v), $customUrl);
83+
$customUrl = str_replace('{{'.$k.'|base64}}', base64_encode($v), $customUrl);
84+
}
85+
}
86+
87+
return $customUrl;
88+
}
89+
90+
/**
91+
* Based on https://github.com/imgproxy/imgproxy/blob/master/examples/signature.php
92+
* @param string $imageUrl
93+
* @param array $params
94+
* @return string
95+
*/
96+
public function getImgProxyUrl(string $imageUrl, array $params)
97+
{
98+
$resize = $this->customConfig->getImgproxyResize();
99+
$width = $params['width'];
100+
$height = $params['height'];
101+
102+
$urlParts = parse_url($imageUrl);
103+
$path = ltrim($urlParts['path'], '/');
104+
$extension = pathinfo($path, PATHINFO_EXTENSION);
105+
106+
if ($this->customConfig->getImgproxySourceType() === CustomConfig::IMGPROXY_S3) {
107+
$prefix = trim($this->customConfig->getImgproxySourcePrefix(), '/');
108+
if (strlen($prefix) === 0) {
109+
throw new \RuntimeException('S3 Bucket cannot be empty');
110+
}
111+
$sourceUrl = 's3://' . $prefix . '/' . $path;
112+
} elseif ($this->customConfig->getImgproxySourceType() === CustomConfig::IMGPROXY_LOCAL) {
113+
$prefix = trim($this->customConfig->getImgproxySourcePrefix(), '/');
114+
if (strlen($prefix) > 0) {
115+
$path = $prefix . '/' . $path;
116+
}
117+
$sourceUrl = 'local:///' . $path;
118+
} else {
119+
$sourceUrl = $imageUrl;
120+
}
121+
122+
$encodedUrl = rtrim(strtr(base64_encode($sourceUrl), '+/', '-_'), '=');
123+
$path = "/resize:{$resize}:$width:$height/{$encodedUrl}.{$extension}";
124+
125+
// Sign the URL
126+
$key = $this->customConfig->getImgproxyKey();
127+
$salt = $this->customConfig->getImgproxySalt();
128+
if (strlen($key) > 0 && strlen($salt) > 0) {
129+
$path = $this->signImgproxyPath($path, $key, $salt);
130+
} else {
131+
$path = '/insecure' . $path;
132+
}
133+
134+
return rtrim($this->customConfig->getImgproxyHost(), '/') . $path;
135+
}
136+
137+
private function signImgproxyPath(string $path, string $key, string $salt): string
138+
{
139+
$keyBin = pack("H*" , $key);
140+
if(empty($keyBin)) {
141+
throw new \RuntimeException('Key expected to be hex-encoded string');
142+
}
143+
144+
$saltBin = pack("H*" , $salt);
145+
if(empty($saltBin)) {
146+
throw new \RuntimeException('Salt expected to be hex-encoded string');
147+
}
148+
149+
$signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $saltBin.$path, $keyBin, true)), '+/', '-_'), '=');
150+
151+
return sprintf("/%s%s", $signature, $path);
152+
}
153+
154+
/**
155+
* @param null|int $storeId
156+
*
157+
* @return string|null
158+
*/
159+
private function getCustomUrlType($storeId = null)
160+
{
161+
// When Dynamic hashing is disabled, return false
162+
if ($this->catalogMediaConfig->getMediaUrlFormat(ScopeInterface::SCOPE_STORE, $storeId) !== CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS) {
163+
return CustomConfig::TYPE_DEFAULT;
164+
}
165+
166+
return $this->customConfig->getCustomType($storeId);
167+
}
168+
169+
private function getBaseUrl($storeId = null)
170+
{
171+
return $this->storeManager->getStore($storeId)->getBaseUrl(UrlInterface::URL_TYPE_LINK);
172+
}
173+
174+
private function getMediaBaseUrl($storeId = null)
175+
{
176+
return $this->storeManager->getStore($storeId)->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);
177+
}
178+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Fruitcake
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Model/Config/CustomConfig.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Fruitcake\CustomImageUrl\Model\Config;
9+
10+
use Magento\Framework\App\Config\ScopeConfigInterface;
11+
use Magento\Store\Model\ScopeInterface;
12+
13+
/**
14+
* Config for catalog media
15+
*/
16+
class CustomConfig
17+
{
18+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_TYPE = 'web/url/catalog_media_url_format_custom_type';
19+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_PATTERN = 'web/url/catalog_media_url_format_custom_pattern';
20+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_HOST = 'web/url/catalog_media_url_format_custom_imgproxy_host';
21+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_RESIZE = 'web/url/catalog_media_url_format_custom_imgproxy_resize';
22+
23+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_KEY = 'web/url/catalog_media_url_format_custom_imgproxy_key';
24+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SALT = 'web/url/catalog_media_url_format_custom_imgproxy_salt';
25+
26+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SOURCE_TYPE = 'web/url/catalog_media_url_format_custom_imgproxy_source_type';
27+
public const XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SOURCE_PREFIX = 'web/url/catalog_media_url_format_custom_imgproxy_source_prefix';
28+
29+
public const TYPE_DEFAULT = 'default';
30+
public const TYPE_PATTERN = 'pattern';
31+
public const TYPE_IMGPROXY = 'imgproxy';
32+
33+
public const IMGPROXY_URL = 'url';
34+
public const IMGPROXY_LOCAL = 'local';
35+
public const IMGPROXY_S3 = 's3';
36+
37+
public const IMGPROXY_FIT = 'fit';
38+
public const IMGPROXY_FILL = 'fill';
39+
public const IMGPROXY_AUTO = 'auto';
40+
41+
/**
42+
* @var ScopeConfigInterface
43+
*/
44+
private $scopeConfig;
45+
46+
/**
47+
* @param ScopeConfigInterface $scopeConfig
48+
*/
49+
public function __construct(ScopeConfigInterface $scopeConfig)
50+
{
51+
$this->scopeConfig = $scopeConfig;
52+
}
53+
54+
/**
55+
* Get the custom types
56+
*
57+
* @param null|int $storeId
58+
* @return string
59+
*/
60+
public function getCustomType($storeId = null): string
61+
{
62+
$value = $this->scopeConfig->getValue(
63+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_TYPE,
64+
ScopeInterface::SCOPE_STORE,
65+
$storeId
66+
);
67+
68+
if ($value === null) {
69+
return self::TYPE_DEFAULT;
70+
}
71+
72+
return (string)$value;
73+
}
74+
75+
public function getImgproxyHost($storeId = null): string
76+
{
77+
return $this->scopeConfig->getValue(
78+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_HOST,
79+
ScopeInterface::SCOPE_STORE,
80+
$storeId
81+
);
82+
}
83+
84+
public function getImgproxyResize($storeId = null): string
85+
{
86+
return $this->scopeConfig->getValue(
87+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_RESIZE,
88+
ScopeInterface::SCOPE_STORE,
89+
$storeId
90+
) ?: '';
91+
}
92+
93+
public function getImgproxySourceType($storeId = null): string
94+
{
95+
return $this->scopeConfig->getValue(
96+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SOURCE_TYPE,
97+
ScopeInterface::SCOPE_STORE,
98+
$storeId
99+
);
100+
}
101+
102+
public function getImgproxySourcePrefix($storeId = null): string
103+
{
104+
return $this->scopeConfig->getValue(
105+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SOURCE_PREFIX,
106+
ScopeInterface::SCOPE_STORE,
107+
$storeId
108+
) ?: '';
109+
}
110+
111+
112+
public function getImgproxyKey($storeId = null): string
113+
{
114+
return $this->scopeConfig->getValue(
115+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_KEY,
116+
ScopeInterface::SCOPE_STORE,
117+
$storeId
118+
) ?: '';
119+
}
120+
121+
public function getImgproxySalt($storeId = null): string
122+
{
123+
return $this->scopeConfig->getValue(
124+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_IMGPROXY_SALT,
125+
ScopeInterface::SCOPE_STORE,
126+
$storeId
127+
) ?: '';
128+
}
129+
130+
public function getCustomPattern($storeId = null): string
131+
{
132+
return $this->scopeConfig->getValue(
133+
self::XML_PATH_CATALOG_MEDIA_URL_FORMAT_CUSTOM_PATTERN,
134+
ScopeInterface::SCOPE_STORE,
135+
$storeId
136+
);
137+
}
138+
139+
}

Model/Config/Source/CustomUrlType.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Fruitcake\CustomImageUrl\Model\Config\Source;
9+
10+
use Fruitcake\CustomImageUrl\Model\Config\CustomConfig;
11+
12+
/**
13+
* Option provider for custom media URL type
14+
*/
15+
class CustomUrlType implements \Magento\Framework\Data\OptionSourceInterface
16+
{
17+
/**
18+
* Get a list of supported catalog media URL formats.
19+
*
20+
* @codeCoverageIgnore
21+
* @return array
22+
*/
23+
public function toOptionArray(): array
24+
{
25+
return [
26+
[
27+
'value' => CustomConfig::TYPE_DEFAULT,
28+
'label' => __('Default Magento Catalog media URL format')
29+
],
30+
[
31+
'value' => CustomConfig::TYPE_PATTERN,
32+
'label' => __('Custom URL pattern')
33+
],
34+
[
35+
'value' => CustomConfig::TYPE_IMGPROXY,
36+
'label' => __('Imgproxy')
37+
],
38+
];
39+
}
40+
}

0 commit comments

Comments
 (0)