Blog

Building WordPress Membership Feature, Part 1: User Roles

Tags:

Barrel_Blog-Post_Members-Only_Part1_treehouse

There are several WordPress plugins that will convert your theme to a fully capable membership system. These let you track your website users, their capabilities on the site, and their access to specific features such as downloads, links, and certain pages. Plugins like S2Member or aMember are extremely powerful – but they aren’t without their drawbacks. Those plugins must be as general as possible to cover a broad spectrum of needs from the various types of users that employ them. For that same reason, much of the plugins’ functionality never gets used.

As part of a recent project, we needed membership capabilities with a simplified approach to work with our custom theme. One of the reasons we didn’t go with an existing plugin was a lack of an approval system. We needed a system that allowed board members to vote on prospective member applications before they gain access to resources and privileges.

Goals and Assumptions

I want to show how easy it is to create a members-only system—a way to make exclusive content available for your registered users—without having to resort to bulky membership plugins. This is not meant as a replacement to those plugins. This tutorial assumes that you are building a more robust system for your own purposes and as such will not simply drop the code in place.

Prerequisites

For this tutorial you should have a basic working knowledge of PHP and WordPress development, including action and filter hooks, a set of predefined roles and capabilities, as well as custom field toggles.

Note: This is not a turnkey solution for a full membership site. Check out Part 2 and Part 3 in this series on how to create user registration and a paywall to convert your members to a premium or subscription model.

The Permissions
Barrel_Blog-Post_Members-Only_Part1

First things first: if you’re not using a plugin for role management (like User Role Editor) then you’ll need to architect the roles and capabilities of your site. The first thing to do is determine what capabilities, native and custom, that your members will need.

function rolescheck() {
    $default_caps = array(
        'read' => false,
        'upload_files' => false,
        'manage_options' => false,
        'manage_members' => false,
        'is_premium_member' => false,
        'is_approved_member' => false,
    );
    if ( current_user_can('administrator') && $admin = get_role('administrator') ) {
        // upgrade admin user for theme
        if (! current_user_can('manage_options') ) $admin->add_cap('manage_options');
        if (! current_user_can('manage_members') ) $admin->add_cap('manage_members');
        if (! current_user_can('is_approved_member') ) $admin->add_cap('is_approved_member');
        if (! current_user_can('is_premium_member') ) $admin->add_cap('is_premium_member');
    }
    if (! get_role( 'pending' ) ) {
        add_role('pending', 'Pending Approval', $default_caps);
    }
    $default_caps['read'] = true;
    $default_caps['is_approved_member'] = true;
    $default_caps['upload_files'] = true;
    if (! get_role( 'standard' ) ) {
        add_role('standard', 'Standard', $default_caps);
    }
    $default_caps['is_premium_member'] = true;
    if (! get_role( 'premium' ) ) {
        add_role('premium', 'Premium', $default_caps);
    }
}
add_action('init', 'rolescheck');

The above function and hook will create three user roles: standard, premium, and pending. You can make as many or as few roles as you like, but for this example, there are essentially two real memberships, the standard and premium, and a third membership, pending. We use the pending role to pool all new user registrations, which might require approval before we give users access to standard or premium features.

You may opt to exclude the pending role so that a user does not need to be approved; however, this is also handy if you need to suspend a user account, but do not want to delete the user from your site. The function is built so that the roles are only created if they do not currently exist for your site. The roles should now appear in the dropdown on the Users page in the WordPress admin area.

The Access

Before we get down and dirty with members-only features, let’s prevent pending users from logging in.

/**
 * Authenticates by username or email and only allow members to login
 *
 * @param    user
 * @param    username
 * @param    password
 * @return    bool
 */
function auth_email_username( $user, $username, $password ) {
    $field = is_email($username) ? 'email' : 'login';
    $user = get_user_by($field, $username);
    if ( $user && !is_wp_error($user) )
            $username = $user->user_login;
    if ( user_can($user, 'administrator') || user_can($user, 'is_approved_member')) {
            return wp_authenticate_username_password( null, $username, $password );
    }
    return new WP_Error('login', __('You do not have permission to login at this time.'));
}
add_filter( 'authenticate', 'auth_email_username', 20, 3 );

