PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / libraries / adapter / module / widget.php

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

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