wpsharks / s2member

s2Member® Framework (membership management for WordPress®).
64 stars 36 forks source link

Feature Request: Client Portal; i.e., User-Specific Pages #551

Open jaswrks opened 9 years ago

jaswrks commented 9 years ago

A customer writes...

The one feature I have not seen that I need is the ability to have each member have their own page similar to their profile page that can have documents and files uploaded to it from my clients My SQL database. Is that possible with S2?

This is possible, but it is non-trivial. The closest (friendly) way to accomplish this is by using a Special Redirection URL in your Login Welcome Page configuration. One which includes the %%user_nicename%% Replacement Code mentioned in this area.

See: Dashboard → s2Member → General Options → Login Welcome Page


Feature Request

Add a new endpoint to WordPress when s2Member is installed, which only the current user can gain access to. For instance: http://example.com/u/johndoe22

KTS915 commented 9 years ago

This is a great idea, but there are several things that need to be taken into account at the same time.

First, I think the basic User-Specific Page should be a page, rather than a post, so that it is then possible to have sub-pages that are automatically also specific to the user.

Second, the (parent) User-Specific Page should be the page to which the user is redirected on login. This is clearly the idea of the Special Redirection URLs and, especially because of the next point, it is very important.

Third, it should not be possible for one user to learn of the existence of another user's User-Specific Page (or sub-pages). This means that the User-Specific Page should not, for example, show up in menus or searches. A "Your Page" type of entry in a menu would be useful, of course, provided that it would automatically redirect to the user's own User-Specific Page.

Fourth, there needs to be a default Login Welcome page for users who do not have their own User-Specific Page. (I don't know what currently happens when a Special Redirection URL is set and a member logs in who doesn't have such a URL/page.)

patdumond commented 9 years ago

Oh, yes! Yes! All good points, @KTS915.

jaswrks commented 9 years ago

@KTS915 Great feedback and ideas! TY ~ I'll scan over your list as work continues on this.

jaswrks commented 8 years ago

@raamdev @KTS915 I'm trying to come up with some ideas for how we might implement this in a way that makes it easy for a site owner to build and customize pages for specific users whenever the site is being used almost exclusively as a client portal.

WP Snippets comes to mind as something that we might harness for this. Thoughts? https://wordpress.org/plugins/wp-snippets/

raamdev commented 8 years ago

@jaswsinc The two things that I feel would greatly improve the experience of setting up a Client Portal would be the following:

Keep in mind that most Client Portal scenarios that I have seen are really simple and most of the features in s2Member are overkill for those scenarios. The things I described above might sound like they "cripple" things a bit, but that's actually the whole point.

Whenever I've had to explain how a site owner could use s2Member to create a Client Portal, the most challenging things were explaining how to use a replacement code to create a dynamic LWP, and then explain that a page with a Slug that matches the username of the user must be created. Then I have to explain that listing additional pages the user has access to is an entirely manual process, as is restricting access to a page to one user (using Custom Capabilities).

The flexibility and power offered by s2Member simply makes the process of setting up a Client Portal (which is often a very simple, one-user-one-page + list of file downloads or other pages the user has access to) a lot more work than it really needs to be at the moment, and the above things, plus a new video and/or KB Article(s) would greatly improve the process.

KTS915 commented 8 years ago

@jaswsinc, @raamdev,