The above function and filter allow for authentication by email or login, which mirrors most login services’ authentication flows. We then authenticate the user if they have administrator or is_approved_member capabilities.

The Functionality

Our objectives should now be to check if a resource is members-only, and to permit or deny access to that resource based on the user’s membership level. For this, we will create three functions: one to check the resource, one to return the resource, and one to test membership against the user. My first function will ensure the proper post types are being scrutinized and that the current user can access those pages.

/**
 * Check if post is members-only content and redirects if necessary.
 *
 * @return    bool
 */
function is_members_only() {
    global $post;

    $post_check = array( 'offer', 'news', 'attachment', 'event', 'job', 'page' );
    $members_only = get_post_meta($post->ID, '_members_only', true);

    if ( !in_array(get_post_type(), $post_check) ) return;
    if ( !is_member() && !headers_sent() && (is_single() || is_page()) ) {
        // not an approved or logged in user
        if ( $members_only && 'get_header' == current_filter()) {
            wp_redirect( site_url() );
            exit;
        } else return $members_only;
    } else {
        return $members_only;
    }

}
add_action('get_header', 'is_members_only');

In the above function, we reference a custom post meta where the key, _members_only, contains either a 1 or 0 boolean value. I use custom field toggle to create the members-only toggle on each post type or page that can be protected. Now all your editor must do is declare a certain page to be members-only by using the toggle.

The above code will run just before the header is output using the get_header hook so that we can redirect to a default page (e.g. the home page) in the event the user does not have access to the target content. The function is built like a template tag so that you can use it anywhere to determine if the current page is_members_only().

Before we move to returning the resource, we have created a template tag to determine if the user has a particular capability.

/**
 * Check if user is registered member
 *
 * @return    bool
 */
function is_member(){
    return current_user_can('is_approved_member');
}

Now when we use the template tag is_member(), we can ensure that the currently logged in user has the capability is_approved_member, but you can make the capability whatever you want so long as only a certain subset of your users, or members, has this permission. This could be modified to encompass one or more permissions, but the general part of this tag is to be used as a conditional to hide or show members-only content within a template. Since we have another level, you might try creating another template tag for the premium membership. Try making is_premium_member().

The Filters

Finally we wish to return a resource, which in this case is a link to a page or post or attachment. To do this, all we’re going to do is filter links.

/**
 * Filter the permalink based on the post type.
 *
 * @return    string
 */
function the_download_link($url, $post = null) {
    if ( is_null($post)) {
        global $post;
    }
    $members_only = is_object($post) ? $post->_members_only : get_post_meta($post, "_members_only", true);
    if ($members_only && !is_member()) return "#login";
    $post_type = get_post_type( $post );
    switch( $post_type ) {
        default: return $url;
    }
}
add_filter('post_type_link', 'the_download_link', 10, 2);
add_filter('the_permalink', 'the_download_link');

In the above, we add two filters, which call the same function the_download_link(), which takes two arguments: the url to be filtered, and the post object/ID. In some cases the filter does not populate the post object, so we initially run a check to determine if the post object is null or not. Also, one or more of the filters used populates the $post argument with an ID instead of the post object, so we can assume that if the $post is not an object that it is a numeric ID.

We then ensure the queried resource is members-only or not by checking the suitable post meta field. If the user is not a member, and we already know the post is members-only, we return a hash. In this case, the hash #login will be a handler for our front-end javascript that triggers a login/registration modal or a refresh to the login/registration page. You can further refine the target post type and the url returned based on the post type.

/**
 * Filter nav menu items that are members_only.
 *
 * @return    array
 */
function nav_item_filter($items){
    foreach($items as $key => $post) $items[$key]->url = the_download_link($post->url, $post->object_id);
    return $items;
}
add_filter('wp_get_nav_menu_items', 'nav_item_filter');

In order for menus to receive the same filter, you must iterate through each of the nav item objects and simply pass it the same function as the link filters. You can repeat this process any time something on the site displays a link that isn’t reachable by one of your existing filters.

