PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Gutenberg / EmbedPressBlockRenderer.php
embedpress / EmbedPress / Gutenberg Last commit date
BlockManager.php 3 days ago EmbedPressBlockRenderer.php 3 days ago FallbackHandler.php 9 months ago InitBlocks.php 9 months ago
EmbedPressBlockRenderer.php
2199 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 'flipbook_highlight_links' => !empty($attributes['flipbookHighlightLinks']) ? 'true' : 'false',
921 'flipbook_highlight_color' => isset($attributes['flipbookHighlightColor']) ? esc_attr($attributes['flipbookHighlightColor']) : '',
922 'selection_tool' => isset($attributes['selection_tool']) ? esc_attr($attributes['selection_tool']) : '0',
923 'scrolling' => isset($attributes['scrolling']) ? esc_attr($attributes['scrolling']) : '-1',
924 'spreads' => isset($attributes['spreads']) ? esc_attr($attributes['spreads']) : '-1',
925 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
926 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkFontSize']) ? esc_attr($attributes['watermarkFontSize']) : '48',
927 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
928 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? esc_attr($attributes['watermarkOpacity']) : '15',
929 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
930 );
931
932 if ($urlParamData['themeMode'] === 'custom') {
933 $urlParamData['customColor'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#403A81';
934 }
935
936 return $urlParamData;
937 }
938
939 private static function generate_pdf_params($attributes)
940 {
941 $urlParamData = array(
942 'themeMode' => !empty($attributes['themeMode']) ? $attributes['themeMode'] : 'default',
943 'toolbar' => !empty($attributes['toolbar']) ? 'true' : 'false',
944 'position' => $attributes['position'] ?? 'top',
945 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
946 'lazyLoad' => !empty($attributes['lazyLoad']) ? 'true' : 'false',
947 'download' => !empty($attributes['download']) ? 'true' : 'false',
948 'copy_text' => !empty($attributes['copy_text']) ? 'true' : 'false',
949 'add_text' => !empty($attributes['add_text']) ? 'true' : 'false',
950 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
951 'doc_rotation' => !empty($attributes['doc_rotation']) ? 'true' : 'false',
952 'add_image' => !empty($attributes['add_image']) ? 'true' : 'false',
953 'doc_details' => !empty($attributes['doc_details']) ? 'true' : 'false',
954 'zoom_in' => !empty($attributes['zoomIn']) ? 'true' : 'false',
955 'zoom_out' => !empty($attributes['zoomOut']) ? 'true' : 'false',
956 'fit_view' => !empty($attributes['fitView']) ? 'true' : 'false',
957 'bookmark' => !empty($attributes['bookmark']) ? 'true' : 'false',
958 'flipbook_toolbar_position' => !empty($attributes['flipbook_toolbar_position']) ? $attributes['flipbook_toolbar_position'] : 'bottom',
959 'flipbook_rtl' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['flipbookPageFlipRTL']) ? 'true' : 'false',
960 'flipbook_highlight_links' => !empty($attributes['flipbookHighlightLinks']) ? 'true' : 'false',
961 'flipbook_highlight_color' => isset($attributes['flipbookHighlightColor']) ? esc_attr($attributes['flipbookHighlightColor']) : '',
962 'selection_tool' => isset($attributes['selection_tool']) ? esc_attr($attributes['selection_tool']) : '0',
963 'scrolling' => isset($attributes['scrolling']) ? esc_attr($attributes['scrolling']) : '-1',
964 'spreads' => isset($attributes['spreads']) ? esc_attr($attributes['spreads']) : '-1',
965 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
966 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkFontSize']) ? esc_attr($attributes['watermarkFontSize']) : '48',
967 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
968 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? esc_attr($attributes['watermarkOpacity']) : '15',
969 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
970 );
971
972 // Add custom color for custom theme mode
973 if ($urlParamData['themeMode'] === 'custom') {
974 $urlParamData['customColor'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#403A81';
975 }
976
977 // Handle flip-book viewer style
978 if (isset($attributes['viewerStyle']) && $attributes['viewerStyle'] === 'flip-book') {
979 return "&key=" . base64_encode(mb_convert_encoding(http_build_query($urlParamData), 'UTF-8'));
980 }
981
982 return "#key=" . base64_encode(mb_convert_encoding(http_build_query($urlParamData), 'UTF-8'));
983 }
984
985 /**
986 * Generate document parameters for viewer configuration
987 * Based on document-specific attributes
988 */
989 private static function generate_document_params($attributes)
990 {
991 $urlParamData = array(
992 'theme_mode' => !empty($attributes['themeMode']) ? $attributes['themeMode'] : 'default',
993 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
994 'position' => $attributes['position'] ?? 'top',
995 'download' => !empty($attributes['download']) ? 'true' : 'false',
996 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
997 );
998
999 // Add custom color for custom theme mode
1000 if ($urlParamData['theme_mode'] === 'custom') {
1001 $urlParamData['custom_color'] = !empty($attributes['customColor']) ? $attributes['customColor'] : '#343434';
1002 }
1003
1004 return '?' . http_build_query($urlParamData);
1005 }
1006
1007 /**
1008 * Extract basic configuration from attributes
1009 *
1010 * @param array $attributes Block attributes
1011 * @return array Basic configuration
1012 */
1013 private static function extract_basic_config($attributes)
1014 {
1015 return [
1016 'block_id' => !empty($attributes['clientId']) ? $attributes['clientId'] : '',
1017 'custom_player' => !empty($attributes['customPlayer']) ? $attributes['customPlayer'] : 0,
1018 'insta_layout' => !empty($attributes['instaLayout']) ? ' ' . $attributes['instaLayout'] : ' insta-grid',
1019 'mode' => !empty($attributes['mode']) ? ' ep-google-photos-' . $attributes['mode'] : '',
1020 'embed_type' => !empty($attributes['cEmbedType']) ? ' ' . $attributes['cEmbedType'] : '',
1021 'url' => $attributes['url'] ?? '',
1022 ];
1023 }
1024
1025 /**
1026 * Build carousel configuration
1027 *
1028 * @param array $attributes Block attributes
1029 * @param string $client_id Client ID
1030 * @return array Carousel configuration
1031 */
1032 private static function build_carousel_config($attributes, $client_id)
1033 {
1034 $carousel_options = '';
1035 $carousel_id = '';
1036
1037 if (!empty($attributes['instaLayout']) && $attributes['instaLayout'] === 'insta-carousel') {
1038 $carousel_id = 'data-carouselid=' . esc_attr($client_id);
1039
1040 $options = [
1041 'layout' => $attributes['instaLayout'],
1042 'slideshow' => !empty($attributes['slidesShow']) ? $attributes['slidesShow'] : 5,
1043 'autoplay' => !empty($attributes['carouselAutoplay']) ? $attributes['carouselAutoplay'] : 0,
1044 'autoplayspeed' => !empty($attributes['autoplaySpeed']) ? $attributes['autoplaySpeed'] : 3000,
1045 'transitionspeed' => !empty($attributes['transitionSpeed']) ? $attributes['transitionSpeed'] : 1000,
1046 'loop' => !empty($attributes['carouselLoop']) ? $attributes['carouselLoop'] : 0,
1047 'arrows' => !empty($attributes['carouselArrows']) ? $attributes['carouselArrows'] : 0,
1048 'spacing' => !empty($attributes['carouselSpacing']) ? $attributes['carouselSpacing'] : 0
1049 ];
1050
1051 $carousel_options = 'data-carousel-options=' . htmlentities(json_encode($options), ENT_QUOTES);
1052 }
1053
1054 return [
1055 'carousel_id' => $carousel_id,
1056 'carousel_options' => $carousel_options,
1057 ];
1058 }
1059
1060 /**
1061 * Build player configuration
1062 *
1063 * @param array $attributes Block attributes
1064 * @param string $client_id Client ID
1065 * @return array Player configuration
1066 */
1067 private static function build_player_config($attributes, $client_id)
1068 {
1069 $custom_player = '';
1070 $player_options = '';
1071 $custom_player_enabled = !empty($attributes['customPlayer']) ? $attributes['customPlayer'] : 0;
1072
1073 if (!empty($custom_player_enabled)) {
1074 $is_self_hosted = Helper::check_media_format($attributes['url']);
1075 $custom_player = 'data-playerid=' . esc_attr($client_id);
1076
1077 $options = self::build_player_options($attributes, $is_self_hosted);
1078 // Wrap in quotes — htmlentities(..., ENT_QUOTES) already encoded `"` as
1079 // `&quot;`, but values like email-capture headlines can contain spaces,
1080 // which would terminate an unquoted attribute mid-JSON and break
1081 // initplyr.js's JSON.parse, silently disabling the custom player.
1082 $player_options = 'data-options="' . htmlentities(json_encode($options), ENT_QUOTES) . '"';
1083 }
1084
1085 return [
1086 'custom_player' => $custom_player,
1087 'player_options' => $player_options,
1088 ];
1089 }
1090
1091 /**
1092 * Build player options array
1093 *
1094 * @param array $attributes Block attributes
1095 * @param array $is_self_hosted Self-hosted media info
1096 * @return array Player options
1097 */
1098 private static function build_player_options($attributes, $is_self_hosted)
1099 {
1100 $options = [
1101 'rewind' => !empty($attributes['playerRewind']),
1102 'restart' => !empty($attributes['playerRestart']),
1103 'pip' => !empty($attributes['playerPip']),
1104 'poster_thumbnail' => $attributes['posterThumbnail'] ?? '',
1105 'player_color' => $attributes['playerColor'] ?? '',
1106 'player_preset' => $attributes['playerPreset'] ?? 'preset-default',
1107 'fast_forward' => !empty($attributes['playerFastForward']),
1108 'player_tooltip' => !empty($attributes['playerTooltip']),
1109 'hide_controls' => !empty($attributes['playerHideControls']),
1110 'download' => !empty($attributes['playerDownload']),
1111 ];
1112
1113 // Pro: advanced custom-player feature data (auto resume, timed CTA,
1114 // chapters, email capture, action lock, adaptive streaming, heatmap,
1115 // LMS tracking, privacy mode, end screen). Pro plugin populates the
1116 // array; with Pro disabled this is a no-op and the keys never reach
1117 // the data-options blob — frontend skips those features entirely.
1118 $options = apply_filters('embedpress/gutenberg/advanced_player_options', $options, $attributes);
1119
1120 // Add conditional options
1121 $conditional_options = [
1122 'fullscreen' => 'fullscreen',
1123 'starttime' => 'start',
1124 'endtime' => 'end',
1125 'relatedvideos' => 'rel',
1126 'muteVideo' => 'mute',
1127 'vstarttime' => 't',
1128 'vautoplay' => 'vautoplay',
1129 'vautopause' => 'autopause',
1130 'vdnt' => 'dnt',
1131 ];
1132
1133 foreach ($conditional_options as $attr_key => $option_key) {
1134 if (!empty($attributes[$attr_key])) {
1135 $options[$option_key] = $attributes[$attr_key];
1136 }
1137 }
1138
1139 // Add self-hosted options
1140 if (!empty($is_self_hosted['selhosted'])) {
1141 $options['self_hosted'] = $is_self_hosted['selhosted'];
1142 $options['hosted_format'] = $is_self_hosted['format'];
1143 }
1144
1145 return $options;
1146 }
1147
1148 /**
1149 * Country Restriction (Pro)
1150 *
1151 * Returns the HTML restricted-fallback when the viewer's country is
1152 * blocked, or `false` to render normally. Reads country from
1153 * Cloudflare's `CF-IPCountry` header, falling back to common Apache
1154 * GeoIP / proxy headers. If no country can be detected, allow.
1155 *
1156 * Adds `Vary: CF-IPCountry` so caches can vary per-country.
1157 */
1158 /**
1159 * Resolve the visitor's ISO country code. Order:
1160 * 1. CDN / reverse-proxy headers (zero-cost when present)
1161 * 2. `embedpress_visitor_country` filter (lets sites inject their own)
1162 * 3. Free HTTP GeoIP lookup at ipwho.is, cached per-IP for 12h
1163 *
1164 * Returns '' (fail-open) if every source is unavailable so a misconfigured
1165 * lookup never silently blocks legitimate visitors.
1166 */
1167 public static function resolve_visitor_country_public()
1168 {
1169 return self::resolve_visitor_country();
1170 }
1171
1172 private static function resolve_visitor_country()
1173 {
1174 $country = '';
1175 foreach (['HTTP_CF_IPCOUNTRY', 'GEOIP_COUNTRY_CODE', 'HTTP_X_COUNTRY_CODE'] as $key) {
1176 if (!empty($_SERVER[$key])) {
1177 $country = strtoupper(sanitize_text_field($_SERVER[$key]));
1178 break;
1179 }
1180 }
1181 $country = apply_filters('embedpress_visitor_country', $country);
1182 if ($country) return $country;
1183
1184 $ip = self::client_ip();
1185 if (!$ip) return '';
1186
1187 $cache_key = 'ep_geo_' . md5($ip);
1188 $cached = get_transient($cache_key);
1189 if ($cached !== false) {
1190 return $cached === '__none__' ? '' : $cached;
1191 }
1192
1193 $resp = wp_remote_get('https://ipwho.is/' . rawurlencode($ip) . '?fields=country_code,success', [
1194 'timeout' => 2,
1195 ]);
1196 if (is_wp_error($resp)) {
1197 // Negative-cache briefly so failed lookups don't repeat per-render.
1198 set_transient($cache_key, '__none__', 5 * MINUTE_IN_SECONDS);
1199 return '';
1200 }
1201 $body = json_decode(wp_remote_retrieve_body($resp), true);
1202 $code = (is_array($body) && !empty($body['country_code'])) ? strtoupper($body['country_code']) : '';
1203 set_transient($cache_key, $code ?: '__none__', $code ? 12 * HOUR_IN_SECONDS : 5 * MINUTE_IN_SECONDS);
1204 return $code;
1205 }
1206
1207 private static function client_ip()
1208 {
1209 foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'] as $key) {
1210 if (empty($_SERVER[$key])) continue;
1211 $candidate = trim(explode(',', $_SERVER[$key])[0]);
1212 if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
1213 return $candidate;
1214 }
1215 // Allow private IPs only as last-resort REMOTE_ADDR (dev environments).
1216 if ($key === 'REMOTE_ADDR' && filter_var($candidate, FILTER_VALIDATE_IP)) {
1217 return $candidate;
1218 }
1219 }
1220 return '';
1221 }
1222
1223
1224 /**
1225 * Rewrite self-hosted video / source URLs to the configured CDN URL
1226 * when an attachment has been offloaded. Best-effort regex pass over
1227 * the embed HTML.
1228 */
1229
1230 /**
1231 * Get embed content with dynamic rendering
1232 *
1233 * @param array $attributes Block attributes
1234 * @param string $content Block content
1235 * @return string Embed content
1236 */
1237 private static function get_embed_content($attributes, $content)
1238 {
1239 return apply_filters('embedpress_render_dynamic_content', $content, $attributes);
1240 }
1241
1242 /**
1243 * Build styling configuration
1244 *
1245 * @param array $attributes Block attributes
1246 * @param array $protection_data Protection data
1247 * @return array Styling configuration
1248 */
1249 private static function build_styling_config($attributes, $protection_data)
1250 {
1251 $client_id = $protection_data['client_id'];
1252
1253 // Content sharing classes
1254 $content_share_class = !empty($attributes['contentShare']) ? 'ep-content-share-enabled' : '';
1255 $share_position = $attributes['sharePosition'] ?? 'right';
1256 $share_position_class = !empty($attributes['contentShare']) ? 'ep-share-position-' . $share_position : '';
1257
1258 // Content protection classes
1259 $password_correct = $_COOKIE['password_correct_' . $client_id] ?? '';
1260 $hash_pass = hash('sha256', wp_salt(32) . md5($attributes['contentPassword'] ?? ''));
1261 $content_protection_class = 'ep-content-protection-enabled';
1262
1263 if (empty($attributes['lockContent']) || empty($attributes['contentPassword']) || $hash_pass === $password_correct) {
1264 $content_protection_class = 'ep-content-protection-disabled';
1265 }
1266
1267 // Alignment classes
1268 $alignment = self::get_alignment_class($attributes['align'] ?? '');
1269
1270 // Ad manager attributes
1271 $ads_attrs = self::build_ads_attributes($attributes, $client_id);
1272
1273 // Custom branding styles and HTML
1274 $custom_branding = self::build_custom_branding($attributes, $client_id);
1275
1276 // Media format classes
1277 $hosted_format = self::get_hosted_format($attributes);
1278 $yt_channel_class = (isset($attributes['url']) && Helper::is_youtube_channel_or_playlist($attributes['url'])) ? 'embedded-youtube-channel' : '';
1279
1280 $auto_pause = !empty($attributes['autoPause']) ? ' enabled-auto-pause' : '';
1281
1282 return [
1283 'content_share_class' => $content_share_class,
1284 'share_position_class' => $share_position_class,
1285 'content_protection_class' => $content_protection_class,
1286 'alignment' => $alignment,
1287 'ads_attrs' => $ads_attrs,
1288 'custom_branding' => $custom_branding,
1289 'hosted_format' => $hosted_format,
1290 'yt_channel_class' => $yt_channel_class,
1291 'auto_pause' => $auto_pause,
1292 'pass_hash_key' => isset($attributes['contentPassword']) ? md5($attributes['contentPassword']) : '',
1293 ];
1294 }
1295
1296 /**
1297 * Get alignment CSS class
1298 *
1299 * @param string $align Alignment value
1300 * @return string CSS class
1301 */
1302 private static function get_alignment_class($align)
1303 {
1304 return isset(self::$alignment_classes[$align])
1305 ? self::$alignment_classes[$align] . ' ep-clear'
1306 : 'aligncenter';
1307 }
1308
1309 /**
1310 * Build ad manager attributes
1311 *
1312 * @param array $attributes Block attributes
1313 * @param string $client_id Client ID
1314 * @return string Ad attributes
1315 */
1316 private static function build_ads_attributes($attributes, $client_id)
1317 {
1318 if (empty($attributes['adManager'])) {
1319 return '';
1320 }
1321
1322 $ad = base64_encode(json_encode($attributes));
1323 return "data-sponsored-id=$client_id data-sponsored-attrs=$ad class=sponsored-mask";
1324 }
1325
1326 /**
1327 * Get hosted media format
1328 *
1329 * @param array $attributes Block attributes
1330 * @return string Media format
1331 */
1332 private static function get_hosted_format($attributes)
1333 {
1334 if (empty($attributes['customPlayer'])) {
1335 return '';
1336 }
1337
1338 $self_hosted = Helper::check_media_format($attributes['url']);
1339 return $self_hosted['format'] ?? '';
1340 }
1341
1342 /**
1343 * Build custom branding configuration
1344 *
1345 * @param array $attributes Block attributes
1346 * @param string $client_id Client ID
1347 * @return array Custom branding configuration
1348 */
1349 private static function build_custom_branding($attributes, $client_id)
1350 {
1351 $custom_branding = [
1352 'html' => '',
1353 'styles' => ''
1354 ];
1355
1356 // Check if custom logo is enabled
1357 if (empty($attributes['customlogo'])) {
1358 return $custom_branding;
1359 }
1360
1361 $logo_url = $attributes['customlogo'];
1362 $logo_x = $attributes['logoX'] ?? 5;
1363 $logo_y = $attributes['logoY'] ?? 10;
1364 $logo_opacity = $attributes['logoOpacity'] ?? 1;
1365 $custom_logo_url = $attributes['customlogoUrl'] ?? '';
1366
1367 // Generate custom logo styles
1368 $custom_branding['styles'] = sprintf(
1369 '#ep-gutenberg-content-%s img.watermark {
1370 border: 0;
1371 position: absolute;
1372 bottom: %s%%;
1373 right: %s%%;
1374 max-width: 150px;
1375 max-height: 75px;
1376 -o-transition: opacity 0.5s ease-in-out;
1377 -moz-transition: opacity 0.5s ease-in-out;
1378 -webkit-transition: opacity 0.5s ease-in-out;
1379 transition: opacity 0.5s ease-in-out;
1380 z-index: 1;
1381 opacity: %s;
1382 }
1383 #ep-gutenberg-content-%s img.watermark:hover {
1384 opacity: 1;
1385 }',
1386 esc_attr($client_id),
1387 esc_attr($logo_y),
1388 esc_attr($logo_x),
1389 esc_attr($logo_opacity),
1390 esc_attr($client_id)
1391 );
1392
1393 // Generate custom logo HTML
1394 $logo_html = sprintf(
1395 '<img decoding="async" src="%s" class="watermark ep-custom-logo" width="auto" height="auto" alt="">',
1396 esc_url($logo_url)
1397 );
1398
1399 // Wrap with link if URL is provided
1400 if (!empty($custom_logo_url)) {
1401 $logo_html = sprintf(
1402 '<a href="%s" target="_blank">%s</a>',
1403 esc_url($custom_logo_url),
1404 $logo_html
1405 );
1406 }
1407
1408 $custom_branding['html'] = $logo_html;
1409
1410 return $custom_branding;
1411 }
1412
1413 /**
1414 * Generate the final HTML output
1415 *
1416 * @param array $attributes Block attributes
1417 * @param string $embed Embed content
1418 * @param array $config Basic configuration
1419 * @param array $carousel_config Carousel configuration
1420 * @param array $player_config Player configuration
1421 * @param array $styling Styling configuration
1422 * @param array $protection_data Protection data
1423 * @param bool $should_display_content Whether content should be displayed
1424 * @return string Final HTML output
1425 */
1426 private static function generate_final_html($attributes, $embed, $config, $carousel_config, $player_config, $styling, $protection_data, $should_display_content)
1427 {
1428 ob_start();
1429
1430 // Extract variables for template
1431 $url = $config['url'];
1432 $block_id = $config['block_id'];
1433 $client_id = $protection_data['client_id'];
1434 $content_id = $protection_data['content_id'];
1435 $content_share = $protection_data['content_share'];
1436
1437 // Build wrapper classes
1438 $wrapper_classes = self::build_wrapper_classes($styling, $config);
1439 $embed_wrapper_classes = self::build_embed_wrapper_classes($attributes);
1440 $content_wrapper_classes = self::build_content_wrapper_classes($attributes, $config, $styling);
1441
1442 // Cap the content wrapper width to the block's width control so the
1443 // outer wrapper doesn't render wider than the embed itself (the inner
1444 // .plyr already gets width/height inline).
1445 $content_wrapper_style = '';
1446 if (!empty($attributes['width'])) {
1447 $unit = ($attributes['unitoption'] ?? 'px') === '%' ? '%' : 'px';
1448 $content_wrapper_style = 'max-width:' . intval($attributes['width']) . $unit;
1449 // max-width alone leaves the wrapper flush-left; mirror the block's
1450 // align control so centered/right alignments still take effect.
1451 $align = $attributes['align'] ?? '';
1452 if ($align === 'center') {
1453 $content_wrapper_style .= ';margin-left:auto;margin-right:auto';
1454 } elseif ($align === 'right') {
1455 $content_wrapper_style .= ';margin-left:auto;margin-right:0';
1456 }
1457 }
1458
1459 // Pro: CDN Offloading and Advanced Privacy Mode. Filters are
1460 // no-ops without Pro; the Pro callbacks gate on the toggle.
1461 $embed = apply_filters('embedpress/gutenberg/cdn_rewrite_html', $embed, $attributes);
1462 $embed = apply_filters('embedpress/gutenberg/privacy_mode_html', $embed, $attributes);
1463 if (!empty($attributes['customPlayer']) && !empty($attributes['playerPrivacyMode'])) {
1464 // Mirror the Pro gate so the click-to-load overlay class
1465 // attaches to the wrapper. Without Pro the overlay JS never
1466 // runs, but the unused class is harmless.
1467 $content_wrapper_classes .= ' ep-privacy-pending';
1468 }
1469
1470 ?>
1471 <?php if (!empty($styling['custom_branding']['styles'])): ?>
1472 <style>
1473 <?php echo $styling['custom_branding']['styles']; ?>
1474 </style>
1475 <?php endif; ?>
1476
1477 <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) ); ?> ">
1478 <div class="wp-block-embed__wrapper <?php echo esc_attr($embed_wrapper_classes); ?>">
1479 <div id="ep-gutenberg-content-<?php echo esc_attr($client_id) ?>" class="ep-gutenberg-content<?php echo esc_attr($styling['auto_pause']); ?>">
1480 <div <?php echo esc_attr($styling['ads_attrs']); ?>>
1481 <div class="ep-embed-content-wraper <?php echo esc_attr($content_wrapper_classes); ?>"
1482 <?php if (!empty($content_wrapper_style)): ?>style="<?php echo esc_attr($content_wrapper_style); ?>"<?php endif; ?>
1483 <?php echo esc_attr($player_config['custom_player']); ?>
1484 <?php echo $player_config['player_options']; // already a complete escaped attribute (data-options="..."); esc_attr would double-encode the outer quotes ?>
1485 <?php echo esc_attr($carousel_config['carousel_id']); ?>
1486 <?php echo esc_attr($carousel_config['carousel_options']); ?>>
1487
1488 <?php
1489 self::render_embed_content($embed, $content_share, $content_id, $attributes, $should_display_content, $protection_data, $styling);
1490 ?>
1491 </div>
1492
1493 <?php self::render_ad_template($attributes, $embed, $client_id); ?>
1494 </div>
1495 </div>
1496 </div>
1497 </div>
1498 <?php
1499
1500 return ob_get_clean();
1501 }
1502
1503 /**
1504 * Build wrapper CSS classes
1505 *
1506 * @param array $styling Styling configuration
1507 * @param array $config Basic configuration
1508 * @return string CSS classes
1509 */
1510 private static function build_wrapper_classes($styling, $config)
1511 {
1512 return trim(sprintf(
1513 '%s %s %s %s%s',
1514 $styling['alignment'],
1515 $styling['content_share_class'],
1516 $styling['share_position_class'],
1517 $styling['content_protection_class'],
1518 $config['embed_type']
1519 ));
1520 }
1521
1522 /**
1523 * Build embed wrapper CSS classes
1524 *
1525 * @param array $attributes Block attributes
1526 * @return string CSS classes
1527 */
1528 private static function build_embed_wrapper_classes($attributes)
1529 {
1530 $classes = [];
1531
1532 if (!empty($attributes['contentShare'])) {
1533 $share_position = $attributes['sharePosition'] ?? 'right';
1534 $classes[] = 'position-' . $share_position . '-wraper';
1535 }
1536
1537 if (($attributes['videosize'] ?? '') === 'responsive') {
1538 $classes[] = 'ep-video-responsive';
1539 }
1540
1541 return implode(' ', $classes);
1542 }
1543
1544 /**
1545 * Build content wrapper CSS classes
1546 *
1547 * @param array $attributes Block attributes
1548 * @param array $config Basic configuration
1549 * @param array $styling Styling configuration
1550 * @return string CSS classes
1551 */
1552 private static function build_content_wrapper_classes($attributes, $config, $styling)
1553 {
1554 return trim(sprintf(
1555 '%s%s%s %s %s',
1556 $attributes['playerPreset'] ?? '',
1557 $config['insta_layout'],
1558 $config['mode'],
1559 $styling['hosted_format'],
1560 $styling['yt_channel_class']
1561 ));
1562 }
1563
1564 /**
1565 * Render embed content with protection logic
1566 *
1567 * @param string $embed Embed content
1568 * @param string $content_share Content share setting
1569 * @param string $content_id Content ID
1570 * @param array $attributes Block attributes
1571 * @param bool $should_display_content Whether content should be displayed
1572 * @param array $protection_data Protection data
1573 * @param array $styling Styling configuration
1574 */
1575 private static function render_embed_content($embed, $content_share, $content_id, $attributes, $should_display_content, $protection_data, $styling)
1576 {
1577 if ($should_display_content) {
1578 self::render_displayable_content($embed, $content_share, $content_id, $attributes, $styling);
1579 } else {
1580 self::render_protected_content($embed, $content_share, $content_id, $attributes, $protection_data, $styling);
1581 }
1582 }
1583
1584 /**
1585 * Render displayable content
1586 *
1587 * @param string $embed Embed content
1588 * @param string $content_share Content share setting
1589 * @param string $content_id Content ID
1590 * @param array $attributes Block attributes
1591 * @param array $styling Styling configuration (optional)
1592 */
1593 private static function render_displayable_content($embed, $content_share, $content_id, $attributes, $styling = [])
1594 {
1595 // Add custom branding if available
1596 if (!empty($styling['custom_branding']['html'])) {
1597 if (is_array($embed)) {
1598 $embed['html'] .= $styling['custom_branding']['html'];
1599 } else {
1600 $embed .= $styling['custom_branding']['html'];
1601 }
1602 }
1603
1604 if (!empty($content_share)) {
1605 $embed .= Helper::embed_content_share($content_id, $attributes);
1606 }
1607
1608 if (is_array($embed)) {
1609 echo $embed['html'];
1610 } else {
1611 echo $embed;
1612 }
1613 }
1614
1615 /**
1616 * Render protected content
1617 *
1618 * @param string $embed Embed content
1619 * @param string $content_share Content share setting
1620 * @param string $content_id Content ID
1621 * @param array $attributes Block attributes
1622 * @param array $protection_data Protection data
1623 * @param array $styling Styling configuration
1624 */
1625 private static function render_protected_content($embed, $content_share, $content_id, $attributes, $protection_data, $styling)
1626 {
1627 // Add custom branding if available
1628 if (!empty($styling['custom_branding']['html'])) {
1629 if (is_array($embed)) {
1630 $embed['html'] .= $styling['custom_branding']['html'];
1631 } else {
1632 $embed .= $styling['custom_branding']['html'];
1633 }
1634 }
1635
1636 if (!empty($content_share)) {
1637 $embed .= Helper::embed_content_share($content_id, $attributes);
1638 }
1639
1640 if ($protection_data['protection_type'] === 'password') {
1641 echo '<div id="ep-gutenberg-content-' . esc_attr($protection_data['client_id']) . '" class="ep-gutenberg-content">';
1642 do_action('embedpress/display_password_form', $protection_data['client_id'], $embed, $styling['pass_hash_key'], $attributes);
1643 echo '</div>';
1644 } else {
1645 do_action('embedpress/content_protection_content', $protection_data['client_id'], $protection_data['protection_message'], $protection_data['user_role']);
1646 }
1647 }
1648
1649 /**
1650 * Render ad template if enabled
1651 *
1652 * @param array $attributes Block attributes
1653 * @param string $embed Embed content
1654 * @param string $client_id Client ID
1655 */
1656 private static function render_ad_template($attributes, $embed, $client_id)
1657 {
1658 if (!empty($attributes['adManager'])) {
1659 $embed = apply_filters('embedpress/generate_ad_template', $embed, $client_id, $attributes, 'gutenberg');
1660 }
1661 }
1662
1663 /**
1664 * YouTube block legacy render method
1665 *
1666 * @param array $attributes Block attributes
1667 * @param string $content Block content
1668 * @param object $block Block object (unused but kept for compatibility)
1669 * @return string Rendered HTML content
1670 */
1671 public static function render_youtube_block($attributes, $content = '', $block = null)
1672 {
1673
1674 if (!empty($content)) {
1675 return $content;
1676 }
1677 // Extract basic attributes for YouTube block
1678 $iframe_src = $attributes['iframeSrc'] ?? '';
1679 $align = $attributes['align'] ?? 'center';
1680
1681 // If no iframe source is provided, return empty
1682 if (empty($iframe_src)) {
1683 return '';
1684 }
1685
1686 // Validate YouTube URL
1687 if (!self::is_youtube_url($iframe_src)) {
1688 return '';
1689 }
1690
1691 // Apply YouTube parameters filter
1692 $youtube_params = apply_filters('embedpress_gutenberg_youtube_params', []);
1693 $processed_iframe_url = $iframe_src;
1694
1695 foreach ($youtube_params as $param => $value) {
1696 $processed_iframe_url = add_query_arg($param, $value, $processed_iframe_url);
1697 }
1698
1699 // Build alignment class
1700 $align_class = 'align' . $align;
1701
1702 // Extract width/height from attributes
1703 $width = isset($attributes['width']) ? intval($attributes['width']) : 640;
1704 $height = isset($attributes['height']) ? intval($attributes['height']) : 360;
1705
1706 // Generate YouTube block HTML
1707 ob_start();
1708 ?>
1709 <div class="ose-youtube responsive wp-block-embed-youtube ose-youtube-single-video <?php echo esc_attr($align_class); ?>" style="max-width: 100%;">
1710 <iframe src="<?php echo esc_url($processed_iframe_url); ?>"
1711 allowtransparency="true"
1712 allowfullscreen="true"
1713 frameborder="0"
1714 style="max-width: 100%;"
1715 width="<?php echo esc_attr($width); ?>" height="<?php echo esc_attr($height); ?>">
1716 </iframe>
1717 </div>
1718 <?php
1719 return ob_get_clean();
1720 }
1721
1722 /**
1723 * Validate if URL is a YouTube URL
1724 *
1725 * @param string $url URL to validate
1726 * @return bool True if valid YouTube URL, false otherwise
1727 */
1728 private static function is_youtube_url($url)
1729 {
1730 $pattern = '/^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?(.*&)?v=|(embed|v)\/))|youtu.be\/([a-zA-Z0-9_-]{11})/';
1731 return preg_match($pattern, $url);
1732 }
1733
1734 /**
1735 * Wistia block legacy render method
1736 *
1737 * @param array $attributes Block attributes
1738 * @param string $content Block content
1739 * @param object $block Block object (unused but kept for compatibility)
1740 * @return string Rendered HTML content
1741 */
1742 public static function render_wistia_block($attributes, $content = '', $block = null)
1743 {
1744 if (!empty($content)) {
1745 return $content;
1746 }
1747
1748 // Extract basic attributes for Wistia block
1749 $url = $attributes['url'] ?? '';
1750 $iframe_src = $attributes['iframeSrc'] ?? '';
1751 $align = $attributes['align'] ?? 'center';
1752
1753 // If no URL is provided, return empty
1754 if (empty($url)) {
1755 return '';
1756 }
1757
1758 // Extract Wistia media ID from URL
1759 $wistia_id = self::extract_wistia_id($url);
1760 if (empty($wistia_id)) {
1761 return '';
1762 }
1763
1764 // Build alignment class
1765 $align_class = 'align' . $align;
1766
1767 // Generate Wistia block HTML
1768 ob_start();
1769 ?>
1770 <div class="ose-wistia wp-block-embed-youtube <?php echo esc_attr($align_class); ?>" id="wistia_<?php echo esc_attr($wistia_id); ?>">
1771 <iframe src="<?php echo esc_url($iframe_src); ?>"
1772 allowtransparency="true"
1773 frameborder="0"
1774 class="wistia_embed"
1775 name="wistia_embed"
1776 width="600"
1777 height="330">
1778 </iframe>
1779 <?php do_action('embedpress_gutenberg_wistia_block_after_embed', $attributes); ?>
1780 </div>
1781 <?php
1782 return ob_get_clean();
1783 }
1784
1785 /**
1786 * Extract Wistia media ID from URL
1787 *
1788 * @param string $url Wistia URL
1789 * @return string|false Wistia media ID or false if not found
1790 */
1791 private static function extract_wistia_id($url)
1792 {
1793 preg_match('~medias/(.*)~i', esc_url($url), $matches);
1794 return isset($matches[1]) ? $matches[1] : false;
1795 }
1796
1797 /**
1798 * Calendar block render method
1799 *
1800 * @param array $attributes Block attributes
1801 * @param string $content Block content
1802 * @param object $block Block object (unused but kept for compatibility)
1803 * @return string Rendered HTML content
1804 */
1805 public static function render_embedpress_calendar($attributes, $content = '', $block = null)
1806 {
1807 if (!empty($content)) {
1808 return $content;
1809 }
1810
1811 // Extract basic attributes for Calendar block
1812 $url = $attributes['url'] ?? '';
1813 $width = $attributes['width'] ?? '600';
1814 $height = $attributes['height'] ?? '600';
1815 $powered_by = $attributes['powered_by'] ?? false;
1816 $is_public = $attributes['is_public'] ?? true;
1817 $align = $attributes['align'] ?? 'center';
1818
1819 $align_class = 'align' . $align;
1820
1821 // Private branch: same path Elementor uses — fire the action that Pro
1822 // ([Embedpress\Pro\Filters\Calendar]) hooks to emit the epgc-calendar
1823 // shortcode markup. Frontend JS then hydrates it via the AJAX endpoint
1824 // (which has its own transient cache via epgc_cache_time).
1825 if (!$is_public) {
1826 if (!apply_filters('embedpress/is_allow_rander', false)) {
1827 return '';
1828 }
1829 // Enqueue the FullCalendar bundle. The Elementor widget gets these
1830 // automatically via get_script_depends(); the Gutenberg block needs
1831 // an explicit call at render time since block.json has no viewScript.
1832 if (class_exists('Embedpress_Google_Helper')) {
1833 \Embedpress_Google_Helper::enqueue_scripts(); // ensures registration
1834 }
1835 wp_enqueue_style('fullcalendar');
1836 wp_enqueue_style('fullcalendar_daygrid');
1837 wp_enqueue_style('fullcalendar_timegrid');
1838 wp_enqueue_style('fullcalendar_list');
1839 wp_enqueue_style('epgc');
1840 wp_enqueue_style('tippy_light');
1841 wp_enqueue_script('fullcalendar_moment_timezone');
1842 wp_enqueue_script('fullcalendar_daygrid');
1843 wp_enqueue_script('fullcalendar_timegrid');
1844 wp_enqueue_script('fullcalendar_list');
1845 wp_enqueue_script('fullcalendar_locales');
1846 wp_enqueue_script('tippy');
1847 wp_enqueue_script('epgc');
1848 // Localize the nonce + ajax URL onto the epgc handle. LocalizationManager
1849 // handles this on Elementor pages; on Gutenberg frontends we have to.
1850 if (class_exists('\\EmbedPress\\Core\\LocalizationManager')) {
1851 \EmbedPress\Core\LocalizationManager::setup_elementor_localization();
1852 }
1853
1854 ob_start();
1855 ?>
1856 <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;">
1857 <?php do_action('embedpress_google_helper_shortcode', 10); ?>
1858 <?php if ($powered_by) : ?>
1859 <p class="embedpress-el-powered"><?php echo esc_html__('Powered By EmbedPress', 'embedpress'); ?></p>
1860 <?php endif; ?>
1861 </figure>
1862 <?php
1863 return ob_get_clean();
1864 }
1865
1866 // Public branch: iframe to calendar.google.com's embed view.
1867 if (empty($url)) {
1868 return '';
1869 }
1870 if (!self::is_google_calendar_url($url)) {
1871 return '<p class="embedpress-el-powered">' . esc_html__('Invalid Calendar Link', 'embedpress') . '</p>';
1872 }
1873
1874 $sanitized_url = esc_url($url);
1875
1876 ob_start();
1877 ?>
1878 <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;">
1879 <iframe src="<?php echo esc_url($sanitized_url); ?>"
1880 width="<?php echo esc_attr($width); ?>"
1881 height="<?php echo esc_attr($height); ?>"
1882 frameborder="0"
1883 scrolling="no"
1884 title="<?php echo esc_attr(self::get_iframe_title_from_url($url)); ?>">
1885 </iframe>
1886
1887 <?php if ($powered_by) : ?>
1888 <p class="embedpress-el-powered"><?php echo esc_html__('Powered By EmbedPress', 'embedpress'); ?></p>
1889 <?php endif; ?>
1890 </figure>
1891 <?php
1892 return ob_get_clean();
1893 }
1894
1895 /**
1896 * Validate if URL is a Google Calendar URL
1897 *
1898 * @param string $url URL to validate
1899 * @return bool True if valid Google Calendar URL, false otherwise
1900 */
1901 private static function is_google_calendar_url($url)
1902 {
1903 $pattern = '/^https:\/\/calendar\.google\.com\/calendar\/(?:u\/\d+\/)?embed\?.*/';
1904 return preg_match($pattern, $url);
1905 }
1906
1907 /**
1908 * Get iframe title from URL
1909 *
1910 * @param string $url URL to derive title from
1911 * @return string Derived title
1912 */
1913 private static function get_iframe_title_from_url($url)
1914 {
1915 if (empty($url)) {
1916 return '';
1917 }
1918
1919 // Try getting title from WordPress attachment if it's a local file
1920 $file_title = Helper::get_file_title($url);
1921 if (!empty($file_title)) {
1922 return $file_title;
1923 }
1924
1925 // Try to get filename from URL
1926 $path = parse_url($url, PHP_URL_PATH);
1927 if ($path) {
1928 $filename = basename($path);
1929 // Remove extension
1930 $filename = preg_replace('/\.[^.]+$/', '', $filename);
1931 // Decode URL encoding
1932 $filename = urldecode($filename);
1933 // Replace hyphens/underscores with spaces
1934 $filename = str_replace(['-', '_'], ' ', $filename);
1935
1936 if (!empty($filename)) {
1937 return ucfirst($filename);
1938 }
1939 }
1940
1941 // Fallback to domain name
1942 $host = parse_url($url, PHP_URL_HOST);
1943 if ($host) {
1944 return $host;
1945 }
1946
1947 return $url;
1948 }
1949
1950 /**
1951 * Render PDF Gallery block
1952 */
1953 public static function render_pdf_gallery($attributes, $content = '', $block = null)
1954 {
1955 // If save.js produced valid content, return it (frontend JS will enhance it)
1956 if (!empty($content)) {
1957 return $content;
1958 }
1959
1960 $items = isset($attributes['pdfItems']) ? $attributes['pdfItems'] : [];
1961 if (empty($items)) {
1962 return '';
1963 }
1964
1965 $layout = isset($attributes['layout']) ? esc_attr($attributes['layout']) : 'grid';
1966 $bookshelf_style = isset($attributes['bookshelfStyle']) ? esc_attr($attributes['bookshelfStyle']) : 'dark-wood';
1967
1968 // Pro gate: bookshelf requires Pro
1969 if (($layout === 'bookshelf' || $layout === 'carousel') && !defined('EMBEDPRESS_SL_ITEM_SLUG')) {
1970 $layout = 'grid';
1971 }
1972
1973 $columns = isset($attributes['columns']) ? intval($attributes['columns']) : 3;
1974 $columns_tablet = isset($attributes['columnsTablet']) ? intval($attributes['columnsTablet']) : 2;
1975 $columns_mobile = isset($attributes['columnsMobile']) ? intval($attributes['columnsMobile']) : 1;
1976 $gap = isset($attributes['gap']) ? intval($attributes['gap']) : 20;
1977 $border_radius = isset($attributes['thumbnailBorderRadius']) ? intval($attributes['thumbnailBorderRadius']) : 8;
1978 $aspect_ratio = isset($attributes['thumbnailAspectRatio']) ? esc_attr($attributes['thumbnailAspectRatio']) : '4:3';
1979 $viewer_style = isset($attributes['viewerStyle']) ? esc_attr($attributes['viewerStyle']) : 'modern';
1980 $client_id = isset($attributes['clientId']) ? esc_attr($attributes['clientId']) : '';
1981 $gallery_id = 'ep-gallery-' . substr(md5(serialize($items)), 0, 8);
1982
1983 // Build viewer params
1984 $viewer_params = self::generate_gallery_viewer_params($attributes);
1985
1986 // Carousel options
1987 $carousel_options = '';
1988 if ($layout === 'carousel' || $layout === 'bookshelf') {
1989 $carousel_options = wp_json_encode([
1990 'autoplay' => !empty($attributes['carouselAutoplay']),
1991 'autoplaySpeed' => isset($attributes['carouselAutoplaySpeed']) ? intval($attributes['carouselAutoplaySpeed']) : 3000,
1992 'loop' => isset($attributes['carouselLoop']) ? (bool)$attributes['carouselLoop'] : true,
1993 'arrows' => isset($attributes['carouselArrows']) ? (bool)$attributes['carouselArrows'] : true,
1994 'dots' => !empty($attributes['carouselDots']),
1995 'slidesPerView' => isset($attributes['slidesPerView']) ? intval($attributes['slidesPerView']) : 3,
1996 ]);
1997 }
1998
1999 $style = sprintf(
2000 '--ep-gallery-columns-desktop:%d;--ep-gallery-columns-tablet:%d;--ep-gallery-columns-mobile:%d;--ep-gallery-gap:%dpx;--ep-gallery-radius:%dpx;',
2001 $columns, $columns_tablet, $columns_mobile, $gap, $border_radius
2002 );
2003
2004 ob_start();
2005 ?>
2006 <div class="ep-pdf-gallery"
2007 data-layout="<?php echo $layout; ?>"
2008 data-shelf-style="<?php echo esc_attr($bookshelf_style); ?>"
2009 data-columns="<?php echo $columns; ?>"
2010 data-columns-tablet="<?php echo $columns_tablet; ?>"
2011 data-columns-mobile="<?php echo $columns_mobile; ?>"
2012 data-gap="<?php echo $gap; ?>"
2013 data-border-radius="<?php echo $border_radius; ?>"
2014 data-viewer-style="<?php echo $viewer_style; ?>"
2015 data-viewer-params="<?php echo esc_attr($viewer_params); ?>"
2016 data-gallery-id="<?php echo esc_attr($gallery_id); ?>"
2017 <?php if ($carousel_options): ?>data-carousel-options="<?php echo esc_attr($carousel_options); ?>"<?php endif; ?>
2018 style="<?php echo esc_attr($style); ?>">
2019
2020 <?php if ($layout === 'carousel' || $layout === 'bookshelf'): ?>
2021 <div class="ep-pdf-gallery__carousel">
2022 <div class="ep-pdf-gallery__carousel-track">
2023 <?php else: ?>
2024 <div class="ep-pdf-gallery__grid">
2025 <?php endif; ?>
2026
2027 <?php foreach ($items as $index => $item):
2028 $pdf_url = isset($item['url']) ? esc_url($item['url']) : '';
2029 $pdf_name = isset($item['fileName']) ? esc_attr($item['fileName']) : '';
2030 $custom_thumb = isset($item['customThumbnailUrl']) ? esc_url($item['customThumbnailUrl']) : '';
2031 $auto_thumb = isset($item['autoThumbnailUrl']) ? esc_url($item['autoThumbnailUrl']) : '';
2032 $thumb_url = $custom_thumb ?: $auto_thumb;
2033 ?>
2034 <div class="ep-pdf-gallery__item"
2035 data-pdf-url="<?php echo $pdf_url; ?>"
2036 data-pdf-index="<?php echo intval($index); ?>"
2037 data-pdf-name="<?php echo $pdf_name; ?>">
2038 <div class="ep-pdf-gallery__thumbnail-wrap" data-ratio="<?php echo $aspect_ratio; ?>">
2039 <?php if ($thumb_url): ?>
2040 <img src="<?php echo $thumb_url; ?>" alt="<?php echo $pdf_name; ?>" />
2041 <?php else: ?>
2042 <canvas class="ep-pdf-gallery__canvas" data-pdf-src="<?php echo $pdf_url; ?>" data-loading="true"></canvas>
2043 <?php endif; ?>
2044 <div class="ep-pdf-gallery__overlay">
2045 <svg class="ep-pdf-gallery__view-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2046 <path d="M8 5v14l11-7z"/>
2047 </svg>
2048 </div>
2049 </div>
2050 <div class="ep-pdf-gallery__book-title"><?php echo esc_html($pdf_name); ?></div>
2051 </div>
2052 <?php endforeach; ?>
2053
2054 <?php if ($layout === 'carousel' || $layout === 'bookshelf'): ?>
2055 </div>
2056 <button class="ep-pdf-gallery__carousel-prev" aria-label="Previous">
2057 <svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
2058 </button>
2059 <button class="ep-pdf-gallery__carousel-next" aria-label="Next">
2060 <svg viewBox="0 0 24 24"><path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/></svg>
2061 </button>
2062 <div class="ep-pdf-gallery__carousel-dots"></div>
2063 </div>
2064 <?php else: ?>
2065 </div>
2066 <?php endif; ?>
2067 </div>
2068 <?php
2069 return ob_get_clean();
2070 }
2071
2072 /**
2073 * Generate base64-encoded viewer params for PDF gallery
2074 */
2075 private static function generate_gallery_viewer_params($attributes)
2076 {
2077 $theme_mode = isset($attributes['themeMode']) ? $attributes['themeMode'] : 'default';
2078
2079 $params = [
2080 'themeMode' => esc_attr($theme_mode),
2081 'toolbar' => !empty($attributes['toolbar']) ? 'true' : 'false',
2082 'position' => isset($attributes['position']) ? esc_attr($attributes['position']) : 'top',
2083 'flipbook_toolbar_position' => isset($attributes['flipbook_toolbar_position']) ? esc_attr($attributes['flipbook_toolbar_position']) : 'bottom',
2084 'flipbook_rtl' => defined('EMBEDPRESS_SL_ITEM_SLUG') && !empty($attributes['flipbookPageFlipRTL']) ? 'true' : 'false',
2085 'flipbook_highlight_links' => !empty($attributes['flipbookHighlightLinks']) ? 'true' : 'false',
2086 'flipbook_highlight_color' => isset($attributes['flipbookHighlightColor']) ? esc_attr($attributes['flipbookHighlightColor']) : '',
2087 'presentation' => !empty($attributes['presentation']) ? 'true' : 'false',
2088 'download' => !empty($attributes['download']) ? 'true' : 'false',
2089 'copy_text' => !empty($attributes['copy_text']) ? 'true' : 'false',
2090 'add_text' => !empty($attributes['add_text']) ? 'true' : 'false',
2091 'draw' => !empty($attributes['draw']) ? 'true' : 'false',
2092 'doc_rotation' => !empty($attributes['doc_rotation']) ? 'true' : 'false',
2093 'doc_details' => !empty($attributes['doc_details']) ? 'true' : 'false',
2094 'add_image' => !empty($attributes['add_image']) ? 'true' : 'false',
2095 'zoom_in' => !empty($attributes['zoomIn']) ? 'true' : 'false',
2096 'zoom_out' => !empty($attributes['zoomOut']) ? 'true' : 'false',
2097 'fit_view' => !empty($attributes['fitView']) ? 'true' : 'false',
2098 'bookmark' => !empty($attributes['bookmark']) ? 'true' : 'false',
2099 'watermark_text' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkText']) ? esc_attr($attributes['watermarkText']) : '',
2100 'watermark_font_size' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkFontSize']) ? intval($attributes['watermarkFontSize']) : 48,
2101 'watermark_color' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkColor']) ? esc_attr($attributes['watermarkColor']) : '#000000',
2102 'watermark_opacity' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkOpacity']) ? intval($attributes['watermarkOpacity']) : 15,
2103 'watermark_style' => defined('EMBEDPRESS_SL_ITEM_SLUG') && isset($attributes['watermarkStyle']) ? esc_attr($attributes['watermarkStyle']) : 'center',
2104 ];
2105
2106 if ($theme_mode === 'custom') {
2107 $params['customColor'] = isset($attributes['customColor']) ? esc_attr($attributes['customColor']) : '#403A81';
2108 }
2109
2110 $query_string = http_build_query($params);
2111 if (function_exists('mb_convert_encoding')) {
2112 $query_string = mb_convert_encoding($query_string, 'UTF-8');
2113 }
2114
2115 return base64_encode($query_string);
2116 }
2117
2118 /**
2119 * Render the Google Reviews block. Maps the JS camelCase attributes to the
2120 * shared GoogleReviewsRenderer args (snake_case) so block + Elementor +
2121 * shortcode all emit identical markup.
2122 */
2123 public static function render_google_reviews($attributes, $content = '', $block = null)
2124 {
2125 \EmbedPress\Includes\Classes\GoogleReviewsRenderer::enqueue_assets();
2126 return \EmbedPress\Includes\Classes\GoogleReviewsRenderer::render([
2127 'place_id' => isset($attributes['placeId']) ? sanitize_text_field($attributes['placeId']) : '',
2128 'place_name' => isset($attributes['placeName']) ? sanitize_text_field($attributes['placeName']) : '',
2129 'limit' => isset($attributes['limit']) ? (int) $attributes['limit'] : 5,
2130 'min_rating' => isset($attributes['minRating']) ? (int) $attributes['minRating'] : 0,
2131 'layout' => isset($attributes['layout']) ? sanitize_key($attributes['layout']) : 'list',
2132 'show_photo' => isset($attributes['showPhoto']) ? (bool) $attributes['showPhoto'] : true,
2133 'show_stars' => isset($attributes['showStars']) ? (bool) $attributes['showStars'] : true,
2134 'show_date' => isset($attributes['showDate']) ? (bool) $attributes['showDate'] : true,
2135 'show_link' => isset($attributes['showLink']) ? (bool) $attributes['showLink'] : false,
2136 'show_images' => isset($attributes['showImages']) ? (bool) $attributes['showImages'] : true,
2137 'columns' => isset($attributes['columns']) ? (int) $attributes['columns'] : 3,
2138 'max_width' => isset($attributes['maxWidth']) ? (int) $attributes['maxWidth'] : 0,
2139 'gap' => isset($attributes['gap']) ? (int) $attributes['gap'] : 32,
2140 'show_arrows' => isset($attributes['showArrows']) ? (bool) $attributes['showArrows'] : true,
2141 'show_dots' => isset($attributes['showDots']) ? (bool) $attributes['showDots'] : true,
2142 'carousel_loop' => isset($attributes['carouselLoop']) ? (bool) $attributes['carouselLoop'] : true,
2143 'autoplay_speed' => isset($attributes['autoplaySpeed']) ? (float) $attributes['autoplaySpeed'] : 5,
2144 'show_summary' => isset($attributes['showSummary']) ? (bool) $attributes['showSummary'] : true,
2145 'show_summary_name' => isset($attributes['showSummaryName']) ? (bool) $attributes['showSummaryName'] : true,
2146 'show_summary_rating' => isset($attributes['showSummaryRating']) ? (bool) $attributes['showSummaryRating'] : true,
2147 'show_summary_stars' => isset($attributes['showSummaryStars']) ? (bool) $attributes['showSummaryStars'] : true,
2148 'show_summary_count' => isset($attributes['showSummaryCount']) ? (bool) $attributes['showSummaryCount'] : true,
2149 'show_write_review' => isset($attributes['showWriteReview']) ? (bool) $attributes['showWriteReview'] : true,
2150 'summary_align' => isset($attributes['summaryAlign']) ? sanitize_key($attributes['summaryAlign']) : 'left',
2151 // Pro-extended attributes. Free's renderer ignores them; Pro's
2152 // render filters read them off the args. Sanitized here so the
2153 // contract holds regardless of which plugin consumes them.
2154 'sort' => isset($attributes['sort']) ? sanitize_key($attributes['sort']) : 'newest',
2155 'keyword' => isset($attributes['keyword']) ? sanitize_text_field($attributes['keyword']) : '',
2156 'hide_empty' => isset($attributes['hideEmpty']) ? (bool) $attributes['hideEmpty'] : false,
2157 'fetch_all' => isset($attributes['fetchAll']) ? (bool) $attributes['fetchAll'] : false,
2158 'theme' => isset($attributes['theme']) ? sanitize_key($attributes['theme']) : 'light',
2159 'accent_color' => isset($attributes['accentColor']) ? sanitize_hex_color($attributes['accentColor']) : '',
2160 'autoplay' => isset($attributes['autoplay']) ? (bool) $attributes['autoplay'] : false,
2161 'schema' => isset($attributes['schema']) ? (bool) $attributes['schema'] : false,
2162 'places' => isset($attributes['places']) && is_array($attributes['places']) ? array_map('sanitize_text_field', $attributes['places']) : [],
2163 'cache_ttl' => isset($attributes['cacheTtl']) ? (int) $attributes['cacheTtl'] : 0,
2164 'load_more' => isset($attributes['loadMore']) ? (bool) $attributes['loadMore'] : false,
2165 // No separate per-page control anymore: 0 means "use the count
2166 // control (limit) as the page size" (see Pro page_size()).
2167 'per_page' => isset($attributes['perPage']) ? (int) $attributes['perPage'] : 0,
2168 // EDITOR PREVIEW flag. The Gutenberg block previews via the
2169 // /wp/v2/block-renderer REST route (REST_REQUEST + context=edit). In
2170 // that case the renderer caps sliding layouts (carousel/marquee/
2171 // spotlight) to a handful of cards instead of rendering up to 200 —
2172 // a 200-card preview is ~1.3MB per request, and ServerSideRender
2173 // re-fires on every attribute change, so rapid layout switches piled
2174 // up huge in-flight requests and FROZE the editor. The frontend is
2175 // unaffected (full set still renders there).
2176 'is_editor_preview' => self::is_block_editor_preview_request(),
2177 ]);
2178 }
2179
2180 /**
2181 * True when the current render is a Gutenberg block-editor SSR preview
2182 * (the /wp/v2/block-renderer REST request with context=edit), so the
2183 * renderer can serve a lightweight preview instead of the full frontend set.
2184 */
2185 private static function is_block_editor_preview_request(): bool
2186 {
2187 if (!defined('REST_REQUEST') || !REST_REQUEST) {
2188 return false;
2189 }
2190 $route = isset($GLOBALS['wp']->query_vars['rest_route']) ? (string) $GLOBALS['wp']->query_vars['rest_route'] : '';
2191 if (strpos($route, 'block-renderer') !== false) {
2192 return true;
2193 }
2194 // Fallback: the block-renderer route also arrives via REQUEST_URI.
2195 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
2196 return strpos($uri, 'block-renderer') !== false;
2197 }
2198 }
2199 ?>