Prevent WordPress from deleting a post

At first glance it seems as though there are no hooks or filters in WordPress for preventing a post/page deletion. But you can do it by filtering user_has_cap (short for user has capability). This is a very powerful filter, and you can use it to block almost anything in WordPress. It has three parameters:

  • $allcaps (an array of all the capabilities, each one set to true or false)
  • $caps (an array of the capabilities being requested by the current operation)
  • $args (an array of arguments relevant to this operation).
  • When a post is being deleted, $args is set to array ('delete_post', $user_id, $post_id). The capabilities required to allow the deletion are stored in the array $caps, and will vary depending on what type of post is being deleted (e.g. ‘delete_published_posts’). Each capability in $caps corresponds to an item in $allcaps. To prevent the post being deleted, all we need to do is modify $allcaps by setting one of the values listed in $caps to false (e.g. $allcaps[$caps[0]] = false).

    As an example, the following code prevents the last published page of a site being deleted.

    add_filter ('user_has_cap', 'wpcs_prevent_last_page_deletion', 10, 3);
    
    function wpcs_prevent_last_page_deletion ($allcaps, $caps, $args) {
    	global $wpdb;
    	if (isset($args[0]) && isset($args[2]) && $args[0] == 'delete_post') {
    		$post = get_post ($args[2]);
    		if ($post->post_status == 'publish' && $post->post_type == 'page') {
    			$query = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type = %s";
    			$num_posts = $wpdb->get_var ($wpdb->prepare ($query, $post->post_type));
    			if ($num_posts < 2)
    				$allcaps[$caps[0]] = false;
    		}
    	}
    	return $allcaps;
    }
    

Using the non-minified version of jQuery in WordPress

If you’re developing in WordPress, you’ll probably have define('SCRIPT_DEBUG', true); in your wp-config.php file, which ensures that WordPress does not load minified javascript files. Unfortunately, because of it’s large size a non-minified version of jQuery is not included in WordPress, which means debugging jQuery related issues can be a real pain. Thankfully, it’s easy to fix, and the code below can be easily be added to your custom developer plugin. The code simply checks which version of jQuery is currently registered, then deregisters it and re-registers Google’s unminified version.

if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) {
	add_action ('wp_enqueue_scripts', 'wpcs_init');
	add_action ('admin_enqueue_scripts', 'wpcs_init');
}

function wpcs_init() {
	global $wp_scripts;
	if (isset($wp_scripts->registered['jquery']->ver)) {
		$jquery_version = $wp_scripts->registered['jquery']->ver;
		wp_deregister_script ('jquery');
		wp_register_script ('jquery', "http://ajax.googleapis.com/ajax/libs/jquery/{$jquery_version}/jquery.js");
	}
}

How to make sure the correct domain is used in WPML

If you use the WPML multilingual plugin for WordPress, and have each language on a different domain, you may have noticed that WPML does not ensure that all URLs in your page point to the correct domain. You might notice, for example, that images are pulled from the main domain, even when you’re accessing a page on another language’s domain. It’s rare that this causes problems for your site, (although it theoretically could if you have .htaccess rules in place to stop hot-linking, or if you have enabled https for one domain and not for the others), but it’s likely to have a small negative impact on your SEO.

If you want to fix this, you need to filter all of the WordPress functions that return a URL. There are lots of them, and I haven’t collected them all together. But there’s enough here to give you the picture, and if you find something is not being filtered it should be easy enough to find the appropriate function and add it to the code.

//Add all the necessary filters. There are a LOT of WordPress functions, and you may need to add more filters for your site.
add_filter ('site_url', 'wpcs_correct_domain_in_url');
add_filter ('get_option_siteurl', 'wpcs_correct_domain_in_url');
add_filter ('stylesheet_directory_uri', 'wpcs_correct_domain_in_url');
add_filter ('template_directory_uri', 'wpcs_correct_domain_in_url');
add_filter ('post_thumbnail_html', 'wpcs_correct_domain_in_url');
add_filter ('plugins_url', 'wpcs_correct_domain_in_url');

