When building a company website, chances are you'll be looking for a WordPress client portal plugin where an administrator can upload files and information related to a particular client. And most of the time it doesn't have to be something complicated.

There are several solutions like simply creating a password protected page or using an existing WordPress plugin. But in some cases, it's just a bit too much.

In this tutorial, we'll learn how to build a simple WordPress Client Portal plugin step by step. And if you aren't a developer, don't worry! We'll also be showing you how to build a secure client portal on your WordPress website without using code! Either way, you'll have the best client portal WordPress plugin for your needs.

There are two main things we'll cover: creating the WordPress customer portal plugin and exploring what plugins we can integrate it with so we have a complete solution.

Building a customer portal in 3 easy steps

Step 1: Download the Client Portal WordPress plugin

On your WordPress site go to your wp-admin > plugins > add new, search for "client portal" and then click on "install now".

Step 2: Setting up your client portal

Once activated in your WordPress admin go to users > Client Portal Settings. Here you'll see all settings available for the plugin.

The settings available are:

  • Page Slug — this is the slug of the private user pages such as example.com/private-page/username.
  • Support Comments — allows you to have communication on the front-end of your site with your clients.
  • Generate Pages — this setting allows you to automatically generate client areas for your existing users with one-click.
  • View all pages — shows the current private pages which can be useful for client management.
  • Restricted Message — The Client Portal plugin is fully white-label allowing you to set custom messages on the front-end of your site for better user experience.
  • Portal Log In Message — Same as the restricted message, with the difference being this is the message that a client sees when they try and log in.
  • Default Page Content — This is the content that is automatically displayed on the private page. You could include things such as invoices, an area to upload files, links out to project management tools, and literally anything you want.

Once you're happy with all your settings scroll down to the bottom of the page and click "Save Settings".

Step 3: Onboarding Your Clients

Now you have your WordPress client portal configured you need to let your clients know about it! There are two ways you can do this:

  1. Use your favorite email marketing tool such as Mailchimp to send out a newsletter to your clients letting them know about the new client area.
  2. Personally email your clients with a unique email to introduce them to the new client portal, explain how it works, and assist them in getting to grips with it. This method helps cultivate your customer relationships, manage client projects,  and makes them feel more valued.

Congratulations! You've now successfully set up a brand-new client portal in just 3 steps.

What to use your client portal for?

There are loads of things you can use your brand new client portal for such as:

  • File uploads.
  • Sharing deliverables — such as completed content work, graphics, zip files, etc.
  • Invoicing — upload client invoices to make it easier for both of you to find.
  • Add a contact form — a quick and easy way for your client to reach out if for any reason they lose your contact details.

Building a WordPress Client Portal Plugin

While there seems to be a lot of functionality in this small plugin, most functions are quite small and easy to understand. We'll go through:

  • creating a plugin
  • building a PHP class that will hold our plugin
  • registering a custom post type so private pages are not mixed with other content
  • automatically create a new private page on new user creation
  • automatically delete a private page on user deletion
  • restrict the content if the user doesn't have permissions to view it
  • adding links to the user private pages in WordPress Dashboard -> Users
  • adding a logout button on the private page for users to use
  • creating the [client-portal] shortcode that redirects users to their private page
  • create a settings page for our plugin where we're editing the default messages
  • add a couple of tweaks like admin notices, flush permalinks and exclude next and pre navigation from our custom post

If you want to skip this, you can download it from WordPress.org

Get Client Portal Plugin

CREATE A NEW PLUGIN

