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

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