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 / templates / widgets / class-widgets-ajax.php

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

303 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MasterAddons\Inc\Admin\Templates\Widgets;
4
5 use MasterAddons\Inc\Classes\Helper;
6
7 defined('ABSPATH') || exit;
8
9 /**
10 * AJAX endpoints backing the Widgets Library screen.
11 */
12 class Widgets_Ajax
13 {
14 const NONCE_ACTION = 'jltma_widgets_library_nonce_action';
15 const CACHE_KEY = 'jltma_widgets_library_cache';
16 const CACHE_TTL = 12 * HOUR_IN_SECONDS;
17 const PER_PAGE = 12;
18
19 private static $_instance = null;
20
21 public function __construct()
22 {
23 add_action('wp_ajax_jltma_get_widgets_library', [$this, 'get_widgets']);
24 add_action('wp_ajax_jltma_get_widget_categories', [$this, 'get_categories']);
25 add_action('wp_ajax_jltma_download_widget', [$this, 'download_widget']);
26 add_action('wp_ajax_jltma_refresh_widgets_cache', [$this, 'refresh_cache']);
27 }
28
29 /**
30 * Shared guard. Dies with a JSON error when the request is not trusted.
31 */
32 private function guard()
33 {
34 $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- collecting nonce for immediate verification
35
36 if (!wp_verify_nonce($nonce, self::NONCE_ACTION) || !current_user_can('manage_options')) {
37 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
38 }
39 }
40
41 /**
42 * Base URL of the remote catalog, reusing the Template Library config.
43 */
44 private function api_base()
45 {
46 $config = null;
47
48 if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) {
49 $templates = \MasterAddons\Inc\Admin\Templates\master_addons_templates();
50 if ($templates && isset($templates->config)) {
51 $config = $templates->config->get('api');
52 }
53 }
54
55 if (empty($config['base']) || empty($config['path'])) {
56 return '';
57 }
58
59 /**
60 * Filter the Widgets Library API base URL.
61 *
62 * Lets a staging or development site point at a non-production
63 * catalog without editing the shared template config.
64 *
65 * @param string $base Fully qualified base, no trailing slash.
66 */
67 return apply_filters('jltma_widgets_library_api_base', $config['base'] . $config['path']);
68 }
69
70 /**
71 * Fetch the catalog, cached in a transient.
72 *
73 * @param bool $force_refresh
74 * @return array|\WP_Error
75 */
76 private function fetch_catalog($force_refresh = false)
77 {
78 if (!$force_refresh) {
79 $cached = get_transient(self::CACHE_KEY);
80 if (is_array($cached)) {
81 return $cached;
82 }
83 }
84
85 $base = $this->api_base();
86 if (!$base) {
87 return new \WP_Error('no_config', __('Template API is not configured.', 'master-addons'));
88 }
89
90 $response = wp_remote_get($base . '/widgets', [
91 'timeout' => 15,
92 'sslverify' => false,
93 'headers' => ['User-Agent' => 'Master Addons Widgets Library/' . JLTMA_VER],
94 ]);
95
96 if (is_wp_error($response)) {
97 return $response;
98 }
99
100 $body = json_decode(wp_remote_retrieve_body($response), true);
101
102 if (json_last_error() !== JSON_ERROR_NONE || empty($body['success']) || !isset($body['widgets'])) {
103 return new \WP_Error('bad_response', __('Could not read the widgets library.', 'master-addons'));
104 }
105
106 set_transient(self::CACHE_KEY, $body['widgets'], self::CACHE_TTL);
107
108 return $body['widgets'];
109 }
110
111 public function get_widgets()
112 {
113 $this->guard();
114
115 $category = isset($_POST['category']) ? sanitize_text_field(wp_unslash($_POST['category'])) : 'all';
116 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
117 $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
118 $force = isset($_POST['force_refresh']) && 'true' === $_POST['force_refresh'];
119
120 $widgets = $this->fetch_catalog($force);
121
122 if (is_wp_error($widgets)) {
123 wp_send_json_error(['message' => $widgets->get_error_message()]);
124 }
125
126 if ('all' !== $category) {
127 $widgets = array_values(array_filter($widgets, function ($widget) use ($category) {
128 return !empty($widget['categories']) && in_array($category, $widget['categories'], true);
129 }));
130 }
131
132 if ('' !== $search) {
133 $needle = strtolower($search);
134 $widgets = array_values(array_filter($widgets, function ($widget) use ($needle) {
135 $haystack = strtolower(
136 $widget['title'] . ' ' .
137 implode(' ', (array) ($widget['keywords'] ?? [])) . ' ' .
138 implode(' ', (array) ($widget['categories'] ?? []))
139 );
140 return false !== strpos($haystack, $needle);
141 }));
142 }
143
144 $total = count($widgets);
145 $offset = ($page - 1) * self::PER_PAGE;
146
147 // The grid reuses the Template Library shape, so each item is exposed
148 // under the keys that component already reads.
149 $templates = array_map(function ($widget) {
150 return [
151 'template_id' => $widget['widget_id'],
152 'title' => $widget['title'],
153 'thumbnail' => $widget['thumbnail'],
154 'preview' => $widget['preview'],
155 // Catalog entries are Elementor documents, so this is the live
156 // preview permalink when one has been laid out. Older payloads
157 // without the key fall back to the thumbnail.
158 'preview_url' => !empty($widget['preview_url']) ? $widget['preview_url'] : $widget['preview'],
159 'url' => $widget['url'],
160 'icon' => $widget['icon'],
161 'is_pro' => !empty($widget['pro']),
162 'purchasable' => false,
163 'categories' => $widget['categories'],
164 'keywords' => $widget['keywords'],
165 'notice' => $widget['notice'],
166 ];
167 }, array_slice($widgets, $offset, self::PER_PAGE));
168
169 wp_send_json_success([
170 'templates' => $templates,
171 'pagination' => [
172 'current_page' => $page,
173 'per_page' => self::PER_PAGE,
174 'total_items' => $total,
175 'total_pages' => (int) ceil($total / self::PER_PAGE),
176 'has_more' => ($offset + self::PER_PAGE) < $total,
177 ],
178 ]);
179 }
180
181 public function get_categories()
182 {
183 $this->guard();
184
185 $widgets = $this->fetch_catalog(false);
186
187 if (is_wp_error($widgets)) {
188 wp_send_json_success([]);
189 }
190
191 // Counts are derived from the widgets actually on offer, not from the
192 // remote taxonomy — stale terms with no live widget never show up.
193 $counts = [];
194 foreach ($widgets as $widget) {
195 foreach ((array) ($widget['categories'] ?? []) as $slug) {
196 $counts[$slug] = isset($counts[$slug]) ? $counts[$slug] + 1 : 1;
197 }
198 }
199
200 // id/name are what the shared CategorySidebar reads; slug/title are kept
201 // so the shape stays readable on its own terms.
202 $categories = [[
203 'id' => 'all',
204 'slug' => 'all',
205 'name' => __('ALL', 'master-addons'),
206 'title' => __('ALL', 'master-addons'),
207 'count' => count($widgets),
208 ]];
209
210 foreach ($counts as $slug => $count) {
211 $label = ucwords(str_replace('-', ' ', $slug));
212
213 $categories[] = [
214 'id' => $slug,
215 'slug' => $slug,
216 'name' => $label,
217 'title' => $label,
218 'count' => $count,
219 ];
220 }
221
222 wp_send_json_success($categories);
223 }
224
225 public function download_widget()
226 {
227 $this->guard();
228
229 $widget_id = isset($_POST['template_id']) ? absint($_POST['template_id']) : 0;
230
231 if (!$widget_id) {
232 wp_send_json_error(['message' => __('No widget specified.', 'master-addons')]);
233 }
234
235 $base = $this->api_base();
236 if (!$base) {
237 wp_send_json_error(['message' => __('Template API is not configured.', 'master-addons')]);
238 }
239
240 $response = wp_remote_get($base . '/widget/' . $widget_id, [
241 'timeout' => 30,
242 'sslverify' => false,
243 'headers' => ['User-Agent' => 'Master Addons Widgets Library/' . JLTMA_VER],
244 ]);
245
246 if (is_wp_error($response)) {
247 wp_send_json_error(['message' => $response->get_error_message()]);
248 }
249
250 if (404 === wp_remote_retrieve_response_code($response)) {
251 wp_send_json_error(['message' => __('This widget is no longer available.', 'master-addons')]);
252 }
253
254 $payload = json_decode(wp_remote_retrieve_body($response), true);
255
256 if (json_last_error() !== JSON_ERROR_NONE) {
257 wp_send_json_error(['message' => __('Could not read the widget from the server.', 'master-addons')]);
258 }
259
260 // Defense in depth: the grid already swaps in an Upgrade button.
261 if (!empty($payload['pro']) && !Helper::jltma_premium()) {
262 wp_send_json_error([
263 'message' => __('This widget requires Master Addons PRO.', 'master-addons'),
264 'upgrade_url' => 'https://master-addons.com/pricing',
265 ], 403);
266 }
267
268 $post_id = Widgets_Downloader::install($payload);
269
270 if (is_wp_error($post_id)) {
271 wp_send_json_error(['message' => $post_id->get_error_message()]);
272 }
273
274 wp_send_json_success([
275 'message' => __('Widget imported successfully!', 'master-addons'),
276 'widget_id' => $post_id,
277 'edit_url' => admin_url('post.php?post=' . $post_id . '&action=edit'),
278 ]);
279 }
280
281 public function refresh_cache()
282 {
283 $this->guard();
284
285 delete_transient(self::CACHE_KEY);
286 $widgets = $this->fetch_catalog(true);
287
288 if (is_wp_error($widgets)) {
289 wp_send_json_error(['message' => $widgets->get_error_message()]);
290 }
291
292 wp_send_json_success(['count' => count($widgets)]);
293 }
294
295 public static function get_instance()
296 {
297 if (is_null(self::$_instance)) {
298 self::$_instance = new self();
299 }
300 return self::$_instance;
301 }
302 }
303