Jump to content

[SOLVED] Hook Code to prevent user from changing the first three characters of a text_field


cwsoft
 Share

Recommended Posts

Posted (edited)

Dear all,

I am struggling with implementing a before/after hook (tested both, didn't work), which should prevent users from changing the first 3 characters of a string in a specific text_field. Here is what I came up with for the moment in my site/ready.php. The bd() debug calls indicated, that the "new" value is reset to the "old" value inside $event. However when clicking "Save" in the backend, the "new" value will be saved and populated instead. 

// Prevent user from changing the first three characters of a text_field on a specific page template.
$wire->addHookBefore('Page(template=my_template)::changed(text_field)', function (HookEvent $event) {
    // Ensure we have a valid page to save.
    $page = $event->object;
    if ($page->id === 0 || $page->isTrash) return;

    // Allow changes of the text_field value if the first three characters remain unchanged.
    $old = $event->arguments(1);
    $new = $event->arguments(2);
    if (mb_substr($new, 0, 3) === mb_substr($old, 0, 3)) return;

    // Revert back user input in case the first three chars differ.
    bd($event);
	$event->arguments(2, $old);
    bd($event);

	$event->warning('First three chars of text_field can  ot be changed. Reset to previous value.');
});

Seems I am missing something here, but I can't figure out what exactly. Could anybody give me a hint why this won't work? Do I need to implement this in the Page:saveReady hook instead? If yes I guess I would need to fetch the old values from the uncached page object to compare old/new values. Thats why I thought the Page:changed hook would make more sense.

Any hints or comments are welcome.

Cheers

Edited by cwsoft
marked solved
Link to comment
Share on other sites

I think the changed hook is not meant to revert any changes but only to listen to changes and trigger actions.

But you can do this (not sure this is the best solution) - example using the title field in /site/ready.php:

wire()->addHookAfter('Page::changed(title)', function (HookEvent $event) {
  $old = $event->arguments(1);
  $new = $event->arguments(2);

  $oldStart = substr($old, 0, 3);
  $newStart = substr($new, 0, 3);
  if ($oldStart === $newStart) return;

  // save old value to a temporary property that will be used
  // in the saveReady hook to revert to the old value
  $page = $event->object;
  $page->revertTitle = $old;
});
wire()->addHookAfter('Pages::saveReady', function (HookEvent $event) {
  $page = $event->arguments(0);
  if ($page->revertTitle) $page->title = $page->revertTitle;
});

 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Assuming that the field would be changed in Page Edit rather than as result of some other API code, you can validate the user input in a hook to InputfieldText::processInput.

Example:

$wire->addHookBefore('InputfieldText::processInput', function(HookEvent $event) {
	$inputfield = $event->object;
	$input = $event->arguments(0);
	$field = $inputfield->hasField;
	$page = $inputfield->hasPage;
	// For a particular field name and template name
	if($field && $field->name === 'text_1' && $page && $page->template == 'events') {
		$old_value = $page->getUnformatted('text_1');
		// Return early if the old value is empty
		if(!$old_value) return;
		$new_value = $input[$inputfield->name];
		// If the first three characters have changed
		if(substr($new_value, 0, 3) !== substr($old_value, 0, 3)) {
			// Show an error message
			$inputfield->error('You are not allowed to change the first three characters of the "Text 1" field.');
			// Replace the hooked method so the new value won't be saved to the field
			$event->replace = true;
		}
	}
});

image.png.a6deb3d81cd908159a6d441a65eb4e3d.png

  • Like 5
  • Thanks 1
Link to comment
Share on other sites

Posted (edited)

@bernhard @Robin S: Thank you very much for your code suggestions. Looks very promising. Will try both tomorrow and then report back. Giving an additional hint/warning directly to the corresponding inputfield like robin suggested seems a good idea in terms of usability. The idea of a temporary field is also a need trick. Tried with saveReady before, which seems to work, but felt a bit hacky to me, as I needed to fetch the uncached old value to check for potential changes in the text field. Always good to learn something new. Your answers are highly appreciated. Made my day. 

Edited by cwsoft
  • Like 2
Link to comment
Share on other sites

Interesting topic!

I don’t know guys how you acquired this mastering of PW hook. I’d be struggling like @cwsoft 😅.
It would be very useful to have some deeper documentation / tutorial about using hooks in PW.

  • Like 1
Link to comment
Share on other sites

Posted (edited)

As promised some feedback. The easiest thing I came up with was a saveReady Hook, where I fetched the previous page state from the database and compared the fields needed with the actual values. However the code became a bit messy, as I have two templates one with three input fields one with two, ending in a gigantic switch block hard to maintain. 

What worked too, was the proposal from Robin S. However fetching the actual values from Inputfields of various types (text, datetime, select array, page select) needs some custom tweaks here and there to get the field values. Things can get tricky very fast, when using multiple hooks, which interfere with each other. Here it‘s key to understand what hooks fire first etc.

At the end, I managed what I had in mind, but it indeed was a long way with lots of trial and error, reading the tutorial over and over again and inspecting the inputfield classes and with quite a lot of life debugging using Tracy. Without Tracy and the ideas and snippets found in the forum, I wouldn‘t be able to solve what I had in mind. So yes, managing hooks is still on my todo list, even after one year using ProcessWire for two part time projects.

Edited by cwsoft
  • Like 1
Link to comment
Share on other sites

Posted (edited)

After playing about two evenings with various kinds of hooks, I just used a plain old Pages::saveReady hook, as this allows me to access all page fields of a certain page/template more easily. So my final code looks like this now.

// Ensure first three chars of the required field 'location_place' can't be changed.
$wire->addHookAfter('Pages::saveReady(template=location)', function (HookEvent $event) {
    // Ensure we have a valid page.
    $page = $event->arguments(0);
    if (!$page || $page->isTrash) return;

    // Get actual field values (required) entered by the user.
    $actualPlace = $page->get('location_place');

    // Fetch previous page from database so we can check if 'location_place' has changed.
	$cachedPage = pages()->getById($page->id, array('cache' => false, 'getFromCache' => false, 'getOne' => true));
    if ($cachedPage->id > 0) {
    	$oldPlace = $cachedPage->getUnformatted('location_place');
        if ($oldPlace && substr($actualPlace, 0, 3) !== substr($oldPlace, 0, 3)) {
        	// Reset location_place to previous value and show an error on the inputfield.
            $page->location_place = $oldPlace;
            $inputfield = $page->getInputfield('location_place');
            $inputfield->error('First three characters can not be changed. Input reset to previous value.');
			// If you prefer less drama, just show a message instead of the error state on the input field itself.
			// $event->message('First three characters can not be changed. Input reset to previous value.');
         }
     }
});

However I may play with this hook too, just to check it's potential use case: https://processwire.com/api/ref/fieldtype-text/save-field-ready/

Cheers and thanks to @bernhard and @Robin S for your valuable feedback and proposals. Pretty sure they will come handy at some time for me.

Edited by cwsoft
Fixed $page->get call, thanks to Bernhard
Link to comment
Share on other sites

39 minutes ago, cwsoft said:

After playing about two evenings with various kinds of hooks, I just used a plain old Pages::saveReady hook, as this allows me to access all page fields of a certain page/template more easily. So my final code looks like this now.

Great you got it working and yeah, if you need to compare multiple values, then having all available in the $page object in saveReady sounds like a good solution!

40 minutes ago, cwsoft said:
$cachedPage = pages()->getById($page->id, array('cache' => false, 'getFromCache' => false, 'getOne' => true));

Some notes here:

  • I think this is the same as just using pages()->getFresh($page) ?
  • I think the variable name $cachedPage is not ideal, because it's actually the non-cached page. It's the page fresh from the DB, not the cached copy that lives in memory
  • I don't know this syntax: $actualPlace = $page->get('location_place', 'text', '');

And in hooks I tend to avoid nested IFs, so you might consider refactoring your hook to this (though yours is absolutely fine, of course):

<?php
// Ensure first three chars of the required field 'location_place' can't be changed.
$wire->addHookAfter('Pages::saveReady(template=location)', function (HookEvent $event) {
    // Ensure we have a valid page.
    $page = $event->arguments(0);
    if (!$page) return;
    if ($page->isTrash) return;
  
    // Load old page from DB
    $dbPage = wire()->pages->getFresh($page);

    // Get actual field values (required) entered by the user.
    $newPlace = (string)$page->get('location_place');
    $oldPlace = (string)$dbPage->get('location_place');

  	// if new and old value match --> nothing to do
	if (substr($newPlace, 0, 3) === substr($oldPlace, 0, 3)) return;
  
  	// otherwise reset location_place to previous value and show an error on the inputfield.
	$page->location_place = $oldPlace;
    $inputfield = $page->getInputfield('location_place');
    $inputfield->error('First three characters can not be changed. Input reset to previous value.');
	
    // If you prefer less drama, just show a message instead of the error state on the input field itself.
	// $event->message('First three characters can not be changed. Input reset to previous value.');
});

But when checking for multiple values it might be better to use if(...) { ... } multiple times as early exits won't work in such a scenario 🙂 

Link to comment
Share on other sites

Posted (edited)

@bernhardThanks for your suggestions. Will try the get Fresh call for sure. Fun fact is that my $cachedPage was named this way only in this example code. I used $dbPage and finally $oldPage for my own code.

I indeed revert back up to four fields (page select, select option, textfield and datetime) on two different templates split in two separate saveReady hooks via saveReady(template=xy). In addition I overwrite page name and page title to some unique values using parts of some input fields and take care of setting a draft value for the page title when a page is newly created but not yet saved (my page title is an auto field not directly set by the user). So early returns are not always possible in this special case.

I usally tend to use short circuit returns instead of nested ifs, whenever possible, which however isn‘t possible in my scenario, as the page name and the final title needs to be set last once all fields which may be part of it where sanitized and maybe reverted back to it‘s previous state. 

Edited by cwsoft
  • Like 1
Link to comment
Share on other sites

On 5/21/2025 at 10:54 PM, bernhard said:
  • I think this is the same as just using pages()->getFresh($page) ?
  • I don't know this syntax: $actualPlace = $page->get('location_place', 'text', '');

Can confirm, that pages()->getFresh($page) works as well. The $page->get('field', 'sanitizer_method', 'default') was accidentally taken over from an $input->get call. Fixed it in my example code above. Thanks.

  • Like 1
Link to comment
Share on other sites

  • cwsoft changed the title to [SOLVED] Hook Code to prevent user from changing the first three characters of a text_field

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
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...