Never-Ending Pagination in WordPress

Take your site’s slideshows to the next level with the following:

// Slightly different way of getting a previous post than get_adjacent_post(), this 
// function will grab a post in a SINGLE specific category. We'll go ahead and 
// return it as a permalink since that's ultimately what we want anyways. 
function rdc_get_prev_url_in_category( $in_category ) {
	global $post, $wpdb;
	
	// Convert category to id if it's a slug
	if ( ! is_numeric( $in_category ) )
		$in_category = get_category_by_slug( $in_category )->term_id;

	// This query will grab the preceding post id in $in_category
	$query = $wpdb->prepare( "
		SELECT p.ID FROM $wpdb->posts AS p 
		INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id 
		INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id 
		AND tt.taxonomy = 'category' 
		AND tt.term_id IN (%d) 
		WHERE p.ID < %d 
		AND p.post_type = 'post' 
		AND p.post_status = 'publish' 
		ORDER BY p.ID DESC LIMIT 1 
	", $in_category, $post->ID );
	
	// Formulate cache key and see if it exists
	$query_key = 'rdc_previous_post_' . md5( $query );
	$result = wp_cache_get( $query_key );

	// Cache key exists so lets use it
	if ( false !== $result ) {
		if ( $result )
			$prev_post = get_post( $result );
		return get_permalink( $prev_post->ID );
	}
	
	// Cache key didn't exist, lets run a new query
	$result = $wpdb->get_var( $query );

	// Query failed for some reason, probably this is
	// the first post in the category. Go home instead.
	if ( null === $result )
		return get_bloginfo( 'home' );
	
	// Save query result for use later
	wp_cache_set( $query_key, $result );
	
	// Return the permalink of the resulting post id
	if ( $result ) {
		$prev_post = get_post( $result );
		return get_permalink( $prev_post->ID );
	}
	
	// Something crazy happened to get here, but you 
	// never know ...
	return get_bloginfo( 'home' );	
}

The next part actually filters the wp_link_pages arguments and decides where to put a next button where one didn’t previously exist. I also threw in an additional previous button, which uses some javascript to take the user back to the previous page. There’s probably some creative logic you can write to do it without javascript, but that’s probably unnecessary in today’s world.

function rdc_filter_wp_link_pages_args( $r ) {
	global $page, $numpages;
	
	// These links should match the design of your existing links
	// and you should change 12345 to your slideshow category
			
	// If last page of a slideshow, show a "next" button where there wouldn't normally be one
	if ( $r['next_or_number'] == 'next' &amp;&amp; $page == $numpages &amp;&amp; '' == $r['previouspagelink'] ) {
		$repl_next_link = '<a href="' . rdc_get_prev_url_in_category( 12345 ) . '">NEXT &amp;raquo;</a>';
		echo $r['before'] . $repl_next_link . $r['after'];
	}
		
	// If first page of a slideshow, showing "back" button where there wouldn't normally be one 
	if ( $r['next_or_number'] == 'next' &amp;&amp; $page == 1 &amp;&amp; '' == $r['nextpagelink'] ) {
		$repl_prev_link = '<a onclick="window.history.back();">&amp;laquo; PREVIOUS</a>';
		echo $r['before'] . $repl_prev_link . $r['after'];
	}
		
	return $r;
}
add_filter( 'wp_link_pages_args', 'rdc_filter_wp_link_pages_args' );

WP-Config Options You Probably Didn’t Know About

1. Override options “siteurl” and “home”

define('WP_SITEURL', 'http://your-sites-address.com');
define('WP_HOME', 'http://your-sites-address.com');

2. Revisions

define('WP_POST_REVISIONS', FALSE);
define('WP_POST_REVISION', 3);

3. AutoSave Interval

define('AUTOSAVE_INTERVAL', 160);

4. New wp-content location

define( 'WP_CONTENT_DIR', $_SERVER['DOCUMENT_ROOT'] . '/somewhere/wp-content' );
define( 'WP_CONTENT_URL', 'http://server.com/somewhere/wp-content');
define( 'WP_PLUGIN_DIR', $_SERVER['DOCUMENT_ROOT'] . '/somewhere/wp-content/plugins' );
define( 'WP_PLUGIN_URL', 'http://server.com/somewhere/wp-content/plugins');

5. Manage cookie options

define('COOKIE_DOMAIN', 'www.server.com');
define('COOKIEPATH', 'www.server.com/' );
define('SITECOOKIEPATH', 'www.server.com/' );
define('ADMIN_COOKIE_PATH', SITECOOKIEPATH . 'wp-admin');

6. Debug options

define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', false);
define('SCRIPT_DEBUG', true); // Enables debugging of WP's built-in Javascript
define('SAVEQUERIES', true); // Saves database queries to $wpdb->queries array for analysis (will slow down site)

7. Memory Limit

define('WP_MEMORY_LIMIT', '64M'); // Probably will be overridden by server settings, but worth trying if you get a memory error

8. Cache

define('WP_CACHE', true); // Basically just includes drop-in advanced-cache.php

9. Custom wp_users and wp_user_meta tables

define('CUSTOM_USER_TABLE', $table_prefix.'a_new_users_table');
define('CUSTOM_USER_META_TABLE', $table_prefix.'a_new_usermeta_table');

10. Override default file permission

define('FS_CHMOD_DIR', (0755 &amp;amp; ~ umask())); // Untested - use at your own risk...
define('FS_CHMOD_FILE', (0644 &amp;amp; ~ umask())); // Untested - use at your own risk...

11. FTP and SFTP Constants

define('FS_METHOD', 'ftpext');
define('FTP_BASE', '/path/to/wordpress/');
define('FTP_CONTENT_DIR', '/path/to/wordpress/wp-content/');
define('FTP_PLUGIN_DIR ', '/path/to/wordpress/wp-content/plugins/');
define('FTP_PUBKEY', '/home/username/.ssh/id_rsa.pub');
define('FTP_PRIKEY', '/home/username/.ssh/id_rsa');
define('FTP_USER', 'username');
define('FTP_PASS', 'password');
define('FTP_HOST', 'ftp.example.org:21');

12. Proxy access

define('WP_HTTP_BLOCK_EXTERNAL', false);
define('WP_ACCESSIBLE_HOSTS', 'api.wordpress.org');

Delete All Tags With 0 Posts

Delete all tags with zero posts tagged. Can also be used to delete all tags with one post. Or other amounts … Use at your own risk. From the MySQL command line. Make sure to BACK UP YOUR DATABASE first.

DELETE a,c FROM dbname.wp_terms AS a
LEFT JOIN dbname.wp_term_taxonomy AS c ON a.term_id = c.term_id
LEFT JOIN dbname.wp_term_relationships AS b on b.term_taxonomy_id = c.term_taxonomy_id
WHERE ( c.taxonomy = 'post_tag' AND c.count = 0 );

On the last line, you could also do c.count < 3 which would remove all tags with less than three posts. Be creative.

Enjoy!

SQL_CALC_FOUND_ROWS and no_found_rows

By default, the WordPress SQL query that selects posts from the database will use “SELECT SQL_CALC_FOUND_ROWS …”. This is mainly for pagination purposes, to speed up subsequent queries for the next set of results. Writing the query that way certainly helps speed up the site as a whole, but is useless in some circumstances.

Remedy: Look through your theme and plugin code for WP_Query objects. On queries that don’t need pagination, add a new query variable to the mix that looks like this:

$a_faster_wp_query = new WP_Query( array(
	'foo' => 'bar',
	'more_foo' => 'extra_bar',
	'no_found_rows' => true,
));

(The magic is on line 4 – it must be passed as an actual boolean, not a string)

Use this tidbit on every WP_Query that doesn’t need pagination to make WordPress omit the “SQL_CALC_FOUND_ROWS” from it’s query, thus speeding up the post selection query.

WordPress Hardening 101

WordPress Security
With the recent public exploit of W3 Total Cache and the release of WordPress 3.5, it’s high time to tighten up security across the board on your WordPress blog. Here are a few things you can do to clean things up.

File Permissions

Perhaps one of the most important things you can do is clean up your file permissions. A lot of modification, use, and upgrading can cause your permissions to go askew. Here are two simple commands you can issue from the linux prompt which quickly and easily fix up file permissions recursively:

find /var/www/wordpress-root-directory/ -type d -exec chmod 755 {} ;

This changes all of your directories to 755, which is what WordPress recommends. Next:

find /var/www/wordpress-root-directory/ -type f -exec chmod 644 {} ;

This changes all of your files to 644, which is also what WordPress recommends. Lastly:

chmod 440 wp-config.php

Issue this command on wp-config.php to ensure that it’s only readable and writable by you and the web server.

Updates

Make sure to update ALL of the items that can be updated. This includes the WordPress core, all plugins, and all themes (even the ones you’re not using). This helps to ensure that all known security patches have been applied. Even unused theme files can supply the needed security holes that hackers take advantage of.

Remove Unnecessary Items

If you have unneeded themes or plugins, remove them. It’s just extra overhead, and provides more gateways for exploitation. Also, any scripts, files, or other things that aren’t needed for WordPress should be removed unless they’re needed for some other necessary function.

Change Passwords

Go ahead and change ALL of your passwords – FTP, WordPress, cPanel, etc. Any account related to your WordPress install can provide an attacker with a gateway to your site. Use the obvious password length and character rules here – it’s incredibly easy to use a computer program to guess every word in the dictionary against your site, so use non-dictionary terms in combination with symbols and numbers.

Disallow File Editing

Add the following to your wp-config.php file to remove the file editors from the wp-admin area. These features are terrible file editors and can lead to exploitation if left active.

define( 'DISALLOW_FILE_EDIT', true );

In my opinion, the file editors have little practical use and should be disabled in all production environments.

Change Default Salts

Go to this link to generate custom salts for your site if you haven’t done so yet. These keys should be updated in wp-config.php.

Backups

You are creating a regular backup and storing it off-site, right? No? Well get started. Most hosts provide a convenient way of doing this. If not, try a search for “backup wordpress” and you’ll find tons of literature on the subject. Believe me, nothing sucks worse than getting hacked or having a server crash, and not having a backup. It’s catastrophic to lose all of your hard work. Here’s how to create a full backup from the bash prompt under linux.

Navigate to your document root. This is usually the directory directly below your WordPress directory. Issue the following command:

tar czf filesystem-backup.tar.gz httpdocs

Depending upon the size of your site, this could take a few moments or it could be instant. It will create a compressed archive called “filesystem-backup.tar.gz” containing all of your site’s files. Copy this archive file and store offsite. Next, you will want to backup your database. Hopefully your host has the mysqldump utility installed. Just issue the following command tailored to your environment:

mysqldump [-h dbserver] -u [dbuser] -p[dbpass] [dbname] > database-backup.sql

The “-h dbserver” part only needs to be used if you have an external database server (non-localhost). Also, remember there should not be a space between the “-p” switch and the actual password. Copy “database-backup.sql” along with the filesystem archive and you have everything needed to restore all aspects of your WordPress site!

In Summary

Most of these items need to be done at regular intervals to minimize your chance of compromise. My biggest recommendation is to keep everything updated. Any major security issues should be addressed promptly by the WordPress core team, plugin developers, or your theme’s author. This is by no means a definitive guide on WordPress security – Tweet me @CodeNinjaRich if you need guidance, advice, or consulting services. Good luck!

Merry Christmas, Now Patch Your WordPress Sites!

W3 Total Cache WordPress exploit

Thanks to the release of http://seclists.org/fulldisclosure/2012/Dec/242 on Christmas Eve, I’ve spent my holiday working security for several sites.

This exploit allows an attacker to take advantage of the “open” nature of W3 Total Cache’s cache files to extract password hashes from the database cache. I agree with Jason on this – why did the author of W3TC keep these directories open? The plugin already modifies .htaccess, why couldn’t they simple add

"Options -Indexes"

for the cache directories and keep everything a bit tighter? This security “misconfiguration” was not documented by W3TC or WordPress (that I could find) and it could lead to a catastrophic security breach. Since the potential bug is caused solely by W3TC, I believe it’s their responsibility to add the necessary fix, and not leave it up to site admins.

After further investigation, the fix suggested by Jason does not work on my servers (don’t know why). Here is my fix, which does the trick and causes w3-total-fail to totally fail.

Navigate to your /wp-content/w3tc folder and add a new .htaccess. Inside it, add the following:

<Files *>
order deny,allow
deny from all
</Files>

Restart your web server for good measure with

/etc/init.d/httpd restart

or the equivalent for your server. Jason’s tool will no longer work against your site, but go ahead and check it once again for good measure. The tool should still show “Attempting…” repetitively, but this time it will iterate through the directories at a much faster pace because it can’t access the contents. No more hashes will appear.

I’ve pondered upon the topic of W3 Total Cache’s security for quite some time, and I’m well aware of this and a few other possible issues with W3TC. I’m surprised that it’s taken this long for someone to have the same thought and attempt to exploit WordPress using this concept.

Anyways, WP-Admins should download this and check the security of their sites and patch their sites accordingly. Hopefully Frederick Townes will release a self-patching version of W3TC really soon, as many WordPress sites heavily rely on this plugin and I’m sure most admins aren’t even aware of this security threat. Merry Christmas!

Rewrite WP Attachment Images on the Fly

server-patch-panel

At first, this may not seem to have any real-world use, but consider this: You need to set up an independent development environment for your site, and you’d rather not copy gigabytes of files from your production server’s wp-content directory. Without the following function, keeping everything synced could turn into another full-time job, especially if your editorial staff is posting hundreds upon hundreds of articles each day.

Here’s how it works: This filter interrupts the output of wp_get_attachment_image and replaces the returned image tag’s src attribute with your production server’s url, therefore effectively telling the page to read images from a different server than the page is served from.

So here’s the WordPress code that makes it all happen. Put it in a file in your development server’s mu-plugins directory so it doesn’t get overwritten with theme synchronizations. Remember to swap out “yourserver.com” and “dev.yourserver.com” with appropriate values.

<?php

function rewrite_images( $atts ) {
	$atts['src'] = str_replace( 'dev.yourserver.com', 'yourserver.com', $atts['src'] );
	return $atts;
}
add_filter( 'wp_get_attachment_image_attributes', 'rewrite_images' );

?>

What other creative uses can you think of for this snippet? Leave me a comment below!

Purge All WordPress Users of a Particular Role

Bulk Delete WordPress Users

I needed to delete all of the subscriber level users on one of my sites recently, and couldn’t find an easy way to do it. My solution? Write some code. It was definitely quicker and easier to run this snippet than to manually delete over 7,700 users from my database. Drop this code into an admin page where you can run it as an administrator, and watch it go to work!

If you need to delete authors, editors, or some other role, simple switch “subscriber” on line 3 to your chosen role. Note: it must be a valid WordPress role registered in your site. This code leaves users with posts in the database. If you would like to remove users that have posts, remove the conditional on line 7 and it’s corresponding closing bracket on line 13. Be sure to modify the wp_delete_user function call on line 8 with it’s second argument – this will keep those user’s posts online and attribute them to whatever user ID you specify in the second argument.

<?php 

$all_users = get_users( array( 'role' => 'subscriber' ) );

foreach( $all_users as $single_user ) {
	$users_posts = get_posts( array( 'author' => $single_user->ID ) );
	if( !$users_posts ) {
		if( wp_delete_user( $single_user->ID ) ) {
			echo 'User ' . $single_user->ID . ' deleted.<br />';
		} else {
			echo 'Delete user ' . $single_user->ID . ' failed!<br />';
		}
	}
}

?>

Disclaimer: This code is fast and aggressive. There is no “undoing” this operation. Use this at your own risk, data is NOT recoverable. Also, don’t believe everything you read online … you cannot perform the same function with raw MySQL without completely hosing your database – so don’t even try unless you REALLY know what you’re doing.