Overhauling roles and capabilities

Read part two here.

Thanks to some Google Summer of Code proposals, there have been some conversations on the wp-hackers list about roles and capabilities, and how we can improve them. It’s important, though, to understand exactly what the current API allows — it’s much more complicated than many realize.

Here’s how an end user generally sees it:

  • A user can be given one of five roles: Administrator, Editor, Author, Contributor, Subscriber.

There’s much more of course behind that. Hopefully, a developer just getting started with WordPress does not notice the long-deprecated user levels. If all goes well, budding developers will also realize that:

  • Each role is made up of a number of capabilities, such as manage_options (only for Administrators), moderate_comments (for Editors and Administrators), and edit_posts (for Contributors, Authors, Editors, and Administrators).

Of course, a more experienced developer, or a blog administrator who has downloaded one of the various capability/role management plugins will also realize that:

  • You can create roles and give those roles a set of capabilities. Example: You can give the Editor role the activate_plugins capability.
  • You can grant individual users a specific capability that their role does not otherwise give them. Example: A user with the Editor role can be given the activate_plugins capability.
  • A user can have capabilities removed from them, negating capabilities they would otherwise have through their role. Example: A user with the Editor role can be stripped of the unfiltered_html capability.

But wait, there’s more:

  • A user can have more than one role. Thus, a user could have both the Editor role, and the Administrator role. (Since all capabilities in the Editor role are also in the Administrator role, then this example would have no effect.)

This last one is not supported by the core WordPress user interface. No publicly released plugins use it (that I know of). In fact, the main use case would be for a membership plugin (which is, incidentally, the rare use case for using your own table).

The problem is, this system cannot scale well due to the overhead. But, since most of our performance woes are from features that aren’t used, the best solution is to remove those features.

A diagram of the complexities of the roles and capabilities system. Right, the proposal currently on the table.

The current consensus — see Trac ticket #10201 — would be to eliminate user-specific capabilities (both additional capabilities, and negation), and we would force users to have only one role.

It would be an admission that the current roles/capability system, in a desire to be leaps and bounds over the original 0-to-10 user levels, went a little too far.

In a later post,* I’ll talk about the schema — how we store roles and capabilities now, how we may store roles and capabilities in the future, and how we’ll bridge the two on an upgrade. Some alternatives should also be discussed, but in the context of the schema.

* While drafting this, I’ve also been replying to that wp-hackers thread, so my thoughts on most of this are already out there.

Akismet case study: Maintaining support for legacy versions

Automattic‘s Akismet, the awesome spam-fighting plugin, needs to maintain support for the oldest of WordPress installations. That poses an issue when new APIs are introduced and old APIs are deprecated. I recently patched up the plugin to make it work great with 3.0 while still supporting far older versions. It also fixed a few minor bugs. I’ve sent the patch over to the Akismet team, but there were a few parts to the patch I figured I would share.

if ( !function_exists('esc_url') ) {
	function esc_url( $url ) { return clean_url( $url ); }
}
if ( !function_exists('esc_html') ) {
	function esc_html( $html ) { return wp_specialchars( $html ); }
}
if ( !function_exists('esc_attr') ) {
	function esc_attr( $attr ) { return attribute_escape( $attr ); }
}

There, now go ahead and use esc_url(), esc_html(), and esc_attr() throughout your plugin. Make sure you always check each function individually, even though these were all introduced in the same version — what if another plugin used the same technique? (What I mean by that is, if you just check esc_url() and define esc_html() and esc_url() with it, but another plugin checks esc_html() and that plugin is loaded first, your plugin will cause a fatal error for trying to redeclare esc_html(), because you didn’t check it first.) And, use the 2.8 widget API if possible, because it’s the Right Thing to do:

if ( function_exists( 'wp_register_sidebar_widget' ) && function_exists('wp_register_widget_control') ) {
	wp_register_sidebar_widget( 'akismet', 'Akismet', 'widget_akismet' );
	wp_register_widget_control( 'akismet', 'Akismet', 'widget_akismet_control', array( 'height' => 75 ) );
} else {
	register_sidebar_widget('Akismet', 'widget_akismet', null, 'akismet');
	register_widget_control('Akismet', 'widget_akismet_control', null, 75, 'akismet');
}

It should be noted, of course, that Akismet already employs this technique for other functions. The goal here is to just catch it up to functions introduced in 2.8.