Using the above techniques, you can really refine the way users register and become members of your website or organization, depending on their permission, and depending on restrictions you put in the theme. Another way to enhance this method is to ensure that all links get the same treatment, as there are a number of ways that links might be accessed. The two included here cover most cases such as menus generated using wp_nav and permalinks retrieved with get_permalink() or the_permalink(). You might have to include a few more filters to cover your bases. Ones not covered here might include the get_term_link, archive links, and category/taxonomy-type filters.

Conclusion

This solution may not work for everyone and all cases, but it is nice to have a simple way to create members-only content without having to deal with a ton of configuration. You can see a full class implementation on GitHub.

If you found this post useful or want to learn more about creating a membership site, be sure to check out Part 2 of this series on User Registration.

Illustrations by Cindy Leong

  • http://ChiefAlchemist.com/ Mark Simchock

    Good stuff. Thanks for sharing Wes. I’ve got some membership’ing to do and this was very helpful.

    Mind you, I only scanned the repo code pretty quickly, but isn’t this bit kinda “bad form”:

    function is_member(){
    return WP_Membership::is_member();
    }

    1) No function_exists()?

    2) Even so, such (procedural-esque) “cheating” kinda compromising using an OOP class, yes? Even a static method would make better sense, yes?

    3) Finally, I’ve been finding it most useful to distill the raw / basic (abstract?) logic from the actual use-centric settings. For example, this example is great but it’s only useful for 3 tiers (i.e., standard, premium and pending). If you want to add / subtract a tier (or even rename them) I sense – perhaps I’m wrong – this would break down. I know what you’re thinking – i.e., “Come on Mark,what do you want for nothing?” 🙂 – I’m simply pointing out that with just a bit effort this could be one helluv a class.

    Again, I’m not trying to be a critic – just killing sometime before the Super Bowl.

    • wturner

      Hey Mark,

      Very useful comments. I suppose I should make it clearer that this is merely a starting point and not to be used as a complete solution on how to construct this type of behavior for WordPress. It’s definitely good form to wrap global functions with function_exists calls to prevent collisions with other plugins, etc, so whether you use a static class method or the global is totally up to how you might use it in your implementation. There are a couple of plugins that do most of this (allow adding/removing tiers) already, but I wanted to show how it’s possible to make such a system without bulking up on plugin overload. In any case, I tweaked my post and gist to reflect some of your feedback. Thanks!

      What a boring Super Bowl!?!

      • http://ChiefAlchemist.com/ Mark Simchock

        Hey Wes

        Plugin bloat? What’s that? hehe.

        In any case, I do understand your intention(s). Thanks for sharing. Did I forget to say to thanks? Sorry 🙂

        To me that’s all the more reason to sketch out classes that are easier to extend – both for yourself and for others. For example, I think the rolescheck method could have been an engine (so to speak) fueled by arrays. That is, distill the logic from the value so all you have to do is feed in the values.

        At the very least, (with the pattern / architecture I follow) $default_caps would have been a method (or property but I prefer methods because they add obvious structure). This way it’s easier to override. So if I wanted to use your class and wanted to change just one of those bools in $default_caps I’d have to copy / refactor the whole method.

        Minor? Yup! But if I write for you, and you write for me, then I think we each end up with better code and more opportunity to share code.

        p.s. If you want to send me an email I can point you at a couple example of what I mean. I don’t want to leave links here and have you think I’m spamming.

  • Pingback: cash loans

  • Pingback: direct payday loan no direct deposit lender

  • Pingback: drugrehabcentershotline.com alcohol treatment

Popular This Week
25 Must-Have Pages for Your E-commerce Website
January 30, 2015

25 Must-Have Pages for Your E-commerce Website

By Yvonne Weng
New Shopify Theme: Mosaic
January 16, 2016

New Shopify Theme: Mosaic

By Peter Kang
Taking Control of Image Loading
July 29, 2013

Taking Control of Image Loading

By Patrick Kunka
Text-align: Justify and RWD
March 12, 2013

Text-align: Justify and RWD

By Patrick Kunka

Like what you’re reading? Sign up for the Barrel newsletter and receive updates.