PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / app / Hooks / Handlers / FluentBlockEditorHandler.php

FluentBlockEditorHandler.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.11.0, at app/Hooks/Handlers/FluentBlockEditorHandler.php

1,237 lines 57.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Hooks\Handlers;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Models\BaseSpace;
7 use FluentCommunity\App\Services\Helper;
8 use FluentCommunity\Framework\Support\Arr;
9 use FluentCommunity\Modules\Course\Model\CourseLesson;
10
11 class FluentBlockEditorHandler
12 {
13 /**
14 * Extra fcomEditorVars entries contributed by whoever answered the
15 * fluent_community/block_editor_context filter. Stashed in renderCustomEditor()
16 * and merged in gutenberg_editor_scripts_and_styles(), which runs later in the
17 * same request off the wp_enqueue_scripts callback registered below.
18 *
19 * @var array
20 */
21 private $contextEditorVars = [];
22
23 public function register()
24 {
25 add_action('init', function () {
26
27 $editorPostTypes = apply_filters('fluent_community/block_editor_post_types', [
28 'fcom-dummy' => [
29 'label' => __('Lesson', 'fluent-community'),
30 'public' => false,
31 'show_in_rest' => true,
32 'supports' => ['title', 'editor', 'thumbnail'],
33 ],
34 'fcom-lockscreen' => [
35 'label' => __('Lock Screen', 'fluent-community'),
36 'public' => false,
37 'show_in_rest' => true,
38 'supports' => ['editor'],
39 ],
40 ]);
41
42 foreach ($editorPostTypes as $editorPostType => $editorPostTypeArgs) {
43 register_post_type($editorPostType, $editorPostTypeArgs);
44 }
45
46 if (!isset($_REQUEST['fluent_community_block_editor'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
47 return;
48 }
49
50 if (!defined('IFRAME_REQUEST')) {
51 define('IFRAME_REQUEST', true); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
52 }
53
54 remove_action('enqueue_block_editor_assets', 'wp_enqueue_editor_block_directory_assets');
55 add_action('fluent_community/block_editor_head', function () {
56 $url = FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/index.css';
57 $contentStylingUrl = FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/content_styling.css';
58 // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- emitted into a custom non-wp_head action hook, wp_enqueue_style not applicable
59 ?>
60 <link rel="stylesheet"
61 href="<?php echo esc_url($url); ?>?version=<?php echo esc_attr(FLUENT_COMMUNITY_PLUGIN_VERSION); ?>"
62 media="screen"/>
63 <link rel="stylesheet"
64 href="<?php echo esc_url($contentStylingUrl); ?>?version=<?php echo esc_attr(FLUENT_COMMUNITY_PLUGIN_VERSION); ?>"
65 media="screen"/>
66 <?php // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ?>
67
68 <style>
69 /* No admin bar here; WP 7.1 offsets the editor by this var, leaving a gap. */
70 html { --wp-admin--admin-bar--height: 0; }
71 <?php echo wp_strip_all_tags($this->getColorSchemaCss()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- generated CSS, no user input ?>
72 </style>
73
74 <?php
75 });
76 add_filter('should_load_separate_core_block_assets', '__return_false', 20);
77 $this->renderCustomEditor($_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
78
79 $actionHook = 'template_redirect';
80 if(is_admin()) {
81 $actionHook = 'admin_init';
82 }
83
84 add_action($actionHook, function () {
85 $this->renderPage();
86 exit(200);
87 }, -1000);
88 }, 2);
89 }
90
91 public function renderCustomEditor($data = [])
92 {
93 do_action('litespeed_control_set_nocache', 'fluentcommunity api request'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
94 // set no cache headers
95 nocache_headers();
96
97 $hasAccess = false;
98 $postType = 'fcom-dummy';
99 $context = Arr::get($data, 'context');
100
101 $lesson = null;
102
103 if ($context === 'course_lesson') {
104 $lessonId = Arr::get($data, 'lesson_id');
105 if ($lessonId) {
106 $lesson = CourseLesson::find($lessonId);
107 $hasAccess = $lesson && $lesson->course && $lesson->course->isCourseAdmin();
108 }
109 }
110
111 if ($context === 'lockscreen') {
112 $postType = 'fcom-lockscreen';
113 $spaceId = Arr::get($data, 'space_id');
114 if ($spaceId) {
115 $space = BaseSpace::query()->onlyMain()->find($spaceId);
116 $hasAccess = $space && $space->isAdmin(get_current_user_id(), true);
117 }
118 }
119
120 /*
121 * Extension point for editor contexts registered outside this plugin (the pro
122 * Space Pages module uses it). Handlers matching $context return their own
123 * access decision, post type and the content to seed the simulated post with.
124 */
125 $contextConfig = apply_filters('fluent_community/block_editor_context', [
126 'has_access' => $hasAccess,
127 'post_type' => $postType,
128 'has_content' => false,
129 'title' => '',
130 'content' => '',
131 ], $context, $data);
132
133 $hasAccess = !empty($contextConfig['has_access']);
134
135 $this->contextEditorVars = (array)Arr::get($contextConfig, 'editor_vars', []);
136
137 $contextPostType = Arr::get($contextConfig, 'post_type');
138 if ($contextPostType) {
139 $postType = $contextPostType;
140 }
141
142 if (!$hasAccess) {
143 echo '<h3 style="padding: 100px; text-align: center;">' . esc_html__('Sorry, you do not have access to this page.', 'fluent-community') . '</h3>';
144 exit(200);
145 }
146
147 add_filter('should_load_separate_core_block_assets', '__return_false', 20);
148 show_admin_bar(false);
149
150 $firstPost = Utility::getApp('db')->table('posts')
151 ->where('post_type', $postType)
152 ->first();
153
154 if ($firstPost) {
155 $simulatedPost = get_post($firstPost->ID);
156 $simulatedPost->post_content = '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->';
157 } else {
158 $newPostId = wp_insert_post(array(
159 'post_title' => $context === 'course_lesson' ? 'Demo Lesson Title' : '',
160 'post_content' => '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->',
161 'post_type' => $postType,
162 'post_status' => 'draft',
163 ));
164
165 $simulatedPost = get_post($newPostId);
166 }
167
168 global $post;
169 $post = $simulatedPost;
170
171 if ($lesson) {
172 $post->post_title = $lesson->title;
173 $post->post_content = $lesson->message ?: '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->';
174 } elseif (!empty($contextConfig['has_content'])) {
175 $contextContent = Arr::get($contextConfig, 'content');
176 $post->post_title = Arr::get($contextConfig, 'title', '');
177 $post->post_content = $contextContent ? $contextContent : '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->';
178 }
179
180 // renderPage() exits during admin_init and manually fires wp_enqueue_scripts in
181 // both the admin and frontend branches. admin_enqueue_scripts never runs here, so
182 // the apiFetch preload must ride wp_enqueue_scripts or the editor falls back to a
183 // live GET /wp/v2/fcom-dummy/{id}?context=edit — which 403s for users without
184 // edit_others_posts (e.g. contributors) on the shared, admin-authored dummy post.
185 add_action('wp_enqueue_scripts', function () use ($post) {
186 wp_enqueue_script('postbox', admin_url('js/postbox.min.js'), array('jquery-ui-sortable'), FLUENT_COMMUNITY_PLUGIN_VERSION, true);
187 wp_enqueue_style('dashicons');
188 wp_enqueue_style('media');
189 wp_enqueue_style('admin-menu');
190 wp_enqueue_style('admin-bar');
191 wp_enqueue_style('l10n');
192
193 wp_add_inline_script(
194 'wp-api-fetch',
195 \sprintf(
196 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
197 wp_json_encode(
198 array(
199 '/wp/v2/' . $post->post_type . '/' . $post->ID . '?context=edit' => array(
200 'body' => array(
201 'id' => $post->ID,
202 'title' => array('raw' => $post->post_title),
203 'content' => array(
204 'block_format' => 1,
205 'raw' => $post->post_content,
206 ),
207 'excerpt' => array('raw' => ''),
208 'date' => '',
209 'date_gmt' => '',
210 'modified' => '',
211 'modified_gmt' => '',
212 'link' => home_url('/'),
213 'guid' => array(),
214 'parent' => 0,
215 'menu_order' => 0,
216 'author' => 0,
217 'featured_media' => 0,
218 'comment_status' => 'closed',
219 'ping_status' => 'closed',
220 'template' => '',
221 'meta' => array(),
222 '_links' => array(),
223 'type' => $post->post_type,
224 'status' => 'pending', // pending is the best state to remove draft saving possibilities.
225 'slug' => '',
226 'generated_slug' => '',
227 'permalink_template' => home_url('/'),
228 ),
229 ),
230 )
231 )
232 ),
233 'after'
234 );
235 }, 11);
236
237 add_action('wp_enqueue_scripts', function ($hook) use ($post) {
238 // Gutenberg requires the post-locking functions defined within:
239 // See `show_post_locked_dialog` and `get_post_metadata` filters below.
240 include_once ABSPATH . 'wp-admin/includes/post.php';
241 $this->gutenberg_editor_scripts_and_styles($hook, $post);
242 });
243
244 // Disable post locking dialogue.
245 add_filter('show_post_locked_dialog', '__return_false');
246
247 // Everyone can richedit! This avoids a case where a page can be cached where a user can't richedit.
248 $GLOBALS['wp_rich_edit'] = true;
249 add_filter('user_can_richedit', '__return_true', 1000);
250
251 // Homepage is always locked by @wordpressdotorg
252 // This prevents other logged-in users taking a lock of the post on the front-end.
253 add_filter('get_post_metadata', function ($value, $post_id, $meta_key) {
254 if ($meta_key !== '_edit_lock') {
255 return $value;
256 }
257 return time() . ':' . get_current_user_id(); // WordPressdotorg user ID
258 }, 10, 3);
259
260 // Disable Jetpack Blocks for now.
261 add_filter('jetpack_gutenberg', '__return_false');
262 }
263
264 private function gutenberg_editor_scripts_and_styles($hook, $post)
265 {
266 $initial_edits = array(
267 'title' => $post->post_title,
268 'content' => $post->post_content,
269 'excerpt' => $post->post_excerpt,
270 );
271
272 $editor_settings = $this->getEditorSettings($post);
273
274 $init_script =
275 "(function() {
276 window._wpLoadBlockEditor = new Promise(function(resolve) {
277 wp.domReady(function() {
278 resolve(wp.editPost.initializeEditor('editor', \"%s\", %d, %s, %s));
279 });
280 });
281 })();";
282
283 $script = sprintf(
284 $init_script,
285 $post->post_type,
286 $post->ID,
287 wp_json_encode($editor_settings),
288 wp_json_encode($initial_edits)
289 );
290 wp_add_inline_script('wp-edit-post', $script);
291
292 /**
293 * Scripts
294 */
295 wp_enqueue_media();
296
297 add_filter('user_can_richedit', '__return_true');
298 wp_tinymce_inline_scripts();
299 wp_enqueue_editor();
300
301 /**
302 * Styles
303 */
304 wp_enqueue_style('wp-edit-post');
305
306 // Include block styles needed when user is not signed in. See: https://github.com/WordPress/wporg-gutenberg/issues/26
307 wp_enqueue_style('global-styles');
308 wp_enqueue_style('wp-block-library');
309 wp_enqueue_style('wp-block-image');
310 wp_enqueue_style('wp-block-group');
311 wp_enqueue_style('wp-block-heading');
312 wp_enqueue_style('wp-block-button');
313 wp_enqueue_style('wp-block-paragraph');
314 wp_enqueue_style('wp-block-separator');
315 wp_enqueue_style('wp-block-columns');
316 wp_enqueue_style('wp-block-cover');
317 wp_enqueue_style('global-styles-css-custom-properties');
318 wp_enqueue_style('wp-block-spacer');
319
320 wp_register_style('fluent_com_editor_styles', FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/style.css', [], FLUENT_COMMUNITY_PLUGIN_VERSION, 'all');
321
322 add_action('fluent_enqueue_block_editor_assets', 'wp_enqueue_editor_format_library_assets');
323
324 // WP 6.9+ loads the LaTeX-to-MathML converter as a script module; core wires this on
325 // 'enqueue_block_editor_assets', which this standalone editor page never fires.
326 if (function_exists('wp_enqueue_block_editor_script_modules')) {
327 add_action('fluent_enqueue_block_editor_assets', 'wp_enqueue_block_editor_script_modules');
328 }
329
330 /**
331 * Fires after block assets have been enqueued for the editing interface.
332 *
333 * Call `add_action` on any hook before 'admin_enqueue_scripts'.
334 *
335 * In the function call you supply, simply use `wp_enqueue_script` and
336 * `wp_enqueue_style` to add your functionality to the Gutenberg editor.
337 *
338 * @since 0.4.0
339 */
340 do_action('fluent_enqueue_block_editor_assets');
341
342 wp_enqueue_script('fcom_editor_custom', FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/index.js', ['react', 'wp-components', 'wp-compose', 'wp-data', 'wp-edit-post', 'wp-i18n', 'wp-plugins'], FLUENT_COMMUNITY_PLUGIN_VERSION . time(), true);
343 wp_localize_script('fcom_editor_custom', 'fcomEditorI18n', $this->getEditorI18nStrings());
344 wp_localize_script('fcom_editor_custom', 'fcomEditorVars', array_merge([
345 'video_gate_default_threshold' => \FluentCommunity\Modules\Course\Services\LessonVideoGateService::getDefaultThreshold(),
346 'can_unfiltered_html' => current_user_can('unfiltered_html') ? 'yes' : 'no',
347 ], $this->contextEditorVars));
348 }
349
350 private function getEditorI18nStrings()
351 {
352 $strings = [
353 'Enable Video Embed' => __('Enable Video Embed', 'fluent-community'),
354 'Enable Comments' => __('Enable Comments', 'fluent-community'),
355 'Free Preview Lesson' => __('Free Preview Lesson', 'fluent-community'),
356 'If enabled, public users can view this lesson without enrolling the course.' => __('If enabled, public users can view this lesson without enrolling the course.', 'fluent-community'),
357 'Media Embed' => __('Media Embed', 'fluent-community'),
358 'Documents & Files' => __('Documents & Files', 'fluent-community'),
359 'Smartcodes' => __('Smartcodes', 'fluent-community'),
360 'You may use following smartcode in your lesson:' => __('You may use following smartcode in your lesson:', 'fluent-community'),
361 "User's Name:" => __("User's Name:", 'fluent-community'),
362 "User's Email:" => __("User's Email:", 'fluent-community'),
363 "User's photo HTML:" => __("User's photo HTML:", 'fluent-community'),
364 "User's Profile Link:" => __("User's Profile Link:", 'fluent-community'),
365 'Failed to fetch embed. Please check the URL.' => __('Failed to fetch embed. Please check the URL.', 'fluent-community'),
366 'Video thumbnail' => __('Video thumbnail', 'fluent-community'),
367 'Media embedded successfully' => __('Media embedded successfully', 'fluent-community'),
368 '+ Add Media' => __('+ Add Media', 'fluent-community'),
369 'Add Media' => __('Add Media', 'fluent-community'),
370 'Edit Media' => __('Edit Media', 'fluent-community'),
371 'Oembed URL' => __('Oembed URL', 'fluent-community'),
372 'Video URL' => __('Video URL', 'fluent-community'),
373 'Supports Vimeo, YouTube, Wistia and more' => __('Supports Vimeo, YouTube, Wistia and more', 'fluent-community'),
374 'Embedding…' => __('Embedding…', 'fluent-community'),
375 'Cancel' => __('Cancel', 'fluent-community'),
376 'Remove' => __('Remove', 'fluent-community'),
377 'Oembed' => __('Oembed', 'fluent-community'),
378 'Custom HTML' => __('Custom HTML', 'fluent-community'),
379 'Custom HTML Code' => __('Custom HTML Code', 'fluent-community'),
380 'Your account cannot save custom HTML. Scripts, iframes and similar tags are removed on save.' => __('Your account cannot save custom HTML. Scripts, iframes and similar tags are removed on save.', 'fluent-community'),
381 'Paste an iframe code' => __('Paste an iframe code', 'fluent-community'),
382 'Embed' => __('Embed', 'fluent-community'),
383 'Paste a URL to embed' => __('Paste a URL to embed', 'fluent-community'),
384 'Embed from Vimeo, YouTube, Wistia and more' => __('Embed from Vimeo, YouTube, Wistia and more', 'fluent-community'),
385 'FluentPlayer' => __('FluentPlayer', 'fluent-community'),
386 'Select Player Block' => __('Select Player Block', 'fluent-community'),
387 'Use FluentPlayer Block' => __('Use FluentPlayer Block', 'fluent-community'),
388 'The feature video is managed by the FluentPlayer block in the editor.' => __('The feature video is managed by the FluentPlayer block in the editor.', 'fluent-community'),
389 'A FluentPlayer block will be added at the top of the lesson content. Add or edit the video directly from that block in the editor.' => __('A FluentPlayer block will be added at the top of the lesson content. Add or edit the video directly from that block in the editor.', 'fluent-community'),
390 'Video Completion' => __('Video Completion', 'fluent-community'),
391 'Require video watch to complete' => __('Require video watch to complete', 'fluent-community'),
392 'Students must watch the video before they can mark this lesson as completed.' => __('Students must watch the video before they can mark this lesson as completed.', 'fluent-community'),
393 'Auto-complete lesson when the video ends' => __('Auto-complete lesson when the video ends', 'fluent-community'),
394 'Marks the lesson complete a few seconds after the student watches the video to the end.' => __('Marks the lesson complete a few seconds after the student watches the video to the end.', 'fluent-community'),
395 'Required watch percentage' => __('Required watch percentage', 'fluent-community'),
396 '100% means the video must be watched to the end.' => __('100% means the video must be watched to the end.', 'fluent-community'),
397 'Lesson Duration' => __('Lesson Duration', 'fluent-community'),
398 'Minutes' => __('Minutes', 'fluent-community'),
399 'Seconds' => __('Seconds', 'fluent-community'),
400 'View' => __('View', 'fluent-community'),
401 'No documents attached yet.' => __('No documents attached yet.', 'fluent-community'),
402 'Manage Documents & Files' => __('Manage Documents & Files', 'fluent-community'),
403 "User's Name" => __("User's Name", 'fluent-community'),
404 "User's Email" => __("User's Email", 'fluent-community'),
405 "User's photo HTML" => __("User's photo HTML", 'fluent-community'),
406 'Profile Link' => __('Profile Link', 'fluent-community'),
407 // Space page settings panel
408 'Who can view this page' => __('Who can view this page', 'fluent-community'),
409 'Everyone who can view the space' => __('Everyone who can view the space', 'fluent-community'),
410 'Space members only' => __('Space members only', 'fluent-community'),
411 'URL Slug' => __('URL Slug', 'fluent-community'),
412 'Changing the slug will break existing links to this page.' => __('Changing the slug will break existing links to this page.', 'fluent-community'),
413 'Show in space menu' => __('Show in space menu', 'fluent-community'),
414 'Menu Label' => __('Menu Label', 'fluent-community'),
415 'Leave empty to use the page title.' => __('Leave empty to use the page title.', 'fluent-community'),
416 'SEO' => __('SEO', 'fluent-community'),
417 'SEO Description' => __('SEO Description', 'fluent-community'),
418 'Used for search engines and link previews' => __('Used for search engines and link previews', 'fluent-community'),
419 // Space page layout picker. The template labels and descriptions are
420 // localized by whoever registers them (SpacePageHelper::getLayoutTemplates
421 // in pro); only the fallback names used when pro is absent live here.
422 'Page Layout' => __('Page Layout', 'fluent-community'),
423 'Show the page title' => __('Show the page title', 'fluent-community'),
424 'Show the featured image' => __('Show the featured image', 'fluent-community'),
425 'The title stays editable here and still names the page in the menu.' => __('The title stays editable here and still names the page in the menu.', 'fluent-community'),
426 'The image still appears in search results and link previews.' => __('The image still appears in search results and link previews.', 'fluent-community'),
427 'Standard' => __('Standard', 'fluent-community'),
428 'Classic' => __('Classic', 'fluent-community'),
429 'Full Width' => __('Full Width', 'fluent-community'),
430 'Unified' => __('Unified', 'fluent-community'),
431 ];
432
433 return apply_filters('fluent_community/editor_i18n_strings', $strings);
434 }
435
436 private function gutenberg_get_available_image_sizes()
437 {
438 $size_names = apply_filters(
439 'fluent_community/image_size_names_choose',
440 array(
441 'thumbnail' => __('Thumbnail', 'fluent-community'),
442 'medium' => __('Medium', 'fluent-community'),
443 'large' => __('Large', 'fluent-community'),
444 'full' => __('Full Size', 'fluent-community'),
445 )
446 );
447 $all_sizes = array();
448 foreach ($size_names as $size_slug => $size_name) {
449 $all_sizes[] = array(
450 'slug' => $size_slug,
451 'name' => $size_name,
452 );
453 }
454 return $all_sizes;
455 }
456
457 protected function renderPage()
458 {
459
460 remove_action( 'wp_print_styles', 'print_emoji_styles' );
461
462 add_action('fluent_community/block_editor_footer', function () {
463 wp_underscore_playlist_templates();
464 wp_print_footer_scripts();
465 $this->printScriptModules();
466 wp_print_media_templates();
467 wp_enqueue_global_styles();
468 if (function_exists('wp_enqueue_stored_styles')) {
469 wp_enqueue_stored_styles();
470 }
471 wp_maybe_inline_styles();
472 });
473
474 add_action('fluent_block_editor/head', 'wp_enqueue_scripts', 1);
475 add_action('fluent_block_editor/head', 'wp_resource_hints', 2);
476 add_action('fluent_block_editor/head', 'wp_preload_resources', 1);
477 add_action('fluent_block_editor/head', 'wp_print_styles', 8);
478 add_action('fluent_block_editor/head', 'wp_print_head_scripts', 9);
479 add_action('fluent_block_editor/head', 'wp_custom_css_cb', 101);
480
481 $this->unloadOtherScripts();
482 ?>
483 <!DOCTYPE html>
484 <html <?php language_attributes(); ?>>
485 <head>
486 <title>FluentCommunity Block Editor</title>
487 <meta charset='utf-8'>
488 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover"/>
489 <meta name="mobile-web-app-capable" content="yes">
490 <meta name="robots" content="noindex">
491 <?php do_action('fluent_block_editor/head'); ?>
492 <?php do_action('fluent_community/block_editor_head'); ?>
493 </head>
494 <body class="fcom_custom_editor">
495 <div class="wp-site-blocks">
496 <div id="editor" class="gutenberg__editor"></div>
497 </div>
498 <?php
499 do_action('fluent_community/block_editor_footer');
500 ?>
501 </body>
502 </html>
503 <?php
504 }
505
506 /**
507 * Core prints script modules on 'wp_footer' / 'admin_print_footer_scripts'. This editor page
508 * renders its own document and fires neither, so blocks relying on script modules (core/math
509 * and its LaTeX-to-MathML converter on WP 6.9+) would never load.
510 */
511 protected function printScriptModules()
512 {
513 if (!function_exists('wp_script_modules')) {
514 return;
515 }
516
517 $scriptModules = wp_script_modules();
518
519 $scriptModules->print_import_map();
520 $scriptModules->print_enqueued_script_modules();
521 $scriptModules->print_script_module_preloads();
522
523 if (method_exists($scriptModules, 'print_script_module_translations')) {
524 $scriptModules->print_script_module_translations();
525 }
526 }
527
528 private function shouldBlockAsset(string $src, string $pluginUrl, string $themesUrl, string $approvedPattern): bool
529 {
530 $isPlugin = strpos($src, $pluginUrl) !== false;
531 $isTheme = strpos($src, $themesUrl) !== false;
532
533 if (!$isPlugin && !$isTheme) {
534 return false;
535 }
536
537 return !preg_match('#' . $approvedPattern . '#', $src);
538 }
539
540 private function unloadOtherScripts()
541 {
542 // skips scripts and styles both; fluent_community/skip_no_conflict is styles only
543 $isSkip = apply_filters('fluent_com_editor/skip_no_conflict', false);
544 if ($isSkip) {
545 return;
546 }
547
548 // scripts only; styles use fluent_community/asset_listed_slugs
549 $approvedSlugs = apply_filters('fluent_com_editor/asset_listed_slugs', [
550 '\/gutenberg\/'
551 ]);
552 $approvedSlugs[] = '\/fluent-community(-pro)?\/';
553 $approvedSlugs = array_unique($approvedSlugs);
554 $approvedSlugs = implode('|', $approvedSlugs);
555
556 $pluginUrl = str_replace(['http:', 'https:'], '', plugins_url());
557
558 $themesUrl = str_replace(['http:', 'https:'], '', get_theme_root_uri());
559
560 add_filter('script_loader_src', function ($src, $handle) use ($approvedSlugs, $pluginUrl, $themesUrl) {
561 if (!$src) {
562 return $src;
563 }
564
565 if ($this->shouldBlockAsset($src, $pluginUrl, $themesUrl, $approvedSlugs)) {
566 return false;
567 }
568
569 return $src;
570 }, 1, 2);
571
572 add_action('wp_print_scripts', function () use ($approvedSlugs, $pluginUrl, $themesUrl) {
573 global $wp_scripts;
574 if (!$wp_scripts) {
575 return;
576 }
577
578 foreach ($wp_scripts->queue as $script) {
579 if (empty($wp_scripts->registered[$script]) || empty($wp_scripts->registered[$script]->src)) {
580 continue;
581 }
582
583 $src = $wp_scripts->registered[$script]->src;
584 if ($this->shouldBlockAsset($src, $pluginUrl, $themesUrl, $approvedSlugs)) {
585 wp_dequeue_script($wp_scripts->registered[$script]->handle);
586 }
587 }
588 }, 1);
589
590 add_action('wp_print_styles', function () {
591 $isSkip = apply_filters('fluent_community/skip_no_conflict', false, 'styles');
592
593 if ($isSkip) {
594 return;
595 }
596
597 global $wp_styles;
598 if (!$wp_styles) {
599 return;
600 }
601
602 // styles only; scripts use fluent_com_editor/asset_listed_slugs
603 $approvedSlugs = apply_filters('fluent_community/asset_listed_slugs', [
604 '\/gutenberg\/',
605 ]);
606
607 $approvedSlugs[] = '\/fluent-community(-pro)?\/';
608
609 $approvedSlugs = array_unique($approvedSlugs);
610 $approvedSlugs = implode('|', $approvedSlugs);
611
612 $pluginUrl = plugins_url();
613 $themeUrl = get_theme_root_uri();
614
615 $pluginUrl = str_replace(['http:', 'https:'], '', $pluginUrl);
616 $themeUrl = str_replace(['http:', 'https:'], '', $themeUrl);
617
618 foreach ($wp_styles->queue as $script) {
619
620 if (empty($wp_styles->registered[$script]) || empty($wp_styles->registered[$script]->src)) {
621 continue;
622 }
623
624 $src = $wp_styles->registered[$script]->src;
625 $pluginMatched = (strpos($src, $pluginUrl) !== false) && !preg_match('#' . $approvedSlugs . '#', $src);
626 $themeMatched = (strpos($src, $themeUrl) !== false) && !preg_match('#' . $approvedSlugs . '#', $src);
627
628 if (!$pluginMatched && !$themeMatched) {
629 continue;
630 }
631
632 wp_dequeue_style($wp_styles->registered[$script]->handle);
633 }
634 }, 999999);
635 }
636
637 private function getEditorSettings($post)
638 {
639 // Media settings.
640 $max_upload_size = wp_max_upload_size();
641 if (!$max_upload_size) {
642 $max_upload_size = 0;
643 }
644
645 $lock_details = array(
646 'isLocked' => false,
647 'user' => '',
648 );
649
650 $editor_settings = array(
651 'maxUploadFileSize' => $max_upload_size,
652 'allowedMimeTypes' => get_allowed_mime_types(),
653 'postLock' => $lock_details,
654 'postLockUtils' => array(
655 'nonce' => wp_create_nonce('lock-post_' . $post->ID),
656 'unlockNonce' => wp_create_nonce('update-post_' . $post->ID),
657 'ajaxUrl' => admin_url('admin-ajax.php'),
658 ),
659 '__experimentalFeatures' => $this->getExperimentalFeatures(),
660 'colors' => $this->getColorPalette(),
661 '__experimentalDiscussionSettings' => [
662 'avatarURL' => 'https://secure.gravatar.com/avatar/?s=96&f=y&r=g',
663 'commentOrder' => 'asc',
664 'commentsPerPage' => '50',
665 'defaultCommentsPage' => 'newest',
666 'defaultCommentStatus' => 'open',
667 'pageComments' => '',
668 'threadComments' => '1',
669 'threadCommentsDepth' => '5'
670 ],
671 '__unstableGalleryWithImageBlocks' => false,
672 '__unstableIsBlockBasedTheme' => false,
673 'enableCustomUnits' => [
674 'px',
675 'em',
676 'rem',
677 '%',
678 'vh',
679 'vw'
680 ],
681 'fontSizes' => [
682 [
683 'name' => 'Small',
684 'size' => 'var(--fcom-font-size-small)',
685 'slug' => 'small'
686 ],
687 [
688 'name' => 'Medium',
689 'size' => 'var(--fcom-font-size-medium)',
690 'slug' => 'medium'
691 ],
692 [
693 'name' => 'Large',
694 'size' => 'var(--fcom-font-size-large)',
695 'slug' => 'large'
696 ],
697 [
698 'name' => 'Larger',
699 'size' => 'var(--fcom-font-size-larger)',
700 'slug' => 'larger'
701 ],
702 [
703 'name' => 'XX-Large',
704 'size' => 'var(--fcom-font-size-xxlarge)',
705 'slug' => 'xxlarge'
706 ]
707 ],
708 'fullscreenMode' => 1,
709 'enableCustomSpacing' => 1,
710 'enableCustomLineHeight' => 1,
711 'enableCustomFields' => false,
712 'disablePostFormats' => true,
713 'disableLayoutStyles' => false,
714 'disableCustomSpacingSizes' => false,
715 'disableCustomGradients' => 1,
716 'alignWide' => true,
717 'disableCustomFontSizes' => false,
718 'disableCustomColors' => false,
719 'canUpdateBlockBindings' => false,
720 'bodyPlaceholder' => __('Start writing or type / to choose a block for your lesson content', 'fluent-community'),
721 'allowedBlockTypes' => apply_filters('fluent_community/allowed_block_types', [
722 'core/audio',
723 'core/block',
724 'core/buttons',
725 'core/button',
726 'core/code',
727 'core/columns',
728 'core/column',
729 'core/cover',
730 'core/details',
731 'core/embed',
732 'core/footnotes',
733 'core/freeform',
734 'core/gallery',
735 'core/group',
736 'core/heading',
737 'core/html',
738 'core/image',
739 'core/list',
740 'core/list-item',
741 'core/math',
742 'core/media-text',
743 'core/missing',
744 'core/paragraph',
745 'core/preformatted',
746 'core/pullquote',
747 'core/quote',
748 'core/separator',
749 'core/social-link',
750 'core/social-links',
751 'core/spacer',
752 'core/table',
753 'core/verse'
754 ]),
755 'gradients' => [],
756 'imageDefaultSize' => 'large',
757 'imageEditing' => true,
758 'isRTL' => Helper::isRtl(),
759 'autosaveInterval' => 999,
760 'localAutosaveInterval' => 999,
761 'richEditingEnabled' => true,
762 'spacingSizes' => [
763 [
764 'name' => '2X-Small',
765 'size' => '0.44rem',
766 'slug' => '20'
767 ],
768 [
769 'name' => 'X-Small',
770 'size' => '0.67rem',
771 'slug' => '30'
772 ],
773 [
774 'name' => 'Small',
775 'size' => '1rem',
776 'slug' => '40'
777 ],
778 [
779 'name' => 'Medium',
780 'size' => '1.5rem',
781 'slug' => '50'
782 ],
783 [
784 'name' => 'Large',
785 'size' => '2.25rem',
786 'slug' => '60'
787 ],
788 [
789 'name' => 'X-Large',
790 'size' => '3.38rem',
791 'slug' => '70'
792 ],
793 [
794 'name' => '2X-Large',
795 'size' => '5.06rem',
796 'slug' => '80'
797 ]
798 ],
799 'titlePlaceholder' => __('Add Lesson title', 'fluent-community')
800 );
801
802 $editor_settings['styles'] = $this->getEditorStyles();
803 $editor_settings['__unstableResolvedAssets'] = $this->getResolvedAssets();
804 $editor_settings['defaultEditorStyles'] = $this->getDefaultEditorStyles();
805 $editor_settings['imageSizes'] = $this->gutenberg_get_available_image_sizes();
806
807 $editor_settings = apply_filters('fluent_community/block_editor_settings', $editor_settings);
808 return $editor_settings;
809 }
810
811 private function getExperimentalFeatures()
812 {
813 return array(
814 'appearanceTools' => true,
815 'useRootPaddingAwareAlignments' => false,
816 'border' => [
817 'color' => 1,
818 'radius' => 1,
819 'style' => 1,
820 'width' => 1,
821 ],
822 'color' => [
823 'background' => true,
824 'button' => 1,
825 'caption' => 1,
826 'customDuotone' => 0,
827 'defaultDuotone' => 0,
828 'defaultGradients' => 0,
829 'defaultPalette' => [],
830 'duotone' => [],
831 'gradients' => [],
832 'heading' => 1,
833 'link' => 1,
834 'palette' => [
835 'default' => [],
836 'theme' => [
837 [
838 'name' => 'Accent',
839 'slug' => 'theme-palette-color-1',
840 'color' => 'var(--theme-palette-color-1)',
841 ],
842 [
843 'name' => 'Accent - alt',
844 'slug' => 'theme-palette-color-2',
845 'color' => 'var(--theme-palette-color-2)',
846 ],
847 [
848 'name' => 'Strongest text',
849 'slug' => 'theme-palette-color-3',
850 'color' => 'var(--theme-palette-color-3)',
851 ],
852 [
853 'name' => 'Strong Text',
854 'slug' => 'theme-palette-color-4',
855 'color' => 'var(--theme-palette-color-4)',
856 ],
857 [
858 'name' => 'Medium text',
859 'slug' => 'theme-palette-color-5',
860 'color' => 'var(--theme-palette-color-5)',
861 ],
862 [
863 'name' => 'Subtle Text',
864 'slug' => 'theme-palette-color-6',
865 'color' => 'var(--theme-palette-color-6)',
866 ],
867 [
868 'name' => 'Subtle Background',
869 'slug' => 'theme-palette-color-7',
870 'color' => 'var(--theme-palette-color-7)',
871 ],
872 [
873 'name' => 'Lighter Background',
874 'slug' => 'theme-palette-color-8',
875 'color' => 'var(--theme-palette-color-8)',
876 ]
877 ]
878 ],
879 'text' => true,
880 ],
881 'dimensions' => [
882 'defaultAspectRatios' => true,
883 'aspectRatios' => [
884 'default' => [
885 [
886 'name' => 'Square - 1:1',
887 'slug' => 'square',
888 'ratio' => '1',
889 ],
890 [
891 'name' => 'Standard - 4:3',
892 'slug' => '4-3',
893 'ratio' => '4/3',
894 ],
895 [
896 'name' => 'Portrait - 3:4',
897 'slug' => '3-4',
898 'ratio' => '3/4',
899 ],
900 [
901 'name' => 'Classic - 3:2',
902 'slug' => '3-2',
903 'ratio' => '3/2',
904 ],
905 [
906 'name' => 'Classic Portrait - 2:3',
907 'slug' => '2-3',
908 'ratio' => '2/3',
909 ],
910 [
911 'name' => 'Wide - 16:9',
912 'slug' => '16-9',
913 'ratio' => '16/9',
914 ],
915 [
916 'name' => 'Tall - 9:16',
917 'slug' => '9-16',
918 'ratio' => '9/16',
919 ],
920 ]
921 ],
922 'aspectRatio' => 1,
923 'minHeight' => 1,
924 ],
925 'shadow' => [
926 'defaultPresets' => true,
927 'presets' => [
928 'default' => [
929 [
930 'name' => 'Natural',
931 'slug' => 'natural',
932 'shadow' => '6px 6px 9px rgba(0, 0, 0, 0.2)',
933 ],
934 [
935 'name' => 'Deep',
936 'slug' => 'deep',
937 'shadow' => '12px 12px 50px rgba(0, 0, 0, 0.4)',
938 ],
939 [
940 'name' => 'Sharp',
941 'slug' => 'sharp',
942 'shadow' => '6px 6px 0px rgba(0, 0, 0, 0.2)',
943 ],
944 [
945 'name' => 'Outlined',
946 'slug' => 'outlined',
947 'shadow' => '6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1)',
948 ],
949 [
950 'name' => 'Crisp',
951 'slug' => 'crisp',
952 'shadow' => '6px 6px 0px rgba(0, 0, 0, 1)',
953 ],
954 ],
955 ],
956 ],
957 'spacing' => [
958 'blockGap' => 1,
959 'margin' => 1,
960 'padding' => 1,
961 'defaultSpacingSizes' => true,
962 'spacingScale' => [
963 'default' => [
964 'operator' => '*',
965 'increment' => 1.5,
966 'steps' => 7,
967 'mediumStep' => 1.5,
968 'unit' => 'rem',
969 ],
970 ],
971 'spacingSizes' => [
972 'default' => [
973 [
974 'name' => '2X-Small',
975 'slug' => '20',
976 'size' => '0.44rem',
977 ],
978 [
979 'name' => 'X-Small',
980 'slug' => '30',
981 'size' => '0.67rem',
982 ],
983 [
984 'name' => 'Small',
985 'slug' => '40',
986 'size' => '1rem',
987 ],
988 [
989 'name' => 'Medium',
990 'slug' => '50',
991 'size' => '1.5rem',
992 ],
993 [
994 'name' => 'Large',
995 'slug' => '60',
996 'size' => '2.25rem',
997 ],
998 [
999 'name' => 'X-Large',
1000 'slug' => '70',
1001 'size' => '3.38rem',
1002 ],
1003 [
1004 'name' => '2X-Large',
1005 'slug' => '80',
1006 'size' => '5.06rem',
1007 ],
1008 ],
1009 ],
1010 ],
1011 'typography' => [
1012 'defaultFontSizes' => NULL,
1013 'dropCap' => true,
1014 'fontSizes' => [
1015 'default' => [
1016 [
1017 'name' => 'Small',
1018 'slug' => 'small',
1019 'size' => '13px',
1020 ],
1021 [
1022 'name' => 'Medium',
1023 'slug' => 'medium',
1024 'size' => '20px',
1025 ],
1026 [
1027 'name' => 'Large',
1028 'slug' => 'large',
1029 'size' => '36px',
1030 ],
1031 [
1032 'name' => 'Extra Large',
1033 'slug' => 'x-large',
1034 'size' => '42px',
1035 ],
1036 ],
1037 'theme' => [
1038 [
1039 'name' => 'Small',
1040 'slug' => 'small',
1041 'size' => 'var(--fcom-font-size-small)',
1042 ],
1043 [
1044 'name' => 'Medium',
1045 'slug' => 'medium',
1046 'size' => 'var(--fcom-font-size-medium)',
1047 ],
1048 [
1049 'name' => 'Large',
1050 'slug' => 'large',
1051 'size' => 'var(--fcom-font-size-large)',
1052 ],
1053 [
1054 'name' => 'Larger',
1055 'slug' => 'larger',
1056 'size' => 'var(--fcom-font-size-larger)',
1057 ],
1058 [
1059 'name' => 'XX-Large',
1060 'slug' => 'xxlarge',
1061 'size' => 'var(--fcom-font-size-xxlarge)',
1062 ],
1063 ],
1064 ],
1065 'fontStyle' => true,
1066 'fontWeight' => true,
1067 'letterSpacing' => true,
1068 'textAlign' => true,
1069 'textDecoration' => true,
1070 'textTransform' => true,
1071 'writingMode' => false,
1072 'fluid' => 0,
1073 ],
1074 'blocks' => [
1075 'core/button' => [
1076 'border' => [
1077 'radius' => true,
1078 ]
1079 ],
1080 'core/image' => [
1081 'lightbox' => [
1082 'allowEditing' => true,
1083 ]
1084 ],
1085 'core/pullquote' => [
1086 'border' => [
1087 'color' => true,
1088 'radius' => true,
1089 'style' => true,
1090 'width' => true,
1091 ]
1092 ],
1093 'core/paragraph' => [
1094 'spacing' => [
1095 'margin' => 1,
1096 'padding' => 1,
1097 ]
1098 ]
1099 ],
1100 'layout' => [
1101 'contentSize' => 'var(--theme-block-max-width)',
1102 'wideSize' => 'var(--theme-block-wide-max-width)',
1103 ],
1104 'background' => [
1105 'backgroundImage' => 1,
1106 'backgroundSize' => 1,
1107 ],
1108 'position' => [
1109 'sticky' => 0,
1110 ]
1111 );
1112 }
1113
1114 private function getColorPalette()
1115 {
1116 return [
1117 [
1118 'color' => 'var(--fcom-primary-bg, #ffffff)',
1119 'name' => 'Accent',
1120 'slug' => 'theme-palette-color-1'
1121 ],
1122 [
1123 'color' => 'var(--fcom-secondary-bg, #f0f2f5)',
1124 'name' => 'Accent - alt',
1125 'slug' => 'theme-palette-color-2'
1126 ],
1127 [
1128 'color' => 'var(--fcom-secondary-text, #525866)',
1129 'name' => 'Strongest text',
1130 'slug' => 'theme-palette-color-3'
1131 ],
1132 [
1133 'color' => 'var(--fcom-secondary-content-bg, #f0f3f5)',
1134 'name' => 'Strong Text',
1135 'slug' => 'theme-palette-color-4'
1136 ],
1137 [
1138 'color' => 'var(--fcom-active-bg, #f0f3f5)',
1139 'name' => 'Medium text',
1140 'slug' => 'theme-palette-color-5'
1141 ],
1142 [
1143 'color' => 'var(--fcom-light-bg, #E1E4EA)',
1144 'name' => 'Subtle Text',
1145 'slug' => 'theme-palette-color-6'
1146 ],
1147 [
1148 'color' => 'var(--fcom-deep-bg, #E1E4EA)',
1149 'name' => 'Subtle Background',
1150 'slug' => 'theme-palette-color-7'
1151 ],
1152 [
1153 'color' => 'var(--fcom-primary-text, #19283a)',
1154 'name' => 'Lighter Background',
1155 'slug' => 'theme-palette-color-8'
1156 ]
1157 ];
1158 }
1159
1160 private function getEditorStyles()
1161 {
1162 $editorDir = FLUENT_COMMUNITY_PLUGIN_DIR . 'Modules/Gutenberg/editor/';
1163
1164 return [
1165 [
1166 '__unstableType' => 'colorSchema',
1167 'css' => $this->getColorSchemaCss(),
1168 'isGlobalStyles' => true
1169 ],
1170 [
1171 '__unstableType' => 'theme',
1172 'css' => file_get_contents($editorDir . 'editor-iframe-styles.css') ?: '',
1173 'isGlobalStyles' => true
1174 ],
1175 [
1176 'css' => file_get_contents($editorDir . 'editor.css') ?: '',
1177 '__unstableType' => 'user'
1178 ]
1179 ];
1180 }
1181
1182 private function getResolvedAssets()
1183 {
1184 $resolvedStyles = [
1185 'wp-components-css' => includes_url('/css/dist/components/style.min.css'),
1186 'wp-preferences-css' => includes_url('/css/dist/preferences/style.min.css'),
1187 'wp-block-editor-css' => includes_url('/css/dist/block-editor/style.min.css'),
1188 'wp-reusable-blocks-css' => includes_url('/css/dist/reusable-blocks/style.min.css'),
1189 'wp-patterns-css' => includes_url('/css/dist/patterns/style.min.css'),
1190 'wp-editor-css' => includes_url('/css/dist/editor/style.min.css'),
1191 'wp-block-library-css' => includes_url('/css/dist/block-library/style.min.css'),
1192 'wp-block-editor-content-css' => includes_url('/css/dist/block-editor/content.min.css'),
1193 'wp-edit-blocks-css' => includes_url('/css/dist/block-library/editor.min.css'),
1194 'fcom-content-styling' => FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/content_styling.css'
1195 ];
1196
1197 global $wp_version;
1198 $cssFiles = '';
1199 foreach ($resolvedStyles as $name => $file) {
1200 $cssFiles .= "<link rel='stylesheet' id='{$name}' href='{$file}?ver={$wp_version}' media='all' />\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
1201 }
1202
1203 return [
1204 'scripts' => '<script src="' . includes_url('/js/dist/vendor/wp-polyfill.min.js?ver=3.15.0') . '" id="wp-polyfill-js"></script>', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
1205 'styles' => $cssFiles
1206 ];
1207 }
1208
1209 private function getDefaultEditorStyles()
1210 {
1211 return [
1212 [
1213 '__unstableType' => 'user',
1214 'css' => ':root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0, 124, 186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0, 107, 161;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0, 90, 135;--wp-admin-border-width-focus:2px;--wp-block-synced-color:#7a00df;--wp-block-synced-color--rgb:122, 0, 223;--wp-bound-block-color:var(--wp-block-synced-color);}@media (min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px;}}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:18px;line-height:1.5;--wp--style--block-gap:2em;}p{line-height:1.8;}.editor-post-title__block{font-size:2.5em;font-weight:800;margin-bottom:1em;margin-top:2em;}'
1215 ]
1216 ];
1217 }
1218
1219 private function getColorSchemaCss()
1220 {
1221 $colorSchema = Utility::getColorSchemaConfig();
1222
1223 $darkSchemaConfig = Arr::get($colorSchema, 'dark');
1224
1225 $colorSchemaCss = ':root {';
1226 foreach (Arr::get($darkSchemaConfig, 'body', []) as $colorKey => $value) {
1227 if ($value) {
1228 $cssVar = ' --fcom-' . str_replace('_', '-', $colorKey);
1229 $colorSchemaCss .= $cssVar . ':' . $value . '; ';
1230 }
1231 }
1232 $colorSchemaCss .= '}';
1233
1234 return $colorSchemaCss;
1235 }
1236 }
1237