PluginProbe
HTTP 410 (Gone) responses / 0.8.1
HTTP 410 (Gone) responses v0.8.1
1.2.1 trunk 0.1 0.2 0.3 0.4 0.5 0.6 0.6.1 0.7 0.7.1 0.7.2 0.8.1 0.8.2 1.0.2 1.0.3 1.1.0
wp-410 / wp-410.php

wp-410.php in HTTP 410 (Gone) responses 0.8.1, at wp-410.php

348 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: 410 for WordPress
4 Plugin URI: http://rayofsolaris.net/code/410-for-wordpress
5 Description: Sends HTTP 410 (Gone) responses to requests for pages that no longer exist on your blog.
6 Version: 0.8.1
7 Author: Samir Shah
8 Author URI: http://rayofsolaris.net/
9 License: GPL2
10 */
11
12 if( ! defined( 'ABSPATH' ) )
13 exit;
14
15 class WP_410 {
16 const db_version = 5;
17 private $permalinks;
18 private $table;
19
20 function __construct() {
21 $this->permalinks = (bool) get_option('permalink_structure');
22 $this->table = $GLOBALS['wpdb']->prefix . '410_links';
23
24 add_action( 'plugins_loaded', array( $this, 'upgrade_check' ) );
25
26 if( is_admin() )
27 add_action( 'admin_menu', array( $this, 'settings_menu' ) );
28 else
29 add_action( 'template_redirect', array( $this, 'check_for_410' ) );
30
31 // these could theoretically happen both with/without is_admin()
32 add_action('wp_insert_post', array( $this, 'note_inserted_post' ));
33 }
34
35 private function install_table() {
36 // remember, two spaces after PRIMARY KEY otherwise WP borks
37 $sql = "CREATE TABLE $this->table (
38 gone_id MEDIUMINT unsigned NOT NULL AUTO_INCREMENT,
39 gone_key VARCHAR(512) NOT NULL,
40 gone_regex VARCHAR(512) NOT NULL,
41 is_404 SMALLINT(1) unsigned NOT NULL DEFAULT 0,
42 PRIMARY KEY (gone_id),
43 KEY is_404 (is_404)
44 );";
45
46 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
47 dbDelta($sql);
48 }
49
50 private function get_links(){
51 global $wpdb;
52 return $wpdb->get_results( "SELECT gone_key, gone_regex FROM $this->table WHERE is_404 = 0", OBJECT_K ); // indexed by gone_key
53 }
54
55 private function get_404s(){
56 global $wpdb;
57 $links = $wpdb->get_results( "SELECT gone_key, gone_regex FROM $this->table WHERE is_404 = 1", OBJECT_K ); // indexed by gone_key
58 // limit how many 404s we store
59 if( count( $links ) > 50 ) {
60 $n = count( $links ) - 50;
61 $wpdb->query( "DELETE FROM `$this->table` WHERE is_404 = 1 ORDER BY gone_id LIMIT $n" );
62 return $this->get_404s();
63 }
64 return $links;
65 }
66
67 private function add_link( $key, $is_404 = false ){ // just supply the link
68 global $wpdb;
69 // build regex
70 $parts = preg_split( '/(\*)/', $key, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
71 foreach( $parts as &$part ) if( '*' != $part )
72 $part = preg_quote( $part, '|' );
73 $parts = str_replace( '*', '.*', $parts );
74 $regex = '|^' . implode( '', $parts ) . '$|i';
75
76 // avoid duplicates - messy but MySQL doesn't allow url-length unique keys
77 if( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `$this->table` WHERE `gone_key` = %s", $key ) ) )
78 return 0;
79
80 return $wpdb->insert( $this->table, array( 'gone_key' => $key, 'gone_regex' => $regex, 'is_404' => intval( $is_404 ) ) );
81 }
82
83 private function convert_404( $key ) {
84 global $wpdb;
85 return $wpdb->query( $wpdb->prepare( "UPDATE `$this->table` SET is_404 = 0 WHERE `gone_key` = %s LIMIT 1", $key ) );
86 }
87
88 private function remove_link( $key ){
89 global $wpdb;
90 return $wpdb->query( $wpdb->prepare( "DELETE FROM $this->table WHERE gone_key = %s", array( $key ) ) );
91 }
92
93 function upgrade_check() {
94 $options_version = get_option( 'wp_410_options_version', 0 );
95
96 if( $options_version == self::db_version ) // nothing to do
97 return;
98
99 // last db change was in version 5
100 if( $options_version < 5 )
101 $this->install_table();
102
103 if( $options_version < 3 ) {
104 $old_links = get_option( 'wp_410_links_list', array() );
105 $new_links = array(); // just a simple array of links
106
107 if( 0 == $options_version ) // links were stored just as links
108 $new_links = array_map( 'rawurldecode', $old_links );
109 elseif( 1 == $options_version ) // links were stored as array( link => regex ), We only need the link
110 $new_links = array_map( 'rawurldecode', array_keys( $old_links ) );
111 else // moved to using the database in db_version 3
112 $new_links = array_keys( $old_links );
113
114 foreach( $new_links as $link )
115 $this->add_link( $link );
116
117 delete_option( 'wp_410_links_list' ); // remove old option
118 }
119
120 update_option( 'wp_410_options_version', self::db_version );
121 }
122
123 function settings_menu() {
124 add_submenu_page('plugins.php', '410 for WordPress', '410 for WordPress', 'manage_options', 'wp_410_settings', array( $this, 'settings_page') );
125 }
126
127 function settings_page() {
128 $links = $this->get_links();
129 $logged_404s = $this->get_404s();
130 $links_to_add = array();
131
132 if ( isset( $_POST['submit'] ) ) {
133 $failed_to_add = array();
134 if( !empty( $_POST['links_to_add'] ) ) {
135 foreach( preg_split( '/(\r?\n)+/', $_POST['links_to_add'], -1, PREG_SPLIT_NO_EMPTY ) as $link ) {
136 if( $this->is_valid_url( $link ) ) {
137 $this->add_link( $link );
138 $links[$link] = $link; // the value doesn't matter, we don't use it here
139 }
140 else {
141 $failed_to_add[] = '<code>' . htmlspecialchars( $link ) . '</code>';
142 }
143 }
144 }
145
146 if( !empty( $_POST['add_404s'] ) ) {
147 foreach( $_POST['add_404s'] as $link ) {
148 $this->convert_404( $link );
149 $links[$link] = $link; // the value doesn't matter, we don't use it here
150 unset( $logged_404s[$link] );
151 }
152 }
153
154 if( !empty( $_POST['old_links_to_remove'] ) ) {
155 foreach( $_POST['old_links_to_remove'] as $key ) {
156 if( isset( $links[$key] ) ) {
157 $this->remove_link( $key );
158 unset( $links[$key] );
159 }
160 }
161 }
162
163 echo '<div id="message" class="updated fade"><p>Options updated.</p></div>';
164
165 if( $failed_to_add )
166 echo '<div class="error"><p>The following entries could not be recognised as URLs that your WordPress site handles, and were not added to the list. This can be because the domain name and path does not match that of your WordPress site, or because pretty permalinks are disabled.</p><p>- ' . implode( '<br> - ', $failed_to_add ) .'</p></div>';
167 }
168
169 ksort( $links );
170 ksort( $logged_404s );
171 ?>
172 <style>
173 .indent {padding-left: 2em}
174 .wp_gone_deleted label {text-decoration: line-through}
175 label.invalid {color: red}
176 p.invalid { background: #FFEBE8; border: 1px solid red; border-radius: 5px; padding: 5px 10px}
177 #wp_410_table th {font-weight: bold}
178 #wp_410_table tr {border-top: 1px solid #EEE}
179 </style>
180 <?php screen_icon(); ?>
181 <div class="wrap">
182 <h2>410 for WordPress</h2>
183 <?php if( WP_CACHE ) :?>
184 <div class="updated">
185 <p><strong style="color: #900">Warning:</strong> It seems that a caching/performance plugin is active on this site. This plugin has only been tested with the following caching plugins:</p>
186 <ul style="list-style: disc; margin-left: 2em">
187 <li>W3 Total Cache</li>
188 <li> WP Super Cache</li>
189 </ul>
190 <p><strong>Other caching plugins may cache responses to requests for pages that have been removed</strong>, in which case this plugin will not be able to intercept the requests and issue a 410 response header.</p>
191 </div>
192 <?php endif; ?>
193 <p>This plugin will issue a HTTP 410 response to articles that no longer exist on your blog. When an article is deleted, it records this and issues a 410 response when pages unique to that article are requested. This HTTP code informs robots visiting your site that the requested page has been permanently removed, and that they should stop trying to access it.</p>
194 <p><strong>Note: this plugin will only return a 410 response if WordPress has not found something valid to display for the requested URL.</strong></p>
195 <form action="" method="post" id="wp-gone-captcha-settings">
196 <table id="wp_410_table" class="form-table"><tbody>
197 <tr><th>Obsolete URLs</th><td>
198 <?php
199 if( empty( $links ) ) {
200 echo '<p>There are currently no obsolete URLs in this list. You can add some manually below.</p>';
201 }
202 else {
203 echo '<p>The following URLs are currently considered obsolete. To remove items from the list, check the corresponding box.</p>';
204 echo '<ul id="wp_gone_old_links" class="indent">';
205
206 // Select all helper
207 echo '<li class="hide-if-no-js"><label><input type="checkbox" id="select-all-410" /> Select all</label></li>';
208
209 $invalid_links_exist = false;
210 foreach( array_keys( $links ) as $k ) {
211 $valid = $this->is_valid_url( $k );
212
213 if( ! $valid )
214 $invalid_links_exist = true;
215
216 $k = htmlspecialchars( $k );
217 $class = $valid ? '' : 'class="invalid"';
218
219 echo "<li><label $class><input type='checkbox' name='old_links_to_remove[]' value='$k' /> <code>$k</code></label></li>";
220 }
221 echo '</ul>';
222
223 if( $invalid_links_exist )
224 echo '<p class="invalid">Warning: WordPress is not able to issue 410 responses for the URLs marked in red above. This is because those URLs are not handled by your WordPress installation. This can be because the domain name and path does not match that of your WordPress site, or because pretty permalinks are disabled.</p>';
225 }
226 ?>
227 <p><small>If you create or update an article whose URL matches one of those above, that URL will automatically be removed from this list.</small></p>
228 </td></tr>
229 <tr><th>Recent 404 errors</th><td>
230
231 <?php
232 if( empty( $logged_404s ) ) {
233 echo '<p>This plugin will automatically log recent 404 (Page Not Found) errors on your site, and show them here, so that you can easily add them to the list above. There are currently no 404 errors reported.</p>';
234 }
235 else {
236 echo '<p>Below are recent 404 (Page Not Found) errors that have occurred on your site. If you want to issue a 410 response for any of these requests, check the corresponding box.</p>';
237 echo '<ul id="wp_gone_404s" class="indent">';
238 // Select all helper
239 echo '<li class="hide-if-no-js"><label><input type="checkbox" id="select-all-404" /> Select all</label></li>';
240
241 foreach( array_keys( $logged_404s ) as $k ) {
242 $k = htmlspecialchars( $k );
243 echo "<li><label><input type='checkbox' name='add_404s[]' value='$k' /> <code>$k</code></label></li>";
244 }
245 echo '</ul>';
246 }
247 ?>
248 </td></tr>
249 <tr><th>Manually add URLs</th><td>
250 <p>You can manually add items to the list by entering them below. Please enter one <strong>fully qualified</strong> URL per line.</p>
251 <p>Use <code>*</code> as a wildcard character. So <code>http://www.example.com/*/music/</code> will match all URLs ending in <code>/music/</code>.</p>
252 <textarea name="links_to_add" rows="8" cols="80"></textarea>
253 </td></tr>
254 <tr><th>410 reponse message</th><td>
255 <p>By default, the plugin issues the following plain-text message as part of the 410 response: <code>Sorry, the page you requested has been permanently removed.</code></p>
256 <?php
257 if( locate_template( '410.php' ) )
258 echo '<p><strong>A template file <code>410.php</code> has been detected in your theme directory. This file will be used to display 410 responses.</strong> To revert back to the default message, remove the file from your theme directory.</p>';
259 else
260 echo '<p>If you would like to use your own template instead, simply place a file called <code>410.php</code> in your theme directory, containing your template. Have a look at your theme\'s <code>404.php</code> template to see what it should look like.</p>';
261 ?>
262 </td></tr>
263 </tbody></table>
264 <p class="submit"><input class="button-primary" type="submit" name="submit" value="Update settings" /></p>
265 </form>
266 </div>
267 <script>
268 jQuery(document).ready(function($){
269 $('#wp_gone_old_links input').change( function(){
270 $(this).parent().toggleClass('wp_gone_deleted', $(this).is(':checked'));
271 });
272 $("#wp-gone-captcha-settings input").change( function(){
273 $("#message").slideUp('slow');
274 });
275 $("#select-all-410, #select-all-404").change(function() {
276 var el = $(this);
277 el.closest("li").siblings().find("input").prop("checked", el.is(":checked"));
278 });
279 });
280 </script>
281 <?php
282 }
283
284 private function is_valid_url ( $link ) {
285 // Determine whether WP will handle a request for this URL
286 $home = home_url();
287 if( strpos( $link, $home ) !== 0 )
288 return false;
289
290 if( !$this->permalinks ) {
291 $req = preg_replace( '^|' . preg_quote( $home, '|' ) . '/?|' , '', $link );
292 if( strlen( $req ) && $req[0] != '?' ) // this is a pretty permalink, but pretty permalinks are disabled
293 return false;
294 }
295
296 return true;
297 }
298
299 function note_inserted_post( $id ) {
300 $post = get_post( $id );
301
302 if( 'revision' == $post->post_type || 'draft' == $post->post_status )
303 return;
304
305 // Check our list of URLs against the new/updated post's permalink, and if they match, scratch it from our list
306 $created_links = array();
307
308 $created_links[] = rawurldecode( get_permalink( $id ) );
309 $created_links[] = get_post_comments_feed_link( $id ); // back compat
310
311 if( $this->permalinks )
312 $created_links[0] .= '*';
313
314 $list = get_option( 'wp_410_links_list', array() );
315
316 foreach( $created_links as $link )
317 $this->remove_link( $link );
318 }
319
320 function check_for_410() {
321 // Don't mess if Wordpress has found something to display
322 if( !is_404() )
323 return;
324
325 $links = $this->get_links();
326 $req = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
327 $req = rawurldecode( $req );
328
329 foreach( $links as $link ) {
330 if( @preg_match( $link->gone_regex, $req ) ) {
331 define( 'DONOTCACHEPAGE', true ); // WP Super Cache and W3 Total Cache recognise this
332 status_header( 410 );
333 do_action( 'wp_410_response' ); // you can use this to customise the response message
334
335 if( ! locate_template( '410.php', true ) )
336 echo 'Sorry, the page you requested has been permanently removed.';
337
338 exit;
339 }
340 }
341
342 // no hit, log 404
343 $this->add_link( $req, true );
344 }
345 }
346
347 new WP_410();
348