ethanbeyer

Custom User Types, Page Edit vs. Profile Edit

Recommended Posts

Hello,

I recently posted in this topic, but I decided to start my own thread because while I believe my issue is related to the one in that thread, they are not exactly the same:

 

I have created a custom User Template in the method outlined in the docs. I am creating a directory, so it made sense that every page in the directory was a Directory Member, so they could log in and edit their own information while also keeping the entire directory protected behind a login wall.

So the new user type is created: "directory-member".

I then created two new roles: "member" and "directory-admin":

  • The "member" only has the ability to View directory-member pages, and "profile-edit", which allows them to manage their own information.
  • The "directory-admin" has the ability to edit any directory-member pages, and administer users.

Some Directory Members are both, but all have at least the "member" role.

The first hint that something was wrong was when I was testing a "member" user and I could not add a new item to a repeater on that profile. The url for the profile edit (this will be important shortly) is site.dev/admin/profile. The repeater is set up to load new items through AJAX. If this option is turned off, the rest of this issue is no longer completely valid. But as I have found what I believe to be a pretty large issue in the Processwire codebase, I thought it worth bringing up.

See, every page (even a user) has a $page->editUrl() method, and it returns a URL like this: site.dev/admin/access/users/edit/?id=2096. That's all good and fine for users that have page-edit permissions, but if they don't, that link will resolve to the admin's equivalent of a 404.

So the way that Processwire currently gets around this is by creating a specific editing area for a user to interact with only their profile: /admin/profile. And that works pretty nicely, except for the fact that nowhere is editUrl() ever made aware of the difference. editUrl() is not hookable, and whether or not a page is editable is based on the PagePermissions module.

On top of that, there are several core modules that hardcode a search-and-replace (see InputfieldRepeater.module:627) where the editing screen is for Users. This doesn't allow for a huge degree of flexibility that is offered in other places throughout Processwire. If line 627 of InputfieldRepeater is changed from this:

$editorUrl = str_replace('/access/users/edit/', '/page/edit/', $editorUrl);

to this:

$editorUrl = str_replace('/access/users/edit/', '/profile/', $editorUrl);

...the AJAX repeaters work. It's maddening!

As is brought up in the thread I attached above, a lot of the features of page editing are missing within /admin/profile/, and it just makes for an altogether strange editing experience. A user who has "page-edit" permissions for templates other than directory-member, but does have "profile-edit" permissions, will see their user in the Page List, but cannot edit their Page unless they hover over the wrench and click the "Profile" link. It just seems - off.

I think what this hinges on for me is that the editUrl() of the user should be "/admin/profile/" if that user is logged in (and their page should be editable from the Page List), or the "/admin/access/users/edit/" url; regardless of the URL, both links should resolve to the Page Edit screen, as the Profile Edit screen seems to be a unnecessarily neutered version of Page Edit.

  • Like 5

Share this post


Link to post
Share on other sites

@ethanbeyer Thank you for bringing this up. I also have the feeling that the profile edit page functionality can be improved.

I recreated your example setup on the latest dev branch.

Same behaviour here for repeater fields on the profile edit page of the member user when they only have profile-edit permissions.
If you add page edit permissions to the member role and then go to the user template Access settings and add view and edit permissions for the member role, then the repeater field is working.

But this is not a good solution. Because once you enable page-edit permissions for the member role, they get access to the page tree which might not be desired. And you'd have to go through all your templates' access settings to make sure that the member user can only do what you want to allow them to do which is a bit tedious.
Also it is not obvious that you need to give page edit permissions for the user template to a role that already has profile edit permissions. Permission to edit the profile should imply the permission to edit the user template.

All in all this is very confusing behaviour.

I'd consider this a bug. Don't know if others would agree and would be happy to hear their opinion on this.

 

Share this post


Link to post
Share on other sites

@Zekathanks for pointing me there. I wasn't even aware that this issue exists. Otherwise I wouldn't have created my issue regarding tabs not being displayed correctly.

 

  • Like 1

Share this post


Link to post
Share on other sites

@ethanbeyer Did you manage to work around the profile edit screen limitations?

I have a directory site where users are the directory pages. Their profiles have lots of fields, including tabs andf repeaters.
Since it was not possible to use the native profile edit process, I needed to find a workaround.
I have a custom Dashboard page in the admin where users have a link to their user page edit screen URL like /access/users/edit/?id=1377
To have this profileEditUrl easily available for every user, I added it as a custom property via a hook:

$this->addHookProperty('User(template=member)::profileEditUrl', $this, 'addPropertyProfileEditUrl');
	/**
	 * adds profile edit URL property to every member user 
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function addPropertyProfileEditUrl($event) {

		$user = $event->object;
		$event->return = $this->pages->get('template=admin, name=users')->url . "edit/?id={$user->id}"; 

	}

I had to give the member role page edit permissions for the member template. So I needed a way to make sure that they could not access other members' edit urls by just changing the id parameter in the URL.
My hook for that looks like

/**
  *  prevent member user from editing other member pages
  */
$this->addHookAfter('ProcessPageEdit::loadPage', function(HookEvent $event) {

  $id = $event->arguments[0];
  $user = $this->user;
  if($user->isSuperuser()) return;
  $editPage = $this->wire('pages')->get((int) $id);
  if($editPage->template != 'member') return;
  
  // normal members can only edit their own profile
  if($editPage->id != $user->id) wire('session')->redirect($user->profileEditUrl);


});

Could also have hooked into Page::editable and apply logic there, I guess...

  • Like 3

Share this post


Link to post
Share on other sites

@gebeer It looks like you got further into the solution than I did. Tabs and repeaters were a problem for the Profile Edit screen for me, too. So I eventually gave up and gave users of that template type a role that gave them page-edit permissions, but I hadn't worked out how to make sure that people were only editing their own profiles - so I think your two hooks are helpful there!

I know there's a difference in form and function for Profile Edit and Page Edit, but our scenario (where Users = Content)  seems like a time when keeping the two completely separate breaks down. I think your method is clean and I will give implementation a shot. Thank you!!

 

Edit: Just saw you'd posted twice in this thread, and just now read the first:

Quote

Because once you enable page-edit permissions for the member role, they get access to the page tree which might not be desired.

That. Exactly that.

9 hours ago, gebeer said:

Also it is not obvious that you need to give page edit permissions for the user template to a role that already has profile edit permissions. Permission to edit the profile should imply the permission to edit the user template.

In full! If it's on a profile page, and I've given the user the permission to edit their page...they should be able to do whatever is necessary on it.

  • Like 1

Share this post


Link to post
Share on other sites

@gebeer I've looked at your solution and put it in place - and it s a much better start than I would've thought of. I'm not sure it does everything I need it to right now, but I can keep tinkering and I will report back.

This is the first time when something ProcessWire does hasn't made a lick of sense to me.

Share this post


Link to post
Share on other sites

@ethanbeyer As I mentioned above, I have a custom dashboard process module with links to the areas that members are allowed to view/edit. One is the edit page for their profile at /access/users/edit/?id=xxxx.
I then redirect them to the dashboard upon login through a hook so they never see the page tree.

$this->addHookBefore('ProcessHome::execute', $this, 'rootPage');
	/**
	 * Redirect users with custom role to another page after login
	 */
	public function rootPage(HookEvent $event) {

		if($this->config->ajax) return; // this is needed so that ajax background tasks still work
		$user = $this->user;
		if($user->isSuperuser() || !$user->hasRole('member')) return;
		if($user->hasRole('member')) {
			$url = $user->statusPageUrl; // custom property added via hook that returns the dashboard URL
		}
		$this->session->redirect($url);		
	}

I also have a hook that prevents them from viewing the page tree (and other pages, if required).

 

    $this->addHookAfter('Page::viewable', $this, 'viewable');
	/**
	 * Give only superusers access to certain pages
	 */
	public function viewable(HookEvent $event) {
		
		if($this->config->ajax) return; // this is needed so that ajax background tasks still work
		$page = $event->object;
		if($this->user->isSuperuser() || $page->template != 'admin') return;
		$denyPages = array(3); // 3=page list
		if (in_array($page->id, $denyPages)) {
			$this->session->redirect($this->user->statusPageUrl);
		}
	}

I then deactivated Breadcrumbs for the member users so they don't see what else would be available.

The whole logic sits on top of a custom admin theme which is based on Reno theme. But I think this is not necessary at all. It just made it easier to remove the main navigation and other stuff that I didn't need.

With all that in place the members can now only see what they need to. And I can use the dashboard to render some summary information about their profile etc.

