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