Complete diff available for download here. It also fixed a few notices caused by unset variables and unchecked indexes, and discovered some unreachable code.

Akismet also assumed the $pagenow global would always be set. This is true — except on activation. (See the notes on the Codex page.) WordPress 3.0 also has stricter activation checks now (which I imagine I will cover in a post soon), which thus prevents Akismet from activating when WP_DEBUG is enabled. The $pagenow issue was handled a few days ago in Akismet trunk, so the patch just cleans up deprecated function use and a few minor notices at this point.

A terminology nightmare: blogs, sites, networks, and the super admin

The goal of this post is to confuse you, and your head will probably hurt by the end. This is a process story on how we came to decide terminology for WordPress 3.0, and why many functions are so detached from the terminology used in the UI.

The main feature of the WordPress 3.0 is the merge of WordPress MU. MU, or multi-user, was a fork of the codebase designed for many blogs and many users, all on one install.

Your WordPress 2.9 install is a blog. WordPress MU hosts multiple blogs in a site, with the site’s subdomains or subdirectories each being a blog. Technically, MU can support multiple sites, whereas each site is a domain, and each site will have more than one blog.

Thus, example.org is a site (and also the main blog for the site), blog.example.org is a blog on the example.org site, and blog.example.com is a blog on the example.com site. All of this can be managed by a single MU installation. The cross-domain aspects require a plugin, but subdirectories or subdomains are out of the box.

Here’s where it got fun. Come WordPress 3.0, there was a UX decision made to call a blog a site, and remove the “blog” terminology.

You can see where this is going.

If a blog is now a site, then what does a site become? “Domain” is one option, but we didn’t want the connotations that came with that. (More on that later.)

When the merge began, the core developers began to use multisite internally to mean many blogs (well, sites), shortened to “ms” where possible, thus removing the “mu” and “wpmu” prefixes on files and functions.

Surely, we can make this more difficult. And so, just as the UX decision was made to call a blog a site, we decided to call a multisite a network.

The function get_option(), as you know, fetches the specified option for the blog (a site, in the new UI parlance). But MU has two other variations of the options API. One is get_blog_option(), which is used to get the specified option for another blog. So we could leave get_option() as is, and rename get_blog_option() to get_site_option().

Right? Nope.

See, the other API that MU has is get_site_option(), which are options for the entire MU site (in 3.0 parlance, that’d be multisite or a network). So get_blog_option() would be for sites (formerly known as a blog), and get_site_option() would be for multisites (or networks). (We also don’t generally rename the underlying functions only when the UI changes for UX reasons.)

A lot of discussion centered around the options API. There are no options across an entire MU multi-domain install, but we still thought about that and it caused plenty of confusion. We also considered get_sitewide_option(), get_domain_option(), get_multisite_option(), and yes, even get_thingieswide_option(). In despair, one suggestion was thing, thingy, and thingies, to replace blog, site, and network. (Edit: I’ve checked the logs, and it was core dev Peter Westwood who proposed this gem.)

Still with me?

We have this other MU function, called is_site_admin(). An MU site admin has complete control over the site’s settings and all blogs. (In 3.0 terms, these users have complete control over the network’s settings and all sites.)

In order to complete the merge, we’d need to make a lot of is_site_admin() calls all over the place. This function should return true if it’s a network admin, but also true if the user is an administrator on a single-blog (single-site?) installation. (MU blog admins have lesser privileges than both MU site admins and single-install admins.)

The problem is, many plugins check for the existence of is_site_admin() to determine whether it’s MU or a single-install, which means we could only use it in files that are only loaded when we’re running multisite. So much for that.

Thus, is_site_admin() was the first MU function to be deprecated. We started a new deprecated file to be included only when running multisite (and we ended up adding 3 more deprecated files for other contexts). Thus, old plugins using function_exists('is_site_admin') will still work. (For reference, you should now use is_multisite() — do not rely on any constants such as MULTISITE.)

Now what? Well, we needed a replacement for is_site_admin(). Since we aren’t changing any terminology in the code, remember, its current name would have been preferred.

Early candidates, is_multisite_admin() and is_network_admin(), pose terminology problems of their own. An MU site admin isn’t necessarily a site admin for another domain on the same install, and there were some concerns that these functions could falsely connotate multiple domains.

This brings up another problem we have yet to address: If a network (an MU site) has many blogs (err, sites), then what is a collection of many MU sites across domains? (We defined “network” as not spanning domains.)

