PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.5.3
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.5.3
4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Gutenberg / EmbedPressBlockRenderer.php
embedpress / EmbedPress / Gutenberg Last commit date
BlockManager.php 3 months ago EmbedPressBlockRenderer.php 3 months ago FallbackHandler.php 11 months ago InitBlocks.php 11 months ago
EmbedPressBlockRenderer.php
1955 lines
1 <?php
2
3 namespace EmbedPress\Gutenberg;
4
5 use EmbedPress\Includes\Classes\Helper;
6 use EmbedPress\Shortcode;
7 use Exception;
8
9 if (!defined('ABSPATH')) {
10 exit;
11 }
12
13 /**
14 * EmbedPress Block Renderer
15 *
16 * Handles rendering of EmbedPress blocks for Gutenberg editor
17 * Manages dynamic content, content protection, and various embed configurations
18 *
19 * @package EmbedPress\Gutenberg
20 * @since 1.0.0
21 */
22 class EmbedPressBlockRenderer
23 {
24 /**
25 * Dynamic providers that require real-time content fetching
26 *
27 * @var array
28 */
29 private static $dynamic_providers = [
30 'photos.app.goo.gl',
31 'photos.google.com',
32 'instagram.com',
33 'opensea.io',
34 'wistia.com',
35 'wistia.net',
36 ];
37
38 /**
39 * Alignment mapping for CSS classes
40 *
41 * @var array
42 */
43 private static $alignment_classes = [
44 'left' => 'alignleft',
45 'right' => 'alignright',
46 'wide' => 'alignwide',
47 'full' => 'alignfull',
48 'center' => 'aligncenter',
49 ];
50
51 /**
52 * Check if URL belongs to a dynamic provider
53 *
54 * @param string $url The URL to check
55 * @return bool True if dynamic provider, false otherwise
56 */
57 public static function is_dynamic_provider($url)
58 {
59 foreach (self::$dynamic_providers as $provider) {
60 if (strpos($url, $provider) !== false) {
61 return true;
62 }
63 }
64
65 return false;
66 }
67
68 /**
69 * Render dynamic content using EmbedPress shortcode parsing
70 *
71 * @param array $attributes Block attributes
72 * @return string|null Rendered content or null on failure
73 */
74 public static function render_dynamic_content($attributes)
75 {
76 $url = $attributes['url'] ?? '';
77
78 if (!class_exists('\\EmbedPress\\Shortcode')) {
79 return null;
80 }
81
82 // Map Meetup-specific Gutenberg attributes to shortcode attributes
83 if (!empty($url) && strpos($url, 'meetup.com') !== false) {
84 if (isset($attributes['meetupOrderBy'])) {
85 $attributes['orderby'] = $attributes['meetupOrderBy'];
86 }
87 if (isset($attributes['meetupOrder'])) {
88 $attributes['order'] = $attributes['meetupOrder'];
89 }
90 if (isset($attributes['meetupPerPage'])) {
91 $attributes['per_page'] = $attributes['meetupPerPage'];
92 }
93 if (isset($attributes['meetupEnablePagination'])) {
94 $attributes['enable_pagination'] = $attributes['meetupEnablePagination'];
95 }
96 if (isset($attributes['meetupTimezone'])) {
97 $attributes['timezone'] = $attributes['meetupTimezone'];
98 }
99 if (isset($attributes['meetupDateFormat'])) {
100 $attributes['date_format'] = $attributes['meetupDateFormat'];
101 }
102 if (isset($attributes['meetupTimeFormat'])) {
103 $attributes['time_format'] = $attributes['meetupTimeFormat'];
104 }
105 }
106
107 try {
108 $embed_result = Shortcode::parseContent($url, false, $attributes);
109
110 if (is_object($embed_result) && isset($embed_result->embed) && !empty($embed_result->embed)) {
111 return $embed_result->embed;
112 }
113 } catch (Exception $e) {
114 if (defined('WP_DEBUG') && WP_DEBUG) {
115 error_log('EmbedPress: Shortcode parsing failed: ' . $e->getMessage());
116 }
117 }
118
119 return null;
120 }
121
122 /**
123 * Main render method for EmbedPress blocks
124 *
125 * @param array $attributes Block attributes
126 * @param string $content Block content
127 * @param object $block Block object (unused but kept for compatibility)
128 * @return string Rendered HTML content
129 */
130 public static function render($attributes, $content = '', $block = null)
131 {
132 // Extract basic attributes
133 $url = $attributes['url'] ?? '';
134 $client_id = !empty($attributes['clientId']) ? md5($attributes['clientId']) : '';
135
136 // Pro: Country Restriction. Filter returns rendered HTML to
137 // short-circuit, or false to render normally. No-op without Pro.
138 $restricted = apply_filters('embedpress/gutenberg/country_restriction_html', false, $attributes);
139 if ($restricted !== false) {
140 return $restricted;
141 }
142
143 // Handle content protection
144 $protection_data = self::extract_protection_data($attributes, $client_id);
145 $should_display_content = self::should_display_content($protection_data);
146 $isAdManager = !empty($attributes['adManager']) ? true : false;
147
148
149 // Early return for non-dynamic providers with displayable content.
150 //
151 // When Custom Player (Pro #81243) is enabled we MUST fall through to
152 // render_embed_html() so build_player_options() can emit the Pro
153 // feature keys (chapters, email_capture, heatmap, etc.) into
154 // data-options. The block's save() function only emits the 13 basic
155 // player keys, so without this gate Pro features never reach
156 // initplyr.js on the front-end.
157 $has_custom_player = !empty($attributes['customPlayer']);
158 if ((!empty($content) && !self::is_dynamic_provider($url)) && !$has_custom_player && $should_display_content && !$isAdManager) {
159 return $content;
160 }
161
162 // Process embed HTML if available
163 if (!empty($attributes['embedHTML'])) {
164 return self::render_embed_html($attributes, $attributes['embedHTML'], $protection_data, $should_display_content);
165 }
166
167 return '';
168 }
169
170
171 /**
172 * Legacy PDF render method - organized following current structure
173 *
174 * @param array $attributes Block attributes
175 * @return string Rendered HTML content
176 */
177 public static function embedpress_pdf_legacy_render_block($attributes)
178 {
179 // Extract basic attributes for PDF block
180 $href = $attributes['href'] ?? '';
181 $id = $attributes['id'] ?? 'embedpress-pdf-' . rand(100, 10000);
182 $client_id = md5($id);
183
184 // If no href is provided, return empty
185 if (empty($href)) {
186 return '';
187 }
188
189 // Handle content protection using existing methods
190 $protection_data = self::extract_protection_data($attributes, $client_id);
191 $should_display_content = self::should_display_content($protection_data);
192
193 // Build styling configuration using existing method
194 $styling = self::build_styling_config($attributes, $protection_data);
195
196 // Generate legacy PDF HTML
197 return self::render_legacy_pdf_html($attributes, $protection_data, $should_display_content, $styling);
198 }
199
200 /**
201 * Render legacy PDF HTML structure
202 *
203 * @param array $attributes Block attributes
204 * @param array $protection_data Protection data
205 * @param bool $should_display_content Whether content should be displayed
206 * @param array $styling Styling configuration
207 * @return string Rendered HTML
208 */
209 private static function render_legacy_pdf_html($attributes, $protection_data, $should_display_content, $styling)
210 {
211 $href = $attributes['href'];
212 $id = $attributes['id'] ?? 'embedpress-pdf-' . rand(100, 10000);
213 $client_id = md5($id);
214
215 // Lightbox mode: render thumbnail instead of inline viewer
216 $displayMode = $attributes['displayMode'] ?? 'inline';
217 if ($displayMode === 'lightbox' && $should_display_content) {
218 return self::render_pdf_lightbox_thumbnail($attributes, $client_id);
219 }
220 if (in_array($displayMode, ['button', 'link', 'text']) && $should_display_content) {
221 return self::render_pdf_trigger($attributes, $client_id, $displayMode);
222 }
223
224 // Extract legacy-specific configurations
225 $legacy_config = self::extract_legacy_pdf_config($attributes);
226
227 // Generate embed code
228 $embed_code = self::generate_legacy_embed_code($attributes, $legacy_config);
229
230 // Build wrapper classes
231 $wrapper_classes = self::build_legacy_wrapper_classes($attributes, $styling, $legacy_config);
232
233 ob_start();
234 ?>
235 <div id="ep-gutenberg-content-<?php echo esc_attr($client_id) ?>" class="ep-gutenberg-content <?php echo esc_attr($wrapper_classes); ?>">
236 <div class="embedpress-inner-iframe <?php echo esc_attr($legacy_config['unit_class']); ?> ep-doc-<?php echo esc_attr($client_id); ?>" style="<?php echo esc_attr($legacy_config['style_attr']); ?>" id="<?php echo esc_attr($id); ?>">
237 <div <?php echo esc_attr($styling['ads_attrs']); ?>>
238 <?php
239 do_action('embedpress_pdf_gutenberg_after_embed', $client_id, 'pdf', $attributes, $href);
240
241 if ($should_display_content) {
242 self::render_legacy_displayable_content($embed_code, $attributes, $styling);
243 } else {
244 self::render_legacy_protected_content($embed_code, $attributes, $protection_data, $styling);
245 }
246
247 // Render ad template if enabled
248 if (!empty($attributes['adManager'])) {
249 $embed_code = apply_filters('embedpress/generate_ad_template', $embed_code, $client_id, $attributes, 'gutenberg');
250 }
251 ?>
252 </div>
253 </div>
254 </div>
255 <?php
256 return ob_get_clean();
257 }
258
259 /**
260 * Extract legacy PDF configuration
261 *
262 * @param array $attributes Block attributes
263 * @return array Legacy configuration
264 */
265 private static function extract_legacy_pdf_config($attributes)
266 {
267 $unitoption = $attributes['unitoption'] ?? 'px';
268 $width = !empty($attributes['width']) ? $attributes['width'] . $unitoption : (Helper::get_options_value('enableEmbedResizeWidth') ?: 600) . 'px';
269 $height = !empty($attributes['height']) ? $attributes['height'] . 'px' : (Helper::get_options_value('enableEmbedResizeHeight') ?: 600) . 'px';
270
271 $width_class = ($unitoption == '%') ? 'ep-percentage-width' : 'ep-fixed-width';
272 $unit_class = ($unitoption === '%') ? 'emebedpress-unit-percent' : '';
273
274 $style_attr = ($unitoption === '%' && !empty($attributes['width']))
275 ? 'max-width:' . $attributes['width'] . '%'
276 : 'max-width:100%';
277
278 $dimension = "width:$width;height:$height";
279
280 return [
281 'unitoption' => $unitoption,
282 'width' => $width,
283 'height' => $height,
284 'width_class' => $width_class,
285 'unit_class' => $unit_class,
286 'style_attr' => $style_attr,
287 'dimension' => $dimension,
288 ];
289 }
290
291 /**
292 * Generate legacy embed code
293 *
294 * @param array $attributes Block attributes
295 * @param array $legacy_config Legacy configuration
296 * @return string Embed code
297 */
298 private static function generate_legacy_embed_code($attributes, $legacy_config)
299 {
300 $href = $attributes['href'];
301 $id = $attributes['id'] ?? 'embedpress-pdf-' . rand(100, 10000);
302 $renderer = Helper::get_pdf_renderer();
303
304 $src = $renderer . ((strpos($renderer, '?') == false) ? '?' : '&') . 'file=' . urlencode($href) . self::generate_pdf_params($attributes);
305
306 $iframe_title = self::get_iframe_title_from_url($href);
307
308 $embed_code = '<iframe title="' . esc_attr($iframe_title) . '" class="embedpress-embed-document-pdf ' . esc_attr($id) . '" style="' . esc_attr($legacy_config['dimension']) . '; max-width:100%; display: inline-block" src="' . esc_url($src) . '" frameborder="0" oncontextmenu="return false;"></iframe> ';
309
310 // Handle flip-book viewer style
311 if (isset($attributes['viewerStyle']) && $attributes['viewerStyle'] === 'flip-book') {
312 $src = urlencode($href) . self::generate_pdf_params($attributes);
313 $renderer = Helper::get_flipbook_renderer();
314 $src_url = $renderer . ((strpos($renderer, '?') == false) ? '?' : '&') . 'file=' . $src;
315 $embed_code = '<iframe title="' . esc_attr(Helper::get_file_title($href)) . '" class="embedpress-embed-document-pdf ' . esc_attr($id) . '" style="' . esc_attr($legacy_config['dimension']) . '; max-width:100%; display: inline-block" src="' . esc_url($src_url) . '" frameborder="0" oncontextmenu="return false;"></iframe> ';
316 }
317
318 // Add powered by if enabled
319 $gen_settings = get_option(EMBEDPRESS_PLG_NAME);
320 $powered_by = isset($gen_settings['embedpress_document_powered_by']) && 'yes' === $gen_settings['embedpress_document_powered_by'];
321 if (isset($attributes['powered_by'])) {
322 $powered_by = $attributes['powered_by'];
323 }
324
325 if ($powered_by) {
326 $embed_code .= sprintf('<p class="embedpress-el-powered">%s</p>', __('Powered By EmbedPress', 'embedpress'));
327 }
328
329 return $embed_code;
330 }
331
332 /**
333 * Build legacy wrapper classes
334 *
335 * @param array $attributes Block attributes
336 * @param array $styling Styling configuration
337 * @param array $legacy_config Legacy configuration
338 * @return string CSS classes
339 */
340 private static function build_legacy_wrapper_classes($attributes, $styling, $legacy_config)
341 {
342 return trim(sprintf(
343 '%s %s %s %s %s',
344 $styling['alignment'],
345 $legacy_config['width_class'],
346 $styling['content_share_class'],
347 $styling['share_position_class'],
348 $styling['content_protection_class']
349 ));
350 }
351
352 /**
353 * Render legacy displayable content
354 *
355 * @param string $embed_code Embed code
356 * @param array $attributes Block attributes
357 * @param array $styling Styling configuration
358 */
359 private static function render_legacy_displayable_content($embed_code, $attributes, $styling)
360 {
361 $share_position = $attributes['sharePosition'] ?? 'right';
362 $content_id = $attributes['id'];
363
364 echo '<div class="ep-embed-content-wraper">';
365 $embed = '<div class="position-' . esc_attr($share_position) . '-wraper gutenberg-pdf-wraper">';
366 $embed .= $embed_code;
367 $embed .= '</div>';
368
369 if (!empty($attributes['contentShare'])) {
370 $embed .= Helper::embed_content_share($content_id, $attributes);
371 }
372 echo $embed;
373 echo '</div>';
374 }
375
376 /**
377 * Render legacy protected content
378 *
379 * @param string $embed_code Embed code
380 * @param array $attributes Block attributes
381 * @param array $protection_data Protection data
382 * @param array $styling Styling configuration
383 */
384 private static function render_legacy_protected_content($embed_code, $attributes, $protection_data, $styling)
385 {
386 $share_position = $attributes['sharePosition'] ?? 'right';
387 $client_id = $protection_data['client_id'];
388
389 // Initialize $embed variable
390 $embed = $embed_code;
391
392 if (!empty($attributes['contentShare'])) {
393 $content_id = $attributes['clientId'];
394 $embed = '<div class="position-' . esc_attr($share_position) . '-wraper gutenberg-pdf-wraper">';
395 $embed .= $embed_code;
396 $embed .= '</div>';
397 $embed .= Helper::embed_content_share($content_id, $attributes);
398 }
399
400 echo '<div class="ep-embed-content-wraper">';
401 if ($attributes['protectionType'] == 'password') {
402 echo '<div id="ep-gutenberg-content-' . esc_attr($client_id) . '" class="ep-gutenberg-content">';
403 do_action('embedpress/display_password_form', $client_id, $embed, $styling['pass_hash_key'], $attributes);
404 echo '</div>';
405 } else {
406 do_action('embedpress/content_protection_content', $client_id, $attributes['protectionMessage'], $attributes['userRole']);
407 }
408 echo '</div>';
409 }
410
411 public static function render_embedpress_pdf($attributes, $content = '', $block = null)
412 {
413
414 // Extract basic attributes for PDF block
415 $href = $attributes['href'] ?? '';
416 $client_id = !empty($attributes['id']) ? md5($attributes['id']) : '';
417
418 // Handle content protection
419 $protection_data = self::extract_protection_data($attributes, $client_id);
420 $should_display_content = self::should_display_content($protection_data);
421 $isAdManager = !empty($attributes['adManager']) ? true : false;
422
423
424 // For PDF blocks, if we have saved content and should display it, return the content
425 if (!empty($content) && $should_display_content && !$isAdManager) {
426 return $content;
427 }
428
429 // If no href is provided, return empty
430 if (empty($href)) {
431 return '';
432 }
433
434
435 if (empty($content)) {
436 return self::embedpress_pdf_legacy_render_block($attributes);
437 }
438
439
440 // Render PDF-specific HTML
441 return self::render_embedpress_pdf_html($attributes, $content, $protection_data, $should_display_content);
442 }
443
444 public static function render_document($attributes, $content = '', $block = null)
445 {
446
447 // Extract basic attributes for PDF block
448 $href = $attributes['href'] ?? '';
449 $client_id = !empty($attributes['id']) ? md5($attributes['id']) : '';
450
451 // Handle content protection
452 $protection_data = self::extract_protection_data($attributes, $client_id);
453 $should_display_content = self::should_display_content($protection_data);
454 $isAdManager = !empty($attributes['adManager']) ? true : false;
455
456 // For PDF blocks, if we have saved content and should display it, return the content
457 if (!empty($content) && $should_display_content && !$isAdManager) {
458 return $content;
459 }
460
461 // If no href is provided, return empty
462 if (empty($href)) {
463 return '';
464 }
465
466
467 // Render PDF-specific HTML
468 return self::render_embedpress_document_html($attributes, $content, $protection_data, $should_display_content);
469 }
470
471 /**
472 * Extract content protection related data from attributes
473 *
474 * @param array $attributes Block attributes
475 * @param string $client_id Client ID for the block
476 * @return array Protection data array
477 */
478 private static function extract_protection_data($attributes, $client_id)
479 {
480 $content_password = $attributes['contentPassword'] ?? '';
481 $hash_pass = hash('sha256', wp_salt(32) . md5($content_password));
482 $password_correct = $_COOKIE['password_correct_' . $client_id] ?? '';
483 return [
484 'content_password' => $content_password,
485 'hash_pass' => $hash_pass,
486 'password_correct' => $password_correct,
487 'protection_type' => $attributes['protectionType'] ?? '',
488 'lock_content' => $attributes['lockContent'] ?? '',
489 'user_role' => $attributes['userRole'] ?? '',
490 'content_share' => $attributes['contentShare'] ?? '',
491 'protection_message' => $attributes['protectionMessage'] ?? '',
492 'content_id' => $attributes['clientId'] ?? '',
493 'client_id' => $client_id,
494 ];
495 }
496
497 /**
498 * Determine if content should be displayed based on protection settings
499 *
500 * @param array $protection_data Protection data array
501 * @return bool True if content should be displayed
502 */
503 private static function should_display_content($protection_data)
504 {
505 return (
506 !apply_filters('embedpress/is_allow_rander', false) ||
507 empty($protection_data['lock_content']) ||
508 ($protection_data['protection_type'] === 'password' && empty($protection_data['content_password'])) ||
509 ($protection_data['protection_type'] === 'password' &&
510 !empty(Helper::is_password_correct($protection_data['client_id'])) &&
511 ($protection_data['hash_pass'] === $protection_data['password_correct'])) ||
512 ($protection_data['protection_type'] === 'user-role' &&
513 Helper::has_content_allowed_roles($protection_data['user_role']))
514 );
515 }
516
517 /**
518 * Render the embed HTML with all configurations
519 *
520 * @param array $attributes Block attributes
521 * @param string $content Block content
522 * @param array $protection_data Protection data
523 * @param bool $should_display_content Whether content should be displayed
524 * @return string Rendered HTML
525 */
526 private static function render_embed_html($attributes, $content, $protection_data, $should_display_content)
527 {
528 // Extract basic configuration
529 $config = self::extract_basic_config($attributes);
530
531 // Build carousel configuration
532 $carousel_config = self::build_carousel_config($attributes, $protection_data['client_id']);
533
534 // Build player configuration
535 $player_config = self::build_player_config($attributes, $protection_data['client_id']);
536
537 // Get dynamic content
538 $embed = self::get_embed_content($attributes, $content);
539
540 // Inject iframeTitle derived from URL
541 $url = $attributes['url'] ?? '';
542 $title = self::get_iframe_title_from_url($url);
543
544 if (!empty($title)) {
545 if (is_array($embed) && isset($embed['html'])) {
546 $embed['html'] = preg_replace('/<iframe(.*?)>/i', '<iframe$1 title="' . esc_attr($title) . '">', $embed['html']);
547 } elseif (is_string($embed)) {
548 $embed = preg_replace('/<iframe(.*?)>/i', '<iframe$1 title="' . esc_attr($title) . '">', $embed);
549 }
550 }
551
552 // Build CSS classes and styling
553 $styling = self::build_styling_config($attributes, $protection_data);
554
555 // Generate final HTML
556 return self::generate_final_html($attributes, $embed, $config, $carousel_config, $player_config, $styling, $protection_data, $should_display_content);
557 }
558
559 private static function render_embedpress_document_html($attributes, $content, $protection_data, $should_display_content)
560 {
561
562 $href = $attributes['href'] ?? '';
563 if (empty($href)) return '';
564
565 $id = $attributes['id'] ?? 'embedpress-document-' . rand(100, 10000);
566 $client_id = $attributes['clientId'] ?? md5($id);
567 $contentShare = $attributes['contentShare'] ?? false;
568 $styling = self::build_styling_config($attributes, $protection_data);
569
570
571 ob_start();
572 ?>
573 <div class="wp-block-embedpress-document" data-embed-type="Document">
574 <?php self::render_embed_content($content, $contentShare, $id, $attributes, $should_display_content, $protection_data, $styling); ?>
575 <?php self::render_ad_template($attributes, $content, $client_id); ?>
576 </div>
577 <?php
578
579 return ob_get_clean();
580 }
581
582
583
584 private static function render_embedpress_pdf_html($attributes, $content, $protection_data, $should_display_content)
585 {
586 // Extract PDF-specific attributes
587 $href = $attributes['href'] ?? '';
588 if (empty($href)) {
589 return '';
590 }
591
592 $id = $attributes['id'] ?? 'embedpress-pdf-' . rand(100, 10000);
593 $client_id = md5($id);
594 $contentShare = $attributes['contentShare'] ?? false;
595
596 $styling = self::build_styling_config($attributes, $protection_data);
597
598 // Build the complete HTML structure
599 ob_start();
600 ?>
601 <div class="wp-block-embedpress-pdf" data-embed-type="PDF">
602 <?php self::render_embed_content($content, $contentShare, $id, $attributes, $should_display_content, $protection_data, $styling); ?>
603 <?php self::render_ad_template($attributes, $content, $client_id); ?>
604 </div>
605 <?php
606
607 return ob_get_clean();
608 }
609
610 /**
611 * Generate PDF parameters for viewer configuration
612 * Based on the self::generate_pdf_params function from the old implementation
613 */
614 /**
615 * Render PDF as a clickable thumbnail that opens in a lightbox
616 */
617 private static function render_pdf_lightbox_thumbnail($attributes, $client_id)
618 {
619 $href = $attributes['href'];
620 $viewerStyle = $attributes['viewerStyle'] ?? 'modern';
621 $unitoption = ($attributes['unitoption'] ?? 'px') === '%' ? '%' : 'px';
622 $width = !empty($attributes['width']) ? $attributes['width'] . $unitoption : '600px';
623 $powered_by = !empty($attributes['powered_by']);
624 $lightboxThumbnail = $attributes['lightboxThumbnail'] ?? '';
625 $lightboxAlign = $attributes['lightboxAlign'] ?? 'left';
626
627 $alignStyle = '';
628 if ($lightboxAlign === 'center') {
629 $alignStyle = 'text-align: center;';
630 } elseif ($lightboxAlign === 'right') {
631 $alignStyle = 'text-align: right;';
632 }
633
634 // Generate base64 viewer params
635 $urlParamData = self::build_pdf_param_array($attributes);
636 $queryString = http_build_query($urlParamData);
637 if (function_exists('mb_convert_encoding')) {
638 $queryString = mb_convert_encoding($queryString, 'UTF-8');
639 }
640 $viewerParams = base64_encode($queryString);
641
642 $pdfTitle = Helper::get_file_title($href);
643
644 ob_start();
645 ?>
646 <div id="ep-gutenberg-content-<?php echo esc_attr($client_id); ?>" class="ep-gutenberg-content">
647 <div class="embedpress-document-embed ep-doc-<?php echo esc_attr($client_id); ?>"
648 style="max-width: <?php echo esc_attr($width); ?>; <?php echo esc_attr($alignStyle); ?>"
649 data-embed-type="PDF">
650 <div class="ep-pdf-thumbnail-card">
651 <div class="ep-pdf-thumbnail-wrap"
652 data-pdf-url="<?php echo esc_url($href); ?>"
653 data-viewer-style="<?php echo esc_attr($viewerStyle); ?>"
654 data-viewer-params="<?php echo esc_attr($viewerParams); ?>">
655 <div class="ep-pdf-thumbnail-inner">
656 <?php if (!empty($lightboxThumbnail)): ?>
657 <img class="ep-pdf-thumbnail-custom" src="<?php echo esc_url($lightboxThumbnail); ?>" alt="<?php echo esc_attr($pdfTitle); ?>" />
658 <?php else: ?>
659 <canvas class="ep-pdf-thumbnail-canvas"
660 data-pdf-url="<?php echo esc_url($href); ?>"
661 data-loading="true"></canvas>
662 <?php endif; ?>
663 <div class="ep-pdf-thumbnail-overlay">
664 <div class="ep-pdf-thumbnail-icon-circle">
665 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"/></svg>
666 </div>
667 </div>
668 </div>
669 </div>
670
671 </div>
672 <?php if ($powered_by): ?>
673 <p class="embedpress-el-powered"><?php esc_html_e('Powered By EmbedPress', 'embedpress'); ?></p>
674 <?php endif; ?>
675 </div>
676 </div>
677 <?php
678 return ob_get_clean();
679 }
680
681 /**
682 * Render PDF as a button, link, or text trigger that opens in a lightbox
683 */
684 private static function render_pdf_trigger($attributes, $client_id, $mode)
685 {
686 $href = $attributes['href'];
687 $viewerStyle = $attributes['viewerStyle'] ?? 'modern';
688 $triggerText = !empty($attributes['triggerText']) ? $attributes['triggerText'] : 'View PDF';
689 $lightboxAlign = $attributes['lightboxAlign'] ?? 'left';
690
691 $alignStyle = '';
692 if ($lightboxAlign === 'center') {
693 $alignStyle = 'text-align: center;';
694 } elseif ($lightboxAlign === 'right') {
695 $alignStyle = 'text-align: right;';
696 }
697
698 // Build inline styles for trigger element
699 $triggerStyles = [];
700 if (!empty($attributes['triggerColor'])) {
701 $triggerStyles[] = 'color:' . esc_attr($attributes['triggerColor']);
702 }
703 if (!empty($attributes['triggerFontSize'])) {
704 $triggerStyles[] = 'font-size:' . intval($attributes['triggerFontSize']) . 'px';
705 }
706 if ($mode === 'button') {
707 if (!empty($attributes['triggerBgColor'])) {
708 $triggerStyles[] = 'background-color:' . esc_attr($attributes['triggerBgColor']);
709 }
710 if (!empty($attributes['triggerBorderRadius'])) {
711 $triggerStyles[] = 'border-radius:' . intval($attributes['triggerBorderRadius']) . 'px';
712 }
713 }
714 $triggerStyleAttr = !empty($triggerStyles) ? implode(';', $triggerStyles) : '';
715
716 // Generate base64 viewer params
717 $urlParamData = self::build_pdf_param_array($attributes);
718 $queryString = http_build_query($urlParamData);
719 if (function_exists('mb_convert_encoding')) {
720 $queryString = mb_convert_encoding($queryString, 'UTF-8');
721 }
722 $viewerParams = base64_encode($queryString);
723
724 ob_start();
725 ?>
726 <div id="ep-gutenberg-content-<?php echo esc_attr($client_id); ?>" class="ep-gutenberg-content">
727 <div class="embedpress-document-embed ep-doc-<?php echo esc_attr($client_id); ?>"
728 style="<?php echo esc_attr($alignStyle); ?>"
729 data-embed-type="PDF">
730 <div class="ep-pdf-thumbnail-wrap"
731 data-pdf-url="<?php echo esc_url($href); ?>"
732 data-viewer-style="<?php echo esc_attr($viewerStyle); ?>"
733 data-viewer-params="<?php echo esc_attr($viewerParams); ?>">
734 <span class="ep-pdf-trigger ep-pdf-trigger--<?php echo esc_attr($mode); ?>"<?php if ($triggerStyleAttr) echo ' style="' . esc_attr($triggerStyleAttr) . '"'; ?>><?php echo esc_html($triggerText); ?></span>
735 </div>
736 </div>
737 </div>
738 <?php
739 return ob_get_clean();
740 }
741
742 /**
743 * Build the PDF param data array (shared by generate_pdf_params and render_pdf_lightbox_thumbnail)
744 */
745 private static function build_pdf_param_array($attributes)
746 {
747 $urlParamData = array(
748 'themeMode' => !empty($attributes['themeMode']) ? $attributes['themeMode'] : 'default',
749 'toolbar' => !empty($attributes['toolbar']) ? 'true' : 'false',
750 'position' => $attributes['position'] ?? 'top',
751 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
752 'lazyLoad' => !empty($attributes['lazyLoad']) ? 'true' : 'false',
753 'download' => !empty($attributes['download']) ? 'true' : 'false',
754 'copy_text' => !empty($attributes['copy_text']) ? 'true' : 'false',
755 'add_text' => !empty($attributes['add_text']) ? 'true' : 'false',
756 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
757 'doc_rotation' => !empty($attributes['doc_rotation']) ? 'true' : 'false',
758 'add_image' => !empty($attributes['add_image']) ? 'true' : 'false',
759 'doc_details' => !empty($attributes['doc_details']) ? 'true' : 'false',
760 'zoom_in' => !empty($attributes['zoomIn']) ? 'true' : 'false',
761 'zoom_out' => !empty($attributes['zoomOut']) ? 'true' : 'false',
762 'fit_view' => !empty($attributes['fitView']) ? 'true' : 'false',
763 'bookmark' => !empty($attributes['bookmark']) ? 'true' : 'false',
764 'flipbook_toolbar_position' => !empty($attributes['flipbook_toolbar_position']) ? $attributes['flipbook_toolbar_position'] : 'bottom',
765 'selection_tool' => isset($attributes['selection_tool']) ? esc_attr($attributes['selection_tool']) : '0',
766 'scrolling' => isset($attributes['scrolling']) ? esc_attr($attributes['scrolling']) : '-1',
767 'spreads' => isset($attributes['spreads']) ? esc_attr($attributes['spreads']) : '-1',
768 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
769 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkFontSize']) ? esc_attr($attributes['watermarkFontSize']) : '48',
770 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
771 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? esc_attr($attributes['watermarkOpacity']) : '15',
772 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
773 );
774
775 if ($urlParamData['themeMode'] === 'custom') {
776 $urlParamData['customColor'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#403A81';
777 }
778
779 return $urlParamData;
780 }
781
782 private static function generate_pdf_params($attributes)
783 {
784 $urlParamData = array(
785 'themeMode' => !empty($attributes['themeMode']) ? $attributes['themeMode'] : 'default',
786 'toolbar' => !empty($attributes['toolbar']) ? 'true' : 'false',
787 'position' => $attributes['position'] ?? 'top',
788 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
789 'lazyLoad' => !empty($attributes['lazyLoad']) ? 'true' : 'false',
790 'download' => !empty($attributes['download']) ? 'true' : 'false',
791 'copy_text' => !empty($attributes['copy_text']) ? 'true' : 'false',
792 'add_text' => !empty($attributes['add_text']) ? 'true' : 'false',
793 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
794 'doc_rotation' => !empty($attributes['doc_rotation']) ? 'true' : 'false',
795 'add_image' => !empty($attributes['add_image']) ? 'true' : 'false',
796 'doc_details' => !empty($attributes['doc_details']) ? 'true' : 'false',
797 'zoom_in' => !empty($attributes['zoomIn']) ? 'true' : 'false',
798 'zoom_out' => !empty($attributes['zoomOut']) ? 'true' : 'false',
799 'fit_view' => !empty($attributes['fitView']) ? 'true' : 'false',
800 'bookmark' => !empty($attributes['bookmark']) ? 'true' : 'false',
801 'flipbook_toolbar_position' => !empty($attributes['flipbook_toolbar_position']) ? $attributes['flipbook_toolbar_position'] : 'bottom',
802 'selection_tool' => isset($attributes['selection_tool']) ? esc_attr($attributes['selection_tool']) : '0',
803 'scrolling' => isset($attributes['scrolling']) ? esc_attr($attributes['scrolling']) : '-1',
804 'spreads' => isset($attributes['spreads']) ? esc_attr($attributes['spreads']) : '-1',
805 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
806 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkFontSize']) ? esc_attr($attributes['watermarkFontSize']) : '48',
807 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
808 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? esc_attr($attributes['watermarkOpacity']) : '15',
809 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
810 );
811
812 // Add custom color for custom theme mode
813 if ($urlParamData['themeMode'] === 'custom') {
814 $urlParamData['customColor'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#403A81';
815 }
816
817 // Handle flip-book viewer style
818 if (isset($attributes['viewerStyle']) && $attributes['viewerStyle'] === 'flip-book') {
819 return "&key=" . base64_encode(mb_convert_encoding(http_build_query($urlParamData), 'UTF-8'));
820 }
821
822 return "#key=" . base64_encode(mb_convert_encoding(http_build_query($urlParamData), 'UTF-8'));
823 }
824
825 /**
826 * Generate document parameters for viewer configuration
827 * Based on document-specific attributes
828 */
829 private static function generate_document_params($attributes)
830 {
831 $urlParamData = array(
832 'theme_mode' => !empty($attributes['themeMode']) ? $attributes['themeMode'] : 'default',
833 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
834 'position' => $attributes['position'] ?? 'top',
835 'download' => !empty($attributes['download']) ? 'true' : 'false',
836 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
837 );
838
839 // Add custom color for custom theme mode
840 if ($urlParamData['theme_mode'] === 'custom') {
841 $urlParamData['custom_color'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#343434';
842 }
843
844 return '?' . http_build_query($urlParamData);
845 }
846
847 /**
848 * Extract basic configuration from attributes
849 *
850 * @param array $attributes Block attributes
851 * @return array Basic configuration
852 */
853 private static function extract_basic_config($attributes)
854 {
855 return [
856 'block_id' => !empty($attributes['clientId']) ? $attributes['clientId'] : '',
857 'custom_player' => !empty($attributes['customPlayer']) ? $attributes['customPlayer'] : 0,
858 'insta_layout' => !empty($attributes['instaLayout']) ? ' ' . $attributes['instaLayout'] : ' insta-grid',
859 'mode' => !empty($attributes['mode']) ? ' ep-google-photos-' . $attributes['mode'] : '',
860 'embed_type' => !empty($attributes['cEmbedType']) ? ' ' . $attributes['cEmbedType'] : '',
861 'url' => $attributes['url'] ?? '',
862 ];
863 }
864
865 /**
866 * Build carousel configuration
867 *
868 * @param array $attributes Block attributes
869 * @param string $client_id Client ID
870 * @return array Carousel configuration
871 */
872 private static function build_carousel_config($attributes, $client_id)
873 {
874 $carousel_options = '';
875 $carousel_id = '';
876
877 if (!empty($attributes['instaLayout']) && $attributes['instaLayout'] === 'insta-carousel') {
878 $carousel_id = 'data-carouselid=' . esc_attr($client_id);
879
880 $options = [
881 'layout' => $attributes['instaLayout'],
882 'slideshow' => !empty($attributes['slidesShow']) ? $attributes['slidesShow'] : 5,
883 'autoplay' => !empty($attributes['carouselAutoplay']) ? $attributes['carouselAutoplay'] : 0,
884 'autoplayspeed' => !empty($attributes['autoplaySpeed']) ? $attributes['autoplaySpeed'] : 3000,
885 'transitionspeed' => !empty($attributes['transitionSpeed']) ? $attributes['transitionSpeed'] : 1000,
886 'loop' => !empty($attributes['carouselLoop']) ? $attributes['carouselLoop'] : 0,
887 'arrows' => !empty($attributes['carouselArrows']) ? $attributes['carouselArrows'] : 0,
888 'spacing' => !empty($attributes['carouselSpacing']) ? $attributes['carouselSpacing'] : 0
889 ];
890
891 $carousel_options = 'data-carousel-options=' . htmlentities(json_encode($options), ENT_QUOTES);
892 }
893
894 return [
895 'carousel_id' => $carousel_id,
896 'carousel_options' => $carousel_options,
897 ];
898 }
899
900 /**
901 * Build player configuration
902 *
903 * @param array $attributes Block attributes
904 * @param string $client_id Client ID
905 * @return array Player configuration
906 */
907 private static function build_player_config($attributes, $client_id)
908 {
909 $custom_player = '';
910 $player_options = '';
911 $custom_player_enabled = !empty($attributes['customPlayer']) ? $attributes['customPlayer'] : 0;
912
913 if (!empty($custom_player_enabled)) {
914 $is_self_hosted = Helper::check_media_format($attributes['url']);
915 $custom_player = 'data-playerid=' . esc_attr($client_id);
916
917 $options = self::build_player_options($attributes, $is_self_hosted);
918 // Wrap in quotes — htmlentities(..., ENT_QUOTES) already encoded `"` as
919 // `&quot;`, but values like email-capture headlines can contain spaces,
920 // which would terminate an unquoted attribute mid-JSON and break
921 // initplyr.js's JSON.parse, silently disabling the custom player.
922 $player_options = 'data-options="' . htmlentities(json_encode($options), ENT_QUOTES) . '"';
923 }
924
925 return [
926 'custom_player' => $custom_player,
927 'player_options' => $player_options,
928 ];
929 }
930
931 /**
932 * Build player options array
933 *
934 * @param array $attributes Block attributes
935 * @param array $is_self_hosted Self-hosted media info
936 * @return array Player options
937 */
938 private static function build_player_options($attributes, $is_self_hosted)
939 {
940 $options = [
941 'rewind' => !empty($attributes['playerRewind']),
942 'restart' => !empty($attributes['playerRestart']),
943 'pip' => !empty($attributes['playerPip']),
944 'poster_thumbnail' => $attributes['posterThumbnail'] ?? '',
945 'player_color' => $attributes['playerColor'] ?? '',
946 'player_preset' => $attributes['playerPreset'] ?? 'preset-default',
947 'fast_forward' => !empty($attributes['playerFastForward']),
948 'player_tooltip' => !empty($attributes['playerTooltip']),
949 'hide_controls' => !empty($attributes['playerHideControls']),
950 'download' => !empty($attributes['playerDownload']),
951 ];
952
953 // Pro: advanced custom-player feature data (auto resume, timed CTA,
954 // chapters, email capture, action lock, adaptive streaming, heatmap,
955 // LMS tracking, privacy mode, end screen). Pro plugin populates the
956 // array; with Pro disabled this is a no-op and the keys never reach
957 // the data-options blob — frontend skips those features entirely.
958 $options = apply_filters('embedpress/gutenberg/advanced_player_options', $options, $attributes);
959
960 // Add conditional options
961 $conditional_options = [
962 'fullscreen' => 'fullscreen',
963 'starttime' => 'start',
964 'endtime' => 'end',
965 'relatedvideos' => 'rel',
966 'muteVideo' => 'mute',
967 'vstarttime' => 't',
968 'vautoplay' => 'vautoplay',
969 'vautopause' => 'autopause',
970 'vdnt' => 'dnt',
971 ];
972
973 foreach ($conditional_options as $attr_key => $option_key) {
974 if (!empty($attributes[$attr_key])) {
975 $options[$option_key] = $attributes[$attr_key];
976 }
977 }
978
979 // Add self-hosted options
980 if (!empty($is_self_hosted['selhosted'])) {
981 $options['self_hosted'] = $is_self_hosted['selhosted'];
982 $options['hosted_format'] = $is_self_hosted['format'];
983 }
984
985 return $options;
986 }
987
988 /**
989 * Country Restriction (Pro)
990 *
991 * Returns the HTML restricted-fallback when the viewer's country is
992 * blocked, or `false` to render normally. Reads country from
993 * Cloudflare's `CF-IPCountry` header, falling back to common Apache
994 * GeoIP / proxy headers. If no country can be detected, allow.
995 *
996 * Adds `Vary: CF-IPCountry` so caches can vary per-country.
997 */
998 /**
999 * Resolve the visitor's ISO country code. Order:
1000 * 1. CDN / reverse-proxy headers (zero-cost when present)
1001 * 2. `embedpress_visitor_country` filter (lets sites inject their own)
1002 * 3. Free HTTP GeoIP lookup at ipwho.is, cached per-IP for 12h
1003 *
1004 * Returns '' (fail-open) if every source is unavailable so a misconfigured
1005 * lookup never silently blocks legitimate visitors.
1006 */
1007 public static function resolve_visitor_country_public()
1008 {
1009 return self::resolve_visitor_country();
1010 }
1011
1012 private static function resolve_visitor_country()
1013 {
1014 $country = '';
1015 foreach (['HTTP_CF_IPCOUNTRY', 'GEOIP_COUNTRY_CODE', 'HTTP_X_COUNTRY_CODE'] as $key) {
1016 if (!empty($_SERVER[$key])) {
1017 $country = strtoupper(sanitize_text_field($_SERVER[$key]));
1018 break;
1019 }
1020 }
1021 $country = apply_filters('embedpress_visitor_country', $country);
1022 if ($country) return $country;
1023
1024 $ip = self::client_ip();
1025 if (!$ip) return '';
1026
1027 $cache_key = 'ep_geo_' . md5($ip);
1028 $cached = get_transient($cache_key);
1029 if ($cached !== false) {
1030 return $cached === '__none__' ? '' : $cached;
1031 }
1032
1033 $resp = wp_remote_get('https://ipwho.is/' . rawurlencode($ip) . '?fields=country_code,success', [
1034 'timeout' => 2,
1035 ]);
1036 if (is_wp_error($resp)) {
1037 // Negative-cache briefly so failed lookups don't repeat per-render.
1038 set_transient($cache_key, '__none__', 5 * MINUTE_IN_SECONDS);
1039 return '';
1040 }
1041 $body = json_decode(wp_remote_retrieve_body($resp), true);
1042 $code = (is_array($body) && !empty($body['country_code'])) ? strtoupper($body['country_code']) : '';
1043 set_transient($cache_key, $code ?: '__none__', $code ? 12 * HOUR_IN_SECONDS : 5 * MINUTE_IN_SECONDS);
1044 return $code;
1045 }
1046
1047 private static function client_ip()
1048 {
1049 foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'] as $key) {
1050 if (empty($_SERVER[$key])) continue;
1051 $candidate = trim(explode(',', $_SERVER[$key])[0]);
1052 if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
1053 return $candidate;
1054 }
1055 // Allow private IPs only as last-resort REMOTE_ADDR (dev environments).
1056 if ($key === 'REMOTE_ADDR' && filter_var($candidate, FILTER_VALIDATE_IP)) {
1057 return $candidate;
1058 }
1059 }
1060 return '';
1061 }
1062
1063
1064 /**
1065 * Rewrite self-hosted video / source URLs to the configured CDN URL
1066 * when an attachment has been offloaded. Best-effort regex pass over
1067 * the embed HTML.
1068 */
1069
1070 /**
1071 * Get embed content with dynamic rendering
1072 *
1073 * @param array $attributes Block attributes
1074 * @param string $content Block content
1075 * @return string Embed content
1076 */
1077 private static function get_embed_content($attributes, $content)
1078 {
1079 return apply_filters('embedpress_render_dynamic_content', $content, $attributes);
1080 }
1081
1082 /**
1083 * Build styling configuration
1084 *
1085 * @param array $attributes Block attributes
1086 * @param array $protection_data Protection data
1087 * @return array Styling configuration
1088 */
1089 private static function build_styling_config($attributes, $protection_data)
1090 {
1091 $client_id = $protection_data['client_id'];
1092
1093 // Content sharing classes
1094 $content_share_class = !empty($attributes['contentShare']) ? 'ep-content-share-enabled' : '';
1095 $share_position = $attributes['sharePosition'] ?? 'right';
1096 $share_position_class = !empty($attributes['contentShare']) ? 'ep-share-position-' . $share_position : '';
1097
1098 // Content protection classes
1099 $password_correct = $_COOKIE['password_correct_' . $client_id] ?? '';
1100 $hash_pass = hash('sha256', wp_salt(32) . md5($attributes['contentPassword'] ?? ''));
1101 $content_protection_class = 'ep-content-protection-enabled';
1102
1103 if (empty($attributes['lockContent']) || empty($attributes['contentPassword']) || $hash_pass === $password_correct) {
1104 $content_protection_class = 'ep-content-protection-disabled';
1105 }
1106
1107 // Alignment classes
1108 $alignment = self::get_alignment_class($attributes['align'] ?? '');
1109
1110 // Ad manager attributes
1111 $ads_attrs = self::build_ads_attributes($attributes, $client_id);
1112
1113 // Custom branding styles and HTML
1114 $custom_branding = self::build_custom_branding($attributes, $client_id);
1115
1116 // Media format classes
1117 $hosted_format = self::get_hosted_format($attributes);
1118 $yt_channel_class = (isset($attributes['url']) && Helper::is_youtube_channel($attributes['url'])) ? 'embedded-youtube-channel' : '';
1119
1120 $auto_pause = !empty($attributes['autoPause']) ? ' enabled-auto-pause' : '';
1121
1122 return [
1123 'content_share_class' => $content_share_class,
1124 'share_position_class' => $share_position_class,
1125 'content_protection_class' => $content_protection_class,
1126 'alignment' => $alignment,
1127 'ads_attrs' => $ads_attrs,
1128 'custom_branding' => $custom_branding,
1129 'hosted_format' => $hosted_format,
1130 'yt_channel_class' => $yt_channel_class,
1131 'auto_pause' => $auto_pause,
1132 'pass_hash_key' => isset($attributes['contentPassword']) ? md5($attributes['contentPassword']) : '',
1133 ];
1134 }
1135
1136 /**
1137 * Get alignment CSS class
1138 *
1139 * @param string $align Alignment value
1140 * @return string CSS class
1141 */
1142 private static function get_alignment_class($align)
1143 {
1144 return isset(self::$alignment_classes[$align])
1145 ? self::$alignment_classes[$align] . ' ep-clear'
1146 : 'aligncenter';
1147 }
1148
1149 /**
1150 * Build ad manager attributes
1151 *
1152 * @param array $attributes Block attributes
1153 * @param string $client_id Client ID
1154 * @return string Ad attributes
1155 */
1156 private static function build_ads_attributes($attributes, $client_id)
1157 {
1158 if (empty($attributes['adManager'])) {
1159 return '';
1160 }
1161
1162 $ad = base64_encode(json_encode($attributes));
1163 return "data-sponsored-id=$client_id data-sponsored-attrs=$ad class=sponsored-mask";
1164 }
1165
1166 /**
1167 * Get hosted media format
1168 *
1169 * @param array $attributes Block attributes
1170 * @return string Media format
1171 */
1172 private static function get_hosted_format($attributes)
1173 {
1174 if (empty($attributes['customPlayer'])) {
1175 return '';
1176 }
1177
1178 $self_hosted = Helper::check_media_format($attributes['url']);
1179 return $self_hosted['format'] ?? '';
1180 }
1181
1182 /**
1183 * Build custom branding configuration
1184 *
1185 * @param array $attributes Block attributes
1186 * @param string $client_id Client ID
1187 * @return array Custom branding configuration
1188 */
1189 private static function build_custom_branding($attributes, $client_id)
1190 {
1191 $custom_branding = [
1192 'html' => '',
1193 'styles' => ''
1194 ];
1195
1196 // Check if custom logo is enabled
1197 if (empty($attributes['customlogo'])) {
1198 return $custom_branding;
1199 }
1200
1201 $logo_url = $attributes['customlogo'];
1202 $logo_x = $attributes['logoX'] ?? 5;
1203 $logo_y = $attributes['logoY'] ?? 10;
1204 $logo_opacity = $attributes['logoOpacity'] ?? 1;
1205 $custom_logo_url = $attributes['customlogoUrl'] ?? '';
1206
1207 // Generate custom logo styles
1208 $custom_branding['styles'] = sprintf(
1209 '#ep-gutenberg-content-%s img.watermark {
1210 border: 0;
1211 position: absolute;
1212 bottom: %s%%;
1213 right: %s%%;
1214 max-width: 150px;
1215 max-height: 75px;
1216 -o-transition: opacity 0.5s ease-in-out;
1217 -moz-transition: opacity 0.5s ease-in-out;
1218 -webkit-transition: opacity 0.5s ease-in-out;
1219 transition: opacity 0.5s ease-in-out;
1220 z-index: 1;
1221 opacity: %s;
1222 }
1223 #ep-gutenberg-content-%s img.watermark:hover {
1224 opacity: 1;
1225 }',
1226 esc_attr($client_id),
1227 esc_attr($logo_y),
1228 esc_attr($logo_x),
1229 esc_attr($logo_opacity),
1230 esc_attr($client_id)
1231 );
1232
1233 // Generate custom logo HTML
1234 $logo_html = sprintf(
1235 '<img decoding="async" src="%s" class="watermark ep-custom-logo" width="auto" height="auto" alt="">',
1236 esc_url($logo_url)
1237 );
1238
1239 // Wrap with link if URL is provided
1240 if (!empty($custom_logo_url)) {
1241 $logo_html = sprintf(
1242 '<a href="%s" target="_blank">%s</a>',
1243 esc_url($custom_logo_url),
1244 $logo_html
1245 );
1246 }
1247
1248 $custom_branding['html'] = $logo_html;
1249
1250 return $custom_branding;
1251 }
1252
1253 /**
1254 * Generate the final HTML output
1255 *
1256 * @param array $attributes Block attributes
1257 * @param string $embed Embed content
1258 * @param array $config Basic configuration
1259 * @param array $carousel_config Carousel configuration
1260 * @param array $player_config Player configuration
1261 * @param array $styling Styling configuration
1262 * @param array $protection_data Protection data
1263 * @param bool $should_display_content Whether content should be displayed
1264 * @return string Final HTML output
1265 */
1266 private static function generate_final_html($attributes, $embed, $config, $carousel_config, $player_config, $styling, $protection_data, $should_display_content)
1267 {
1268 ob_start();
1269
1270 // Extract variables for template
1271 $url = $config['url'];
1272 $block_id = $config['block_id'];
1273 $client_id = $protection_data['client_id'];
1274 $content_id = $protection_data['content_id'];
1275 $content_share = $protection_data['content_share'];
1276
1277 // Build wrapper classes
1278 $wrapper_classes = self::build_wrapper_classes($styling, $config);
1279 $embed_wrapper_classes = self::build_embed_wrapper_classes($attributes);
1280 $content_wrapper_classes = self::build_content_wrapper_classes($attributes, $config, $styling);
1281
1282 // Cap the content wrapper width to the block's width control so the
1283 // outer wrapper doesn't render wider than the embed itself (the inner
1284 // .plyr already gets width/height inline).
1285 $content_wrapper_style = '';
1286 if (!empty($attributes['width'])) {
1287 $unit = ($attributes['unitoption'] ?? 'px') === '%' ? '%' : 'px';
1288 $content_wrapper_style = 'max-width:' . intval($attributes['width']) . $unit;
1289 // max-width alone leaves the wrapper flush-left; mirror the block's
1290 // align control so centered/right alignments still take effect.
1291 $align = $attributes['align'] ?? '';
1292 if ($align === 'center') {
1293 $content_wrapper_style .= ';margin-left:auto;margin-right:auto';
1294 } elseif ($align === 'right') {
1295 $content_wrapper_style .= ';margin-left:auto;margin-right:0';
1296 }
1297 }
1298
1299 // Pro: CDN Offloading and Advanced Privacy Mode. Filters are
1300 // no-ops without Pro; the Pro callbacks gate on the toggle.
1301 $embed = apply_filters('embedpress/gutenberg/cdn_rewrite_html', $embed, $attributes);
1302 $embed = apply_filters('embedpress/gutenberg/privacy_mode_html', $embed, $attributes);
1303 if (!empty($attributes['customPlayer']) && !empty($attributes['playerPrivacyMode'])) {
1304 // Mirror the Pro gate so the click-to-load overlay class
1305 // attaches to the wrapper. Without Pro the overlay JS never
1306 // runs, but the unused class is harmless.
1307 $content_wrapper_classes .= ' ep-privacy-pending';
1308 }
1309
1310 ?>
1311 <?php if (!empty($styling['custom_branding']['styles'])): ?>
1312 <style>
1313 <?php echo $styling['custom_branding']['styles']; ?>
1314 </style>
1315 <?php endif; ?>
1316
1317 <div class="embedpress-gutenberg-wrapper source-provider-<?php echo esc_attr( Helper::get_provider_name($url) ); ?> <?php echo esc_attr($wrapper_classes); ?>" id="<?php echo esc_attr($block_id); ?>" data-embed-type="<?php echo esc_attr( Helper::get_provider_name($url) ); ?> ">
1318 <div class="wp-block-embed__wrapper <?php echo esc_attr($embed_wrapper_classes); ?>">
1319 <div id="ep-gutenberg-content-<?php echo esc_attr($client_id) ?>" class="ep-gutenberg-content<?php echo esc_attr($styling['auto_pause']); ?>">
1320 <div <?php echo esc_attr($styling['ads_attrs']); ?>>
1321 <div class="ep-embed-content-wraper <?php echo esc_attr($content_wrapper_classes); ?>"
1322 <?php if (!empty($content_wrapper_style)): ?>style="<?php echo esc_attr($content_wrapper_style); ?>"<?php endif; ?>
1323 <?php echo esc_attr($player_config['custom_player']); ?>
1324 <?php echo $player_config['player_options']; // already a complete escaped attribute (data-options="..."); esc_attr would double-encode the outer quotes ?>
1325 <?php echo esc_attr($carousel_config['carousel_id']); ?>
1326 <?php echo esc_attr($carousel_config['carousel_options']); ?>>
1327
1328 <?php
1329 self::render_embed_content($embed, $content_share, $content_id, $attributes, $should_display_content, $protection_data, $styling);
1330 ?>
1331 </div>
1332
1333 <?php self::render_ad_template($attributes, $embed, $client_id); ?>
1334 </div>
1335 </div>
1336 </div>
1337 </div>
1338 <?php
1339
1340 return ob_get_clean();
1341 }
1342
1343 /**
1344 * Build wrapper CSS classes
1345 *
1346 * @param array $styling Styling configuration
1347 * @param array $config Basic configuration
1348 * @return string CSS classes
1349 */
1350 private static function build_wrapper_classes($styling, $config)
1351 {
1352 return trim(sprintf(
1353 '%s %s %s %s%s',
1354 $styling['alignment'],
1355 $styling['content_share_class'],
1356 $styling['share_position_class'],
1357 $styling['content_protection_class'],
1358 $config['embed_type']
1359 ));
1360 }
1361
1362 /**
1363 * Build embed wrapper CSS classes
1364 *
1365 * @param array $attributes Block attributes
1366 * @return string CSS classes
1367 */
1368 private static function build_embed_wrapper_classes($attributes)
1369 {
1370 $classes = [];
1371
1372 if (!empty($attributes['contentShare'])) {
1373 $share_position = $attributes['sharePosition'] ?? 'right';
1374 $classes[] = 'position-' . $share_position . '-wraper';
1375 }
1376
1377 if (($attributes['videosize'] ?? '') === 'responsive') {
1378 $classes[] = 'ep-video-responsive';
1379 }
1380
1381 return implode(' ', $classes);
1382 }
1383
1384 /**
1385 * Build content wrapper CSS classes
1386 *
1387 * @param array $attributes Block attributes
1388 * @param array $config Basic configuration
1389 * @param array $styling Styling configuration
1390 * @return string CSS classes
1391 */
1392 private static function build_content_wrapper_classes($attributes, $config, $styling)
1393 {
1394 return trim(sprintf(
1395 '%s%s%s %s %s',
1396 $attributes['playerPreset'] ?? '',
1397 $config['insta_layout'],
1398 $config['mode'],
1399 $styling['hosted_format'],
1400 $styling['yt_channel_class']
1401 ));
1402 }
1403
1404 /**
1405 * Render embed content with protection logic
1406 *
1407 * @param string $embed Embed content
1408 * @param string $content_share Content share setting
1409 * @param string $content_id Content ID
1410 * @param array $attributes Block attributes
1411 * @param bool $should_display_content Whether content should be displayed
1412 * @param array $protection_data Protection data
1413 * @param array $styling Styling configuration
1414 */
1415 private static function render_embed_content($embed, $content_share, $content_id, $attributes, $should_display_content, $protection_data, $styling)
1416 {
1417 if ($should_display_content) {
1418 self::render_displayable_content($embed, $content_share, $content_id, $attributes, $styling);
1419 } else {
1420 self::render_protected_content($embed, $content_share, $content_id, $attributes, $protection_data, $styling);
1421 }
1422 }
1423
1424 /**
1425 * Render displayable content
1426 *
1427 * @param string $embed Embed content
1428 * @param string $content_share Content share setting
1429 * @param string $content_id Content ID
1430 * @param array $attributes Block attributes
1431 * @param array $styling Styling configuration (optional)
1432 */
1433 private static function render_displayable_content($embed, $content_share, $content_id, $attributes, $styling = [])
1434 {
1435 // Add custom branding if available
1436 if (!empty($styling['custom_branding']['html'])) {
1437 if (is_array($embed)) {
1438 $embed['html'] .= $styling['custom_branding']['html'];
1439 } else {
1440 $embed .= $styling['custom_branding']['html'];
1441 }
1442 }
1443
1444 if (!empty($content_share)) {
1445 $embed .= Helper::embed_content_share($content_id, $attributes);
1446 }
1447
1448 if (is_array($embed)) {
1449 echo $embed['html'];
1450 } else {
1451 echo $embed;
1452 }
1453 }
1454
1455 /**
1456 * Render protected content
1457 *
1458 * @param string $embed Embed content
1459 * @param string $content_share Content share setting
1460 * @param string $content_id Content ID
1461 * @param array $attributes Block attributes
1462 * @param array $protection_data Protection data
1463 * @param array $styling Styling configuration
1464 */
1465 private static function render_protected_content($embed, $content_share, $content_id, $attributes, $protection_data, $styling)
1466 {
1467 // Add custom branding if available
1468 if (!empty($styling['custom_branding']['html'])) {
1469 if (is_array($embed)) {
1470 $embed['html'] .= $styling['custom_branding']['html'];
1471 } else {
1472 $embed .= $styling['custom_branding']['html'];
1473 }
1474 }
1475
1476 if (!empty($content_share)) {
1477 $embed .= Helper::embed_content_share($content_id, $attributes);
1478 }
1479
1480 if ($protection_data['protection_type'] === 'password') {
1481 echo '<div id="ep-gutenberg-content-' . esc_attr($protection_data['client_id']) . '" class="ep-gutenberg-content">';
1482 do_action('embedpress/display_password_form', $protection_data['client_id'], $embed, $styling['pass_hash_key'], $attributes);
1483 echo '</div>';
1484 } else {
1485 do_action('embedpress/content_protection_content', $protection_data['client_id'], $protection_data['protection_message'], $protection_data['user_role']);
1486 }
1487 }
1488
1489 /**
1490 * Render ad template if enabled
1491 *
1492 * @param array $attributes Block attributes
1493 * @param string $embed Embed content
1494 * @param string $client_id Client ID
1495 */
1496 private static function render_ad_template($attributes, $embed, $client_id)
1497 {
1498 if (!empty($attributes['adManager'])) {
1499 $embed = apply_filters('embedpress/generate_ad_template', $embed, $client_id, $attributes, 'gutenberg');
1500 }
1501 }
1502
1503 /**
1504 * YouTube block legacy render method
1505 *
1506 * @param array $attributes Block attributes
1507 * @param string $content Block content
1508 * @param object $block Block object (unused but kept for compatibility)
1509 * @return string Rendered HTML content
1510 */
1511 public static function render_youtube_block($attributes, $content = '', $block = null)
1512 {
1513
1514 if (!empty($content)) {
1515 return $content;
1516 }
1517 // Extract basic attributes for YouTube block
1518 $iframe_src = $attributes['iframeSrc'] ?? '';
1519 $align = $attributes['align'] ?? 'center';
1520
1521 // If no iframe source is provided, return empty
1522 if (empty($iframe_src)) {
1523 return '';
1524 }
1525
1526 // Validate YouTube URL
1527 if (!self::is_youtube_url($iframe_src)) {
1528 return '';
1529 }
1530
1531 // Apply YouTube parameters filter
1532 $youtube_params = apply_filters('embedpress_gutenberg_youtube_params', []);
1533 $processed_iframe_url = $iframe_src;
1534
1535 foreach ($youtube_params as $param => $value) {
1536 $processed_iframe_url = add_query_arg($param, $value, $processed_iframe_url);
1537 }
1538
1539 // Build alignment class
1540 $align_class = 'align' . $align;
1541
1542 // Extract width/height from attributes
1543 $width = isset($attributes['width']) ? intval($attributes['width']) : 640;
1544 $height = isset($attributes['height']) ? intval($attributes['height']) : 360;
1545
1546 // Generate YouTube block HTML
1547 ob_start();
1548 ?>
1549 <div class="ose-youtube responsive wp-block-embed-youtube ose-youtube-single-video <?php echo esc_attr($align_class); ?>" style="max-width: 100%;">
1550 <iframe src="<?php echo esc_url($processed_iframe_url); ?>"
1551 allowtransparency="true"
1552 allowfullscreen="true"
1553 frameborder="0"
1554 style="max-width: 100%;"
1555 width="<?php echo esc_attr($width); ?>" height="<?php echo esc_attr($height); ?>">
1556 </iframe>
1557 </div>
1558 <?php
1559 return ob_get_clean();
1560 }
1561
1562 /**
1563 * Validate if URL is a YouTube URL
1564 *
1565 * @param string $url URL to validate
1566 * @return bool True if valid YouTube URL, false otherwise
1567 */
1568 private static function is_youtube_url($url)
1569 {
1570 $pattern = '/^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?(.*&)?v=|(embed|v)\/))|youtu.be\/([a-zA-Z0-9_-]{11})/';
1571 return preg_match($pattern, $url);
1572 }
1573
1574 /**
1575 * Wistia block legacy render method
1576 *
1577 * @param array $attributes Block attributes
1578 * @param string $content Block content
1579 * @param object $block Block object (unused but kept for compatibility)
1580 * @return string Rendered HTML content
1581 */
1582 public static function render_wistia_block($attributes, $content = '', $block = null)
1583 {
1584 if (!empty($content)) {
1585 return $content;
1586 }
1587
1588 // Extract basic attributes for Wistia block
1589 $url = $attributes['url'] ?? '';
1590 $iframe_src = $attributes['iframeSrc'] ?? '';
1591 $align = $attributes['align'] ?? 'center';
1592
1593 // If no URL is provided, return empty
1594 if (empty($url)) {
1595 return '';
1596 }
1597
1598 // Extract Wistia media ID from URL
1599 $wistia_id = self::extract_wistia_id($url);
1600 if (empty($wistia_id)) {
1601 return '';
1602 }
1603
1604 // Build alignment class
1605 $align_class = 'align' . $align;
1606
1607 // Generate Wistia block HTML
1608 ob_start();
1609 ?>
1610 <div class="ose-wistia wp-block-embed-youtube <?php echo esc_attr($align_class); ?>" id="wistia_<?php echo esc_attr($wistia_id); ?>">
1611 <iframe src="<?php echo esc_url($iframe_src); ?>"
1612 allowtransparency="true"
1613 frameborder="0"
1614 class="wistia_embed"
1615 name="wistia_embed"
1616 width="600"
1617 height="330">
1618 </iframe>
1619 <?php do_action('embedpress_gutenberg_wistia_block_after_embed', $attributes); ?>
1620 </div>
1621 <?php
1622 return ob_get_clean();
1623 }
1624
1625 /**
1626 * Extract Wistia media ID from URL
1627 *
1628 * @param string $url Wistia URL
1629 * @return string|false Wistia media ID or false if not found
1630 */
1631 private static function extract_wistia_id($url)
1632 {
1633 preg_match('~medias/(.*)~i', esc_url($url), $matches);
1634 return isset($matches[1]) ? $matches[1] : false;
1635 }
1636
1637 /**
1638 * Calendar block render method
1639 *
1640 * @param array $attributes Block attributes
1641 * @param string $content Block content
1642 * @param object $block Block object (unused but kept for compatibility)
1643 * @return string Rendered HTML content
1644 */
1645 public static function render_embedpress_calendar($attributes, $content = '', $block = null)
1646 {
1647 if (!empty($content)) {
1648 return $content;
1649 }
1650
1651 // Extract basic attributes for Calendar block
1652 $url = $attributes['url'] ?? '';
1653 $width = $attributes['width'] ?? '600';
1654 $height = $attributes['height'] ?? '600';
1655 $powered_by = $attributes['powered_by'] ?? false;
1656 $is_public = $attributes['is_public'] ?? true;
1657 $align = $attributes['align'] ?? 'center';
1658
1659 $align_class = 'align' . $align;
1660
1661 // Private branch: same path Elementor uses — fire the action that Pro
1662 // ([Embedpress\Pro\Filters\Calendar]) hooks to emit the epgc-calendar
1663 // shortcode markup. Frontend JS then hydrates it via the AJAX endpoint
1664 // (which has its own transient cache via epgc_cache_time).
1665 if (!$is_public) {
1666 if (!apply_filters('embedpress/is_allow_rander', false)) {
1667 return '';
1668 }
1669 // Enqueue the FullCalendar bundle. The Elementor widget gets these
1670 // automatically via get_script_depends(); the Gutenberg block needs
1671 // an explicit call at render time since block.json has no viewScript.
1672 if (class_exists('Embedpress_Google_Helper')) {
1673 \Embedpress_Google_Helper::enqueue_scripts(); // ensures registration
1674 }
1675 wp_enqueue_style('fullcalendar');
1676 wp_enqueue_style('fullcalendar_daygrid');
1677 wp_enqueue_style('fullcalendar_timegrid');
1678 wp_enqueue_style('fullcalendar_list');
1679 wp_enqueue_style('epgc');
1680 wp_enqueue_style('tippy_light');
1681 wp_enqueue_script('fullcalendar_moment_timezone');
1682 wp_enqueue_script('fullcalendar_daygrid');
1683 wp_enqueue_script('fullcalendar_timegrid');
1684 wp_enqueue_script('fullcalendar_list');
1685 wp_enqueue_script('fullcalendar_locales');
1686 wp_enqueue_script('tippy');
1687 wp_enqueue_script('epgc');
1688 // Localize the nonce + ajax URL onto the epgc handle. LocalizationManager
1689 // handles this on Elementor pages; on Gutenberg frontends we have to.
1690 if (class_exists('\\EmbedPress\\Core\\LocalizationManager')) {
1691 \EmbedPress\Core\LocalizationManager::setup_elementor_localization();
1692 }
1693
1694 ob_start();
1695 ?>
1696 <figure class="wp-block-embedpress-embedpress-calendar <?php echo esc_attr($align_class); ?>" style="width: <?php echo esc_attr($width); ?>px; height: <?php echo esc_attr($height); ?>px;">
1697 <?php do_action('embedpress_google_helper_shortcode', 10); ?>
1698 <?php if ($powered_by) : ?>
1699 <p class="embedpress-el-powered"><?php echo esc_html__('Powered By EmbedPress', 'embedpress'); ?></p>
1700 <?php endif; ?>
1701 </figure>
1702 <?php
1703 return ob_get_clean();
1704 }
1705
1706 // Public branch: iframe to calendar.google.com's embed view.
1707 if (empty($url)) {
1708 return '';
1709 }
1710 if (!self::is_google_calendar_url($url)) {
1711 return '<p class="embedpress-el-powered">' . esc_html__('Invalid Calendar Link', 'embedpress') . '</p>';
1712 }
1713
1714 $sanitized_url = esc_url($url);
1715
1716 ob_start();
1717 ?>
1718 <figure class="wp-block-embedpress-embedpress-calendar <?php echo esc_attr($align_class); ?>" style="width: <?php echo esc_attr($width); ?>px; height: <?php echo esc_attr($height); ?>px;">
1719 <iframe src="<?php echo esc_url($sanitized_url); ?>"
1720 width="<?php echo esc_attr($width); ?>"
1721 height="<?php echo esc_attr($height); ?>"
1722 frameborder="0"
1723 scrolling="no"
1724 title="<?php echo esc_attr(self::get_iframe_title_from_url($url)); ?>">
1725 </iframe>
1726
1727 <?php if ($powered_by) : ?>
1728 <p class="embedpress-el-powered"><?php echo esc_html__('Powered By EmbedPress', 'embedpress'); ?></p>
1729 <?php endif; ?>
1730 </figure>
1731 <?php
1732 return ob_get_clean();
1733 }
1734
1735 /**
1736 * Validate if URL is a Google Calendar URL
1737 *
1738 * @param string $url URL to validate
1739 * @return bool True if valid Google Calendar URL, false otherwise
1740 */
1741 private static function is_google_calendar_url($url)
1742 {
1743 $pattern = '/^https:\/\/calendar\.google\.com\/calendar\/(?:u\/\d+\/)?embed\?.*/';
1744 return preg_match($pattern, $url);
1745 }
1746
1747 /**
1748 * Get iframe title from URL
1749 *
1750 * @param string $url URL to derive title from
1751 * @return string Derived title
1752 */
1753 private static function get_iframe_title_from_url($url)
1754 {
1755 if (empty($url)) {
1756 return '';
1757 }
1758
1759 // Try getting title from WordPress attachment if it's a local file
1760 $file_title = Helper::get_file_title($url);
1761 if (!empty($file_title)) {
1762 return $file_title;
1763 }
1764
1765 // Try to get filename from URL
1766 $path = parse_url($url, PHP_URL_PATH);
1767 if ($path) {
1768 $filename = basename($path);
1769 // Remove extension
1770 $filename = preg_replace('/\.[^.]+$/', '', $filename);
1771 // Decode URL encoding
1772 $filename = urldecode($filename);
1773 // Replace hyphens/underscores with spaces
1774 $filename = str_replace(['-', '_'], ' ', $filename);
1775
1776 if (!empty($filename)) {
1777 return ucfirst($filename);
1778 }
1779 }
1780
1781 // Fallback to domain name
1782 $host = parse_url($url, PHP_URL_HOST);
1783 if ($host) {
1784 return $host;
1785 }
1786
1787 return $url;
1788 }
1789
1790 /**
1791 * Render PDF Gallery block
1792 */
1793 public static function render_pdf_gallery($attributes, $content = '', $block = null)
1794 {
1795 // If save.js produced valid content, return it (frontend JS will enhance it)
1796 if (!empty($content)) {
1797 return $content;
1798 }
1799
1800 $items = isset($attributes['pdfItems']) ? $attributes['pdfItems'] : [];
1801 if (empty($items)) {
1802 return '';
1803 }
1804
1805 $layout = isset($attributes['layout']) ? esc_attr($attributes['layout']) : 'grid';
1806 $bookshelf_style = isset($attributes['bookshelfStyle']) ? esc_attr($attributes['bookshelfStyle']) : 'dark-wood';
1807
1808 // Pro gate: bookshelf requires Pro
1809 if (($layout === 'bookshelf' || $layout === 'carousel') && !defined('EMBEDPRESS_SL_ITEM_SLUG')) {
1810 $layout = 'grid';
1811 }
1812
1813 $columns = isset($attributes['columns']) ? intval($attributes['columns']) : 3;
1814 $columns_tablet = isset($attributes['columnsTablet']) ? intval($attributes['columnsTablet']) : 2;
1815 $columns_mobile = isset($attributes['columnsMobile']) ? intval($attributes['columnsMobile']) : 1;
1816 $gap = isset($attributes['gap']) ? intval($attributes['gap']) : 20;
1817 $border_radius = isset($attributes['thumbnailBorderRadius']) ? intval($attributes['thumbnailBorderRadius']) : 8;
1818 $aspect_ratio = isset($attributes['thumbnailAspectRatio']) ? esc_attr($attributes['thumbnailAspectRatio']) : '4:3';
1819 $viewer_style = isset($attributes['viewerStyle']) ? esc_attr($attributes['viewerStyle']) : 'modern';
1820 $client_id = isset($attributes['clientId']) ? esc_attr($attributes['clientId']) : '';
1821 $gallery_id = 'ep-gallery-' . substr(md5(serialize($items)), 0, 8);
1822
1823 // Build viewer params
1824 $viewer_params = self::generate_gallery_viewer_params($attributes);
1825
1826 // Carousel options
1827 $carousel_options = '';
1828 if ($layout === 'carousel' || $layout === 'bookshelf') {
1829 $carousel_options = wp_json_encode([
1830 'autoplay' => !empty($attributes['carouselAutoplay']),
1831 'autoplaySpeed' => isset($attributes['carouselAutoplaySpeed']) ? intval($attributes['carouselAutoplaySpeed']) : 3000,
1832 'loop' => isset($attributes['carouselLoop']) ? (bool)$attributes['carouselLoop'] : true,
1833 'arrows' => isset($attributes['carouselArrows']) ? (bool)$attributes['carouselArrows'] : true,
1834 'dots' => !empty($attributes['carouselDots']),
1835 'slidesPerView' => isset($attributes['slidesPerView']) ? intval($attributes['slidesPerView']) : 3,
1836 ]);
1837 }
1838
1839 $style = sprintf(
1840 '--ep-gallery-columns-desktop:%d;--ep-gallery-columns-tablet:%d;--ep-gallery-columns-mobile:%d;--ep-gallery-gap:%dpx;--ep-gallery-radius:%dpx;',
1841 $columns, $columns_tablet, $columns_mobile, $gap, $border_radius
1842 );
1843
1844 ob_start();
1845 ?>
1846 <div class="ep-pdf-gallery"
1847 data-layout="<?php echo $layout; ?>"
1848 data-shelf-style="<?php echo esc_attr($bookshelf_style); ?>"
1849 data-columns="<?php echo $columns; ?>"
1850 data-columns-tablet="<?php echo $columns_tablet; ?>"
1851 data-columns-mobile="<?php echo $columns_mobile; ?>"
1852 data-gap="<?php echo $gap; ?>"
1853 data-border-radius="<?php echo $border_radius; ?>"
1854 data-viewer-style="<?php echo $viewer_style; ?>"
1855 data-viewer-params="<?php echo esc_attr($viewer_params); ?>"
1856 data-gallery-id="<?php echo esc_attr($gallery_id); ?>"
1857 <?php if ($carousel_options): ?>data-carousel-options="<?php echo esc_attr($carousel_options); ?>"<?php endif; ?>
1858 style="<?php echo esc_attr($style); ?>">
1859
1860 <?php if ($layout === 'carousel' || $layout === 'bookshelf'): ?>
1861 <div class="ep-pdf-gallery__carousel">
1862 <div class="ep-pdf-gallery__carousel-track">
1863 <?php else: ?>
1864 <div class="ep-pdf-gallery__grid">
1865 <?php endif; ?>
1866
1867 <?php foreach ($items as $index => $item):
1868 $pdf_url = isset($item['url']) ? esc_url($item['url']) : '';
1869 $pdf_name = isset($item['fileName']) ? esc_attr($item['fileName']) : '';
1870 $custom_thumb = isset($item['customThumbnailUrl']) ? esc_url($item['customThumbnailUrl']) : '';
1871 $auto_thumb = isset($item['autoThumbnailUrl']) ? esc_url($item['autoThumbnailUrl']) : '';
1872 $thumb_url = $custom_thumb ?: $auto_thumb;
1873 ?>
1874 <div class="ep-pdf-gallery__item"
1875 data-pdf-url="<?php echo $pdf_url; ?>"
1876 data-pdf-index="<?php echo intval($index); ?>"
1877 data-pdf-name="<?php echo $pdf_name; ?>">
1878 <div class="ep-pdf-gallery__thumbnail-wrap" data-ratio="<?php echo $aspect_ratio; ?>">
1879 <?php if ($thumb_url): ?>
1880 <img src="<?php echo $thumb_url; ?>" alt="<?php echo $pdf_name; ?>" />
1881 <?php else: ?>
1882 <canvas class="ep-pdf-gallery__canvas" data-pdf-src="<?php echo $pdf_url; ?>" data-loading="true"></canvas>
1883 <?php endif; ?>
1884 <div class="ep-pdf-gallery__overlay">
1885 <svg class="ep-pdf-gallery__view-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1886 <path d="M8 5v14l11-7z"/>
1887 </svg>
1888 </div>
1889 </div>
1890 <div class="ep-pdf-gallery__book-title"><?php echo esc_html($pdf_name); ?></div>
1891 </div>
1892 <?php endforeach; ?>
1893
1894 <?php if ($layout === 'carousel' || $layout === 'bookshelf'): ?>
1895 </div>
1896 <button class="ep-pdf-gallery__carousel-prev" aria-label="Previous">
1897 <svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
1898 </button>
1899 <button class="ep-pdf-gallery__carousel-next" aria-label="Next">
1900 <svg viewBox="0 0 24 24"><path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/></svg>
1901 </button>
1902 <div class="ep-pdf-gallery__carousel-dots"></div>
1903 </div>
1904 <?php else: ?>
1905 </div>
1906 <?php endif; ?>
1907 </div>
1908 <?php
1909 return ob_get_clean();
1910 }
1911
1912 /**
1913 * Generate base64-encoded viewer params for PDF gallery
1914 */
1915 private static function generate_gallery_viewer_params($attributes)
1916 {
1917 $theme_mode = isset($attributes['themeMode']) ? $attributes['themeMode'] : 'default';
1918
1919 $params = [
1920 'themeMode' => esc_attr($theme_mode),
1921 'toolbar' => !empty($attributes['toolbar']) ? 'true' : 'false',
1922 'position' => isset($attributes['position']) ? esc_attr($attributes['position']) : 'top',
1923 'flipbook_toolbar_position' => isset($attributes['flipbook_toolbar_position']) ? esc_attr($attributes['flipbook_toolbar_position']) : 'bottom',
1924 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
1925 'download' => !empty($attributes['download']) ? 'true' : 'false',
1926 'copy_text' => !empty($attributes['copy_text']) ? 'true' : 'false',
1927 'add_text' => !empty($attributes['add_text']) ? 'true' : 'false',
1928 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
1929 'doc_rotation' => !empty($attributes['doc_rotation']) ? 'true' : 'false',
1930 'doc_details' => !empty($attributes['doc_details']) ? 'true' : 'false',
1931 'add_image' => !empty($attributes['add_image']) ? 'true' : 'false',
1932 'zoom_in' => !empty($attributes['zoomIn']) ? 'true' : 'false',
1933 'zoom_out' => !empty($attributes['zoomOut']) ? 'true' : 'false',
1934 'fit_view' => !empty($attributes['fitView']) ? 'true' : 'false',
1935 'bookmark' => !empty($attributes['bookmark']) ? 'true' : 'false',
1936 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
1937 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkFontSize']) ? intval($attributes['watermarkFontSize']) : 48,
1938 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
1939 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? intval($attributes['watermarkOpacity']) : 15,
1940 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
1941 ];
1942
1943 if ($theme_mode === 'custom') {
1944 $params['customColor'] = isset($attributes['customColor']) ? esc_attr($attributes['customColor']) : '#403A81';
1945 }
1946
1947 $query_string = http_build_query($params);
1948 if (function_exists('mb_convert_encoding')) {
1949 $query_string = mb_convert_encoding($query_string, 'UTF-8');
1950 }
1951
1952 return base64_encode($query_string);
1953 }
1954 }
1955 ?>