/**
* Changes the domain for a URL so it has the correct domain for the current language
* Designed to be used by various filters
* 
* @param string $url
* @return string
*/
function wpcs_correct_domain_in_url($url){
    if (function_exists('icl_get_home_url')) {
        // Use the language switcher object, because that contains WPML settings, and it's available globally
        global $icl_language_switcher;
        // Only make the change if we're using the languages-per-domain option
        if (isset($icl_language_switcher->settings['language_negotiation_type']) && $icl_language_switcher->settings['language_negotiation_type'] == 2)
            return str_replace(untrailingslashit(get_option('home')), untrailingslashit(icl_get_home_url()), $url);
    }
    return $url;
}

Adding a favicon to your site (including at the root)

Adding a favicon is an important finishing touch to building your site. If your theme doesn’t support favicons, you can add a favicon by adding the following code to your functions.php :

add_action('wp_head', 'emw_add_favicon');

function emw_add_favicon () {
	echo '<link rel="shortcut icon" type="image/x-icon" href="'.get_stylesheet_directory_uri().'favicon.ico">'."\r\n";
	echo '<link rel="icon" type="image/x-icon" href="'.get_stylesheet_directory_uri().'favicon.ico">'."\r\n"; // Shouldn't be necessary, but added for maximum browser compatibility
}

If your theme already supports favicons, chances are there’s a filter in place so you can provide your own. For example, this is how to add a favicon to the Genesis theme:

add_filter ('genesis_pre_load_favicon', 'emw_add_genesis_favicon');

function emw_add_genesis_favicon ($text) {
	return get_stylesheet_directory_uri()."favicon.ico";
}

Or, if you prefer to keep your code ultra-compact:

add_filter ('genesis_pre_load_favicon', create_function ('', 'return get_stylesheet_directory_uri()."favicon.ico";'));

There’s one final problem to solve. Many browsers look for the favicon in your domain root: i.e. at www.domain.com/favicon.ico We could of course place the icon file there, but there’s no need to. A few lines of WordPress code will redirect browser requests to the right place:

add_filter('rewrite_rules_array', 'emw_favicon_rewrite');

function emw_favicon_rewrite($rewrite_rules) {
    $new_rules = array('favicon.ico' => get_stylesheet_directory_uri()."favicon.ico");
    $rewrite_rules = $new_rules + $rewrite_rules;
    return $rewrite_rules;
}

This function works by adding a favicon line to the array of rewrite rules, every time the array is updated. This means that you’ll need to force an update of all the rules before this extra rule will be added – and the easiest way to do that is to change your permalink structure, and then immediately change it back.

Don’t edit child themes – use grandchild themes!

Child themes are the easiest way to style WordPress sites. Rather than create a site from scratch, you can create a theme that shares most of its code and styling with a parent theme. The benefits of creating a child theme instead of editing the main theme is that if the main theme gets updates, none of your changes are lost.

But what if you need to modify a child theme? If you’re working with a Framework, like Genesis, it’s very likely you are already using a child theme – and editing that brings about all the disadvantages of editing a parent theme – you lose your changes if the theme is ever updated.

The solution is surprisingly simple. Instead of editing the child theme, create a grandchild theme. It’s very similar to creating a child theme, except you do it via a plugin. You add your custom functions to the plugin, just as you normally would in functions.php (though remember your plugin will be called much earlier than functions.php, so you’ll need to make sure that any code in your plugin only runs when an action is fired). Use the wp_register_style and wp_enqueue_style functions to add your CSS (by default a grandchild theme will add your CSS to the CSS of the parent or child theme, but you can change this behaviour by using the wp_dequeue_style function). Finally filter the result of the get_query_template function to control which PHP files are executed when a page is requested.

A typical grandchild theme might look like this:

/*
Plugin Name: Grandchild Theme
Plugin URI: http://www.wp-code.com/
Description: A WordPress Grandchild Theme (as a plugin)
Author: Mark Barnes
Version: 0.1
Author URI: http://www.wp-code.com/
*/

