PluginProbe
Vision – Interactive Image Map with Hotspots Builder / 1.12.0
Vision – Interactive Image Map with Hotspots Builder v1.12.0
1.12.2 1.12.1 1.12.0 1.11.0 1.6.0 1.6.1 1.6.2 1.7.1 1.7.2 1.7.3 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 All 33 releases
vision / includes / plugin.php

plugin.php in Vision – Interactive Image Map with Hotspots Builder 1.12.0, at includes/plugin.php

1,893 lines 72.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') || exit;
3
4 class Vision_Builder
5 {
6 private $pluginBasename = NULL;
7
8 private $ajax_action_item_save_chunk = NULL;
9 private $ajax_action_item_save_complete = NULL;
10 private $ajax_action_item_update = NULL;
11 private $ajax_action_item_update_status = NULL;
12 private $ajax_action_settings_update = NULL;
13 private $ajax_action_settings_get = NULL;
14 private $ajax_action_delete_data = NULL;
15 private $ajax_action_modal = NULL;
16 private $ajax_action_change_author = NULL;
17 private $ajax_action_picpoints_promo = NULL;
18
19 private $vision_map_id = null;
20 private $vision_map_version = null;
21 private $shortcodes = [];
22
23 function __construct($pluginBasename)
24 {
25 $this->pluginBasename = $pluginBasename;
26
27 add_action('clean_old_chunks', [$this, 'clean_old_chunks']);
28 }
29
30 function run()
31 {
32 $upload_dir = wp_upload_dir();
33 $plugin_url = plugin_dir_url(dirname(__FILE__));
34
35 define('VISION_PLUGIN_UPLOAD_DIR', wp_normalize_path($upload_dir['basedir'] . '/vision'));
36 define('VISION_PLUGIN_UPLOAD_URL', set_url_scheme($upload_dir['baseurl'] . '/vision/'));
37
38 define('VISION_PLUGIN_PLAN', 'lite');
39
40 $user = wp_get_current_user(); //is_super_admin()
41 $allowed_roles = $this->getAllowedRoles();
42 if ((array_intersect($allowed_roles, $user->roles) || current_user_can('manage_options')) && is_admin()) {
43 $this->ajax_action_item_save_chunk = 'vision_ajax_item_save_chunk';
44 $this->ajax_action_item_save_complete = 'vision_ajax_item_save_complete';
45 $this->ajax_action_item_update = 'vision_ajax_item_update';
46 $this->ajax_action_item_update_status = 'vision_ajax_item_update_status';
47 $this->ajax_action_settings_update = 'vision_ajax_settings_update';
48 $this->ajax_action_settings_get = 'vision_ajax_settings_get';
49 $this->ajax_action_delete_data = 'vision_ajax_delete_data';
50 $this->ajax_action_modal = 'vision_ajax_modal';
51 $this->ajax_action_change_author = 'vision_ajax_change_author';
52 $this->ajax_action_picpoints_promo = 'vision_ajax_picpoints_promo';
53
54 load_plugin_textdomain('vision', false, dirname(dirname(plugin_basename(__FILE__))) . '/languages/');
55
56 add_action('admin_menu', [$this, 'admin_menu']);
57 add_filter('submenu_file', [$this, 'admin_menu_highlight'], 10, 2);
58 add_action('admin_footer', [$this, 'admin_footer']);
59 add_action('admin_notices', [$this, 'admin_notices']);
60 add_action('admin_notices', [$this, 'admin_notices_picpoints']);
61 add_action('in_admin_header', [$this, 'in_admin_header']);
62 add_action('wp_loaded', [$this, 'page_redirects']);
63
64 // important, because ajax has another url
65 add_action('wp_ajax_' . $this->ajax_action_item_save_chunk, [$this, 'ajax_item_save_chunk']);
66 add_action('wp_ajax_' . $this->ajax_action_item_save_complete, [$this, 'ajax_item_save_complete']);
67 add_action('wp_ajax_' . $this->ajax_action_item_update, [$this, 'ajax_item_update']);
68 add_action('wp_ajax_' . $this->ajax_action_item_update_status, [$this, 'ajax_item_update_status']);
69 add_action('wp_ajax_' . $this->ajax_action_settings_update, [$this, 'ajax_settings_update']);
70 add_action('wp_ajax_' . $this->ajax_action_settings_get, [$this, 'ajax_settings_get']);
71 add_action('wp_ajax_' . $this->ajax_action_delete_data, [$this, 'ajax_delete_data']);
72 add_action('wp_ajax_' . $this->ajax_action_modal, [$this, 'ajax_modal']);
73 add_action('wp_ajax_' . $this->ajax_action_change_author, [$this, 'ajax_change_author']);
74 add_action('wp_ajax_' . $this->ajax_action_picpoints_promo, [$this, 'ajax_picpoints_promo']);
75 } else {
76 add_shortcode(VISION_SHORTCODE_NAME, [$this, 'shortcode']);
77 }
78
79 // only logged users with right roles can preview a vision map
80 if (array_intersect($allowed_roles, $user->roles) || current_user_can('manage_options')) {
81 add_filter('do_parse_request', [$this, 'do_parse_request'], 10, 3);
82 }
83
84 add_action('rest_api_init', [$this, 'rest_api_init']);
85
86 if (!wp_next_scheduled('clean_old_chunks')) {
87 wp_schedule_event(time(), 'hourly', 'clean_old_chunks');
88 }
89 }
90
91 function clean_old_chunks()
92 {
93 $upload_dir = wp_upload_dir();
94 $chunks_dir = trailingslashit($upload_dir['basedir']) . '/vision/chunks/';
95
96 if (!file_exists($chunks_dir) || !is_dir($chunks_dir)) {
97 if (defined('WP_DEBUG') && WP_DEBUG) {
98 // phpcs:ignore WordPress.PHP.DevelopmentFunctions
99 error_log('Vision chunks directory not found');
100 }
101 return;
102 }
103
104 $handle = opendir($chunks_dir);
105 if (!$handle) {
106 return;
107 }
108
109 while (($entry = readdir($handle)) !== false) {
110 if ($entry === '.' || $entry === '..') continue;
111
112 $full_path = $chunks_dir . $entry;
113 $is_dir = is_dir($full_path);
114
115 if (!$is_dir) {
116 if (defined('WP_DEBUG') && WP_DEBUG) {
117 // phpcs:ignore WordPress.PHP.DevelopmentFunctions
118 error_log("Skipping non-directory: $full_path");
119 }
120 continue;
121 }
122
123
124 if (filemtime($full_path) < time() - 21600) {
125 $files = new RecursiveIteratorIterator(
126 new RecursiveDirectoryIterator($full_path, FilesystemIterator::SKIP_DOTS),
127 RecursiveIteratorIterator::CHILD_FIRST
128 );
129
130 foreach ($files as $file) {
131 if ($file->isDir()) {
132 $this->remove_directory($file->getRealPath());
133 } else {
134 wp_delete_file($file->getRealPath());
135 }
136 }
137
138 $this->remove_directory($full_path);
139
140 if (defined('WP_DEBUG') && WP_DEBUG) {
141 // phpcs:ignore WordPress.PHP.DevelopmentFunctions
142 error_log("Deleted old chunk session: $entry");
143 }
144 }
145 }
146
147 closedir($handle);
148 }
149
150 function rest_api_init()
151 {
152 register_rest_route(
153 VISION_PLUGIN_REST_URL,
154 '/item/(?P<id>\d+)',
155 [
156 'methods' => 'GET',
157 'callback' => [$this, 'rest_api_get_item'],
158 'permission_callback' => '__return_true',
159 ]
160 );
161
162 register_rest_route(
163 VISION_PLUGIN_REST_URL,
164 '/preview/(?P<id>\d+)', [
165 'methods' => 'GET',
166 'callback' => [$this, 'rest_api_get_preview'],
167 'permission_callback' => function() { return is_user_logged_in(); },
168 ]
169 );
170 }
171
172 function rest_api_get_item($request)
173 {
174 $id = intval($request->get_param('id'));
175
176 global $wpdb;
177 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
178
179 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
180 $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $id);
181 $item = $wpdb->get_row($sql, OBJECT);
182 // phpcs:enable
183
184 $config = null;
185 if ($item->active) {
186 $config = unserialize($item->config);
187 }
188
189 if ($config) {
190 return new WP_REST_Response($config);
191 }
192
193 return new WP_REST_Response(null, 404);
194 }
195
196 function rest_api_get_preview($request)
197 {
198 $id = intval($request->get_param('id'));
199
200 global $wpdb;
201 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
202
203 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
204 $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $id);
205 $item = $wpdb->get_row($sql, OBJECT);
206 // phpcs:enable
207
208 if (!$item) {
209 return new WP_REST_Response(null, 404);
210 }
211
212 $user = wp_get_current_user();
213 $allowed_roles = $this->getAllowedRoles();
214
215 if (array_intersect($allowed_roles, $user->roles) || current_user_can('manage_options')) {
216 $config = unserialize($item->config);
217 return new WP_REST_Response($config);
218 }
219
220 return new WP_REST_Response(null, 403);
221 }
222
223 function filesystem_method()
224 {
225 return 'direct';
226 }
227
228 function request_filesystem_credentials()
229 {
230 return true;
231 }
232
233 function getFileSystem()
234 {
235 global $wp_filesystem;
236 $result = true;
237
238 if (!$wp_filesystem) {
239 require_once(ABSPATH . '/wp-admin/includes/file.php');
240
241 add_filter('filesystem_method', [$this, 'filesystem_method']);
242 add_filter('request_filesystem_credentials', [$this, 'request_filesystem_credentials']);
243
244 $credentials = request_filesystem_credentials(site_url(), '', true, false, null);
245
246 $result = WP_Filesystem($credentials);
247
248 remove_filter('filesystem_method', [$this, 'filesystem_method']);
249 remove_filter('request_filesystem_credentials', [$this, 'request_filesystem_credentials']);
250 }
251
252 if ($result)
253 return $wp_filesystem;
254 return null;
255 }
256
257 function joinPaths()
258 {
259 $paths = [];
260
261 foreach (func_get_args() as $arg) {
262 if ($arg !== '') {
263 $paths[] = $arg;
264 }
265 }
266
267 return preg_replace('#/+#', '/', join('/', $paths));
268 }
269
270 function joinUrls()
271 {
272 $urls = [];
273
274 foreach (func_get_args() as $arg) {
275 if ($arg !== '') {
276 $urls[] = $arg;
277 }
278 }
279
280 return preg_replace('/([^:])(\/{2,})/', '$1/', join('/', $urls));
281 }
282
283 function IsNullOrEmptyString($str)
284 {
285 return (!isset($str) || trim($str) === '');
286 }
287
288 function getAllowedRoles()
289 {
290 $allowed_roles = ['administrator'];
291
292 $settings_key = 'vision_settings';
293 $settings_value = get_option($settings_key);
294 if ($settings_value) {
295 $settings = unserialize($settings_value);
296 if (is_array($settings->roles)) $allowed_roles = array_merge($allowed_roles, $settings->roles);
297 }
298
299 return $allowed_roles;
300 }
301
302 function getLoaderGlobals($timestamp)
303 {
304 $plugin_url = plugin_dir_url(dirname(__FILE__));
305
306 $globals = [
307 'plan' => VISION_PLUGIN_PLAN,
308 'version' => $timestamp,
309 'effects_url' => $plugin_url . 'assets/css/vision-effects.css',
310 'theme_base_url' => $plugin_url . 'assets/themes/',
311 'plugin_base_url' => $plugin_url . 'assets/vendor/vision/',
312 'plugin_version' => VISION_PLUGIN_VERSION,
313 'ssl' => is_ssl(),
314 'api' => [
315 'nonce' => wp_create_nonce('wp_rest'),
316 'url' => esc_url_raw(rest_url(VISION_PLUGIN_REST_URL))
317 ]
318 ];
319
320 return $globals;
321 }
322
323 function embedLoader($in_footer, $timestamp)
324 {
325 $plugin_url = plugin_dir_url(dirname(__FILE__));
326 wp_enqueue_script('vision_loader', $plugin_url . 'assets/js/loader.js', ['jquery'], VISION_PLUGIN_VERSION, $in_footer);
327 wp_localize_script('vision_loader', 'vision_globals', $this->getLoaderGlobals($timestamp));
328 }
329
330 /**
331 * generate main css text
332 */
333 function getMainCss($itemData, $itemId)
334 {
335 $upload_dir = wp_upload_dir();
336
337 // create main css
338 $main_css = '';
339 $main_css .= '.vision-map-' . $itemId . ' {';
340
341 $main_css .= (!$this->IsNullOrEmptyString($itemData->background->color) ? 'background-color:' . $itemData->background->color . ';' : '');
342 if (!$this->IsNullOrEmptyString($itemData->background->image->url)) {
343 $imageUrl = ($itemData->background->image->relative ? $upload_dir['baseurl'] : '') . $itemData->background->image->url;
344 $main_css .= 'background-image:url(' . $imageUrl . ');';
345 }
346 $main_css .= ($itemData->background->size ? 'background-size:' . $itemData->background->size . ';' : '');
347 $main_css .= ($itemData->background->repeat ? 'background-repeat:' . $itemData->background->repeat . ';' : '');
348 $main_css .= ($itemData->background->position ? 'background-position:' . $itemData->background->position . ';' : '');
349
350 $main_css .= '}';
351
352 $layerId = 0;
353 foreach ($itemData->layers as $layerKey => $layer) {
354 if (!$layer->visible) {
355 continue;
356 }
357
358 $layerId++;
359 $layerSelector = '.vision-map-' . $itemId . ' .vision-layers [data-layer-id="' . $layer->id . '"] .vision-body';
360
361 // main
362 $main_css .= $layerSelector . ' {';
363 switch ($layer->type) {
364 case 'link': {
365 $main_css .= ($layer->link->normalColor ? 'background-color:' . $layer->link->normalColor . ';' : '');
366 $main_css .= ($layer->link->radius != NULL ? 'border-radius:' . $layer->link->radius . ';' : '');
367 }
368 break;
369 case 'image': {
370 $main_css .= (!$this->IsNullOrEmptyString($layer->image->background->color) ? 'background-color:' . $layer->image->background->color . ';' : '');
371 if (!$this->IsNullOrEmptyString($layer->image->background->file->url)) {
372 $imageUrl = ($layer->image->background->file->relative ? $upload_dir['baseurl'] : '') . $layer->image->background->file->url;
373 $main_css .= 'background-image:url(' . $imageUrl . ');';
374 }
375 $main_css .= ($layer->image->background->size ? 'background-size:' . $layer->image->background->size . ';' : '');
376 $main_css .= ($layer->image->background->repeat ? 'background-repeat:' . $layer->image->background->repeat . ';' : '');
377 $main_css .= ($layer->image->background->position ? 'background-position:' . $layer->image->background->position . ';' : '');
378 }
379 break;
380 case 'text': {
381 $main_css .= (!$this->IsNullOrEmptyString($layer->text->background->color) ? 'background-color:' . $layer->text->background->color . ';' : '');
382 if (!$this->IsNullOrEmptyString($layer->text->background->file->url)) {
383 $imageUrl = ($layer->text->background->file->relative ? $upload_dir['baseurl'] : '') . $layer->text->background->file->url;
384 $main_css .= 'background-image:url(' . $imageUrl . ');';
385 }
386 $main_css .= ($layer->text->background->size ? 'background-size:' . $layer->text->background->size . ';' : '');
387 $main_css .= ($layer->text->background->repeat ? 'background-repeat:' . $layer->text->background->repeat . ';' : '');
388 $main_css .= ($layer->text->background->position ? 'background-position:' . $layer->text->background->position . ';' : '');
389
390 $main_css .= ($layer->text->font ? 'font-family:"' . str_replace('+', ' ', $layer->text->font) . '",sans-serif;' : '');
391 $main_css .= ($layer->text->color ? 'color:' . $layer->text->color . ';' : '');
392 $main_css .= ($layer->text->size != NULL ? 'font-size:' . $layer->text->size . 'px;' : '');
393 $main_css .= ($layer->text->lineHeight != NULL ? 'line-height:' . $layer->text->lineHeight . 'px;' : '');
394 $main_css .= ($layer->text->align ? 'text-align:' . $layer->text->align . ';' : '');
395 $main_css .= ($layer->text->letterSpacing != NULL ? 'letter-spacing:' . $layer->text->letterSpacing . 'px;' : '');
396 }
397 break;
398 }
399 $main_css .= '}';
400
401 if ($layer->type == 'link') {
402 $main_css .= $layerSelector . ':hover {';
403 $main_css .= ($layer->link->hoverColor ? 'background-color:' . $layer->link->hoverColor . ';' : '');
404 $main_css .= '}';
405 }
406 }
407
408 return $main_css;
409 }
410
411 /**
412 * Shortcode output for the plugin
413 */
414 function shortcode($atts)
415 {
416 extract(shortcode_atts(['id' => 0, 'slug' => NULL, 'class' => NULL], $atts, VISION_SHORTCODE_NAME));
417
418 if (!$id && !$slug) {
419 return '<p>' . esc_html__('Error: invalid vision identifier attribute', 'vision') . '</p>';
420 }
421
422 $id = intval($id, 10);
423 $slug = sanitize_key($slug);
424 $class = sanitize_text_field($class);
425
426 global $wpdb;
427 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
428 $upload_dir = wp_upload_dir();
429
430 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
431 $sql = ($id ? $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $id) : $wpdb->prepare("SELECT * FROM {$table} WHERE slug=%s AND NOT deleted LIMIT 0, 1", $slug));
432 $item = $wpdb->get_row($sql, OBJECT);
433 // phpcs:enable
434
435 $preview = filter_input(INPUT_GET, 'preview', FILTER_SANITIZE_NUMBER_INT);
436
437 if ($item && ($item->active || (!$item->active && $preview == 1))) {
438 $version = strtotime(mysql2date('d M Y H:i:s', $item->modified));
439 $itemData = unserialize($item->data);
440 $id = $item->id;
441 $id_postfix = strtolower(wp_generate_password(5, false)); // generate unique postfix for $id to avoid clashes with multiple same shortcode use
442 $id_element = 'vision-' . $id . '-' . $id_postfix;
443
444 array_push($this->shortcodes, ['id' => $item->id, 'version' => $version]);
445
446 if (sizeof($this->shortcodes) == 1) {
447 $this->embedLoader(true, $version);
448 }
449
450 $output = '';
451
452 $output .= '<div ';
453 $output .= (property_exists($itemData, 'containerId') && $itemData->containerId ? 'id="' . esc_attr($itemData->containerId) . '" ' : '');
454 $output .= 'class="vision-map vision-map-' . esc_attr($id . ($class ? ' ' . $class : '')) . '"';
455
456 $json_src = esc_url_raw(rest_url(VISION_PLUGIN_REST_URL)) . '/item/' . esc_attr($item->id);
457 if ($preview == 1) {
458 $json_src = esc_url_raw(rest_url(VISION_PLUGIN_REST_URL)) . '/preview/' . esc_attr($item->id);
459 }
460 $output .= 'data-json-src="' . $json_src . '" ';
461
462 $output .= 'data-item-id="' . esc_attr($item->id) . '" ';
463 $output .= 'tabindex="1" ';
464 $output .= '>';
465 if (property_exists($itemData, 'image')) {
466 $upload_dir = wp_upload_dir();
467 $imageUrl = ($itemData->image->relative ? $upload_dir['baseurl'] : '') . $itemData->image->url;
468 $output .= '<div class="vision-img-placeholder"><img src="' . esc_url($imageUrl) . '" width="100%"></div>';
469 }
470 //=============================================
471 // STORE BEGIN
472 $output .= '<div class="vision-store" style="display:none;">';
473
474 $output .= '<div class="vision-layers-data">';
475 foreach ($itemData->layers as $layerKey => $layer) {
476 if (!$layer->visible) {
477 continue;
478 }
479 //=============================================
480 // LAYER BEGIN
481 $output .= '<div class="vision-layer" data-layer-id="' . esc_attr($layer->id) . '">';
482
483 if ($layer->contentData) {
484 $output .= do_shortcode($layer->contentData);
485 }
486 if ($layer->type == 'text') {
487 $output .= wp_kses_post($layer->text->data);
488 }
489
490 $output .= '</div>';
491 // LAYER END
492 //=============================================
493 }
494 $output .= '</div>';
495
496 $output .= '<div class="vision-tooltips-data">';
497 foreach ($itemData->layers as $layerKey => $layer) {
498 if (!$layer->visible) {
499 continue;
500 }
501 //=============================================
502 // TOOLTIP BEGIN
503 $output .= '<div class="vision-data" data-layer-id="' . esc_attr($layer->id) . '">';
504 $output .= do_shortcode($layer->tooltip->data);
505 $output .= '</div>';
506 // TOOLTIP END
507 //=============================================
508 }
509 $output .= '</div>';
510
511 $output .= '<div class="vision-popovers-data">';
512 foreach ($itemData->layers as $layerKey => $layer) {
513 if (!$layer->visible) {
514 continue;
515 }
516 //=============================================
517 // POPOVER BEGIN
518 $output .= '<div class="vision-data" data-layer-id="' . esc_attr($layer->id) . '">';
519 $output .= do_shortcode($layer->popover->data);
520 $output .= '</div>';
521 // POPOVER END
522 //=============================================
523 }
524
525 $output .= '</div>';
526 $output .= '</div>';
527 // STORE END
528 //=============================================
529
530 $output .= '</div>';
531
532 $css = $this->getMainCss($itemData, $id) . ($itemData->customCSS->active ? $itemData->customCSS->data : '');
533 $css = preg_replace('/[^\/\\\\a-zA-Z0-9\s\_\%\=\[\]\(\)\{\}\:\;\.\,\#\$\-\"\'\!@]/', '', $css);
534
535 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
536 $output .= '<style>' . $css . '</style>';
537
538 $output = preg_replace('/\s+/', ' ', $output);
539 $output = force_balance_tags($output);
540
541 return $output;
542 }
543
544 return '<p>' . esc_html__('Error: the vision item can’t be found', 'vision') . '</p>';
545 }
546
547 /**
548 * Run a filter to obtain some custom url settings, compare them to the current url
549 * and if a match is found the custom callback is fired, the custom view is loaded
550 * and request is stopped.
551 */
552 function do_parse_request($result)
553 {
554 if (current_filter() !== 'do_parse_request') {
555 return $result;
556 }
557
558 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
559 $url = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']));
560
561 if (preg_match('/vision\/map\/([a-z0-9_-]+)/', $url, $matches)) {
562 $preview = filter_input(INPUT_GET, 'preview', FILTER_SANITIZE_NUMBER_INT);
563
564 global $wpdb;
565 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
566 $shortcode = false;
567
568 if (is_numeric($matches[1])) {
569 $vision_map_id = $matches[1];
570
571 if ($vision_map_id != null) {
572 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
573 $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $vision_map_id);
574 $item = $wpdb->get_row($sql, OBJECT);
575 // phpcs:enable
576
577 if ($item && ($item->active || (!$item->active && $preview == 1))) {
578 $this->vision_map_id = $item->id;
579 $this->vision_map_version = strtotime(mysql2date('Y-m-d H:i:s', $item->modified));
580 $shortcode = true;
581 }
582 }
583 } else {
584 $vision_map_slug = $matches[1];
585
586 if ($vision_map_slug != null) {
587 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
588 $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE slug=%s AND NOT deleted", $vision_map_slug);
589 $item = $wpdb->get_row($sql, OBJECT);
590 // phpcs:enable
591
592 if ($item && ($item->active || (!$item->active && $preview == 1))) {
593 $this->vision_map_id = $item->id;
594 $this->vision_map_version = strtotime(mysql2date('Y-m-d H:i:s', $item->modified));
595
596 $shortcode = true;
597 }
598 }
599 }
600
601 if ($shortcode) {
602 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-preview.php');
603 exit();
604 }
605 }
606
607 return $result;
608 }
609
610 /**
611 * Prepare upload directory
612 */
613 function admin_notices()
614 {
615 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
616 if (!($page === 'vision' || $page === 'vision_item')) {
617 return;
618 }
619
620 if (!file_exists(VISION_PLUGIN_UPLOAD_DIR)) {
621 wp_mkdir_p(VISION_PLUGIN_UPLOAD_DIR);
622 }
623
624 if (!file_exists(VISION_PLUGIN_UPLOAD_DIR)) {
625 echo '<div class="notice notice-error is-dismissible">';
626 echo '<p>' . esc_html__('The plugin upload directory could not be created', 'vision') . '</p>';
627 echo '<p>' . esc_html__('Please run the following commands in order to make the directory', 'vision') . '<br>';
628 echo '<b>mkdir ' . esc_attr(VISION_PLUGIN_UPLOAD_DIR) . '</b><br>';
629 echo '<b>chmod 777 ' . esc_attr(VISION_PLUGIN_UPLOAD_DIR) . '</b></p>';
630 echo '</div>';
631 return;
632 }
633
634 if (!wp_is_writable(VISION_PLUGIN_UPLOAD_DIR)) {
635 echo '<div class="notice notice-error is-dismissible">';
636 echo '<p>' . esc_html__('The plugin upload directory is not writable, therefore the css and js files cannot be saved.', 'vision') . '</p>';
637 echo '<p>' . esc_html__('Please run the following commands in order to make the directory', 'vision') . '<br>';
638 echo '<b>chmod 777 ' . esc_attr(VISION_PLUGIN_UPLOAD_DIR) . '</b></p>';
639 echo '</div>';
640 return;
641 }
642
643 if (!file_exists(VISION_PLUGIN_UPLOAD_DIR . '/' . 'index.php')) {
644 $data = '<?php' . PHP_EOL . '// silence is golden' . PHP_EOL . '?>';
645
646 $wp_filesystem = $this->getFileSystem();
647 $wp_filesystem->put_contents(VISION_PLUGIN_UPLOAD_DIR . '/' . 'index.php', $data);
648 }
649 }
650
651 /**
652 * PicPoints promo notice
653 */
654 function admin_notices_picpoints()
655 {
656 if (!current_user_can('manage_options')) {
657 return;
658 }
659
660 $promo = get_option('vision_picpoints_promo');
661 if (!is_array($promo)) {
662 $promo = ['status' => 'active', 'remind_at' => 0];
663 }
664
665 $status = isset($promo['status']) ? $promo['status'] : 'active';
666 $remind_at = isset($promo['remind_at']) ? (int) $promo['remind_at'] : 0;
667
668 if ($status === 'dismissed') {
669 return;
670 }
671 if ($status === 'remind' && time() < $remind_at) {
672 return;
673 }
674
675 wp_enqueue_script('jquery');
676
677 $nonce = wp_create_nonce('vision_ajax');
678 $ajax_url = esc_url(admin_url('admin-ajax.php'));
679
680 echo '<div class="notice notice-info vision-picpoints-notice" style="position:relative;">';
681 echo '<p style="font-weight:600;font-size:14px;margin:10px 0 0 0;">';
682 echo esc_html__('To our amazing Vision community!', 'vision');
683 echo '</p>';
684 echo '<p style="font-weight:600;font-size:14px;margin:0 0 10px 0;">';
685 echo esc_html__('We are proud to introduce PicPoints, our next-generation interactive image map builder. 🚀', 'vision');
686 echo '</p>';
687 echo '<p style="margin-bottom:10px;">';
688 echo esc_html__('We built PicPoints from the ground up on modern web standards to solve old, frustrating layout problems once and for all.', 'vision');
689 echo '</p>';
690 echo '<p style="margin-bottom:10px;">';
691 echo esc_html__('Why you’ll love it:', 'vision');
692 echo '</p>';
693 echo '<p style="margin-left:10px;">';
694 echo esc_html__('🛡️ Zero Conflict: Full CSS/JS isolation (Shadow DOM) ensures your maps look perfect without messing up your website theme.', 'vision');
695 echo '</p>';
696 echo '<p style="margin-left:10px;">';
697 echo esc_html__('🎨 Figma-Like Vector Editor: Draw, customize, and place markers & polygons with a powerful, intuitive, and modern editor interface.', 'vision');
698 echo '</p>';
699 echo '<p style="margin-left:10px;">';
700 echo esc_html__('⚡ Pro-Grade Performance: Optimized to handle heavy multi-level floor plans or highly engaging shoppable lookbooks flawlessly.', 'vision');
701 echo '</p>';
702 echo '<p style="margin-top:40px;display:flex;gap:8px;flex-wrap:wrap;justify-content:space-between;">';
703 echo '<span style="display:flex;gap:8px;flex-wrap:wrap;">';
704 echo '<a href="https://wordpress.org/plugins/picpoints/" target="_blank" rel="noopener" class="button button-primary button-vision-picpoints-promo">Try Free Version</a>';
705 echo '<a href="https://checkout.freemius.com/product/30659/?billing_cycle=annual&billing_cycle_selector=list&coupon=vision35" target="_blank" rel="noopener" class="button button-primary button-vision-picpoints-promo" style="background:#8e44ad;border-color:#8e44ad;">Get 35% OFF for PRO</a>';
706 echo '</span>';
707 echo '<span style="display:flex;gap:8px;flex-wrap:wrap;">';
708 echo '<button type="button" class="button button-vision-picpoints-remind">' . esc_html__('Remind me later', 'vision') . '</button>';
709 echo '<button type="button" class="button button-vision-picpoints-dismiss">' . esc_html__('No, thanks', 'vision') . '</button>';
710 echo '</span>';
711 echo '</p>';
712 echo '</div>';
713
714 // Inline JS
715 echo '<script>
716 (function($){
717 $(function(){
718 var $notice = $(".vision-picpoints-notice");
719 var ajaxUrl = ' . wp_json_encode($ajax_url) . ';
720 var nonce = ' . wp_json_encode($nonce) . ';
721
722 function sendAction(action){
723 $.post(ajaxUrl, {
724 action: "vision_ajax_picpoints_promo",
725 nonce: nonce,
726 promo_action: action
727 });
728 }
729
730 $notice.on("click", ".button-vision-picpoints-dismiss", function(){
731 sendAction("dismiss");
732 $notice.slideUp(function(){ $notice.remove(); });
733 });
734
735 $notice.on("click", ".button-vision-picpoints-remind", function(){
736 sendAction("remind");
737 $notice.slideUp(function(){ $notice.remove(); });
738 });
739
740 $notice.on("click", ".button-vision-picpoints-promo", function(){
741 $notice.slideUp(function(){ $notice.remove(); });
742 });
743 });
744 })(jQuery);
745 </script>';
746 }
747
748 /**
749 * Ajax: handle PicPoints promo action (dismiss / remind)
750 */
751 function ajax_picpoints_promo()
752 {
753 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
754 wp_send_json_error(['msg' => esc_html__('The operation failed', 'vision')]);
755 }
756
757 if (!current_user_can('manage_options')) {
758 wp_send_json_error(['msg' => esc_html__('Permission denied', 'vision')]);
759 }
760
761 $action = sanitize_key(filter_input(INPUT_POST, 'promo_action'));
762 $now = time();
763
764 if ($action === 'dismiss') {
765 update_option('vision_picpoints_promo', [
766 'status' => 'dismissed',
767 'remind_at' => 0,
768 ], false);
769 } elseif ($action === 'remind') {
770 update_option('vision_picpoints_promo', [
771 'status' => 'remind',
772 'remind_at' => $now + MONTH_IN_SECONDS,
773 ], false);
774 } else {
775 wp_send_json_error(['msg' => esc_html__('Invalid action', 'vision')]);
776 }
777
778 wp_send_json_success(['msg' => 'ok']);
779 wp_die();
780 }
781
782 /**
783 * Fires at the beginning of the content section in an admin page
784 */
785 function in_admin_header()
786 {
787 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
788
789 if (!(($page === 'vision') || ($page === 'vision_item') || ($page === 'vision_settings'))) {
790 return;
791 }
792
793 remove_all_actions('admin_notices');
794 remove_all_actions('all_admin_notices');
795 add_action('admin_notices', [$this, 'admin_notices']);
796 add_action('admin_notices', [$this, 'admin_notices_picpoints']);
797 }
798
799 /**
800 * Register the administration menu for this plugin into the WordPress Dashboard menu.
801 */
802 function admin_menu()
803 {
804 // add "edit_posts" if we want to give access to author, editor and contributor roles
805 add_menu_page(esc_html__('Vision', 'vision'), esc_html__('Vision', 'vision'), 'read', 'vision', [$this, 'admin_menu_page_items'], 'dashicons-format-image');
806 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('All Items', 'vision'), 'read', 'vision', [$this, 'admin_menu_page_items']);
807 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('Add New', 'vision'), 'read', 'vision_item', [$this, 'admin_menu_page_item']);
808 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('Settings', 'vision'), 'manage_options', 'vision_settings', [$this, 'admin_menu_page_settings']);
809
810 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('Upgrade to Pro', 'vision'), 'manage_options', 'vision_upgrade_to_pro', [$this, 'admin_menu_page_upgrade_to_pro']);
811
812 }
813
814 function admin_menu_highlight($submenu_file, $parent_file)
815 {
816 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
817 if (in_array($page, ['vision_item'])) {
818 $id = sanitize_key(filter_input(INPUT_GET, 'id', FILTER_DEFAULT));
819 if (!empty($id)) {
820 $submenu_file = 'vision';
821 }
822 }
823 return $submenu_file;
824 }
825
826 function admin_footer()
827 {
828 if (get_current_screen() && get_current_screen()->base !== 'plugins') {
829 return;
830 }
831
832 $globals = [
833 'token' => $this->get_token(),
834 'ajax' => [
835 'url' => VISION_FEEDBACK_URL
836 ]
837 ];
838
839 wp_enqueue_style('vision-feedback', VISION_PLUGIN_URL . 'assets/css/feedback.css', [], VISION_PLUGIN_VERSION);
840 wp_enqueue_script('vision-feedback', VISION_PLUGIN_URL . 'assets/js/feedback.js', ['jquery'], VISION_PLUGIN_VERSION, false);
841 wp_localize_script('vision-feedback', 'vision_feedback_globals', $globals);
842
843 require_once(plugin_dir_path(dirname(__FILE__)) . 'templates/feedback.php');
844 }
845
846 function get_token()
847 {
848 global $wp_version;
849 $current_user = wp_get_current_user();
850
851 $data = [
852 'plugin_name' => VISION_PLUGIN_NAME,
853 'plugin_version' => VISION_PLUGIN_VERSION,
854 'wordpress' => $wp_version,
855 'php' => PHP_VERSION,
856 'email' => $current_user->user_email,
857 'site' => trim(str_replace(['http://', 'https://'], '', get_site_url()), '/')
858 ];
859 return base64_encode(wp_json_encode($data));
860 }
861
862 /**
863 * Custom redirects
864 */
865 function page_redirects()
866 {
867 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
868
869 if ($page === 'vision') {
870 $action = sanitize_key(filter_input(INPUT_GET, 'action', FILTER_DEFAULT));
871 if ($action) {
872 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
873 $url = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']));
874
875 $url = remove_query_arg(['action', 'id', '_wpnonce'], $url);
876 header('Refresh:0; url="' . $url . '"', true, 303);
877 //wp_redirect($url); // does not work delete and dublicate operations on XAMPP
878 }
879 }
880 }
881
882 /**
883 * Show admin menu items page
884 */
885 function admin_menu_page_items()
886 {
887 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
888
889 if ($page === 'vision') {
890 $plugin_url = plugin_dir_url(dirname(__FILE__));
891 $upload_dir = wp_upload_dir();
892
893 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
894 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
895 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
896
897 // global settings to help ajax work
898 $globals = [
899 'plan' => VISION_PLUGIN_PLAN,
900 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
901 'upload_url' => $upload_dir['baseurl'],
902 'ajax_url' => admin_url('admin-ajax.php'),
903 'ajax_nonce' => wp_create_nonce('vision_ajax'),
904 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
905 ];
906 $globals['ajax_action_update'] = $this->ajax_action_item_update_status;
907 $globals['ajax_action_change_author'] = $this->ajax_action_change_author;
908
909 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/list-table-items.php');
910 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-items.php');
911
912 wp_localize_script('vision_admin', 'vision_globals', $globals);
913 }
914 }
915
916 /**
917 * Show admin menu item page
918 */
919 function admin_menu_page_item()
920 {
921 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
922 if ($page === 'vision_item') {
923 $id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
924
925 $plugin_url = plugin_dir_url(dirname(__FILE__));
926 $upload_dir = wp_upload_dir();
927
928 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
929
930 if (VISION_PLUGIN_PLAN == 'lite' && !$id) {
931 global $wpdb;
932 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
933
934 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
935 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
936
937 if ($count >= 1) {
938 echo '<p>Vision: ' . esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision') . '</p>';
939 return;
940 }
941 }
942
943 wp_enqueue_style('vision_notify', $plugin_url . 'assets/css/notify.css', [], VISION_PLUGIN_VERSION, 'all');
944 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
945 wp_enqueue_style('vision_vision_effects', $plugin_url . 'assets/css/vision-effects.css', [], VISION_PLUGIN_VERSION, 'all');
946
947 wp_enqueue_script('vision_notify', $plugin_url . 'assets/js/notify.js', ['jquery'], VISION_PLUGIN_VERSION, false);
948 wp_enqueue_script('vision_ace', $plugin_url . 'assets/vendor/ace/ace.js', [], VISION_PLUGIN_VERSION, false);
949 wp_enqueue_script('vision_url', $plugin_url . 'assets/vendor/url/url.js', [], VISION_PLUGIN_VERSION, false);
950 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
951
952 wp_enqueue_media();
953
954 // global settings to help ajax work
955 $globals = [
956 'plan' => VISION_PLUGIN_PLAN,
957 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
958 'msg_custom_js_error' => esc_html__('Custom js code error', 'vision'),
959 'msg_layer_id_error' => esc_html__('The layer ID should be unique', 'vision'),
960 'wp_base_url' => get_site_url(),
961 'upload_base_url' => $upload_dir['baseurl'],
962 'plugin_base_url' => $plugin_url,
963 'ajax_url' => admin_url('admin-ajax.php'),
964 'ajax_nonce' => wp_create_nonce('vision_ajax'),
965 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
966 ];
967
968 $globals['ajax_action_get'] = $this->ajax_action_settings_get;
969 $globals['ajax_action_save_chunk'] = $this->ajax_action_item_save_chunk;
970 $globals['ajax_action_save_complete'] = $this->ajax_action_item_save_complete;
971 $globals['ajax_action_update'] = $this->ajax_action_item_update;
972 $globals['ajax_action_modal'] = $this->ajax_action_modal;
973 $globals['ajax_item_id'] = $id;
974 $globals['settings'] = NULL;
975 $globals['config'] = NULL;
976
977 $settings_key = 'vision_settings';
978 $settings_value = get_option($settings_key);
979 if ($settings_value) {
980 $globals['settings'] = unserialize($settings_value); // json_encode(unserialize($settings_value)) problem with double quotes
981 }
982
983 // get item data from DB
984 if ($id) {
985 global $wpdb;
986 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
987
988 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
989 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $id);
990 $item = $wpdb->get_row($query, OBJECT);
991 // phpcs:enable
992
993 if ($item) {
994 $globals['config'] = unserialize($item->data); // json_encode(unserialize($item->data)) problem with double quotes
995 }
996
997 if ($item) {
998 $current_user_id = get_current_user_id();
999 if (!current_user_can('manage_options') && $current_user_id != $item->author) {
1000 echo '<p>' . esc_html__('You do not have permission to edit this item.', 'vision') . '</p>';
1001 return;
1002 }
1003 }
1004 } else {
1005 // new item
1006 $item = (object) [
1007 'author' => get_current_user_id(),
1008 'editor' => get_current_user_id(),
1009 'created' => current_time('mysql', 1),
1010 'modified' => current_time('mysql', 1)
1011 ];
1012 }
1013
1014 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-item.php');
1015
1016 // set global settings
1017 wp_localize_script('vision_admin', 'vision_globals', $globals);
1018 }
1019 }
1020
1021 /**
1022 * Show admin menu settings page
1023 */
1024 function admin_menu_page_settings()
1025 {
1026 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
1027 if ($page === 'vision_settings') {
1028 $plugin_url = plugin_dir_url(dirname(__FILE__));
1029
1030 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
1031 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
1032 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
1033
1034 // global settings to help ajax work
1035 $globals = [
1036 'plan' => VISION_PLUGIN_PLAN,
1037 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
1038 'ajax_url' => admin_url('admin-ajax.php'),
1039 'ajax_nonce' => wp_create_nonce('vision_ajax'),
1040 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
1041 ];
1042
1043 $globals['ajax_action_update'] = $this->ajax_action_settings_update;
1044 $globals['ajax_action_get'] = $this->ajax_action_settings_get;
1045 $globals['ajax_action_modal'] = $this->ajax_action_modal;
1046 $globals['ajax_action_delete_data'] = $this->ajax_action_delete_data;
1047 $globals['config'] = NULL;
1048
1049 // read settings
1050 $settings_key = 'vision_settings';
1051 $settings_value = get_option($settings_key);
1052 if ($settings_value) {
1053 $globals['config'] = wp_json_encode(unserialize($settings_value));
1054 }
1055
1056 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-settings.php');
1057
1058 wp_localize_script('vision_admin', 'vision_globals', $globals);
1059 }
1060 }
1061
1062 /**
1063 * Show admin menu upgrade to pro page
1064 */
1065 function admin_menu_page_upgrade_to_pro()
1066 {
1067 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
1068 if ($page === 'vision_upgrade_to_pro') {
1069 echo '<script>window.location = "https://1.envato.market/getvision"</script>';
1070 }
1071 }
1072
1073 /**
1074 * Ajax update item state
1075 */
1076 function ajax_item_update_status()
1077 {
1078 $error = false;
1079 $data = [];
1080 $config = filter_input(INPUT_POST, 'config');
1081
1082 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1083 global $wpdb;
1084 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1085
1086 $config = json_decode($config);
1087 $result = false;
1088
1089 if (isset($config->id) && isset($config->active)) {
1090 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1091 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $config->id);
1092 $item = $wpdb->get_row($query, OBJECT);
1093 // phpcs:enable
1094
1095 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1096 $itemData = unserialize($item->data);
1097 $itemData->active = $config->active;
1098
1099 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1100 $result = $wpdb->update(
1101 $table,
1102 ['active' => $itemData->active, 'data' => serialize($itemData)],
1103 ['id' => $config->id]
1104 );
1105 }
1106 }
1107
1108 if ($result) {
1109 $data['id'] = $config->id;
1110 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1111 } else {
1112 $error = true;
1113 $data['msg'] = esc_html__('The operation failed, can\'t update item', 'vision');
1114 }
1115 } else {
1116 $error = true;
1117 $data['msg'] = esc_html__('The operation failed', 'vision');
1118 }
1119
1120 if ($error) {
1121 wp_send_json_error($data);
1122 } else {
1123 wp_send_json_success($data);
1124 }
1125
1126 wp_die(); // this is required to terminate immediately and return a proper response
1127 }
1128
1129 /**
1130 * Ajax update item data
1131 */
1132 function ajax_item_update()
1133 {
1134 $error = false;
1135 $data = [];
1136
1137 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1138 global $wpdb;
1139 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1140
1141 $inputId = filter_input(INPUT_POST, 'id');
1142 $inputData = filter_input(INPUT_POST, 'data');
1143 $inputConfig = filter_input(INPUT_POST, 'config');
1144 $itemData = json_decode($inputData);
1145 $itemConfig = json_decode($inputConfig);
1146 $flag = true;
1147
1148 if (VISION_PLUGIN_PLAN == 'lite' && !$inputId) {
1149 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1150 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
1151
1152 if ($count >= 1) {
1153 $flag = false;
1154 $error = true;
1155 $data['msg'] = esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision');
1156 }
1157 }
1158
1159 if ($itemData === NULL || $itemConfig === NULL) {
1160 $flag = false;
1161 $error = true;
1162 $data['msg'] = 'Error decoding JSON: ' . json_last_error_msg();
1163 }
1164
1165 if ($flag) {
1166 $itemConfig->modified = current_time('mysql', 1);
1167
1168 if ($inputId) {
1169 $result = false;
1170
1171 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1172 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $inputId);
1173 $item = $wpdb->get_row($query, OBJECT);
1174 // phpcs:enable
1175
1176 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1177 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1178
1179 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1180 $result = $wpdb->update(
1181 $table,
1182 [
1183 'title' => $itemData->title,
1184 'slug' => $itemData->slug,
1185 'active' => $itemData->active,
1186 'data' => serialize($itemData),
1187 'config' => serialize($itemConfig),
1188 //'author' => get_current_user_id(),
1189 'editor' => get_current_user_id(),
1190 //'date' => NULL,
1191 'modified' => current_time('mysql', 1)
1192 ],
1193 ['id' => $inputId]
1194 );
1195 }
1196
1197 if ($result) {
1198 $data['id'] = $inputId;
1199 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1200 } else {
1201 $error = true;
1202 $data['msg'] = esc_html__('The operation failed, can\'t update item', 'vision');
1203 }
1204 } else {
1205 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1206
1207 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1208 $result = $wpdb->insert(
1209 $table,
1210 [
1211 'title' => $itemData->title,
1212 'slug' => $itemData->slug,
1213 'active' => $itemData->active,
1214 'data' => serialize($itemData),
1215 'config' => serialize($itemConfig),
1216 'author' => get_current_user_id(),
1217 'editor' => get_current_user_id(),
1218 'created' => current_time('mysql', 1),
1219 'modified' => current_time('mysql', 1)
1220 ]
1221 );
1222
1223 if ($result) {
1224 $data['id'] = $inputId = $wpdb->insert_id;
1225 $data['msg'] = esc_html__('The item was successfully created', 'vision');
1226 } else {
1227 $error = true;
1228 $data['msg'] = esc_html__('The operation failed, can\'t create item', 'vision');
1229 }
1230 }
1231 }
1232 } else {
1233 $error = true;
1234 $data['msg'] = esc_html__('The operation failed', 'vision');
1235 }
1236
1237 if ($error) {
1238 wp_send_json_error($data);
1239 } else {
1240 wp_send_json_success($data);
1241 }
1242
1243 wp_die(); // this is required to terminate immediately and return a proper response
1244 }
1245
1246 /**
1247 * Ajax save item data
1248 */
1249 function ajax_item_save_chunk()
1250 {
1251 try {
1252 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
1253 throw new Exception('Nonce verification failed');
1254 }
1255
1256 $session_id = sanitize_text_field(filter_input(INPUT_POST, 'session_id'));
1257 $chunk_index = (int)filter_input(INPUT_POST, 'chunk_index');
1258 $total_chunks = (int)filter_input(INPUT_POST, 'total_chunks');
1259 $chunk = filter_input(INPUT_POST, 'chunk');
1260 $data_type = sanitize_text_field(filter_input(INPUT_POST, 'data_type')); // 'data' или 'config'
1261 $is_last_chunk = filter_input(INPUT_POST, 'is_last_chunk') === '1';
1262 $item_id = (int)filter_input(INPUT_POST, 'item_id');
1263
1264 if (empty($session_id) || !is_string($session_id)) {
1265 throw new Exception('Invalid session ID');
1266 }
1267
1268 if (!in_array($data_type, ['data', 'config'], true)) {
1269 throw new Exception('Invalid data_type');
1270 }
1271
1272 if ($chunk_index < 0 || $total_chunks < 1 || $chunk_index >= $total_chunks) {
1273 throw new Exception('Invalid chunk index or total chunks');
1274 }
1275
1276 if (strlen($chunk) > 5 * 1024 * 1024) {
1277 throw new Exception('Chunk too large');
1278 }
1279
1280 $upload_dir = wp_upload_dir();
1281 $chunk_dir = $upload_dir['basedir'] . '/vision/chunks/' . $session_id . '/';
1282
1283 if (!file_exists($chunk_dir)) {
1284 wp_mkdir_p($chunk_dir);
1285 }
1286
1287 $chunk_file = $chunk_dir . $data_type . '_' . $chunk_index . '.chunk';
1288
1289 $wp_filesystem = $this->getFileSystem();
1290 if (!$wp_filesystem->put_contents($chunk_file, $chunk)) {
1291 throw new Exception('Failed to write chunk');
1292 }
1293
1294 wp_send_json_success([
1295 'status' => 'chunk_saved',
1296 'type' => $data_type,
1297 'index' => $chunk_index
1298 ]);
1299 } catch (Exception $e) {
1300 wp_send_json_error($e->getMessage(), 400);
1301 }
1302
1303 wp_die();
1304 }
1305
1306 /**
1307 * Ajax save item data complete
1308 */
1309 function ajax_item_save_complete()
1310 {
1311 $data = [];
1312
1313 try {
1314 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
1315 throw new Exception('Nonce verification failed');
1316 }
1317
1318 $session_id = sanitize_text_field(filter_input(INPUT_POST, 'session_id'));
1319 $item_id = (int)filter_input(INPUT_POST, 'item_id');
1320
1321 $upload_dir = wp_upload_dir();
1322 $chunk_dir = $upload_dir['basedir'] . '/vision/chunks/' . $session_id . '/';
1323
1324 $collected_data = [
1325 'data' => '',
1326 'config' => ''
1327 ];
1328
1329 foreach (['data', 'config'] as $type) {
1330 $chunks = glob($chunk_dir . $type . '_*.chunk');
1331
1332 if (count($chunks) === 0) {
1333 throw new Exception("No chunks found for $type");
1334 }
1335
1336 natsort($chunks);
1337
1338 foreach ($chunks as $chunk_file) {
1339 $collected_data[$type] .= file_get_contents($chunk_file);
1340 wp_delete_file($chunk_file);
1341 }
1342
1343 $collected_data[$type] = json_decode($collected_data[$type]);
1344
1345 if (json_last_error() !== JSON_ERROR_NONE) {
1346 throw new Exception("Invalid JSON for $type: " . json_last_error_msg());
1347 }
1348 }
1349
1350 $this->remove_directory($chunk_dir);
1351
1352 // save to DB
1353 global $wpdb;
1354 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1355
1356 if (VISION_PLUGIN_PLAN == 'lite' && !$item_id) {
1357 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1358 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
1359
1360 if ($count >= 1) {
1361 throw new Exception(esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision'));
1362 }
1363 }
1364
1365 $itemData = $collected_data['data'];
1366 $itemConfig = $collected_data['config'];
1367 $itemConfig->modified = current_time('mysql', 1);
1368
1369 if ($item_id) {
1370 $result = false;
1371
1372 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1373 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $item_id);
1374 $item = $wpdb->get_row($query, OBJECT);
1375 // phpcs:enable
1376
1377 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1378 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1379
1380 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1381 $result = $wpdb->update(
1382 $table,
1383 [
1384 'title' => $itemData->title,
1385 'slug' => $itemData->slug,
1386 'active' => $itemData->active,
1387 'data' => serialize($itemData),
1388 'config' => serialize($itemConfig),
1389 //'author' => get_current_user_id(),
1390 'editor' => get_current_user_id(),
1391 //'date' => NULL,
1392 'modified' => current_time('mysql', 1)
1393 ],
1394 ['id' => $item_id]
1395 );
1396 }
1397
1398 if ($result) {
1399 $data['id'] = $item_id;
1400 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1401 wp_send_json_success($data);
1402 } else {
1403 throw new Exception(esc_html__('The operation failed, can\'t update item', 'vision'));
1404 }
1405 } else {
1406 $itemData->slug = sanitize_title($itemData->slug ? $itemData->slug : $itemData->title);
1407
1408 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1409 $result = $wpdb->insert(
1410 $table,
1411 [
1412 'title' => $itemData->title,
1413 'slug' => $itemData->slug,
1414 'active' => $itemData->active,
1415 'data' => serialize($itemData),
1416 'config' => serialize($itemConfig),
1417 'author' => get_current_user_id(),
1418 'editor' => get_current_user_id(),
1419 'created' => current_time('mysql', 1),
1420 'modified' => current_time('mysql', 1)
1421 ]
1422 );
1423
1424 if ($result) {
1425 $data['id'] = $item_id = $wpdb->insert_id;
1426 $data['msg'] = esc_html__('The item was successfully created', 'vision');
1427 wp_send_json_success($data);
1428 } else {
1429 throw new Exception(esc_html__('The operation failed, can\'t create item', 'vision'));
1430 }
1431 }
1432 } catch (Exception $e) {
1433 $data['msg'] = $e->getMessage();
1434 wp_send_json_error($data, 400);
1435 }
1436 }
1437
1438 /**
1439 * Ajax update settings data
1440 */
1441 function ajax_settings_update()
1442 {
1443 $error = false;
1444 $data = [];
1445 $config = filter_input(INPUT_POST, 'config');
1446 $config = json_decode($config);
1447
1448 if ($config !== NULL) {
1449 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1450 $settings_key = 'vision_settings';
1451 $settings_value = serialize($config);
1452 $result = false;
1453
1454 if (get_option($settings_key) == false) {
1455 $autoload = 'no';
1456 $result = add_option($settings_key, $settings_value, "", $autoload);
1457 } else {
1458 $old_settings_value = get_option($settings_key);
1459 if ($old_settings_value === $settings_value) {
1460 $result = true;
1461 } else {
1462 $result = update_option($settings_key, $settings_value);
1463 }
1464 }
1465
1466 if ($result) {
1467 $data['msg'] = esc_html__('The settings were successfully updated', 'vision');
1468 } else {
1469 $error = true;
1470 $data['msg'] = esc_html__('The operation failed, can\'t update settings', 'vision');
1471 }
1472 }
1473 } else {
1474 $error = true;
1475 $data['msg'] = 'Error decoding JSON: ' . json_last_error_msg();
1476 }
1477
1478 if ($error) {
1479 wp_send_json_error($data);
1480 } else {
1481 wp_send_json_success($data);
1482 }
1483
1484 wp_die(); // this is required to terminate immediately and return a proper response
1485 }
1486
1487 /**
1488 * Ajax settings get data
1489 */
1490 function ajax_settings_get()
1491 {
1492 $error = false;
1493 $data = [];
1494 $type = sanitize_key(filter_input(INPUT_POST, 'type', FILTER_DEFAULT));
1495
1496 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1497 switch ($type) {
1498 case 'roles': {
1499 $data['list'] = [];
1500
1501 $roles = wp_roles()->roles;
1502 foreach ($roles as $key => $role) {
1503 if (array_key_exists('read', $role['capabilities'])) {
1504 array_push($data['list'], ['id' => $key, 'name' => translate_user_role($role['name'])]);
1505 }
1506 }
1507 }
1508 break;
1509 case 'themes': {
1510 $data['list'] = [];
1511
1512 $files = glob(plugin_dir_path(dirname(__FILE__)) . 'assets/themes/*.css');
1513 foreach ($files as $file) {
1514 $filename = basename($file, '.css');
1515 array_push($data['list'], ['id' => $filename, 'title' => str_replace('-', ' ', $filename)]);
1516 }
1517 }
1518 break;
1519 case 'editor-themes': {
1520 $data['list'] = [];
1521
1522 $files = glob(plugin_dir_path(dirname(__FILE__)) . 'assets/vendor/ace/theme-*.js');
1523 foreach ($files as $file) {
1524 $filename = str_replace('theme-', '', basename($file, '.js'));
1525 array_push($data['list'], ['id' => $filename, 'title' => str_replace('_', ' ', $filename)]);
1526 }
1527 }
1528 break;
1529 case 'fonts': {
1530 $data['list'] = array(
1531 array('fontname' => 'none'),
1532 array('fontname' => 'Aclonica'),
1533 array('fontname' => 'Allan'),
1534 array('fontname' => 'Annie+Use+Your+Telescope'),
1535 array('fontname' => 'Anonymous+Pro'),
1536 array('fontname' => 'Allerta+Stencil'),
1537 array('fontname' => 'Allerta'),
1538 array('fontname' => 'Amaranth'),
1539 array('fontname' => 'Anton'),
1540 array('fontname' => 'Architects+Daughter'),
1541 array('fontname' => 'Arimo'),
1542 array('fontname' => 'Artifika'),
1543 array('fontname' => 'Arvo'),
1544 array('fontname' => 'Asset'),
1545 array('fontname' => 'Astloch'),
1546 array('fontname' => 'Bangers'),
1547 array('fontname' => 'Bentham'),
1548 array('fontname' => 'Bevan'),
1549 array('fontname' => 'Bigshot+One'),
1550 array('fontname' => 'Bowlby+One'),
1551 array('fontname' => 'Bowlby+One+SC'),
1552 array('fontname' => 'Brawler'),
1553 array('fontname' => 'Cabin'),
1554 array('fontname' => 'Calligraffitti'),
1555 array('fontname' => 'Candal'),
1556 array('fontname' => 'Cantarell'),
1557 array('fontname' => 'Cardo'),
1558 array('fontname' => 'Carter One'),
1559 array('fontname' => 'Caudex'),
1560 array('fontname' => 'Cedarville+Cursive'),
1561 array('fontname' => 'Cherry+Cream+Soda'),
1562 array('fontname' => 'Chewy'),
1563 array('fontname' => 'Coda'),
1564 array('fontname' => 'Coming+Soon'),
1565 array('fontname' => 'Copse'),
1566 array('fontname' => 'Cousine'),
1567 array('fontname' => 'Covered+By+Your+Grace'),
1568 array('fontname' => 'Crafty+Girls'),
1569 array('fontname' => 'Crimson+Text'),
1570 array('fontname' => 'Crushed'),
1571 array('fontname' => 'Cuprum'),
1572 array('fontname' => 'Damion'),
1573 array('fontname' => 'Dancing+Script'),
1574 array('fontname' => 'Dawning+of+a+New+Day'),
1575 array('fontname' => 'Didact+Gothic'),
1576 array('fontname' => 'Droid+Sans'),
1577 array('fontname' => 'Droid+Sans+Mono'),
1578 array('fontname' => 'Droid+Serif'),
1579 array('fontname' => 'EB+Garamond'),
1580 array('fontname' => 'Expletus+Sans'),
1581 array('fontname' => 'Fontdiner+Swanky'),
1582 array('fontname' => 'Forum'),
1583 array('fontname' => 'Francois+One'),
1584 array('fontname' => 'Geo'),
1585 array('fontname' => 'Give+You+Glory'),
1586 array('fontname' => 'Goblin+One'),
1587 array('fontname' => 'Goudy+Bookletter+1911'),
1588 array('fontname' => 'Gravitas+One'),
1589 array('fontname' => 'Gruppo'),
1590 array('fontname' => 'Hammersmith+One'),
1591 array('fontname' => 'Holtwood+One+SC'),
1592 array('fontname' => 'Homemade+Apple'),
1593 array('fontname' => 'Inconsolata'),
1594 array('fontname' => 'Indie+Flower'),
1595 array('fontname' => 'IM+Fell+DW+Pica'),
1596 array('fontname' => 'IM+Fell+DW+Pica+SC'),
1597 array('fontname' => 'IM+Fell+Double+Pica'),
1598 array('fontname' => 'IM+Fell+Double+Pica+SC'),
1599 array('fontname' => 'IM+Fell+English'),
1600 array('fontname' => 'IM+Fell+English+SC'),
1601 array('fontname' => 'IM+Fell+French+Canon'),
1602 array('fontname' => 'IM+Fell+French+Canon+SC'),
1603 array('fontname' => 'IM+Fell+Great+Primer'),
1604 array('fontname' => 'IM+Fell+Great+Primer+SC'),
1605 array('fontname' => 'Irish+Grover'),
1606 array('fontname' => 'Irish+Growler'),
1607 array('fontname' => 'Istok+Web'),
1608 array('fontname' => 'Josefin+Sans'),
1609 array('fontname' => 'Josefin+Slab'),
1610 array('fontname' => 'Judson'),
1611 array('fontname' => 'Jura'),
1612 array('fontname' => 'Just+Another+Hand'),
1613 array('fontname' => 'Just+Me+Again+Down+Here'),
1614 array('fontname' => 'Kameron'),
1615 array('fontname' => 'Kenia'),
1616 array('fontname' => 'Kranky'),
1617 array('fontname' => 'Kreon'),
1618 array('fontname' => 'Kristi'),
1619 array('fontname' => 'La+Belle+Aurore'),
1620 array('fontname' => 'Lato'),
1621 array('fontname' => 'League+Script'),
1622 array('fontname' => 'Lekton'),
1623 array('fontname' => 'Limelight'),
1624 array('fontname' => 'Lobster'),
1625 array('fontname' => 'Lobster Two'),
1626 array('fontname' => 'Lora'),
1627 array('fontname' => 'Love+Ya+Like+A+Sister'),
1628 array('fontname' => 'Loved+by+the+King'),
1629 array('fontname' => 'Luckiest+Guy'),
1630 array('fontname' => 'Maiden+Orange'),
1631 array('fontname' => 'Mako'),
1632 array('fontname' => 'Maven+Pro'),
1633 array('fontname' => 'Meddon'),
1634 array('fontname' => 'MedievalSharp'),
1635 array('fontname' => 'Megrim'),
1636 array('fontname' => 'Merriweather'),
1637 array('fontname' => 'Metrophobic'),
1638 array('fontname' => 'Michroma'),
1639 array('fontname' => 'Miltonian+Tattoo'),
1640 array('fontname' => 'Miltonian'),
1641 array('fontname' => 'Modern Antiqua'),
1642 array('fontname' => 'Monofett'),
1643 array('fontname' => 'Molengo'),
1644 array('fontname' => 'Mountains of Christmas'),
1645 array('fontname' => 'Muli'),
1646 array('fontname' => 'Neucha'),
1647 array('fontname' => 'Neuton'),
1648 array('fontname' => 'News+Cycle'),
1649 array('fontname' => 'Nixie+One'),
1650 array('fontname' => 'Nobile'),
1651 array('fontname' => 'Nova+Cut'),
1652 array('fontname' => 'Nova+Flat'),
1653 array('fontname' => 'Nova+Mono'),
1654 array('fontname' => 'Nova+Oval'),
1655 array('fontname' => 'Nova+Round'),
1656 array('fontname' => 'Nova+Script'),
1657 array('fontname' => 'Nova+Slim'),
1658 array('fontname' => 'Nova+Square'),
1659 array('fontname' => 'Nunito'),
1660 array('fontname' => 'OFL+Sorts+Mill+Goudy+TT'),
1661 array('fontname' => 'Old+Standard+TT'),
1662 array('fontname' => 'Open+Sans'),
1663 array('fontname' => 'Orbitron'),
1664 array('fontname' => 'Oswald'),
1665 array('fontname' => 'Over+the+Rainbow'),
1666 array('fontname' => 'Reenie+Beanie'),
1667 array('fontname' => 'Pacifico'),
1668 array('fontname' => 'Patrick+Hand'),
1669 array('fontname' => 'Paytone+One'),
1670 array('fontname' => 'Permanent+Marker'),
1671 array('fontname' => 'Philosopher'),
1672 array('fontname' => 'Play'),
1673 array('fontname' => 'Playfair+Display'),
1674 array('fontname' => 'Podkova'),
1675 array('fontname' => 'PT+Sans'),
1676 array('fontname' => 'PT+Sans+Narrow'),
1677 array('fontname' => 'PT+Serif'),
1678 array('fontname' => 'PT+Serif Caption'),
1679 array('fontname' => 'Puritan'),
1680 array('fontname' => 'Quattrocento'),
1681 array('fontname' => 'Quattrocento+Sans'),
1682 array('fontname' => 'Radley'),
1683 array('fontname' => 'Redressed'),
1684 array('fontname' => 'Rock+Salt'),
1685 array('fontname' => 'Rokkitt'),
1686 array('fontname' => 'Ruslan+Display'),
1687 array('fontname' => 'Schoolbell'),
1688 array('fontname' => 'Shadows+Into+Light'),
1689 array('fontname' => 'Shanti'),
1690 array('fontname' => 'Sigmar+One'),
1691 array('fontname' => 'Six+Caps'),
1692 array('fontname' => 'Slackey'),
1693 array('fontname' => 'Smythe'),
1694 array('fontname' => 'Special+Elite'),
1695 array('fontname' => 'Stardos+Stencil'),
1696 array('fontname' => 'Sue+Ellen+Francisco'),
1697 array('fontname' => 'Sunshiney'),
1698 array('fontname' => 'Swanky+and+Moo+Moo'),
1699 array('fontname' => 'Syncopate'),
1700 array('fontname' => 'Tangerine'),
1701 array('fontname' => 'Tenor+Sans'),
1702 array('fontname' => 'Terminal+Dosis+Light'),
1703 array('fontname' => 'The+Girl+Next+Door'),
1704 array('fontname' => 'Tinos'),
1705 array('fontname' => 'Ubuntu'),
1706 array('fontname' => 'Ultra'),
1707 array('fontname' => 'Unkempt'),
1708 array('fontname' => 'UnifrakturMaguntia'),
1709 array('fontname' => 'Varela'),
1710 array('fontname' => 'Varela Round'),
1711 array('fontname' => 'Vibur'),
1712 array('fontname' => 'Vollkorn'),
1713 array('fontname' => 'VT323'),
1714 array('fontname' => 'Waiting+for+the+Sunrise'),
1715 array('fontname' => 'Wallpoet'),
1716 array('fontname' => 'Walter+Turncoat'),
1717 array('fontname' => 'Wire+One'),
1718 array('fontname' => 'Yanone+Kaffeesatz'),
1719 array('fontname' => 'Yeseva+One'),
1720 array('fontname' => 'Zeyada')
1721 );
1722 }
1723 break;
1724 default: {
1725 $error = true;
1726 $data['msg'] = esc_html__('The operation failed', 'vision');
1727 }
1728 break;
1729 }
1730 } else {
1731 $error = true;
1732 $data['msg'] = esc_html__('The operation failed', 'vision');
1733 }
1734
1735 if ($error) {
1736 wp_send_json_error($data);
1737 } else {
1738 wp_send_json_success($data);
1739 }
1740
1741 wp_die(); // this is required to terminate immediately and return a proper response
1742 }
1743
1744 /**
1745 * Ajax delete all data from tables
1746 */
1747 function ajax_delete_data()
1748 {
1749 $error = true;
1750 $data = [];
1751 $data['msg'] = esc_html__('The operation failed, can\'t delete data', 'vision');
1752
1753 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1754 global $wpdb;
1755 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1756
1757 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1758 foreach ($wpdb->get_results("SELECT id FROM {$table}") as $key => $item) {
1759 // [filemanager] delete file
1760 if (wp_is_writable(VISION_PLUGIN_UPLOAD_DIR)) {
1761 $file_json = 'config.json';
1762 $file_main_css = 'main.css';
1763 $file_custom_css = 'custom.css';
1764 $file_root_path = VISION_PLUGIN_UPLOAD_DIR . '/' . $item->id . '/';
1765
1766 if (file_exists($file_root_path . $file_json)) {
1767 wp_delete_file($file_root_path . $file_json);
1768 }
1769 wp_delete_file($file_root_path . $file_main_css);
1770 wp_delete_file($file_root_path . $file_custom_css);
1771
1772 $wp_filesystem = $this->getFileSystem();
1773 if ($wp_filesystem->is_dir($file_root_path)) {
1774 $wp_filesystem->rmdir($file_root_path);
1775 }
1776 }
1777 }
1778
1779 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1780 $result = $wpdb->query("TRUNCATE TABLE {$table}");
1781
1782 if ($result) {
1783 $error = false;
1784 $data['msg'] = esc_html__('All data deleted', 'vision');
1785 }
1786 }
1787
1788 if ($error) {
1789 wp_send_json_error($data);
1790 } else {
1791 wp_send_json_success($data);
1792 }
1793
1794 wp_die(); // this is required to terminate immediately and return a proper response
1795 }
1796
1797 /**
1798 * Ajax settings get data
1799 */
1800 function ajax_modal()
1801 {
1802 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1803 $modalName = sanitize_file_name(filter_input(INPUT_GET, 'name', FILTER_DEFAULT));
1804 $modalPath = plugin_dir_path(dirname(__FILE__)) . 'includes/modal-' . $modalName . '.php';
1805
1806 if (file_exists($modalPath)) {
1807 require_once($modalPath);
1808 }
1809 }
1810
1811 wp_die(); // this is required to terminate immediately and return a proper response
1812 }
1813
1814 function ajax_change_author()
1815 {
1816 $error = false;
1817 $data = [];
1818
1819 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1820 global $wpdb;
1821 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1822 $item_id = (int) filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT);
1823 $author_id = (int) filter_input(INPUT_POST, 'author_id', FILTER_SANITIZE_NUMBER_INT);
1824
1825 $target_user = get_userdata($author_id);
1826 if (!$target_user) {
1827 $error = true;
1828 $data['msg'] = esc_html__('Invalid user', 'vision');
1829 } else {
1830 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1831 $item = $wpdb->get_row(
1832 $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $item_id)
1833 );
1834 // phpcs:enable
1835
1836 $current_user_id = get_current_user_id();
1837
1838 // permission check, admin - any record, other - only own
1839 if ($item && (current_user_can('manage_options') || $current_user_id == $item->author)) {
1840 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1841 $result = $wpdb->update(
1842 $table,
1843 [
1844 'author' => $author_id,
1845 'editor' => $current_user_id,
1846 'modified' => current_time('mysql', 1)
1847 ],
1848 ['id' => $item_id]
1849 );
1850
1851 if ($result !== false) {
1852 $data['id'] = $item_id;
1853 $data['author_name'] = $target_user->display_name;
1854 $data['msg'] = esc_html__('Author changed successfully', 'vision');
1855 } else {
1856 $error = true;
1857 $data['msg'] = esc_html__('Failed to update author', 'vision');
1858 }
1859 } else {
1860 $error = true;
1861 $data['msg'] = esc_html__('You do not have permission to edit this item', 'vision');
1862 }
1863 }
1864 } else {
1865 $error = true;
1866 $data['msg'] = esc_html__('Security check failed', 'vision');
1867 }
1868
1869 if ($error) {
1870 wp_send_json_error($data);
1871 } else {
1872 wp_send_json_success($data);
1873 }
1874
1875 wp_die();
1876 }
1877
1878 function remove_directory($path)
1879 {
1880 global $wp_filesystem;
1881
1882 if (empty($wp_filesystem)) {
1883 require_once ABSPATH . '/wp-admin/includes/file.php';
1884 WP_Filesystem();
1885 }
1886
1887 if (!$wp_filesystem) {
1888 return false;
1889 }
1890
1891 return $wp_filesystem->rmdir($path, true);
1892 }
1893 }