In WordPress creating a plugin is as simple as creating a .php file with a special header. Here's how our plugin file will look like:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23                        
                          <?php                          /** * Plugin Name: Client Portal * Plugin URI: http://www.cozmoslabs.com/ * Description: Build a company site with a client portal where clients login and see a restricted-access, personalized page of content with links and downloads. * Version: 1.0.0 * Author: Cozmoslabs, Madalin Ungureanu, Antohe Cristian * Author URI: http://www.cozmoslabs.com * License: GPL2 */                          /* Copyright 2019 Cozmoslabs (www.cozmoslabs.com)   This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation.   This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.   You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation                        

BUILD A PHP CLASS THAT WILL CONTAIN OUR PLUGIN FUNCTIONS

The main reason for using a PHP class instead of functional programming is to keep everything nice and tidy and limit the risk of naming conflicts.

1 2 3 4 5 6 7 8 9 10 11 12                        
                          class                          CL_Client_Portal                          {                          private                          $slug                          ;                          private                          $defaults                          ;                          public                          $options                          ;                          function                          __construct(                          )                          {                          }                          }                        

The __construct() function will contain all our hooks and filters as well as initiating the $slug, $defaults and $options variables that we'll use throughout our client portal plugin.

INITIALIZE THE DEFAULT VARIABLES PARAMETERS

Any plugin that has options also needs some carefully chosen defaults that will work for the majority of users. We're doing this inside the __construct() function.

  • $slug – the admin page slug. Also used for the plugin option name in the database
  • $options – if we have user defined options, we're loading them from the database
  • $defaults – the default settings for the private page slug, restricted message and portal login message
1 2 3 4 5 6 7                        
                          $this                          ->slug                          =                          'cp-options'                          ;                          $this                          ->options                          =                          get_option(                          $this                          ->slug                          )                          ;                          $this                          ->defaults                          =                          array                          (                          'page-slug'                          =>                          'private-page'                          ,                          'restricted-message'                          =>                          __(                          'You do not have permission to view this page.'                          ,                          'client-portal'                          )                          ,                          'portal-log-in-message'                          =>                          __(                          'Please log in in order to access the client portal.'                          ,                          'client-portal'                          )                          )                          ;                        

REGISTER THE PRIVATE PAGE CUSTOM POST TYPE

Using a custom post type instead of normal pages keeps everything clear and organized. The Custom Post Type gets added on the init hook.

The private page slug is dynamic and can be changed by the administrator if needed. We're also setting it up so it doesn't show in the WordPress menu since we'll be adding it under each user in the WordPress -> Users interface.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47                        
                          /* register the post type */                          add_action(                          'init'                          ,                          array                          (                          $this                          ,                          'cp_create_post_type'                          )                          )                          ;                          /** * Function that registers the post type */                          function                          cp_create_post_type(                          )                          {                          $labels                          =                          array                          (                          'name'                          =>                          _x(                          'Private Pages'                          ,                          'post type general name'                          ,                          'client-portal'                          )                          ,                          'singular_name'                          =>                          _x(                          'Private Page'                          ,                          'post type singular name'                          ,                          'client-portal'                          )                          ,                          'menu_name'                          =>                          _x(                          'Private Page'                          ,                          'admin menu'                          ,                          'client-portal'                          )                          ,                          'name_admin_bar'                          =>                          _x(                          'Private Page'                          ,                          'add new on admin bar'                          ,                          'client-portal'                          )                          ,                          'add_new'                          =>                          _x(                          'Add New'                          ,                          'private Page'                          ,                          'client-portal'                          )                          ,                          'add_new_item'                          =>                          __(                          'Add New Private Page'                          ,                          'client-portal'                          )                          ,                          'new_item'                          =>                          __(                          'New Private Page'                          ,                          'client-portal'                          )                          ,                          'edit_item'                          =>                          __(                          'Edit Private Page'                          ,                          'client-portal'                          )                          ,                          'view_item'                          =>                          __(                          'View Private Page'                          ,                          'client-portal'                          )                          ,                          'all_items'                          =>                          __(                          'All Private Pages'                          ,                          'client-portal'                          )                          ,                          'search_items'                          =>                          __(                          'Search Private Pages'                          ,                          'client-portal'                          )                          ,                          'parent_item_colon'                          =>                          __(                          'Parent Private Page:'                          ,                          'client-portal'                          )                          ,                          'not_found'                          =>                          __(                          'No Private Pages found.'                          ,                          'client-portal'                          )                          ,                          'not_found_in_trash'                          =>                          __(                          'No Private Pages found in Trash.'                          ,                          'client-portal'                          )                          )                          ;                          $args                          =                          array                          (                          'labels'                          =>                          $labels                          ,                          'description'                          =>                          __(                          'Description.'                          ,                          'client-portal'                          )                          ,                          'public'                          =>                          true                          ,                          'publicly_queryable'                          =>                          true                          ,                          'show_ui'                          =>                          true                          ,                          'show_in_menu'                          =>                          false                          ,                          'query_var'                          =>                          true                          ,                          'capability_type'                          =>                          'post'                          ,                          'has_archive'                          =>                          false                          ,                          'hierarchical'                          =>                          true                          ,                          'supports'                          =>                          array                          (                          'title'                          ,                          'editor'                          ,                          'thumbnail'                          )                          )                          ;                          if                          (                          !                          empty                          (                          $this                          ->options[                          'page-slug'                          ]                          )                          )                          {                          $args                          [                          'rewrite'                          ]                          =                          array                          (                          'slug'                          =>                          $this                          ->options[                          'page-slug'                          ]                          )                          ;                          }                          else                          {                          $args                          [                          'rewrite'                          ]                          =                          array                          (                          'slug'                          =>                          $this                          ->defaults[                          'page-slug'                          ]                          )                          ;                          }                          register_post_type(                          'private-page'                          ,                          $args                          )                          ;                          }                        

CREATING AND DELETING THE PRIVATE PAGES ON USER CREATION AND DELETION

Every time a new user registers or is created by an administrator, we also need to create its private page. The same thing goes on post deletion.

