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

355 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.7.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('delete_post', array( $this, 'note_deleted_post' ) );
33 add_action('wp_insert_post', array( $this, 'note_inserted_post' ));
34
35 }
36
37 private function install_table() {
38 // remember, two spaces after PRIMARY KEY otherwise WP borks
39 $sql = "CREATE TABLE $this->table (
40 gone_id MEDIUMINT unsigned NOT NULL AUTO_INCREMENT,
41 gone_key VARCHAR(512) NOT NULL,
42 gone_regex VARCHAR(512) NOT NULL,
43 is_404 SMALLINT(1) unsigned NOT NULL DEFAULT 0,
44 PRIMARY KEY (gone_id),
45 KEY is_404 (is_404)
46 );";
47
48 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
49 dbDelta($sql);
50 }
51
52 private function get_links(){
53 global $wpdb;
54 return $wpdb->get_results( "SELECT gone_key, gone_regex FROM $this->table WHERE is_404 = 0", OBJECT_K ); // indexed by gone_key
55 }
56
57 private function get_404s(){
58 global $wpdb;
59 $links = $wpdb->get_results( "SELECT gone_key, gone_regex FROM $this->table WHERE is_404 = 1", OBJECT_K ); // indexed by gone_key
60 // limit how many 404s we store
61 if( count( $links ) > 50 ) {
62 $n = count( $links ) - 50;
63 $wpdb->query( "DELETE FROM `$this->table` WHERE is_404 = 1 ORDER BY gone_id LIMIT $n" );
64 return $this->get_404s();
65 }
66 return $links;
67 }
68
69 private function add_link( $key, $is_404 = false ){ // just supply the link
70 global $wpdb;
71 // build regex
72 $parts = preg_split( '/(\*)/', $key, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
73 foreach( $parts as &$part ) if( '*' != $part )
74 $part = preg_quote( $part, '|' );
75 $parts = str_replace( '*', '.*', $parts );
76 $regex = '|^' . implode( '', $parts ) . '$|i';
77
78 // avoid duplicates - messy but MySQL doesn't allow url-length unique keys
79 if( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `$this->table` WHERE `gone_key` = %s", $key ) ) )
80 return 0;
81
82 return $wpdb->insert( $this->table, array( 'gone_key' => $key, 'gone_regex' => $regex, 'is_404' => intval( $is_404 ) ) );
83 }
84
85 private function convert_404( $key ) {
86 global $wpdb;
87 return $wpdb->query( $wpdb->prepare( "UPDATE `$this->table` SET is_404 = 0 WHERE `gone_key` = %s LIMIT 1", $key ) );
88 }
89
90 private function remove_link( $key ){
91 global $wpdb;
92 return $wpdb->query( $wpdb->prepare( "DELETE FROM $this->table WHERE gone_key = %s", array( $key ) ) );
93 }
94
95 function upgrade_check() {
96 $options_version = get_option( 'wp_410_options_version', 0 );
97
98 if( $options_version == self::db_version ) // nothing to do
99 return;
100
101 // last db change was in version 5
102 if( $options_version < 5 )
103 $this->install_table();
104
105 if( $options_version < 3 ) {
106 $old_links = get_option( 'wp_410_links_list', array() );
107 $new_links = array(); // just a simple array of links
108
109 if( 0 == $options_version ) // links were stored just as links
110 $new_links = array_map( 'rawurldecode', $old_links );
111 elseif( 1 == $options_version ) // links were stored as array( link => regex ), We only need the link
112 $new_links = array_map( 'rawurldecode', array_keys( $old_links ) );
113 else // moved to using the database in db_version 3
114 $new_links = array_keys( $old_links );
115
116 foreach( $new_links as $link )
117 $this->add_link( $link );
118
119 delete_option( 'wp_410_links_list' ); // remove old option
120 }
121
122 update_option( 'wp_410_options_version', self::db_version );
123 }
124
125 function settings_menu() {
126 add_submenu_page('plugins.php', '410 for WordPress', '410 for WordPress', 'manage_options', 'wp_410_settings', array( $this, 'settings_page') );
127 }
128
129 function settings_page() {
130 $links = $this->get_links();
131 $logged_404s = $this->get_404s();
132 $links_to_add = array();
133
134 if ( isset( $_POST['submit'] ) ) {
135 $failed_to_add = array();
136 if( !empty( $_POST['links_to_add'] ) ) {
137 foreach( preg_split( '/(\r?\n)+/', $_POST['links_to_add'], -1, PREG_SPLIT_NO_EMPTY ) as $link ) {
138 if( $this->is_valid_url( $link ) ) {
139 $this->add_link( $link );
140 $links[$link] = $link; // the value doesn't matter, we don't use it here
141 }
142 else {
143 $failed_to_add[] = '<code>' . htmlspecialchars( $link ) . '</code>';
144 }
145 }
146 }
147
148 if( !empty( $_POST['add_404s'] ) ) {
149 foreach( $_POST['add_404s'] as $link ) {
150 $this->convert_404( $link );
151 $links[$link] = $link; // the value doesn't matter, we don't use it here
152 unset( $logged_404s[$link] );
153 }
154 }
155
156 if( !empty( $_POST['old_links_to_remove'] ) ) {
157 foreach( $_POST['old_links_to_remove'] as $key ) {
158 if( isset( $links[$key] ) ) {
159 $this->remove_link( $key );
160 unset( $links[$key] );
161 }
162 }
163 }
164
165 echo '<div id="message" class="updated fade"><p>Options updated.</p></div>';
166
167 if( $failed_to_add )
168 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>';
169 }
170
171 ksort( $links );
172 ksort( $logged_404s );
173 ?>
174 <style>
175 .indent {padding-left: 2em}
176 .wp_gone_deleted label {text-decoration: line-through}
177 label.invalid {color: red}
178 p.invalid { background: #FFEBE8; border: 1px solid red; border-radius: 5px; padding: 5px 10px}
179 #wp_410_table th {font-weight: bold}
180 #wp_410_table tr {border-top: 1px solid #EEE}
181 </style>
182 <?php screen_icon(); ?>
183 <div class="wrap">
184 <h2>410 for WordPress</h2>
185 <?php
186 if( WP_CACHE )
187 echo '<div class="error"><p><strong>It seems that a caching/performance plugin is active on this site. Please note that with most caching plugins, WordPress does not handle requests for pages that are not found on your site (your web server handles them directly), and so this plugin cannot intercept them to issue a 410 response.</strong></div>';
188 ?>
189 <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>
190 <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>
191 <form action="" method="post" id="wp-gone-captcha-settings">
192 <table id="wp_410_table" class="form-table"><tbody>
193 <tr><th>Obsolete URLs</th><td>
194 <?php
195 if( empty( $links ) ) {
196 echo '<p>There are currently no obsolete URLs in this list. You can add some manually below.</p>';
197 }
198 else {
199 echo '<p>The following URLs are currently considered obsolete. To remove items from the list, check the corresponding box.</p>';
200 echo '<ul id="wp_gone_old_links" class="indent">';
201
202 $invalid_links_exist = false;
203 foreach( array_keys( $links ) as $k ) {
204 $valid = $this->is_valid_url( $k );
205
206 if( ! $valid )
207 $invalid_links_exist = true;
208
209 $k = htmlspecialchars( $k );
210 $class = $valid ? '' : 'class="invalid"';
211
212 echo "<li><input type='checkbox' name='old_links_to_remove[]' value='$k' id='old_link_$k' /> <label for='old_link_$k' $class><code>$k</code></label></li>";
213 }
214 echo '</ul>';
215
216 if( $invalid_links_exist )
217 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>';
218 }
219 ?>
220 <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>
221 </td></tr>
222 <tr><th>Recent 404 errors</th><td>
223
224 <?php
225 if( empty( $logged_404s ) ) {
226 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>';
227 }
228 else {
229 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>';
230 echo '<ul id="wp_gone_404s" class="indent">';
231 foreach( array_keys( $logged_404s ) as $k ) {
232 $k = htmlspecialchars( $k );
233 echo "<li><input type='checkbox' name='add_404s[]' value='$k' id='404_$k' /> <label for='404_$k'><code>$k</code></label></li>";
234 }
235 echo '</ul>';
236 }
237 ?>
238 </td></tr>
239 <tr><th>Manually add URLs</th><td>
240 <p>You can manually add items to the list by entering them below. Please enter one <strong>fully qualified</strong> URL per line.</p>
241 <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>
242 <textarea name="links_to_add" rows="8" cols="80"></textarea>
243 </td></tr>
244 <tr><th>410 reponse message</th><td>
245 <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>
246 <?php
247 if( locate_template( '410.php' ) )
248 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>';
249 else
250 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>';
251 ?>
252 </td></tr>
253 </tbody></table>
254 <p class="submit"><input class="button-primary" type="submit" name="submit" value="Update settings" /></p>
255 </form>
256 </div>
257 <script>
258 jQuery(document).ready(function($){
259 $('#wp_gone_old_links input').change( function(){
260 $(this).parent().toggleClass('wp_gone_deleted', $(this).is(':checked'));
261 });
262 $("#wp-gone-captcha-settings input").change( function(){
263 $("#message").slideUp('slow');
264 });
265 });
266 </script>
267 <?php
268 }
269
270 private function is_valid_url ( $link ) {
271 // Determine whether WP will handle a request for this URL
272 $home = home_url();
273 if( strpos( $link, $home ) !== 0 )
274 return false;
275
276 if( !$this->permalinks ) {
277 $req = preg_replace( '^|' . preg_quote( $home, '|' ) . '/?|' , '', $link );
278 if( strlen( $req ) && $req[0] != '?' ) // this is a pretty permalink, but pretty permalinks are disabled
279 return false;
280 }
281
282 return true;
283 }
284
285 function note_deleted_post( $id ) {
286 // NOTE: function wp_delete_post calls the delete_post action twice, so we store deleted IDs in an array to avoid repeating
287 // but still allow repeated use of the function for batch deletes
288 static $noted = array();
289 $id = (int) $id;
290
291 if( in_array( $id, $noted ) )
292 return;
293 $noted[] = $id;
294
295 $post = get_post( $id );
296
297 if( 'revision' == $post->post_type )
298 return;
299
300 $link = rawurldecode( get_permalink( $id ) );
301 if( $this->permalinks )
302 $link .= '*'; // catch everything like /page/ and /feed/
303
304 $this->add_link( $link );
305 }
306
307 function note_inserted_post( $id ) {
308 $post = get_post( $id );
309
310 if( 'revision' == $post->post_type || 'draft' == $post->post_status )
311 return;
312
313 // Check our list of URLs against the new/updated post's permalink, and if they match, scratch it from our list
314 $created_links = array();
315
316 $created_links[] = rawurldecode( get_permalink( $id ) );
317 $created_links[] = get_post_comments_feed_link( $id ); // back compat
318
319 if( $this->permalinks )
320 $created_links[0] .= '*';
321
322 $list = get_option( 'wp_410_links_list', array() );
323
324 foreach( $created_links as $link )
325 $this->remove_link( $link );
326 }
327
328 function check_for_410() {
329 // Don't mess if Wordpress has found something to display
330 if( !is_404() )
331 return;
332
333 $links = $this->get_links();
334 $req = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
335 $req = rawurldecode( $req );
336
337 foreach( $links as $link ) {
338 if( @preg_match( $link->gone_regex, $req ) ) {
339 status_header( 410 );
340 do_action( 'wp_410_response' ); // you can use this to customise the response message
341
342 if( ! locate_template( '410.php', true ) )
343 echo 'Sorry, the page you requested has been permanently removed.';
344
345 exit;
346 }
347 }
348
349 // no hit, log 404
350 $this->add_link( $req, true );
351 }
352 }
353
354 new WP_410();
355