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