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