hugogogo
hugogogo

Reputation: 637

in wordpress, with ACF, make any field read-only for front

first let me clarify 2 things :

  1. by front I mean any client-side rendering, as opposed to server-side actions. So it can be the admin panel to edit a post for example.
  2. by read-only, I actually mean that the acf field will be displayed, but can not be modified.

ACF proposes to add 'disabled' or 'readonly' to the inputs elements that supports this attributes, but it's not quite sufficient :

  1. not all acf fields can be customized with readonly or disabled (of course, some fields don't need this, but others could use it, like checkbox or range)

  2. 'disabled' don't do the job : if you submit a form with a checkbox acf field disabled, it will delete the data of the field, so the field was modifiable, and in a bad way. You can only add it to the following fields :

    • with $field['disabled']=1; : 'text', 'textarea', 'number', 'email', 'url', 'password', 'date_picker', 'time_picker', 'select'
    • with $field['disabled']=array(<elements to disable>) : 'checkbox', 'radio'
  3. 'readonly' is available on even less fields, and don't removes the values from the post request, so if a page with the field is open and the field was modified elsewhere, if you submit this page because you made changes on something else, it will also override the changes of this 'readonly' field

So, I tried a solution : I hook when the fields are updated, and I check if it comes from the front, by looking if the $_POST['acf'] is set, but I don't know if it's reliable ?

function prevent_acf_field_from_updating($null, $value, $post_id, $field) {
  /*
  * if $_POST['acf'] is not set, the update is from server side, so I don't prevent it
  * I suppose ?
  *
  */
  if (!isset($_POST['acf'])) {
    return $null;
  }


  /*
  * to make a field disabled, I use a class
  *
  */
  if (!str_contains($field['wrapper']['class'], 'read_only_acf')) {
    return $null;
  }

  return false;
}
add_filter('acf/pre_update_value', 'prevent_acf_field_from_updating', 10, 4);

I also add inert attribute to the wrapper with filter acf/field_wrapper_attributes

But I'm really not sure it's a good way to do it, and if checking the $_POST['acf'] is reliable ?

Upvotes: 0

Views: 1177

Answers (1)

hugogogo
hugogogo

Reputation: 637

After more investigation, I found a way that I think is good, so I'll answer my own question (if you don't want to read all the reasoning, jump to the code at the end of the answer).

As CBroe pointed out in its comment, the acf fields are only rendered to the admin panel, not the front (in wordpress, 'front' does not mean what is accessed by web browser, but what is not admin and is accessed by web browser. Admin section is referred as backend).

So there is 2 places to prevent specific acf fields from being updated from admin :

  1. detect if they are from admin and prevent the update with the filter acf/pre_update_value, as I tried to do in my question
  2. disable them in html

prevent update with acf/pre_update_value filter :

to prevent update from this filter, we need a reliable way to detect whether the field update action was initiated in admin or in server-side. I can think of 3 ways to do that :

  1. by checking if the $_POST['acf'] is set, as I proposed in my answer

  2. by detecting if the action is in admin, it could be simply done with is_admin() but the link posted by CBroe shows a better and more complete way : https://florianbrinkmann.com/en/wordpress-backend-request-3815/

    • this should work, but it needs to check a lot of things to work well in all situation, as explained in the article, so it feels a little bit hacky
  3. by wrapping the server side calls in a function, e.g. my_plugin_acf_field_update(), and then use the function debug_backtrace() to check if the call comes from this function

    • it should be robust, but it would require to always wrap server side calls to update_field() in the wrapper function, which can be a painful depending of the state and structure of the project (in my case this is just fine, and I almost used that)
    • and also it will prevent any other plugin from updating those acf field, which can not always be suitable, or in the contrary a good point, depending of the situation

disable in html :

Another way to prevent the update, is to prevent the fields from being submited in the first place, in the html.

To do that, html have the disabled attribute for forms elements, and acf propose a way to add them to some of the fields, but it does not work well :

  • for some fields, it seems to work : ['text', 'textarea', 'number', 'email', 'url', 'password', 'date_picker', 'time_picker', 'select']

  • for some other fields it does not work : ['checkbox', 'radio']. Why ? actually because acf adds a hidden field with the field ref, like that : <input type="hidden" name="acf[field_6617daff359b2]">, so if a checkboxe is not submited because it's disabled, then acf receives the hidden input anyway, and then checks for any of the checkbox values, find none and deduce they are all unchecked, so it updates the field accordingly

  • and finally, for all other fields, acf does not propose any way to disable them, even if they are made of input elements, like the range field.

But there is another way, and this is the one I will use in my project : you can use the action hook that acf uses internally to render each field, and hook it before and after acf does, to add a fieldset element with the disabled attribute -> it will disable any element inside of it, including the hidden field. This should work and be robust, I hope.

This is how i do it :

/*
* there is one action hook suitable to open and close the fieldset element :
* 'acf/render_field' -> line 807 in advanced-custom-fields/includes/acf-field-functions.php
*
* acf uses priority 9 for its 'acf/render_field' hook -> see line 64 in advanced-custom-fields/includes/fields/class-acf-field.php
* so we hook before and after
*
* side note : we use a class, 'read_only_acf', to detect if a field should be disabled
*
*/
function open_fieldset_acf_disabled($field) {
  if (str_contains($field['wrapper']['class'], 'read_only_acf')) {
    echo "<fieldset disabled class='acf_disabled_fieldset' style='border:none; margin:0px; padding:0px;'>\n";
  }
}
function close_fieldset_acf_disabled($field) {
  if (str_contains($field['wrapper']['class'], 'read_only_acf')) {
    echo "</fieldset>\n";
  }
}
add_action('acf/render_field', 'open_fieldset_acf_disabled',  8);
add_action('acf/render_field', 'close_fieldset_acf_disabled', 10);

Upvotes: 2

Related Questions