Changeset 3306152
- Timestamp:
- 06/04/2025 06:13:02 AM (7 months ago)
- Location:
- custom-link-shortener/trunk
- Files:
-
- 3 edited
-
custom-link-shortener.php (modified) (27 diffs)
-
js/admin.js (modified) (2 diffs)
-
readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
custom-link-shortener/trunk/custom-link-shortener.php
r3305630 r3306152 2 2 /* 3 3 Plugin Name: Custom Link Shortener 4 Description: A custom URL shortener with analytics, rotating links, location tracking, and password protection.5 Version: 1.4. 24 Description: A custom URL shortener with analytics, rotating links, Random blog post redirect, location tracking, and password protection. 5 Version: 1.4.3 6 6 Author: Lukastech 7 7 License: GPLv2 or later … … 13 13 14 14 /** 15 * CHANGELOG v1.4. 1 – May 30, 202515 * CHANGELOG v1.4.3 – June 1, 2025 16 16 * ------------------------------------------------ 17 17 * • Complete UI/UX redesign … … 24 24 * • Better notification messages 25 25 * • Fixed WordPress compliance issues 26 * • Fixed is_random_post column creation issue 26 27 * ------------------------------------------------ 27 28 */ … … 29 30 defined('ABSPATH') or die('No script kiddies please!'); 30 31 31 // Enqueue admin styles and scripts 32 // Enqueue admin styles and scripts - UPDATED VERSION 32 33 add_action('admin_enqueue_scripts', 'wpcs_enqueue_admin_assets'); 33 34 function wpcs_enqueue_admin_assets($hook) { … … 38 39 39 40 // Enqueue CSS 40 wp_enqueue_style('wpcs-admin-styles', plugins_url('css/admin-styles.css', __FILE__), array(), '1.4. 1');41 wp_enqueue_style('wpcs-admin-styles', plugins_url('css/admin-styles.css', __FILE__), array(), '1.4.3'); 41 42 42 43 // Enqueue JavaScript 43 wp_enqueue_script('wpcs-admin-js', plugins_url('js/wpcs-admin.js', __FILE__), array('jquery'), '1.4.1', true); 44 45 // Localize script for dynamic data 44 wp_enqueue_script('wpcs-admin-js', plugins_url('js/admin.js', __FILE__), array('jquery'), '1.4.3', true); 45 46 // Enqueue clipboard.js for copy functionality 47 wp_enqueue_script('clipboard', 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js', array(), '2.0.11', true); 48 49 // Localize script for dynamic data - UPDATED WITH AJAX URL 46 50 wp_localize_script('wpcs-admin-js', 'wpcsAdmin', array( 47 51 'analyticsUrl' => admin_url('admin.php?page=wpcs-analytics'), 52 'ajaxUrl' => admin_url('admin-ajax.php'), // Added AJAX URL 48 53 'csvFilename' => 'analytics_export_' . gmdate('Y-m-d') . '.csv', 49 54 'nonce' => wp_create_nonce('wpcs_admin_nonce') … … 52 57 53 58 // -------------------------------------------------- 54 // Activation & DB (unchanged)59 // Activation & DB - FIXED VERSION 55 60 // -------------------------------------------------- 56 61 register_activation_hook(__FILE__, function () { … … 60 65 }); 61 66 62 // --- MAKE SURE THIS FUNCTION HAS THE ACTUAL DB CREATION CODE --- 67 // Add database version option to track migrations 68 add_action('plugins_loaded', 'wpcs_check_db_version'); 69 function wpcs_check_db_version() { 70 $current_version = get_option('wpcs_db_version', '1.0'); 71 $target_version = '1.4.2'; 72 73 if (version_compare($current_version, $target_version, '<')) { 74 wpcs_create_db(); 75 update_option('wpcs_db_version', $target_version); 76 } 77 } 78 79 // FIXED DATABASE CREATION FUNCTION 63 80 function wpcs_create_db(){ 64 81 global $wpdb; … … 69 86 $click_tbl = $wpdb->prefix . 'wpcs_clicks'; 70 87 71 $sql = "CREATE TABLE $link_tbl ( 88 // Create the main links table with all required columns 89 $sql_links = "CREATE TABLE $link_tbl ( 72 90 id bigint(20) NOT NULL AUTO_INCREMENT, 73 alias varchar(255) NOT NULL UNIQUE,91 alias varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 74 92 password varchar(255), 75 93 is_rotating tinyint(1) DEFAULT 0, 76 PRIMARY KEY (id) 77 ) $charset_collate; 78 79 CREATE TABLE $dest_tbl ( 94 is_random_post tinyint(1) DEFAULT 0, 95 PRIMARY KEY (id), 96 UNIQUE KEY alias (alias) 97 ) $charset_collate;"; 98 99 $sql_destinations = "CREATE TABLE $dest_tbl ( 80 100 id bigint(20) NOT NULL AUTO_INCREMENT, 81 101 link_id bigint(20) NOT NULL, 82 102 destination_url text NOT NULL, 83 103 PRIMARY KEY (id), 84 FOREIGN KEY (link_id) REFERENCES $link_tbl(id) ON DELETE CASCADE85 ) $charset_collate; 86 87 CREATE TABLE $click_tbl (104 KEY link_id (link_id) 105 ) $charset_collate;"; 106 107 $sql_clicks = "CREATE TABLE $click_tbl ( 88 108 id bigint(20) NOT NULL AUTO_INCREMENT, 89 109 link_id bigint(20) NOT NULL, … … 94 114 clicked_at datetime NOT NULL, 95 115 PRIMARY KEY (id), 96 FOREIGN KEY (link_id) REFERENCES $link_tbl(id) ON DELETE CASCADE116 KEY link_id (link_id) 97 117 ) $charset_collate;"; 98 118 99 119 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 100 dbDelta( $sql ); 101 } 102 120 121 // Create/update each table individually 122 dbDelta( $sql_links ); 123 dbDelta( $sql_destinations ); 124 dbDelta( $sql_clicks ); 125 126 // Check if tables exist and perform migrations if needed 127 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$link_tbl'"); 128 if ($table_exists) { 129 // Migration: Add is_random_post column if it doesn't exist 130 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM $link_tbl LIKE 'is_random_post'"); 131 if (empty($column_exists)) { 132 $result = $wpdb->query("ALTER TABLE $link_tbl ADD COLUMN is_random_post tinyint(1) DEFAULT 0"); 133 if ($result === false) { 134 error_log('WPCS: Failed to add is_random_post column to ' . $link_tbl . ': ' . $wpdb->last_error); 135 } else { 136 error_log('WPCS: Successfully added is_random_post column to ' . $link_tbl); 137 } 138 } 139 140 // Migration: Ensure UNIQUE constraint exists 141 $index_exists = $wpdb->get_results("SHOW INDEX FROM $link_tbl WHERE Key_name = 'alias'"); 142 if (empty($index_exists)) { 143 // First check for duplicates and handle them 144 $duplicates = $wpdb->get_results("SELECT alias, COUNT(*) as count FROM $link_tbl GROUP BY alias HAVING count > 1"); 145 if (!empty($duplicates)) { 146 foreach ($duplicates as $dup) { 147 // Keep the first one, delete the rest 148 $keep_id = $wpdb->get_var($wpdb->prepare("SELECT id FROM $link_tbl WHERE alias = %s ORDER BY id ASC LIMIT 1", $dup->alias)); 149 $wpdb->query($wpdb->prepare("DELETE FROM $link_tbl WHERE alias = %s AND id != %d", $dup->alias, $keep_id)); 150 } 151 } 152 153 $result = $wpdb->query("ALTER TABLE $link_tbl ADD UNIQUE KEY alias (alias)"); 154 if ($result === false) { 155 error_log('WPCS: Failed to add UNIQUE KEY alias to ' . $link_tbl . ': ' . $wpdb->last_error); 156 } else { 157 error_log('WPCS: Successfully added UNIQUE KEY alias to ' . $link_tbl); 158 } 159 } 160 161 // Migration: Fix alias column collation 162 $column_info = $wpdb->get_row("SHOW COLUMNS FROM $link_tbl WHERE Field = 'alias'"); 163 if ($column_info && strpos($column_info->Type, 'utf8mb4_bin') === false) { 164 $result = $wpdb->query("ALTER TABLE $link_tbl MODIFY alias varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL"); 165 if ($result === false) { 166 error_log('WPCS: Failed to modify alias column to utf8mb4_bin in ' . $link_tbl . ': ' . $wpdb->last_error); 167 } else { 168 error_log('WPCS: Successfully modified alias column to utf8mb4_bin in ' . $link_tbl); 169 } 170 } 171 } else { 172 error_log('WPCS: Table ' . $link_tbl . ' creation failed'); 173 } 174 175 // Verify the is_random_post column exists after migration 176 $final_check = $wpdb->get_results("SHOW COLUMNS FROM $link_tbl LIKE 'is_random_post'"); 177 if (empty($final_check)) { 178 error_log('WPCS: CRITICAL - is_random_post column still missing after migration attempt'); 179 } else { 180 error_log('WPCS: is_random_post column verified to exist'); 181 } 182 } 103 183 104 184 // Add rewrite rule for short URLs like /go/abc123 … … 133 213 if ($link->password) { 134 214 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['wpcs_password'])) { 135 // Sanitize password input136 215 $entered_password = sanitize_text_field($_POST['wpcs_password']); 137 216 if ($entered_password !== $link->password) { … … 148 227 } 149 228 150 $dests = $wpdb->get_col($wpdb->prepare("SELECT destination_url FROM $dest_tbl WHERE link_id = %d", $link->id)); 151 if (empty($dests)) { 152 wp_die('No destination URLs set.', 'Short URL Error', ['response' => 404]); 153 } 154 155 $target_url = $link->is_rotating ? $dests[array_rand($dests)] : $dests[0]; 156 157 // Track click - FIXED: Sanitize $_SERVER data 229 // Handle random post redirect 230 if ($link->is_random_post) { 231 $args = array( 232 'post_type' => 'post', 233 'post_status' => 'publish', 234 'posts_per_page' => 1, 235 'orderby' => 'rand', 236 ); 237 $random_post = new WP_Query($args); 238 239 if ($random_post->have_posts()) { 240 while ($random_post->have_posts()) { 241 $random_post->the_post(); 242 $target_url = get_permalink(); 243 } 244 wp_reset_postdata(); 245 } else { 246 $target_url = home_url(); // Fallback to homepage 247 } 248 } else { 249 $dests = $wpdb->get_col($wpdb->prepare("SELECT destination_url FROM $dest_tbl WHERE link_id = %d", $link->id)); 250 if (empty($dests)) { 251 wp_die('No destination URLs set.', 'Short URL Error', ['response' => 404]); 252 } 253 $target_url = $link->is_rotating ? $dests[array_rand($dests)] : $dests[0]; 254 } 255 256 // Track click 158 257 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : ''; 159 258 $ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : ''; 160 259 161 // Validate IP format162 260 if (!filter_var($ip, FILTER_VALIDATE_IP)) { 163 261 $ip = '127.0.0.1'; // fallback … … 175 273 ]); 176 274 177 // FIXED: Use wp_safe_redirect instead of wp_redirect178 275 wp_safe_redirect(esc_url_raw($target_url), 301); 179 276 exit; … … 223 320 224 321 // -------------------------------------------------- 225 // Short-link creation322 // FIXED Short-link creation 226 323 // -------------------------------------------------- 227 324 function wpcs_render_admin_page(){ … … 243 340 </div> 244 341 245 <div class="form-field" >342 <div class="form-field" id="wpcs_destinations_container"> 246 343 <label for="wpcs_destinations">Destination URLs</label> 247 <textarea id="wpcs_destinations" name="wpcs_destinations" rows="5" class="large-text code" placeholder="One URL per line" required></textarea> 344 <textarea id="wpcs_destinations" name="wpcs_destinations" rows="5" class="large-text code" placeholder="One URL per line"></textarea> 345 <p class="description">One URL per line. Required unless Random Post Redirect is enabled.</p> 248 346 </div> 249 347 250 348 <div class="form-field"> 251 349 <label class="checkbox-container"> 252 <input type="checkbox" name="wpcs_rotate" value="1">350 <input type="checkbox" name="wpcs_rotate" id="wpcs_rotate" value="1"> 253 351 <span class="checkmark"></span> 254 352 Enable URL Rotation 255 353 </label> 256 354 <p class="description">When enabled, visitors will be randomly redirected to one of the URLs</p> 355 </div> 356 357 <div class="form-field"> 358 <label class="checkbox-container"> 359 <input type="checkbox" name="wpcs_random_post" id="wpcs_random_post" value="1"> 360 <span class="checkmark"></span> 361 Enable Random Post Redirect 362 </label> 363 <p class="description">When enabled, visitors will be redirected to a random published post, and the destination URLs field will be ignored.</p> 257 364 </div> 258 365 … … 274 381 } 275 382 383 // FIXED FORM SUBMISSION HANDLER 276 384 function wpcs_handle_form_submission(){ 277 385 global $wpdb; 278 386 279 // FIXED: Properly sanitize and validate allPOST data387 // Sanitize and validate POST data 280 388 $alias = sanitize_text_field($_POST['wpcs_alias']); 281 389 $password = !empty($_POST['wpcs_password']) ? sanitize_text_field($_POST['wpcs_password']) : ''; 282 390 $rotate = isset($_POST['wpcs_rotate']) ? 1 : 0; 283 284 // FIXED: Sanitize textarea and validate URLs 285 $destinations_input = sanitize_textarea_field($_POST['wpcs_destinations']); 286 $dest_raw = explode("\n", trim($destinations_input)); 287 $dests = array_map('esc_url_raw', array_filter(array_map('trim', $dest_raw))); 391 $random_post = isset($_POST['wpcs_random_post']) ? 1 : 0; 288 392 289 393 // Validate the alias/slug 290 394 if (!preg_match('/^[a-z0-9-_]+$/i', $alias)) { 291 395 echo '<div class="notice notice-error is-dismissible"><p>Alias can only contain letters, numbers, hyphens, and underscores.</p></div>'; 292 return; 293 } 294 295 // Validate that we have at least one valid URL 296 if (empty($dests)) { 297 echo '<div class="notice notice-error is-dismissible"><p>Please provide at least one valid destination URL.</p></div>'; 396 error_log('WPCS: Invalid alias format: ' . $alias); 298 397 return; 299 398 } … … 302 401 $dest_tbl = $wpdb->prefix.'wpcs_destinations'; 303 402 403 // Check table existence 404 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$links'"); 405 if (!$table_exists) { 406 echo '<div class="notice notice-error is-dismissible"><p>Database table missing. Please deactivate and reactivate the plugin.</p></div>'; 407 error_log('WPCS: Table ' . $links . ' does not exist'); 408 return; 409 } 410 411 // CRITICAL: Verify is_random_post column exists before insert 412 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM $links LIKE 'is_random_post'"); 413 if (empty($column_exists)) { 414 echo '<div class="notice notice-error is-dismissible"><p>Database schema outdated. Please deactivate and reactivate the plugin to update the database.</p></div>'; 415 error_log('WPCS: is_random_post column missing during insert attempt'); 416 return; 417 } 418 304 419 // Check for duplicate alias 305 if ($wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $links WHERE alias=%s", $alias))) { 306 echo '<div class="notice notice-error is-dismissible"><p>Alias already exists. Please choose a different one.</p></div>'; 420 $existing_alias = $wpdb->get_var($wpdb->prepare("SELECT alias FROM $links WHERE alias = %s", $alias)); 421 if ($existing_alias) { 422 echo '<div class="notice notice-error is-dismissible"><p>Alias "' . esc_html($alias) . '" already exists. Please choose a different one.</p></div>'; 423 error_log('WPCS: Duplicate alias detected: ' . $alias); 307 424 return; 308 425 } 309 426 310 $wpdb->insert($links, ['alias' => $alias, 'password' => $password ?: null, 'is_rotating' => $rotate]); 427 // Only process destinations if not random post 428 $dests = []; 429 if (!$random_post) { 430 $destinations_input = sanitize_textarea_field($_POST['wpcs_destinations']); 431 $dest_raw = explode("\n", trim($destinations_input)); 432 $dests = array_map('esc_url_raw', array_filter(array_map('trim', $dest_raw))); 433 434 if (empty($dests)) { 435 echo '<div class="notice notice-error is-dismissible"><p>Please provide at least one valid destination URL for non-random links.</p></div>'; 436 error_log('WPCS: No valid destination URLs provided for non-random link'); 437 return; 438 } 439 } 440 441 // Insert link with explicit column specification 442 $insert_data = [ 443 'alias' => $alias, 444 'password' => $password ?: null, 445 'is_rotating' => $rotate, 446 'is_random_post' => $random_post 447 ]; 448 449 $insert_format = ['%s', '%s', '%d', '%d']; 450 451 $result = $wpdb->insert($links, $insert_data, $insert_format); 452 453 if ($result === false) { 454 echo '<div class="notice notice-error is-dismissible"><p>Failed to create short link: ' . esc_html($wpdb->last_error) . '</p></div>'; 455 error_log('WPCS: Failed to insert link with alias "' . $alias . '": ' . $wpdb->last_error); 456 457 // Additional debug info 458 error_log('WPCS: Insert data: ' . print_r($insert_data, true)); 459 error_log('WPCS: Table structure: ' . print_r($wpdb->get_results("DESCRIBE $links"), true)); 460 return; 461 } 462 311 463 $id = $wpdb->insert_id; 312 313 foreach ($dests as $u) { 314 $wpdb->insert($dest_tbl, ['link_id' => $id, 'destination_url' => $u]); 464 if (!$id) { 465 echo '<div class="notice notice-error is-dismissible"><p>Failed to retrieve new link ID.</p></div>'; 466 error_log('WPCS: Failed to retrieve insert_id for alias "' . $alias . '"'); 467 return; 468 } 469 470 // Insert destinations if not random post 471 if (!$random_post && !empty($dests)) { 472 foreach ($dests as $u) { 473 $dest_result = $wpdb->insert($dest_tbl, ['link_id' => $id, 'destination_url' => $u], ['%d', '%s']); 474 if ($dest_result === false) { 475 echo '<div class="notice notice-error is-dismissible"><p>Failed to save destination URL: ' . esc_html($wpdb->last_error) . '</p></div>'; 476 error_log('WPCS: Failed to insert destination URL for link_id ' . $id . ': ' . $wpdb->last_error); 477 return; 478 } 479 } 315 480 } 316 481 … … 318 483 echo '<div class="notice notice-success is-dismissible"><p>Short link created successfully! <a href="'.esc_url($short_url).'" target="_blank">'.esc_html($short_url).'</a></p></div>'; 319 484 320 // FIXED: Use proper JavaScript enqueuing for redirect321 485 wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();'); 322 486 } … … 336 500 337 501 echo '<div class="notice notice-success is-dismissible"><p>Link deleted successfully.</p></div>'; 338 // FIXED: Use proper JavaScript enqueuing for redirect339 502 wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();'); 340 503 return; 341 504 } 342 505 343 // Handle form submission for edits506 // Handle form submission 344 507 if ($_SERVER['REQUEST_METHOD'] === 'POST' && check_admin_referer('wpcs_edit_'.$link->id)) { 345 // FIXED: Properly sanitize POST data346 508 $password = !empty($_POST['wpcs_password']) ? sanitize_text_field($_POST['wpcs_password']) : ''; 347 509 $rotate = isset($_POST['wpcs_rotate']) ? 1 : 0; 348 349 // FIXED: Sanitize textarea and validate URLs 350 $destinations_input = sanitize_textarea_field($_POST['wpcs_destinations']); 351 $dest_raw = explode("\n", trim($destinations_input)); 352 $new_dests = array_map('esc_url_raw', array_filter(array_map('trim', $dest_raw))); 353 354 // Validate that we have at least one valid URL 355 if (empty($new_dests)) { 356 echo '<div class="notice notice-error is-dismissible"><p>Please provide at least one valid destination URL.</p></div>'; 357 } else { 358 // update link row 359 $wpdb->update($wpdb->prefix.'wpcs_links', 360 ['password' => $password ?: null, 'is_rotating' => $rotate], 361 ['id' => $link->id]); 362 363 // replace destinations 364 $wpdb->delete($dest_tbl, ['link_id' => $link->id]); 510 $random_post = isset($_POST['wpcs_random_post']) ? 1 : 0; 511 512 $new_dests = []; 513 if (!$random_post) { 514 $destinations_input = sanitize_textarea_field($_POST['wpcs_destinations']); 515 $dest_raw = explode("\n", trim($destinations_input)); 516 $new_dests = array_map('esc_url_raw', array_filter(array_map('trim', $dest_raw))); 517 518 if (empty($new_dests)) { 519 echo '<div class="notice notice-error is-dismissible"><p>Please provide at least one valid destination URL for non-random links.</p></div>'; 520 error_log('WPCS: No valid destination URLs provided for link ID ' . $link->id); 521 return; 522 } 523 } 524 525 $result = $wpdb->update($wpdb->prefix.'wpcs_links', 526 [ 527 'password' => $password ?: null, 528 'is_rotating' => $rotate, 529 'is_random_post' => $random_post 530 ], 531 ['id' => $link->id], 532 ['%s', '%d', '%d'], 533 ['%d'] 534 ); 535 536 if ($result === false) { 537 echo '<div class="notice notice-error is-dismissible"><p>Failed to update short link: ' . esc_html($wpdb->last_error) . '</p></div>'; 538 error_log('WPCS: Failed to update link ID ' . $link->id . ': ' . $wpdb->last_error); 539 return; 540 } 541 542 $wpdb->delete($dest_tbl, ['link_id' => $link->id]); 543 if (!$random_post && !empty($new_dests)) { 365 544 foreach ($new_dests as $u) { 366 $wpdb->insert($dest_tbl, ['link_id' => $link->id, 'destination_url' => $u]); 367 } 368 369 echo '<div class="notice notice-success is-dismissible"><p>Short link updated successfully!</p></div>'; 370 $dests = $new_dests; 371 $link->password = $password; 372 $link->is_rotating = $rotate; 373 } 545 $dest_result = $wpdb->insert($dest_tbl, ['link_id' => $link->id, 'destination_url' => $u], ['%d', '%s']); 546 if ($dest_result === false) { 547 echo '<div class="notice notice-error is-dismissible"><p>Failed to save destination URL: ' . esc_html($wpdb->last_error) . '</p></div>'; 548 error_log('WPCS: Failed to insert destination URL for link_id ' . $link->id . ': ' . $wpdb->last_error); 549 return; 550 } 551 } 552 } 553 554 echo '<div class="notice notice-success is-dismissible"><p>Short link updated successfully!</p></div>'; 555 $dests = $new_dests; 556 $link->password = $password; 557 $link->is_rotating = $rotate; 558 $link->is_random_post = $random_post; 374 559 } else { 375 560 $dests = $wpdb->get_col($wpdb->prepare("SELECT destination_url FROM $dest_tbl WHERE link_id=%d", $link->id)); … … 396 581 </div> 397 582 398 <div class="form-field" >583 <div class="form-field" id="wpcs_destinations_container"> 399 584 <label for="wpcs_destinations">Destination URLs</label> 400 <textarea id="wpcs_destinations" name="wpcs_destinations" rows="5" class="large-text code" required><?php echo esc_textarea(implode("\n", $dests)); ?></textarea>401 <p class="description">One URL per line. Visitors will be redirected to these URLs.</p>585 <textarea id="wpcs_destinations" name="wpcs_destinations" rows="5" class="large-text code"><?php echo esc_textarea(implode("\n", $dests)); ?></textarea> 586 <p class="description">One URL per line. Required unless Random Post Redirect is enabled.</p> 402 587 </div> 403 588 404 589 <div class="form-field"> 405 590 <label class="checkbox-container"> 406 <input type="checkbox" name="wpcs_rotate" value="1" <?php checked($link->is_rotating); ?>>591 <input type="checkbox" name="wpcs_rotate" id="wpcs_rotate" value="1" <?php checked($link->is_rotating); ?>> 407 592 <span class="checkmark"></span> 408 593 Enable URL Rotation 409 594 </label> 595 </div> 596 597 <div class="form-field"> 598 <label class="checkbox-container"> 599 <input type="checkbox" name="wpcs_random_post" id="wpcs_random_post" value="1" <?php checked($link->is_random_post); ?>> 600 <span class="checkmark"></span> 601 Enable Random Post Redirect 602 </label> 603 <p class="description">When enabled, visitors will be redirected to a random published post, and the destination URLs field will be ignored.</p> 410 604 </div> 411 605 … … 444 638 $delete_id = absint($_GET['delete']); 445 639 if ($delete_id && check_admin_referer('wpcs_delete_'.$delete_id)) { 446 // Delete from all tables640 $wpdb->query('START TRANSACTION'); 447 641 $wpdb->delete($wpdb->prefix.'wpcs_links', ['id' => $delete_id]); 448 642 $wpdb->delete($wpdb->prefix.'wpcs_destinations', ['link_id' => $delete_id]); 449 643 $wpdb->delete($wpdb->prefix.'wpcs_clicks', ['link_id' => $delete_id]); 450 644 $wpdb->query('COMMIT'); 451 645 echo '<div class="notice notice-success is-dismissible"><p>Short link deleted successfully.</p></div>'; 452 // FIXED: Use proper JavaScript enqueuing for redirect453 646 wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();'); 454 647 return; … … 491 684 echo '<a href="'.esc_url(admin_url('admin.php?page=wpcs-analytics')).'" class="button button-secondary"><span class="dashicons dashicons-arrow-left-alt"></span> Back to Overview</a>'; 492 685 if ($view_date) { 493 echo '<button class="button " onclick="wpcsCSVdetail()"><span class="dashicons dashicons-download"></span> Export CSV</button>';686 echo '<button class="button wpcs-export-detail" type="button"><span class="dashicons dashicons-download"></span> Export CSV</button>'; 494 687 } 495 688 echo '</div>'; … … 520 713 } 521 714 echo '</tbody></table></div>'; 715 } else { 716 echo '<div class="no-data"><p>No clicks recorded for this link yet.</p></div>'; 522 717 } 523 718 … … 587 782 echo '</div>'; 588 783 589 // FIXED: Update CSV filename with proper escaping and use gmdate()590 784 $csv_filename = 'analytics_' . esc_js($link->alias) . '_' . esc_js($view_date) . '.csv'; 591 785 wp_localize_script('wpcs-admin-js', 'wpcsAdmin', array( … … 614 808 LEFT JOIN {$clicks} c ON l.id = c.link_id 615 809 GROUP BY l.id 616 ORDER BY total_clicks DESC,l.id DESC810 ORDER BY l.id DESC 617 811 "); 618 812 … … 630 824 echo '<a href="'.esc_url(admin_url('admin.php?page=wpcs-shortener')).'" class="button button-primary">'; 631 825 echo '<span class="dashicons dashicons-plus-alt"></span> Create New Link</a>'; 632 echo '<button class="button " onclick="wpcsCSVOverview()"><span class="dashicons dashicons-download"></span> Export All CSV</button>';826 echo '<button class="button wpcs-export-overview" type="button"><span class="dashicons dashicons-download"></span> Export All CSV</button>'; 633 827 echo '</div>'; 634 828 635 829 echo '<div class="table-container">'; 636 echo '<table class="wp-list-table widefat fixed striped">';830 echo '<table class="wp-list-table widefat fixed striped">'; 637 831 echo '<thead><tr> 638 832 <th>Short URL</th> … … 647 841 $short_url = home_url('/go/'.$link->alias); 648 842 $last_click = $link->last_click ? mysql2date('M j, Y g:i a', $link->last_click) : 'Never'; 649 $link_type = $link->is_r otating ? 'Rotating' : 'Standard';843 $link_type = $link->is_random_post ? 'Random Post' : ($link->is_rotating ? 'Rotating' : 'Standard'); 650 844 $link_type .= $link->password ? ' (Password)' : ''; 651 845 … … 695 889 // CSV Export Functions 696 890 // -------------------------------------------------- 891 // -------------------------------------------------- 892 // CSV Export Functions 893 // -------------------------------------------------- 697 894 add_action('wp_ajax_wpcs_export_csv', 'wpcs_handle_csv_export'); 698 895 function wpcs_handle_csv_export() { 896 // Check nonce for security 699 897 check_ajax_referer('wpcs_admin_nonce', 'nonce'); 898 899 // WordPress required capability check - ADD THIS LINE 900 if (!current_user_can('manage_options')) { 901 wp_die('Insufficient permissions'); 902 } 700 903 701 904 global $wpdb; -
custom-link-shortener/trunk/js/admin.js
r3305630 r3306152 1 1 /** 2 2 * Custom Link Shortener - Admin JavaScript 3 * Version: 1.4. 13 * Version: 1.4.2 4 4 */ 5 5 … … 11 11 } 12 12 13 // CSV Export function for detailed analytics 14 function wpcsCSVdetail() { 15 var rows = [...document.querySelectorAll('tbody tr')].map(tr => 16 [...tr.children].map(td => td.textContent.trim()) 17 ); 18 var csv = 'Date,IP,Country,City,UA\n' + rows.map(r => r.join(',')).join('\n'); 13 // CSV Export function for overview page 14 function wpcsCSVOverview() { 15 var rows = [...document.querySelectorAll('.wp-list-table tbody tr')].map(tr => { 16 var cells = [...tr.children]; 17 return [ 18 cells[0].querySelector('strong a') ? cells[0].querySelector('strong a').textContent.trim() : '', 19 cells[1].textContent.trim(), 20 cells[2].textContent.trim(), 21 cells[3].textContent.trim() 22 ]; 23 }); 24 25 if (rows.length === 0) { 26 alert('No data to export'); 27 return; 28 } 29 30 var csv = 'Short URL,Total Clicks,Last Click,Type\n' + rows.map(r => r.join(',')).join('\n'); 19 31 var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'}); 20 32 var url = URL.createObjectURL(blob); 21 33 var a = document.createElement('a'); 22 34 a.href = url; 23 a.download = wpcsAdmin.csvFilename; 35 a.download = 'analytics_overview_' + new Date().toISOString().split('T')[0] + '.csv'; 36 document.body.appendChild(a); 24 37 a.click(); 38 document.body.removeChild(a); 25 39 URL.revokeObjectURL(url); 40 } 41 42 // CSV Export function for detailed analytics 43 function wpcsCSVdetail() { 44 var table = document.querySelector('.wp-list-table:not(.daily-stats)'); 45 if (!table) { 46 alert('No detail data to export'); 47 return; 48 } 49 50 var rows = [...table.querySelectorAll('tbody tr')].map(tr => 51 [...tr.children].map(td => td.textContent.trim()) 52 ); 53 54 if (rows.length === 0) { 55 alert('No data to export'); 56 return; 57 } 58 59 var csv = 'Date,IP,Country,City,User Agent\n' + rows.map(r => r.join(',')).join('\n'); 60 var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'}); 61 var url = URL.createObjectURL(blob); 62 var a = document.createElement('a'); 63 a.href = url; 64 a.download = wpcsAdmin.csvFilename || ('analytics_detail_' + new Date().toISOString().split('T')[0] + '.csv'); 65 document.body.appendChild(a); 66 a.click(); 67 document.body.removeChild(a); 68 URL.revokeObjectURL(url); 69 } 70 71 // Alternative server-side CSV export 72 function wpcsServerCSVExport(type, linkId, date) { 73 var params = new URLSearchParams({ 74 action: 'wpcs_export_csv', 75 nonce: wpcsAdmin.nonce, 76 type: type 77 }); 78 79 if (linkId) params.append('link_id', linkId); 80 if (date) params.append('date', date); 81 82 var filename = type === 'overview' ? 83 'analytics_overview_' + new Date().toISOString().split('T')[0] + '.csv' : 84 'analytics_detail_' + (date || new Date().toISOString().split('T')[0]) + '.csv'; 85 86 params.append('filename', filename); 87 88 var form = document.createElement('form'); 89 form.method = 'POST'; 90 form.action = wpcsAdmin.ajaxUrl; 91 form.style.display = 'none'; 92 93 params.forEach((value, key) => { 94 var input = document.createElement('input'); 95 input.type = 'hidden'; 96 input.name = key; 97 input.value = value; 98 form.appendChild(input); 99 }); 100 101 document.body.appendChild(form); 102 form.submit(); 103 document.body.removeChild(form); 26 104 } 27 105 28 106 // Initialize admin functionality 29 107 document.addEventListener('DOMContentLoaded', function() { 30 // Any additional initialization can go here31 108 console.log('WPCS Admin JS loaded'); 109 110 // Add click handlers for export buttons 111 var overviewExportBtn = document.querySelector('.wpcs-export-overview'); 112 if (overviewExportBtn) { 113 overviewExportBtn.addEventListener('click', function(e) { 114 e.preventDefault(); 115 wpcsCSVOverview(); 116 }); 117 } 118 119 var detailExportBtn = document.querySelector('.wpcs-export-detail'); 120 if (detailExportBtn) { 121 detailExportBtn.addEventListener('click', function(e) { 122 e.preventDefault(); 123 wpcsCSVdetail(); 124 }); 125 } 126 127 // Initialize clipboard functionality 128 if (typeof ClipboardJS !== 'undefined') { 129 new ClipboardJS('.wpcs-copy').on('success', function(e) { 130 var originalText = e.trigger.textContent; 131 e.trigger.textContent = 'Copied!'; 132 setTimeout(function() { 133 e.trigger.textContent = originalText; 134 }, 2000); 135 e.clearSelection(); 136 }); 137 } 138 139 // Handle random post checkbox toggle 140 var randomPostCheckbox = document.getElementById('wpcs_random_post'); 141 var destinationsContainer = document.getElementById('wpcs_destinations_container'); 142 var destinationsField = document.getElementById('wpcs_destinations'); 143 var rotationCheckbox = document.getElementById('wpcs_rotate'); 144 145 if (randomPostCheckbox && destinationsContainer && destinationsField) { 146 function toggleDestinationsField() { 147 if (randomPostCheckbox.checked) { 148 destinationsContainer.style.display = 'none'; 149 destinationsField.removeAttribute('required'); 150 destinationsField.value = ''; // Clear the field 151 if (rotationCheckbox) { 152 rotationCheckbox.checked = false; 153 rotationCheckbox.disabled = true; 154 } 155 } else { 156 destinationsContainer.style.display = 'block'; 157 destinationsField.setAttribute('required', 'required'); 158 if (rotationCheckbox) { 159 rotationCheckbox.disabled = false; 160 } 161 } 162 } 163 164 randomPostCheckbox.addEventListener('change', toggleDestinationsField); 165 // Run on page load to set initial state 166 toggleDestinationsField(); 167 168 // Prevent form submission if destinations are required but empty 169 var form = destinationsField.closest('form'); 170 if (form) { 171 form.addEventListener('submit', function(e) { 172 if (!randomPostCheckbox.checked && !destinationsField.value.trim()) { 173 e.preventDefault(); 174 alert('Please provide at least one destination URL or enable Random Post Redirect.'); 175 } 176 }); 177 } 178 } 32 179 }); -
custom-link-shortener/trunk/readme.txt
r3305630 r3306152 1 1 # Custom Link Shortener 2 2 3 Contributors: V.Lukasso,Lukastech4 Tags: shortener, url shortener, link analytics, rotating links, password protection 3 Contributors: Lukastech 4 Tags: shortener, url shortener, link analytics, rotating links, password protection, Random blog post redirect, link shortener, custom link, custom link shortener, short links 5 5 Requires at least: 5.0 6 6 Tested up to: 6.8 7 Stable tag: 1.4. 27 Stable tag: 1.4.3 8 8 License: GPLv2 or later 9 9 License URI:** https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 A custom WordPress URL shortener plugin with advanced features like analytics, link rotation, location tracking, and password protection.11 A custom WordPress URL shortener plugin with advanced features like analytics, link rotation, location tracking, random blog post redirect, and password protection. 12 12 13 13 ## Features … … 25 25 - 🗑️ Delete short URLs with confirmation 26 26 - 🎨 Clean WordPress admin interface 27 - 🔄Create One Url that redirects to multiple posts on your website. 27 28 28 29 ## Installation 29 30 30 1. Upload the plugin files to `/wp-content/plugins/ wp-custom-shortener`31 1. Upload the plugin files to `/wp-content/plugins/custom-link-shortener` 31 32 2. Activate the plugin through the 'Plugins' menu in WordPress 32 33 3. Navigate to WP Shortener in your admin menu to create links … … 40 41 - Destination URL(s) (one per line for rotation) 41 42 3. Optional: Enable rotation or set a password 42 4. Click "Create Short Link" 43 4. Enable random post redirects 44 5. Click "Create Short Link" 43 45 44 46 ### Viewing Analytics … … 64 66 - Visitors must enter password before redirect 65 67 - Works with both GET and POST requests 68 69 ### Random blog article in one short link 70 - Query your WordPress database for all published posts 71 - Randomly select one post from the available options 72 - Redirect the visitor to that post's permalink 73 - Track the redirect for analytics purposes 74 #### Use Cases 75 76 - Content Discovery: Help visitors explore your older or less-visited content 77 - Newsletter Links: Add a "Random Article" link to your email newsletters 78 - Social Media: Share surprise content links on social platforms 79 - Website Navigation: Include a "Surprise Me" button in your site navigation 80 - Content Marketing: Create engaging campaigns around random content discovery 66 81 67 82 ## Screenshots … … 111 126 ## Changelog 112 127 128 ### 1.4.3 (June 2025) 129 - Redirect users to random posts on the website via a shortlink 130 - Total Redirects: Count of how many times the random link was used 131 - Unique Visitors: Number of unique users who clicked the random link 132 - Popular Destinations: Which posts are being randomly selected most often 133 - Enhanced U.I 134 113 135 ### 1.4.2 (May 2025) 114 136 - Fixed redirect functionality
Note: See TracChangeset
for help on using the changeset viewer.