// These two lines ensure that your CSS is loaded alongside the parent or child theme's CSS
add_action('wp_head', 'wpc_theme_add_headers', 0);
add_action('init', 'wpc_theme_add_css');

// This filter replaces a complete file from the parent theme or child theme with your file (in this case the archive page).
// Whenever the archive is requested, it will use YOUR archive.php instead of that of the parent or child theme.
add_filter ('archive_template', create_function ('', 'return plugin_dir_path(__FILE__)."archive.php";'));

function wpc_theme_add_headers () {
	wp_enqueue_style('grandchild_style');
}

function wpc_theme_add_css() {
	$timestamp = @filemtime(plugin_dir_path(__FILE__).'/style.css');
	wp_register_style ('grandchild_style', plugins_url('style.css', __FILE__).'', array(), $timestamp);
}

// In the rest of your plugin, add your normal actions and filters, just as you would in functions.php in a child theme.

The two actions to add the CSS should be familiar to you (although you may also want to read this post). The filter is the important function. You can create as many of these as you wish, for different pages. Supported pages include:

  • index
  • 404
  • archive
  • author
  • category
  • tag
  • taxonomy
  • date
  • home
  • front_page
  • page
  • paged
  • search
  • single
  • attachment

If you want more control before deciding which template file to give to WordPress, create a proper function and insert your logic code into it:

add_filter ('archive_template', 'emw_archive_template', 10, 1);

function wpc_archive_template ($suggested_templates) {
	// Your logic here
	if ($test_passed)
		return $template_file;
	else
		return $alternative_template_file;
}

Synchronize a menu with your page hierarchy

WordPress’s menu feature is a fantastic way of creating a hierarchical menu, but unfortunately you still have to keep your pages in their own, separate hierarchy, using a horribly cumbersome interface, or a special plugin. The code snippet below will adjust your page hierarchy to match your menu, every time you edit your menu (so long as javascript is turned on). It does this by changing post_parent and menu_order for any pages that are no longer in sync with the menu. Pages that are not in the menu are not changed. To use the function in your own plugin, on the first line, you’ll need to enter the id of the menu you want to keep in sync.

The function gets the details of the menu by calling get_term_by, whilst wp_get_nav_menu_items returns all of the items in the menu. Finally, wp_update_post modifies any pages that need to be changed.

<?php
add_action ('wp_update_nav_menu', 'emw_create_hierarchy_from_menu', 10, 2);

function emw_create_hierarchy_from_menu($menu_id, $menu_data = NULL) {
	if ($menu_id != 1)  // You should update this integer to the id of the menu you want to keep in sync
		return;
	if ($menu_data !== NULL) // If $menu_date !== NULL, this means the action was fired in nav-menu.php, BEFORE the menu items have been updated, and we should ignore it.
		return;
	$menu_details = get_term_by('id', $menu_id, 'nav_menu');
	if ($items = wp_get_nav_menu_items ($menu_details->term_id)) {
	    // Create an index of menu item IDs, so we can find parents easily
		foreach ($items as $key => $item)
    		$item_index[$item->ID] = $key;
    	// Loop through each menu item
		foreach ($items as $item)
			// Only proceed if we're dealing with a page
			if ($item->object == 'page') {
				// Get the details of the page
				$post = get_post($item->object_id, ARRAY_A);
				if ($item->menu_item_parent != 0)
					// This is not top-level menu items, so we need to find the parent page
					if ($items[$item_index[$item->menu_item_parent]]->object != 'page') {
						// The parent isn't a page. Queue an error message.
						global $messages;
						$messages[] = '<div id="message" class="error"><p>' . sprintf( __("The parent of <strong>%1s</strong> is <strong>%2s</strong>, which is not a page, which means that this part of the menu cannot sync with your page hierarchy.", ETTD), $item->title, $items[$item_index[$item->menu_item_parent]]->title) . '</p></div>';
						$new_post['post_parent'] = new WP_Error;
					} else
						// Get the new parent page from the index
						$new_post['post_parent'] = $items[$item_index[$item->menu_item_parent]]->object_id;
				else
					$new_post['post_parent'] = 0; // Top-level menu item, so the new parent page is 0
				if (!is_wp_error ($new_post['post_parent'])) {
					$new_post['ID'] = $post['ID'];
					$new_post['menu_order'] = $item->menu_order;
					if ($new_post['menu_order'] !== $post['menu_order'] || $new_post['post_parent'] !== $post['post_parent'])
						// Only update the page if something has changed
						wp_update_post ($new_post);
				}
			}
	}
}
?>

