From 5c4a7cdeacfaa61ee7f6e7be4adc2f6c3664489d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E6=B4=BE=E5=A4=87=E6=A1=88?= <130886204+modiqi@users.noreply.github.com> Date: Sun, 1 Jun 2025 18:01:15 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.0=20=E7=A8=B3=E5=AE=9A=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-page.php | 1008 ++++++++++++++++++++++++++++++++ admin.css | 920 +++++++++++++++++++++++++++++ admin.js | 494 ++++++++++++++++ assets/i-like-food.svg | 1 + assets/leaf.svg | 1 + class-admin.php | 131 +++++ class-public.php | 323 ++++++++++ class-wpnav-links.php | 494 ++++++++++++++++ external-indicator.css | 135 +++++ frontend.css | 837 ++++++++++++++++++++++++++ languages/wpnav-links-zh_CN.mo | Bin 0 -> 17388 bytes languages/wpnav-links-zh_CN.po | 964 ++++++++++++++++++++++++++++++ languages/wpnav-links.pot | 389 ++++++++++++ readme.txt | 176 ++++++ redirect-template.php | 561 ++++++++++++++++++ redirect.js | 343 +++++++++++ wpnav-links.php | 533 +++++++++++++++++ 17 files changed, 7310 insertions(+) create mode 100644 admin-page.php create mode 100644 admin.css create mode 100644 admin.js create mode 100644 assets/i-like-food.svg create mode 100644 assets/leaf.svg create mode 100644 class-admin.php create mode 100644 class-public.php create mode 100644 class-wpnav-links.php create mode 100644 external-indicator.css create mode 100644 frontend.css create mode 100644 languages/wpnav-links-zh_CN.mo create mode 100644 languages/wpnav-links-zh_CN.po create mode 100644 languages/wpnav-links.pot create mode 100644 readme.txt create mode 100644 redirect-template.php create mode 100644 redirect.js create mode 100644 wpnav-links.php diff --git a/admin-page.php b/admin-page.php new file mode 100644 index 0000000..58994ce --- /dev/null +++ b/admin-page.php @@ -0,0 +1,1008 @@ + isset($_POST['auto_whitelist_same_root']) ? 1 : 0, + 'search_engines' => isset($_POST['auto_whitelist_search_engines']) ? 1 : 0 + ); + + $old_options_json = json_encode($old_options); + $new_options_json = json_encode($options); + $data_changed = ($old_options_json !== $new_options_json); + + if ($data_changed) { + $result = update_option('wpnav_links_options', $options); + if ($result !== false) { + $success_message = __('Whitelist settings saved successfully!', 'wpnav-links'); + } else { + $error_message = __('Failed to save whitelist settings. Database error.', 'wpnav-links'); + } + } else { + $success_message = __('Whitelist settings saved successfully!', 'wpnav-links'); + } + $current_tab = 'whitelist'; + } +} + +if (isset($_POST['wpnav_redirect_nonce']) && wp_verify_nonce($_POST['wpnav_redirect_nonce'], 'wpnav_redirect_page_nonce')) { + if (!current_user_can('manage_options')) { + $error_message = __('You do not have sufficient permissions to access this page.', 'wpnav-links'); + } else { + $old_options = get_option('wpnav_links_options', array()); + $options = $old_options; + + $options['template'] = isset($_POST['template']) ? sanitize_text_field($_POST['template']) : 'default'; + $options['color_scheme'] = isset($_POST['color_scheme']) ? sanitize_text_field($_POST['color_scheme']) : 'blue'; + $options['page_title'] = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : 'External Link Warning'; + $options['page_subtitle'] = isset($_POST['page_subtitle']) ? sanitize_text_field($_POST['page_subtitle']) : ''; + $options['url_label'] = isset($_POST['url_label']) ? sanitize_text_field($_POST['url_label']) : 'You are about to visit:'; + $options['warning_text'] = isset($_POST['warning_text']) ? sanitize_textarea_field($_POST['warning_text']) : ''; + $options['show_warning_message'] = isset($_POST['show_warning_message']) ? 1 : 0; + $options['show_logo'] = isset($_POST['show_logo']) ? 1 : 0; + $options['show_url_full'] = isset($_POST['show_url_full']) ? 1 : 0; + $options['show_security_info'] = isset($_POST['show_security_info']) ? 1 : 0; + $options['show_security_tips'] = isset($_POST['show_security_tips']) ? 1 : 0; + $options['show_back_button'] = isset($_POST['show_back_button']) ? 1 : 0; + $options['custom_css'] = isset($_POST['custom_css']) ? wp_strip_all_tags($_POST['custom_css']) : ''; + $options['button_text_continue'] = isset($_POST['button_text_continue']) ? sanitize_text_field($_POST['button_text_continue']) : 'Continue'; + $options['button_text_back'] = isset($_POST['button_text_back']) ? sanitize_text_field($_POST['button_text_back']) : 'Back'; + $options['button_style'] = isset($_POST['button_style']) ? sanitize_text_field($_POST['button_style']) : 'rounded'; + $options['countdown_text'] = isset($_POST['countdown_text']) ? sanitize_text_field($_POST['countdown_text']) : 'Auto redirect in {seconds} seconds'; + $options['show_progress_bar'] = isset($_POST['show_progress_bar']) ? 1 : 0; + + $old_options_json = json_encode($old_options); + $new_options_json = json_encode($options); + $data_changed = ($old_options_json !== $new_options_json); + + if ($data_changed) { + $result = update_option('wpnav_links_options', $options); + if ($result !== false) { + $success_message = __('Redirect page settings saved successfully!', 'wpnav-links'); + } else { + $error_message = __('Failed to save redirect page settings. Database error.', 'wpnav-links'); + } + } else { + $success_message = __('Redirect page settings saved successfully!', 'wpnav-links'); + } + $current_tab = 'redirect_page'; + } +} + +if (isset($_POST['wpnav_import_nonce']) && wp_verify_nonce($_POST['wpnav_import_nonce'], 'wpnav_import_csv')) { + if (!current_user_can('manage_options')) { + $error_message = __('You do not have sufficient permissions to access this page.', 'wpnav-links'); + } elseif (!$wp_china_yes_active) { + $error_message = __('Import functionality requires 文派叶子 🍃(WPCY.COM)to be active.', 'wpnav-links'); + } else { + if (!empty($_FILES['whitelist_csv']['tmp_name'])) { + $csv_file = $_FILES['whitelist_csv']['tmp_name']; + $domains = array(); + + if (($handle = fopen($csv_file, "r")) !== FALSE) { + while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { + if (!empty($data[0])) { + $domains[] = sanitize_text_field($data[0]); + } + } + fclose($handle); + } + + if (!empty($domains)) { + $options = get_option('wpnav_links_options', array()); + $existing_domains = explode("\n", isset($options['whitelist_domains']) ? $options['whitelist_domains'] : ''); + $existing_domains = array_map('trim', $existing_domains); + $combined_domains = array_merge($existing_domains, $domains); + $combined_domains = array_filter($combined_domains); + $combined_domains = array_unique($combined_domains); + + $options['whitelist_domains'] = implode("\n", $combined_domains); + + $result = update_option('wpnav_links_options', $options); + if ($result !== false) { + $imported_count = count($domains); + $success_message = sprintf(__('Successfully imported %d domains.', 'wpnav-links'), $imported_count); + } else { + $error_message = __('Failed to import CSV data. Database error.', 'wpnav-links'); + } + } else { + $error_message = __('No valid domains found in the CSV file.', 'wpnav-links'); + } + } else { + $error_message = __('Please select a CSV file to upload.', 'wpnav-links'); + } + $current_tab = 'import_export'; + } +} + +if (isset($_GET['message'])) { + $message_type = sanitize_text_field($_GET['message']); + if ($message_type === 'saved') { + $success_message = __('Settings saved successfully!', 'wpnav-links'); + } elseif ($message_type === 'imported') { + $count = isset($_GET['count']) ? intval($_GET['count']) : 0; + $success_message = sprintf(__('Successfully imported %d domains.', 'wpnav-links'), $count); + } +} + +$options = get_option('wpnav_links_options', array()); + +$current_time = current_time('timestamp'); +$end_date = isset($_GET['end_date']) ? sanitize_text_field($_GET['end_date']) : date('Y-m-d', $current_time); +$start_date = isset($_GET['start_date']) ? sanitize_text_field($_GET['start_date']) : date('Y-m-d', strtotime('-30 days', $current_time)); + +$limit = 20; +$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; +$offset = ($current_page - 1) * $limit; + +$orderby = isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : 'click_time'; +$order = isset($_GET['order']) ? sanitize_text_field($_GET['order']) : 'DESC'; + +$stats = $plugin->get_stats(array( + 'start_date' => $start_date, + 'end_date' => $end_date, + 'limit' => $limit, + 'offset' => $offset, + 'order_by' => $orderby, + 'order' => $order +)); + +$total_items = $plugin->get_total_count($start_date, $end_date); +$total_pages = ceil($total_items / $limit); +$top_urls = $plugin->get_top_urls(10); + +$custom_template_exists = file_exists(get_stylesheet_directory() . '/wpnav-redirect-template.php') || + file_exists(get_template_directory() . '/wpnav-redirect-template.php'); +?> +
+

+ + + + + + + + + +

+ + +
+

+
+ + + +
+

+
+ + + +
+