I agree with almost all of what @raamdev has said. Client Portals are generally much simpler than the sorts of sites for which s2Member was originally designed. But they also have some very specific requirements.

  1. The first thing is that the Membership Options Page should play no visible role for logged-out users. They should, instead, be directed straight to the login page. The mu-plugins code here (https://github.com/websharks/s2member-kb/issues/131) gets that job done nicely. But it would be even better if activation of Raam's "Enable Client Portal Mode" would bring that code into play automatically without the need for an mu-plugin.
  2. I don't think "Enable Client Portal Mode" should have any effect at all on posts. For this sort of site, posts will essentially be reserved for news and PR stuff, which would generally be public anyway, so there's no need to over-complicate things.
  3. Pages are, therefore, where it all happens. I like Raam's idea of a "dropdown of users ... for flagging the page as the user's LWP." My one point of disagreement with Raam is that I don't think there should be a checkbox to indicate which page is the LWP.

My reason for disagreeing is that I think this could get extremely confusing. Instead, I think what's needed is to make use of the fact that pages can have parents and children. So client Smithco & Sons gets something like mysite.com/smithco/ as its LWP, and all child pages of mysite.com/smithco/ then automatically "belong" to Smithco.

This way everything is standardized. The parent "root" page is always a client's LWP. And every other page belonging to that client is a child (or, feasibly, a grandchild) of this (grand)parent.

  1. Doing it like this means that it should also facilitate Raam's idea of still allowing s2Member levels to work for "common" pages. I agree with this idea, which avoids the need to duplicate content unnecessarily.
  2. But then we come to the trickier bits. Raam is right that there needs to be a way for the client to see a list of all "its" pages. But that's not all that needs to be available. Every client will also need to be able to see a list of its files. These are likely to include things like quotes, contracts, specifications, invoices, and receipts.

Obviously, s2Member is already well equipped for storing such files. Presumably, though, another consequence of "Enable Client Portal Mode" should be that the slug of the parent page should become a ccap that dictates the relevant s2member-files sub-folder for that client.

But there still needs to be some way of enabling the client to find all these files. The simplest way would, obviously, require just a shortcode. But there would need to be some options here, such as ordering by date, or grouping by type. And, of course, grouping by type means being able to store by type too.

  1. That leaves menus and widgets. Maybe widgets can be dealt with just by having a shortcode or two, with various options, that can be included in text widgets. Obviously, these shortcodes would need to take relevant variables, so as to relate to the right client. But that should work out fine.

I think menus are trickier. One option would be to do nothing here. That would be fine for a site owner with only a few clients. But it could get out of hand for someone with lots of clients.

I am not sure if this is where WP Snippets might come in handy. Perhaps it could provide (a) a new menu area at the top of each client's pages, and (b) some way of making just the client's own pages available to be included in such a menu.

Maybe -- and I'm not sure about this -- there could even be an option for the site owner to set up a default set of pages (e.g. LWP, files page, profile page, payments and invoices page) and for them to form a default menu.

KTS915 commented 8 years ago

Hmm, evidently I need to find out how to continue previous enumeration on here!

jaswrks commented 8 years ago

Woohoo! Wow, that's some great insight from both of you. I'm soaking some of this in at the moment, but I'll see what I can get started with so we can try to move this forward. I will post some follow-ups soon. Thanks so much :-)

mitdrivhus commented 8 years ago

This sounds great and is just what I need for my site. Some thoughs from me:

KTS915 commented 8 years ago

@mitdrivhus,

I like the idea of a "a menu item called Clients" for all the reasons you give. It could also, perhaps, have a box where the admin can choose the slug/ccap -- which, when saved, could also auto-generate the equivalent s2member-files subfolder.

KTS915 commented 8 years ago

In this thread (https://wordpress.org/support/topic/custom-capabilities-not-working-1?replies=10#post-7859605) caseyfriday apparently both creates a ccap and a custom post type when a user is created.

Presumably, a similar approach could be adopted for a client portal, albeit setting both a ccap and a page protected by that ccap when a user is created.

KTS915 commented 8 years ago

Just come across this plugin and thought you might be interested to see how it works: https://wordpress.org/plugins/client-portal/

In fact, all it does is create a simple means of generating private pages for users, and then offers a shortcode to be placed on a page which redirects each user to his or her own private page.

The shortcode redirection is too slow, and you get a flash of the original page before the redirection, while the plugin also offers no means of creating sub-pages for each user.

But the method of generating one private page for each user works well.

KTS915 commented 7 years ago

I have been thinking about this a lot recently because I'd really like to build some sites with this facility. Re-reading this discussion, I think we need to take things in stages, and just start with the basics. So I've been trying to work out what I might suggest that @jaswsinc, @raamdev and @patdumond might be able to create right now. I suggest the following things:

  1. Whenever a new member is added to the site, a new ccap is automatically created that corresponds to the member's username (or nicename). In other words, a member with nicename smithco causes the ccap smithco to be generated.
  2. That user is then automatically assigned the ccap that corresponds to theeir nicename. So a member with nicename smithco is automatically given the ccap smithco.
  3. Pages protected with a ccap cause their sub-pages to be protected with the same ccap.

What I think this then produces is the ability to have users automatically redirected to their own LWP by means of the %%current_user_nicename%% and the using the Special Redirection URL facility. It also keeps others out, and enables the site admin to create sub-pages for that particular member that will be automatically available only to that member.

In addition, sub-folders within the s2member-files folder will automatically work. So the subfolder smithco within the the s2member-files folder could automatically be used to store files for that member. Whether s2Member needs to generate the sub-folder automatically is open to debate, but the point is that the facility to protect that member's files will be generated as soon as the member is created.

Does this sound doable in the short term?

@mitdrivhus, @ethanpil: would this work for you?

mitdrivhus commented 7 years ago

Yep. It sure sounds like a solution 👍🏼 …

KTS915 commented 7 years ago

Coming back to this again, I am trying to create an mu-plugin that will achieve this with the current functionality of s2Member and with a minimum of manual intervention.

The only bits of the puzzle that I'm missing are these:

Having first obtained the page ID, I have tried this: add_post_meta( $page_id, 's2member_ccaps_req', 'name-of-ccap');

But, while that adds the ccap to the page's metabox field, it doesn't add it to the admin's list of pages, and it doesn't enter the ccap into the database in a form that protects the page.

Any ideas?

jaswrks commented 7 years ago

Try it like this:

add_post_meta( $page_id, 's2member_ccaps_req', ['name-of-ccap']);
KTS915 commented 7 years ago

That works perfectly. Thank you, Jason! (Amazing what a pair of brackets can do, and I've no idea what role they perform, so I'll need to look up that syntax.)

Assuming that I can do the same thing using when I create a sub-page, that means the only thing left is to obtain the protections from the parent page. This is what I have so far:

function inherit_parent_protections( $post_id, $post, $update ) {
    if ( get_post_type() == 'page' ) {
        // Check if page has parent
        if ( $post->post_parent > 0 ) {
            $parent = wp_get_post_parent_id( $post_id );
            $ccaps = get_post_meta( $parent, 's2member_ccaps_req', true );
            add_post_meta( $post_id, 's2member_ccaps_req', [$ccaps] );
        }
    }
}
add_action( 'save_post', 'inherit_parent_protections', 10, 3 );

The problem here seems to be that I'm just not getting the protections of the parent, so I can't apply them to the new page.

(Updated, but it makes no difference to the result)

KTS915 commented 7 years ago

Aha, got it! It turned out that (a) I needed to use the wp_insert_post hook, and (b) because I am getting the ccaps from postmeta, I don't need to put them within brackets this time. So, in case anyone cares, this function now looks like this:

function inherit_parent_protections( $post_id, $post, $update ) {
    $post_type = get_post_type( $post_id );
    if ( "page" != $post_type ) {
        return;
    }

    // Check if page has parent
    if ( $post->post_parent > 0 ) {
        $parent = wp_get_post_parent_id( $post_id );
        $ccaps = get_post_meta( $parent, 's2member_ccaps_req', true );
        add_post_meta( $post_id, 's2member_ccaps_req', $ccaps );
    }
}
add_action( 'wp_insert_post', 'inherit_parent_protections', 10, 3 ); 
KTS915 commented 7 years ago

I'm pasting the full code below, and would really appreciate comments and suggestions. I have tried it on one site, where everything now works as I expected. (There was an issue before, which I've now resolved, and I have amended the code below accordingly.) Whether it meets others' expectations, I'd love to hear.

The code presupposes a regular install of s2Member with a regular Login Welcome Page (i.e. not a Special Redirection URL). It also presupposes that all clients are given one role. I have set it as s2member_level 2 but, any role is possible so long as the role is kept consistent throughout the code. What this code is then supposed to do is as follows:

  1. Take the MOP out of play for logged-out users by redirecting attempts to access protected content to the login page.
  2. Provide a Login Welcome Page (LWP) for each client with a page slug equivalent to the client's username.
  3. Enable other users, who are not clients, to continue to use the regular LWP.
  4. Protect the LWP with a ccap equivalent to the client's username.
  5. Protect all sub-pages (if and when created) of the client's LWP with a ccap equivalent to the client's username.
  6. Create a sub-folder of the s2member-files folder that is protected with a ccap equivalent to the client's username.
  7. When created as a site member, give the client a ccap equivalent to the client's username (and thus enable him or her to access the relevant pages and sub-folder).
  8. Add a shortcut to the list of Clients in the Admin Menu under Users.

So here's the code (now revised so that it will also work when a current member becomes a client, and also to ensure that pages and folders are not created when non-clients are added to the site):

<?php
/* AUTO-REDIRECT TO BYPASS MOP */
add_action( 'template_redirect', function () {
    if ( !is_page( S2MEMBER_MEMBERSHIP_OPTIONS_PAGE_ID ) ) {
        return; // nothing to do if no initial redirection to MOP
    }
    else if ( !empty( $_REQUEST["_s2member_vars"] ) ) {
        @list ( $restriction_type, $requirement_type, $requirement_type_value, $seeking_type, $seeking_type_value, $seeking_uri ) = explode ( "..", stripslashes( ( string )$_REQUEST["_s2member_vars"] ) );
    }
    if ( !empty( $seeking_uri ) ) {
        $URI = base64_decode( $seeking_uri );
    }
    if ( !is_user_logged_in() && !empty( $URI ) ) {
        $redirect = home_url( '/wp-login.php' ); // login page: change as required
        $redirect = add_query_arg( 'redirect_to', urlencode( $URI ), $redirect );
        wp_redirect( $redirect ); // perform the redirection
        exit;
    }
});

function s2_redirect( $redirect, $vars = array() ) {
    if ( ( isset( $_GET['action'] ) && $_GET['action'] != 'logout' ) || ( isset( $_POST['login_location'] ) && !empty( $_POST['login_location'] ) ) ) {
        $redirect = $_SERVER['HTTP_REFERER'];
        return $redirect;
    }
}
add_filter( 'ws_plugin__s2member_login_redirect', 's2_redirect', 10, 2 );

/* CREATE CLIENT PORTAL */
function create_client_portal( $user_id, $new_role, $old_role ) {
    if ( !$user_id > 0 ) {
        return;
    }
    if ( $new_role == 's2member_level2' ) {
        $data = get_userdata( $user_id );
        $username = strtolower( $data->user_login );

        // Create client portal page
        $client_page = array(
            'post_title' => 'Client Portal for ' . trim( $data->first_name . ' ' . $data->last_name ),
            'post_name' => $username,
            'post_type' => 'page',
            'post_content' => 'Welcome to your portal. Any messages for you will be posted here.',
            'post_status' => 'publish',
            'comment_status' => 'closed',
            'ping_status' => 'closed',
            'post_author' => 3 // ID of administrator: change as necessary
        );  
        $page_id = wp_insert_post( $client_page ); // insert into database and get page ID

        // Give client a custom capability equivalent to their username
        $ccap_username = 'access_s2member_ccap_' . $username;
        $user = new WP_User( $user_id );
        $user->add_cap( $ccap_username );

        // Require the same custom capability to access client portal page
        add_post_meta( $page_id, 's2member_ccaps_req', [$username] );

        // Create file storage folder requiring the same custom capability
        $url = WP_PLUGIN_DIR . '/s2member-files/access-s2member-ccap-' . $username;
        wp_mkdir_p( $url );
    }
}
add_action( 'set_user_role', 'create_client_portal', 10, 3 );

/* REDIRECT CLIENTS TO OWN CLIENT PAGE WHEN NOT SEEKING SPECIFIC URL */
function client_redirect_to_portal_page() {
    if ( !is_page( S2MEMBER_LOGIN_WELCOME_PAGE_ID ) ) {
        return; // nothing to do if no initial attempt to go to Login Welcome Page
    }
    if ( current_user_is( 's2member_level2' ) ) { // must match clients user role
        $current_user = wp_get_current_user();
        $username = $current_user->user_login;
        $page = get_page_by_path( $username, OBJECT );
        if ( isset( $page ) ) {
            $redirect = get_permalink( $page );
            wp_redirect( $redirect ); // perform the redirection
            exit;
        }
    }
}
add_action( 'template_redirect', 'client_redirect_to_portal_page' );

/* MAKE SUB-PAGE INHERIT CCAPS PROTECTION OF PARENT */
function inherit_parent_protections( $post_id, $post, $update ) {
    $post_type = get_post_type( $post_id );
    if ( 'page' != $post_type ) {
        return;
    }

    if ( $post->post_parent > 0 ) { // check if page has parent
        $parent = wp_get_post_parent_id( $post_id );
        $ccaps = get_post_meta( $parent, 's2member_ccaps_req', true );
        add_post_meta( $post_id, 's2member_ccaps_req', $ccaps );
    }
}
add_action( 'wp_insert_post', 'inherit_parent_protections', 10, 3 );

/* ADD CLIENTS AS ADMIN MENU ITEM WITHIN USERS MENU */
function admin_clients_menu() {
    add_users_page(__( 'Clients' ), __( 'Clients' ), 'read', 'users.php?role=s2member_level2'); // must match clients user role
}
add_action( 'admin_menu', 'admin_clients_menu' );
KTS915 commented 7 years ago

A variation on this approach, which will probably be preferable for those with a large number of clients, is to use a custom post type for client pages instead of a regular page. This allows clients' pages to be kept separate from regular pages.

If you'd prefer to do this, you first need to make three small changes to the above code. The first requires that you replace this line:

'post_type' => 'page',

with this:

'post_type' => 's2-client-page',

The second change is to replace this line:

if ( 'page' != $post_type ) {

with this:

if ( 's2-client-page' != $post_type ) {

The third is to replace this line:

$page = get_page_by_path( $username, OBJECT );

with this:

$page = get_page_by_path( $username, OBJECT, 's2-client-page' );

Then you can just add the following code after all the code above:

/* CREATE CLIENT PAGE CUSTOM POST TYPE */
function create_portal_page_custom_post_type() {
    $labels = array(
        'name'                => _x( 'Client Pages', 'client pages', 's2-client-portal' ),
        'singular_name'       => _x( 'Client Page', 'client page', 's2-client-portal' ),
        'menu_name'           => _x( 'Client Pages', 's2-client-portal' ),
        'name_admin_bar'      => _x( 'Client Page', 'add new on admin bar', 's2-client-portal' ),
        'add_new'             => _x( 'Add New', 's2-client-portal' ),
        'add_new_item'        => __( 'Add New Client Page', 's2-client-portal' ),
        'new_item'            => __( 'New Client Page', 's2-client-portal' ),
        'edit_item'           => __( 'Edit Client Page', 's2-client-portal' ),
        'update_item'         => __( 'Update Client Page', 's2-client-portal' ),
        'view_item'           => __( 'View Client Page', 's2-client-portal' ),
        'all_items'           => __( 'All Client Pages', 's2-client-portal' ),
        'search_items'        => __( 'Search Client Pages', 's2-client-portal' ),
        'parent_item_colon'   => __( 'Parent Client Page:', 's2-client-portal' ),
        'not_found'           => __( 'Not Found', 's2-client-portal' ),
        'not_found_in_trash'  => __( 'Not found in Trash', 's2-client-portal' ),
    );

    $args = array(
        'label'               => __( 'Client Pages', 's2-client-portal' ),
        'description'         => __( 'Client Page description', 's2-client-portal' ),
        'labels'              => $labels,
        // Features the Client Page CPT supports in post editor
        'supports'            => array( 'title', 'editor', 'author', 'thumbnail', 'comments', 'custom-fields', 'page-attributes' ), // change as you like
        'rewrite'        => array( 'slug' => 'clients', 'with_front' => false ),
        'hierarchical'        => true,
        'public'              => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'menu_position'       => 5, // change as you like
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'can_export'          => true,
        'has_archive'         => false,
        'exclude_from_search' => false,
        'query_var'           => true,
        'menu_icon'           => 'dashicons-businessman',
        'publicly_queryable'  => true,
        'capability_type'     => 'page',
    );

    // Register the Client Page Custom Post Type
    register_post_type( 's2-client-page', $args );
}
add_action( 'init', 'create_portal_page_custom_post_type', 0 );

Note: one thing you should do if you take this approach is to check your settings in s2Member -> Restriction Options -> Post Access Restrictions

Although this new custom post type for Client Pages will act like a page (so that it can have parents and children), WordPress still considers it a post. So if you have, for example, set all posts to be protected at a certain level, that protection will also be applied to the Client Pages you create. For many site admins, this might be a good thing, but you need to check to avoid surprises later on.

KTS915 commented 7 years ago

Having tested this further, I can say that it's easy to augment the above code to create subfolders to hold different types of files. Providing a client with the ability to download such files is then just a matter of installing the s2Member Secure File Browser plugin and including its shortcode in the code above.

For menus, you can provide a client with a list of accessible pages by installing an appropriate Child Pages plugin (whose shortcode you could also include in the code above to automate everything). This prevents the creation of an enormous menu that's out of control, and it can be done within a widget if preferred.

So I think, in fact, that that covers everything we have discussed on this thread. Unless someone has some additional comments, I would be content to see this thread closed.

jaswrks commented 7 years ago

(Amazing what a pair of brackets can do, and I've no idea what role they perform, so I'll need to look up that syntax.)

@KTS915 Woohoo! I love the work you're doing here. I'm reviewing it now, but just wanted to stop and answer the [] mystery. Those are array brackets. So instead of it being a string, the brackets make it an array. That's what the meta box does. It takes a comma-delimited list of CCAPs and converts them into an array. So that's why the array syntax was necessary there.

raamdev commented 7 years ago

the [] mystery.

Note also that's known as a short array syntax and was added as a new feature in PHP 5.4: http://php.net/manual/en/migration54.new-features.php (So instead of array('key'=>'value') you can just do ['key'=>'value'].)

jaswrks commented 7 years ago

Lead Developer Review

@KTS915 👍 💯 On your work here! I love the approach. Thank you for explaining how it all works too, and for documenting this for us and for others. I imagine this approach is likely to end up in a future KBA at the very least. So again, thank you!

jaswrks commented 7 years ago

Possible Enhancement

One thing that caught my eye in the technique above, is the way in which CCAPs are inherited from the parent, and that it's assumed there will always be just a single ancestor.

In the outline posted by @KTS915, CCAPs are inherited as they are saved, and so this parent/child relationship is not dynamic, which could make it somewhat difficult to update if CCAP requirements are altered in the future; i.e., those changes would then need to trickle-down to children. The way @KTS915 set this up makes it work just fine for this use case though.

So I can't say I'm against the approach taken. It works just fine!

However, perhaps a cleaner way of doing this would be to use the CPT (Custom Post Type) that he mentioned, and then have Posts of that type automatically inherit permissions from all of their ancestors, infinitely deep; i.e., inherit dynamically, instead of by copying/juggling them in the database metadata.

If anyone wants to try this, you could start by reviewing this file, which handles the security checks associated with single posts (of any type, except pages). For pages, see this file.

You'll notice there is already a special consideration in this file for posts, for bbPress forums. Protecting a bbPress forum post type will automatically protect any topics/replies in that forum. The same sort of thing could be applied to any post type, and that would make it more dynamic. Also, there is a function in WP core that might be helpful too. See: get_post_ancestors().

KTS915 commented 7 years ago

@jaswsinc: Thanks very much for your comments! I am pleased that you haven't found any major problems, and would be happy to write this up as a KBA.

Thanks too (and to @raamdev) for the explanation about the use of brackets in that bit of code.

You have also read my mind about the need to consider further what happens if a ccap is changed. I really intended that the ccaps would rarely be changed, but you are right that there needs nevertheless to be a way to do so without causing new problems.

I don't think the issue is so much that my code assume a single ancestor. Each page inherits its protections from its parent, so you can have an endless chain of "generations," where each new one inherits from the previous one. In fact, that's precisely how I intend to use it. The problem is simply what happens to the other generations when one generation's ccap protection changes.

It could be the first generation ancestor whose ccap changes, or it could be a change made in any other generation. For the use-cases for which I intend to use this code, simply having later generations inherit all the ccaps of the previous generations would be highly problematic because I think that would make it too easy to cause one client accidentally to be granted access to another client's pages without the user (or his/her organization) realizing his/her mistake.

So I'm thinking that a change to the ccap protection of the first generation ancestor should trigger a change to the ccap protection of all subsequent generations, but that a ccap change in any other generation should not do so. Instead, that should generate some sort of warning that there is a mismatch between the ccaps and one of the parent-child relationships. At the moment, I think that this would provide the best way of trying to ensure that such changes are not made by mistake and, when they are, making them noticeable and easy to correct.

KTS915 commented 7 years ago

Something I've just realized is that the fact that the inherit_parent_protections function fires via the wp_insert_post significantly mitigates the risk of mistakenly associating a page with the wrong ccap protection. This is because that hook fires on updates as well as on new posts, so that it's triggered when an attempt is made to change the ccaps.

All we then need to do is change this line:

add_post_meta( $post_id, 's2member_ccaps_req', $ccaps );

to this:

update_post_meta( $post_id, 's2member_ccaps_req', $ccaps );

This ensures that, if the page's parent has not changed, the ccap will be reset to the parent's ccap, as it should be, even if the ccap has just been manually changed. In other words, this method ensures that the only way to change a child page's ccap protection is by changing its parent.

And, just to be clear, although the new code talks of updating the post_meta instead of adding it, this still works even if a new page is being created, because it then reverts to adding the post_meta anyway.

So now I just need to work out the function for changing the ccap protection for all the sub-generations when that of the first generation (which, by definition, has no parent of its own) is changed.

KTS915 commented 7 years ago

I've finally worked out how to create a function that auto-updates the ccaps protection of all descendants to match that of the first ancestor if the latter's ccaps protection is changed:

/* UPDATE CCAPS FOR ALL CHILDREN WHEN CCAPS OF FIRST ANCESTOR CHANGE */
function update_s2_client_ccaps_meta( $meta_id, $post_id, $meta_key, $meta_value ) {
    $post_type = get_post_type( $post_id );
    if ( 's2-client-page' != $post_type ) {
        return; // only apply to Client Pages
    }
    $ancestors = get_post_ancestors( $post_id );
    if ( empty( $ancestors ) && 's2member_ccaps_req' == $meta_key ) { // check that page has no parent and that its ccaps protection has been updated
        $pages = get_pages( array( 'post_type' => $post_type ) );
        $children =&get_page_children( $post_id, $pages );
        foreach ( $children as $child ) {
            update_post_meta( $child->ID, 's2member_ccaps_req', $meta_value );
        }
    }
}
add_action( 'updated_post_meta', 'update_s2_client_ccaps_meta', 10, 4 );

This means that if the first ancestor's ccaps protection (i.e. the ccaps protection for the original client portal page) is changed so as to associate it with a different client, all its sub-pages (no matter how many generations there might be) will now be automatically given the same ccaps protection too and therefore only be accessible by that new client.

KTS915 commented 7 years ago

Thinking about this further, it raises the question of whether the update_s2_client_ccaps_meta function should be restricted in its operation only to situations when an updated client page has no ancestors.

Would it simply be better to leave out the empty( $ancestors ) && check and just have the ccaps protection of all sub-pages updated whenever an ancestor's protection is changed?

jaswrks commented 7 years ago

I don't think the issue is so much that my code assume a single ancestor. Each page inherits its protections from its parent, so you can have an endless chain of "generations," where each new one inherits from the previous one. In fact, that's precisely how I intend to use it. The problem is simply what happens to the other generations when one generation's ccap protection changes.

Right, I see. Thanks for pointing that out.

jaswrks commented 7 years ago

Would it simply be better to leave out the empty( $ancestors ) && check and just have the ccaps protection of all sub-pages updated whenever an ancestor's protection is changed?

I think so, yes. Actually, here's how I imagine it working best.


With that structure, then you can support an unlimited number of parent/child relationships with very little code. For example, you could have a child that is 12 levels deep, and it would inherit (automatically) all CCAP restrictions from each ancestor before it. Moving up the tree, a child at level 5 could introduce a new CCAP, and that would impact all levels beneath it.

KTS915 commented 7 years ago

@jaswsinc: Thanks for these comments.

Having thought about it further, I agree that it would be better to leave out the empty( $ancestors ) && check in my last piece of code.

I also agree that your suggestions as to how the whole process should work are technically far superior to what I have coded!

Nevertheless, they cause me two problems.

The first is that such coding requires a level of skill with PHP well above what I possess! So you -- or someone else with that level of skill -- would need to work out the necessary code modifications. It's simply beyond my expertise.

Second, though, I don't think your model of how this should work corresponds to the use to which I am planning to put this. I envision it being used by attorneys, for whom the possibility that additional ccaps might be added part-way down a "tree" of client pages would be quite unacceptable. Attorneys are under a strict obligation of client confidentiality, and permitting the addition of new ccaps would mean opening up access to client information to people other than the client. That would be a disciplinary offense. That's why my focus has been so linear: it deliberately associates each client page with just one client.

I am guessing that you have very different businesses or organizations in mind -- probably in a more co-operative environment -- for when a client portal may be useful, and I don't doubt at all that your approach would be better suited for those circumstances. I just don't think it's appropriate for the use-case I have in mind.