Automatically add Lorem Ipsum text to blank WordPress pages

If you’re creating a new WordPress theme, it can be helpful to have some filler text in your pages. The snippet below adds a few paragraphs of lorem ipsum text to any blank post or page by filtering the_content. The text is retrieved from the www.lipsum.com site, and is cached, site-wide, for one hour using the set_transient and get_transient functions.

add_filter ('the_content', 'emw_custom_filter_the_content');

/**
* Returns Lorem Ipsum text for blank pages
* 
* @param string $content - the page's current contents
* @return string
*/
function emw_custom_filter_the_content ($content) {
	if ($content == '') {
		if ($c = get_transient ('lipsum'))
			return $c;
		$content = wp_remote_get ('http://www.lipsum.com/feed/json');
		if (!is_wp_error($content)) {
			$content = json_decode (str_replace ("\n", '</p><p>', $content['body']));
			$content = '<p>'.$content->feed->lipsum.'</p>';
			set_transient ('lipsum', $content, 3600); // Cache the text for one hour
			return $content;
		}
	} else
		return $content;
}

Changing the settings for Genesis Reponsive Slider to allow multiple slideshows

Out of the box, the Genesis Responsive Slider can only be used in one way on your site because it’s options are set in an options panel. However, you can hook in to every setting, to create different sliders on different pages (although this code will not allow you to have different sized slideshows on every page – we’ll cover that in a future post).

I hook to the ‘wp’ action, because that is late enough for you to be able to calculate what slideshow parameters you may want, but early enough that the slideshow won’t have been generated yet. The parameters are changed by filtering genesis_pre_get_option_*, and a very useful PHP function called create_function.

add_action ('wp', 'emw_change_slide_show');

function emw_change_slide_show() {
	$slide_show_vars = array(
		'slideshow_timer' => 8000,
		'slideshow_delay' => 1500,
		'slideshow_arrows' => 1,
		'slideshow_pager' => 0,
		'slideshow_loop' => 1,
		'slideshow_height' => 250,
		'slideshow_width' => 978,
		'slideshow_effect' => 'fade',
		'slideshow_title_show' => 1,
		'slideshow_excerpt_show' => 0,
		'slideshow_excerpt_width' => 30,
		'location_vertical' => 'bottom',
		'location_horizontal' => 'right',
		'slideshow_hide_mobile' => 1,
		'include_exclude' => 'include',
		'post_type' => 'page',
		'post_id' => '4, 7, 9'
	);
	foreach ($slide_show_vars as $option => $value)
		add_filter ("genesis_pre_get_option_{$option}", create_function ('', "return '{$value}';"));
}

If you want to change the size of the slideshow, as we’ve done for this example, you’ll need a few additional lines (and you’ll need to re-generate WordPress thumbnails after you’ve made the change). Although the code on this page allows you to change the size of the slideshow from the default, you’ll need to have the same size on every page:

add_action ('wp', 'emw_change_slider_image_size');

function emw_change_slider_image_size() {
    add_image_size ('slider', 978, 250, true);
}

Finally, if you want to remove the Responsive Slider menu from the admin (because those settings are now overridden):

add_action ('init', 'emw_remove_responsive_slider_menu');

function emw_remove_responsive_slider_menu() {
	if (is_admin())
		remove_action('admin_menu', 'genesis_responsive_slider_settings_init', 15);
}

Caching a downloaded file

It’s often useful to download external files such as RSS feeds, Twitter feeds and so on. But you’ll also want those files cached, so that you’re not downloading them on every page view. Thankfully, WordPress provides two functions that make downloading and caching files very easy — wp_remote_get and set_transient.

