PluginProbe
Vision – Interactive Image Map with Hotspots Builder / 1.12.1
Vision – Interactive Image Map with Hotspots Builder v1.12.1
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.1, at includes/plugin.php

1,912 lines 73.0 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 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
661 if (!($page === 'vision' || $page === 'vision_item' || $page === 'vision_settings')) {
662 return;
663 }
664
665 $promo = get_option('vision_picpoints_promo');
666 if (!is_array($promo)) {
667 $promo = ['status' => 'active', 'remind_at' => 0];
668 }
669
670 $status = isset($promo['status']) ? $promo['status'] : 'active';
671 $remind_at = isset($promo['remind_at']) ? (int) $promo['remind_at'] : 0;
672
673 if ($status === 'dismissed') {
674 return;
675 }
676 if ($status === 'remind' && time() < $remind_at) {
677 return;
678 }
679
680 wp_enqueue_script('jquery');
681
682 $nonce = wp_create_nonce('vision_ajax');
683 $ajax_url = esc_url(admin_url('admin-ajax.php'));
684
685 echo '<div class="notice notice-info vision-picpoints-notice" style="position:relative;">';
686
687 echo '<div style="display:flex;gap:100px;padding:10px 0;">';
688
689 echo '<div style="max-width:600px;">';
690 echo '<p style="font-weight:600;font-size:14px;">';
691 echo esc_html__('To our amazing Vision community!', 'vision');
692 echo '</p>';
693 echo '<p style="font-weight:600;font-size:14px;margin:0 0 10px 0;">';
694 echo esc_html__('We are proud to introduce PicPoints, our next-generation interactive image map builder. 🚀', 'vision');
695 echo '</p>';
696 echo '<p style="margin-bottom:10px;">';
697 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');
698 echo '</p>';
699 echo '<p style="margin-bottom:10px;">';
700 echo esc_html__('Why you will love it:', 'vision');
701 echo '</p>';
702 echo '<p>';
703 echo esc_html__('�
704 Easy-to-use editor - place markers, draw shapes and add polygons just like in Figma.', 'vision');
705 echo '</p>';
706 echo '<p>';
707 echo esc_html__("�
708 No theme conflicts - your maps always look perfect and won't break your site's design.", 'vision');
709 echo '</p>';
710 echo '<p>';
711 echo esc_html__('�
712 Fast & smooth - works flawlessly even with large floor plans or interactive product catalogs.', 'vision');
713 echo '</p>';
714 echo '</div>';
715
716 echo '<style>@media(max-width:1279px){.vision-mod-hide{display:none!important}}</style>';
717 echo '<iframe class="vision-mod-hide" style="padding:10px; box-sizing: border-box; border-radius:5px; box-shadow: 0 0 2px rgba(0, 0, 0, .7); height:240px; width:auto; aspect-ratio:16/9;" src="https://www.youtube.com/embed/_HStw93NAf8" frameborder="0" allow="encrypted-media; web-share" allowfullscreen></iframe>';
718
719 echo '</div>';
720
721 echo '<p style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap;justify-content:space-between;">';
722 echo '<span style="display:flex;gap:8px;flex-wrap:wrap;">';
723 echo '<a href="https://wordpress.org/plugins/picpoints/" target="_blank" rel="noopener" class="button button-primary button-vision-picpoints-promo">Try Free Version</a>';
724 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>';
725 echo '</span>';
726 echo '<span style="display:flex;gap:8px;flex-wrap:wrap;">';
727 echo '<button type="button" class="button button-vision-picpoints-remind">' . esc_html__('Remind me later', 'vision') . '</button>';
728 echo '<button type="button" class="button button-vision-picpoints-dismiss">' . esc_html__('No, thanks', 'vision') . '</button>';
729 echo '</span>';
730 echo '</p>';
731 echo '</div>';
732
733 // Inline JS
734 echo '<script>
735 (function($){
736 $(function(){
737 var $notice = $(".vision-picpoints-notice");
738 var ajaxUrl = ' . wp_json_encode($ajax_url) . ';
739 var nonce = ' . wp_json_encode($nonce) . ';
740
741 function sendAction(action){
742 $.post(ajaxUrl, {
743 action: "vision_ajax_picpoints_promo",
744 nonce: nonce,
745 promo_action: action
746 });
747 }
748
749 $notice.on("click", ".button-vision-picpoints-dismiss", function(){
750 sendAction("dismiss");
751 $notice.slideUp(function(){ $notice.remove(); });
752 });
753
754 $notice.on("click", ".button-vision-picpoints-remind", function(){
755 sendAction("remind");
756 $notice.slideUp(function(){ $notice.remove(); });
757 });
758
759 $notice.on("click", ".button-vision-picpoints-promo", function(){
760 $notice.slideUp(function(){ $notice.remove(); });
761 });
762 });
763 })(jQuery);
764 </script>';
765 }
766
767 /**
768 * Ajax: handle PicPoints promo action (dismiss / remind)
769 */
770 function ajax_picpoints_promo()
771 {
772 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
773 wp_send_json_error(['msg' => esc_html__('The operation failed', 'vision')]);
774 }
775
776 if (!current_user_can('manage_options')) {
777 wp_send_json_error(['msg' => esc_html__('Permission denied', 'vision')]);
778 }
779
780 $action = sanitize_key(filter_input(INPUT_POST, 'promo_action'));
781 $now = time();
782
783 if ($action === 'dismiss') {
784 update_option('vision_picpoints_promo', [
785 'status' => 'dismissed',
786 'remind_at' => 0,
787 ], false);
788 } elseif ($action === 'remind') {
789 update_option('vision_picpoints_promo', [
790 'status' => 'remind',
791 'remind_at' => $now + MONTH_IN_SECONDS,
792 ], false);
793 } else {
794 wp_send_json_error(['msg' => esc_html__('Invalid action', 'vision')]);
795 }
796
797 wp_send_json_success(['msg' => 'ok']);
798 wp_die();
799 }
800
801 /**
802 * Fires at the beginning of the content section in an admin page
803 */
804 function in_admin_header()
805 {
806 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
807
808 if (!(($page === 'vision') || ($page === 'vision_item') || ($page === 'vision_settings'))) {
809 return;
810 }
811
812 remove_all_actions('admin_notices');
813 remove_all_actions('all_admin_notices');
814 add_action('admin_notices', [$this, 'admin_notices']);
815 add_action('admin_notices', [$this, 'admin_notices_picpoints']);
816 }
817
818 /**
819 * Register the administration menu for this plugin into the WordPress Dashboard menu.
820 */
821 function admin_menu()
822 {
823 // add "edit_posts" if we want to give access to author, editor and contributor roles
824 add_menu_page(esc_html__('Vision', 'vision'), esc_html__('Vision', 'vision'), 'read', 'vision', [$this, 'admin_menu_page_items'], 'dashicons-format-image');
825 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('All Items', 'vision'), 'read', 'vision', [$this, 'admin_menu_page_items']);
826 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('Add New', 'vision'), 'read', 'vision_item', [$this, 'admin_menu_page_item']);
827 add_submenu_page('vision', esc_html__('Vision', 'vision'), esc_html__('Settings', 'vision'), 'manage_options', 'vision_settings', [$this, 'admin_menu_page_settings']);
828
829 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']);
830
831 }
832
833 function admin_menu_highlight($submenu_file, $parent_file)
834 {
835 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
836 if (in_array($page, ['vision_item'])) {
837 $id = sanitize_key(filter_input(INPUT_GET, 'id', FILTER_DEFAULT));
838 if (!empty($id)) {
839 $submenu_file = 'vision';
840 }
841 }
842 return $submenu_file;
843 }
844
845 function admin_footer()
846 {
847 if (get_current_screen() && get_current_screen()->base !== 'plugins') {
848 return;
849 }
850
851 $globals = [
852 'token' => $this->get_token(),
853 'ajax' => [
854 'url' => VISION_FEEDBACK_URL
855 ]
856 ];
857
858 wp_enqueue_style('vision-feedback', VISION_PLUGIN_URL . 'assets/css/feedback.css', [], VISION_PLUGIN_VERSION);
859 wp_enqueue_script('vision-feedback', VISION_PLUGIN_URL . 'assets/js/feedback.js', ['jquery'], VISION_PLUGIN_VERSION, false);
860 wp_localize_script('vision-feedback', 'vision_feedback_globals', $globals);
861
862 require_once(plugin_dir_path(dirname(__FILE__)) . 'templates/feedback.php');
863 }
864
865 function get_token()
866 {
867 global $wp_version;
868 $current_user = wp_get_current_user();
869
870 $data = [
871 'plugin_name' => VISION_PLUGIN_NAME,
872 'plugin_version' => VISION_PLUGIN_VERSION,
873 'wordpress' => $wp_version,
874 'php' => PHP_VERSION,
875 'email' => $current_user->user_email,
876 'site' => trim(str_replace(['http://', 'https://'], '', get_site_url()), '/')
877 ];
878 return base64_encode(wp_json_encode($data));
879 }
880
881 /**
882 * Custom redirects
883 */
884 function page_redirects()
885 {
886 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
887
888 if ($page === 'vision') {
889 $action = sanitize_key(filter_input(INPUT_GET, 'action', FILTER_DEFAULT));
890 if ($action) {
891 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
892 $url = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']));
893
894 $url = remove_query_arg(['action', 'id', '_wpnonce'], $url);
895 header('Refresh:0; url="' . $url . '"', true, 303);
896 //wp_redirect($url); // does not work delete and dublicate operations on XAMPP
897 }
898 }
899 }
900
901 /**
902 * Show admin menu items page
903 */
904 function admin_menu_page_items()
905 {
906 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
907
908 if ($page === 'vision') {
909 $plugin_url = plugin_dir_url(dirname(__FILE__));
910 $upload_dir = wp_upload_dir();
911
912 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
913 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
914 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
915
916 // global settings to help ajax work
917 $globals = [
918 'plan' => VISION_PLUGIN_PLAN,
919 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
920 'upload_url' => $upload_dir['baseurl'],
921 'ajax_url' => admin_url('admin-ajax.php'),
922 'ajax_nonce' => wp_create_nonce('vision_ajax'),
923 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
924 ];
925 $globals['ajax_action_update'] = $this->ajax_action_item_update_status;
926 $globals['ajax_action_change_author'] = $this->ajax_action_change_author;
927
928 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/list-table-items.php');
929 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-items.php');
930
931 wp_localize_script('vision_admin', 'vision_globals', $globals);
932 }
933 }
934
935 /**
936 * Show admin menu item page
937 */
938 function admin_menu_page_item()
939 {
940 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
941 if ($page === 'vision_item') {
942 $id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
943
944 $plugin_url = plugin_dir_url(dirname(__FILE__));
945 $upload_dir = wp_upload_dir();
946
947 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
948
949 if (VISION_PLUGIN_PLAN == 'lite' && !$id) {
950 global $wpdb;
951 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
952
953 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
954 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
955
956 if ($count >= 1) {
957 echo '<p>Vision: ' . esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision') . '</p>';
958 return;
959 }
960 }
961
962 wp_enqueue_style('vision_notify', $plugin_url . 'assets/css/notify.css', [], VISION_PLUGIN_VERSION, 'all');
963 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
964 wp_enqueue_style('vision_vision_effects', $plugin_url . 'assets/css/vision-effects.css', [], VISION_PLUGIN_VERSION, 'all');
965
966 wp_enqueue_script('vision_notify', $plugin_url . 'assets/js/notify.js', ['jquery'], VISION_PLUGIN_VERSION, false);
967 wp_enqueue_script('vision_ace', $plugin_url . 'assets/vendor/ace/ace.js', [], VISION_PLUGIN_VERSION, false);
968 wp_enqueue_script('vision_url', $plugin_url . 'assets/vendor/url/url.js', [], VISION_PLUGIN_VERSION, false);
969 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
970
971 wp_enqueue_media();
972
973 // global settings to help ajax work
974 $globals = [
975 'plan' => VISION_PLUGIN_PLAN,
976 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
977 'msg_custom_js_error' => esc_html__('Custom js code error', 'vision'),
978 'msg_layer_id_error' => esc_html__('The layer ID should be unique', 'vision'),
979 'wp_base_url' => get_site_url(),
980 'upload_base_url' => $upload_dir['baseurl'],
981 'plugin_base_url' => $plugin_url,
982 'ajax_url' => admin_url('admin-ajax.php'),
983 'ajax_nonce' => wp_create_nonce('vision_ajax'),
984 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
985 ];
986
987 $globals['ajax_action_get'] = $this->ajax_action_settings_get;
988 $globals['ajax_action_save_chunk'] = $this->ajax_action_item_save_chunk;
989 $globals['ajax_action_save_complete'] = $this->ajax_action_item_save_complete;
990 $globals['ajax_action_update'] = $this->ajax_action_item_update;
991 $globals['ajax_action_modal'] = $this->ajax_action_modal;
992 $globals['ajax_item_id'] = $id;
993 $globals['settings'] = NULL;
994 $globals['config'] = NULL;
995
996 $settings_key = 'vision_settings';
997 $settings_value = get_option($settings_key);
998 if ($settings_value) {
999 $globals['settings'] = unserialize($settings_value); // json_encode(unserialize($settings_value)) problem with double quotes
1000 }
1001
1002 // get item data from DB
1003 if ($id) {
1004 global $wpdb;
1005 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1006
1007 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1008 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $id);
1009 $item = $wpdb->get_row($query, OBJECT);
1010 // phpcs:enable
1011
1012 if ($item) {
1013 $globals['config'] = unserialize($item->data); // json_encode(unserialize($item->data)) problem with double quotes
1014 }
1015
1016 if ($item) {
1017 $current_user_id = get_current_user_id();
1018 if (!current_user_can('manage_options') && $current_user_id != $item->author) {
1019 echo '<p>' . esc_html__('You do not have permission to edit this item.', 'vision') . '</p>';
1020 return;
1021 }
1022 }
1023 } else {
1024 // new item
1025 $item = (object) [
1026 'author' => get_current_user_id(),
1027 'editor' => get_current_user_id(),
1028 'created' => current_time('mysql', 1),
1029 'modified' => current_time('mysql', 1)
1030 ];
1031 }
1032
1033 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-item.php');
1034
1035 // set global settings
1036 wp_localize_script('vision_admin', 'vision_globals', $globals);
1037 }
1038 }
1039
1040 /**
1041 * Show admin menu settings page
1042 */
1043 function admin_menu_page_settings()
1044 {
1045 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
1046 if ($page === 'vision_settings') {
1047 $plugin_url = plugin_dir_url(dirname(__FILE__));
1048
1049 wp_enqueue_style('vision_admin', $plugin_url . 'assets/css/admin.css', [], VISION_PLUGIN_VERSION, 'all');
1050 wp_enqueue_style('vision_lucide', $plugin_url . 'assets/vendor/lucide/lucide.css', [], VISION_PLUGIN_VERSION, 'all');
1051 wp_enqueue_script('vision_admin', $plugin_url . 'assets/js/admin.js', ['jquery'], VISION_PLUGIN_VERSION, false);
1052
1053 // global settings to help ajax work
1054 $globals = [
1055 'plan' => VISION_PLUGIN_PLAN,
1056 'msg_pro_title' => esc_html__('Available only in Pro version', 'vision'),
1057 'ajax_url' => admin_url('admin-ajax.php'),
1058 'ajax_nonce' => wp_create_nonce('vision_ajax'),
1059 'ajax_msg_error' => esc_html__('Uncaught Error', 'vision') //Look at the console (F12 or Ctrl+Shift+I, Console tab) for more information
1060 ];
1061
1062 $globals['ajax_action_update'] = $this->ajax_action_settings_update;
1063 $globals['ajax_action_get'] = $this->ajax_action_settings_get;
1064 $globals['ajax_action_modal'] = $this->ajax_action_modal;
1065 $globals['ajax_action_delete_data'] = $this->ajax_action_delete_data;
1066 $globals['config'] = NULL;
1067
1068 // read settings
1069 $settings_key = 'vision_settings';
1070 $settings_value = get_option($settings_key);
1071 if ($settings_value) {
1072 $globals['config'] = wp_json_encode(unserialize($settings_value));
1073 }
1074
1075 require_once(plugin_dir_path(dirname(__FILE__)) . 'includes/page-settings.php');
1076
1077 wp_localize_script('vision_admin', 'vision_globals', $globals);
1078 }
1079 }
1080
1081 /**
1082 * Show admin menu upgrade to pro page
1083 */
1084 function admin_menu_page_upgrade_to_pro()
1085 {
1086 $page = sanitize_key(filter_input(INPUT_GET, 'page', FILTER_DEFAULT));
1087 if ($page === 'vision_upgrade_to_pro') {
1088 echo '<script>window.location = "https://1.envato.market/getvision"</script>';
1089 }
1090 }
1091
1092 /**
1093 * Ajax update item state
1094 */
1095 function ajax_item_update_status()
1096 {
1097 $error = false;
1098 $data = [];
1099 $config = filter_input(INPUT_POST, 'config');
1100
1101 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1102 global $wpdb;
1103 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1104
1105 $config = json_decode($config);
1106 $result = false;
1107
1108 if (isset($config->id) && isset($config->active)) {
1109 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1110 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $config->id);
1111 $item = $wpdb->get_row($query, OBJECT);
1112 // phpcs:enable
1113
1114 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1115 $itemData = unserialize($item->data);
1116 $itemData->active = $config->active;
1117
1118 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1119 $result = $wpdb->update(
1120 $table,
1121 ['active' => $itemData->active, 'data' => serialize($itemData)],
1122 ['id' => $config->id]
1123 );
1124 }
1125 }
1126
1127 if ($result) {
1128 $data['id'] = $config->id;
1129 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1130 } else {
1131 $error = true;
1132 $data['msg'] = esc_html__('The operation failed, can\'t update item', 'vision');
1133 }
1134 } else {
1135 $error = true;
1136 $data['msg'] = esc_html__('The operation failed', 'vision');
1137 }
1138
1139 if ($error) {
1140 wp_send_json_error($data);
1141 } else {
1142 wp_send_json_success($data);
1143 }
1144
1145 wp_die(); // this is required to terminate immediately and return a proper response
1146 }
1147
1148 /**
1149 * Ajax update item data
1150 */
1151 function ajax_item_update()
1152 {
1153 $error = false;
1154 $data = [];
1155
1156 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1157 global $wpdb;
1158 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1159
1160 $inputId = filter_input(INPUT_POST, 'id');
1161 $inputData = filter_input(INPUT_POST, 'data');
1162 $inputConfig = filter_input(INPUT_POST, 'config');
1163 $itemData = json_decode($inputData);
1164 $itemConfig = json_decode($inputConfig);
1165 $flag = true;
1166
1167 if (VISION_PLUGIN_PLAN == 'lite' && !$inputId) {
1168 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1169 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
1170
1171 if ($count >= 1) {
1172 $flag = false;
1173 $error = true;
1174 $data['msg'] = esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision');
1175 }
1176 }
1177
1178 if ($itemData === NULL || $itemConfig === NULL) {
1179 $flag = false;
1180 $error = true;
1181 $data['msg'] = 'Error decoding JSON: ' . json_last_error_msg();
1182 }
1183
1184 if ($flag) {
1185 $itemConfig->modified = current_time('mysql', 1);
1186
1187 if ($inputId) {
1188 $result = false;
1189
1190 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1191 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $inputId);
1192 $item = $wpdb->get_row($query, OBJECT);
1193 // phpcs:enable
1194
1195 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1196 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1197
1198 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1199 $result = $wpdb->update(
1200 $table,
1201 [
1202 'title' => $itemData->title,
1203 'slug' => $itemData->slug,
1204 'active' => $itemData->active,
1205 'data' => serialize($itemData),
1206 'config' => serialize($itemConfig),
1207 //'author' => get_current_user_id(),
1208 'editor' => get_current_user_id(),
1209 //'date' => NULL,
1210 'modified' => current_time('mysql', 1)
1211 ],
1212 ['id' => $inputId]
1213 );
1214 }
1215
1216 if ($result) {
1217 $data['id'] = $inputId;
1218 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1219 } else {
1220 $error = true;
1221 $data['msg'] = esc_html__('The operation failed, can\'t update item', 'vision');
1222 }
1223 } else {
1224 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1225
1226 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1227 $result = $wpdb->insert(
1228 $table,
1229 [
1230 'title' => $itemData->title,
1231 'slug' => $itemData->slug,
1232 'active' => $itemData->active,
1233 'data' => serialize($itemData),
1234 'config' => serialize($itemConfig),
1235 'author' => get_current_user_id(),
1236 'editor' => get_current_user_id(),
1237 'created' => current_time('mysql', 1),
1238 'modified' => current_time('mysql', 1)
1239 ]
1240 );
1241
1242 if ($result) {
1243 $data['id'] = $inputId = $wpdb->insert_id;
1244 $data['msg'] = esc_html__('The item was successfully created', 'vision');
1245 } else {
1246 $error = true;
1247 $data['msg'] = esc_html__('The operation failed, can\'t create item', 'vision');
1248 }
1249 }
1250 }
1251 } else {
1252 $error = true;
1253 $data['msg'] = esc_html__('The operation failed', 'vision');
1254 }
1255
1256 if ($error) {
1257 wp_send_json_error($data);
1258 } else {
1259 wp_send_json_success($data);
1260 }
1261
1262 wp_die(); // this is required to terminate immediately and return a proper response
1263 }
1264
1265 /**
1266 * Ajax save item data
1267 */
1268 function ajax_item_save_chunk()
1269 {
1270 try {
1271 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
1272 throw new Exception('Nonce verification failed');
1273 }
1274
1275 $session_id = sanitize_text_field(filter_input(INPUT_POST, 'session_id'));
1276 $chunk_index = (int)filter_input(INPUT_POST, 'chunk_index');
1277 $total_chunks = (int)filter_input(INPUT_POST, 'total_chunks');
1278 $chunk = filter_input(INPUT_POST, 'chunk');
1279 $data_type = sanitize_text_field(filter_input(INPUT_POST, 'data_type')); // 'data' или 'config'
1280 $is_last_chunk = filter_input(INPUT_POST, 'is_last_chunk') === '1';
1281 $item_id = (int)filter_input(INPUT_POST, 'item_id');
1282
1283 if (empty($session_id) || !is_string($session_id)) {
1284 throw new Exception('Invalid session ID');
1285 }
1286
1287 if (!in_array($data_type, ['data', 'config'], true)) {
1288 throw new Exception('Invalid data_type');
1289 }
1290
1291 if ($chunk_index < 0 || $total_chunks < 1 || $chunk_index >= $total_chunks) {
1292 throw new Exception('Invalid chunk index or total chunks');
1293 }
1294
1295 if (strlen($chunk) > 5 * 1024 * 1024) {
1296 throw new Exception('Chunk too large');
1297 }
1298
1299 $upload_dir = wp_upload_dir();
1300 $chunk_dir = $upload_dir['basedir'] . '/vision/chunks/' . $session_id . '/';
1301
1302 if (!file_exists($chunk_dir)) {
1303 wp_mkdir_p($chunk_dir);
1304 }
1305
1306 $chunk_file = $chunk_dir . $data_type . '_' . $chunk_index . '.chunk';
1307
1308 $wp_filesystem = $this->getFileSystem();
1309 if (!$wp_filesystem->put_contents($chunk_file, $chunk)) {
1310 throw new Exception('Failed to write chunk');
1311 }
1312
1313 wp_send_json_success([
1314 'status' => 'chunk_saved',
1315 'type' => $data_type,
1316 'index' => $chunk_index
1317 ]);
1318 } catch (Exception $e) {
1319 wp_send_json_error($e->getMessage(), 400);
1320 }
1321
1322 wp_die();
1323 }
1324
1325 /**
1326 * Ajax save item data complete
1327 */
1328 function ajax_item_save_complete()
1329 {
1330 $data = [];
1331
1332 try {
1333 if (!check_ajax_referer('vision_ajax', 'nonce', false)) {
1334 throw new Exception('Nonce verification failed');
1335 }
1336
1337 $session_id = sanitize_text_field(filter_input(INPUT_POST, 'session_id'));
1338 $item_id = (int)filter_input(INPUT_POST, 'item_id');
1339
1340 $upload_dir = wp_upload_dir();
1341 $chunk_dir = $upload_dir['basedir'] . '/vision/chunks/' . $session_id . '/';
1342
1343 $collected_data = [
1344 'data' => '',
1345 'config' => ''
1346 ];
1347
1348 foreach (['data', 'config'] as $type) {
1349 $chunks = glob($chunk_dir . $type . '_*.chunk');
1350
1351 if (count($chunks) === 0) {
1352 throw new Exception("No chunks found for $type");
1353 }
1354
1355 natsort($chunks);
1356
1357 foreach ($chunks as $chunk_file) {
1358 $collected_data[$type] .= file_get_contents($chunk_file);
1359 wp_delete_file($chunk_file);
1360 }
1361
1362 $collected_data[$type] = json_decode($collected_data[$type]);
1363
1364 if (json_last_error() !== JSON_ERROR_NONE) {
1365 throw new Exception("Invalid JSON for $type: " . json_last_error_msg());
1366 }
1367 }
1368
1369 $this->remove_directory($chunk_dir);
1370
1371 // save to DB
1372 global $wpdb;
1373 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1374
1375 if (VISION_PLUGIN_PLAN == 'lite' && !$item_id) {
1376 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1377 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
1378
1379 if ($count >= 1) {
1380 throw new Exception(esc_html__('You can create only 1 map. If you need more, upgrade to the pro version.', 'vision'));
1381 }
1382 }
1383
1384 $itemData = $collected_data['data'];
1385 $itemConfig = $collected_data['config'];
1386 $itemConfig->modified = current_time('mysql', 1);
1387
1388 if ($item_id) {
1389 $result = false;
1390
1391 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1392 $query = $wpdb->prepare("SELECT * FROM {$table} WHERE id=%s", $item_id);
1393 $item = $wpdb->get_row($query, OBJECT);
1394 // phpcs:enable
1395
1396 if ($item && (current_user_can('manage_options') || get_current_user_id() == $item->author)) {
1397 $itemData->slug = sanitize_title(($itemData->slug ? $itemData->slug : $itemData->title));
1398
1399 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1400 $result = $wpdb->update(
1401 $table,
1402 [
1403 'title' => $itemData->title,
1404 'slug' => $itemData->slug,
1405 'active' => $itemData->active,
1406 'data' => serialize($itemData),
1407 'config' => serialize($itemConfig),
1408 //'author' => get_current_user_id(),
1409 'editor' => get_current_user_id(),
1410 //'date' => NULL,
1411 'modified' => current_time('mysql', 1)
1412 ],
1413 ['id' => $item_id]
1414 );
1415 }
1416
1417 if ($result) {
1418 $data['id'] = $item_id;
1419 $data['msg'] = esc_html__('The item was successfully updated', 'vision');
1420 wp_send_json_success($data);
1421 } else {
1422 throw new Exception(esc_html__('The operation failed, can\'t update item', 'vision'));
1423 }
1424 } else {
1425 $itemData->slug = sanitize_title($itemData->slug ? $itemData->slug : $itemData->title);
1426
1427 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1428 $result = $wpdb->insert(
1429 $table,
1430 [
1431 'title' => $itemData->title,
1432 'slug' => $itemData->slug,
1433 'active' => $itemData->active,
1434 'data' => serialize($itemData),
1435 'config' => serialize($itemConfig),
1436 'author' => get_current_user_id(),
1437 'editor' => get_current_user_id(),
1438 'created' => current_time('mysql', 1),
1439 'modified' => current_time('mysql', 1)
1440 ]
1441 );
1442
1443 if ($result) {
1444 $data['id'] = $item_id = $wpdb->insert_id;
1445 $data['msg'] = esc_html__('The item was successfully created', 'vision');
1446 wp_send_json_success($data);
1447 } else {
1448 throw new Exception(esc_html__('The operation failed, can\'t create item', 'vision'));
1449 }
1450 }
1451 } catch (Exception $e) {
1452 $data['msg'] = $e->getMessage();
1453 wp_send_json_error($data, 400);
1454 }
1455 }
1456
1457 /**
1458 * Ajax update settings data
1459 */
1460 function ajax_settings_update()
1461 {
1462 $error = false;
1463 $data = [];
1464 $config = filter_input(INPUT_POST, 'config');
1465 $config = json_decode($config);
1466
1467 if ($config !== NULL) {
1468 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1469 $settings_key = 'vision_settings';
1470 $settings_value = serialize($config);
1471 $result = false;
1472
1473 if (get_option($settings_key) == false) {
1474 $autoload = 'no';
1475 $result = add_option($settings_key, $settings_value, "", $autoload);
1476 } else {
1477 $old_settings_value = get_option($settings_key);
1478 if ($old_settings_value === $settings_value) {
1479 $result = true;
1480 } else {
1481 $result = update_option($settings_key, $settings_value);
1482 }
1483 }
1484
1485 if ($result) {
1486 $data['msg'] = esc_html__('The settings were successfully updated', 'vision');
1487 } else {
1488 $error = true;
1489 $data['msg'] = esc_html__('The operation failed, can\'t update settings', 'vision');
1490 }
1491 }
1492 } else {
1493 $error = true;
1494 $data['msg'] = 'Error decoding JSON: ' . json_last_error_msg();
1495 }
1496
1497 if ($error) {
1498 wp_send_json_error($data);
1499 } else {
1500 wp_send_json_success($data);
1501 }
1502
1503 wp_die(); // this is required to terminate immediately and return a proper response
1504 }
1505
1506 /**
1507 * Ajax settings get data
1508 */
1509 function ajax_settings_get()
1510 {
1511 $error = false;
1512 $data = [];
1513 $type = sanitize_key(filter_input(INPUT_POST, 'type', FILTER_DEFAULT));
1514
1515 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1516 switch ($type) {
1517 case 'roles': {
1518 $data['list'] = [];
1519
1520 $roles = wp_roles()->roles;
1521 foreach ($roles as $key => $role) {
1522 if (array_key_exists('read', $role['capabilities'])) {
1523 array_push($data['list'], ['id' => $key, 'name' => translate_user_role($role['name'])]);
1524 }
1525 }
1526 }
1527 break;
1528 case 'themes': {
1529 $data['list'] = [];
1530
1531 $files = glob(plugin_dir_path(dirname(__FILE__)) . 'assets/themes/*.css');
1532 foreach ($files as $file) {
1533 $filename = basename($file, '.css');
1534 array_push($data['list'], ['id' => $filename, 'title' => str_replace('-', ' ', $filename)]);
1535 }
1536 }
1537 break;
1538 case 'editor-themes': {
1539 $data['list'] = [];
1540
1541 $files = glob(plugin_dir_path(dirname(__FILE__)) . 'assets/vendor/ace/theme-*.js');
1542 foreach ($files as $file) {
1543 $filename = str_replace('theme-', '', basename($file, '.js'));
1544 array_push($data['list'], ['id' => $filename, 'title' => str_replace('_', ' ', $filename)]);
1545 }
1546 }
1547 break;
1548 case 'fonts': {
1549 $data['list'] = array(
1550 array('fontname' => 'none'),
1551 array('fontname' => 'Aclonica'),
1552 array('fontname' => 'Allan'),
1553 array('fontname' => 'Annie+Use+Your+Telescope'),
1554 array('fontname' => 'Anonymous+Pro'),
1555 array('fontname' => 'Allerta+Stencil'),
1556 array('fontname' => 'Allerta'),
1557 array('fontname' => 'Amaranth'),
1558 array('fontname' => 'Anton'),
1559 array('fontname' => 'Architects+Daughter'),
1560 array('fontname' => 'Arimo'),
1561 array('fontname' => 'Artifika'),
1562 array('fontname' => 'Arvo'),
1563 array('fontname' => 'Asset'),
1564 array('fontname' => 'Astloch'),
1565 array('fontname' => 'Bangers'),
1566 array('fontname' => 'Bentham'),
1567 array('fontname' => 'Bevan'),
1568 array('fontname' => 'Bigshot+One'),
1569 array('fontname' => 'Bowlby+One'),
1570 array('fontname' => 'Bowlby+One+SC'),
1571 array('fontname' => 'Brawler'),
1572 array('fontname' => 'Cabin'),
1573 array('fontname' => 'Calligraffitti'),
1574 array('fontname' => 'Candal'),
1575 array('fontname' => 'Cantarell'),
1576 array('fontname' => 'Cardo'),
1577 array('fontname' => 'Carter One'),
1578 array('fontname' => 'Caudex'),
1579 array('fontname' => 'Cedarville+Cursive'),
1580 array('fontname' => 'Cherry+Cream+Soda'),
1581 array('fontname' => 'Chewy'),
1582 array('fontname' => 'Coda'),
1583 array('fontname' => 'Coming+Soon'),
1584 array('fontname' => 'Copse'),
1585 array('fontname' => 'Cousine'),
1586 array('fontname' => 'Covered+By+Your+Grace'),
1587 array('fontname' => 'Crafty+Girls'),
1588 array('fontname' => 'Crimson+Text'),
1589 array('fontname' => 'Crushed'),
1590 array('fontname' => 'Cuprum'),
1591 array('fontname' => 'Damion'),
1592 array('fontname' => 'Dancing+Script'),
1593 array('fontname' => 'Dawning+of+a+New+Day'),
1594 array('fontname' => 'Didact+Gothic'),
1595 array('fontname' => 'Droid+Sans'),
1596 array('fontname' => 'Droid+Sans+Mono'),
1597 array('fontname' => 'Droid+Serif'),
1598 array('fontname' => 'EB+Garamond'),
1599 array('fontname' => 'Expletus+Sans'),
1600 array('fontname' => 'Fontdiner+Swanky'),
1601 array('fontname' => 'Forum'),
1602 array('fontname' => 'Francois+One'),
1603 array('fontname' => 'Geo'),
1604 array('fontname' => 'Give+You+Glory'),
1605 array('fontname' => 'Goblin+One'),
1606 array('fontname' => 'Goudy+Bookletter+1911'),
1607 array('fontname' => 'Gravitas+One'),
1608 array('fontname' => 'Gruppo'),
1609 array('fontname' => 'Hammersmith+One'),
1610 array('fontname' => 'Holtwood+One+SC'),
1611 array('fontname' => 'Homemade+Apple'),
1612 array('fontname' => 'Inconsolata'),
1613 array('fontname' => 'Indie+Flower'),
1614 array('fontname' => 'IM+Fell+DW+Pica'),
1615 array('fontname' => 'IM+Fell+DW+Pica+SC'),
1616 array('fontname' => 'IM+Fell+Double+Pica'),
1617 array('fontname' => 'IM+Fell+Double+Pica+SC'),
1618 array('fontname' => 'IM+Fell+English'),
1619 array('fontname' => 'IM+Fell+English+SC'),
1620 array('fontname' => 'IM+Fell+French+Canon'),
1621 array('fontname' => 'IM+Fell+French+Canon+SC'),
1622 array('fontname' => 'IM+Fell+Great+Primer'),
1623 array('fontname' => 'IM+Fell+Great+Primer+SC'),
1624 array('fontname' => 'Irish+Grover'),
1625 array('fontname' => 'Irish+Growler'),
1626 array('fontname' => 'Istok+Web'),
1627 array('fontname' => 'Josefin+Sans'),
1628 array('fontname' => 'Josefin+Slab'),
1629 array('fontname' => 'Judson'),
1630 array('fontname' => 'Jura'),
1631 array('fontname' => 'Just+Another+Hand'),
1632 array('fontname' => 'Just+Me+Again+Down+Here'),
1633 array('fontname' => 'Kameron'),
1634 array('fontname' => 'Kenia'),
1635 array('fontname' => 'Kranky'),
1636 array('fontname' => 'Kreon'),
1637 array('fontname' => 'Kristi'),
1638 array('fontname' => 'La+Belle+Aurore'),
1639 array('fontname' => 'Lato'),
1640 array('fontname' => 'League+Script'),
1641 array('fontname' => 'Lekton'),
1642 array('fontname' => 'Limelight'),
1643 array('fontname' => 'Lobster'),
1644 array('fontname' => 'Lobster Two'),
1645 array('fontname' => 'Lora'),
1646 array('fontname' => 'Love+Ya+Like+A+Sister'),
1647 array('fontname' => 'Loved+by+the+King'),
1648 array('fontname' => 'Luckiest+Guy'),
1649 array('fontname' => 'Maiden+Orange'),
1650 array('fontname' => 'Mako'),
1651 array('fontname' => 'Maven+Pro'),
1652 array('fontname' => 'Meddon'),
1653 array('fontname' => 'MedievalSharp'),
1654 array('fontname' => 'Megrim'),
1655 array('fontname' => 'Merriweather'),
1656 array('fontname' => 'Metrophobic'),
1657 array('fontname' => 'Michroma'),
1658 array('fontname' => 'Miltonian+Tattoo'),
1659 array('fontname' => 'Miltonian'),
1660 array('fontname' => 'Modern Antiqua'),
1661 array('fontname' => 'Monofett'),
1662 array('fontname' => 'Molengo'),
1663 array('fontname' => 'Mountains of Christmas'),
1664 array('fontname' => 'Muli'),
1665 array('fontname' => 'Neucha'),
1666 array('fontname' => 'Neuton'),
1667 array('fontname' => 'News+Cycle'),
1668 array('fontname' => 'Nixie+One'),
1669 array('fontname' => 'Nobile'),
1670 array('fontname' => 'Nova+Cut'),
1671 array('fontname' => 'Nova+Flat'),
1672 array('fontname' => 'Nova+Mono'),
1673 array('fontname' => 'Nova+Oval'),
1674 array('fontname' => 'Nova+Round'),
1675 array('fontname' => 'Nova+Script'),
1676 array('fontname' => 'Nova+Slim'),
1677 array('fontname' => 'Nova+Square'),
1678 array('fontname' => 'Nunito'),
1679 array('fontname' => 'OFL+Sorts+Mill+Goudy+TT'),
1680 array('fontname' => 'Old+Standard+TT'),
1681 array('fontname' => 'Open+Sans'),
1682 array('fontname' => 'Orbitron'),
1683 array('fontname' => 'Oswald'),
1684 array('fontname' => 'Over+the+Rainbow'),
1685 array('fontname' => 'Reenie+Beanie'),
1686 array('fontname' => 'Pacifico'),
1687 array('fontname' => 'Patrick+Hand'),
1688 array('fontname' => 'Paytone+One'),
1689 array('fontname' => 'Permanent+Marker'),
1690 array('fontname' => 'Philosopher'),
1691 array('fontname' => 'Play'),
1692 array('fontname' => 'Playfair+Display'),
1693 array('fontname' => 'Podkova'),
1694 array('fontname' => 'PT+Sans'),
1695 array('fontname' => 'PT+Sans+Narrow'),
1696 array('fontname' => 'PT+Serif'),
1697 array('fontname' => 'PT+Serif Caption'),
1698 array('fontname' => 'Puritan'),
1699 array('fontname' => 'Quattrocento'),
1700 array('fontname' => 'Quattrocento+Sans'),
1701 array('fontname' => 'Radley'),
1702 array('fontname' => 'Redressed'),
1703 array('fontname' => 'Rock+Salt'),
1704 array('fontname' => 'Rokkitt'),
1705 array('fontname' => 'Ruslan+Display'),
1706 array('fontname' => 'Schoolbell'),
1707 array('fontname' => 'Shadows+Into+Light'),
1708 array('fontname' => 'Shanti'),
1709 array('fontname' => 'Sigmar+One'),
1710 array('fontname' => 'Six+Caps'),
1711 array('fontname' => 'Slackey'),
1712 array('fontname' => 'Smythe'),
1713 array('fontname' => 'Special+Elite'),
1714 array('fontname' => 'Stardos+Stencil'),
1715 array('fontname' => 'Sue+Ellen+Francisco'),
1716 array('fontname' => 'Sunshiney'),
1717 array('fontname' => 'Swanky+and+Moo+Moo'),
1718 array('fontname' => 'Syncopate'),
1719 array('fontname' => 'Tangerine'),
1720 array('fontname' => 'Tenor+Sans'),
1721 array('fontname' => 'Terminal+Dosis+Light'),
1722 array('fontname' => 'The+Girl+Next+Door'),
1723 array('fontname' => 'Tinos'),
1724 array('fontname' => 'Ubuntu'),
1725 array('fontname' => 'Ultra'),
1726 array('fontname' => 'Unkempt'),
1727 array('fontname' => 'UnifrakturMaguntia'),
1728 array('fontname' => 'Varela'),
1729 array('fontname' => 'Varela Round'),
1730 array('fontname' => 'Vibur'),
1731 array('fontname' => 'Vollkorn'),
1732 array('fontname' => 'VT323'),
1733 array('fontname' => 'Waiting+for+the+Sunrise'),
1734 array('fontname' => 'Wallpoet'),
1735 array('fontname' => 'Walter+Turncoat'),
1736 array('fontname' => 'Wire+One'),
1737 array('fontname' => 'Yanone+Kaffeesatz'),
1738 array('fontname' => 'Yeseva+One'),
1739 array('fontname' => 'Zeyada')
1740 );
1741 }
1742 break;
1743 default: {
1744 $error = true;
1745 $data['msg'] = esc_html__('The operation failed', 'vision');
1746 }
1747 break;
1748 }
1749 } else {
1750 $error = true;
1751 $data['msg'] = esc_html__('The operation failed', 'vision');
1752 }
1753
1754 if ($error) {
1755 wp_send_json_error($data);
1756 } else {
1757 wp_send_json_success($data);
1758 }
1759
1760 wp_die(); // this is required to terminate immediately and return a proper response
1761 }
1762
1763 /**
1764 * Ajax delete all data from tables
1765 */
1766 function ajax_delete_data()
1767 {
1768 $error = true;
1769 $data = [];
1770 $data['msg'] = esc_html__('The operation failed, can\'t delete data', 'vision');
1771
1772 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1773 global $wpdb;
1774 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1775
1776 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1777 foreach ($wpdb->get_results("SELECT id FROM {$table}") as $key => $item) {
1778 // [filemanager] delete file
1779 if (wp_is_writable(VISION_PLUGIN_UPLOAD_DIR)) {
1780 $file_json = 'config.json';
1781 $file_main_css = 'main.css';
1782 $file_custom_css = 'custom.css';
1783 $file_root_path = VISION_PLUGIN_UPLOAD_DIR . '/' . $item->id . '/';
1784
1785 if (file_exists($file_root_path . $file_json)) {
1786 wp_delete_file($file_root_path . $file_json);
1787 }
1788 wp_delete_file($file_root_path . $file_main_css);
1789 wp_delete_file($file_root_path . $file_custom_css);
1790
1791 $wp_filesystem = $this->getFileSystem();
1792 if ($wp_filesystem->is_dir($file_root_path)) {
1793 $wp_filesystem->rmdir($file_root_path);
1794 }
1795 }
1796 }
1797
1798 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1799 $result = $wpdb->query("TRUNCATE TABLE {$table}");
1800
1801 if ($result) {
1802 $error = false;
1803 $data['msg'] = esc_html__('All data deleted', 'vision');
1804 }
1805 }
1806
1807 if ($error) {
1808 wp_send_json_error($data);
1809 } else {
1810 wp_send_json_success($data);
1811 }
1812
1813 wp_die(); // this is required to terminate immediately and return a proper response
1814 }
1815
1816 /**
1817 * Ajax settings get data
1818 */
1819 function ajax_modal()
1820 {
1821 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1822 $modalName = sanitize_file_name(filter_input(INPUT_GET, 'name', FILTER_DEFAULT));
1823 $modalPath = plugin_dir_path(dirname(__FILE__)) . 'includes/modal-' . $modalName . '.php';
1824
1825 if (file_exists($modalPath)) {
1826 require_once($modalPath);
1827 }
1828 }
1829
1830 wp_die(); // this is required to terminate immediately and return a proper response
1831 }
1832
1833 function ajax_change_author()
1834 {
1835 $error = false;
1836 $data = [];
1837
1838 if (check_ajax_referer('vision_ajax', 'nonce', false)) {
1839 global $wpdb;
1840 $table = $wpdb->prefix . VISION_PLUGIN_NAME;
1841 $item_id = (int) filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT);
1842 $author_id = (int) filter_input(INPUT_POST, 'author_id', FILTER_SANITIZE_NUMBER_INT);
1843
1844 $target_user = get_userdata($author_id);
1845 if (!$target_user) {
1846 $error = true;
1847 $data['msg'] = esc_html__('Invalid user', 'vision');
1848 } else {
1849 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1850 $item = $wpdb->get_row(
1851 $wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND NOT deleted", $item_id)
1852 );
1853 // phpcs:enable
1854
1855 $current_user_id = get_current_user_id();
1856
1857 // permission check, admin - any record, other - only own
1858 if ($item && (current_user_can('manage_options') || $current_user_id == $item->author)) {
1859 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1860 $result = $wpdb->update(
1861 $table,
1862 [
1863 'author' => $author_id,
1864 'editor' => $current_user_id,
1865 'modified' => current_time('mysql', 1)
1866 ],
1867 ['id' => $item_id]
1868 );
1869
1870 if ($result !== false) {
1871 $data['id'] = $item_id;
1872 $data['author_name'] = $target_user->display_name;
1873 $data['msg'] = esc_html__('Author changed successfully', 'vision');
1874 } else {
1875 $error = true;
1876 $data['msg'] = esc_html__('Failed to update author', 'vision');
1877 }
1878 } else {
1879 $error = true;
1880 $data['msg'] = esc_html__('You do not have permission to edit this item', 'vision');
1881 }
1882 }
1883 } else {
1884 $error = true;
1885 $data['msg'] = esc_html__('Security check failed', 'vision');
1886 }
1887
1888 if ($error) {
1889 wp_send_json_error($data);
1890 } else {
1891 wp_send_json_success($data);
1892 }
1893
1894 wp_die();
1895 }
1896
1897 function remove_directory($path)
1898 {
1899 global $wp_filesystem;
1900
1901 if (empty($wp_filesystem)) {
1902 require_once ABSPATH . '/wp-admin/includes/file.php';
1903 WP_Filesystem();
1904 }
1905
1906 if (!$wp_filesystem) {
1907 return false;
1908 }
1909
1910 return $wp_filesystem->rmdir($path, true);
1911 }
1912 }