| 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 |
|