Ultimately, we decided on is_super_admin(), as that is what many will end up referring to it as anyway. This later inspired talk of capes, but I digress.

“Leave API as is and let UI do what it wants,” Ryan Boren said. “Expect devs to be able to bridge the terms.” To do that, I wish you all lots of luck.

This is more or less a summary of an actual IRC conversation among a handful of contributors and developers, including myself. It was mind-numbingly confusing. I would link to the logs, but its contents make a lot of smart people look really silly. Okay, fine, the logs are here — but I’m only sharing it with you because now that I’ve located them, I realize this post has only scraped the surface of the confusion, and you deserve a bigger headache. For reference, Ryan’s quote above came from the IRC discussion.

Deprecated functions and WP_DEBUG

(Updated June 27, 2010: You may wish to check out my Log Deprecated Notices plugin.)

In every major version, the WordPress developers send functions to the graveyard. Many developers continue to use deprecated functions in their pluigns, or may even begin to use a function after it was already deprecated.

Deprecated functions cannot be relied upon to work efficiently or correctly and may be disabled by default or entirely removed in future versions of WordPress. But really, I would think about it like this: By using them, you’re likely missing out on new features afforded by the new function.

There are various reasons we deprecate functions:

  • We may decide to change or improve how it the function works, but doing so might break backwards compatibility. So we deprecate the old function and come up with a new one.
  • We may rename a function to standardize its name with other functions or to clarify its usage. Developers tend to like APIs standardized as much as possible, as it means we can work faster.
  • We may consolidate functions — for example, the_author_meta($meta) and get_the_author_meta($meta) replaced 25 functions.[1] Yes, 25. And it can return any usermeta field. current_user_can(), likewise, replaced a bunch of functions along the lines of user_can_create_draft() and user_can_edit_post_date(). In another example, we’re consolidating category and tag operations under term/taxonomy operations.
  • We may remove functions that no longer do anything. (Why do we keep a function that doesn’t do anything? If a plugin tries calling a function that no longer exists, you’ll get a fatal error. Also, if a plugin uses the same function name in a later version, the plugin would be incompatible on an old version that has the function.)
  • When merging in WordPress MU in 3.0, we removed dozens of functions, in many cases to merge them with their non-MU counterparts.

We normally don’t deprecate functions when we change front-end terminology, as we trust developers to make the connection, and we’re not nuts enough to rename functions just for the sake of renaming them.

What else do we deprecate?

We also don’t have just deprecated functions. We have deprecated files and, as of 3.0, deprecated function arguments.

Deprecated files: MagpieRSS (wp-includes/rss.php) has not been developed for years, and is deprecated in favor of Simplepie (wp-includes/class-simplepie.php). Most others were files that have been renamed, but we had to keep the old one because they were ones often included directly by plugins. In these cases, the old file includes the new files, so you don’t lose functionality. Deprecated files include:

  • wp-includes/rss-functions.php, now wp-includes/rss.php (also deprecated; use Simplepie)
  • wp-includes/registration-functions.php, now wp-includes/registration.php
  • wp-admin/upgrade-functions.php, now wp-admin/includes/upgrade.php (contains wp_install(), dbDelta(), etc.)
  • wp-admin/admin-functions.php, now wp-admin/includes/admin.php
  • WordPress does not support the legacy my-hacks.php file

Deprecated arguments: (new in 3.0) In nearly all cases, deprecated function arguments no longer have any functionality tied to them and often don’t have alternatives available. If we later add an argument to that function, though, we don’t want to take the spot of an argument that used to be there, in case a plugin developer still tried using the original argument.

Where we keep them

Deprecated files and arguments are scattered throughout core, but we consolidate deprecated functions so developers can reference those files. In 2.9, the file was wp-includes/deprecated.php. In 3.0, we now have five files:

  • wp-includes/deprecated.php — Regular WordPress functions available everywhere.
  • wp-admin/includes/deprecated.php — For WordPress functions only usable in the administration area.
  • wp-includes/ms-deprecated.php — Functions from WordPress MU, loaded only when running multisite.
  • wp-admin/includes/ms-deprecated.php — Functions from WordPress MU only usable in the administration area.
  • wp-includes/pluggable-deprecated.php — Pluggable functions. Because pluggable functions are defined later (so a plugin can override them), we can’t put these in the normal deprecated.php file. Moving them into its own file allows us to lower their profile.

Keeping track of deprecated usage