The function below tries to act intelligently — the file will be cached for the time specified in $cache_time, which defaults to the time given by the ‘Cache-control’ header; and if the cache-control time is overridden, the ‘cache-control’ and ‘expires’ headers will be modified before being returned/cached, so that you can easily output the most appropriate headers if you need to.

/**
* Downloads a URL, or returns it from the cache
* 
* The function tries to act intelligently:
* 	(1) The file will be cached for the time specified in $cache_time, which defaults to the time given by the 'Cache-control:' header.
* 	(2) If cache-control time is overridden, the 'cache-control' and 'expires' headers will be modified before being returned/cached, so that you can easily output appropriate headers if you need to.
* 
* @param string $url - the URL to be downloaded or returned
* @param integer $suggested_cache_time - the time to cache the file for (in seconds). If the time given by the 'Cache-control:' header is longer, this value will be overridden. If no time is provided by this variable or by the header, then the time will be set to 1 day
* @param boolean $also_return_headers
* @return mixed - if an error occurs, returns WP_ERROR. Otherwise, if $also_return_headers is true, returns an array with the keys 'body', 'headers', 'response' and 'cookies', otherwise returns the body as a string. See the documentation for wp_remote_get for examples.
*/
function emw_get_cached_url ($url, $suggested_cache_time = 0, $also_return_headers = false) {
	$name = 'egcu-'.md5($url); // A short, unique name for the file to be cached
	// Return from the cache if possible
	if ($item = get_transient ($name)) {
		$v = unserialize(base64_decode($item));
		if ($also_return_headers)
			return unserialize(base64_decode($item));
		else
			return $v['body'];
	} else {
		// Download the file
		$download = wp_remote_get ($url, array ('timeout' => 30, 'sslverify' => false));
		// Check for error
		if (is_wp_error ($download))
			return $download;
		else {
			// Calculate the cache time from the headers
			$actual_cache_time = $suggested_cache_time;
			if (isset($download['headers']['cache-control']) && ($start = strpos($download['headers']['cache-control'], 'max-age=')+8) !== FALSE) {
				$headers_cache_time = substr($download['headers']['cache-control'], $start);
				if (($a = strpos($headers_cache_time, ',')) !== FALSE)
					$headers_cache_time = substr($headers_cache_time, 0, $a);
				if ($headers_cache_time > $suggested_cache_time)
					$actual_cache_time = $headers_cache_time;
			}
			// If no cache time is specified in the headers or the $suggested_cache_time variable, set the cache to 1 day
			if ($actual_cache_time == 0)
				$actual_cache_time = 86400;
			// Modify the headers to take account of the new cache time
			if (!isset($download['headers']['date']))
				$download['headers']['date'] = gmdate('D, d M Y H:i:s \G\M\T', $t = time());
			$download['headers']['cache-control'] = "public, max-age=".round($actual_cache_time*1.01);  // Multiplying by 1.01 ensures the browser will cache the file for slightly longer than the server. This ensures that by the time the browser requests the file again, it will have definitely been refreshed.
			$download['headers']['expires'] = gmdate('D, d M Y H:i:s \G\M\T', $t+(round($actual_cache_time*1.01)));
			// Cache the download
			set_transient ($name, base64_encode(serialize($download)), $actual_cache_time);
			// Return what was requested
			if ($also_return_headers)
				return $download;
			else
				return $download['body'];
		}
	}
}

(The data is stored base64 encoded, because I’ve encountered bugs when simply storing it as serialised data.)

How to remove deprecated widgets from the Genesis Framework

The latest version of Genesis has deprecated two widgets, but they still annoyingly show up in the widgets panel even if you’re not using them. Thankfully it’s easy to remove them using WordPress’s unregister_widget function. Just place the following code in your functions.php:

add_action ('widgets_init', 'emw_remove_deprecated_widgets', 15);

function emw_remove_deprecated_widgets() {
	unregister_widget ('Genesis_Widget_Menu_Categories');
	unregister_widget ('Genesis_Menu_Pages_Widget');
}