PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
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 1.8.6, at libraries/adapter/module/widget.php

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