As of right now in 3.0, we have 131 deprecated functions and 39 deprecated arguments. How should you, a plugin developer, keep up with them all?

define( 'WP_DEBUG', true );

If you’re a plugin or theme developer and you don’t know what the WP_DEBUG is, you’re missing out. (If you do know what it is and you don’t use it during development, you’re crazy.)

The WP_DEBUG constant, which you would define as true in wp-config.php, does two things. First, it tells PHP to report more errors, specifically “notices.” (WordPress normally instructs PHP to only report warnings and fatal errors.)

This means you will see potential problems in your code, such as unchecked indexes (empty() and isset() are your friend) and undefined variables. (You may even find problems in WordPress itself, in which case you should file a bug report.)

Second, WP_DEBUG exposes debug messages generated by WordPress, such as:

  • When a deprecated function or function argument is used, or a deprecated file is included.
  • When user levels are used instead of the roles and capabilities system.
  • When old APIs are utilized instead of new ones, such as the Settings API (register your settings!).

In a nutshell, there’s an entire API devoted to informing you of the best practices and the latest APIs. Here’s two example notices:

Notice: options.php was called with an argument that is deprecated since version 2.7! The “your_option_name” setting is unregistered. Unregistered settings are deprecated. See http://codex.wordpress.org/Settings_API

Notice: get_category_rss_link is deprecated since version 2.5! Use get_category_feed_link() instead.

You can suppress notices if you prefer, by using the WP_DEBUG_LOG and WP_DEBUG_DISPLAY constants (see the inline documentation I wrote here). You can also use hooks and filters in the Deprecated API (view the functions). (To know where functions are being run, you could run a backtrace on one of those hooks, for example.)

Many even run WP_DEBUG on their production websites. Generally, they log it (using either WP_DEBUG_LOG or a server PHP error log) and force the hiding of errors, using:

define( 'WP_DEBUG', true ); // turn on debug mode
define( 'WP_DEBUG_LOG', true ); // log to wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // don't force display_errors to on
ini_set( 'display_errors', 0 ); // hide errors

“But I need to support old versions”

Honestly, I wished you didn’t ask that. But if you must support a legacy version, conditionally check for the new functions and use those if available.

// esc_url() introduced 2.8
$cleaned_url = ( function_exists( 'esc_url' ) )
	? esc_url( $url )
	: clean_url( $url );

 // introduced 2.8
if ( function_exists( 'wp_register_sidebar_widget' ) )
	wp_register_sidebar_widget( 'widget-id', 'My Widget', 'my_widget_callback' );
// introduced 2.2, if you must check
elseif ( function_exists( 'register_sidebar_widget' ) )
	register_sidebar_widget('My Widget', 'my_widget_callback' );

Just remember: friends don’t let friends develop plugins without WP_DEBUG.

[1] These functions were deprecated in favor of get_the_author_meta(): get_author_name(), get_the_author_ID(), get_the_author_aim(), get_the_author_description(), get_the_author_email(), get_the_author_firstname(), get_the_author_icq(), get_the_author_lastname(), get_the_author_login(), get_the_author_msn(), get_the_author_nickname(), get_the_author_url(), get_the_author_yim(). In favor of the_author_meta(): the_author_ID(), the_author_aim(), the_author_description(), the_author_email(), the_author_firstname(), the_author_icq(), the_author_lastname(), the_author_login(), the_author_msn(), the_author_nickname(), the_author_url(), the_author_yim().

Google Summer of Code idea

I am strongly considering applying for the Google Summer of Code with WordPress. As a core developer, I’m in a unique position, because a stated goal of GSoC is to increase participation in the open source community, recruit contributors, and identify possible future committers. (I’m also the only student on the commit team as far as I know, though Dion Hulse was a GSoC student for two years.)

I was not contributing to core during last year’s GSoC term, and I’ve only really thought about applying to GSoC in the last few months. In the end, the powers that be have said I am welcome to apply. With that out of the way, I now need to come up with a project. I’ve thought about it for a while, and I’ve had a few good ideas, but nothing that I feel I can stretch out over a summer. Problem is, a typical project — one that may span several months of iterations under normal circumstances — might be something I could complete in a caffeine-powered weekend.* A few people I spoke to said that’s definitely something I need to consider.

An Advanced Theme Revisioning System

A project on theme revisions is something that has been on the official WordPress list of ideas since last year. My proposal would put this idea on steroids. Continue reading Google Summer of Code idea