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+ }
0 commit comments