PluginProbe
Extend Link / trunk
Extend Link vtrunk
trunk 2.0.0 2.0.1 2.0.2
extend-link / extend-link.php

extend-link.php in Extend Link trunk, at extend-link.php

303 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }