PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.7
VikBooking Hotel Booking Engine & PMS v1.6.7
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / libraries / adapter / module / widget.php

widget.php in VikBooking Hotel Booking Engine & PMS 1.6.7, at libraries/adapter/module/widget.php

1,056 lines 27.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikWP - Libraries
4 * @subpackage adapter.module
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 JLoader::import('adapter.form.form');
15
16 /**
17 * Adapter class to extend WP widget functionalities.
18 *
19 * @see WP_Widget
20 * @since 10.0
21 */
22 class JWidget extends WP_Widget
23 {
24 /**
25 * Absolute module path.
26 *
27 * @var string
28 */
29 protected $_path;
30
31 /**
32 * Internal ID.
33 *
34 * @var string
35 */
36 protected $_id;
37
38 /**
39 * Widget form data.
40 *
41 * @var JForm
42 */
43 protected $_form = null;
44
45 /**
46 * The name of the plugin that owns the module.
47 *
48 * @var string
49 */
50 protected $_option = null;
51
52 /**
53 * Whether or not the widget has been registered yet.
54 *
55 * @var boolean
56 * @since 10.1.38
57 */
58 protected $registered = false;
59
60 /**
61 * Class constructor.
62 *
63 * @param string $path The widget absolute path.
64 *
65 * @uses loadLanguage()
66 * @uses loadXml()
67 */
68 public function __construct($path)
69 {
70 // widget ID
71 $id = basename($path);
72
73 $this->_path = $path;
74 $this->_id = $id;
75
76 /**
77 * Extract component name from path.
78 *
79 * @since 10.1.20
80 */
81 if (preg_match("/plugins[\/\\\\]([a-z0-9_]+)[\/\\\\]modules/i", $this->_path, $match))
82 {
83 $this->_option = end($match);
84 }
85
86 // load text domain
87 $this->loadLanguage($path, $id);
88
89 /**
90 * @note: translations will be available only from here.
91 */
92
93 // load widget data from XML
94 $data = $this->loadXml($path, $id);
95
96 // translate widget name
97 $name = JText::translate((string) $data->name);
98
99 // build arguments
100 $args = array();
101 $args['description'] = JText::translate((string) $data->description);
102 // $args['version'] = $data->version;
103
104 /**
105 * Since the widget description is displayed by escaping HTML tags,
106 * we should strip them in order to display a plain text.
107 *
108 * @since 10.1.21
109 */
110 $args['description'] = strip_tags($args['description']);
111
112 parent::__construct($id, $name, $args);
113
114 /**
115 * Add support for jQuery in page head every time
116 * a widget is instantiated. Proceed only in case the
117 * headers haven't been sent yet.
118 *
119 * @since 10.1.22
120 */
121 if (!headers_sent())
122 {
123 add_filter('wp_enqueue_scripts', function() {
124 wp_enqueue_script('jquery', null, [], false, false);
125 });
126 }
127 }
128
129 /**
130 * Front-end display of widget.
131 *
132 * @param array $args Widget arguments.
133 * @param array $config Saved values from database.
134 *
135 * @return void
136 */
137 public function widget($args, $config)
138 {
139 // make the module helper accessible
140 JLoader::import('adapter.module.helper');
141 JModuleHelper::setPath($this->_path);
142
143 $layout = $this->_path . DIRECTORY_SEPARATOR . $this->_id . '.php';
144
145 // check if the widget owns a layout
146 if (!JFile::exists($layout))
147 {
148 return;
149 }
150
151 // include system.js file to support JoomlaCore
152 JHtml::fetch('system.js');
153
154 /**
155 * Added support for module class suffix.
156 *
157 * @since 10.1.21
158 */
159 if (!empty($config['moduleclass_sfx']))
160 {
161 // extract class from wrapper
162 if (preg_match("/class=\"([a-z0-9_\-\s]*)\"/i", $args['before_widget'], $match))
163 {
164 // replace class attribute with previous classes and the custom suffix
165 $args['before_widget'] = str_replace($match[0], 'class="' . $match[1] . ' ' . $config['moduleclass_sfx'] . '"', $args['before_widget']);
166 }
167 }
168
169 // begin widget
170 echo $args['before_widget'];
171
172 // display the title if set
173 if (!empty($config['title']))
174 {
175 echo $args['before_title'] . apply_filters('widget_title', $config['title']) . $args['after_title'];
176 }
177
178 // wrap the $config in a registry
179 $params = new JObject($config);
180
181 /**
182 * Create $module object for accessing the widget ID.
183 *
184 * @since 10.1.30
185 */
186 $module = new stdClass;
187 $module->id = $this->number;
188
189 /**
190 * Plugins can manipulate the configuration of the widget at runtime.
191 * Fires before dispatching the widget in the front-end.
192 *
193 * @param string $id The widget ID (path name).
194 * @param JObject &$params The widget configuration registry.
195 *
196 * @since 10.1.28
197 */
198 do_action_ref_array('vik_widget_before_dispatch_site', array($this->_id, &$params));
199
200 // start buffer
201 ob_start();
202 // include layout file
203 include $layout;
204 // get contents
205 $html = ob_get_contents();
206 // clear buffer
207 ob_end_clean();
208
209 /**
210 * Plugins can manipulate here the fetched HTML of the widget.
211 * Fires before displaying the HTML of the widget in the front-end.
212 *
213 * @param string $id The widget ID (path name).
214 * @param string &$html The HTML of the widget to display.
215 *
216 * @since 10.1.28
217 */
218 do_action_ref_array('vik_widget_after_dispatch_site', array($this->_id, &$html));
219
220 // display the widget HTML
221 echo $html;
222
223 // terminate widget
224 echo $args['after_widget'];
225
226 // print JSON configuration
227 JHtml::fetch('behavior.core');
228
229 // add support for Joomla JS variable
230 JFactory::getDocument()->addScriptDeclaration(
231 <<<JS
232 if (typeof Joomla === 'undefined') {
233 var Joomla = new JoomlaCore();
234 } else {
235 // reload options
236 JoomlaCore.loadOptions();
237 }
238 JS
239 );
240 }
241
242 /**
243 * Loads widget text domain.
244 *
245 * @param string $path The widget path.
246 * @param string $id The domain name.
247 *
248 * @return void
249 */
250 private function loadLanguage($path, $id)
251 {
252 // init language
253 $lang = JFactory::getLanguage();
254
255 // search for a language handler (/language/handler.php)
256 $handler = $path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'handler.php';
257
258 if (!is_file($handler))
259 {
260 /**
261 * Try also to search within "languages" folder.
262 *
263 * @since 10.1.21
264 */
265 $handler = $path . DIRECTORY_SEPARATOR . 'languages' . DIRECTORY_SEPARATOR . 'handler.php';
266 }
267
268 if (is_file($handler))
269 {
270 // attach handler
271 $lang->attachHandler($handler, $id);
272 }
273
274 /**
275 * @since 10.0.1 It is no more needed to load the language
276 * file (.mo) of the widget as all the translations
277 * are contained within the main language file
278 * of the plugin.
279 */
280 }
281
282 /**
283 * Loads widget data from the XML installation file.
284 *
285 * @param string $path The widget path.
286 * @param string $id The widget name.
287 *
288 * @return object The XML data.
289 */
290 private function loadXml($path, $id)
291 {
292 $file = $path . DIRECTORY_SEPARATOR . $id . '.xml';
293
294 // make sure the installation file exists
295 if (!is_file($file))
296 {
297 throw new Exception('Missing installation file [' . $id . '.xml].', 404);
298 }
299
300 // load form data
301 $this->_form = JForm::getInstance($id, $file, array('client' => $this->_option));
302
303 // get XML element
304 $xml = $this->_form->getXml();
305
306 $data = new stdClass;
307
308 // iterate the args and assign them to the $data object
309 foreach (array('name', 'description') as $k)
310 {
311 $data->{$k} = (string) $xml->{$k};
312 }
313
314 return $data;
315 }
316
317 /**
318 * Back-end widget form.
319 *
320 * @param array $instance Previously saved values from database.
321 *
322 * @return void
323 */
324 public function form($instance)
325 {
326 // get form fields
327 $fields = $this->_form->getFields();
328
329 /**
330 * Add support for title field by creating a custom XML field,
331 * only if the XML of the module doesn't declare it.
332 *
333 * @since 10.1.21
334 */
335 if (!$this->_form->getField('title'))
336 {
337 // create title field
338 $title = simplexml_load_string('<field name="title" type="text" default="" label="TITLE" />');
339 // push title at the beginning of the list
340 array_unshift($fields, $title);
341 }
342
343 /**
344 * Filter the fields by removing useless settings.
345 *
346 * @since 10.1.31
347 */
348 $fields = array_filter($fields, function($field)
349 {
350 // exclude field in case it starts with "loadjquery"
351 return preg_match("/^loadjquery/", (string) $field->attributes()->name) == false;
352 });
353
354 // create layout file
355 $file = new JLayoutFile('html.widget.fieldset.open');
356
357 if ($this->_option)
358 {
359 // we found an option, add an include path to make sure layouts are accessible
360 $file->addIncludePath(implode(DIRECTORY_SEPARATOR, array(WP_PLUGIN_DIR, $this->_option, 'libraries')));
361 }
362
363 // open fieldset
364 echo $file->render();
365
366 foreach ($fields as $field)
367 {
368 $attrs = $field->attributes();
369 $name = (string) $attrs->name;
370
371 $data = array();
372 $data['id'] = $this->get_field_id($name);
373 $data['label'] = (string) $attrs->label;
374 $data['description'] = (string) $attrs->description;
375 $data['name'] = $this->get_field_name($name);
376 $data['required'] = ((string) $attrs->required) === 'true';
377
378 /**
379 * Open control only in case the input shouldn't be hidden.
380 *
381 * @since 10.1.21
382 */
383 if ($attrs->type != 'hidden' && $attrs->type != 'spacer' && empty($attrs->hidden))
384 {
385 // open control
386 $file->setLayoutId('html.widget.control.open');
387 echo $file->render($data);
388 }
389
390 if (isset($instance[$name]))
391 {
392 $data['value'] = $instance[$name];
393 }
394
395 // attach module path (useful to obtain the available layouts)
396 $data['modpath'] = $this->_path;
397 $data['modowner'] = $this->_option;
398
399 // obtain field class and display input layout
400 echo $this->_form->renderField($field, $data);
401
402 /**
403 * Close control only in case the input shouldn't be hidden.
404 *
405 * @since 10.1.21
406 */
407 if ($attrs->type != 'hidden' && $attrs->type != 'spacer' && empty($attrs->hidden))
408 {
409 // close control
410 $file->setLayoutId('html.widget.control.close');
411 echo $file->render();
412 }
413 }
414
415 // close fieldset
416 $file->setLayoutId('html.widget.fieldset.close');
417 echo $file->render();
418
419 // include form scripts
420 // $this->useScript();
421 }
422
423 /**
424 * Includes the scripts used by the form.
425 *
426 * @return void
427 */
428 protected function useScript()
429 {
430 if (wp_doing_ajax())
431 {
432 return;
433 }
434
435 $document = JFactory::getDocument();
436
437 /**
438 * Include system.js file to support JFormValidator.
439 *
440 * Since WP 5.9, the widgets resources must be loaded through the
441 * _register_one method, which seems to be invoked on every page.
442 * So, we should load them only if we are under widgets.php.
443 */
444 global $pagenow;
445 if ($pagenow === 'widgets.php')
446 {
447 JHtml::fetch('system.js');
448 }
449
450 JHtml::fetch('formbehavior.chosen');
451
452 static $loaded = 0;
453
454 // load only once
455 if (!$loaded)
456 {
457 // override getLabel() method to attach invalid
458 // class to the correct form structure
459 $document->addScriptDeclaration(
460 <<<JS
461 if (typeof JFormValidator !== 'undefined') {
462 JFormValidator.prototype.getLabel = function(input) {
463 var name = jQuery(input).attr('name');
464
465 if (this.labels.hasOwnProperty(name)) {
466 return jQuery(this.labels[name]);
467 }
468
469 return jQuery(input).parent().find('label').first();
470 }
471 }
472 JS
473 );
474 }
475
476 // load form validation
477 $document->addScriptDeclaration(
478 <<<JS
479 if (typeof VIK_WIDGET_SAVE_LOOKUP === 'undefined') {
480 var VIK_WIDGET_SAVE_LOOKUP = {};
481 }
482
483 (function($) {
484 $(document).on('widget-added', function(event, control) {
485 registerWidgetScripts($(control).find('form'));
486 });
487
488 function registerWidgetScripts(form) {
489 if (!form) {
490 // if the form was not provided, find it using the widget ID (before WP 5.8)
491 form = $('div[id$="{$this->id}"] form');
492 }
493
494 if (typeof JFormValidator !== 'undefined') {
495 // init internal validator
496 var validator = new JFormValidator(form);
497
498 // validate fields every time the SAVE button is clicked
499 form.find('input[name="savewidget"]').on('click', function(event) {
500 return validator.validate();
501 });
502 }
503
504 // init select2 on dropdown with multiple selection
505 if (jQuery.fn.select2) {
506 form.find('select[multiple]').select2({
507 width: '100%'
508 });
509 }
510
511 // initialize popover within the form
512 if (jQuery.fn.popover) {
513 form.find('.inline-popover').popover({sanitize: false, container: 'body'});
514 }
515 }
516
517 $(function() {
518 // If the widget is not a template, register the scripts.
519 // A widget template ID always ends with "__i__"
520 if (!"{$this->id}".match(/__i__$/)) {
521 registerWidgetScripts();
522 }
523
524 // Attach event to the "ADD WIDGET" button
525 $('.widgets-chooser-add').on('click', function(e) {
526 // find widget parent of the clicked button
527 var parent = this.closest('div[id$="{$this->id}"]');
528
529 if (!parent) {
530 return;
531 }
532
533 // extract ID from the template parent (exclude "__i__")
534 var id = $(parent).attr('id').match(/(.*?)__i__$/);
535
536 if (!id) {
537 return;
538 }
539
540 // register scripts with a short delay to make sure the
541 // template has been moved on the right side
542 setTimeout(function() {
543 // obtain the box that has been created
544 var createdForm = $('div[id^="' + id.pop() + '"]').last();
545
546 // find form within the box
547 var _form = $(createdForm).find('form');
548
549 // register scripts at runtime
550 registerWidgetScripts(_form);
551 }, 32);
552 });
553
554 // register save callback for this kind of widget only once
555 if (!VIK_WIDGET_SAVE_LOOKUP.hasOwnProperty('{$this->_id}')) {
556 // flag as loaded
557 VIK_WIDGET_SAVE_LOOKUP['{$this->_id}'] = 1;
558
559 // Attach event to SAVE callback
560 $(document).ajaxSuccess(function(event, xhr, settings) {
561 // make sure the request was used to save the widget settings
562 if (!settings.data || typeof settings.data !== 'string' || settings.data.indexOf('action=save-widget') === -1) {
563 // wrong request
564 return;
565 }
566
567 // extract widget ID from request
568 var widget_id = settings.data.match(/widget-id=([a-z0-9_-]+)(?:&|$)/i);
569
570 // make sure this is the widget that was saved
571 if (!widget_id) {
572 // wrong widget
573 return;
574 }
575
576 // get cleansed widget ID
577 widget_id = widget_id.pop();
578
579 // make sure the widget starts with this ID
580 if (widget_id.indexOf('{$this->_id}') !== 0) {
581 // wrong widget
582 return;
583 }
584
585 // obtain the box that has been updated
586 var updatedForm = $('div[id$="' + widget_id + '"]').find('form');
587
588 // register scripts at runtime
589 registerWidgetScripts(updatedForm);
590 });
591 }
592 });
593 })(jQuery);
594 JS
595 );
596 }
597
598 /**
599 * Sanitize widget form values as they are saved.
600 *
601 * @param array $new_instance Values just sent to be saved.
602 * @param array $old_instance Previously saved values from database.
603 *
604 * @return array Updated safe values to be saved.
605 *
606 * @since 10.1.21
607 */
608 public function update($new_instance, $old_instance)
609 {
610 if (!empty($new_instance['moduleclass_sfx']))
611 {
612 // make mod class suffix safe
613 $new_instance['moduleclass_sfx'] = preg_replace("/[^a-zA-Z0-9_\-\s]+/", '', $new_instance['moduleclass_sfx']);
614 }
615
616 return $new_instance;
617 }
618
619 /**
620 * Add hooks for enqueueing assets when registering all widget instances of this widget class.
621 *
622 * @param integer $number Optional. The unique order number of this widget instance
623 * compared to other instances of the same class. Default -1.
624 *
625 * @return void
626 *
627 * @since 10.1.38
628 */
629 public function _register_one($number = -1)
630 {
631 // invoke parent
632 parent::_register_one($number);
633
634 if (!$this->registered)
635 {
636 // load required resources
637 $this->useScript();
638
639 // flag as already registered
640 $this->registered = true;
641 }
642 }
643
644 /**
645 * Converts a WordPress widget into a WordPress block.
646 * The usage of this method requires an external script with a
647 * path built as the following one:
648 * /modules/mod_[PLUGIN]_[NAME]/[PLUGIN]-[NAME]-widget-block.js
649 *
650 * @param string $blockScriptUri The relative URI of the script declaring the tools
651 * that will be actually used by the block.
652 * @param array $data The widget manifest data (@see WP_Block_Type).
653 *
654 * @return void
655 *
656 * @since 10.1.51
657 */
658 protected function registerBlockType(string $blockScriptUri, array $data)
659 {
660 /**
661 * Make sure Gutenberg is up and running to avoid
662 * any fatal errors, as the register_block_type()
663 * function may be not available on old instances.
664 */
665 if (!function_exists('register_block_type'))
666 {
667 return;
668 }
669
670 // create a block identifier for this widget
671 $block_id = preg_replace("/^mod_{$this->_option}_/", '', $this->_id);
672 $block_id = preg_replace("/_/", '-', $block_id) . '-widget-block';
673
674 // define the block manifest
675 $data = array_merge([
676 'id' => $this->_option . '/' . $block_id,
677 'title' => $this->name,
678 'description' => $this->widget_options['description'],
679 'textdomain' => $this->_option,
680 'category' => 'widgets',
681 'attributes' => [],
682 'supports' => [
683 // do not edit as HTML
684 'html' => false,
685 // use the block just once per post
686 'multiple' => true,
687 // don't allow the block to be converted into a reusable block
688 'reusable' => false,
689 ],
690 ], $data);
691
692 $form = [];
693
694 // get form fieldsets
695 foreach ($this->_form->getFieldset() as $fieldset)
696 {
697 $set = [];
698 $set['name'] = (string) $fieldset->attributes()->name;
699 $set['title'] = JText::translate((string) $fieldset->attributes()->label ?: 'COM_MENUS_' . strtoupper($set['name']) . '_FIELDSET_LABEL');
700 $set['fields'] = [];
701
702 // take only the fields that belong to this fieldset
703 $fields = $fieldset->xpath('//fieldset[@name="' . $set['name'] . '"] //field');
704
705 /**
706 * Add support for title field by creating a custom XML field,
707 * only if the XML of the module doesn't declare it.
708 *
709 * @since 10.1.21
710 */
711 if ($set['name'] === 'basic' && !$this->_form->getField('title'))
712 {
713 // create title field
714 $title = simplexml_load_string('<field name="title" type="text" default="" label="COM_MODULES_FIELD_TITLE_LABEL" description="COM_MODULES_FIELD_TITLE_DESC" />');
715 // push title at the beginning of the list
716 array_unshift($fields, $title);
717 }
718
719 // get form fields
720 foreach ($fields as $field)
721 {
722 // get form field
723 $field = JFormField::getInstance($field);
724
725 // skip the module class suffix field as it is supported by default by Gutenberg blocks
726 if ($field->name === 'moduleclass_sfx')
727 {
728 continue;
729 }
730
731 // attach module path (useful to obtain the available layouts)
732 $field->bind($this->_path, 'modpath');
733 $field->bind($this->_option, 'modowner');
734
735 // obtain field layout data
736 $displayData = array_merge(
737 [
738 'type' => $field->type,
739 'layout' => $field->layoutId,
740 'label' => JText::translate($field->label ?? ''),
741 'description' => strip_tags(JText::translate($field->description ?? '')),
742 'showon' => $field->showon,
743 ],
744 $field->getLayoutData()
745 );
746
747 // in case the field does not provide the layout, use the HTML type
748 // and render here the input data
749 if (!$field->layoutId)
750 {
751 // convert a HTML document into a JSON-compatible structure
752 $json = $this->createElementsFromHtml($field->getInput());
753
754 // overwrite type and inject the converted structure within the layout attribute
755 $displayData['type'] = 'html';
756 $displayData['layout'] = $json;
757 }
758
759 // fetch default value
760 $default = $displayData['value'] ?? '';
761 $default = ($default !== '' && $default !== null) ? $default : ($displayData['default'] ?? ($field->multiple ? [] : ''));
762
763 // normalize options structure
764 if (isset($displayData['options']) && is_array($displayData['options']))
765 {
766 /**
767 * In case the default value is not a valid option, use the first available one.
768 *
769 * @todo In case the option is not an associative array, the following condition
770 * will not work. If we want to extend this compatibility we should manually
771 * iterate the normalized array in search of an option with matching value.
772 */
773 if (is_scalar($default) && !isset($displayData['options'][$default]))
774 {
775 $default = key($displayData['options']);
776 }
777
778 $options = [];
779
780 foreach ($displayData['options'] as $value => $label)
781 {
782 if (is_object($label) || is_array($label))
783 {
784 $label = (object) $label;
785
786 $options[] = [
787 'label' => JText::translate($label->text),
788 'value' => $label->value,
789 ];
790 }
791 else
792 {
793 $options[] = [
794 'label' => JText::translate($label),
795 'value' => $value,
796 ];
797 }
798 }
799
800 $displayData['options'] = $options;
801 }
802
803 if ($field->multiple)
804 {
805 $attrType = 'array';
806 }
807 else if ($field->type === 'radio' && $field->class === 'btn-group btn-group-yesno')
808 {
809 $attrType = 'integer';
810 }
811 else
812 {
813 $attrType = 'string';
814 }
815
816 if (!empty($displayData['name']))
817 {
818 // bind field attributes
819 $data['attributes'][$displayData['name']] = [
820 'type' => $attrType,
821 'default' => $default,
822 ];
823 }
824
825 // enqueue form field
826 $set['fields'][] = $displayData;
827 }
828
829 $form[] = $set;
830 }
831
832 // register the script declaring the reusable functions for Gutenberg
833 wp_register_script(
834 $this->_option . '-gutenberg-tools',
835 $blockScriptUri . 'js/gutenberg-tools.js',
836 ['wp-blocks', 'wp-element', 'wp-i18n'],
837 constant(strtoupper($this->_option . '_software_version'))
838 );
839
840 // register the script that contains all the JS functions used
841 // to implement a new block for Gutenberg editor
842 wp_register_script(
843 $this->_option . '-gutenberg-widgets',
844 $blockScriptUri . 'js/gutenberg-widgets.js',
845 ['wp-blocks', 'wp-element', 'wp-i18n'],
846 constant(strtoupper($this->_option . '_software_version'))
847 );
848
849 // register the script that will be used to support this widget
850 // as Gutenberg block editor
851 wp_register_script(
852 $this->_option . '-' . $block_id,
853 plugin_dir_url($this->_path) . $this->_id . '/' . $this->_option . '-' . $block_id . '.js',
854 ['wp-blocks', 'wp-element', 'wp-i18n'],
855 constant(strtoupper($this->_option . '_software_version'))
856 );
857
858 // Pass the manifest data to the script previously loaded.
859 // The object variable will be named as:
860 // MOD_[PLUGIN]_[NAME]_BLOCK_DATA
861 wp_localize_script(
862 $this->_option . '-' . $block_id,
863 strtoupper($this->_id . '_block_data'),
864 array_merge(
865 $data,
866 [
867 'form' => $form,
868 ]
869 )
870 );
871
872 // create a new block type, which must provide the scripts previously loaded
873 register_block_type($this->_option . '/' . $block_id, array_merge($data, [
874 'render_callback' => function($config) {
875 // prepare widget arguments
876 $args = [
877 'before_widget' => '<div class="widget widget_' . $this->_id . '" id="' . $this->_id . '_' . $this->number . '">',
878 'before_title' => '<h3 class="widget-title">',
879 'after_title' => '</h3>',
880 'after_widget' => '</div>',
881 ];
882
883 // adjust widget configuration
884 $config['moduleclass_sfx'] = $config['className'] ?? '';
885
886 $requestUri = JFactory::getApplication()->input->server->getString('REQUEST_URI', '');
887
888 $is_rest_api = strpos($requestUri, trailingslashit(rest_get_url_prefix())) !== false
889 || JUri::getInstance($requestUri)->hasVar('rest_route');
890
891 // define a callback to include a placeholder in case the widget is not able to render any contents
892 $previewPlaceholderCallback = function($id, &$html) {
893 // get rid of any script and style declared by the widget layout
894 $test = preg_replace("/<script(?:.*?)>(?:.*?)<\/script>/s", '', $html);
895 $test = preg_replace("/<style(?:.*?)>(?:.*?)<\/style>/s", '', $test);
896
897 // check whether the test var contains some texts
898 if (!trim(strip_tags($test))) {
899 $html = '<div style="padding: 10px; background: #eee; border: 2px solid #ddd;">'
900 . JText::translate('COM_MODULES_PREVIEW_NOT_AVAIL')
901 . '</div>';
902 }
903 };
904
905 // start buffer
906 ob_start();
907
908 if ($is_rest_api || is_admin())
909 {
910 // overwrite the callback to register an asset declaration at runtime
911 JFactory::getDocument()->attachToHeadCustomCallback = function($callback) {
912 // prevent the system document from displaying the asset
913 };
914
915 // register a callback to display a placeholder when the widget contents are empty
916 add_action('vik_widget_after_dispatch_site', $previewPlaceholderCallback, 10, 2);
917 }
918
919 // render the widget for the front-end
920 $this->widget($args, $config);
921
922 // if we are under a REST API, the block is probably
923 // requesting a server-side rendering of the widget
924 if ($is_rest_api)
925 {
926 // force WordPress to include the styles and the scripts
927 // within the rendered HTML
928 wp_print_styles();
929
930 // DO NOT print the scripts to prevent JS errors
931 // wp_print_head_scripts();
932 }
933
934 // get contents
935 $html = ob_get_contents();
936 // clear buffer
937 ob_end_clean();
938
939 if ($is_rest_api || is_admin())
940 {
941 // get rid of any script declared by the widget layout
942 $html = preg_replace("/<script(?:.*?)>(?:.*?)<\/script>/s", '', $html);
943
944 // restore the original callback used to register the assets
945 JFactory::getDocument()->attachToHeadCustomCallback = null;
946
947 // unregister the callback used to display a placeholder when the widget contents are empty
948 remove_action('vik_widget_after_dispatch_site', $previewPlaceholderCallback);
949 }
950
951 return $html;
952 },
953 'editor_script_handles' => [
954 $this->_option . '-gutenberg-tools',
955 $this->_option . '-gutenberg-widgets',
956 $this->_option . '-' . $block_id,
957 ],
958 ]));
959 }
960
961 /**
962 * Converts an HTML string into a JSON-compatible structure.
963 *
964 * @param string $html The HTML to convert.
965 *
966 * @return object[] A list of nodes.
967 *
968 * @since 10.1.51
969 */
970 private function createElementsFromHtml(string $html)
971 {
972 // wrap the HTML into a dom document
973 $dom = new DOMDocument;
974 $dom->loadHTML($html);
975
976 // set up root
977 $root = new stdClass;
978 $root->tag = 'html';
979 $root->children = [];
980
981 // recursively extract the nodes from the document
982 $this->extractHtmlElements($dom, $root);
983
984 // take only the children of the root ("html")
985 return $root->children;
986 }
987
988 /**
989 * Extracts the HTML tags from the provided node.
990 *
991 * @param DOMNode $domNode The current DOM node to scan.
992 * @param object &$parent When the extracted tags should be attached.
993 *
994 * @return void
995 *
996 * @since 10.1.51
997 */
998 private function extractHtmlElements(DOMNode $domNode, &$parent)
999 {
1000 foreach ($domNode->childNodes as $node)
1001 {
1002 $tag = $parent;
1003
1004 if (!in_array($node->nodeName, ['html', 'body']))
1005 {
1006 $tag = new stdClass;
1007 $tag->tag = $node->nodeName;
1008 $tag->children = [];
1009
1010 if ($node->hasAttributes())
1011 {
1012 $tag->attributes = [];
1013
1014 foreach ($node->attributes as $attr)
1015 {
1016 if ($attr->nodeName === 'style')
1017 {
1018 $tag->attributes['style'] = [];
1019
1020 // convert style string into an associative array
1021 if (preg_match_all("/([a-z0-9-]+)\s*:\s*([^;]+);/i", (string) $attr->nodeValue, $matches))
1022 {
1023 for ($i = 0; $i < count($matches[0]); $i++)
1024 {
1025 $propertyName = $matches[1][$i];
1026 $propertyValue = $matches[2][$i];
1027
1028 $tag->attributes['style'][$propertyName] = $propertyValue;
1029 }
1030 }
1031 }
1032 else
1033 {
1034 $tag->attributes[$attr->nodeName] = $attr->nodeValue;
1035 }
1036 }
1037 }
1038
1039 if ($node->nodeName === '#text')
1040 {
1041 $parent->content = trim((string) $node->nodeValue);
1042 }
1043 else
1044 {
1045 $parent->children[] = $tag;
1046 }
1047 }
1048
1049 if ($node->hasChildNodes())
1050 {
1051 $this->extractHtmlElements($node, $tag);
1052 }
1053 }
1054 }
1055 }
1056