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