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