+
+ +
+ + +
+ +
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
prefix . WPNAV_LINKS_TABLE; ?>
+
+
diff --git a/admin.css b/admin.css new file mode 100644 index 0000000..7067e27 --- /dev/null +++ b/admin.css @@ -0,0 +1,920 @@ +.card { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + max-width: unset; + margin-top: 20px; + padding: 20px; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); +} + +.card h2 { + margin-top: 0; + margin-bottom: 15px; + font-size: 18px; + font-weight: 600; + padding-bottom: 10px; + color: #23282d; +} + +.card p { + color: #50575e; +} + +.wpnav-tabs { + display: flex; + flex-wrap: wrap; + gap: 5px; + border-bottom: 1px solid #c3c4c7; + margin-bottom: 20px; +} + +.wpnav-tab { + padding: 8px 16px; + border: none; + background: none; + cursor: pointer; + font-size: 14px; + border-bottom: 2px solid transparent; + color: #646970; + text-decoration: none; + white-space: nowrap; + transition: all 0.2s ease; +} + +.wpnav-tab:hover:not(.active) { + background: #f0f0f1; + border-bottom-color: #dcdcde; + color: #1d2327; +} + +.wpnav-tab.active { + border-bottom: 2px solid #0073aa; + font-weight: 600; + background: #f0f0f1; + color: #1d2327; +} + +.wpnav-tab:focus { + outline: 2px solid #0073aa; + outline-offset: -2px; +} + +.wpnav-tab-content { + flex: 1; +} + +.wpnav-tab-section { + display: block; +} + +.wpnav-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #ddd; +} + +.wpnav-section-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #23282d; +} + +.wpnav-section-actions { + display: flex; + gap: 8px; +} + +.wpnav-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.wpnav-info-item { + background: #f9f9f9; + border: 1px solid #e5e5e5; + border-radius: 4px; + padding: 15px; +} + +.wpnav-info-item h3, +.wpnav-info-item h4 { + margin-top: 0; + margin-bottom: 10px; + font-size: 14px; + font-weight: 600; + color: #23282d; +} + +.wpnav-export-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.wpnav-export-item { + background: #f6f7f7; + border: 1px solid #ddd; + border-radius: 4px; + padding: 20px; + margin-top: 20px; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); +} + +.wpnav-export-item h2 { + margin-top: 0; + margin-bottom: 15px; + font-size: 18px; + font-weight: 600; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + color: #23282d; +} + +.wpnav-export-item p { + font-size: 13px; + color: #50575e; + margin-bottom: 15px; +} + +.wpnav-notice { + padding: 8px 12px; + border-radius: 3px; + margin: 10px 0; + display: block; +} + +.wpnav-notice-success { + background-color: #d1e7dd; + border: 1px solid #badbcc; + color: #0f5132; +} + +.wpnav-notice-error { + background-color: #f8d7da; + border: 1px solid #f5c2c7; + color: #842029; +} + +.wpnav-auto-rules { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #ddd; +} + +.wpnav-auto-rules h4 { + margin-top: 0; + margin-bottom: 15px; + font-size: 15px; + font-weight: 600; + color: #23282d; +} + +.wpnav-auto-rules .wp-list-table td { + padding: 15px 12px; + border-radius: 4px; +} + +.wpnav-auto-rules label { + font-weight: 600; + margin-bottom: 5px; + display: block; + color: #23282d; +} + +.wpnav-auto-rules .description { + margin-top: 5px; + font-size: 13px; + color: #50575e; +} + +.wpnav-quick-domains { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.wpnav-quick-domains .button-small { + font-size: 12px; + padding: 4px 10px; + height: auto; + line-height: 1.4; + border-radius: 3px; +} + +.wpnav-stats-overview { + display: flex; + gap: 15px; + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; +} + +.wpnav-stat-item { + text-align: center; + flex: 1; + padding: 10px; +} + +.wpnav-stat-number { + display: block; + font-size: 20px; + font-weight: 600; + color: #495057; + line-height: 1.2; + margin-bottom: 4px; +} + +.wpnav-stat-label { + display: block; + font-size: 12px; + color: #6c757d; + font-weight: 500; +} + +.wpnav-url-link { + color: #0073aa; + text-decoration: none; + transition: color 0.2s ease; +} + +.wpnav-url-link:hover { + color: #005a87; + text-decoration: underline; +} + +.wpnav-url-link .dashicons { + opacity: 0.6; + margin-left: 3px; + transition: opacity 0.2s ease; +} + +.wpnav-url-link:hover .dashicons { + opacity: 1; +} + +.wpnav-no-data, +.wpnav-no-data-message { + color: #50575e; + font-style: italic; + text-align: center; + padding: 30px 20px; +} + +.wpnav-no-data-message p { + margin: 8px 0; + font-size: 14px; +} + +.wpnav-no-data-message .description { + font-size: 13px; + color: #8c8f94; +} + +.wpnav-https-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + color: white; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 50px; + text-align: center; +} + +.wpnav-https-badge.secure { + background-color: #00a32a; +} + +.wpnav-https-badge.insecure { + background-color: #d63638; +} + +.wpnav-top-links { + margin: 0; +} + +.wpnav-top-link-item { + display: flex; + align-items: center; + padding: 5px 0; + border-bottom: 1px solid #f0f0f0; +} + +.wpnav-top-link-item:last-child { + border-bottom: none; +} + +.wpnav-rank { + font-weight: 700; + color: #0073aa; + margin-right: 12px; + min-width: 30px; + font-size: 14px; +} + +.wpnav-link-info { + flex: 1; + min-width: 0; +} + +.wpnav-link-title { + display: block; + color: #0073aa; + text-decoration: none; + font-size: 13px; + margin-bottom: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s ease; +} + +.wpnav-link-title:hover { + color: #005a87; + text-decoration: underline; +} + +.wpnav-click-count { + font-size: 11px; + color: #50575e; + font-weight: 500; +} + +.column-target-url { width: 30%; } +.column-source-page { width: 25%; } +.column-click-time { width: 15%; } +.column-ip-address { width: 15%; } +.column-https-status { width: 15%; } + +.wp-list-table tbody tr:hover, +.wpnav-row-hover { + background-color: #f6f7f7; +} + +.button-small { + font-size: 12px; + padding: 6px 10px; + line-height: 1.4; + height: auto; + border-radius: 3px; + transition: all 0.2s ease; +} + +.button-small:hover { + transform: translateY(-1px); +} + +.wp-list-table.fixed { + table-layout: fixed; +} + +.wp-list-table th:first-child { + width: 200px; +} + +.wp-list-table td code { + background: #f1f1f1; + padding: 3px 6px; + border-radius: 3px; + font-family: Consolas, Monaco, monospace; + font-size: 12px; +} + +.form-table th { + font-weight: 600; + color: #23282d; +} + +.form-table td .description { + font-size: 13px; + color: #50575e; + margin-top: 5px; +} + +.form-table input[type="number"], +.form-table select { + min-width: 100px; +} + +.wpnav-redirect-preview { + border: 2px solid #ddd; + border-radius: 8px; + padding: 20px; + background: #f8f9fa; + margin-top: 20px; + position: relative; + overflow: hidden; +} + +.wpnav-redirect-preview::before { + content: "Preview"; + position: absolute; + top: 10px; + right: 15px; + background: #0073aa; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + z-index: 10; +} + +.preview-container { + padding: 20px; + border-radius: 12px; + min-height: 400px; + display: flex; + align-items: center; + justify-content: center; + transform: scale(0.8); + transform-origin: center; + transition: all 0.3s ease; + overflow: visible; +} + +.preview-container .wpnav-container { + max-width: 400px; + width: 100%; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 16px; + padding: 30px; + text-align: center; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + position: relative; + z-index: 1; +} + +.preview-container .wpnav-container.wpnav-simple { + max-width: 320px; + padding: 25px; +} + +.preview-container .wpnav-container.wpnav-minimal { + max-width: 360px; + padding: 28px; +} + +.preview-container .wpnav-container.wpnav-full { + max-width: 480px; + padding: 35px; + text-align: left; +} + +.preview-container .wpnav-container.wpnav-default { + max-width: 400px; + padding: 30px; + text-align: center; +} + +.preview-container .wpnav-title { + font-size: 24px; + font-weight: 700; + margin: 0 0 15px; + color: #2c3e50; + transition: font-size 0.3s ease; +} + +.preview-container .wpnav-simple .wpnav-title { + font-size: 20px; + margin-bottom: 12px; +} + +.preview-container .wpnav-minimal .wpnav-title { + font-size: 22px; +} + +.preview-container .wpnav-full .wpnav-title { + font-size: 28px; + text-align: center; +} + +.preview-container .wpnav-default .wpnav-title { + font-size: 24px; +} + +.preview-container .wpnav-subtitle { + font-size: 14px; + color: #7f8c8d; + margin: 5px 0 20px; + font-weight: 400; +} + +.preview-container .wpnav-warning { + background: #fff3cd; + border: 1px solid #f39c12; + border-radius: 8px; + padding: 15px; + margin: 20px 0; + color: #8b4513; + font-size: 13px; +} + +.preview-container .wpnav-url-container { + margin: 15px 0; +} + +.preview-container .wpnav-url-label { + font-size: 12px; + color: #7f8c8d; + margin-bottom: 8px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.preview-container .wpnav-url { + background: #f8f9fa; + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 15px; + margin: 10px 0; + word-break: break-all; +} + +.preview-container .wpnav-simple .wpnav-url { + padding: 12px; +} + +.preview-container .wpnav-url-domain { + font-weight: 700; + color: #2c3e50; + font-size: 16px; +} + +.preview-container .wpnav-simple .wpnav-url-domain { + font-size: 14px; +} + +.preview-container .wpnav-url-full { + font-size: 11px; + color: #95a5a6; + margin-top: 8px; + font-family: 'Monaco', 'Consolas', monospace; + word-break: break-all; +} + +.preview-container .wpnav-security-status { + margin-top: 10px; +} + +.preview-container .wpnav-security-tips { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 15px; + margin: 15px 0; + text-align: left; +} + +.preview-container .wpnav-security-tips h4 { + margin: 0 0 10px; + font-size: 14px; + color: #2c3e50; + font-weight: 600; +} + +.preview-container .wpnav-security-tips ul { + margin: 0; + padding-left: 15px; + font-size: 12px; + color: #5a6c7d; +} + +.preview-container .wpnav-security-tips li { + margin-bottom: 5px; +} + +.preview-container .wpnav-buttons { + display: flex; + gap: 10px; + margin-top: 20px; + justify-content: center; +} + +.preview-container .wpnav-simple .wpnav-buttons { + gap: 8px; + flex-direction: column; +} + +.preview-container .wpnav-btn { + flex: 1; + padding: 12px 16px; + border-radius: 8px; + border: none; + font-weight: 600; + font-size: 14px; + cursor: default; + transition: all 0.3s ease; + max-width: 140px; +} + +.preview-container .wpnav-simple .wpnav-btn { + padding: 10px 14px; + font-size: 13px; + max-width: none; +} + +.preview-container .wpnav-btn-primary { + background: #667eea; + color: white; +} + +.preview-container .wpnav-btn-secondary { + background: rgba(248, 249, 250, 0.9); + color: #5a6c7d; + border: 2px solid #e9ecef; +} + +.preview-container .wpnav-options { + margin: 20px 0 10px; +} + +.preview-container .wpnav-checkbox { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 12px; + color: #5a6c7d; +} + +.preview-container .wpnav-simple .wpnav-checkbox { + font-size: 11px; +} + +.preview-container .checkmark { + width: 16px; + height: 16px; + border: 2px solid #bdc3c7; + border-radius: 3px; + position: relative; + flex-shrink: 0; +} + +.preview-container .wpnav-countdown { + margin-top: 15px; + font-size: 12px; + color: #7f8c8d; + padding: 10px; + background: rgba(52, 152, 219, 0.1); + border-radius: 6px; + text-align: center; +} + +.preview-container .wpnav-simple .wpnav-countdown { + margin-top: 10px; + padding: 8px; + font-size: 11px; +} + +.preview-container .wpnav-progress-bar { + width: 100%; + height: 4px; + background: rgba(0, 0, 0, 0.1); + border-radius: 2px; + margin-top: 10px; + overflow: hidden; +} + +.preview-container .wpnav-progress-fill { + height: 100%; + background: #667eea; + border-radius: 2px; + width: 60%; + animation: wpnav-progress 5s linear infinite; +} + +@keyframes wpnav-progress { + 0% { width: 100%; } + 100% { width: 0%; } +} + +.preview-container .wpnav-btn-rounded { + border-radius: 8px; +} + +.preview-container .wpnav-btn-square { + border-radius: 0; +} + +.preview-container .wpnav-btn-pill { + border-radius: 25px; +} + +.preview-container.wpnav-color-blue .wpnav-btn-primary { + background: #3498db; +} + +.preview-container.wpnav-color-green .wpnav-btn-primary { + background: #27ae60; +} + +.preview-container.wpnav-color-red .wpnav-btn-primary { + background: #e74c3c; +} + +.wpnav-loading { + opacity: 0.6; + pointer-events: none; +} + +.wpnav-loading::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #0073aa; + border-radius: 50%; + border-top-color: transparent; + animation: wpnav-spin 1s linear infinite; +} + +@keyframes wpnav-spin { + to { + transform: rotate(360deg); + } +} + +@media screen and (max-width: 1024px) { + .wpnav-info-grid, + .wpnav-export-grid { + grid-template-columns: 1fr; + } +} + +@media screen and (max-width: 768px) { + .wrap { + margin: 10px; + } + + .card { + padding: 15px; + margin-top: 15px; + } + + .wpnav-stats-overview { + flex-direction: column; + gap: 10px; + } + + .wpnav-section-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .wpnav-section-actions { + width: 100%; + justify-content: flex-start; + } + + .wpnav-quick-domains { + flex-direction: column; + align-items: stretch; + } + + .wpnav-quick-domains .button-small { + text-align: center; + margin-bottom: 5px; + } + + .preview-container { + transform: scale(0.7); + min-height: 320px; + } + + .wpnav-redirect-preview { + padding: 15px; + } + + .wpnav-activity-stats { + grid-template-columns: 1fr; + gap: 10px; + } + + .wpnav-activity-item { + padding: 12px 8px; + } + + .wpnav-activity-number { + font-size: 20px; + } +} + +@media screen and (max-width: 600px) { + .wpnav-stat-number { + font-size: 18px; + } + + .wp-list-table { + font-size: 12px; + } + + .wp-list-table th, + .wp-list-table td { + padding: 8px 5px; + } + + .column-target-url, + .column-source-page { + width: 35%; + } + + .column-click-time, + .column-ip-address, + .column-https-status { + width: 10%; + } + + .preview-container { + transform: scale(0.6); + min-height: 280px; + } + + .wpnav-activity-stats { + grid-template-columns: 1fr; + gap: 8px; + } + + .wpnav-activity-item { + padding: 10px 6px; + } + + .wpnav-activity-number { + font-size: 18px; + } + + .wpnav-activity-label { + font-size: 10px; + } +} + +.wpnav-activity-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-top: 10px; +} + +.wpnav-activity-item { + text-align: center; + padding: 5px 5px; + background: #ffffff; + border: 1px solid #e2e4e7; + border-radius: 6px; + transition: all 0.2s ease; +} + +.wpnav-activity-item:hover { + background: #ffffff; + border-color: #72aee6; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.wpnav-activity-number { + display: block; + font-size: 20px; + font-weight: 600; + color: #2271b1; + line-height: 1.2; + margin-bottom: 5px; +} + +.wpnav-activity-label { + display: block; + font-size: 11px; + color: #646970; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.wpnav-tabs .nav-tab:focus, +.button:focus, +input:focus, +textarea:focus, +select:focus { + outline: 2px solid #0073aa; + outline-offset: 2px; +} diff --git a/admin.js b/admin.js new file mode 100644 index 0000000..2bc569b --- /dev/null +++ b/admin.js @@ -0,0 +1,494 @@ +jQuery(document).ready(function($) { + $('.wpnav-tab').click(function(e) { + e.preventDefault(); + + $('.wpnav-tab').removeClass('active'); + $(this).addClass('active'); + + var tab = $(this).data('tab'); + $('.wpnav-tab-section').hide(); + $('.wpnav-tab-section[data-section="' + tab + '"]').show(); + + if (history.pushState) { + var newUrl = updateUrlParameter(window.location.href, 'tab', tab); + history.pushState({path: newUrl}, '', newUrl); + } + }); + + $('.wpnav-add-domain').click(function(e) { + e.preventDefault(); + var domain = $(this).data('domain'); + var $textarea = $('textarea[name="whitelist_domains"]'); + var domains = $textarea.val().split('\n').filter(function(d) { return d.trim(); }); + + if (domains.indexOf(domain) >= 0) { + showNotice('wpnav-whitelist-status', wpnav_admin.strings.domain_exists.replace('%s', domain), 'error'); + return; + } + + domains.push(domain); + $textarea.val(domains.join('\n')); + showNotice('wpnav-whitelist-status', wpnav_admin.strings.domain_added.replace('%s', domain), 'success'); + + $(this).addClass('wpnav-loading').prop('disabled', true); + setTimeout(function() { + $(this).removeClass('wpnav-loading').prop('disabled', false); + }.bind(this), 1000); + }); + + $('#wpnav-select-all-domains').click(function(e) { + e.preventDefault(); + var commonDomains = [ + 'google.com', 'facebook.com', 'youtube.com', 'twitter.com', + 'instagram.com', 'linkedin.com', 'baidu.com', 'bing.com' + ]; + var $textarea = $('textarea[name="whitelist_domains"]'); + var currentDomains = $textarea.val().split('\n').filter(function(d) { return d.trim(); }); + var newDomains = [...new Set([...currentDomains, ...commonDomains])]; + + $textarea.val(newDomains.join('\n')); + showNotice('wpnav-whitelist-status', wpnav_admin.strings.common_domains_added, 'success'); + }); + + $('#wpnav-clear-domains').click(function(e) { + e.preventDefault(); + if (confirm(wpnav_admin.strings.confirm_clear)) { + $('textarea[name="whitelist_domains"]').val(''); + showNotice('wpnav-whitelist-status', wpnav_admin.strings.whitelist_cleared, 'success'); + } + }); + + $('#wpnav-export-whitelist').click(function(e) { + e.preventDefault(); + + var whitelist = $('textarea[name="whitelist_domains"]').val(); + var domains = whitelist.split('\n').filter(function(d) { return d.trim(); }); + + if (domains.length === 0) { + showNotice('wpnav-whitelist-status', wpnav_admin.strings.whitelist_empty, 'error'); + return; + } + + var csv = domains.join('\n'); + var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + var link = document.createElement('a'); + + if (link.download !== undefined) { + var url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'wpnav_whitelist_' + getFormattedDate() + '.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showNotice('wpnav-whitelist-status', wpnav_admin.strings.export_success, 'success'); + } else { + showNotice('wpnav-whitelist-status', wpnav_admin.strings.export_unsupported, 'error'); + } + }); + + $('form').submit(function(e) { + var $form = $(this); + var $submitButton = $form.find('input[type="submit"], button[type="submit"]'); + + if ($form.attr('id') === 'sync-settings-form' || $form.attr('id') === 'sync-config-form') { + return true; + } + + var formValid = validateForm($form); + if (!formValid) { + e.preventDefault(); + return false; + } + + if ($submitButton.length) { + var originalValue = $submitButton.val() || $submitButton.text(); + $submitButton.prop('disabled', true).addClass('wpnav-loading'); + + if ($submitButton.is('input')) { + $submitButton.val(wpnav_admin.strings.saving); + } else { + $submitButton.text(wpnav_admin.strings.saving); + } + + setTimeout(function() { + $submitButton.prop('disabled', false).removeClass('wpnav-loading'); + if ($submitButton.is('input')) { + $submitButton.val(originalValue); + } else { + $submitButton.text(originalValue); + } + }, 10000); + } + }); + + if (window.location.search.indexOf('settings-updated=true') !== -1) { + var message = wpnav_admin.strings.settings_saved || 'Settings saved successfully!'; + if (window.location.search.indexOf('imported=') !== -1) { + var match = window.location.search.match(/imported=(\d+)/); + if (match) { + message = wpnav_admin.strings.domains_imported ? + wpnav_admin.strings.domains_imported.replace('%d', match[1]) : + 'Successfully imported ' + match[1] + ' domains.'; + } + } + + $('.wrap h1').after('

' + message + '

'); + + setTimeout(function() { + $('.notice-success').fadeIn(); + }, 100); + } + + if (window.location.search.indexOf('tab=') !== -1) { + setTimeout(function() { + $('.notice-success').fadeIn(); + }, 100); + } + + $('.wp-list-table tbody tr').hover( + function() { + $(this).addClass('wpnav-row-hover'); + }, + function() { + $(this).removeClass('wpnav-row-hover'); + } + ); + + $('#auto_redirect_enabled').change(function() { + if ($(this).is(':checked')) { + $('#redirect_delay_row').show(); + } else { + $('#redirect_delay_row').hide(); + } + }); + + // Enhanced preview functionality + $('#page_title').on('input', function() { + $('#preview-page-title').text($(this).val()); + }); + + $('#page_subtitle').on('input', function() { + var subtitle = $(this).val(); + if (subtitle) { + $('#preview-page-subtitle').text(subtitle).show(); + } else { + $('#preview-page-subtitle').hide(); + } + }); + + $('#url_label').on('input', function() { + $('#preview-url-label').text($(this).val()); + }); + + $('#warning_text').on('input', function() { + $('#preview-warning-text').text($(this).val()); + }); + + $('#countdown_text').on('input', function() { + var text = $(this).val().replace('{seconds}', '5'); + $('#preview-countdown-text').text(text); + }); + + $('#button_text_continue').on('input', function() { + $('#preview-continue-btn').text($(this).val()); + }); + + $('#button_text_back').on('input', function() { + $('#preview-back-btn').text($(this).val()); + }); + + // Show/hide warning message functionality + $('input[name="show_warning_message"], textarea[name="warning_text"]').on('change input', function() { + var showWarning = $('input[name="show_warning_message"]').is(':checked'); + var warningText = $('textarea[name="warning_text"]').val().trim(); + + if (showWarning && warningText !== '') { + $('#preview-warning').show(); + } else { + $('#preview-warning').hide(); + } + }); + + function setupRedirectPagePreview() { + function updatePreview() { + var colorScheme = $('select[name="color_scheme"]').val(); + var template = $('input[name="template"]:checked').val(); + var warningText = $('textarea[name="warning_text"]').val(); + var showWarningMessage = $('input[name="show_warning_message"]').is(':checked'); + var continueText = $('input[name="button_text_continue"]').val(); + var backText = $('input[name="button_text_back"]').val(); + var showLogo = $('input[name="show_logo"]').is(':checked'); + var showUrlFull = $('input[name="show_url_full"]').is(':checked'); + var showSecurityTips = $('input[name="show_security_tips"]').is(':checked'); + var showBackButton = $('input[name="show_back_button"]').is(':checked'); + + console.log('Updating preview - Template:', template, 'Color:', colorScheme); + + var $preview = $('.preview-container'); + $preview.removeClass('wpnav-color-blue wpnav-color-green wpnav-color-red'); + $preview.addClass('wpnav-color-' + colorScheme); + + var $container = $preview.find('.wpnav-container'); + $container.removeClass('wpnav-simple wpnav-minimal wpnav-default wpnav-full'); + $container.addClass('wpnav-' + template); + + $('#preview-warning-text').text(warningText); + $('#preview-continue-btn').text(continueText); + $('#preview-back-btn').text(backText); + + $('.wpnav-site-logo').toggle(showLogo); + $('.wpnav-url-full').toggle(showUrlFull); + $('.wpnav-security-tips').toggle(showSecurityTips); + $('#preview-back-btn').toggle(showBackButton); + $('#preview-warning').toggle(showWarningMessage && warningText.trim() !== ''); + + updateTemplateSpecificElements(template); + } + + function updateTemplateSpecificElements(template) { + var $container = $('.wpnav-container'); + var $header = $('.wpnav-header'); + + switch(template) { + case 'simple': + $container.css({ + 'max-width': '480px', + 'padding': '30px 25px', + 'text-align': 'center' + }); + $header.find('.wpnav-title').css('font-size', '22px'); + break; + + case 'minimal': + $container.css({ + 'max-width': '680px', + 'padding': '30px', + 'text-align': 'center' + }); + $header.find('.wpnav-title').css('font-size', '24px'); + break; + + case 'full': + $container.css({ + 'max-width': '880px', + 'padding': '50px', + 'text-align': 'left' + }); + $header.find('.wpnav-title').css('font-size', '36px'); + break; + + default: // default + $container.css({ + 'max-width': '780px', + 'padding': '40px', + 'text-align': 'center' + }); + $header.find('.wpnav-title').css('font-size', '28px'); + break; + } + } + + $('select[name="color_scheme"]').on('change', updatePreview); + $('input[name="template"]').on('change', updatePreview); + $('textarea[name="warning_text"]').on('input', updatePreview); + $('input[name="show_warning_message"]').on('change', updatePreview); + $('input[name="button_text_continue"]').on('input', updatePreview); + $('input[name="button_text_back"]').on('input', updatePreview); + $('input[name="show_logo"]').on('change', updatePreview); + $('input[name="show_url_full"]').on('change', updatePreview); + $('input[name="show_security_tips"]').on('change', updatePreview); + $('input[name="show_back_button"]').on('change', updatePreview); + + updatePreview(); + } + + if ($('.wpnav-tab[data-tab="redirect_page"]').length) { + setupRedirectPagePreview(); + } + + $('#redirect_delay, #cookie_duration, #stats_retention').on('input', function() { + var $this = $(this); + var val = parseInt($this.val()); + var min = parseInt($this.attr('min')); + var max = parseInt($this.attr('max')); + + if (val < min || val > max) { + $this.addClass('error').css('border-color', '#dc3232'); + } else { + $this.removeClass('error').css('border-color', ''); + } + }); + + var draftTimeout; + $('textarea[name="whitelist_domains"], textarea[name="warning_text"], textarea[name="custom_css"]').on('input', function() { + var $this = $(this); + clearTimeout(draftTimeout); + + draftTimeout = setTimeout(function() { + var draftKey = 'wpnav_draft_' + $this.attr('name'); + try { + sessionStorage.setItem(draftKey, $this.val()); + + if ($this.siblings('.draft-saved').length === 0) { + $this.after('' + wpnav_admin.strings.draft_saved + ''); + setTimeout(function() { + $('.draft-saved').fadeOut(); + }, 2000); + } + } catch (e) { + console.log('Session storage not available'); + } + }, 2000); + }); + + $('textarea[name="whitelist_domains"], textarea[name="warning_text"], textarea[name="custom_css"]').each(function() { + var $this = $(this); + var draftKey = 'wpnav_draft_' + $this.attr('name'); + + try { + var draft = sessionStorage.getItem(draftKey); + if (draft && draft !== $this.val() && draft.trim() !== '') { + var restore = confirm(wpnav_admin.strings.restore_draft); + if (restore) { + $this.val(draft); + if ($this.attr('name') === 'warning_text') { + $('#preview-warning-text').text(draft); + } + } + } + } catch (e) { + console.log('Session storage not available'); + } + }); + + $('form').on('submit', function() { + setTimeout(function() { + if (window.location.search.indexOf('settings-updated=true') !== -1) { + try { + sessionStorage.removeItem('wpnav_draft_whitelist_domains'); + sessionStorage.removeItem('wpnav_draft_warning_text'); + sessionStorage.removeItem('wpnav_draft_custom_css'); + } catch (e) { + console.log('Session storage not available'); + } + } + }, 100); + }); + + function validateForm($form) { + var isValid = true; + var errors = []; + + var redirectDelay = $('#redirect_delay').val(); + if (redirectDelay && (redirectDelay < 1 || redirectDelay > 30)) { + errors.push('Redirect delay must be between 1 and 30 seconds.'); + isValid = false; + } + + var cookieDuration = $('#cookie_duration').val(); + if (cookieDuration && (cookieDuration < 1 || cookieDuration > 365)) { + errors.push('Cookie duration must be between 1 and 365 days.'); + isValid = false; + } + + var statsRetention = $('#stats_retention').val(); + if (statsRetention && (statsRetention < 1 || statsRetention > 365)) { + errors.push('Stats retention must be between 1 and 365 days.'); + isValid = false; + } + + if (!isValid) { + var tabName = $form.closest('.wpnav-tab-section').attr('data-section'); + var statusId = 'wpnav-settings-status'; + + if (tabName === 'whitelist') { + statusId = 'wpnav-whitelist-status'; + } else if (tabName === 'redirect_page') { + statusId = 'wpnav-redirect-status'; + } else if (tabName === 'logs_statistics') { + statusId = 'wpnav-logs-status'; + } + + showNotice(statusId, errors.join(' '), 'error'); + } + + return isValid; + } + + function updateUrlParameter(url, param, paramVal) { + var newAdditionalURL = ""; + var tempArray = url.split("?"); + var baseURL = tempArray[0]; + var additionalURL = tempArray[1]; + var temp = ""; + + if (additionalURL) { + tempArray = additionalURL.split("&"); + for (var i = 0; i < tempArray.length; i++) { + if (tempArray[i].split('=')[0] != param) { + newAdditionalURL += temp + tempArray[i]; + temp = "&"; + } + } + } + + var rows_txt = temp + "" + param + "=" + paramVal; + return baseURL + "?" + newAdditionalURL + rows_txt; + } + + function showNotice(elementId, message, type) { + var $notice = $('#' + elementId); + + if ($notice.length === 0) { + $notice = $(''); + $('.card h2').first().after($notice); + } + + $notice.removeClass('wpnav-notice-success wpnav-notice-error') + .addClass('wpnav-notice-' + type) + .text(message) + .show() + .delay(4000) + .fadeOut(); + } + + function getFormattedDate() { + var now = new Date(); + var year = now.getFullYear(); + var month = ('0' + (now.getMonth() + 1)).slice(-2); + var day = ('0' + now.getDate()).slice(-2); + return year + month + day; + } + + function initColorSchemePreview() { + $('select[name="color_scheme"]').on('change', function() { + var scheme = $(this).val(); + var $preview = $('.wpnav-redirect-preview'); + $preview.removeClass('wpnav-color-blue wpnav-color-green wpnav-color-red wpnav-color-light'); + $preview.addClass('wpnav-color-' + scheme); + }); + } + + if ($('.wpnav-redirect-preview').length) { + initColorSchemePreview(); + } + + console.log('WPNav Links Admin initialized'); + + var activeTab = $('.wpnav-tab.active').data('tab') || 'basic_settings'; + if (activeTab) { + $('.wpnav-tab-section').hide(); + $('.wpnav-tab-section[data-section="' + activeTab + '"]').show(); + } + + var urlParams = new URLSearchParams(window.location.search); + var tabFromUrl = urlParams.get('tab'); + if (tabFromUrl) { + $('.wpnav-tab').removeClass('active'); + $('.wpnav-tab[data-tab="' + tabFromUrl + '"]').addClass('active'); + $('.wpnav-tab-section').hide(); + $('.wpnav-tab-section[data-section="' + tabFromUrl + '"]').show(); + } +}); diff --git a/assets/i-like-food.svg b/assets/i-like-food.svg new file mode 100644 index 0000000..212d82f --- /dev/null +++ b/assets/i-like-food.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/leaf.svg b/assets/leaf.svg new file mode 100644 index 0000000..a4565a6 --- /dev/null +++ b/assets/leaf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/class-admin.php b/class-admin.php new file mode 100644 index 0000000..b59fd3f --- /dev/null +++ b/class-admin.php @@ -0,0 +1,131 @@ +options = $options; + $this->plugin = new WPNAV_Links(); + + add_action('admin_menu', array($this, 'add_admin_menu')); + add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); + add_filter('plugin_action_links_wpnav-links/wpnav-links.php', array($this, 'add_settings_link')); + add_action('wp_ajax_wpnav_export_stats', array($this, 'ajax_export_stats')); + } + + public function add_admin_menu() { + add_submenu_page( + 'tools.php', + __('External Links Redirect', 'wpnav-links'), + __('External Links', 'wpnav-links'), + 'manage_options', + 'wpnav-links', + array($this, 'display_admin_page') + ); + } + + public function enqueue_admin_scripts($hook) { + if ($hook !== 'tools_page_wpnav-links') { + return; + } + + wp_enqueue_style( + 'wpnav-admin-style', + WPNAV_LINKS_PLUGIN_URL . 'admin.css', + array(), + WPNAV_LINKS_VERSION + ); + + wp_enqueue_script( + 'wpnav-admin-script', + WPNAV_LINKS_PLUGIN_URL . 'admin.js', + array('jquery'), + WPNAV_LINKS_VERSION, + true + ); + + wp_localize_script( + 'wpnav-admin-script', + 'wpnav_admin', + array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wpnav_admin_nonce'), + 'strings' => array( + 'domain_exists' => __('Domain %s is already in the whitelist.', 'wpnav-links'), + 'domain_added' => __('Added %s to whitelist.', 'wpnav-links'), + 'common_domains_added' => __('Added common domains to whitelist.', 'wpnav-links'), + 'whitelist_cleared' => __('Whitelist cleared.', 'wpnav-links'), + 'confirm_clear' => __('Are you sure you want to clear all domains from the whitelist?', 'wpnav-links'), + 'whitelist_empty' => __('Whitelist is empty, cannot export.', 'wpnav-links'), + 'export_success' => __('Whitelist exported successfully.', 'wpnav-links'), + 'export_unsupported' => __('Export not supported in this browser.', 'wpnav-links'), + 'saving' => __('Saving...', 'wpnav-links'), + 'draft_saved' => __('Draft saved', 'wpnav-links'), + 'restore_draft' => __('A draft was found for this field. Would you like to restore it?', 'wpnav-links'), + 'settings_saved' => __('Settings saved successfully!', 'wpnav-links'), + 'domains_imported' => __('Successfully imported %d domains.', 'wpnav-links') + ) + ) + ); + } + + public function add_settings_link($links) { + $settings_link = '' . __('Settings', 'wpnav-links') . ''; + array_unshift($links, $settings_link); + return $links; + } + + public function display_admin_page() { + include WPNAV_LINKS_PLUGIN_DIR . 'admin-page.php'; + } + + public function ajax_export_stats() { + if (!check_ajax_referer('wpnav_admin_nonce', 'nonce', false)) { + wp_send_json_error(array('message' => __('Security check failed', 'wpnav-links'))); + } + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Insufficient permissions', 'wpnav-links'))); + } + + $start_date = isset($_POST['start_date']) ? sanitize_text_field($_POST['start_date']) : ''; + $end_date = isset($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : ''; + + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=wpnav_stats_' . date('Y-m-d') . '.csv'); + + $output = fopen('php://output', 'w'); + + fputcsv($output, array( + __('Target URL', 'wpnav-links'), + __('Source Page', 'wpnav-links'), + __('Click Time', 'wpnav-links'), + __('IP Address', 'wpnav-links'), + __('User Agent', 'wpnav-links') + )); + + $stats = $this->plugin->get_stats(array( + 'start_date' => $start_date, + 'end_date' => $end_date, + 'limit' => 5000 + )); + + foreach ($stats as $row) { + fputcsv($output, array( + $row->target_url, + $row->source_page, + $row->click_time, + $row->user_ip, + isset($row->user_agent) ? $row->user_agent : '' + )); + } + + fclose($output); + wp_die(); + } +} diff --git a/class-public.php b/class-public.php new file mode 100644 index 0000000..35e3a4a --- /dev/null +++ b/class-public.php @@ -0,0 +1,323 @@ +options = $options; + $this->security = $security; + + if (isset($options['enabled']) && $options['enabled']) { + add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); + $this->setup_content_filters(); + add_action('wp_footer', array($this, 'add_footer_script')); + } + } + + public function enqueue_scripts() { + wp_enqueue_script( + 'wpnav-redirect-script', + WPNAV_LINKS_PLUGIN_URL . 'redirect.js', + array('jquery'), + WPNAV_LINKS_VERSION, + true + ); + + wp_localize_script( + 'wpnav-redirect-script', + 'wpnav_params', + array( + 'home_url' => home_url(), + 'site_domain' => parse_url(home_url(), PHP_URL_HOST), + 'exclude_class' => $this->options['exclude_css_class'], + 'open_new_tab' => $this->options['open_in_new_tab'], + 'redirect_method' => isset($this->options['url_format']) ? $this->options['url_format'] : 'query', + 'url_encoding' => 'base64', + 'permalink_structure' => get_option('permalink_structure') ? true : false, + 'whitelist_domains' => $this->get_whitelist_domains(), + 'strings' => array( + 'external_link' => __('External link', 'wpnav-links'), + 'opens_new_window' => __('(opens in a new window)', 'wpnav-links') + ) + ) + ); + } + + private function get_whitelist_domains() { + $whitelist = array(); + if (!empty($this->options['whitelist_domains'])) { + $whitelist = explode("\n", $this->options['whitelist_domains']); + $whitelist = array_map('trim', $whitelist); + $whitelist = array_filter($whitelist); + } + return $whitelist; + } + + private function setup_content_filters() { + if (isset($this->options['intercept_content']) && $this->options['intercept_content']) { + add_filter('the_content', array($this, 'process_content')); + } + + if (isset($this->options['intercept_comments']) && $this->options['intercept_comments']) { + add_filter('comment_text', array($this, 'process_content')); + } + + if (isset($this->options['intercept_widgets']) && $this->options['intercept_widgets']) { + add_filter('widget_text', array($this, 'process_content')); + add_filter('widget_custom_html_content', array($this, 'process_content')); + } + } + + public function process_content($content) { + if (empty($content)) { + return $content; + } + + $dom = new DOMDocument(); + libxml_use_internal_errors(true); + + $dom->loadHTML('
' . $content . '
', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + + $links = $dom->getElementsByTagName('a'); + $modified = false; + + $links_to_process = array(); + foreach ($links as $link) { + $links_to_process[] = $link; + } + + foreach ($links_to_process as $link) { + $href = $link->getAttribute('href'); + if (empty($href)) { + continue; + } + + if (!empty($this->options['exclude_css_class'])) { + $class = $link->getAttribute('class'); + if (strpos($class, $this->options['exclude_css_class']) !== false) { + continue; + } + } + + if ($this->is_external_link($href)) { + if ($this->security->check_whitelist_domain($href)) { + continue; + } + + $rel = $link->getAttribute('rel'); + $rel_values = empty($rel) ? array() : explode(' ', $rel); + $rel_values = array_merge($rel_values, array('nofollow', 'noopener', 'noreferrer')); + $rel_values = array_unique($rel_values); + $link->setAttribute('rel', implode(' ', $rel_values)); + + if ($this->options['open_in_new_tab']) { + $link->setAttribute('target', '_blank'); + + // Add screen reader text for accessibility + $title = $link->getAttribute('title'); + if (empty($title)) { + $link->setAttribute('title', __('External link (opens in a new window)', 'wpnav-links')); + } + } + + $redirect_url = $this->security->get_redirect_url($href); + $link->setAttribute('href', $redirect_url); + $link->setAttribute('data-wpnav-external', '1'); + + $modified = true; + } + } + + if (!$modified) { + return $content; + } + + $new_html = $dom->saveHTML($dom->documentElement); + $new_html = preg_replace('/<\?xml encoding="UTF-8"\?>/', '', $new_html); + $new_html = preg_replace('/<\/?div>/', '', $new_html); + + return $new_html; + } + + private function is_external_link($url) { + if (strpos($url, '://') === false && strpos($url, '//') !== 0) { + return false; + } + + $parsed_url = parse_url($url); + + if (empty($parsed_url) || !isset($parsed_url['host'])) { + return false; + } + + $site_domain = parse_url(home_url(), PHP_URL_HOST); + $is_external = ($parsed_url['host'] !== $site_domain); + + if ($is_external) { + if (!empty($this->options['auto_whitelist'])) { + if (!empty($this->options['auto_whitelist']['same_root'])) { + $site_root = $this->get_root_domain($site_domain); + $url_root = $this->get_root_domain($parsed_url['host']); + + if ($site_root === $url_root) { + $is_external = false; + } + } + + if ($is_external && !empty($this->options['auto_whitelist']['search_engines'])) { + $search_engines = array('google.', 'bing.', 'yahoo.', 'baidu.', 'yandex.', 'duckduckgo.'); + foreach ($search_engines as $engine) { + if (strpos($parsed_url['host'], $engine) !== false) { + $is_external = false; + break; + } + } + } + } + } + + return $is_external; + } + + private function get_root_domain($domain) { + $domain_parts = explode('.', $domain); + if (count($domain_parts) > 2) { + $tld_part = array_slice($domain_parts, -2, 2); + if (in_array($tld_part[0], array('co', 'com', 'net', 'org', 'gov', 'edu'))) { + return implode('.', array_slice($domain_parts, -3, 3)); + } + return implode('.', array_slice($domain_parts, -2, 2)); + } + return $domain; + } + + public function add_footer_script() { + ?> + + table_name = $wpdb->prefix . WPNAV_LINKS_TABLE; + } + + public function init() { + add_action('plugins_loaded', array($this, 'load_textdomain')); + + $this->options = get_option('wpnav_links_options'); + + if (is_admin()) { + new WPNAV_Admin($this->options); + } + + new WPNAV_Public($this->options, $this); + + add_action('init', array($this, 'add_rewrite_rules')); + add_filter('query_vars', array($this, 'add_query_vars')); + add_action('template_redirect', array($this, 'handle_external_redirect')); + add_action('admin_bar_menu', array($this, 'add_admin_bar_menu'), 100); + } + + public function load_textdomain() { + load_plugin_textdomain('wpnav-links', false, dirname(plugin_basename(__FILE__)) . '/languages/'); + } + + public function add_rewrite_rules() { + $url_format = isset($this->options['url_format']) ? $this->options['url_format'] : 'query'; + + if ($url_format === 'path') { + add_rewrite_rule('^go/([^/]+)/?$', 'index.php?wpnav_redirect=1&url_param=$matches[1]', 'top'); + } else { + add_rewrite_rule('^go/?$', 'index.php?wpnav_redirect=1', 'top'); + } + } + + public function add_query_vars($vars) { + $vars[] = 'wpnav_redirect'; + $vars[] = 'target'; + $vars[] = 'url_param'; + $vars[] = 'ref'; + $vars[] = 'url'; + return $vars; + } + + public function handle_external_redirect() { + global $wp_query; + + if (!isset($wp_query->query_vars['wpnav_redirect']) || $wp_query->query_vars['wpnav_redirect'] != 1) { + return; + } + + $url = $this->get_redirect_url_from_request(); + + if (empty($url)) { + $this->handle_redirect_error(__('Invalid or missing URL parameter', 'wpnav-links')); + return; + } + + if (!$this->validate_url($url)) { + $this->handle_redirect_error(__('Invalid URL format', 'wpnav-links')); + return; + } + + $ref = isset($wp_query->query_vars['ref']) ? esc_url_raw($wp_query->query_vars['ref']) : wp_get_referer(); + + set_query_var('target_url', $url); + set_query_var('source_url', $ref); + + if ($this->should_skip_redirect($url)) { + wp_redirect($url, 302); + exit; + } + + $this->record_redirect($url, $ref); + $this->load_redirect_template($url, $ref); + exit; + } + + private function get_redirect_url_from_request() { + global $wp_query; + + $url_format = isset($this->options['url_format']) ? $this->options['url_format'] : 'query'; + $url = ''; + + if ($url_format === 'path') { + if (isset($wp_query->query_vars['url_param'])) { + $url_param = $wp_query->query_vars['url_param']; + $decoded = base64_decode($url_param); + if ($decoded !== false) { + $url = $decoded; + } + } + } elseif ($url_format === 'target') { + if (isset($wp_query->query_vars['target'])) { + $url = urldecode($wp_query->query_vars['target']); + } elseif (isset($_GET['target'])) { + $url = urldecode(sanitize_text_field($_GET['target'])); + } + } else { + if (isset($wp_query->query_vars['target'])) { + $url = urldecode($wp_query->query_vars['target']); + } elseif (isset($_GET['target'])) { + $url = urldecode(sanitize_text_field($_GET['target'])); + } elseif (isset($wp_query->query_vars['url'])) { + $url_param = $wp_query->query_vars['url']; + $decoded = base64_decode($url_param); + if ($decoded !== false) { + $url = $decoded; + } else { + $url = urldecode($url_param); + } + } elseif (isset($_GET['url'])) { + $url_param = sanitize_text_field($_GET['url']); + $decoded = base64_decode($url_param); + if ($decoded !== false) { + $url = $decoded; + } else { + $url = urldecode($url_param); + } + } + } + + if (!empty($url) && strpos($url, 'http') !== 0) { + $url = 'http://' . $url; + } + + return $url; + } + + private function handle_redirect_error($message) { + if (WP_DEBUG) { + error_log('WPNAV Redirect Error: ' . $message); + } + + wp_die( + esc_html__('Redirect Error: ', 'wpnav-links') . esc_html($message), + esc_html__('External Link Redirect Error', 'wpnav-links'), + array( + 'response' => 400, + 'back_link' => true + ) + ); + } + + private function should_skip_redirect($url) { + if (!empty($this->options['admin_exempt']) && current_user_can('manage_options')) { + return true; + } + + if ($this->check_whitelist_domain($url)) { + return true; + } + + if (isset($_COOKIE['wpnav_noredirect']) && $_COOKIE['wpnav_noredirect'] == '1') { + return true; + } + + return false; + } + + public function check_whitelist_domain($url) { + $domain = parse_url($url, PHP_URL_HOST); + if (empty($domain)) { + return false; + } + + $whitelist = array(); + if (!empty($this->options['whitelist_domains'])) { + $whitelist = explode("\n", $this->options['whitelist_domains']); + $whitelist = array_map('trim', $whitelist); + } + + if (in_array($domain, $whitelist)) { + return true; + } + + foreach ($whitelist as $pattern) { + if (strpos($pattern, '*') !== false) { + $pattern = str_replace('*', '(.*)', preg_quote($pattern, '/')); + if (preg_match('/^' . $pattern . '$/i', $domain)) { + return true; + } + } + } + + if (!empty($this->options['auto_whitelist']['same_root'])) { + $site_domain = parse_url(home_url(), PHP_URL_HOST); + $site_root = $this->get_root_domain($site_domain); + $url_root = $this->get_root_domain($domain); + + if ($site_root == $url_root) { + return true; + } + } + + if (!empty($this->options['auto_whitelist']['search_engines'])) { + $search_engines = array('google.', 'bing.', 'yahoo.', 'baidu.', 'yandex.', 'duckduckgo.'); + foreach ($search_engines as $engine) { + if (strpos($domain, $engine) !== false) { + return true; + } + } + } + + return false; + } + + private function get_root_domain($domain) { + $domain_parts = explode('.', $domain); + if (count($domain_parts) > 2) { + $tld_part = array_slice($domain_parts, -2, 2); + if (in_array($tld_part[0], array('co', 'com', 'net', 'org', 'gov', 'edu'))) { + return implode('.', array_slice($domain_parts, -3, 3)); + } + return implode('.', array_slice($domain_parts, -2, 2)); + } + return $domain; + } + + private function load_redirect_template($url, $ref) { + wp_enqueue_style( + 'wpnav-redirect-style', + WPNAV_LINKS_PLUGIN_URL . 'frontend.css', + array(), + WPNAV_LINKS_VERSION + ); + + $template_locations = array( + get_stylesheet_directory() . '/wpnav-redirect-template.php', + get_template_directory() . '/wpnav-redirect-template.php', + WPNAV_LINKS_PLUGIN_DIR . 'redirect-template.php' + ); + + foreach ($template_locations as $template) { + if (file_exists($template)) { + include $template; + return; + } + } + + include WPNAV_LINKS_PLUGIN_DIR . 'redirect-template.php'; + } + + public function get_redirect_url($url) { + $url_format = isset($this->options['url_format']) ? $this->options['url_format'] : 'query'; + + if ($url_format === 'path') { + $encoded_url = base64_encode($url); + if (get_option('permalink_structure')) { + return home_url('/go/' . $encoded_url); + } else { + return home_url('?wpnav_redirect=1&url_param=' . urlencode($encoded_url)); + } + } elseif ($url_format === 'target') { + return home_url('?wpnav_redirect=1&target=' . urlencode($url)); + } else { + return home_url('?wpnav_redirect=1&url=' . urlencode(base64_encode($url))); + } + } + + public function add_admin_bar_menu($wp_admin_bar) { + if (!current_user_can('manage_options')) { + return; + } + + $wp_admin_bar->add_node(array( + 'id' => 'wpnav-links', + 'title' => esc_html__('External Links', 'wpnav-links'), + 'href' => admin_url('tools.php?page=wpnav-links'), + )); + } + + public function create_tables() { + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $this->table_name ( + id BIGINT UNSIGNED AUTO_INCREMENT, + target_url VARCHAR(512) NOT NULL, + source_page VARCHAR(255) NOT NULL, + click_time DATETIME DEFAULT CURRENT_TIMESTAMP, + user_ip VARCHAR(45), + user_agent TEXT, + is_https TINYINT(1) DEFAULT 0, + PRIMARY KEY (id), + INDEX (target_url(191)), + INDEX (click_time) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + + update_option('wpnav_links_db_version', WPNAV_LINKS_DB_VERSION); + } + + public function record_redirect($target_url, $source_page) { + global $wpdb; + + $is_https = strpos($target_url, 'https://') === 0 ? 1 : 0; + $ip = $this->get_client_ip(); + $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + + $data = array( + 'target_url' => esc_url_raw($target_url), + 'source_page' => esc_url_raw($source_page), + 'click_time' => current_time('mysql'), + 'user_ip' => sanitize_text_field($ip), + 'user_agent' => sanitize_text_field($user_agent), + 'is_https' => $is_https + ); + + $result = $wpdb->insert($this->table_name, $data); + return $result ? $wpdb->insert_id : false; + } + + private function get_client_ip() { + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } else { + $ip = $_SERVER['REMOTE_ADDR']; + } + return $ip; + } + + public function get_stats($args = array()) { + global $wpdb; + + $defaults = array( + 'start_date' => date('Y-m-d', strtotime('-30 days')), + 'end_date' => date('Y-m-d'), + 'limit' => 10, + 'offset' => 0, + 'order_by' => 'click_time', + 'order' => 'DESC' + ); + + $args = wp_parse_args($args, $defaults); + + $start_date = sanitize_text_field($args['start_date']); + $end_date = sanitize_text_field($args['end_date']); + $limit = intval($args['limit']); + $offset = intval($args['offset']); + $order_by = sanitize_sql_orderby($args['order_by']) ?: 'click_time'; + $order = strtoupper($args['order']) == 'ASC' ? 'ASC' : 'DESC'; + + $query = $wpdb->prepare( + "SELECT * FROM {$this->table_name} + WHERE click_time BETWEEN %s AND %s + ORDER BY {$order_by} {$order} + LIMIT %d OFFSET %d", + "{$start_date} 00:00:00", + "{$end_date} 23:59:59", + $limit, + $offset + ); + + return $wpdb->get_results($query); + } + + public function get_total_count($start_date = '', $end_date = '') { + global $wpdb; + + if (empty($start_date) || empty($end_date)) { + return $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); + } + + $query = $wpdb->prepare( + "SELECT COUNT(*) FROM {$this->table_name} + WHERE click_time BETWEEN %s AND %s", + "{$start_date} 00:00:00", + "{$end_date} 23:59:59" + ); + + return $wpdb->get_var($query); + } + + public function get_top_urls($limit = 10) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT target_url, COUNT(*) as count + FROM {$this->table_name} + GROUP BY target_url + ORDER BY count DESC + LIMIT %d", + $limit + ); + + return $wpdb->get_results($query); + } + + public function get_https_stats() { + global $wpdb; + + $total = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); + + if ($total == 0) { + return false; + } + + $https_count = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name} WHERE is_https = 1"); + $http_count = $total - $https_count; + + return array( + 'total' => $total, + 'https_count' => $https_count, + 'http_count' => $http_count, + 'https_percentage' => ($https_count / $total) * 100, + 'http_percentage' => ($http_count / $total) * 100 + ); + } + + public function get_unique_domains_count() { + global $wpdb; + + $query = "SELECT COUNT(DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(target_url, '/', 3), '://', -1)) as unique_domains FROM {$this->table_name}"; + + $result = $wpdb->get_var($query); + + return $result ? intval($result) : 0; + } + + public function cleanup_old_data($days = 90) { + global $wpdb; + + $days = max(1, intval($days)); + + $query = $wpdb->prepare( + "DELETE FROM {$this->table_name} + WHERE click_time < DATE_SUB(NOW(), INTERVAL %d DAY)", + $days + ); + + return $wpdb->query($query); + } + + public function validate_url($url) { + if (empty($url) || !wp_http_validate_url($url)) { + return false; + } + + $parsed_url = parse_url($url); + if (empty($parsed_url['host'])) { + return false; + } + + if ($this->contains_script_injection($url)) { + return false; + } + + return true; + } + + private function contains_script_injection($url) { + $patterns = array( + '/javascript:/i', + '/data:/i', + '/vbscript:/i', + '/ + + + + diff --git a/redirect.js b/redirect.js new file mode 100644 index 0000000..58a85dc --- /dev/null +++ b/redirect.js @@ -0,0 +1,343 @@ +(function($) { + 'use strict'; + + var wpnavRedirect = { + init: function() { + this.processExistingLinks(); + this.setupMutationObserver(); + this.setupEventHandlers(); + this.optimizeForMobile(); + }, + + processExistingLinks: function() { + var self = this; + $('a:not([data-wpnav-processed])').each(function() { + self.processLink($(this)); + }); + }, + + processLink: function($link) { + var href = $link.attr('href'); + + if (!href || this.shouldSkipLink(href, $link)) { + $link.attr('data-wpnav-processed', '1'); + return; + } + + if (this.isExternalLink(href) && !this.isWhitelistedDomain(href)) { + this.processExternalLink($link, href); + } + + $link.attr('data-wpnav-processed', '1'); + }, + + shouldSkipLink: function(href, $link) { + if (href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) { + return true; + } + + if (wpnav_params.exclude_class && $link.hasClass(wpnav_params.exclude_class)) { + return true; + } + + if ($link.attr('data-wpnav-external') || $link.attr('data-wpnav-processed')) { + return true; + } + + return false; + }, + + isExternalLink: function(url) { + if (!url.match(/^(https?:)?\/\//i)) { + return false; + } + + try { + var parser = document.createElement('a'); + parser.href = url; + var linkDomain = parser.hostname.toLowerCase(); + var siteDomain = wpnav_params.site_domain.toLowerCase(); + + return linkDomain !== siteDomain; + } catch (e) { + return false; + } + }, + + isWhitelistedDomain: function(url) { + if (!wpnav_params.whitelist_domains || wpnav_params.whitelist_domains.length === 0) { + return false; + } + + try { + var parser = document.createElement('a'); + parser.href = url; + var hostname = parser.hostname.toLowerCase(); + + for (var i = 0; i < wpnav_params.whitelist_domains.length; i++) { + var whitelistDomain = wpnav_params.whitelist_domains[i].toLowerCase(); + if (hostname === whitelistDomain) { + return true; + } + if (whitelistDomain.indexOf('*') !== -1) { + var pattern = whitelistDomain.replace(/\*/g, '.*'); + var regex = new RegExp('^' + pattern + '$', 'i'); + if (regex.test(hostname)) { + return true; + } + } + } + } catch (e) { + return false; + } + + return false; + }, + + processExternalLink: function($link, href) { + var rel = $link.attr('rel') || ''; + var relValues = rel ? rel.split(' ') : []; + + ['nofollow', 'noopener', 'noreferrer'].forEach(function(value) { + if (relValues.indexOf(value) === -1) { + relValues.push(value); + } + }); + + $link.attr('rel', relValues.join(' ')); + + if (wpnav_params.open_new_tab) { + $link.attr('target', '_blank'); + } + + var redirectUrl = this.getRedirectUrl(href); + $link.attr('href', redirectUrl); + $link.attr('data-wpnav-external', '1'); + + this.addExternalIndicator($link); + }, + + addExternalIndicator: function($link) { + if (!$link.find('.wpnav-external-icon').length) { + var iconHtml = ''; + $link.append(iconHtml); + } + }, + + getRedirectUrl: function(url) { + var currentPage = encodeURIComponent(window.location.href); + var baseParams = 'wpnav_redirect=1&ref=' + currentPage; + + switch(wpnav_params.redirect_method) { + case 'path': + var encodedUrl = this.encodeUrl(url); + if (wpnav_params.permalink_structure) { + return wpnav_params.home_url + '/go/' + encodedUrl; + } else { + return wpnav_params.home_url + '?' + baseParams + '&url_param=' + encodeURIComponent(encodedUrl); + } + + case 'target': + return wpnav_params.home_url + '?' + baseParams + '&target=' + encodeURIComponent(url); + + default: + var encodedUrl = this.encodeUrl(url); + return wpnav_params.home_url + '?' + baseParams + '&url=' + encodeURIComponent(encodedUrl); + } + }, + + encodeUrl: function(url) { + if (wpnav_params.redirect_method === 'target') { + return url; + } + + switch(wpnav_params.url_encoding) { + case 'base64': + try { + return btoa(encodeURIComponent(url)).replace(/[+/=]/g, function(match) { + return {'+': '-', '/': '_', '=': ''}[match]; + }); + } catch (e) { + return encodeURIComponent(url); + } + + case 'urlencode': + return encodeURIComponent(url); + + case 'none': + return url; + + default: + try { + return btoa(encodeURIComponent(url)).replace(/[+/=]/g, function(match) { + return {'+': '-', '/': '_', '=': ''}[match]; + }); + } catch (e) { + return encodeURIComponent(url); + } + } + }, + + setupMutationObserver: function() { + if (!window.MutationObserver) { + return; + } + + var self = this; + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.addedNodes && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1) { + if (node.nodeName === 'A' && !node.hasAttribute('data-wpnav-processed')) { + self.processLink($(node)); + } + + var $links = $(node).find('a:not([data-wpnav-processed])'); + $links.each(function() { + self.processLink($(this)); + }); + } + }); + } + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }, + + setupEventHandlers: function() { + var self = this; + + $(document).ajaxComplete(function() { + setTimeout(function() { + self.processExistingLinks(); + }, 100); + }); + + $(document).on('click', 'a[data-wpnav-external]', function(e) { + var $link = $(this); + var href = $link.attr('href'); + + if (typeof gtag !== 'undefined') { + gtag('event', 'click', { + event_category: 'external_link', + event_label: href, + transport_type: 'beacon' + }); + } + + $(document).trigger('wpnav.external_link_click', { + link: $link, + url: href, + target: $link.attr('target') + }); + }); + + $(document).keydown(function(e) { + if (e.keyCode === 27 && window.parent !== window) { + if (typeof window.parent.postMessage === 'function') { + window.parent.postMessage('wpnav_close', '*'); + } + } + }); + + $(document).on('mouseenter', 'a[data-wpnav-external]', function() { + $(this).find('.wpnav-external-icon').css('opacity', '1'); + }); + + $(document).on('mouseleave', 'a[data-wpnav-external]', function() { + $(this).find('.wpnav-external-icon').css('opacity', '0.6'); + }); + }, + + optimizeForMobile: function() { + if (!('ontouchstart' in window)) { + return; + } + + $(document).on('touchstart', 'a[data-wpnav-external]', function(e) { + var $this = $(this); + $this.addClass('wpnav-touch-active'); + + setTimeout(function() { + $this.removeClass('wpnav-touch-active'); + }, 150); + }); + + var style = document.createElement('style'); + style.textContent = ` + a[data-wpnav-external].wpnav-touch-active { + opacity: 0.7; + transform: scale(0.98); + transition: all 0.15s ease; + } + + @media (hover: none) and (pointer: coarse) { + a[data-wpnav-external]:hover { + opacity: 1; + transform: none; + } + + .wpnav-external-icon { + opacity: 0.8 !important; + } + } + `; + document.head.appendChild(style); + }, + + refresh: function() { + this.processExistingLinks(); + } + }; + + $(document).ready(function() { + wpnavRedirect.init(); + }); + + window.wpnavRedirect = wpnavRedirect; + + if (typeof addComment !== 'undefined') { + var originalAddComment = addComment.moveForm; + addComment.moveForm = function() { + var result = originalAddComment.apply(this, arguments); + setTimeout(function() { + wpnavRedirect.processExistingLinks(); + }, 100); + return result; + }; + } + + if (typeof wp !== 'undefined' && wp.data) { + wp.data.subscribe(function() { + setTimeout(function() { + wpnavRedirect.processExistingLinks(); + }, 500); + }); + } + + $(document).on('wpcf7mailsent wpcf7invalid wpcf7spam wpcf7mailfailed', function() { + setTimeout(function() { + wpnavRedirect.processExistingLinks(); + }, 100); + }); + + $(document.body).on('updated_wc_div updated_cart_totals', function() { + setTimeout(function() { + wpnavRedirect.processExistingLinks(); + }, 100); + }); + + if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + setTimeout(function() { + wpnavRedirect.processExistingLinks(); + }, 1000); + }); + } + +})(jQuery); diff --git a/wpnav-links.php b/wpnav-links.php new file mode 100644 index 0000000..f6b6758 --- /dev/null +++ b/wpnav-links.php @@ -0,0 +1,533 @@ +create_tables(); + + $default_options = array( + 'enabled' => true, + 'auto_redirect_enabled' => true, + 'redirect_delay' => 5, + 'open_in_new_tab' => true, + 'url_format' => 'query', + 'auto_whitelist' => array( + 'same_root' => true, + 'search_engines' => true + ), + 'whitelist_domains' => "google.com\nbaidu.com\nbing.com\nyoutube.com\nfacebook.com\ntwitter.com", + 'intercept_content' => true, + 'intercept_comments' => true, + 'intercept_widgets' => true, + 'exclude_css_class' => 'no-redirect', + 'template' => 'default', + 'color_scheme' => 'blue', + 'page_title' => 'External Link Warning', + 'page_subtitle' => '', + 'url_label' => 'You are about to visit:', + 'warning_text' => 'You are about to leave this site and visit an external website. We are not responsible for the content of external websites.', + 'show_warning_message' => true, + 'show_logo' => false, + 'show_url_full' => false, + 'show_security_info' => true, + 'show_security_tips' => false, + 'show_back_button' => true, + 'custom_css' => '', + 'button_text_continue' => 'Continue', + 'button_text_back' => 'Back', + 'button_style' => 'rounded', + 'countdown_text' => 'Auto redirect in {seconds} seconds', + 'show_progress_bar' => false, + 'admin_exempt' => true, + 'cookie_duration' => 30, + 'stats_retention' => 90 + ); + + $existing_options = get_option('wpnav_links_options', array()); + $merged_options = array_merge($default_options, $existing_options); + update_option('wpnav_links_options', $merged_options); + + if (!wp_next_scheduled('wpnav_cleanup_old_data')) { + wp_schedule_event(time(), 'daily', 'wpnav_cleanup_old_data'); + } + + flush_rewrite_rules(); + add_option('wpnav_links_activation_redirect', true); +} +register_activation_hook(__FILE__, 'wpnav_activate_plugin'); + +function wpnav_deactivate_plugin() { + wp_clear_scheduled_hook('wpnav_cleanup_old_data'); + flush_rewrite_rules(); +} +register_deactivation_hook(__FILE__, 'wpnav_deactivate_plugin'); + +function wpnav_uninstall_plugin() { + delete_option('wpnav_links_options'); + delete_option('wpnav_links_db_version'); + delete_option('wpnav_links_activation_redirect'); + + global $wpdb; + $table_name = $wpdb->prefix . WPNAV_LINKS_TABLE; + $wpdb->query("DROP TABLE IF EXISTS $table_name"); + + wp_clear_scheduled_hook('wpnav_cleanup_old_data'); +} +register_uninstall_hook(__FILE__, 'wpnav_uninstall_plugin'); + +add_action('wpnav_cleanup_old_data', 'wpnav_clean_old_statistics'); +function wpnav_clean_old_statistics() { + $options = get_option('wpnav_links_options'); + $retention_days = isset($options['stats_retention']) ? intval($options['stats_retention']) : 90; + + $plugin = new WPNAV_Links(); + $deleted_count = $plugin->cleanup_old_data($retention_days); + + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log("WPNav Links: Cleaned up {$deleted_count} old records"); + } +} + +function wpnav_run_plugin() { + $plugin = new WPNAV_Links(); + $plugin->init(); +} + +add_action('plugins_loaded', 'wpnav_run_plugin'); + +add_action('admin_init', 'wpnav_activation_redirect'); +function wpnav_activation_redirect() { + if (get_option('wpnav_links_activation_redirect', false)) { + delete_option('wpnav_links_activation_redirect'); + if (!isset($_GET['activate-multi'])) { + wp_redirect(admin_url('tools.php?page=wpnav-links&tab=basic_settings')); + exit; + } + } +} + +add_action('wp_dashboard_setup', 'wpnav_add_dashboard_widget'); +function wpnav_add_dashboard_widget() { + if (current_user_can('manage_options')) { + wp_add_dashboard_widget( + 'wpnav_links_stats', + __('External Links Monitor', 'wpnav-links'), + 'wpnav_dashboard_widget_content', + null, + null, + 'normal', + 'high' + ); + } +} + +function wpnav_dashboard_widget_content() { + $plugin = new WPNAV_Links(); + $options = get_option('wpnav_links_options', array()); + + $total_redirects = $plugin->get_total_count(); + $today_redirects = $plugin->get_total_count(date('Y-m-d'), date('Y-m-d')); + $recent_redirects = $plugin->get_total_count(date('Y-m-d', strtotime('-7 days')), date('Y-m-d')); + $last_week_redirects = $plugin->get_total_count(date('Y-m-d', strtotime('-14 days')), date('Y-m-d', strtotime('-7 days'))); + $top_urls = $plugin->get_top_urls(3); + + $trend = 0; + if ($last_week_redirects > 0) { + $trend = (($recent_redirects - $last_week_redirects) / $last_week_redirects) * 100; + } elseif ($recent_redirects > 0) { + $trend = 100; + } + + $plugin_enabled = !empty($options['enabled']); + $auto_redirect = !empty($options['auto_redirect_enabled']); + $whitelist_count = 0; + if (!empty($options['whitelist_domains'])) { + $domains = explode("\n", $options['whitelist_domains']); + $whitelist_count = count(array_filter(array_map('trim', $domains))); + } + + echo ''; + + echo ''; +} + +add_filter('plugin_row_meta', 'wpnav_plugin_row_meta', 10, 2); +function wpnav_plugin_row_meta($links, $file) { + if (plugin_basename(__FILE__) === $file) { + $row_meta = array( + 'settings' => '' . esc_html__('Settings', 'wpnav-links') . '', + 'support' => '' . esc_html__('Support', 'wpnav-links') . '', + 'docs' => '' . esc_html__('Documentation', 'wpnav-links') . '', + ); + return array_merge($links, $row_meta); + } + return $links; +} + +add_action('init', 'wpnav_check_database_version'); +function wpnav_check_database_version() { + $installed_version = get_option('wpnav_links_db_version'); + + if ($installed_version !== WPNAV_LINKS_DB_VERSION) { + $plugin = new WPNAV_Links(); + $plugin->create_tables(); + update_option('wpnav_links_db_version', WPNAV_LINKS_DB_VERSION); + } +} + +add_action('wp_enqueue_scripts', 'wpnav_enqueue_frontend_styles'); +function wpnav_enqueue_frontend_styles() { + $options = get_option('wpnav_links_options'); + if (!isset($options['enabled']) || !$options['enabled']) { + return; + } + + if (!is_admin()) { + wp_enqueue_style( + 'wpnav-external-indicator', + WPNAV_LINKS_PLUGIN_URL . 'external-indicator.css', + array(), + WPNAV_LINKS_VERSION + ); + } +} + +if (!wp_next_scheduled('wpnav_daily_maintenance')) { + wp_schedule_event(time(), 'daily', 'wpnav_daily_maintenance'); +} + +add_action('wpnav_daily_maintenance', 'wpnav_daily_maintenance_tasks'); +function wpnav_daily_maintenance_tasks() { + wp_cache_flush(); + + global $wpdb; + $table_name = $wpdb->prefix . WPNAV_LINKS_TABLE; + $wpdb->query("OPTIMIZE TABLE $table_name"); +}