PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / libraries / adapter / module / widget.php
vikappointments / libraries / adapter / module Last commit date
factory.php 2 days ago helper.php 2 days ago widget.php 2 days ago
widget.php
1112 lines
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