The correlation between a user and a private page is the CPT's author.

We're also making use of cp_get_private_page_for_user() function that returns the ID for the private page for a particular user.

                /* action to create a private page when a user registers */     add_action( 'user_register', array( $this, 'cp_create_private_page' ) );     /* remove the page when a user is deleted */     add_action( 'deleted_user', array( $this, 'cp_delete_private_page' ), 10, 2 );       /**      * Function that creates the private page for a user      * @param $user_id the id of the user for which to create the page      */     function cp_create_private_page( $user_id ){         /* make sure get_userdata() is available at this point */         if(is_admin()) require_once( ABSPATH . 'wp-includes/pluggable.php' );           $user = get_userdata( $user_id );         $display_name = '';         if( $user ){             $display_name = ($user->display_name) ? ($user->display_name) : ($user->user_login);         }           $private_page = array(             'post_title'    => $display_name,             'post_status'   => 'publish',             'post_type'     => 'private-page',             'post_author'   => $user_id         );           // Insert the post into the database         wp_insert_post( $private_page );     }       /**      * Function that deletes the private page when the user is deleted      * @param $id the id of the user which page we are deleting      * @param $reassign      */     function cp_delete_private_page( $id, $reassign ){         $private_page_id = $this->cp_get_private_page_for_user( $id );         if( !empty( $private_page_id ) ){             wp_delete_post( $private_page_id, true );         }     }       /**      * Function that returns the id for the private page for the provided user      * @param $user_id the user id for which we want to get teh private page for      * @return mixed      */     function cp_get_private_page_for_user( $user_id ){         $args = array(             'author'            =>  $user_id,             'posts_per_page'    =>  1,             'post_type'         => 'private-page',         );         $users_private_pages = get_posts( $args );           if( !empty( $users_private_pages ) ){             foreach( $users_private_pages as $users_private_page ){                 return $users_private_page->ID;                 break;             }         }     }

RESTRICT THE CONTENT OF THE PRIVATE PAGE IF THE USER DOESN'T HAVE ACCESS TO IT

We need to make sure that logged in users can only access their private page. The only exception is the administrator or users with the capability to delete_users.

We're blocking access by simply filtering the_content and returning something else instead if the user doesn't have access or is not logged in.

The error message displayed is taken from the plugin options that we'll go through a bit down the line.

                /* restrict the content of the page only to the user */     add_filter( 'the_content', array( $this, 'cp_restrict_content' ) );     /**      * Function that restricts the content only to the author of the page      * @param $content the content of the page      * @return mixed      */     function cp_restrict_content( $content ){         global $post;         if( $post->post_type == 'private-page' ){               if( !empty( $this->options['restricted-message'] ) )                 $message = $this->options['restricted-message'];             else                 $message = $this->defaults['restricted-message'];               if( is_user_logged_in() ){                 if( ( get_current_user_id() == $post->post_author ) || current_user_can('delete_user') ){                     return $content;                 }                 else return $message;             }             else return $message;           }         return $content;     }

ADDING LINKS TO THE USER PRIVATE PAGES IN WORDPRESS DASHBOARD -> USERS

Now that we've created the private pages for the client portal, we need to allow the administrator to access and edit them.

Since the private pages are user-related, we'll simply list them in the User listing dashboard.

The filter we're using is user_row_actions.

                /* add a link in the Users List Table in admin area to access the page */     add_filter( 'user_row_actions', array( $this, 'cp_add_link_to_private_page' ), 10, 2);     /**      * Function that adds a link in the user listing in admin area to access the private page      * @param $actions The actions available on the user listing in admin area      * @param $user_object The user object      * @return mixed      */     function cp_add_link_to_private_page( $actions, $user_object ){         $private_page_id = $this->cp_get_private_page_for_user( $user_object->ID );         if( !empty( $private_page_id ) ){             $actions['private_page_link'] = "<a class='cp_private_page' href='" . admin_url( "post.php?post=$private_page_id&action=edit") . "'>" . __( 'Private Page', 'client-portal' ) . "</a>";         }           return $actions;     }

ADDING A LOGOUT BUTTON ON THE PRIVATE PAGE FOR USERS TO USE

Once a client accesses his private page, it's important to allow him to logout.

So we're adding a small logout link as well as his name in case the admin visits that page, so he know what private page he's looking at.

                /* create client portal extra information */     add_filter('the_content', array( $this, 'cp_add_private_page_info'));       /**      * Function that creates a private page extra information div      * @param $content the content of the private page      * @return mixed      */     function cp_add_private_page_info( $content ){         global $post;         if ( is_singular('private-page') && is_user_logged_in() ){             // logout link             $logout_link = wp_loginout( home_url(), false);               // author display name. Fallback to username if no display name is set.             $author_id=$post->post_author;             $user = get_user_by('id', $author_id);             $display_name = '';             if( $user ){                 $display_name = ($user->display_name) ? ($user->display_name) : ($user->user_login);             }               $extra_info = "<p class='cp-logout' style='border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 0.5rem 0; text-align: right'> $logout_link - $display_name </p>";               return  $extra_info . $content;         }           return $content;     }

CREATING THE [CLIENT-PORTAL] SHORTCODE

This is probably the most important function of the entire plugin. Once the user is logged in he needs to be able to access his client portal. By default, there's no way for the user or the administrator to create a menu for each individual user.

So instead we're simply redirecting the user to his individual private page with the use of the [client-portal] shortcode.

Since the shortcode executes in the middle of the page, we can't really do a PHP redirect without some sort of workaround. So instead we've opted for a simple Javascript redirect that should work just fine for the majority of use cases.

                /* create the shortcode for the main page */     add_shortcode( 'client-portal', array( $this, 'cp_shortcode' ) );       /**      * Function that creates a shortcode which redirects the user to its private page      * @param $atts the shortcode attributes      */     function cp_shortcode( $atts ){         if( !is_user_logged_in() ){             if( !empty( $this->options['portal-log-in-message'] ) )                 $message = $this->options['portal-log-in-message'];             else                 $message = $this->defaults['portal-log-in-message'];               return $message;         }         else{             $user_id = get_current_user_id();             $private_page_id = $this->cp_get_private_page_for_user( $user_id );             $private_page_link = get_permalink( $private_page_id );             ?>             <script>                 window.location.replace("<?php echo $private_page_link ?>");             </script>         <?php         }     }

CREATE SETTINGS PAGE FOR OUR PLUGIN

There are just 3 settings and a button for generating private pages for existing clients. If you want to learn more about the WordPress Settings API this tutorial covers it all.

The function that generates a new private page for existing users is needed when you have clients already registered.

                /* create the settings page */     add_action( 'admin_menu', array( $this, 'cp_add_settings_page' ) );     /* register the settings */     add_action( 'admin_init', array( $this, 'cp_register_settings' ) );         /**      * Function that creates the admin settings page under the Users menu      */     function cp_add_settings_page(){         add_users_page( 'Client Portal Settings', 'Client Portal Settings', 'manage_options', 'client_portal_settings', array( $this, 'cp_settings_page_content' ) );     }       /**      * Function that outputs the content for the settings page      */     function cp_settings_page_content(){         /* if the user pressed the generate button then generate pages for existing users */         if( !empty( $_GET[ 'cp_generate_for_all' ] ) && $_GET[ 'cp_generate_for_all' ] == true ){             $this->cp_create_private_pages_for_all_users();         }           ?>         <div class="wrap form-wrap">               <h2><?php _e( 'Client Portal Settings', 'client-portal'); ?></h2>               <?php settings_errors(); ?>               <form method="POST" action="options.php">                   <?php settings_fields( $this->slug ); ?>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="page-slug"><?php echo __( 'Page Slug' , 'client-portal' ) ?></label>                     <input type="text" class="widefat" id="page-slug" name="cp-options[page-slug]" value="<?php echo ( isset( $this->options['page-slug'] ) ? $this->options['page-slug'] : 'private-page' ); ?>" />                     <p class="description"><?php echo __( 'The slug of the pages.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label"><?php echo __( 'Generate pages' , 'client-portal' ) ?></label>                     <a class="button" href="<?php echo add_query_arg( 'cp_generate_for_all', 'true', admin_url("/users.php?page=client_portal_settings") ) ?>"><?php _e( 'Generate pages for existing users' ); ?></a>                     <p class="description"><?php echo __( 'Generate pages for already existing users.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="restricted-message"><?php echo __( 'Restricted Message' , 'client-portal' ) ?></label>                     <textarea name="cp-options[restricted-message]" id="restricted-message" class="widefat"><?php echo ( isset( $this->options['restricted-message'] ) ? $this->options['restricted-message'] : $this->defaults['restricted-message'] ); ?></textarea>                     <p class="description"><?php echo __( 'The default message showed on pages that are restricted.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="portal-log-in-message"><?php echo __( 'Portal Log In Message' , 'client-portal' ) ?></label>                     <textarea name="cp-options[portal-log-in-message]" id="portal-log-in-message" class="widefat"><?php echo ( isset( $this->options['portal-log-in-message'] ) ? $this->options['portal-log-in-message'] : $this->defaults['portal-log-in-message'] ); ?></textarea>                     <p class="description"><?php echo __( 'The default message showed on pages that are restricted.', 'client-portal' ); ?></p>                 </div>                   <?php submit_button( __( 'Save Settings', 'client_portal_settings' ) ); ?>               </form>         </div>     <?php     }       /**      * Function that registers the settings for the settings page with the Settings API      */     public function cp_register_settings() {         register_setting( $this->slug, $this->slug );     }       /**      *  Function that creates private pages for all existing users      */     function cp_create_private_pages_for_all_users(){         $all_users = get_users( array(  'fields' => array( 'ID' ) ) );         if( !empty( $all_users ) ){             foreach( $all_users as $user ){                 $args = array(                     'author'            =>  $user->ID, // I could also use $user_ID, right?                     'posts_per_page'    => 1,                     'post_type'         => 'private-page',                 );                 $users_private_pages = get_posts( $args );                 if( empty( $users_private_pages ) ) {                     $this->cp_create_private_page( $user->ID );                 }               }         }     }

A FEW MORE TWEAKS FOR A BETTER CLIENT PORTAL PLUGIN

In order to take this a bit further, we'll have to style the settings page a bit, add some admin notices, regenerate the permalinks if we're changing the private page slug, and make sure we don't get next and previous posts navigation on our private pages.

                /* show notices on the admin settings page */     add_action( 'admin_notices', array( $this, 'cp_admin_notices' ) );     // Enqueue scripts on the admin side     add_action( 'admin_enqueue_scripts', array( $this, 'cp_enqueue_admin_scripts' ) );     /* flush the rewrite rules when settings saved in case page slug was changed */     add_action('init', array( $this, 'cp_flush_rules' ), 20 );       /* make sure we don't have post navigation on the private pages */     add_filter( "get_previous_post_where", array( $this, 'cp_exclude_from_post_navigation' ), 10, 5 );     add_filter( "get_next_post_where", array( $this, 'cp_exclude_from_post_navigation' ), 10, 5 );      /**      * Function that creates the notice messages on the settings page      */     function cp_admin_notices(){         if( !empty( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' ) {             if( !empty( $_GET['cp_generate_for_all'] ) && $_GET['cp_generate_for_all'] == true ) {                 ?>                 <div class="notice notice-success is-dismissible">                     <p><?php _e( 'Successfully generated private pages for existing users.', 'client-portal'); ?></p>                 </div>                 <?php                 if( !empty( $_REQUEST['settings-updated'] ) && $_GET['settings-updated'] == 'true' ) {                     ?>                     <div class="notice notice-success is-dismissible">                         <p><?php _e( 'Settings saved.', 'client-portal'); ?></p>                     </div>                 <?php                 }             }         }     }       /**      * Function that enqueues the scripts on the admin settings page      */     function cp_enqueue_admin_scripts() {         if( !empty( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' )             wp_enqueue_style( 'cp_style-back-end', plugins_url( 'assets/style.css', __FILE__ ) );     }       /**      * Function that flushes the rewrite rules when we save the settings page      */     function cp_flush_rules(){         if( isset( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' && isset( $_REQUEST['settings-updated'] ) && $_REQUEST['settings-updated'] == 'true' ) {             flush_rewrite_rules(false);         }     }         /**      * Function that filters the WHERE clause in the select for adjacent posts so we exclude private pages      * @param $where      * @param $in_same_term      * @param $excluded_terms      * @param $taxonomy      * @param $post      * @return mixed      */     function cp_exclude_from_post_navigation( $where, $in_same_term, $excluded_terms, $taxonomy, $post ){         if( $post->post_type == 'private-page' ){             $where = str_replace( "'private-page'", "'do not show this'", $where );         }         return $where;     }       /**      * Function that returns the id for the private page for the provided user      * @param $user_id the user id for which we want to get teh private page for      * @return mixed      */

CREATE A NEW OBJECT

The last thing we're doing is create a new object with our CL_Client_Portal class.

1                        
                          $CP_Object                          =                          new                          CL_Client_Portal(                          )                          ;                        

ENTIRE PLUGIN

After all this, the entire plugin looks like this:

<?php /**  * Plugin Name: Client Portal  * Plugin URI: http://www.cozmoslabs.com/  * Description:  Build a company site with a client portal where clients login and see a restricted-access, personalized page of content with links and downloads.  * Version: 1.0.0  * Author: Cozmoslabs, Madalin Ungureanu, Antohe Cristian  * Author URI: http://www.cozmoslabs.com  * License: GPL2  */ /*  Copyright 2015 Cozmoslabs (www.cozmoslabs.com)       This program is free software; you can redistribute it and/or modify     it under the terms of the GNU General Public License, version 2, as     published by the Free Software Foundation.       This program is distributed in the hope that it will be useful,     but WITHOUT ANY WARRANTY; without even the implied warranty of     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the     GNU General Public License for more details.       You should have received a copy of the GNU General Public License     along with this program; if not, write to the Free Software     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA */ /* * Define plugin path */   class CL_Client_Portal {     private $slug;     private $defaults;     public $options;         function __construct()     {         $this->slug = 'cp-options';         $this->options = get_option( $this->slug );         $this->defaults = array(                                 'page-slug' => 'private-page',                                 'restricted-message' => __( 'You do not have permission to view this page.', 'client-portal' ),                                 'portal-log-in-message' => __( 'Please log in in order to access the client portal.', 'client-portal' )                                 );           /* register the post type */         add_action( 'init', array( $this, 'cp_create_post_type' ) );         /* action to create a private page when a user registers */         add_action( 'user_register', array( $this, 'cp_create_private_page' ) );         /* remove the page when a user is deleted */         add_action( 'deleted_user', array( $this, 'cp_delete_private_page' ), 10, 2 );         /* restrict the content of the page only to the user */         add_filter( 'the_content', array( $this, 'cp_restrict_content' ) );         /* add a link in the Users List Table in admin area to access the page */         add_filter( 'user_row_actions', array( $this, 'cp_add_link_to_private_page' ), 10, 2);           /* create client portal extra information */         add_filter('the_content', array( $this, 'cp_add_private_page_info'));           /* create the shortcode for the main page */         add_shortcode( 'client-portal', array( $this, 'cp_shortcode' ) );           /* create the settings page */         add_action( 'admin_menu', array( $this, 'cp_add_settings_page' ) );         /* register the settings */         add_action( 'admin_init', array( $this, 'cp_register_settings' ) );         /* show notices on the admin settings page */         add_action( 'admin_notices', array( $this, 'cp_admin_notices' ) );         // Enqueue scripts on the admin side         add_action( 'admin_enqueue_scripts', array( $this, 'cp_enqueue_admin_scripts' ) );         /* flush the rewrite rules when settings saved in case page slug was changed */         add_action('init', array( $this, 'cp_flush_rules' ), 20 );           /* make sure we don't have post navigation on the private pages */         add_filter( "get_previous_post_where", array( $this, 'cp_exclude_from_post_navigation' ), 10, 5 );         add_filter( "get_next_post_where", array( $this, 'cp_exclude_from_post_navigation' ), 10, 5 );       }       /**      * Function that registers the post type      */     function cp_create_post_type() {           $labels = array(             'name'               => _x( 'Private Pages', 'post type general name', 'client-portal' ),             'singular_name'      => _x( 'Private Page', 'post type singular name', 'client-portal' ),             'menu_name'          => _x( 'Private Page', 'admin menu', 'client-portal' ),             'name_admin_bar'     => _x( 'Private Page', 'add new on admin bar', 'client-portal' ),             'add_new'            => _x( 'Add New', 'private Page', 'client-portal' ),             'add_new_item'       => __( 'Add New Private Page', 'client-portal' ),             'new_item'           => __( 'New Private Page', 'client-portal' ),             'edit_item'          => __( 'Edit Private Page', 'client-portal' ),             'view_item'          => __( 'View Private Page', 'client-portal' ),             'all_items'          => __( 'All Private Pages', 'client-portal' ),             'search_items'       => __( 'Search Private Pages', 'client-portal' ),             'parent_item_colon'  => __( 'Parent Private Page:', 'client-portal' ),             'not_found'          => __( 'No Private Pages found.', 'client-portal' ),             'not_found_in_trash' => __( 'No Private Pages found in Trash.', 'client-portal' )         );           $args = array(             'labels'             => $labels,             'description'        => __( 'Description.', 'client-portal' ),             'public'             => true,             'publicly_queryable' => true,             'show_ui'            => true,             'show_in_menu'       => false,             'query_var'          => true,             'capability_type'    => 'post',             'has_archive'        => false,             'hierarchical'       => true,             'supports'           => array( 'title', 'editor', 'thumbnail' )         );           if( !empty( $this->options['page-slug'] ) ){             $args['rewrite'] = array( 'slug' => $this->options['page-slug'] );         }         else{             $args['rewrite'] = array( 'slug' => $this->defaults['page-slug'] );         }           register_post_type( 'private-page', $args );     }       /**      * Function that creates the private page for a user      * @param $user_id the id of the user for which to create the page      */     function cp_create_private_page( $user_id ){         /* make sure get_userdata() is available at this point */         if(is_admin()) require_once( ABSPATH . 'wp-includes/pluggable.php' );           $user = get_userdata( $user_id );         $display_name = '';         if( $user ){             $display_name = ($user->display_name) ? ($user->display_name) : ($user->user_login);         }           $private_page = array(             'post_title'    => $display_name,             'post_status'   => 'publish',             'post_type'     => 'private-page',             'post_author'   => $user_id         );           // Insert the post into the database         wp_insert_post( $private_page );     }       /**      * Function that deletes the private page when the user is deleted      * @param $id the id of the user which page we are deleting      * @param $reassign      */     function cp_delete_private_page( $id, $reassign ){         $private_page_id = $this->cp_get_private_page_for_user( $id );         if( !empty( $private_page_id ) ){             wp_delete_post( $private_page_id, true );         }     }       /**      * Function that restricts the content only to the author of the page      * @param $content the content of the page      * @return mixed      */     function cp_restrict_content( $content ){         global $post;         if( $post->post_type == 'private-page' ){               if( !empty( $this->options['restricted-message'] ) )                 $message = $this->options['restricted-message'];             else                 $message = $this->defaults['restricted-message'];               if( is_user_logged_in() ){                 if( ( get_current_user_id() == $post->post_author ) || current_user_can('delete_user') ){                     return $content;                 }                 else return $message;             }             else return $message;           }         return $content;     }       /**      * Function that adds a link in the user listing in admin area to access the private page      * @param $actions The actions available on the user listing in admin area      * @param $user_object The user object      * @return mixed      */     function cp_add_link_to_private_page( $actions, $user_object ){         $private_page_id = $this->cp_get_private_page_for_user( $user_object->ID );         if( !empty( $private_page_id ) ){             $actions['private_page_link'] = "<a class='cp_private_page' href='" . admin_url( "post.php?post=$private_page_id&action=edit") . "'>" . __( 'Private Page', 'client-portal' ) . "</a>";         }           return $actions;     }       /**      * Function that creates a private page extra information div      * @param $content the content of the private page      * @return mixed      */     function cp_add_private_page_info( $content ){         global $post;         if ( is_singular('private-page') && is_user_logged_in() ){             // logout link             $logout_link = wp_loginout( home_url(), false);               // author display name. Fallback to username if no display name is set.             $author_id=$post->post_author;             $user = get_user_by('id', $author_id);             $display_name = '';             if( $user ){                 $display_name = ($user->display_name) ? ($user->display_name) : ($user->user_login);             }               $extra_info = "<p class='cp-logout' style='border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 0.5rem 0; text-align: right'> $logout_link - $display_name </p>";               return  $extra_info . $content;         }           return $content;     }       /**      * Function that creates a shortcode which redirects the user to its private page      * @param $atts the shortcode attributes      */     function cp_shortcode( $atts ){         if( !is_user_logged_in() ){             if( !empty( $this->options['portal-log-in-message'] ) )                 $message = $this->options['portal-log-in-message'];             else                 $message = $this->defaults['portal-log-in-message'];               return $message;         }         else{             $user_id = get_current_user_id();             $private_page_id = $this->cp_get_private_page_for_user( $user_id );             $private_page_link = get_permalink( $private_page_id );             ?>             <script>                 window.location.replace("<?php echo $private_page_link ?>");             </script>         <?php         }     }       /**      * Function that creates the admin settings page under the Users menu      */     function cp_add_settings_page(){         add_users_page( 'Client Portal Settings', 'Client Portal Settings', 'manage_options', 'client_portal_settings', array( $this, 'cp_settings_page_content' ) );     }       /**      * Function that outputs the content for the settings page      */     function cp_settings_page_content(){         /* if the user pressed the generate button then generate pages for existing users */         if( !empty( $_GET[ 'cp_generate_for_all' ] ) && $_GET[ 'cp_generate_for_all' ] == true ){             $this->cp_create_private_pages_for_all_users();         }           ?>         <div class="wrap form-wrap">               <h2><?php _e( 'Client Portal Settings', 'client-portal'); ?></h2>               <?php settings_errors(); ?>               <form method="POST" action="options.php">                   <?php settings_fields( $this->slug ); ?>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="page-slug"><?php echo __( 'Page Slug' , 'client-portal' ) ?></label>                     <input type="text" class="widefat" id="page-slug" name="cp-options[page-slug]" value="<?php echo ( isset( $this->options['page-slug'] ) ? $this->options['page-slug'] : 'private-page' ); ?>" />                     <p class="description"><?php echo __( 'The slug of the pages.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label"><?php echo __( 'Generate pages' , 'client-portal' ) ?></label>                     <a class="button" href="<?php echo add_query_arg( 'cp_generate_for_all', 'true', admin_url("/users.php?page=client_portal_settings") ) ?>"><?php _e( 'Generate pages for existing users' ); ?></a>                     <p class="description"><?php echo __( 'Generate pages for already existing users.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="restricted-message"><?php echo __( 'Restricted Message' , 'client-portal' ) ?></label>                     <textarea name="cp-options[restricted-message]" id="restricted-message" class="widefat"><?php echo ( isset( $this->options['restricted-message'] ) ? $this->options['restricted-message'] : $this->defaults['restricted-message'] ); ?></textarea>                     <p class="description"><?php echo __( 'The default message showed on pages that are restricted.', 'client-portal' ); ?></p>                 </div>                   <div class="scp-form-field-wrapper">                     <label class="scp-form-field-label" for="portal-log-in-message"><?php echo __( 'Portal Log In Message' , 'client-portal' ) ?></label>                     <textarea name="cp-options[portal-log-in-message]" id="portal-log-in-message" class="widefat"><?php echo ( isset( $this->options['portal-log-in-message'] ) ? $this->options['portal-log-in-message'] : $this->defaults['portal-log-in-message'] ); ?></textarea>                     <p class="description"><?php echo __( 'The default message showed on pages that are restricted.', 'client-portal' ); ?></p>                 </div>                   <?php submit_button( __( 'Save Settings', 'client_portal_settings' ) ); ?>               </form>         </div>     <?php     }       /**      * Function that registers the settings for the settings page with the Settings API      */     public function cp_register_settings() {         register_setting( $this->slug, $this->slug );     }       /**      * Function that creates the notice messages on the settings page      */     function cp_admin_notices(){         if( !empty( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' ) {             if( !empty( $_GET['cp_generate_for_all'] ) && $_GET['cp_generate_for_all'] == true ) {                 ?>                 <div class="notice notice-success is-dismissible">                     <p><?php _e( 'Successfully generated private pages for existing users.', 'client-portal'); ?></p>                 </div>                 <?php                 if( !empty( $_REQUEST['settings-updated'] ) && $_GET['settings-updated'] == 'true' ) {                     ?>                     <div class="notice notice-success is-dismissible">                         <p><?php _e( 'Settings saved.', 'client-portal'); ?></p>                     </div>                 <?php                 }             }         }     }       /**      * Function that enqueues the scripts on the admin settings page      */     function cp_enqueue_admin_scripts() {         if( !empty( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' )             wp_enqueue_style( 'cp_style-back-end', plugins_url( 'assets/style.css', __FILE__ ) );     }       /**      * Function that flushes the rewrite rules when we save the settings page      */     function cp_flush_rules(){         if( isset( $_GET['page'] ) && $_GET['page'] == 'client_portal_settings' && isset( $_REQUEST['settings-updated'] ) && $_REQUEST['settings-updated'] == 'true' ) {             flush_rewrite_rules(false);         }     }         /**      * Function that filters the WHERE clause in the select for adjacent posts so we exclude private pages      * @param $where      * @param $in_same_term      * @param $excluded_terms      * @param $taxonomy      * @param $post      * @return mixed      */     function cp_exclude_from_post_navigation( $where, $in_same_term, $excluded_terms, $taxonomy, $post ){         if( $post->post_type == 'private-page' ){             $where = str_replace( "'private-page'", "'do not show this'", $where );         }         return $where;     }       /**      * Function that returns the id for the private page for the provided user      * @param $user_id the user id for which we want to get teh private page for      * @return mixed      */     function cp_get_private_page_for_user( $user_id ){         $args = array(             'author'            =>  $user_id,             'posts_per_page'    =>  1,             'post_type'         => 'private-page',         );         $users_private_pages = get_posts( $args );           if( !empty( $users_private_pages ) ){             foreach( $users_private_pages as $users_private_page ){                 return $users_private_page->ID;                 break;             }         }     }       /**      * Function that returns all the private pages post objects      * @return array      */     function cp_get_all_private_pages(){         $args = array(             'posts_per_page'    =>  -1,             'numberposts'       =>   -1,             'post_type'         => 'private-page',         );           $users_private_pages = get_posts( $args );         return $users_private_pages;     }       /**      *  Function that creates private pages for all existing users      */     function cp_create_private_pages_for_all_users(){         $all_users = get_users( array(  'fields' => array( 'ID' ) ) );         if( !empty( $all_users ) ){             foreach( $all_users as $user ){                 $args = array(                     'author'            =>  $user->ID, // I could also use $user_ID, right?                     'posts_per_page'    => 1,                     'post_type'         => 'private-page',                 );                 $users_private_pages = get_posts( $args );                 if( empty( $users_private_pages ) ) {                     $this->cp_create_private_page( $user->ID );                 }               }         }     }   }   $CP_Object = new CL_Client_Portal();

You can download the entire plugin from WordPress.org

Get Client Portal Plugin

Moving forward

The reason we were able to make this work in ~430 lines of code (~30% comments) is simply that we can offload anything else we might need to existing free plugins, which we can use as add-ons:

  • You can use login and registration from Profile Builder
  • If you want paid users you can also use Paid Member Subscriptions
  • Adding extra fields to the private page custom post type is possible with WordPress Creation Kit

Obviously there are other plugins that do the above so any login, membership, or custom fields plugin will allow you to extend the Client Portal plugin to fit your needs exactly. And an SEO plugin will help you attract more visitors to sign up to your client portal.

Get Client Portal Plugin

Subscribe to get early access

to new plugins, discounts and brief updates about what's new with Cozmoslabs!