Plugin Directory

Changeset 3306152


Ignore:
Timestamp:
06/04/2025 06:13:02 AM (7 months ago)
Author:
lukastech
Message:

Update to version 1.4.3

Location:
custom-link-shortener/trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • custom-link-shortener/trunk/custom-link-shortener.php

    r3305630 r3306152  
    22/*
    33Plugin Name: Custom Link Shortener
    4 Description: A custom URL shortener with analytics, rotating links, location tracking, and password protection.
    5 Version: 1.4.2
     4Description: A custom URL shortener with analytics, rotating links, Random blog post redirect, location tracking, and password protection.
     5Version: 1.4.3
    66Author: Lukastech
    77License: GPLv2 or later
     
    1313
    1414/**
    15  * CHANGELOG v1.4.1 – May 30, 2025
     15 * CHANGELOG v1.4.3 – June 1, 2025
    1616 * ------------------------------------------------
    1717 * • Complete UI/UX redesign
     
    2424 * • Better notification messages
    2525 * • Fixed WordPress compliance issues
     26 * • Fixed is_random_post column creation issue
    2627 * ------------------------------------------------
    2728 */
     
    2930defined('ABSPATH') or die('No script kiddies please!');
    3031
    31 // Enqueue admin styles and scripts
     32// Enqueue admin styles and scripts - UPDATED VERSION
    3233add_action('admin_enqueue_scripts', 'wpcs_enqueue_admin_assets');
    3334function wpcs_enqueue_admin_assets($hook) {
     
    3839   
    3940    // 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');
    4142   
    4243    // 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
    4650    wp_localize_script('wpcs-admin-js', 'wpcsAdmin', array(
    4751        'analyticsUrl' => admin_url('admin.php?page=wpcs-analytics'),
     52        'ajaxUrl' => admin_url('admin-ajax.php'), // Added AJAX URL
    4853        'csvFilename' => 'analytics_export_' . gmdate('Y-m-d') . '.csv',
    4954        'nonce' => wp_create_nonce('wpcs_admin_nonce')
     
    5257
    5358// --------------------------------------------------
    54 // Activation & DB (unchanged)
     59// Activation & DB - FIXED VERSION
    5560// --------------------------------------------------
    5661register_activation_hook(__FILE__, function () {
     
    6065});
    6166
    62 // --- MAKE SURE THIS FUNCTION HAS THE ACTUAL DB CREATION CODE ---
     67// Add database version option to track migrations
     68add_action('plugins_loaded', 'wpcs_check_db_version');
     69function 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
    6380function wpcs_create_db(){
    6481    global $wpdb;
     
    6986    $click_tbl = $wpdb->prefix . 'wpcs_clicks';
    7087
    71     $sql = "CREATE TABLE $link_tbl (
     88    // Create the main links table with all required columns
     89    $sql_links = "CREATE TABLE $link_tbl (
    7290        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,
    7492        password varchar(255),
    7593        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 (
    80100        id bigint(20) NOT NULL AUTO_INCREMENT,
    81101        link_id bigint(20) NOT NULL,
    82102        destination_url text NOT NULL,
    83103        PRIMARY KEY (id),
    84         FOREIGN KEY (link_id) REFERENCES $link_tbl(id) ON DELETE CASCADE
    85     ) $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 (
    88108        id bigint(20) NOT NULL AUTO_INCREMENT,
    89109        link_id bigint(20) NOT NULL,
     
    94114        clicked_at datetime NOT NULL,
    95115        PRIMARY KEY (id),
    96         FOREIGN KEY (link_id) REFERENCES $link_tbl(id) ON DELETE CASCADE
     116        KEY link_id (link_id)
    97117    ) $charset_collate;";
    98118
    99119    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}
    103183
    104184// Add rewrite rule for short URLs like /go/abc123
     
    133213    if ($link->password) {
    134214        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['wpcs_password'])) {
    135             // Sanitize password input
    136215            $entered_password = sanitize_text_field($_POST['wpcs_password']);
    137216            if ($entered_password !== $link->password) {
     
    148227    }
    149228
    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
    158257    $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
    159258    $ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '';
    160259   
    161     // Validate IP format
    162260    if (!filter_var($ip, FILTER_VALIDATE_IP)) {
    163261        $ip = '127.0.0.1'; // fallback
     
    175273    ]);
    176274
    177     // FIXED: Use wp_safe_redirect instead of wp_redirect
    178275    wp_safe_redirect(esc_url_raw($target_url), 301);
    179276    exit;
     
    223320
    224321// --------------------------------------------------
    225 // Short-link creation
     322// FIXED Short-link creation
    226323// --------------------------------------------------
    227324function wpcs_render_admin_page(){
     
    243340                </div>
    244341               
    245                 <div class="form-field">
     342                <div class="form-field" id="wpcs_destinations_container">
    246343                    <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>
    248346                </div>
    249347               
    250348                <div class="form-field">
    251349                    <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">
    253351                        <span class="checkmark"></span>
    254352                        Enable URL Rotation
    255353                    </label>
    256354                    <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>
    257364                </div>
    258365               
     
    274381}
    275382
     383// FIXED FORM SUBMISSION HANDLER
    276384function wpcs_handle_form_submission(){
    277385    global $wpdb;
    278386   
    279     // FIXED: Properly sanitize and validate all POST data
     387    // Sanitize and validate POST data
    280388    $alias = sanitize_text_field($_POST['wpcs_alias']);
    281389    $password = !empty($_POST['wpcs_password']) ? sanitize_text_field($_POST['wpcs_password']) : '';
    282390    $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;
    288392   
    289393    // Validate the alias/slug
    290394    if (!preg_match('/^[a-z0-9-_]+$/i', $alias)) {
    291395        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);
    298397        return;
    299398    }
     
    302401    $dest_tbl = $wpdb->prefix.'wpcs_destinations';
    303402   
     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   
    304419    // 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);
    307424        return;
    308425    }
    309426   
    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   
    311463    $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        }
    315480    }
    316481   
     
    318483    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>';
    319484   
    320     // FIXED: Use proper JavaScript enqueuing for redirect
    321485    wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();');
    322486}
     
    336500       
    337501        echo '<div class="notice notice-success is-dismissible"><p>Link deleted successfully.</p></div>';
    338         // FIXED: Use proper JavaScript enqueuing for redirect
    339502        wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();');
    340503        return;
    341504    }
    342505   
    343     // Handle form submission for edits
     506    // Handle form submission
    344507    if ($_SERVER['REQUEST_METHOD'] === 'POST' && check_admin_referer('wpcs_edit_'.$link->id)) {
    345         // FIXED: Properly sanitize POST data
    346508        $password = !empty($_POST['wpcs_password']) ? sanitize_text_field($_POST['wpcs_password']) : '';
    347509        $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)) {
    365544            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;
    374559    } else {
    375560        $dests = $wpdb->get_col($wpdb->prepare("SELECT destination_url FROM $dest_tbl WHERE link_id=%d", $link->id));
     
    396581                </div>
    397582               
    398                 <div class="form-field">
     583                <div class="form-field" id="wpcs_destinations_container">
    399584                    <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>
    402587                </div>
    403588               
    404589                <div class="form-field">
    405590                    <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); ?>>
    407592                        <span class="checkmark"></span>
    408593                        Enable URL Rotation
    409594                    </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>
    410604                </div>
    411605               
     
    444638        $delete_id = absint($_GET['delete']);
    445639        if ($delete_id && check_admin_referer('wpcs_delete_'.$delete_id)) {
    446             // Delete from all tables
     640            $wpdb->query('START TRANSACTION');
    447641            $wpdb->delete($wpdb->prefix.'wpcs_links', ['id' => $delete_id]);
    448642            $wpdb->delete($wpdb->prefix.'wpcs_destinations', ['link_id' => $delete_id]);
    449643            $wpdb->delete($wpdb->prefix.'wpcs_clicks', ['link_id' => $delete_id]);
    450 
     644            $wpdb->query('COMMIT');
    451645            echo '<div class="notice notice-success is-dismissible"><p>Short link deleted successfully.</p></div>';
    452             // FIXED: Use proper JavaScript enqueuing for redirect
    453646            wp_add_inline_script('wpcs-admin-js', 'wpcsRedirectToAnalytics();');
    454647            return;
     
    491684        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>';
    492685        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>';
    494687        }
    495688        echo '</div>';
     
    520713            }
    521714            echo '</tbody></table></div>';
     715        } else {
     716            echo '<div class="no-data"><p>No clicks recorded for this link yet.</p></div>';
    522717        }
    523718       
     
    587782            echo '</div>';
    588783           
    589             // FIXED: Update CSV filename with proper escaping and use gmdate()
    590784            $csv_filename = 'analytics_' . esc_js($link->alias) . '_' . esc_js($view_date) . '.csv';
    591785            wp_localize_script('wpcs-admin-js', 'wpcsAdmin', array(
     
    614808        LEFT JOIN {$clicks} c ON l.id = c.link_id
    615809        GROUP BY l.id
    616         ORDER BY total_clicks DESC, l.id DESC
     810        ORDER BY l.id DESC
    617811    ");
    618812   
     
    630824        echo '<a href="'.esc_url(admin_url('admin.php?page=wpcs-shortener')).'" class="button button-primary">';
    631825        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>';
    633827        echo '</div>';
    634828       
    635829        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">';
    637831        echo '<thead><tr>
    638832                <th>Short URL</th>
     
    647841            $short_url = home_url('/go/'.$link->alias);
    648842            $last_click = $link->last_click ? mysql2date('M j, Y g:i a', $link->last_click) : 'Never';
    649             $link_type = $link->is_rotating ? 'Rotating' : 'Standard';
     843            $link_type = $link->is_random_post ? 'Random Post' : ($link->is_rotating ? 'Rotating' : 'Standard');
    650844            $link_type .= $link->password ? ' (Password)' : '';
    651845           
     
    695889// CSV Export Functions
    696890// --------------------------------------------------
     891// --------------------------------------------------
     892// CSV Export Functions
     893// --------------------------------------------------
    697894add_action('wp_ajax_wpcs_export_csv', 'wpcs_handle_csv_export');
    698895function wpcs_handle_csv_export() {
     896    // Check nonce for security
    699897    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    }
    700903   
    701904    global $wpdb;
  • custom-link-shortener/trunk/js/admin.js

    r3305630 r3306152  
    11/**
    22 * Custom Link Shortener - Admin JavaScript
    3  * Version: 1.4.1
     3 * Version: 1.4.2
    44 */
    55
     
    1111}
    1212
    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
     14function 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');
    1931    var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
    2032    var url = URL.createObjectURL(blob);
    2133    var a = document.createElement('a');
    2234    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);
    2437    a.click();
     38    document.body.removeChild(a);
    2539    URL.revokeObjectURL(url);
     40}
     41
     42// CSV Export function for detailed analytics
     43function 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
     72function 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);
    26104}
    27105
    28106// Initialize admin functionality
    29107document.addEventListener('DOMContentLoaded', function() {
    30     // Any additional initialization can go here
    31108    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    }
    32179});
  • custom-link-shortener/trunk/readme.txt

    r3305630 r3306152  
    11# Custom Link Shortener
    22
    3 Contributors: V.Lukasso, Lukastech
    4 Tags: shortener, url shortener, link analytics, rotating links, password protection 
     3Contributors: Lukastech
     4Tags: shortener, url shortener, link analytics, rotating links, password protection, Random blog post redirect, link shortener, custom link, custom link shortener, short links 
    55Requires at least: 5.0
    66Tested up to: 6.8
    7 Stable tag: 1.4.2
     7Stable tag: 1.4.3
    88License: GPLv2 or later 
    99License URI:** https://www.gnu.org/licenses/gpl-2.0.html 
    1010
    11 A custom WordPress URL shortener plugin with advanced features like analytics, link rotation, location tracking, and password protection.
     11A custom WordPress URL shortener plugin with advanced features like analytics, link rotation, location tracking, random blog post redirect, and password protection.
    1212
    1313## Features
     
    2525- 🗑️ Delete short URLs with confirmation
    2626- 🎨 Clean WordPress admin interface
     27- 🔄Create One Url that redirects to multiple posts on your website.
    2728
    2829## Installation
    2930
    30 1. Upload the plugin files to `/wp-content/plugins/wp-custom-shortener`
     311. Upload the plugin files to `/wp-content/plugins/custom-link-shortener`
    31322. Activate the plugin through the 'Plugins' menu in WordPress
    32333. Navigate to WP Shortener in your admin menu to create links
     
    4041   - Destination URL(s) (one per line for rotation)
    41423. Optional: Enable rotation or set a password
    42 4. Click "Create Short Link"
     434. Enable random post redirects
     445. Click "Create Short Link"
    4345
    4446### Viewing Analytics
     
    6466- Visitors must enter password before redirect
    6567- 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
    6681
    6782## Screenshots
     
    111126## Changelog
    112127
     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
    113135### 1.4.2 (May 2025)
    114136- Fixed redirect functionality
Note: See TracChangeset for help on using the changeset viewer.