Skip to content

Instantly share code, notes, and snippets.

@phillipwilhelm
Forked from RadGH/edit-gf-entry.php
Created August 17, 2023 01:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phillipwilhelm/d30bf4143b473ebcd32a7bf6e9f3e014 to your computer and use it in GitHub Desktop.
Save phillipwilhelm/d30bf4143b473ebcd32a7bf6e9f3e014 to your computer and use it in GitHub Desktop.
Edit an existing gravityforms entry on the frontend
<?php
/*
Plugin Name: GF Editable by Radley
Description: Example classes to make a particular gravity form editable on the front-end.
Author: Radley Sustaire
Author URI: https://radleysustaire.com/
Version: 1.0.0
*/
// QUICK TEST INSTRUCTIONS:
// 1. Import this example form in Gravity Forms (download as json):
// https://gist.github.com/RadGH/afb032d642515aedbb068f6b5990b668
//
// 2. Modify "GF_Form_12" below to be your own form ID after import.
// * If you change the class, make sure to replace "new GF_Form_12()" below too.
//
// 3. Submit an entry on the form.
// 4. Go back to the form and add ?edited_entry_id=100 (where 100 is your entry id)
// 5. Field values should populate, allowing to edit the same entry.
// RECOMMENDED AFTER TESTING:
//
// 1. Put the "abstract" class Editable_GF_Form into a separate file.
// 2. Create a copy of GF_Form_12 for any of your forms.
// 3. Include the abstract class, and your custom classes, then instantiate your custom classes like: new GF_Form_12()
// Extending "Editable_GF_Form" gives the ability to edit entries. You only need to supply $form_id and $query_arg
/**
* @class GF_Form_12
* @version 1.0
*/
class GF_Form_12 extends Editable_GF_Form {
// Each form should have its own class.
public $form_id = 12;
public $query_arg = 'edited_entry_id';
public function __construct() {
// Add essential hooks from the parent Editable_GF_Form class
parent::__construct();
// Register actions or filters for the form here.
// EXAMPLE: Add a key when the form gets saved
// add_filter( 'gform_entry_post_save', array($this, 'fill_random_key'), 30, 2 );
}
// EXAMPLE: The function to add a key when the form gets saved
/*
public function fill_random_key( $entry, $form ) {
if ( $this->form_id != $form['id'] ) return $entry;
// Some form of key
$key = uniqid();
// Save to the database
gform_update_meta( $entry['id'], 'secret_key', $key );
// Add to current entry too
$entry['secret_key'] = $key;
return $entry;
}
*/
}
// Remember to instantiate your class, or else it won't do anything.
new GF_Form_12();
/**
* This class is used to make a gravity form entry editable.
* Do not edit below, create your own object that extends this class.
* An example is above "GF_Form_12"
*/
abstract class Editable_GF_Form {
/**
* Form ID that will allow editing entries.
*
* @var int
*/
public $form_id = null;
// Internal variables
protected $uploads = array();
protected $sub_inputs = array();
public function __construct() {
if ( $this->form_id === null ) {
_doing_it_wrong(__FUNCTION__, '$this->form_id must be an integer', '1.0');
exit;
}
// When editing an entry, change the entry ID to the edited entry instead of creating a new entry
add_filter( "gform_entry_id_pre_save_lead_{$this->form_id}", array($this, 'change_saved_entry_id'), 50, 2 );
// Prepare the form to be editable on a very early hook
add_filter( 'gform_form_args', array( $this, 'prepare_editable_form' ), 20 );
// Insert field values for most fields
add_filter( "gform_field_value", array($this, 'load_regular_field_value'), 20, 3 );
// Keeps file uploads unless the user actually deletes or replaces them
add_filter( "gform_pre_process", array( $this, 'pre_restore_existing_uploads' ), 20, 1 );
}
/**
* Get the entry id that is being edited from the url ?entry_id=100
*
* @return int|false
*/
public function get_edited_entry_id() {
$entry_id = (int) rgar( $_GET, $this->query_arg );
if ( !$entry_id ) $entry_id = false;
if ( $this->can_user_edit_entry( get_current_user_id(), $entry_id ) ) {
return $entry_id;
}else{
return false;
}
}
/**
* Keeps file uploads unless the user actually deletes or replaces them.
* Gravity forms seems to do this, but fails at it.
* To make this work we capture values before (here) and restore them after the entry is saved.
*
* @param $form
*
* @return mixed
*/
public function pre_restore_existing_uploads( $form ) {
if ( $form['id'] != $this->form_id ) return $form;
$entry_id = $this->get_edited_entry_id();
// Get uploads from $_POST, served as JSON string, which has file upload info
$uploads = rgpost( 'gform_uploaded_files' );
if ( !$uploads ) return $form;
// Decode the json
$uploads = json_decode( $uploads );
// Each file upload field will have the previous filename, or NULL if removing that file.
if ( $uploads ) foreach( $uploads as $input_name => $file ) {
// If file was removed by user, or replaced with new file
if ( ! $file ) continue;
// File should be kept.
$field_id = (int) str_replace('input_', '', $input_name );
$url = gform_get_meta( $entry_id, $field_id );
$this->uploads[ $input_name ] = array(
'entry_id' => $entry_id,
'input_name' => $input_name,
'field_id' => $field_id,
'url' => $url
);
}
add_filter( "gform_after_submission_{$form['id']}", array( $this, 'restore_existing_uploads' ), 10, 2 );
return $form;
}
/**
* Restore files preserved by pre_restore_existing_uploads() after the entry has been saved
*
* @param $entry
* @param $form
*
* @return mixed
*/
public function restore_existing_uploads( $entry, $form ) {
if ( $form['id'] != $this->form_id ) return $entry;
if ( $this->uploads ) foreach( $this->uploads as $u ) {
if ( $entry['id'] != $u['entry_id'] ) continue;
// Get the field ID and URL of the file that should be preserved
$field_id = $u['field_id']; // 13
$url = $u['url']; // https://example.com/.../icon-zm3.png
// Restore the URL to the entry
$entry[ $field_id ] = $url;
// Update the entry
GFAPI::update_entry( $entry );
}
return $entry;
}
/**
* Make all fields on the form editable (allowPrepopulate) and add names if not already given.
*
* @param $form
*
* @return mixed
*/
public function add_form_prepopulate_names( $form ) {
// Modify every field and add a name if needed.
// Names added manually work too, by enabling "Allow field to be populated dynamically" on the field.
if ( $form['fields'] ) foreach( $form['fields'] as &$field ) {
if ( !($field instanceof GF_Field) ) continue;
// Don't affect display fields (like html)
if ( $field->displayOnly ) continue;
// Enable pre-populate
$field->allowsPrepopulate = true;
// Get regular field name
$inputName = $field->inputName;
if ( $inputName ) continue;
// Check if we need a regular field name, or if we need sub fields with names.
if ( empty( $field->inputs ) ) {
// Regular field
// 1st and 2nd are form ID ($form['id']), and field ID ($form->fields[0]->id)
// field_12_4_value
$field->inputName = "field_{$field['formId']}_{$field['id']}_value";
}else{
// Sub fields
// Loop through sub fields and add names to any that are missing
foreach( $field->inputs as $key => &$input ) {
if ( empty($input['name']) ) {
// 3rd number is the sub field ID: ($form->fields[0]->inputs[0]->id)
// input name="field_12_4_1_value"
$input['name'] = "sub_field_" . $field['formId'] . '_' . $field['id'] . '_'. $key .'_value';
}
}
}
}
return $form;
}
/**
* Fill the value on our form using the existing entry's data
*
* @param $value
* @param GF_Field|null $field
* @param $name
*
* @return array
*/
public function load_regular_field_value( $value = null, GF_Field $field = null, $name = null ) {
if ( ! ($field instanceof GF_Field) ) return $value;
if ( $field->formId != $this->form_id ) return $value;
$existing_entry_id = $this->get_edited_entry_id();
if ( !$existing_entry_id ) return $value;
$existing_entry = GFAPI::get_entry( $existing_entry_id );
if ( !$existing_entry ) return $value;
$value = GFFormsModel::get_lead_field_value( $existing_entry, $field );
// If no sub fields just return value
if ( empty($field->inputs) ) return $value;
// Checkboxes work with the given value
if ( $field->type == 'checkbox' ) return $value;
// Names do not seem to work.
return $value;
}
/**
* Add certain hooks only when the form is going to be displayed (based on shortcode usage)
*
* $args = array(7) {
* "form_id" => "12"
* "display_title" => true
* "display_description" => true
* "force_display" => false
* "field_values" => array() (empty)
* "ajax" => false
* "tabindex" => "0"
* }
*
* @param $args
* @return array
*/
public function prepare_editable_form( $args ) {
// Only for this form
if ( $this->form_id !== (int) $args['form_id'] ) return $args;
// Add saved files to edited entry when the form started to be rendered
add_filter( "gform_pre_render_{$this->form_id}", array($this, 'prepare_previously_uploaded_files'), 20, 3 );
// Fill in the value of all fields that have a name. The name must match the "autofill parameter name" in the field's settings.
$form = GFAPI::get_form( $this->form_id );
$form = $this->add_form_prepopulate_names( $form );
foreach( $form['fields'] as $field ) {
if ( !($field instanceof GF_Field) ) continue;
$sub_inputs = rgobj($field, 'inputs');
// Advanced inputs like Name and Address have multiple sub inputs, each with their own name and values
// They are tricky to fill because Gravity Forms doesn't give you the key with this filter.
if ( $sub_inputs ) {
foreach( $sub_inputs as $sub_input ) {
if ( !is_array($sub_input) ) continue;
// Get the name, if any
$name = rgar($sub_input, 'name' );
if ( !$name ) continue;
// Store the sub input field assigned to the name (first_name) so we can get field data later
$this->sub_inputs[ $name ] = $sub_input;
// Use a special filter for sub inputs
add_filter( "gform_field_value_{$name}", array( $this, 'fill_sub_input_value' ), 20, 3 );
}
continue;
}
}
return $args;
}
/**
* Return true if the given user is able to make edits to the entry.
*
* @param int $user_id
* @param array|int $entry
*
* @return bool
*/
public function can_user_edit_entry( $user_id, $entry ) {
if ( is_numeric($entry) ) $entry = GFAPI::get_entry( $entry );
if ( !$entry || is_wp_error( $entry ) ) return false;
// Must be an entry belonging to this form
if ( $entry['form_id'] != $this->form_id ) return false;
// The user ID must match the owner of the entry
if ( (int) $entry['created_by'] != (int) $user_id ) return false;
return true;
}
/**
* Set up "uploaded_files" using values from the previous entry. Allows you to keep your previous upload, or remove it and start over.
*
* If file is kept, form submits:
* gform_uploaded_files: {"input_13":"icon-aa3.png"}
*
* If file is removed, form submits:
* gform_uploaded_files: {"input_13":null}
*
*
*
* @param $form
* @param $ajax
* @param $field_values
*
* @return mixed
*/
public function prepare_previously_uploaded_files( $form, $ajax, $field_values ) {
$user_id = get_current_user_id();
// Get the edited entry
$entry_id = $this->get_edited_entry_id();
if ( !$entry_id ) return $form;
// Check permissions
if ( !$this->can_user_edit_entry( $user_id, $entry_id ) ) return $form;
// Check if any field is a file upload. If not, we can ignore this function
do {
foreach( $form['fields'] as $field ) {
if ( $field instanceof GF_Field_FileUpload ) {
// File upload found. Abort the do{} loop and proceed with the function
break 2;
}
}
// No file uploads found
return $form;
} while(false);
// Get the entry object
$entry = GFAPI::get_entry( $entry_id );
if ( !isset( GFFormsModel::$uploaded_files[ $form['id'] ] ) ) {
GFFormsModel::$uploaded_files[ $form['id'] ] = array();
}
// Loop through each file upload and put the basename as an uploaded file
foreach( $form['fields'] as $field ) {
if ( !$field instanceof GF_Field_FileUpload ) continue;
$value = rgar( $entry, $field->id );
GFFormsModel::$uploaded_files[ $form['id'] ]["input_{$field->id}"] = basename( $value );
}
return $form;
}
/**
* Make Gravity Forms edit an existing entry ($entry_id = int), instead of creating a new one ($entry_id = null).
*
* @param $entry_id null|int
* @param $form array
*
* @return null|int
*/
public function change_saved_entry_id( $entry_id, $form ) {
if ( $entry_id !== null ) return $entry_id;
$user_id = get_current_user_id();
// Get the entry being edited
$existing_entry_id = $this->get_edited_entry_id();
if ( !$existing_entry_id ) return $entry_id;
// Check access, if user can edit then return the previous entry ID, instead of creating a new one (null).
if ( $this->can_user_edit_entry( $user_id, $existing_entry_id ) ) {
// Edit previous entry
return $existing_entry_id;
}else{
// Create a new entry
return null;
}
}
/**
* Get text used for sub inputs
*
* @param null $value
* @param GF_Field|null $field
* @param null $name
*
* @return string
*/
public function fill_sub_input_value( $value = null, GF_Field $field = null, $name = null ) {
if ( $field->formId != $this->form_id ) return $value;
// Name must be defined in $this->sub_inputs so we know what sub field this hook relates to.
if ( !isset($this->sub_inputs[$name]) ) return $value;
$id = rgar( $this->sub_inputs[$name], 'id' );
if ( !$id ) return $value;
$existing_entry_id = $this->get_edited_entry_id();
if ( $existing_entry_id ) {
$value = gform_get_meta( $existing_entry_id, $id );
}
return $value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment