PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.9
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / admin / widget-builder / class-widget-builder-init.php

class-widget-builder-init.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.9, at inc/admin/widget-builder/class-widget-builder-init.php

408 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Master Addons Widget Builder Initialization
4 *
5 * @package MasterAddons
6 * @subpackage WidgetBuilder
7 */
8
9 namespace MasterAddons\Inc\Admin\WidgetBuilder;
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 class Widget_Builder_Init {
16
17 private static $instance = null;
18
19 public static function get_instance() {
20 if (is_null(self::$instance)) {
21 self::$instance = new self();
22 }
23 return self::$instance;
24 }
25
26 private function __construct() {
27 add_action('init', [$this, 'initialize'], 1);
28 add_action('admin_init', [$this, 'admin_redirects']);
29 add_action('admin_init', [$this, 'maybe_migrate']);
30 }
31
32 public function initialize() {
33 Widget_CPT::get_instance();
34 Widget_Admin::get_instance();
35
36 // Initialize REST API
37 add_action('rest_api_init', function() {
38 $controller = new REST_Controller();
39 $controller->register_routes();
40 });
41
42 // Initialize Shortcode Manager
43 Shortcode_Manager::get_instance();
44
45 // Register custom widgets with Elementor
46 add_action('elementor/widgets/register', [$this, 'register_custom_widgets']);
47 }
48
49 /** Handle prefix for the per-instance scripts carrying widget custom JS. */
50 const INLINE_JS_HANDLE = 'jltma-widget-builder-inline';
51
52 /**
53 * Normalise an includes payload to the canonical shape, dropping entries
54 * that could not be enqueued anyway.
55 *
56 * Every consumer used to re-implement its own subset of these checks, so a
57 * library with a relative src was skipped by the Elementor widget but still
58 * enqueued by the shortcode. One shape, validated once.
59 *
60 * @param mixed $includes Raw includes payload.
61 * @return array {
62 * @type array $css_libraries List of ['handle' => string, 'src' => string, 'dependencies' => string[]].
63 * @type array $js_libraries Same shape.
64 * }
65 */
66 public static function normalize_includes($includes) {
67 $normalized = ['css_libraries' => [], 'js_libraries' => []];
68
69 if (!is_array($includes)) {
70 return $normalized;
71 }
72
73 foreach (array_keys($normalized) as $group) {
74 if (empty($includes[$group]) || !is_array($includes[$group])) {
75 continue;
76 }
77
78 foreach ($includes[$group] as $lib) {
79 if (!is_array($lib) || empty($lib['handle']) || empty($lib['src'])) {
80 continue;
81 }
82
83 // Only absolute URLs: these become wp_enqueue_* sources.
84 if (!filter_var($lib['src'], FILTER_VALIDATE_URL)) {
85 continue;
86 }
87
88 $deps = (!empty($lib['dependencies']) && is_array($lib['dependencies']))
89 ? array_values(array_map('sanitize_text_field', $lib['dependencies']))
90 : [];
91
92 $normalized[$group][] = [
93 'handle' => sanitize_text_field($lib['handle']),
94 'src' => esc_url_raw($lib['src']),
95 'dependencies' => $deps,
96 ];
97 }
98 }
99
100 return $normalized;
101 }
102
103 /**
104 * The external CSS/JS libraries a widget may load, after the premium gate.
105 *
106 * Declaring external libraries is a premium capability. The free build
107 * resolves to an empty set; the Pro build returns the stored libraries by
108 * filtering `master_addons/widget_builder/render_includes` — see
109 * MasterAddons\Pro\Classes\Pro_Modules. The stored value is passed as the
110 * second argument, already normalised.
111 *
112 * @param int $widget_id Widget post ID.
113 * @return array Normalised includes.
114 */
115 public static function get_widget_includes($widget_id) {
116 $stored = self::normalize_includes(get_post_meta($widget_id, '_jltma_widget_includes', true));
117
118 $includes = apply_filters(
119 'master_addons/widget_builder/render_includes',
120 self::normalize_includes(null),
121 $stored,
122 $widget_id
123 );
124
125 return self::normalize_includes($includes);
126 }
127
128 /**
129 * Are we rendering for the Elementor editor (canvas, preview iframe, or an
130 * editor ajax round-trip) rather than for a visitor?
131 *
132 * @return bool
133 */
134 public static function is_editor_context() {
135 if (!class_exists('\Elementor\Plugin')) {
136 return is_admin();
137 }
138
139 $elementor = \Elementor\Plugin::$instance;
140
141 if (is_admin() || (isset($elementor->editor) && $elementor->editor->is_edit_mode())) {
142 return true;
143 }
144
145 return isset($elementor->preview) && $elementor->preview->is_preview_mode();
146 }
147
148 /**
149 * Queue a widget's rendered custom JS for the footer.
150 *
151 * Widget markup is emitted from inside the `the_content` filter chain, and
152 * core's convert_chars() rewrites every bare `&` in that output to `&#038;`
153 * without skipping <script> blocks — so an inline <script> holding `a && b`
154 * reaches the browser as `a &#038;&#038; b` and dies with a SyntaxError.
155 * (Elementor drops wpautop around builder content, but not convert_chars.)
156 *
157 * Routing the JS through the script queue prints it after wp_footer, well
158 * clear of the content filters, and keeps execution after the markup exists.
159 *
160 * Every rendered instance gets its own handle, so it gets its own <script>
161 * tag. That isolation is the point: sharing one handle concatenates every
162 * widget on the page into a single block, where one widget throwing — a
163 * premium-only library that is not loaded, say — aborts the block and takes
164 * every later widget's JS down with it.
165 *
166 * @param string $js Rendered JS body (no <script> wrapper).
167 * @param int $widget_id Widget post ID, used to label the handle.
168 */
169 public static function enqueue_inline_js($js, $widget_id = 0) {
170 if (!is_string($js) || '' === trim($js)) {
171 return;
172 }
173
174 static $instance = 0;
175 $instance++;
176
177 $handle = self::INLINE_JS_HANDLE . '-' . absint($widget_id) . '-' . $instance;
178 $ver = defined('JLTMA_VER') ? JLTMA_VER : false;
179
180 wp_register_script($handle, false, [], $ver, true);
181 wp_enqueue_script($handle);
182 wp_add_inline_script($handle, $js);
183 }
184
185 /**
186 * Register custom widgets from CPT with Elementor
187 */
188 public function register_custom_widgets($widgets_manager) {
189 // Register custom categories first
190 $this->register_custom_categories();
191
192 // Get all published widgets
193 $args = [
194 'post_type' => 'jltma_widget',
195 'post_status' => 'publish',
196 'posts_per_page' => -1,
197 'orderby' => 'date',
198 'order' => 'DESC'
199 ];
200
201 $widgets = get_posts($args);
202
203 if (empty($widgets)) {
204 return;
205 }
206
207 // Widgets are rendered at runtime by Dynamic_Widget — no generated or
208 // executed PHP. (No user input is ever written to or required as PHP.)
209 require_once __DIR__ . '/class-dynamic-widget.php';
210
211 foreach ($widgets as $widget_post) {
212 $widgets_manager->register(new Dynamic_Widget([], ['jltma_post_id' => $widget_post->ID]));
213 }
214 }
215
216 /**
217 * Register custom Elementor categories
218 */
219 private function register_custom_categories() {
220 if (!did_action('elementor/loaded')) {
221 return;
222 }
223
224 // Get custom categories from options
225 $custom_categories = get_option('jltma_custom_widget_categories', []);
226
227 if (empty($custom_categories) || !is_array($custom_categories)) {
228 return;
229 }
230
231 $elements_manager = \Elementor\Plugin::$instance->elements_manager;
232
233 // Register each custom category
234 foreach ($custom_categories as $slug => $title) {
235 // Check if category doesn't already exist
236 $existing_categories = $elements_manager->get_categories();
237
238 if (!isset($existing_categories[$slug])) {
239 $elements_manager->add_category(
240 $slug,
241 [
242 'title' => $title,
243 'icon' => 'eicon-posts-ticker',
244 ]
245 );
246 }
247 }
248 }
249
250 public function admin_redirects() {
251 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only checks of WordPress-set admin query vars for redirect flow; no form data is processed.
252 global $pagenow;
253 $target_post_type = 'jltma_widget';
254 $redirect_url = admin_url('edit.php?post_type=jltma_widget');
255
256 if ('post.php' === $pagenow && isset($_GET['post'])) {
257 if (isset($_GET['action']) && in_array($_GET['action'], ['elementor', 'trash', 'delete', 'restore', 'untrash'], true)) {
258 return;
259 }
260 $post_id = absint($_GET['post']);
261 if ($post_id && $target_post_type === get_post_type($post_id)) {
262 wp_safe_redirect($redirect_url);
263 exit;
264 }
265 }
266
267 if ('post-new.php' === $pagenow && isset($_GET['post_type'])) {
268 $current_post_type = sanitize_key($_GET['post_type']);
269 if ($target_post_type === $current_post_type) {
270 wp_safe_redirect($redirect_url);
271 exit;
272 }
273 }
274 // phpcs:enable WordPress.Security.NonceVerification.Recommended
275 }
276
277 /**
278 * One-time migration. Re-sanitizes stored widget code (strips any PHP/script that
279 * older versions may have persisted), purges stale generated files from current and
280 * legacy locations, and regenerates every widget from the now-clean data. Existing
281 * widget posts are never deleted — only their data is scrubbed and files rebuilt.
282 * Runs once per version (gated by an option) inside an authorised admin request.
283 */
284 public function maybe_migrate() {
285 $option_key = 'jltma_widget_builder_migrated';
286 $version = '3.1.2-runtime';
287
288 if (get_option($option_key) === $version) {
289 return;
290 }
291
292 if (!current_user_can('manage_options')) {
293 return;
294 }
295
296 // 1) Remove stale generated trees (current + legacy locations).
297 $upload = wp_upload_dir();
298 $base = trailingslashit($upload['basedir']);
299 $this->delete_directory($base . 'master_addons/widgets');
300 $this->delete_directory($base . 'master-addons/widget-builder/generated');
301 $this->delete_directory($base . 'master-addons/widget-builder/tmp');
302
303 // 2) Scrub persisted widget data, then regenerate files from clean data.
304 $widget_ids = get_posts([
305 'post_type' => 'jltma_widget',
306 'post_status' => 'any',
307 'posts_per_page' => -1,
308 'fields' => 'ids',
309 ]);
310
311 foreach ($widget_ids as $widget_id) {
312 $data = get_post_meta($widget_id, '_jltma_widget_data', true);
313 if (is_array($data)) {
314 if (isset($data['html_code'])) {
315 $data['html_code'] = $this->scrub_html($data['html_code']);
316 }
317 if (isset($data['css_code'])) {
318 $data['css_code'] = $this->scrub_css($data['css_code']);
319 }
320 if (isset($data['js_code'])) {
321 $data['js_code'] = $this->scrub_js($data['js_code']);
322 }
323 update_post_meta($widget_id, '_jltma_widget_data', $data);
324 }
325
326 $generator = new Widget_Generator($widget_id);
327 $generator->generate();
328 }
329
330 // 3) Drop the orphaned option left by the old option-based builder.
331 delete_option('jltma_custom_widgets');
332
333 update_option($option_key, $version);
334 }
335
336 /**
337 * Strip PHP tags and inline <script> from widget HTML.
338 *
339 * @param string $code
340 * @return string
341 */
342 private function scrub_html($code) {
343 if (!is_string($code) || '' === $code) {
344 return '';
345 }
346 $code = str_replace(chr(0), '', $code);
347 $code = preg_replace('/<\?php/i', '', $code);
348 $code = str_replace(['<?=', '<?', '?>'], '', $code);
349 $code = preg_replace('#<script\b[^>]*>.*?</script>#is', '', $code);
350 $code = preg_replace('#</?script\b[^>]*>#i', '', $code);
351 return $code;
352 }
353
354 /**
355 * Strip PHP tags and <style>/<script> tags from widget CSS.
356 *
357 * @param string $code
358 * @return string
359 */
360 private function scrub_css($code) {
361 if (!is_string($code) || '' === $code) {
362 return '';
363 }
364 $code = str_replace(chr(0), '', $code);
365 $code = preg_replace('/<\?php/i', '', $code);
366 $code = str_replace(['<?=', '<?', '?>'], '', $code);
367 $code = preg_replace('#</?style\b[^>]*>#i', '', $code);
368 $code = preg_replace('#</?script\b[^>]*>#i', '', $code);
369 return $code;
370 }
371
372 /**
373 * Strip PHP tags and <script> tags from widget JS.
374 *
375 * @param string $code
376 * @return string
377 */
378 private function scrub_js($code) {
379 if (!is_string($code) || '' === $code) {
380 return '';
381 }
382 $code = str_replace(chr(0), '', $code);
383 $code = preg_replace('/<\?php/i', '', $code);
384 $code = str_replace(['<?=', '<?', '?>'], '', $code);
385 $code = preg_replace('#</?script\b[^>]*>#i', '', $code);
386 return $code;
387 }
388
389 /**
390 * Recursively delete a directory via WP_Filesystem.
391 *
392 * @param string $dir
393 */
394 private function delete_directory($dir) {
395 if (empty($dir) || !is_dir($dir)) {
396 return;
397 }
398 global $wp_filesystem;
399 if (empty($wp_filesystem)) {
400 require_once ABSPATH . 'wp-admin/includes/file.php';
401 WP_Filesystem();
402 }
403 if (!empty($wp_filesystem)) {
404 $wp_filesystem->delete($dir, true);
405 }
406 }
407 }
408