How to build a WordPress Client Portal plugin

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

There are solutions like simply creating a password protected page or using an existing plugin. But in some cases is just a bit too much.

In this tutorial we’ll learn how to build a simple WordPress Client Portal plugin step by step.

There are two main things we’ll go through: creating the client portal plugin and exploring what plugins we can integrate it with so we have a complete solution.

Building a WordPress Client Portal Plugin

While there seams 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 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 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
24
25
<?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
*/

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

Everytime a new user registers or is created by an administrator, we also need to create it’s private page. 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.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    /* 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.

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
    /* 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.

user private page

The filter we’re using is user_row_actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /* 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.

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
    /* 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 functionality 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 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.

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
    /* 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

client portal settings

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.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    /* 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 a 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.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    /* 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:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
<?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 because we can offload anything else we might need to existing free plugins:

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.

Get Client Portal Plugin

Subscribe to get early access

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

6 thoughts on “How to build a WordPress Client Portal plugin

  1. Hi,

    Thanks so much for this tutorial and plugin. I use it on my website and I love it. I have one small question. Is there a way I can have pre-entered data on each private page when a new one is created for each user. I use my private pages as a transcript and there is a specific template that needs to be inserted into each page, how would I go about doing that?

    Thanks

  2. Please I want to add a welcome note and other related content/data to be displayed immediately a user private page is created. Please guide me on how to do this. I love you for what you are doing, God will reward abundantly. More power to your elbow.

  3. Good day! Nice addon, but the user can change the content of other pages. How can I restrict my rights so that the user can only edit their own page?

    1. Hello Vasily,

      The idea of this plugin is not to let the user edit this page, but only the admin to place private content, specific to that user on it.

  4. I too would like to be able to add a generic text that would automatically go on all newly created private pages. Is this possible?
    Thank you

    1. Hello Charles,

      Yes, this is possible. After installing, go to Users -> Client Portal Settings and you will have a Content Box that lets you do this.

Leave a Reply

Your email address will not be published. Required fields are marked *