Analytics
1 month ago
Database
3 months ago
DynamicFieldResolver.php
2 months ago
Elementor_Enhancer.php
7 months ago
EmbedPress_Core_Installer.php
6 years ago
EmbedPress_Notice.php
4 months ago
EmbedPress_Plugin_Usage_Tracker.php
2 months ago
Extend_CustomPlayer_Controls.php
3 months ago
Extend_Elementor_Controls.php
1 year ago
FeatureNoticeManager.php
1 month ago
FeatureNotices.php
2 weeks ago
FeaturePreviewModal.php
1 month ago
Feature_Enhancer.php
2 months ago
GoogleReviewsAdminPage.php
1 month ago
GoogleReviewsApify.php
1 month ago
GoogleReviewsManaged.php
5 days ago
GoogleReviewsRenderer.php
5 days ago
GoogleReviewsRestController.php
5 days ago
GoogleReviewsStore.php
5 days ago
Helper.php
5 days ago
Pdf_Thumbnail_Handler.php
5 days ago
PermalinkHelper.php
11 months ago
View_Count_Display.php
1 month ago
Pdf_Thumbnail_Handler.php
527 lines
| 1 | <?php |
| 2 | |
| 3 | namespace EmbedPress\Includes\Classes; |
| 4 | |
| 5 | (defined('ABSPATH')) or die("No direct script access allowed."); |
| 6 | |
| 7 | /** |
| 8 | * Handles PDF thumbnail generation and upload via AJAX. |
| 9 | * Extracted from Embedpress_Pdf_Gallery to avoid Elementor dependency. |
| 10 | */ |
| 11 | class Pdf_Thumbnail_Handler |
| 12 | { |
| 13 | /** |
| 14 | * Attachment meta recording the last failed poster attempt. |
| 15 | */ |
| 16 | const FAILED_META = '_ep_pdf_thumbnail_failed'; |
| 17 | |
| 18 | /** |
| 19 | * Transient caching whether this host's Imagick can rasterise PDFs. |
| 20 | */ |
| 21 | const SUPPORT_TRANSIENT = 'embedpress_pdf_rasterise_support'; |
| 22 | |
| 23 | /** |
| 24 | * How long to wait before retrying a failed poster generation. |
| 25 | */ |
| 26 | const RETRY_AFTER = WEEK_IN_SECONDS; |
| 27 | |
| 28 | /** |
| 29 | * Cron hook that generates a poster out of band. |
| 30 | */ |
| 31 | const BACKFILL_HOOK = 'embedpress_generate_pdf_poster'; |
| 32 | |
| 33 | /** |
| 34 | * Attachment meta marking a backfill as already queued. |
| 35 | */ |
| 36 | const QUEUED_META = '_ep_pdf_thumbnail_queued'; |
| 37 | |
| 38 | /** |
| 39 | * Register the upload-time hook. |
| 40 | * |
| 41 | * Generating the poster once, when the PDF is uploaded, is what lets every |
| 42 | * render surface (Gutenberg block, Elementor widget, shortcode, gallery, |
| 43 | * lightbox) just read a cached image instead of each solving the problem |
| 44 | * itself. Before this, nothing generated a poster at upload: the front end |
| 45 | * fell back to rendering page 1 with PDF.js in the visitor's browser on |
| 46 | * every page load — measured at 18 requests / 19.16MB / ~7s for a single |
| 47 | * 18MB PDF, with no cache. See FB #84167. |
| 48 | * |
| 49 | * @return void |
| 50 | */ |
| 51 | public static function init() |
| 52 | { |
| 53 | // Priority 20: after WP has built its own attachment metadata (which |
| 54 | // already rasterises page 1 for PDFs on most hosts), so the common case |
| 55 | // is a no-op and we only do work when WP produced nothing usable. |
| 56 | add_filter('wp_generate_attachment_metadata', [__CLASS__, 'ensure_pdf_poster'], 20, 2); |
| 57 | |
| 58 | // Backfill for PDFs that predate upload-time generation. Runs out of |
| 59 | // band so no visitor ever waits on a rasterise. |
| 60 | add_action(self::BACKFILL_HOOK, [__CLASS__, 'generate_poster_attachment']); |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Ensure an uploaded PDF has a usable poster image. |
| 65 | * |
| 66 | * Hooked to wp_generate_attachment_metadata, so it runs once per upload |
| 67 | * rather than per page view. Returns $metadata untouched in every case — |
| 68 | * this only ever adds a cached thumbnail alongside it, and must never |
| 69 | * interfere with WP's own metadata. |
| 70 | * |
| 71 | * @param array $metadata Attachment metadata as built by WordPress. |
| 72 | * @param int $attachment_id Attachment being processed. |
| 73 | * @return array The unmodified metadata. |
| 74 | */ |
| 75 | public static function ensure_pdf_poster($metadata, $attachment_id) |
| 76 | { |
| 77 | if ('application/pdf' !== get_post_mime_type($attachment_id)) { |
| 78 | return $metadata; |
| 79 | } |
| 80 | |
| 81 | // WordPress already rasterised page 1 (Imagick present, PDF readable). |
| 82 | // That is the normal path and needs nothing from us. |
| 83 | if (!empty($metadata['sizes'])) { |
| 84 | return $metadata; |
| 85 | } |
| 86 | |
| 87 | // Already generated on a previous run. |
| 88 | if (get_post_meta($attachment_id, '_ep_pdf_thumbnail_id', true)) { |
| 89 | return $metadata; |
| 90 | } |
| 91 | |
| 92 | self::generate_poster_attachment($attachment_id); |
| 93 | |
| 94 | return $metadata; |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Return an existing poster attachment ID, generating one if needed. |
| 99 | * |
| 100 | * The upload-time hook only covers PDFs uploaded after this feature |
| 101 | * shipped. Every PDF already in the media library — and any whose upload |
| 102 | * ran on a host where rasterising was momentarily unavailable — would |
| 103 | * otherwise never get a poster, and every render surface would fall back |
| 104 | * to rendering page 1 with PDF.js in the visitor's browser. That fallback |
| 105 | * is what produced the torn/vertically-flipped preview in FB #83295 on |
| 106 | * large documents, so backfilling here fixes the reported bug for existing |
| 107 | * content rather than only for new uploads. |
| 108 | * |
| 109 | * Generation itself is NOT performed inline. Rasterising a large PDF takes |
| 110 | * seconds (measured at 8.2s for an 18MB/29-page document), and this runs |
| 111 | * during a front-end render, so doing it here would stall the visitor's |
| 112 | * page load — a worse symptom than the flipped preview it fixes. Instead |
| 113 | * the work is queued and this returns false for that first render, which |
| 114 | * simply keeps the existing canvas fallback for one page view. |
| 115 | * |
| 116 | * Queueing is attempted at most once per RETRY_AFTER per attachment, so a |
| 117 | * host that genuinely cannot rasterise PDFs does not schedule an event on |
| 118 | * every page view. |
| 119 | * |
| 120 | * @param int $attachment_id PDF attachment ID. |
| 121 | * @return int|false Poster attachment ID when one already exists, else false. |
| 122 | */ |
| 123 | public static function get_or_create_poster($attachment_id) |
| 124 | { |
| 125 | $existing = get_post_meta($attachment_id, '_ep_pdf_thumbnail_id', true); |
| 126 | if ($existing && wp_get_attachment_url($existing)) { |
| 127 | return (int) $existing; |
| 128 | } |
| 129 | |
| 130 | if ($existing) { |
| 131 | // Stale pointer — the poster attachment was deleted. |
| 132 | delete_post_meta($attachment_id, '_ep_pdf_thumbnail_id'); |
| 133 | } |
| 134 | |
| 135 | if ('application/pdf' !== get_post_mime_type($attachment_id)) { |
| 136 | return false; |
| 137 | } |
| 138 | |
| 139 | self::queue_backfill($attachment_id); |
| 140 | |
| 141 | return false; |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Schedule out-of-band poster generation for an existing PDF. |
| 146 | * |
| 147 | * @param int $attachment_id PDF attachment ID. |
| 148 | * @return void |
| 149 | */ |
| 150 | private static function queue_backfill($attachment_id) |
| 151 | { |
| 152 | $failed = get_post_meta($attachment_id, self::FAILED_META, true); |
| 153 | if (!empty($failed['time']) && (time() - (int) $failed['time']) < self::RETRY_AFTER) { |
| 154 | return; |
| 155 | } |
| 156 | |
| 157 | $queued = (int) get_post_meta($attachment_id, self::QUEUED_META, true); |
| 158 | if ($queued && (time() - $queued) < HOUR_IN_SECONDS) { |
| 159 | return; |
| 160 | } |
| 161 | |
| 162 | if (wp_next_scheduled(self::BACKFILL_HOOK, [$attachment_id])) { |
| 163 | return; |
| 164 | } |
| 165 | |
| 166 | // Cheap enough to run inline (cached for a day) and avoids scheduling |
| 167 | // work that can never succeed on this host. |
| 168 | if (!self::can_rasterise_pdf()) { |
| 169 | return; |
| 170 | } |
| 171 | |
| 172 | update_post_meta($attachment_id, self::QUEUED_META, time()); |
| 173 | wp_schedule_single_event(time(), self::BACKFILL_HOOK, [$attachment_id]); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Rasterise page 1 of a PDF to a PNG attachment and cache its ID. |
| 178 | * |
| 179 | * This is the Imagick fallback that previously lived inline in |
| 180 | * ajax_generate_pdf_thumbnail(), lifted out so it can be called from the |
| 181 | * upload hook as well as the AJAX endpoint. Unlike the AJAX version it |
| 182 | * returns a value instead of emitting JSON, and performs no capability or |
| 183 | * nonce check — callers are responsible for authorisation. The upload hook |
| 184 | * is inherently authorised (the user just uploaded the file). |
| 185 | * |
| 186 | * @param int $attachment_id PDF attachment ID. |
| 187 | * @return int|false Thumbnail attachment ID, or false on failure. |
| 188 | */ |
| 189 | public static function generate_poster_attachment($attachment_id) |
| 190 | { |
| 191 | if (!self::can_rasterise_pdf()) { |
| 192 | self::mark_failed($attachment_id, 'Imagick cannot decode PDF (missing Ghostscript delegate or blocked by policy.xml)'); |
| 193 | return false; |
| 194 | } |
| 195 | |
| 196 | $file_path = get_attached_file($attachment_id); |
| 197 | if (!$file_path || !file_exists($file_path)) { |
| 198 | self::mark_failed($attachment_id, 'Source file missing on disk'); |
| 199 | return false; |
| 200 | } |
| 201 | |
| 202 | try { |
| 203 | $imagick = new \Imagick(); |
| 204 | $imagick->setResolution(200, 200); |
| 205 | $imagick->readImage($file_path . '[0]'); |
| 206 | $imagick->setImageFormat('png'); |
| 207 | $imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE); |
| 208 | $imagick->setImageBackgroundColor('white'); |
| 209 | $imagick = self::flatten($imagick); |
| 210 | $imagick->thumbnailImage(800, 0); |
| 211 | |
| 212 | $upload_dir = wp_upload_dir(); |
| 213 | $base_name = pathinfo(basename($file_path), PATHINFO_FILENAME); |
| 214 | $thumb_file = 'pdf-thumb-' . sanitize_file_name($base_name) . '.png'; |
| 215 | $thumb_path = trailingslashit($upload_dir['path']) . $thumb_file; |
| 216 | |
| 217 | $imagick->writeImage($thumb_path); |
| 218 | $imagick->destroy(); |
| 219 | |
| 220 | $thumb_id = wp_insert_attachment([ |
| 221 | 'post_mime_type' => 'image/png', |
| 222 | 'post_title' => sanitize_file_name($base_name) . ' - PDF Thumbnail', |
| 223 | 'post_content' => '', |
| 224 | 'post_status' => 'inherit', |
| 225 | ], $thumb_path); |
| 226 | |
| 227 | if (is_wp_error($thumb_id)) { |
| 228 | self::mark_failed($attachment_id, 'wp_insert_attachment failed: ' . $thumb_id->get_error_message()); |
| 229 | return false; |
| 230 | } |
| 231 | |
| 232 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 233 | wp_update_attachment_metadata($thumb_id, wp_generate_attachment_metadata($thumb_id, $thumb_path)); |
| 234 | update_post_meta($attachment_id, '_ep_pdf_thumbnail_id', $thumb_id); |
| 235 | delete_post_meta($attachment_id, self::FAILED_META); |
| 236 | delete_post_meta($attachment_id, self::QUEUED_META); |
| 237 | |
| 238 | return $thumb_id; |
| 239 | } catch (\Exception $e) { |
| 240 | self::mark_failed($attachment_id, $e->getMessage()); |
| 241 | return false; |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Flatten the page onto its background. |
| 247 | * |
| 248 | * Imagick::flattenImages() is deprecated in ImageMagick 7 and emits a |
| 249 | * deprecation notice on some builds; mergeImageLayers() is the supported |
| 250 | * equivalent. Both are kept because the minimum supported Imagick version |
| 251 | * predates mergeImageLayers(). |
| 252 | * |
| 253 | * @param \Imagick $imagick |
| 254 | * @return \Imagick |
| 255 | */ |
| 256 | private static function flatten($imagick) |
| 257 | { |
| 258 | // setIteratorIndex is required before merging: readImage('file[0]') can |
| 259 | // leave the iterator past page 0 on some Ghostscript builds, which |
| 260 | // merges the wrong page. |
| 261 | if (method_exists($imagick, 'setIteratorIndex')) { |
| 262 | $imagick->setIteratorIndex(0); |
| 263 | } |
| 264 | |
| 265 | if (method_exists($imagick, 'mergeImageLayers')) { |
| 266 | return $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN); |
| 267 | } |
| 268 | |
| 269 | return $imagick->flattenImages(); |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Whether this host's Imagick can actually rasterise a PDF. |
| 274 | * |
| 275 | * class_exists('Imagick') is not sufficient: rasterising a PDF needs the |
| 276 | * Ghostscript delegate, and many shared hosts ship Imagick but disable PDF |
| 277 | * decoding in policy.xml (the CVE-2018-16509 hardening). On those hosts |
| 278 | * readImage() throws NoDecodeDelegateForThisImageFormat, so every PDF fell |
| 279 | * through to the browser <canvas> renderer. Result is cached for a day — |
| 280 | * this shells out through Ghostscript and is far too slow to run per call. |
| 281 | * |
| 282 | * @return bool |
| 283 | */ |
| 284 | public static function can_rasterise_pdf() |
| 285 | { |
| 286 | if (!class_exists('Imagick')) { |
| 287 | return false; |
| 288 | } |
| 289 | |
| 290 | $cached = get_transient(self::SUPPORT_TRANSIENT); |
| 291 | if (false !== $cached) { |
| 292 | return 'yes' === $cached; |
| 293 | } |
| 294 | |
| 295 | $supported = false; |
| 296 | try { |
| 297 | $formats = \Imagick::queryFormats('PDF'); |
| 298 | if (!empty($formats)) { |
| 299 | // queryFormats() only reports the coder is compiled in, not that |
| 300 | // the delegate works, so actually rasterise a minimal PDF. |
| 301 | $probe = new \Imagick(); |
| 302 | $probe->setResolution(18, 18); |
| 303 | $probe->readImageBlob(self::probe_pdf()); |
| 304 | $supported = $probe->getImageWidth() > 0; |
| 305 | $probe->destroy(); |
| 306 | } |
| 307 | } catch (\Exception $e) { |
| 308 | $supported = false; |
| 309 | } |
| 310 | |
| 311 | set_transient(self::SUPPORT_TRANSIENT, $supported ? 'yes' : 'no', DAY_IN_SECONDS); |
| 312 | |
| 313 | return $supported; |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * Smallest valid one-page PDF, used to probe the Ghostscript delegate. |
| 318 | * |
| 319 | * @return string |
| 320 | */ |
| 321 | private static function probe_pdf() |
| 322 | { |
| 323 | return "%PDF-1.4\n" |
| 324 | . "1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" |
| 325 | . "2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" |
| 326 | . "3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 9 9]>>endobj\n" |
| 327 | . "trailer<</Root 1 0 R>>\n"; |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Record why poster generation failed. |
| 332 | * |
| 333 | * Stored on the attachment (not just logged) so the retry path can tell |
| 334 | * "never attempted" from "attempted and impossible on this host" and stop |
| 335 | * re-running an expensive rasterise on every page view. |
| 336 | * |
| 337 | * @param int $attachment_id |
| 338 | * @param string $reason |
| 339 | * @return void |
| 340 | */ |
| 341 | private static function mark_failed($attachment_id, $reason) |
| 342 | { |
| 343 | update_post_meta($attachment_id, self::FAILED_META, [ |
| 344 | 'reason' => substr((string) $reason, 0, 500), |
| 345 | 'time' => time(), |
| 346 | ]); |
| 347 | |
| 348 | if (defined('WP_DEBUG') && WP_DEBUG) { |
| 349 | error_log(sprintf('EmbedPress: PDF poster generation failed for attachment %d — %s', $attachment_id, $reason)); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * AJAX handler: generate a thumbnail for a PDF attachment. |
| 355 | */ |
| 356 | public static function ajax_generate_pdf_thumbnail() |
| 357 | { |
| 358 | check_ajax_referer('ep_pdf_gallery_nonce', 'nonce'); |
| 359 | |
| 360 | if (!current_user_can('upload_files')) { |
| 361 | wp_send_json_error(['message' => 'Insufficient permissions']); |
| 362 | } |
| 363 | |
| 364 | $attachment_id = isset($_POST['attachment_id']) ? intval($_POST['attachment_id']) : 0; |
| 365 | if (!$attachment_id) { |
| 366 | wp_send_json_error(['message' => 'Invalid attachment ID']); |
| 367 | } |
| 368 | |
| 369 | // Verify it's a PDF |
| 370 | if (get_post_mime_type($attachment_id) !== 'application/pdf') { |
| 371 | wp_send_json_error(['message' => 'Not a PDF file']); |
| 372 | } |
| 373 | |
| 374 | // Check cached thumbnail first (stored as post meta on the PDF attachment) |
| 375 | $cached_thumb_id = get_post_meta($attachment_id, '_ep_pdf_thumbnail_id', true); |
| 376 | if ($cached_thumb_id) { |
| 377 | $cached_url = wp_get_attachment_url($cached_thumb_id); |
| 378 | if ($cached_url) { |
| 379 | wp_send_json_success([ |
| 380 | 'url' => $cached_url, |
| 381 | 'id' => (int) $cached_thumb_id, |
| 382 | ]); |
| 383 | return; |
| 384 | } |
| 385 | // Cached thumbnail attachment no longer exists, clear stale meta |
| 386 | delete_post_meta($attachment_id, '_ep_pdf_thumbnail_id'); |
| 387 | } |
| 388 | |
| 389 | // Method 1: WordPress already generated a preview during upload |
| 390 | $thumb_src = wp_get_attachment_image_src($attachment_id, 'medium'); |
| 391 | if ($thumb_src && !empty($thumb_src[0])) { |
| 392 | wp_send_json_success([ |
| 393 | 'url' => $thumb_src[0], |
| 394 | 'id' => $attachment_id, |
| 395 | ]); |
| 396 | return; |
| 397 | } |
| 398 | |
| 399 | // Method 2: Try regenerating attachment metadata (triggers WP PDF preview) |
| 400 | $file_path = get_attached_file($attachment_id); |
| 401 | if ($file_path && file_exists($file_path)) { |
| 402 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 403 | $metadata = wp_generate_attachment_metadata($attachment_id, $file_path); |
| 404 | if (!empty($metadata)) { |
| 405 | wp_update_attachment_metadata($attachment_id, $metadata); |
| 406 | $thumb_src = wp_get_attachment_image_src($attachment_id, 'medium'); |
| 407 | if ($thumb_src && !empty($thumb_src[0])) { |
| 408 | wp_send_json_success([ |
| 409 | 'url' => $thumb_src[0], |
| 410 | 'id' => $attachment_id, |
| 411 | ]); |
| 412 | return; |
| 413 | } |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | // Method 3: Direct Imagick rendering as last resort. Shares the single |
| 418 | // generator so hardening applied there (delegate probe, iterator reset, |
| 419 | // failure logging) covers this path too. |
| 420 | $thumb_id = self::generate_poster_attachment($attachment_id); |
| 421 | if ($thumb_id) { |
| 422 | wp_send_json_success([ |
| 423 | 'url' => wp_get_attachment_url($thumb_id), |
| 424 | 'id' => $thumb_id, |
| 425 | ]); |
| 426 | return; |
| 427 | } |
| 428 | |
| 429 | wp_send_json_error(['message' => 'Could not generate thumbnail']); |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * AJAX handler: receive a client-rendered thumbnail (base64 PNG) and save as WP attachment. |
| 434 | */ |
| 435 | public static function ajax_upload_pdf_thumbnail() |
| 436 | { |
| 437 | check_ajax_referer('ep_pdf_gallery_nonce', 'nonce'); |
| 438 | |
| 439 | if (!current_user_can('upload_files')) { |
| 440 | wp_send_json_error(['message' => 'Insufficient permissions']); |
| 441 | } |
| 442 | |
| 443 | $image_data = isset($_POST['image_data']) ? $_POST['image_data'] : ''; |
| 444 | $pdf_url = isset($_POST['pdf_url']) ? esc_url_raw($_POST['pdf_url']) : ''; |
| 445 | $file_name = isset($_POST['file_name']) ? sanitize_file_name($_POST['file_name']) : ''; |
| 446 | $attachment_id = isset($_POST['attachment_id']) ? intval($_POST['attachment_id']) : 0; |
| 447 | |
| 448 | if (empty($image_data) || empty($pdf_url)) { |
| 449 | wp_send_json_error(['message' => 'Missing data']); |
| 450 | } |
| 451 | |
| 452 | // Check cached thumbnail for this PDF attachment |
| 453 | if ($attachment_id) { |
| 454 | $cached_thumb_id = get_post_meta($attachment_id, '_ep_pdf_thumbnail_id', true); |
| 455 | if ($cached_thumb_id) { |
| 456 | $cached_url = wp_get_attachment_url($cached_thumb_id); |
| 457 | if ($cached_url) { |
| 458 | wp_send_json_success([ |
| 459 | 'url' => $cached_url, |
| 460 | 'id' => (int) $cached_thumb_id, |
| 461 | ]); |
| 462 | return; |
| 463 | } |
| 464 | // Stale meta, clear it |
| 465 | delete_post_meta($attachment_id, '_ep_pdf_thumbnail_id'); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | // Strip data-URI prefix |
| 470 | $image_data = preg_replace('/^data:image\/\w+;base64,/', '', $image_data); |
| 471 | $decoded = base64_decode($image_data); |
| 472 | if (!$decoded || strlen($decoded) < 8) { |
| 473 | wp_send_json_error(['message' => 'Invalid image data']); |
| 474 | } |
| 475 | |
| 476 | // Detect MIME from raw bytes |
| 477 | $finfo = new \finfo(FILEINFO_MIME_TYPE); |
| 478 | $mime = $finfo->buffer($decoded); |
| 479 | if ($mime !== 'image/png' && $mime !== 'image/jpeg') { |
| 480 | wp_send_json_error(['message' => 'Invalid image format']); |
| 481 | } |
| 482 | |
| 483 | $ext = ($mime === 'image/jpeg') ? '.jpg' : '.png'; |
| 484 | $upload_dir = wp_upload_dir(); |
| 485 | $base_name = $file_name ? pathinfo($file_name, PATHINFO_FILENAME) : md5($pdf_url); |
| 486 | $thumb_file = 'pdf-thumb-' . sanitize_file_name($base_name) . $ext; |
| 487 | $thumb_path = trailingslashit($upload_dir['path']) . $thumb_file; |
| 488 | |
| 489 | // Avoid overwriting existing files |
| 490 | $counter = 0; |
| 491 | while (file_exists($thumb_path)) { |
| 492 | $counter++; |
| 493 | $thumb_file = 'pdf-thumb-' . sanitize_file_name($base_name) . '-' . $counter . $ext; |
| 494 | $thumb_path = trailingslashit($upload_dir['path']) . $thumb_file; |
| 495 | } |
| 496 | |
| 497 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents |
| 498 | file_put_contents($thumb_path, $decoded); |
| 499 | |
| 500 | $attachment = [ |
| 501 | 'post_mime_type' => $mime, |
| 502 | 'post_title' => sanitize_file_name($base_name) . ' - PDF Thumbnail', |
| 503 | 'post_content' => '', |
| 504 | 'post_status' => 'inherit', |
| 505 | ]; |
| 506 | |
| 507 | $thumb_id = wp_insert_attachment($attachment, $thumb_path); |
| 508 | if (is_wp_error($thumb_id)) { |
| 509 | wp_send_json_error(['message' => 'Failed to create attachment']); |
| 510 | } |
| 511 | |
| 512 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 513 | $meta = wp_generate_attachment_metadata($thumb_id, $thumb_path); |
| 514 | wp_update_attachment_metadata($thumb_id, $meta); |
| 515 | |
| 516 | // Cache thumbnail ID on the PDF attachment for future lookups |
| 517 | if ($attachment_id) { |
| 518 | update_post_meta($attachment_id, '_ep_pdf_thumbnail_id', $thumb_id); |
| 519 | } |
| 520 | |
| 521 | wp_send_json_success([ |
| 522 | 'url' => wp_get_attachment_url($thumb_id), |
| 523 | 'id' => $thumb_id, |
| 524 | ]); |
| 525 | } |
| 526 | } |
| 527 |