| 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 |
|