This was quite some work to setup but in the end it seemed more feasible to go that route rather than trying to change the core profile edit page functionalities.

But I still can't see the security benefits that come through the current restrictions in the profile edit page. If that page allows users to change their password why not allow them to change other information?

EDIT: Setting up a custom dashboard process module actually isn't that hard and can be fun @bernhard wrote a great tutorial about it.

  • Like 2

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By rafaoski
      This profile can be used as a business card or very simple blog.
      Milligram Site Profile For Processwire 3x with include functions like:
      MarkupRegions
      FunctionsAPI
      wireIncludeFile | wireRenderFile
      Essentially, this structure uses minimalist CSS framework Milligram and the Flexbox Grid System Gridlex
      Live Example
      CAN DOWNLOAD FROM THIS LINK ( Basic Version and simple Blog Version )
      https://github.com/rafaoski/site-milligram
      https://github.com/rafaoski/site-milligram-blog
      Screenshot:

      If you want to use Laravel Mix you must first ensure that Node.js and NPM are installed on your machine.
      Basic example to Debian and Ubuntu based Linux distributions:
      Node.js
      curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
      See more installation options LINK
      npm is installed with Node.js just check in linux terminal like below:
      node -v
      npm -v
      Set BrowserSync inside folder /templates/webpack.mix.js and change your dev url
      proxy: 'http://localhost/mix/', to your installation processwire folder like:
      proxy: 'http://localhost/your-processwire-installation-folder/',
      Next install npm packages in your templates folder with command npm install
      Now, boot up the dev server npm run watch, and you're all set go!
      On completion, use the command npm run production to build styles and scripts in the dist folder
      Simple Usage ( Basic Command )
      Run npm install Watch npm run watch Build npm run production All files to Webpack build steps is inside file ( webpack.mix.js )
      Folder With all SCSS files is inside templates/src/scss
      All build styles and scripts is inside the ( dist ) folder
      References:
      Milligram
      Gridlex
      Laravel Mix
      Feather Icons
      Web Font Loader
      Verlok Lazy Load
      Cookie Consent
      Particles.js
       
       
       
       
    • By louisstephens
      Ill be honest, I am a bit unsure how accomplish this. I have a repeater (dev_repeater) that contains an image field set to 1 image. Nested within this repeater, is another repeater (dev_child_repeater) that allows a user to add in some urls. However, there is also a hidden field that I am trying to pass the parent repeater's image path. 
      I know I can output all the data by using:
      <?php foreach($page->dev_repeater as $repeater) { foreach($repeater as $url) { # do some stuff } } ?> For the life of me, I can not figure out how to obtain the image url in my php to pass to a variable inside the nested foreach loop. Hopefully this made some sense.
    • By Xonox
      Hi,
      I have a template that it's working fine in development, however I can't get it to work on production! It shows every information inside repeater fields except the images.
      Here's the template:

      These are the circuit_day_image settings:

      This is the code:
      <?php foreach($page->circuit_days as $circuit_day) { if($circuit_day->circuit_day_image) { $day_image = $circuit_day->circuit_day_image->size(300, 300)->url; echo '<img src="' . $day_image . '" />'; } else { echo 'No image! :('; } echo '<h2>' . $circuit_day->title . '</h2>'; echo $circuit_day->body; } ?> I always get "No image! :("
      I think I'm doing everything right!
      Anyone else with a similar problem?
      Update
      After uploading the production database into my server, the images stopped working. It can be one of two problems:
      1. Bad field configuration;
      2. Something wrong with the Database.
      I can't find the problem. Any suggestion is welcome, thanks,
      Update 2
      I forgot to upload the images. It's working on dev and not on production. Still no clue!
      Clue 1
      When I insert
       <pre><?php print_r($circuit_day); ?></pre>
      On development I get a clean list for each repetition:

      However, on production, the command gets on a weird recursive loop that takes forever (it even slows the browser to a halt):

      What might be going on?
    • By burning
      Hi All, maybe a really stupid question, but is there a way to sort repeater items on -created?
      Like $page->social_activity('limit=10,sort=created'), with 'social_activity' as the repeater field?
      Tried this but it didnt work 😞 
      Any help appreciated! 
    • By karian
      Hi, based on the work of @microcipcip and @gebeer  (see their posts here and here), I put together a Processwire + React boilerplate (profile).
      Github repo: https://github.com/lapico/process-react

      Cheers,
      K