| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plugin Name: Extend Link |
| 4 |
* Plugin URI: https://wp-time.com/how-to-add-link-attributes-in-wordpress/ |
| 5 |
* Description: Allows you to add classes, IDs, titles, rel attributes, and file download options to links directly from the "Extend Link" dialog in the Classic Editor and Classic Block in Gutenberg. It also provides H1–H6 support, so you can, for example, add an ID or classes to a heading. A lightweight, professional plugin, free and always will remain free! |
| 6 |
* Version: 2.0.2 |
| 7 |
* Author: Alobaidi |
| 8 |
* Author URI: https://wp-time.com/how-to-add-link-attributes-in-wordpress/ |
| 9 |
* Text Domain: extend-link |
| 10 |
* Requires at least: 5.8 |
| 11 |
* Requires PHP: 7.4 |
| 12 |
* License: GPLv2 or later |
| 13 |
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| 14 |
* |
| 15 |
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU |
| 16 |
* General Public License version 2, as published by the Free Software Foundation. You may NOT assume |
| 17 |
* that you can use any other version of the GPL. |
| 18 |
* |
| 19 |
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without |
| 20 |
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 21 |
*/ |
| 22 |
|
| 23 |
// Prevent direct access |
| 24 |
if ( !defined('ABSPATH') ) { |
| 25 |
exit; |
| 26 |
} |
| 27 |
|
| 28 |
if ( !class_exists('Extend_Link_TinyMCE') ) { |
| 29 |
|
| 30 |
class Extend_Link_TinyMCE { |
| 31 |
private static $instance = null; |
| 32 |
|
| 33 |
/** |
| 34 |
* Retrieves the singleton instance of this class |
| 35 |
* Creates a new instance if one doesn't exist yet |
| 36 |
* |
| 37 |
* @return Extend_Link_TinyMCE The single instance of this class |
| 38 |
*/ |
| 39 |
public static function get_instance() { |
| 40 |
if (null === self::$instance) { |
| 41 |
self::$instance = new self(); |
| 42 |
} |
| 43 |
return self::$instance; |
| 44 |
} |
| 45 |
|
| 46 |
/** |
| 47 |
* constructor - intentionally empty |
| 48 |
* Initialization happens in the init() method instead for better control |
| 49 |
*/ |
| 50 |
public function __construct() { |
| 51 |
// No initialization here. |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Initialize plugin |
| 56 |
*/ |
| 57 |
public function init() { |
| 58 |
add_filter('plugin_row_meta', array($this, 'plugin_row_meta_custom'), 10, 2); |
| 59 |
add_action('admin_enqueue_scripts', array($this, 'enqueue_translations')); |
| 60 |
add_filter('mce_buttons', array($this, 'add_tinymce_button')); |
| 61 |
add_filter('mce_external_plugins', array($this, 'add_tinymce_plugin')); |
| 62 |
add_action('wp_ajax_extend_link_plu_check_link', array($this, 'check_link_status')); |
| 63 |
} |
| 64 |
|
| 65 |
/** |
| 66 |
* Adds custom links to plugin's meta information row |
| 67 |
*/ |
| 68 |
public function plugin_row_meta_custom($links, $file) { |
| 69 |
$plugin_file = plugin_basename(__FILE__); |
| 70 |
|
| 71 |
if ( $file == $plugin_file ) { |
| 72 |
|
| 73 |
$custom_links = array( |
| 74 |
'<a style="font-weight:bold;" href="https://wp-time.com/how-to-add-link-attributes-in-wordpress/" target="_blank">' . esc_html__('Plugin Reference', 'extend-link') . '</a>', |
| 75 |
'<a style="font-weight:bold;color:#4f9915;" href="https://wp-time.com/video-popup-plugin-for-wordpress/#live-demo" target="_blank">' . esc_html__('Video Popup Plugin', 'extend-link') . '</a>', |
| 76 |
'<a style="font-weight:bold;color:#ce7825;" href="https://wp-time.com/wordpress-preloader/#preloader-for-wp" target="_blank">' . esc_html__('Preloader Plugin', 'extend-link') . '</a>' |
| 77 |
); |
| 78 |
|
| 79 |
$links = array_merge($links, $custom_links); |
| 80 |
} |
| 81 |
return $links; |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* Add TinyMCE plugin script |
| 86 |
*/ |
| 87 |
public function add_tinymce_button($buttons) { |
| 88 |
array_push($buttons, 'extend_link'); |
| 89 |
return $buttons; |
| 90 |
} |
| 91 |
|
| 92 |
public function add_tinymce_plugin($plugin_array) { |
| 93 |
$version = '2.0.1'; |
| 94 |
$plugin_array['extend_link'] = plugins_url('/js/tinymce-button.js?ver='.$version, __FILE__); |
| 95 |
return $plugin_array; |
| 96 |
} |
| 97 |
|
| 98 |
/** |
| 99 |
* Enqueue translations for JavaScript |
| 100 |
*/ |
| 101 |
public function enqueue_translations($hook) { |
| 102 |
// Only load on post/page editor screens |
| 103 |
if ( !in_array($hook, array('post.php', 'post-new.php')) ) { |
| 104 |
return; |
| 105 |
} |
| 106 |
|
| 107 |
wp_enqueue_style( |
| 108 |
'extend_link_tinymce-button-style', |
| 109 |
plugins_url('/css/tinymce-button-style.css', __FILE__), |
| 110 |
array(), |
| 111 |
'2.0.1' |
| 112 |
); |
| 113 |
|
| 114 |
// Prepare translations array |
| 115 |
$translations = array( |
| 116 |
'plugin_name' => __('Extend Link', 'extend-link'), |
| 117 |
'dialog_btn_tooltip' => __('Allows you to add classes, IDs, titles, rel attributes, and file download options to links. It also allows adding an ID or classes to a heading.', 'extend-link'), |
| 118 |
'dialog_title' => __('Insert/Edit Link or Add an ID or Classes to a Heading', 'extend-link'), |
| 119 |
'link' => __('Link', 'extend-link'), |
| 120 |
'url' => __('URL', 'extend-link'), |
| 121 |
'link_text' => __('Link Text', 'extend-link'), |
| 122 |
'link_text_placeholder' => __('Click here', 'extend-link'), |
| 123 |
'title' => __('Title (tooltip)', 'extend-link'), |
| 124 |
'title_placeholder' => __('Link description', 'extend-link'), |
| 125 |
'id' => __('ID', 'extend-link'), |
| 126 |
'id_placeholder' => __('link-id', 'extend-link'), |
| 127 |
'classes' => __('Class(es)', 'extend-link'), |
| 128 |
'classes_placeholder' => __('class-1 class-2', 'extend-link'), |
| 129 |
'open_new_tab' => __('Open in new tab', 'extend-link'), |
| 130 |
'add_nofollow' => __('Add rel="nofollow"', 'extend-link'), |
| 131 |
'add_noreferrer' => __('Add rel="noreferrer"', 'extend-link'), |
| 132 |
'add_noopener' => __('Add rel="noopener"', 'extend-link'), |
| 133 |
'download_file' => __('Download file (instead of opening)', 'extend-link'), |
| 134 |
'cancel' => __('Cancel', 'extend-link'), |
| 135 |
'save' => __('Save', 'extend-link'), |
| 136 |
'error_text_required' => __('Please enter link text', 'extend-link'), |
| 137 |
|
| 138 |
'url_tooltip' => __('Enter a link.', 'extend-link'), |
| 139 |
'link_text_tooltip' => __('Enter the text that will appear for the link.', 'extend-link'), |
| 140 |
'title_tooltip' => __('Optional tooltip text shown on hover.', 'extend-link'), |
| 141 |
'id_tooltip' => __('A single unique ID for the link (only only allowed).', 'extend-link'), |
| 142 |
'classes_tooltip' => __('Add one or more CSS classes separated by spaces.', 'extend-link'), |
| 143 |
|
| 144 |
'open_new_tab_tooltip' => __('Open the link in a new browser tab.', 'extend-link'), |
| 145 |
'add_nofollow_tooltip' => __('Prevent search engines from following this link.', 'extend-link'), |
| 146 |
'add_noreferrer_tooltip' => __('Hide the referrer information when the link is clicked.', 'extend-link'), |
| 147 |
'add_noopener_tooltip' => __('Improve security by preventing the new page from accessing the opener.', 'extend-link'), |
| 148 |
'download_file_tooltip' => __('Force the browser to download the file instead of opening it. Use this only with a direct file URL (e.g., https://example.com/video.mp4). Note: This option may not work in some browsers.', 'extend-link'), |
| 149 |
|
| 150 |
'check_btn_text' => __('Check', 'extend-link'), |
| 151 |
'check_btn_label' => __('Check Link Status', 'extend-link'), |
| 152 |
'check_btn_tooltip' => __('Check whether the link is working or broken. Useful for SEO.', 'extend-link'), |
| 153 |
'check_btn_no_url' => __('Please enter a URL first to check its status.', 'extend-link'), |
| 154 |
'check_btn_disabled' => __('Please wait...', 'extend-link'), |
| 155 |
'check_btn_working_link' => __('Link is working!', 'extend-link'), |
| 156 |
'check_btn_broken_link' => __('Link is broken!', 'extend-link'), |
| 157 |
'check_btn_error' => __('Error checking link. Please try again after a few seconds.', 'extend-link'), |
| 158 |
|
| 159 |
'plugin_ref_btn_text' => __('Plugin Reference', 'extend-link'), |
| 160 |
'plugin_ref_btn_tooltip' => __('Everything related to the plugin can be found on this page.', 'extend-link'), |
| 161 |
'plugin_support_btn_text' => __('Support', 'extend-link'), |
| 162 |
'plugin_support_btn_tooltip' => __('The Extend Link plugin is free and will always remain free. Contact us if you have any questions or feature suggestions to help improve the plugin.', 'extend-link'), |
| 163 |
'plugin_rate_btn_text' => __('Rate Plugin', 'extend-link'), |
| 164 |
'plugin_rate_btn_tooltip' => __('Rate this plugin to support its development.', 'extend-link'), |
| 165 |
'plugin_label_note' => __('This plugin is completely free and will remain free, even as we add more features. So, suggestions for our other plugins, like Preloader and Video Popup, help us continue development.', 'extend-link'), |
| 166 |
|
| 167 |
'vp_plugin_btn_text' => __('Video Popup Plugin', 'extend-link'), |
| 168 |
'vp_plugin_btn_tooltip' => __('From our plugins: The ultimate Video Popup plugin for WordPress. Smart, flexible, and made for easy control. Create unlimited, on-brand, and elegant responsive popups for YouTube, Vimeo, MP4 & WebM videos on click or On-Page Load.', 'extend-link'), |
| 169 |
'preloader_plugin_btn_text' => __('Preloader Plugin', 'extend-link'), |
| 170 |
'preloader_plugin_btn_tooltip' => __('From our plugins: The ultimate Preloader plugin for WordPress. Add a preloader to your website easily in only 3 steps. Simple, fast, and compatible with all major browsers.', 'extend-link') |
| 171 |
); |
| 172 |
|
| 173 |
// Add translations to page as inline script |
| 174 |
wp_add_inline_script( |
| 175 |
'wp-tinymce', |
| 176 |
'window.extendLinkI18n = ' . wp_json_encode($translations) . ';', |
| 177 |
'before' |
| 178 |
); |
| 179 |
|
| 180 |
wp_add_inline_script( |
| 181 |
'wp-tinymce', |
| 182 |
'var extendLinkAjax = ' . json_encode(array( |
| 183 |
'ajaxurl' => admin_url('admin-ajax.php'), |
| 184 |
'nonce' => wp_create_nonce('extend_link_check_status_nonce') |
| 185 |
)) . ';', |
| 186 |
'before' |
| 187 |
); |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Enhanced security check for URLs |
| 192 |
* Validates that the URL is safe to check and not pointing to internal resources |
| 193 |
* |
| 194 |
* @param string $url The URL to validate |
| 195 |
* @return bool True if URL is safe, false otherwise |
| 196 |
*/ |
| 197 |
public function is_safe_url($url) { |
| 198 |
$parsed = wp_parse_url($url); |
| 199 |
|
| 200 |
if (!$parsed || !isset($parsed['host'])) { |
| 201 |
return false; |
| 202 |
} |
| 203 |
|
| 204 |
$host = $parsed['host']; |
| 205 |
|
| 206 |
// Block common localhost and internal addresses |
| 207 |
$blocked_hosts = array( |
| 208 |
'localhost', |
| 209 |
'127.0.0.1', |
| 210 |
'0.0.0.0', |
| 211 |
'::1', |
| 212 |
'[::1]' |
| 213 |
); |
| 214 |
|
| 215 |
if (in_array(strtolower($host), $blocked_hosts)) { |
| 216 |
return false; |
| 217 |
} |
| 218 |
|
| 219 |
// Check for private/reserved IP ranges |
| 220 |
if (filter_var($host, FILTER_VALIDATE_IP)) { |
| 221 |
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { |
| 222 |
return false; |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
// Only allow HTTP and HTTPS protocols |
| 227 |
if (!in_array($parsed['scheme'], array('http', 'https'))) { |
| 228 |
return false; |
| 229 |
} |
| 230 |
|
| 231 |
return true; |
| 232 |
} |
| 233 |
|
| 234 |
/** |
| 235 |
* AJAX handler to check link status |
| 236 |
* Uses wp_safe_remote_head() for enhanced security against SSRF attacks |
| 237 |
*/ |
| 238 |
public function check_link_status() { |
| 239 |
// Verify user capabilities |
| 240 |
if (!current_user_can('edit_posts')) { |
| 241 |
wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'extend-link')]); |
| 242 |
} |
| 243 |
|
| 244 |
// Verify nonce for CSRF protection |
| 245 |
if (!isset($_POST['nonce']) || empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'extend_link_check_status_nonce')) { |
| 246 |
wp_send_json_error(['message' => __('Security check failed. Please refresh the page and try again.', 'extend-link')]); |
| 247 |
} |
| 248 |
|
| 249 |
// Sanitize and validate URL |
| 250 |
$url = isset($_POST['url']) ? sanitize_url(wp_unslash($_POST['url'])) : ''; |
| 251 |
$url = !empty(trim($url)) ? esc_url_raw($url) : ''; |
| 252 |
|
| 253 |
if (empty($url)) { |
| 254 |
wp_send_json_error(['message' => __('Please enter a URL first to check its status.', 'extend-link')]); |
| 255 |
} |
| 256 |
|
| 257 |
if (!filter_var($url, FILTER_VALIDATE_URL)) { |
| 258 |
wp_send_json_error(['message' => __('Please enter a valid URL.', 'extend-link')]); |
| 259 |
} |
| 260 |
|
| 261 |
// Additional security check for internal addresses |
| 262 |
if (!$this->is_safe_url($url)) { |
| 263 |
wp_send_json_error(['message' => __('This URL is not allowed for security reasons.', 'extend-link')]); |
| 264 |
} |
| 265 |
|
| 266 |
$response = wp_safe_remote_head($url, array( |
| 267 |
'timeout' => 10, |
| 268 |
'sslverify' => true, |
| 269 |
'redirection' => 3, |
| 270 |
'user-agent' => 'WordPress/' . get_bloginfo('version') . '; ' . get_bloginfo('url'), |
| 271 |
'httpversion' => '1.1' |
| 272 |
)); |
| 273 |
|
| 274 |
if (is_wp_error($response)) { |
| 275 |
wp_send_json_error(['message' => __('There is an error. Please try again after a few seconds.', 'extend-link')]); |
| 276 |
} |
| 277 |
|
| 278 |
$status = wp_remote_retrieve_response_code($response); |
| 279 |
|
| 280 |
if ($status >= 200 && $status < 400) { |
| 281 |
wp_send_json_success([ |
| 282 |
'message' => sprintf( |
| 283 |
// translators: %s will be replaced with the HTTP status code of the working link. |
| 284 |
__('Link is working (Status: %s)', 'extend-link'), |
| 285 |
$status |
| 286 |
) |
| 287 |
]); |
| 288 |
} else { |
| 289 |
wp_send_json_error([ |
| 290 |
'message' => sprintf( |
| 291 |
// translators: %s will be replaced with the HTTP status code of the broken link. |
| 292 |
__('Link is broken (Status: %s)', 'extend-link'), |
| 293 |
$status |
| 294 |
) |
| 295 |
]); |
| 296 |
} |
| 297 |
} |
| 298 |
} |
| 299 |
|
| 300 |
// Initialize plugin |
| 301 |
$Extend_Link_TinyMCE = Extend_Link_TinyMCE::get_instance(); |
| 302 |
$Extend_Link_TinyMCE->init(); |
| 303 |
} |