Skip to content

Commit 2a604c2

Browse files
committed
Load site's Logo from Cloudinary
1 parent 0a623c7 commit 2a604c2

2 files changed

Lines changed: 363 additions & 0 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
<?php
2+
3+
namespace Cloudinary\Cloudinary\Plugin\Theme\Block\Html\Header;
4+
5+
use Cloudinary\Api\Admin\AdminApi;
6+
use Cloudinary\Api\Upload\UploadApi;
7+
use Cloudinary\Cloudinary\Core\CloudinaryImageManager;
8+
use Cloudinary\Cloudinary\Core\ConfigurationInterface;
9+
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
10+
use Cloudinary\Cloudinary\Core\Image;
11+
use Cloudinary\Cloudinary\Core\UrlGenerator;
12+
use Cloudinary\Configuration\Configuration;
13+
use Magento\Framework\App\Filesystem\DirectoryList;
14+
use Magento\Framework\Filesystem;
15+
use Magento\Framework\UrlInterface;
16+
use Magento\Store\Model\ScopeInterface;
17+
use Magento\Framework\App\Config\ScopeConfigInterface;
18+
use Psr\Log\LoggerInterface;
19+
20+
class Logo
21+
{
22+
/**
23+
* @var ConfigurationInterface
24+
*/
25+
private $configuration;
26+
27+
/**
28+
* @var CloudinaryImageManager
29+
*/
30+
private $cloudinaryImageManager;
31+
32+
/**
33+
* @var ConfigurationBuilder
34+
*/
35+
private $configurationBuilder;
36+
37+
/**
38+
* @var UrlGenerator
39+
*/
40+
private $urlGenerator;
41+
42+
/**
43+
* @var Filesystem
44+
*/
45+
private $filesystem;
46+
47+
/**
48+
* @var ScopeConfigInterface
49+
*/
50+
private $scopeConfig;
51+
52+
/**
53+
* @var UrlInterface
54+
*/
55+
private $urlBuilder;
56+
57+
/**
58+
* @var LoggerInterface
59+
*/
60+
private $logger;
61+
62+
/**
63+
* @param ConfigurationInterface $configuration
64+
* @param CloudinaryImageManager $cloudinaryImageManager
65+
* @param ConfigurationBuilder $configurationBuilder
66+
* @param UrlGenerator $urlGenerator
67+
* @param Filesystem $filesystem
68+
* @param ScopeConfigInterface $scopeConfig
69+
* @param UrlInterface $urlBuilder
70+
* @param LoggerInterface $logger
71+
*/
72+
public function __construct(
73+
ConfigurationInterface $configuration,
74+
CloudinaryImageManager $cloudinaryImageManager,
75+
ConfigurationBuilder $configurationBuilder,
76+
UrlGenerator $urlGenerator,
77+
Filesystem $filesystem,
78+
ScopeConfigInterface $scopeConfig,
79+
UrlInterface $urlBuilder,
80+
LoggerInterface $logger
81+
) {
82+
$this->configuration = $configuration;
83+
$this->cloudinaryImageManager = $cloudinaryImageManager;
84+
$this->configurationBuilder = $configurationBuilder;
85+
$this->urlGenerator = $urlGenerator;
86+
$this->filesystem = $filesystem;
87+
$this->scopeConfig = $scopeConfig;
88+
$this->urlBuilder = $urlBuilder;
89+
$this->logger = $logger;
90+
}
91+
92+
/**
93+
* Plugin to intercept logo loading and serve from Cloudinary
94+
*
95+
* @param \Magento\Theme\Block\Html\Header\Logo $subject
96+
* @param callable $proceed
97+
* @return string
98+
*/
99+
public function aroundGetLogoSrc(
100+
\Magento\Theme\Block\Html\Header\Logo $subject,
101+
callable $proceed
102+
) {
103+
// Check if Cloudinary is enabled
104+
if (!$this->configuration->isEnabled()) {
105+
return $proceed();
106+
}
107+
108+
try {
109+
// Get the original logo URL first
110+
$originalLogoUrl = $proceed();
111+
112+
// Get logo path from configuration
113+
$logoPath = $this->scopeConfig->getValue(
114+
'design/header/logo_src',
115+
ScopeInterface::SCOPE_STORE
116+
);
117+
118+
if (!$logoPath) {
119+
// If no custom logo is set, use the original URL
120+
return $this->processLogoFromUrl($originalLogoUrl);
121+
}
122+
123+
// The logo path configuration doesn't include the 'logo/' prefix, so we need to add it
124+
$fullLogoPath = 'logo/' . $logoPath;
125+
126+
// Create a unique public ID for the logo (without logo/ prefix since we add it later)
127+
$publicId = pathinfo($logoPath, PATHINFO_FILENAME);
128+
129+
// Check if logo exists on Cloudinary
130+
$cloudinaryUrl = $this->checkAndUploadToCloudinary($fullLogoPath, $publicId, $originalLogoUrl);
131+
132+
if ($cloudinaryUrl) {
133+
return $cloudinaryUrl;
134+
}
135+
} catch (\Exception $e) {
136+
$this->logger->error('Cloudinary Logo Plugin Error: ' . $e->getMessage());
137+
}
138+
139+
// Fallback to original logo URL if anything fails
140+
return $proceed();
141+
}
142+
143+
/**
144+
* Check if image exists on Cloudinary and upload if necessary
145+
*
146+
* @param string $logoPath
147+
* @param string $publicId
148+
* @param string $originalUrl
149+
* @return string|null
150+
*/
151+
private function checkAndUploadToCloudinary($logoPath, $publicId, $originalUrl)
152+
{
153+
try {
154+
// Initialize Cloudinary configuration
155+
Configuration::instance($this->configurationBuilder->build());
156+
157+
// Check if the image exists on Cloudinary
158+
$existingUrl = $this->checkCloudinaryImageExists($publicId);
159+
160+
if (!$existingUrl) {
161+
// Image doesn't exist on Cloudinary, upload it
162+
$mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
163+
$logoFullPath = null;
164+
165+
if ($logoPath && $mediaDirectory->isFile($logoPath)) {
166+
$logoFullPath = $mediaDirectory->getAbsolutePath($logoPath);
167+
} else {
168+
// If the file doesn't exist in media, try to download from original URL
169+
$logoFullPath = $this->downloadLogoFromUrl($originalUrl);
170+
if (!$logoFullPath) {
171+
return null;
172+
}
173+
}
174+
175+
// Upload to Cloudinary with specific public_id
176+
$uploader = new UploadApi($this->configuration->getCredentials());
177+
$uploadOptions = array_merge(
178+
$this->configuration->getUploadConfig()->toArray(),
179+
[
180+
'public_id' => 'logo/' . $publicId,
181+
'overwrite' => true,
182+
'resource_type' => 'image'
183+
]
184+
);
185+
186+
$uploadResult = $uploader->upload($logoFullPath, $uploadOptions);
187+
188+
if (isset($uploadResult['secure_url'])) {
189+
// Add Magento plugin metadata
190+
if (isset($uploadResult['public_id'])) {
191+
$metadata = "cld_mag_plugin=1";
192+
$uploader->addContext($metadata, [$uploadResult['public_id']]);
193+
}
194+
195+
$this->logger->info('Logo uploaded to Cloudinary with public_id: ' . $publicId);
196+
197+
// Clean up temp file if it was downloaded
198+
if (strpos($logoFullPath, '/tmp/') !== false) {
199+
@unlink($logoFullPath);
200+
}
201+
202+
return $uploadResult['secure_url'];
203+
}
204+
} else {
205+
// Image exists on Cloudinary, return its URL
206+
$this->logger->info('Logo already exists on Cloudinary with public_id: ' . $publicId);
207+
return $existingUrl;
208+
}
209+
} catch (\Exception $e) {
210+
$this->logger->error('Error uploading logo to Cloudinary: ' . $e->getMessage());
211+
}
212+
213+
return null;
214+
}
215+
216+
/**
217+
* Check if image exists on Cloudinary
218+
*
219+
* @param string $publicId
220+
* @return string|false
221+
*/
222+
private function checkCloudinaryImageExists($publicId)
223+
{
224+
try {
225+
// Initialize AdminApi to check resource existence
226+
$adminApi = new AdminApi($this->configuration->getCredentials());
227+
228+
// Build the full public_id with folder
229+
$fullPublicId = 'logo/' . $publicId;
230+
231+
try {
232+
// Try to get the resource details
233+
$result = $adminApi->asset($fullPublicId, ['resource_type' => 'image']);
234+
235+
if (isset($result['secure_url'])) {
236+
$imageUrl = $result['secure_url'];
237+
238+
// Verify the image is actually accessible by making a HEAD request
239+
if ($this->verifyImageAccessibility($imageUrl)) {
240+
return $imageUrl;
241+
} else {
242+
$this->logger->info('Cloudinary image URL not accessible, will re-upload: ' . $imageUrl);
243+
return false;
244+
}
245+
}
246+
} catch (\Exception $e) {
247+
// Resource doesn't exist, which is expected if not uploaded yet
248+
$this->logger->debug('Image not found on Cloudinary: ' . $fullPublicId);
249+
}
250+
} catch (\Exception $e) {
251+
$this->logger->debug('Error checking Cloudinary image existence: ' . $e->getMessage());
252+
}
253+
254+
return false;
255+
}
256+
257+
/**
258+
* Verify if an image URL is accessible
259+
*
260+
* @param string $url
261+
* @return bool
262+
*/
263+
private function verifyImageAccessibility($url)
264+
{
265+
try {
266+
$context = stream_context_create([
267+
'http' => [
268+
'method' => 'HEAD',
269+
'timeout' => 5,
270+
],
271+
'ssl' => [
272+
'verify_peer' => false,
273+
'verify_peer_name' => false,
274+
]
275+
]);
276+
277+
$headers = @get_headers($url, 1, $context);
278+
279+
if ($headers && isset($headers[0])) {
280+
// Check for successful HTTP status codes (200, 201, etc.)
281+
return strpos($headers[0], '200') !== false || strpos($headers[0], '201') !== false;
282+
}
283+
} catch (\Exception $e) {
284+
$this->logger->debug('Error verifying image accessibility: ' . $e->getMessage());
285+
}
286+
287+
return false;
288+
}
289+
290+
/**
291+
* Process logo from URL when no custom logo is set
292+
*
293+
* @param string $originalUrl
294+
* @return string
295+
*/
296+
private function processLogoFromUrl($originalUrl)
297+
{
298+
try {
299+
// Extract filename from URL
300+
$urlParts = parse_url($originalUrl);
301+
$path = $urlParts['path'] ?? '';
302+
$filename = pathinfo($path, PATHINFO_FILENAME);
303+
304+
if ($filename) {
305+
$publicId = 'logo/' . $filename;
306+
307+
// Check and upload to Cloudinary
308+
$cloudinaryUrl = $this->checkAndUploadToCloudinary('', $publicId, $originalUrl);
309+
310+
if ($cloudinaryUrl) {
311+
return $cloudinaryUrl;
312+
}
313+
}
314+
} catch (\Exception $e) {
315+
$this->logger->error('Error processing logo from URL: ' . $e->getMessage());
316+
}
317+
318+
return $originalUrl;
319+
}
320+
321+
/**
322+
* Download logo from URL to temporary location
323+
*
324+
* @param string $url
325+
* @return string|null
326+
*/
327+
private function downloadLogoFromUrl($url)
328+
{
329+
try {
330+
$tempDir = $this->filesystem->getDirectoryWrite(DirectoryList::TMP);
331+
$extension = pathinfo($url, PATHINFO_EXTENSION) ?: 'svg';
332+
$filename = 'logo_' . uniqid() . '.' . $extension;
333+
$tempPath = $tempDir->getAbsolutePath($filename);
334+
335+
// Create context for HTTPS requests to disable SSL verification for local development
336+
$context = stream_context_create([
337+
'http' => [
338+
'timeout' => 10,
339+
'method' => 'GET',
340+
],
341+
'ssl' => [
342+
'verify_peer' => false,
343+
'verify_peer_name' => false,
344+
]
345+
]);
346+
347+
$logoContent = file_get_contents($url, false, $context);
348+
if ($logoContent) {
349+
file_put_contents($tempPath, $logoContent);
350+
return $tempPath;
351+
}
352+
} catch (\Exception $e) {
353+
$this->logger->error('Error downloading logo from URL: ' . $e->getMessage());
354+
}
355+
356+
return null;
357+
}
358+
}

etc/di.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@
9999
<type name="Magento\Cms\Block\Widget\Block">
100100
<plugin name="cloudinary_plugin_block_cms_block_widget_block" type="Cloudinary\Cloudinary\Plugin\Cms\Block\Widget\Block"/>
101101
</type>
102+
103+
<type name="Magento\Theme\Block\Html\Header\Logo">
104+
<plugin name="cloudinary_plugin_theme_logo" type="Cloudinary\Cloudinary\Plugin\Theme\Block\Html\Header\Logo"/>
105+
</type>
106+
102107
<type name="Magento\Catalog\Model\ResourceModel\Eav\Attribute">
103108
<plugin name="cloudinary_update_swatch_plugin" type="Cloudinary\Cloudinary\Plugin\AttributeSavePlugin" />
104109
</type>

0 commit comments

Comments
 (0)