PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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
← All changes | libraries/adapter/module/widget.php +1111 -0 1.6.0 → 1.8.16 View file →
@@ -1,0 +1,1111 @@
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 +}