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

351 lines 13.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: 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
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 = 4; // DB version, since 0.2
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 SMALLINT NOT NULL AUTO_INCREMENT,
41 gone_key VARCHAR(512) NOT NULL,
42 gone_regex VARCHAR(512) NOT NULL,
43 is_404 SMALLINT(1) 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 4
102 if( $options_version < 4 )
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 <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>
186 <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>
187 <form action="" method="post" id="wp-gone-captcha-settings">
188 <table id="wp_410_table" class="form-table"><tbody>
189 <tr><th>Obsolete URLs</th><td>
190 <?php
191 if( empty( $links ) ) {
192 echo '<p>There are currently no obsolete URLs in this list. You can add some manually below.</p>';
193 }
194 else {
195 echo '<p>The following URLs are currently considered obsolete. To remove items from the list, check the corresponding box.</p>';
196 echo '<ul id="wp_gone_old_links" class="indent">';
197
198 $invalid_links_exist = false;
199 foreach( array_keys( $links ) as $k ) {
200 $valid = $this->is_valid_url( $k );
201
202 if( ! $valid )
203 $invalid_links_exist = true;
204
205 $k = htmlspecialchars( $k );
206 $class = $valid ? '' : 'class="invalid"';
207
208 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>";
209 }
210 echo '</ul>';
211
212 if( $invalid_links_exist )
213 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>';
214 }
215 ?>
216 <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>
217 </td></tr>
218 <tr><th>Recent 404 errors</th><td>
219
220 <?php
221 if( empty( $logged_404s ) ) {
222 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>';
223 }
224 else {
225 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>';
226 echo '<ul id="wp_gone_404s" class="indent">';
227 foreach( array_keys( $logged_404s ) as $k ) {
228 $k = htmlspecialchars( $k );
229 echo "<li><input type='checkbox' name='add_404s[]' value='$k' id='404_$k' /> <label for='404_$k'><code>$k</code></label></li>";
230 }
231 echo '</ul>';
232 }
233 ?>
234 </td></tr>
235 <tr><th>Manually add URLs</th><td>
236 <p>You can manually add items to the list by entering them below. Please enter one <strong>fully qualified</strong> URL per line.</p>
237 <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>
238 <textarea name="links_to_add" rows="8" cols="80"></textarea>
239 </td></tr>
240 <tr><th>410 reponse message</th><td>
241 <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>
242 <?php
243 if( locate_template( '410.php' ) )
244 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>';
245 else
246 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.</p>';
247 ?>
248 </td></tr>
249 </tbody></table>
250 <p class="submit"><input class="button-primary" type="submit" name="submit" value="Update settings" /></p>
251 </form>
252 </div>
253 <script>
254 jQuery(document).ready(function($){
255 $('#wp_gone_old_links input').change( function(){
256 $(this).parent().toggleClass('wp_gone_deleted', $(this).is(':checked'));
257 });
258 $("#wp-gone-captcha-settings input").change( function(){
259 $("#message").slideUp('slow');
260 });
261 });
262 </script>
263 <?php
264 }
265
266 private function is_valid_url ( $link ) {
267 // Determine whether WP will handle a request for this URL
268 $home = home_url();
269 if( strpos( $link, $home ) !== 0 )
270 return false;
271
272 if( !$this->permalinks ) {
273 $req = preg_replace( '^|' . preg_quote( $home, '|' ) . '/?|' , '', $link );
274 if( strlen( $req ) && $req[0] != '?' ) // this is a pretty permalink, but pretty permalinks are disabled
275 return false;
276 }
277
278 return true;
279 }
280
281 function note_deleted_post( $id ) {
282 // NOTE: function wp_delete_post calls the delete_post action twice, so we store deleted IDs in an array to avoid repeating
283 // but still allow repeated use of the function for batch deletes
284 static $noted = array();
285 $id = (int) $id;
286
287 if( in_array( $id, $noted ) )
288 return;
289 $noted[] = $id;
290
291 $post = get_post( $id );
292
293 if( 'revision' == $post->post_type )
294 return;
295
296 $link = rawurldecode( get_permalink( $id ) );
297 if( $this->permalinks )
298 $link .= '*'; // catch everything like /page/ and /feed/
299
300 $this->add_link( $link );
301 }
302
303 function note_inserted_post( $id ) {
304 $post = get_post( $id );
305
306 if( 'revision' == $post->post_type || 'draft' == $post->post_status )
307 return;
308
309 // Check our list of URLs against the new/updated post's permalink, and if they match, scratch it from our list
310 $created_links = array();
311
312 $created_links[] = rawurldecode( get_permalink( $id ) );
313 $created_links[] = get_post_comments_feed_link( $id ); // back compat
314
315 if( $this->permalinks )
316 $created_links[0] .= '*';
317
318 $list = get_option( 'wp_410_links_list', array() );
319
320 foreach( $created_links as $link )
321 $this->remove_link( $link );
322 }
323
324 function check_for_410() {
325 // Don't mess if Wordpress has found something to display
326 if( !is_404() )
327 return;
328
329 $links = $this->get_links();
330 $req = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
331 $req = rawurldecode( $req );
332
333 foreach( $links as $link ) {
334 if( @preg_match( $link->gone_regex, $req ) ) {
335 status_header( 410 );
336 do_action( 'wp_410_response' ); // you can use this to customise the response message
337
338 if( ! locate_template( '410.php', true ) )
339 echo 'Sorry, the page you requested has been permanently removed.';
340
341 exit;
342 }
343 }
344
345 // no hit, log 404
346 $this->add_link( $req, true );
347 }
348 }
349
350 new WP_410();
351