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 +481 -23 1.6.4 → 1.8.16 View file →
@@ -2,9 +2,9 @@
2 2 /**
3 3 * @package VikWP - Libraries
4 4 * @subpackage adapter.module
5 5 * @author E4J s.r.l.
6 - * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
6 + * @copyright Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
7 7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 8 * @link https://vikwp.com
9 9 */
10 10
@@ -15,10 +15,10 @@
15 15
16 16 /**
17 17 * Adapter class to extend WP widget functionalities.
18 18 *
19 - * @see WP_Widget
20 - * @since 10.0
19 + * @see WP_Widget
20 + * @since 10.0
21 21 */
22 22 class JWidget extends WP_Widget
23 23 {
24 24 /**
@@ -57,8 +57,18 @@
57 57 */
58 58 protected $registered = false;
59 59
60 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 + /**
61 71 * Class constructor.
62 72 *
63 73 * @param string $path The widget absolute path.
64 74 *
@@ -119,11 +129,10 @@
119 129 * @since 10.1.22
120 130 */
121 131 if (!headers_sent())
122 132 {
123 - add_filter('wp_enqueue_scripts', function()
124 - {
125 - wp_enqueue_script('jquery', null, array(), false, false);
133 + add_filter('wp_enqueue_scripts', function() {
134 + wp_enqueue_script('jquery', null, [], false, false);
126 135 });
127 136 }
128 137 }
129 138
@@ -140,18 +149,33 @@
140 149 // make the module helper accessible
141 150 JLoader::import('adapter.module.helper');
142 151 JModuleHelper::setPath($this->_path);
143 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 +
144 161 /**
145 - * Include system.js file to support JFormValidator.
146 - *
147 - * @todo Check whether this callback is actually needed.
148 - * The comment below mentions about the support for
149 - * the validator, but it shouldn't be used in the
150 - * front-end. In case the system.js file is required
151 - * we could consider to load Bootstrap only in case
152 - * we are in the site section.
162 + * If we are under a block preview, prefer a different layout.
163 + *
164 + * @since 10.1.71
153 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
154 178 JHtml::fetch('system.js');
155 179
156 180 /**
157 181 * Added support for module class suffix.
@@ -176,16 +200,8 @@
176 200 {
177 201 echo $args['before_title'] . apply_filters('widget_title', $config['title']) . $args['after_title'];
178 202 }
179 203
180 - $layout = $this->_path . DIRECTORY_SEPARATOR . $this->_id . '.php';
181 -
182 - // check if the widget owns a layout
183 - if (!is_file($layout))
184 - {
185 - return;
186 - }
187 -
188 204 // wrap the $config in a registry
189 205 $params = new JObject($config);
190 206
191 207 /**
@@ -193,9 +209,9 @@
193 209 *
194 210 * @since 10.1.30
195 211 */
196 212 $module = new stdClass;
197 - $module->id = $this->number;
213 + $module->id = ++static::$incrementalCounter;
198 214
199 215 /**
200 216 * Plugins can manipulate the configuration of the widget at runtime.
201 217 * Fires before dispatching the widget in the front-end.
@@ -648,6 +664,448 @@
648 664
649 665 // flag as already registered
650 666 $this->registered = true;
651 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;
652 1110 }
653 1111 }