PluginProbe
HTTP 410 (Gone) responses / 0.8.2
HTTP 410 (Gone) responses v0.8.2
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.2, at wp-410.php

367 lines 14.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.2
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 max_404_list_length() {
56 return get_option( 'wp_410_max_404s', 50 );
57 }
58
59 private function get_404s(){
60 global $wpdb;
61 $this->concat_404_list();
62 return $wpdb->get_results( "SELECT gone_key, gone_regex FROM $this->table WHERE is_404 = 1", OBJECT_K ); // indexed by gone_key
63 }
64
65 private function add_link( $key, $is_404 = false ){ // just supply the link
66 global $wpdb;
67
68 // 404 logging enabled?
69 if( $is_404 && $this->max_404_list_length() == 0 )
70 return;
71
72 // build regex
73 $parts = preg_split( '/(\*)/', $key, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
74 foreach( $parts as &$part ) if( '*' != $part )
75 $part = preg_quote( $part, '|' );
76 $parts = str_replace( '*', '.*', $parts );
77 $regex = '|^' . implode( '', $parts ) . '$|i';
78
79 // avoid duplicates - messy but MySQL doesn't allow url-length unique keys
80 if( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `$this->table` WHERE `gone_key` = %s", $key ) ) )
81 return 0;
82
83 $wpdb->insert( $this->table, array( 'gone_key' => $key, 'gone_regex' => $regex, 'is_404' => intval( $is_404 ) ) );
84
85 // Don't let 404 list grow forever
86 if( $is_404 )
87 $this->concat_404_list();
88 }
89
90 private function concat_404_list() {
91 global $wpdb;
92 $n = intval( $wpdb->get_var( "SELECT COUNT(*) FROM `$this->table` WHERE `is_404` = 1" ) - $this->max_404_list_length() );
93 if( $n > 0 )
94 $wpdb->query( "DELETE FROM `$this->table` WHERE is_404 = 1 ORDER BY gone_id LIMIT $n" );
95 }
96
97 private function convert_404( $key ) {
98 global $wpdb;
99 return $wpdb->query( $wpdb->prepare( "UPDATE `$this->table` SET is_404 = 0 WHERE `gone_key` = %s LIMIT 1", $key ) );
100 }
101
102 private function remove_link( $key ){
103 global $wpdb;
104 return $wpdb->query( $wpdb->prepare( "DELETE FROM $this->table WHERE gone_key = %s", array( $key ) ) );
105 }
106
107 function upgrade_check() {
108 $options_version = get_option( 'wp_410_options_version', 0 );
109
110 if( $options_version == self::db_version ) // nothing to do
111 return;
112
113 // last db change was in version 5
114 if( $options_version < 5 )
115 $this->install_table();
116
117 if( $options_version < 3 ) {
118 $old_links = get_option( 'wp_410_links_list', array() );
119 $new_links = array(); // just a simple array of links
120
121 if( 0 == $options_version ) // links were stored just as links
122 $new_links = array_map( 'rawurldecode', $old_links );
123 elseif( 1 == $options_version ) // links were stored as array( link => regex ), We only need the link
124 $new_links = array_map( 'rawurldecode', array_keys( $old_links ) );
125 else // moved to using the database in db_version 3
126 $new_links = array_keys( $old_links );
127
128 foreach( $new_links as $link )
129 $this->add_link( $link );
130
131 delete_option( 'wp_410_links_list' ); // remove old option
132 }
133
134 update_option( 'wp_410_options_version', self::db_version );
135 }
136
137 function settings_menu() {
138 add_submenu_page('plugins.php', '410 for WordPress', '410 for WordPress', 'manage_options', 'wp_410_settings', array( $this, 'settings_page') );
139 }
140
141 function settings_page() {
142 $links = $this->get_links();
143 $logged_404s = $this->get_404s();
144 $links_to_add = array();
145
146 // Entries to delete
147 if( isset( $_POST['delete-from-410-list'] ) && !empty( $_POST['old_links_to_remove'] ) ) {
148 foreach( $_POST['old_links_to_remove'] as $key ) {
149 if( isset( $links[$key] ) ) {
150 $this->remove_link( $key );
151 unset( $links[$key] );
152 }
153 }
154 }
155 // Entries to add, either manually or from 404 list
156 else if ( isset( $_POST['add-to-410-list'] ) ) {
157 $failed_to_add = array();
158 if( !empty( $_POST['links_to_add'] ) ) {
159 foreach( preg_split( '/(\r?\n)+/', $_POST['links_to_add'], -1, PREG_SPLIT_NO_EMPTY ) as $link ) {
160 if( $this->is_valid_url( $link ) ) {
161 $this->add_link( $link );
162 }
163 else {
164 $failed_to_add[] = '<code>' . htmlspecialchars( $link ) . '</code>';
165 }
166 }
167 }
168
169 if( !empty( $_POST['add_404s'] ) ) {
170 foreach( $_POST['add_404s'] as $link ) {
171 $this->convert_404( $link );
172 }
173 }
174
175 // Update lists
176 $links = $this->get_links();
177 $logged_404s = $this->get_404s();
178
179 if( $failed_to_add )
180 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>';
181 }
182 else if ( isset( $_POST['set-404-list-length'] ) ) {
183 update_option( 'wp_410_max_404s', intval( $_POST['max_404_list_length'] ) );
184 $logged_404s = $this->get_404s();
185 }
186
187 if( !empty( $_POST ) )
188 echo '<div id="message" class="updated fade"><p>Options updated.</p></div>';
189
190 ksort( $links );
191 ksort( $logged_404s );
192 ?>
193 <style>
194 tr.invalid label {color: red}
195 p.invalid { background: #FFEBE8; border: 1px solid red; border-radius: 5px; padding: 5px 10px}
196 .wp-410-table-wrap {max-width: 890px; max-height: 300px; overflow-y: scroll}
197 </style>
198 <?php screen_icon(); ?>
199 <div class="wrap">
200 <h2>410 for WordPress</h2>
201 <?php if( WP_CACHE ) :?>
202 <div class="updated">
203 <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>
204 <ul style="list-style: disc; margin-left: 2em">
205 <li>W3 Total Cache</li>
206 <li> WP Super Cache</li>
207 </ul>
208 <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>
209 </div>
210 <?php endif; ?>
211 <p>This plugin will issue a HTTP 410 response to articles that no longer exist on your blog. This informs robots that the requested page has been permanently removed, and that they should stop trying to access it.</p>
212 <p><strong>A 410 response will only be issued if WordPress has not found something valid to display for the requested URL.</strong></p>
213 <h3>Obsolete URLs</h3>
214 <form action="" method="post">
215 <?php
216 if( empty( $links ) ) {
217 echo '<p>There are currently no obsolete URLs in this list. You can add some manually below.</p>';
218 }
219 else {
220 echo '<p>The following URLs (or masks) will receive a 410 response. If you create or update an article whose URL matches one of those below, that URL will automatically be removed from the list.</p>';
221 echo '<div class="wp-410-table-wrap"><table id="wp_gone_old_links" class="wp-list-table widefat fixed">';
222 echo '<thead><th class="check-column"><input type="checkbox" id="select-all-410" /><label for="select-all-410" class="screen-reader-text"> Select all</label></th><th>Obsolete URL</th></thead>';
223 echo '<tbody>';
224
225 $invalid_links_exist = false;
226 foreach( array_keys( $links ) as $k ) {
227 $valid = $this->is_valid_url( $k );
228
229 if( ! $valid )
230 $invalid_links_exist = true;
231
232 $k = htmlspecialchars( $k );
233 $class = $valid ? '' : 'class="invalid"';
234
235 echo "<tr $class><td><input type='checkbox' name='old_links_to_remove[]' id='wp-410-$k' value='$k' /></td><td><label for='wp-410-$k'><code>$k</code></label></td</tr>";
236 }
237 echo '</tbody></table></div>';
238
239 if( $invalid_links_exist )
240 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>';
241
242 echo '<p class="submit"><input class="button button-primary" type="submit" name="delete-from-410-list" value="Delete selected entries" /></p>';
243 }
244 ?>
245 </form>
246
247 <h3>Recent 404 errors</h3>
248 <p>Recent 404 (Page Not Found) errors on your site are shown here, so that you can easily add them to the list above.</p>
249 <form action="" method="post">
250 <p><label>Maximum number of 404 errors to keep: <input type="number" size="3" name="max_404_list_length" value="<?php echo $this->max_404_list_length(); ?>" /></label> <input class="button button-secondary" type="submit" name="set-404-list-length" value="Save" /> (setting this to zero will disable logging).</p>
251 </form>
252 <form action="" method="post">
253 <?php
254 if( empty( $logged_404s ) ) {
255 if( $this->max_404_list_length() )
256 echo '<p>There are currently no 404 errors reported.</p>';
257 }
258 else {
259 echo '<p>Below are recent 404 (Page Not Found) errors that have occurred on your site. You can add these to the list of obsolete URLs.</p>';
260 echo '<div class="wp-410-table-wrap"><table id="wp_gone_404s" class="wp-list-table widefat fixed">';
261 echo '<thead><th class="check-column"><input type="checkbox" id="select-all-404" /><label for="select-all-404" class="screen-reader-text"> Select all</label></th><th>URL</th></thead>';
262 echo '<tbody>';
263
264 foreach( array_keys( $logged_404s ) as $k ) {
265 $k = htmlspecialchars( $k );
266 echo "<tr><td><input type='checkbox' name='add_404s[]' id='wp-404-$k' value='$k' /></td><td><label for='wp-404-$k'><code>$k</code></label></td></tr>";
267 }
268 echo '</tbody></table></div>';
269 echo '<p class="submit"><input class="button button-primary" type="submit" name="add-to-410-list" value="Add selected entries to 410 list" /></p>';
270 }
271 ?>
272 </form>
273
274 <h3>Manually add URLs</h3>
275 <form action="" method="post">
276 <p>You can manually add items to the list by entering them below. Please enter one <strong>fully qualified</strong> URL per line.</p>
277 <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>
278 <textarea name="links_to_add" rows="8" cols="80"></textarea>
279 <p class="submit"><input class="button button-primary" type="submit" name="add-to-410-list" value="Add entries to 410 list" /></p>
280 </form>
281
282 <h3>410 reponse message</h3>
283 <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>
284 <?php
285 if( locate_template( '410.php' ) )
286 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>';
287 else
288 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>';
289 ?>
290 </div>
291 <script>
292 jQuery(function($){
293 $("#wp-gone-captcha-settings input").change( function(){
294 $("#message").slideUp('slow');
295 });
296 $("#select-all-410, #select-all-404").change(function() {
297 var el = $(this);
298 el.closest("table").find("tbody input").prop("checked", el.is(":checked"));
299 });
300 });
301 </script>
302 <?php
303 }
304
305 private function is_valid_url ( $link ) {
306 // Determine whether WP will handle a request for this URL
307 $home = home_url();
308 if( strpos( $link, $home ) !== 0 )
309 return false;
310
311 if( !$this->permalinks ) {
312 $req = preg_replace( '^|' . preg_quote( $home, '|' ) . '/?|' , '', $link );
313 if( strlen( $req ) && $req[0] != '?' ) // this is a pretty permalink, but pretty permalinks are disabled
314 return false;
315 }
316
317 return true;
318 }
319
320 function note_inserted_post( $id ) {
321 $post = get_post( $id );
322
323 if( 'revision' == $post->post_type || 'draft' == $post->post_status )
324 return;
325
326 // Check our list of URLs against the new/updated post's permalink, and if they match, scratch it from our list
327 $created_links = array();
328
329 $created_links[] = rawurldecode( get_permalink( $id ) );
330 $created_links[] = get_post_comments_feed_link( $id ); // back compat
331
332 if( $this->permalinks )
333 $created_links[] .= $created_links[0] . '*';
334
335 foreach( $created_links as $link )
336 $this->remove_link( $link );
337 }
338
339 function check_for_410() {
340 // Don't mess if Wordpress has found something to display
341 if( !is_404() )
342 return;
343
344 $links = $this->get_links();
345 $req = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
346 $req = rawurldecode( $req );
347
348 foreach( $links as $link ) {
349 if( @preg_match( $link->gone_regex, $req ) ) {
350 define( 'DONOTCACHEPAGE', true ); // WP Super Cache and W3 Total Cache recognise this
351 status_header( 410 );
352 do_action( 'wp_410_response' ); // you can use this to customise the response message
353
354 if( ! locate_template( '410.php', true ) )
355 echo 'Sorry, the page you requested has been permanently removed.';
356
357 exit;
358 }
359 }
360
361 // no hit, log 404
362 $this->add_link( $req, true );
363 }
364 }
365
366 new WP_410();
367