PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Smart_Links / Smart_Links.php

Smart_Links.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.76, at includes/extensions/Smart_Links/Smart_Links.php

818 lines 29.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Smart Links extension.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Smart_Links;
9
10 use WP_Post;
11 use WP_Query;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 require_once __DIR__ . '/Smart_Links_DB.php';
18 require_once __DIR__ . '/Smart_Links_Settings.php';
19 require_once __DIR__ . '/Smart_Links_Service.php';
20
21 class Smart_Links
22 {
23 public const POST_TYPE = 'kng_short_link';
24 public const META_SLUG = '_kng_slug';
25 public const META_DESTINATION = '_kng_destination_url';
26 public const META_STATUS = '_kng_status';
27 public const META_TAGS = '_kng_tags';
28 public const META_NOTES = '_kng_notes';
29 public const META_UTM = '_kng_utm_defaults';
30 public const META_REDIRECT = '_kng_redirect_type';
31 public const META_ALLOW_QUERY = '_kng_allow_query';
32 public const META_CLICKS = '_kng_clicks_total';
33 public const META_UNIQUE_CLICKS = '_kng_unique_clicks_total';
34
35 private static ?Smart_Links $instance = null;
36
37 public static function instance(): Smart_Links
38 {
39 if (self::$instance === null) {
40 self::$instance = new self();
41 }
42
43 return self::$instance;
44 }
45
46 private function __construct()
47 {
48 add_action('init', [$this, 'register_post_type']);
49 add_action('init', [$this, 'register_rewrite_rules']);
50 add_filter('query_vars', [$this, 'register_query_vars']);
51 add_action('template_redirect', [$this, 'handle_redirect']);
52 add_action('init', [$this, 'maybe_create_tables']);
53 add_action('init', [$this, 'maybe_flush_rewrite_rules']);
54
55 add_action('admin_menu', [$this, 'register_admin_menu']);
56 add_action('admin_init', [$this, 'register_settings']);
57 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
58
59 add_action('admin_post_kng_smart_links_save', [$this, 'handle_save_link']);
60 add_action('admin_post_kng_smart_links_delete', [$this, 'handle_delete_link']);
61 add_action('admin_post_kng_smart_links_duplicate', [$this, 'handle_duplicate_link']);
62 add_action('admin_post_kng_smart_links_bulk', [$this, 'handle_bulk_action']);
63 add_action('admin_post_kng_smart_links_export', [$this, 'handle_export_csv']);
64 add_action('admin_post_kng_smart_links_import', [$this, 'handle_import_csv']);
65 add_action('admin_post_kng_smart_links_toggle', [$this, 'handle_toggle_status']);
66 }
67
68 public function register_post_type(): void
69 {
70 register_post_type(self::POST_TYPE, [
71 'labels' => [
72 'name' => __('Smart Links', 'king-addons'),
73 'singular_name' => __('Smart Link', 'king-addons'),
74 ],
75 'public' => false,
76 'show_ui' => false,
77 'show_in_menu' => false,
78 'supports' => ['title'],
79 'capability_type' => 'post',
80 'map_meta_cap' => true,
81 ]);
82 }
83
84 public function register_rewrite_rules(): void
85 {
86 $base_path = trim(Smart_Links_Settings::get_base_path(), '/');
87 if ($base_path === '') {
88 return;
89 }
90
91 add_rewrite_tag('%kng_go%', '([0-1])');
92 add_rewrite_tag('%kng_slug%', '([^&]+)');
93
94 $pattern = '^' . preg_quote($base_path, '/') . '/([^/]+)/?$';
95 add_rewrite_rule($pattern, 'index.php?kng_go=1&kng_slug=$matches[1]', 'top');
96 }
97
98 public function register_query_vars(array $vars): array
99 {
100 $vars[] = 'kng_go';
101 $vars[] = 'kng_slug';
102 return $vars;
103 }
104
105 public function maybe_create_tables(): void
106 {
107 Smart_Links_DB::maybe_create_tables();
108 }
109
110 public function maybe_flush_rewrite_rules(): void
111 {
112 if (!get_option('king_addons_smart_links_rewrite_version')) {
113 update_option('king_addons_smart_links_rewrite_version', '1');
114 update_option('king_addons_smart_links_flush_rewrite', 1);
115 }
116
117 if (!get_option('king_addons_smart_links_flush_rewrite')) {
118 return;
119 }
120
121 delete_option('king_addons_smart_links_flush_rewrite');
122 flush_rewrite_rules();
123 }
124
125 public function handle_redirect(): void
126 {
127 $go = get_query_var('kng_go');
128 if (empty($go)) {
129 return;
130 }
131
132 $slug = (string) get_query_var('kng_slug');
133 $service = new Smart_Links_Service();
134 $link = $service->get_link_by_slug($slug);
135
136 if (!$link) {
137 $this->render_not_found();
138 }
139
140 $status = get_post_meta($link->ID, self::META_STATUS, true);
141 if ($status === 'disabled') {
142 $this->render_not_found();
143 }
144
145 $destination = (string) get_post_meta($link->ID, self::META_DESTINATION, true);
146 $destination = $service->sanitize_destination_url($destination);
147 if ($destination === '') {
148 $this->render_not_found();
149 }
150
151 $settings = Smart_Links_Settings::get_settings();
152 $utm = get_post_meta($link->ID, self::META_UTM, true);
153 if (!is_array($utm)) {
154 $utm = [];
155 }
156
157 $destination = $service->apply_utm($destination, $utm);
158
159 $allow_query = get_post_meta($link->ID, self::META_ALLOW_QUERY, true);
160 $allow_query = $allow_query === '' ? !empty($settings['pass_query_params']) : (bool) $allow_query;
161 if ($allow_query) {
162 $destination = $service->merge_query_params($destination, $_GET, $settings);
163 }
164
165 $redirect_type = (string) get_post_meta($link->ID, self::META_REDIRECT, true);
166 if (!in_array($redirect_type, ['301', '302'], true)) {
167 $redirect_type = (string) $settings['default_redirect_type'];
168 }
169
170 $this->log_click($link->ID, $destination);
171
172 wp_redirect($destination, (int) $redirect_type);
173 exit;
174 }
175
176 public function register_admin_menu(): void
177 {
178 add_submenu_page(
179 'king-addons',
180 __('Smart Links', 'king-addons'),
181 __('Smart Links', 'king-addons'),
182 'manage_options',
183 'king-addons-smart-links',
184 [$this, 'render_admin_page']
185 );
186 }
187
188 public function register_settings(): void
189 {
190 register_setting(
191 'king_addons_smart_links',
192 'king_addons_smart_links_settings',
193 [
194 'type' => 'array',
195 'sanitize_callback' => [Smart_Links_Settings::class, 'sanitize'],
196 'default' => Smart_Links_Settings::defaults(),
197 ]
198 );
199 }
200
201 public function enqueue_admin_assets(string $hook): void
202 {
203 if ($hook !== 'king-addons_page_king-addons-smart-links') {
204 return;
205 }
206
207 $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
208 $shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css';
209 $shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION;
210
211 wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version);
212
213 $admin_css = KING_ADDONS_URL . 'includes/extensions/Smart_Links/assets/admin.css';
214 $admin_path = KING_ADDONS_PATH . 'includes/extensions/Smart_Links/assets/admin.css';
215 $admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION;
216 wp_enqueue_style('king-addons-smart-links', $admin_css, ['king-addons-admin-v3'], $admin_version);
217
218 $admin_js = KING_ADDONS_URL . 'includes/extensions/Smart_Links/assets/admin.js';
219 $admin_js_path = KING_ADDONS_PATH . 'includes/extensions/Smart_Links/assets/admin.js';
220 $admin_js_version = file_exists($admin_js_path) ? filemtime($admin_js_path) : KING_ADDONS_VERSION;
221
222 wp_enqueue_script('king-addons-smart-links', $admin_js, ['jquery'], $admin_js_version, true);
223 wp_localize_script('king-addons-smart-links', 'KNGSmartLinks', [
224 'ajaxUrl' => admin_url('admin-ajax.php'),
225 'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'),
226 'homeUrl' => home_url('/'),
227 'basePath' => Smart_Links_Settings::get_base_path(),
228 ]);
229 }
230
231 public function render_admin_page(): void
232 {
233 if (!current_user_can('manage_options')) {
234 return;
235 }
236
237 $view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard';
238 $settings = Smart_Links_Settings::get_settings();
239 $service = new Smart_Links_Service();
240 $is_pro = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
241
242 include __DIR__ . '/templates/admin-page.php';
243 }
244
245 public function handle_save_link(): void
246 {
247 if (!current_user_can('manage_options')) {
248 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
249 }
250
251 check_admin_referer('kng_smart_links_save');
252
253 $service = new Smart_Links_Service();
254 $settings = Smart_Links_Settings::get_settings();
255
256 $link_id = isset($_POST['link_id']) ? absint($_POST['link_id']) : 0;
257 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
258 $destination = isset($_POST['destination_url']) ? wp_unslash($_POST['destination_url']) : '';
259 $destination = $service->sanitize_destination_url($destination);
260
261 if ($destination === '') {
262 $this->redirect_with_message('add', 'error_invalid_url', $link_id);
263 }
264
265 $slug_mode = isset($_POST['slug_mode']) ? sanitize_key($_POST['slug_mode']) : 'auto';
266 $manual_slug = isset($_POST['slug']) ? sanitize_text_field(wp_unslash($_POST['slug'])) : '';
267 $slug = '';
268
269 if ($slug_mode === 'manual' && !empty($settings['allow_manual_slug'])) {
270 $slug = $service->sanitize_slug($manual_slug);
271 }
272
273 if ($slug === '' && $link_id > 0) {
274 $existing_slug = (string) get_post_meta($link_id, self::META_SLUG, true);
275 $slug = $service->sanitize_slug($existing_slug);
276 }
277
278 if ($slug === '') {
279 $slug = $service->generate_slug((int) $settings['default_slug_length']);
280 }
281
282 $slug = $service->ensure_unique_slug($slug, $link_id);
283
284 $status = !empty($_POST['status']) && $_POST['status'] === 'disabled' ? 'disabled' : 'active';
285 $tags_input = isset($_POST['tags']) ? sanitize_text_field(wp_unslash($_POST['tags'])) : '';
286 $tags = $service->parse_tags($tags_input);
287 $notes = isset($_POST['notes']) ? sanitize_textarea_field(wp_unslash($_POST['notes'])) : '';
288
289 $utm_enabled = !empty($_POST['utm_enabled']);
290 $utm_defaults = [
291 'enabled' => $utm_enabled,
292 'utm_source' => isset($_POST['utm_source']) ? sanitize_text_field(wp_unslash($_POST['utm_source'])) : '',
293 'utm_medium' => isset($_POST['utm_medium']) ? sanitize_text_field(wp_unslash($_POST['utm_medium'])) : '',
294 'utm_campaign' => isset($_POST['utm_campaign']) ? sanitize_text_field(wp_unslash($_POST['utm_campaign'])) : '',
295 'utm_term' => isset($_POST['utm_term']) ? sanitize_text_field(wp_unslash($_POST['utm_term'])) : '',
296 'utm_content' => isset($_POST['utm_content']) ? sanitize_text_field(wp_unslash($_POST['utm_content'])) : '',
297 ];
298
299 $redirect_type = isset($_POST['redirect_type']) ? sanitize_text_field(wp_unslash($_POST['redirect_type'])) : '';
300 $redirect_type = in_array($redirect_type, ['301', '302'], true) ? $redirect_type : (string) $settings['default_redirect_type'];
301 $allow_query = !empty($_POST['allow_query']);
302
303 $post_args = [
304 'post_title' => $title !== '' ? $title : $slug,
305 'post_type' => self::POST_TYPE,
306 'post_status' => 'publish',
307 ];
308
309 if ($link_id > 0) {
310 $post_args['ID'] = $link_id;
311 }
312
313 $result = wp_insert_post($post_args, true);
314 if (is_wp_error($result)) {
315 $this->redirect_with_message('add', 'error_save', $link_id);
316 }
317
318 $link_id = (int) $result;
319
320 update_post_meta($link_id, self::META_SLUG, $slug);
321 update_post_meta($link_id, self::META_DESTINATION, $destination);
322 update_post_meta($link_id, self::META_STATUS, $status);
323 update_post_meta($link_id, self::META_TAGS, $tags);
324 update_post_meta($link_id, self::META_NOTES, $notes);
325 update_post_meta($link_id, self::META_UTM, $utm_defaults);
326 update_post_meta($link_id, self::META_REDIRECT, $redirect_type);
327 update_post_meta($link_id, self::META_ALLOW_QUERY, $allow_query ? '1' : '0');
328
329 $message = $link_id > 0 && isset($_POST['link_id']) && absint($_POST['link_id']) > 0 ? 'updated' : 'created';
330 $this->redirect_with_message('add', $message, $link_id);
331 }
332
333 public function handle_delete_link(): void
334 {
335 if (!current_user_can('manage_options')) {
336 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
337 }
338
339 $link_id = isset($_GET['link_id']) ? absint($_GET['link_id']) : 0;
340 check_admin_referer('kng_smart_links_delete_' . $link_id);
341
342 if ($link_id > 0) {
343 wp_delete_post($link_id, true);
344 }
345
346 $this->redirect_with_message('links', 'deleted');
347 }
348
349 public function handle_duplicate_link(): void
350 {
351 if (!current_user_can('manage_options')) {
352 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
353 }
354
355 $link_id = isset($_GET['link_id']) ? absint($_GET['link_id']) : 0;
356 check_admin_referer('kng_smart_links_duplicate_' . $link_id);
357
358 $service = new Smart_Links_Service();
359 $link = $service->get_link($link_id);
360 if (!$link) {
361 $this->redirect_with_message('links', 'error_not_found');
362 }
363
364 $slug = get_post_meta($link_id, self::META_SLUG, true);
365 $slug = $service->ensure_unique_slug((string) $slug, 0);
366 $destination = get_post_meta($link_id, self::META_DESTINATION, true);
367
368 $new_id = wp_insert_post([
369 'post_title' => $link->post_title . ' (Copy)',
370 'post_type' => self::POST_TYPE,
371 'post_status' => 'publish',
372 ]);
373
374 if (is_wp_error($new_id)) {
375 $this->redirect_with_message('links', 'error_save');
376 }
377
378 update_post_meta($new_id, self::META_SLUG, $slug);
379 update_post_meta($new_id, self::META_DESTINATION, $destination);
380 update_post_meta($new_id, self::META_STATUS, get_post_meta($link_id, self::META_STATUS, true));
381 update_post_meta($new_id, self::META_TAGS, get_post_meta($link_id, self::META_TAGS, true));
382 update_post_meta($new_id, self::META_NOTES, get_post_meta($link_id, self::META_NOTES, true));
383 update_post_meta($new_id, self::META_UTM, get_post_meta($link_id, self::META_UTM, true));
384 update_post_meta($new_id, self::META_REDIRECT, get_post_meta($link_id, self::META_REDIRECT, true));
385 update_post_meta($new_id, self::META_ALLOW_QUERY, get_post_meta($link_id, self::META_ALLOW_QUERY, true));
386
387 $this->redirect_with_message('links', 'duplicated');
388 }
389
390 public function handle_toggle_status(): void
391 {
392 if (!current_user_can('manage_options')) {
393 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
394 }
395
396 $link_id = isset($_GET['link_id']) ? absint($_GET['link_id']) : 0;
397 $status = isset($_GET['status']) ? sanitize_key($_GET['status']) : 'active';
398 check_admin_referer('kng_smart_links_toggle_' . $link_id);
399
400 if ($link_id > 0 && in_array($status, ['active', 'disabled'], true)) {
401 update_post_meta($link_id, self::META_STATUS, $status);
402 }
403
404 $this->redirect_with_message('links', 'updated');
405 }
406
407 public function handle_bulk_action(): void
408 {
409 if (!current_user_can('manage_options')) {
410 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
411 }
412
413 check_admin_referer('kng_smart_links_bulk');
414
415 $action = isset($_POST['bulk_action']) ? sanitize_key($_POST['bulk_action']) : '';
416 $link_ids = isset($_POST['link_ids']) ? array_map('absint', (array) $_POST['link_ids']) : [];
417 $link_ids = array_filter($link_ids);
418
419 if (empty($action) || empty($link_ids)) {
420 $this->redirect_with_message('links', 'error_bulk');
421 }
422
423 if ($action === 'export') {
424 $this->output_csv($link_ids);
425 }
426
427 if (in_array($action, ['enable', 'disable'], true)) {
428 $status = $action === 'enable' ? 'active' : 'disabled';
429 foreach ($link_ids as $link_id) {
430 update_post_meta($link_id, self::META_STATUS, $status);
431 }
432 $this->redirect_with_message('links', 'updated');
433 }
434
435 if ($action === 'delete') {
436 foreach ($link_ids as $link_id) {
437 wp_delete_post($link_id, true);
438 }
439 $this->redirect_with_message('links', 'deleted');
440 }
441
442 if ($action === 'add_tag') {
443 $tag = isset($_POST['bulk_tag']) ? sanitize_text_field(wp_unslash($_POST['bulk_tag'])) : '';
444 if ($tag !== '') {
445 foreach ($link_ids as $link_id) {
446 $tags = (array) get_post_meta($link_id, self::META_TAGS, true);
447 $tags[] = $tag;
448 $tags = array_values(array_unique(array_filter(array_map('sanitize_text_field', $tags))));
449 update_post_meta($link_id, self::META_TAGS, $tags);
450 }
451 $this->redirect_with_message('links', 'updated');
452 }
453 }
454
455 $this->redirect_with_message('links', 'error_bulk');
456 }
457
458 public function handle_export_csv(): void
459 {
460 if (!current_user_can('manage_options')) {
461 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
462 }
463
464 check_admin_referer('kng_smart_links_export');
465
466 $link_id = isset($_GET['link_id']) ? absint($_GET['link_id']) : 0;
467 $link_ids = $link_id > 0 ? [$link_id] : [];
468 $this->output_csv($link_ids);
469 }
470
471 public function handle_import_csv(): void
472 {
473 if (!current_user_can('manage_options')) {
474 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
475 }
476
477 check_admin_referer('kng_smart_links_import');
478
479 if (empty($_FILES['import_file']) || !isset($_FILES['import_file']['tmp_name'])) {
480 $this->redirect_with_message('import-export', 'error_import');
481 }
482
483 $file = $_FILES['import_file'];
484 if (!empty($file['error'])) {
485 $this->redirect_with_message('import-export', 'error_import');
486 }
487
488 $handle = fopen($file['tmp_name'], 'r');
489 if (!$handle) {
490 $this->redirect_with_message('import-export', 'error_import');
491 }
492
493 $header = fgetcsv($handle);
494 if (!$header) {
495 fclose($handle);
496 $this->redirect_with_message('import-export', 'error_import');
497 }
498
499 $map = array_flip(array_map('trim', $header));
500 $service = new Smart_Links_Service();
501 $settings = Smart_Links_Settings::get_settings();
502 $imported = 0;
503
504 while (($row = fgetcsv($handle)) !== false) {
505 $destination = $row[$map['destination_url'] ?? -1] ?? '';
506 $destination = $service->sanitize_destination_url((string) $destination);
507 if ($destination === '') {
508 continue;
509 }
510
511 $slug = $row[$map['slug'] ?? -1] ?? '';
512 $slug = $service->sanitize_slug((string) $slug);
513 if ($slug === '') {
514 $slug = $service->generate_slug((int) $settings['default_slug_length']);
515 }
516 $slug = $service->ensure_unique_slug($slug);
517
518 $title = $row[$map['title'] ?? -1] ?? '';
519 $title = sanitize_text_field((string) $title);
520
521 $tags = $row[$map['tags'] ?? -1] ?? '';
522 $tags = $service->parse_tags((string) $tags);
523
524 $new_id = wp_insert_post([
525 'post_title' => $title !== '' ? $title : $slug,
526 'post_type' => self::POST_TYPE,
527 'post_status' => 'publish',
528 ]);
529
530 if (is_wp_error($new_id)) {
531 continue;
532 }
533
534 update_post_meta($new_id, self::META_SLUG, $slug);
535 update_post_meta($new_id, self::META_DESTINATION, $destination);
536 update_post_meta($new_id, self::META_STATUS, 'active');
537 update_post_meta($new_id, self::META_TAGS, $tags);
538
539 $imported++;
540 }
541
542 fclose($handle);
543
544 $this->redirect_with_message('import-export', $imported > 0 ? 'imported' : 'error_import');
545 }
546
547 private function output_csv(array $link_ids = []): void
548 {
549 $args = [
550 'post_type' => self::POST_TYPE,
551 'post_status' => 'any',
552 'posts_per_page' => -1,
553 ];
554
555 if (!empty($link_ids)) {
556 $args['post__in'] = $link_ids;
557 $args['orderby'] = 'post__in';
558 }
559
560 $query = new WP_Query($args);
561
562 header('Content-Type: text/csv; charset=utf-8');
563 header('Content-Disposition: attachment; filename=smart-links-' . gmdate('Y-m-d') . '.csv');
564
565 $output = fopen('php://output', 'w');
566 fputcsv($output, [
567 'id',
568 'title',
569 'slug',
570 'short_url',
571 'destination_url',
572 'status',
573 'tags',
574 'created',
575 'clicks',
576 'unique_clicks',
577 ]);
578
579 $service = new Smart_Links_Service();
580
581 foreach ($query->posts as $post) {
582 $slug = (string) get_post_meta($post->ID, self::META_SLUG, true);
583 $short_url = $service->build_short_url($slug);
584 $tags = (array) get_post_meta($post->ID, self::META_TAGS, true);
585
586 fputcsv($output, [
587 $post->ID,
588 $post->post_title,
589 $slug,
590 $short_url,
591 (string) get_post_meta($post->ID, self::META_DESTINATION, true),
592 (string) get_post_meta($post->ID, self::META_STATUS, true),
593 implode(', ', $tags),
594 $post->post_date,
595 (int) get_post_meta($post->ID, self::META_CLICKS, true),
596 (int) get_post_meta($post->ID, self::META_UNIQUE_CLICKS, true),
597 ]);
598 }
599
600 fclose($output);
601 exit;
602 }
603
604 private function log_click(int $link_id, string $landing_url): void
605 {
606 $settings = Smart_Links_Settings::get_settings();
607 if (empty($settings['tracking_enabled'])) {
608 return;
609 }
610
611 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
612 if (!empty($settings['exclude_bots']) && $this->is_bot($user_agent)) {
613 return;
614 }
615
616 $ip = $this->get_client_ip();
617 $ip_hash = hash('sha256', $ip . '|' . wp_salt('kng_link'));
618 $ua_hash = hash('sha256', $user_agent . '|' . wp_salt('kng_link'));
619
620 $date = current_time('Y-m-d');
621 $unique_hash = hash('sha256', $ip_hash . '|' . $ua_hash . '|' . $date);
622
623 global $wpdb;
624 $clicks_table = Smart_Links_DB::get_clicks_table();
625 $daily_table = Smart_Links_DB::get_daily_table();
626
627 $is_unique = false;
628
629 if (($settings['unique_click_window'] ?? 'daily') === 'rolling') {
630 $window_start = wp_date('Y-m-d H:i:s', current_time('timestamp') - DAY_IN_SECONDS);
631 $exists = $wpdb->get_var(
632 $wpdb->prepare(
633 "SELECT id FROM {$clicks_table} WHERE link_id = %d AND ip_hash = %s AND user_agent_hash = %s AND created_at >= %s LIMIT 1",
634 $link_id,
635 $ip_hash,
636 $ua_hash,
637 $window_start
638 )
639 );
640 $is_unique = empty($exists);
641 } else {
642 $exists = $wpdb->get_var(
643 $wpdb->prepare(
644 "SELECT id FROM {$clicks_table} WHERE link_id = %d AND unique_hash = %s LIMIT 1",
645 $link_id,
646 $unique_hash
647 )
648 );
649 $is_unique = empty($exists);
650 }
651
652 $referrer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
653 $referrer_host = $referrer ? (string) wp_parse_url($referrer, PHP_URL_HOST) : '';
654
655 $wpdb->insert(
656 $clicks_table,
657 [
658 'link_id' => $link_id,
659 'created_at' => current_time('mysql'),
660 'ip_hash' => $ip_hash,
661 'user_agent_hash' => $ua_hash,
662 'unique_hash' => $unique_hash,
663 'referrer' => $referrer_host,
664 'landing_url' => esc_url_raw($landing_url),
665 'device_type' => $this->detect_device($user_agent),
666 'browser' => $this->detect_browser($user_agent),
667 'os' => $this->detect_os($user_agent),
668 'country' => '',
669 ],
670 ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
671 );
672
673 $daily_row = $wpdb->get_row(
674 $wpdb->prepare(
675 "SELECT clicks, unique_clicks FROM {$daily_table} WHERE link_id = %d AND date = %s",
676 $link_id,
677 $date
678 ),
679 ARRAY_A
680 );
681
682 if ($daily_row) {
683 $wpdb->update(
684 $daily_table,
685 [
686 'clicks' => (int) $daily_row['clicks'] + 1,
687 'unique_clicks' => (int) $daily_row['unique_clicks'] + ($is_unique ? 1 : 0),
688 ],
689 [
690 'link_id' => $link_id,
691 'date' => $date,
692 ],
693 ['%d', '%d'],
694 ['%d', '%s']
695 );
696 } else {
697 $wpdb->insert(
698 $daily_table,
699 [
700 'link_id' => $link_id,
701 'date' => $date,
702 'clicks' => 1,
703 'unique_clicks' => $is_unique ? 1 : 0,
704 ],
705 ['%d', '%s', '%d', '%d']
706 );
707 }
708
709 $total_clicks = (int) get_post_meta($link_id, self::META_CLICKS, true);
710 update_post_meta($link_id, self::META_CLICKS, $total_clicks + 1);
711
712 if ($is_unique) {
713 $unique_clicks = (int) get_post_meta($link_id, self::META_UNIQUE_CLICKS, true);
714 update_post_meta($link_id, self::META_UNIQUE_CLICKS, $unique_clicks + 1);
715 }
716 }
717
718 private function get_client_ip(): string
719 {
720 $ip = '';
721
722 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
723 $ip = (string) $_SERVER['HTTP_CLIENT_IP'];
724 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
725 $parts = explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR']);
726 $ip = trim($parts[0]);
727 } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
728 $ip = (string) $_SERVER['REMOTE_ADDR'];
729 }
730
731 return preg_replace('/[^0-9a-fA-F:.,]/', '', (string) $ip);
732 }
733
734 private function is_bot(string $user_agent): bool
735 {
736 if ($user_agent === '') {
737 return false;
738 }
739
740 return (bool) preg_match('/bot|crawl|spider|slurp|bingpreview|yandex|baiduspider|duckduckbot|facebookexternalhit|facebot|ia_archiver/i', $user_agent);
741 }
742
743 private function detect_device(string $user_agent): string
744 {
745 $ua = strtolower($user_agent);
746 if (strpos($ua, 'ipad') !== false || strpos($ua, 'tablet') !== false) {
747 return 'tablet';
748 }
749 if (strpos($ua, 'mobile') !== false || strpos($ua, 'android') !== false || strpos($ua, 'iphone') !== false) {
750 return 'mobile';
751 }
752 return 'desktop';
753 }
754
755 private function detect_browser(string $user_agent): string
756 {
757 $ua = strtolower($user_agent);
758 if (strpos($ua, 'edge') !== false || strpos($ua, 'edg') !== false) {
759 return 'edge';
760 }
761 if (strpos($ua, 'chrome') !== false && strpos($ua, 'safari') !== false) {
762 return 'chrome';
763 }
764 if (strpos($ua, 'safari') !== false && strpos($ua, 'chrome') === false) {
765 return 'safari';
766 }
767 if (strpos($ua, 'firefox') !== false) {
768 return 'firefox';
769 }
770 if (strpos($ua, 'opera') !== false || strpos($ua, 'opr') !== false) {
771 return 'opera';
772 }
773 return 'other';
774 }
775
776 private function detect_os(string $user_agent): string
777 {
778 $ua = strtolower($user_agent);
779 if (strpos($ua, 'windows') !== false) {
780 return 'windows';
781 }
782 if (strpos($ua, 'mac os') !== false || strpos($ua, 'macintosh') !== false) {
783 return 'macos';
784 }
785 if (strpos($ua, 'iphone') !== false || strpos($ua, 'ipad') !== false) {
786 return 'ios';
787 }
788 if (strpos($ua, 'android') !== false) {
789 return 'android';
790 }
791 if (strpos($ua, 'linux') !== false) {
792 return 'linux';
793 }
794 return 'other';
795 }
796
797 private function render_not_found(): void
798 {
799 wp_die(esc_html__('Link not found.', 'king-addons'), '', ['response' => 404]);
800 }
801
802 private function redirect_with_message(string $view, string $message, int $link_id = 0): void
803 {
804 $args = [
805 'page' => 'king-addons-smart-links',
806 'view' => $view,
807 'message' => $message,
808 ];
809
810 if ($link_id > 0) {
811 $args['link_id'] = $link_id;
812 }
813
814 wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
815 exit;
816 }
817 }
818