doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: class name */
__( 'None of the callbacks are required as of Charitable 1.5; use `Charitable_Public_Form_View` instead. Issue encountered in %s class.', 'charitable' ),
get_class( $this )
),
'1.5.0'
);
add_action( 'charitable_form_before_fields', array( $this, 'render_error_notices' ) );
add_action( 'charitable_form_before_fields', array( $this, 'add_hidden_fields' ) );
add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
add_filter( 'charitable_form_field_increment', array( $this, 'increment_index' ), 10, 2 );
}
/**
* Set the Form View object.
*
* @since 1.5.0
*
* @param Charitable_Form_View_Interface $form_view An object implementing the `Charitable_Form_View_Interface`.
* @return void
*/
public function set_form_view( Charitable_Form_View_Interface $form_view ) {
$this->view = $form_view;
}
/**
* Return the Form_View object for this form.
*
* @since 1.5.0
*
* @return Charitable_Form_View_Interface
*/
public function view() {
if ( ! isset( $this->view ) ) {
$this->view = new Charitable_Public_Form_View( $this );
}
return $this->view;
}
/**
* Compares the ID of the form passed by the action and the current form object to ensure they're the same.
*
* @since 1.0.0
*
* @param string $id Current form ID.
* @return boolean
*/
public function is_current_form( $id ) {
return $id === $this->id;
}
/**
* Return the form fields.
*
* @since 1.5.0
*
* @return array
*/
public function get_fields() {
return array();
}
/**
* Return the form action.
*
* @since 1.3.1
*
* @return string
*/
public function get_form_action() {
return $this->form_action;
}
/**
* Return the form ID.
*
* @since 1.0.0
*
* @return string
*/
public function get_form_identifier() {
return $this->id;
}
/**
* Retrieve hidden fields.
*
* @since 1.5.0
*
* @return array
*/
public function get_hidden_fields() {
/**
* Filter hidden fields.
*
* @since 1.5.0
*
* @param array $fields The hidden fields as a key=>value array.
* @param Charitable_Form $form This instance of `Charitable_Form`.
*/
return apply_filters(
'charitable_form_hidden_fields',
array(
$this->nonce_name => wp_create_nonce( $this->nonce_action ),
'_wp_http_referer' => wp_unslash( $_SERVER['REQUEST_URI'] ), // phpcs:ignore
'charitable_action' => $this->form_action,
),
$this
);
}
/**
* Output the nonce.
*
* @since 1.0.0
*
* @return void
*/
public function nonce_field() {
wp_nonce_field( $this->nonce_action, $this->nonce_name );
}
/**
* Validate nonce data passed by the submitted form.
*
* @since 1.0.0
*
* @return boolean
*/
public function validate_nonce() {
$submitted = $this->get_submitted_values();
$validated = isset( $submitted[ $this->nonce_name ] ) && wp_verify_nonce( $submitted[ $this->nonce_name ], $this->nonce_action );
if ( ! $validated ) {
charitable_get_notices()->add_error( __( 'Unable to submit form. Please try again.', 'charitable' ) );
if ( charitable_is_debug() ) {
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
sprintf(
/* translators: %1$s: nonce name; %2$s: nonce action; %3$s: submitted nonce */
__( 'Nonce verification failed for nonce with name "%1$s" and action "%2$s". Submitted nonce is "%3$s".', 'charitable' ),
$this->nonce_name,
$this->nonce_action,
array_key_exists( $this->nonce_name, $submitted ) ? $submitted[ $this->nonce_name ] : ''
)
);
}
}
return $validated;
}
/**
* Make sure that the honeypot field is empty.
*
* @since 1.4.3
*
* @return boolean
*/
public function validate_honeypot() {
$submitted = $this->get_submitted_values();
if ( ! isset( $submitted['charitable_form_id'] ) ) {
return true;
}
$form_id = $submitted['charitable_form_id'];
return array_key_exists( $form_id, $submitted ) && 0 === strlen( $submitted[ $form_id ] );
}
/**
* Callback method used to filter out non-required fields.
*
* @since 1.0.0
*
* @param array $field Field definition.
* @return array
*/
public function filter_required_fields( $field ) {
return isset( $field['required'] ) && true == $field['required'];
}
/**
* Filters array returning just the required fields.
*
* @since 1.0.0
*
* @param array[] $fields Array of form fields.
* @return array[]
*/
public function get_required_fields( $fields ) {
$required_fields = array_filter( $fields, array( $this, 'filter_required_fields' ) );
return $required_fields;
}
/**
* Check the passed fields to ensure that all required fields have been submitted.
*
* @since 1.0.0
* @version 1.8.4.5 Added check for minimum password length.
*
* @param array $fields Array of form fields.
* @param array $submitted Submitted values.
* @return boolean
*/
public function check_required_fields( $fields, $submitted = array() ) {
if ( empty( $submitted ) ) {
$submitted = $this->get_submitted_values();
}
$ret = true;
$missing = array();
foreach ( $this->get_required_fields( $fields ) as $key => $field ) {
/* We already have a value for this field. */
if ( ! empty( $field['value'] ) && 'checkbox' !== $field['type'] ) {
continue;
}
$exists = isset( $submitted[ $key ] );
/* Verify that a value was provided. */
if ( $exists ) {
$value = $submitted[ $key ];
$exists = ! empty( $value ) || ( is_string( $value ) && strlen( $value ) );
}
/* If a value was not provided, check if it's in the $_FILES array. */
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( ! $exists ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_FILES array, validated by isset and empty checks
$exists = ( 'picture' == $field['type'] && isset( $_FILES[ $key ] ) && ! empty( $_FILES[ $key ]['name'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
$exists = apply_filters( 'charitable_required_field_exists', $exists, $key, $field, $submitted, $this );
if ( ! $exists ) {
$label = isset( $field['label'] ) ? $field['label'] : $key;
$missing[] = $label;
}
} // end foreach
// If the user_pass field is required we need to confirm a minimum number of characters for security reasons.
if ( isset( $fields['user_pass'] ) && ! empty( $fields['user_pass']['required'] ) ) {
$min_length = apply_filters( 'charitable_minimum_password_length', 8 );
if ( isset( $submitted['user_pass'] ) && strlen( $submitted['user_pass'] ) < $min_length ) {
charitable_get_notices()->add_error(
sprintf(
'%s',
sprintf(
/* translators: %d is the minimum password length required. */
__( 'Your password must be at least %d characters long.', 'charitable' ),
$min_length
)
)
);
}
}
$missing = apply_filters( 'charitable_form_missing_fields', $missing, $this, $fields, $submitted );
if ( count( $missing ) ) {
$missing_fields = implode( '
', $missing );
charitable_get_notices()->add_error(
sprintf(
'%s
',
__( 'There were problems with your form submission. The following required fields were not filled out:', 'charitable' ),
$missing_fields
)
);
$ret = false;
}
return $ret;
}
/**
* Organize fields by data type, also filtering out unused parameters (we just need the key and the type).
*
* @since 1.0.0
*
* @param string $key Key of the field to sort.
* @param array $field Field definition.
* @param array $ret Return value that we're carrying.
* @return array[]
*/
public function sort_field_by_data_type( $key, $field, $ret ) {
/* Filter out paragraphs and fields without a type. */
if ( ! isset( $field['type'] ) || 'paragraph' == $field['type'] ) {
return $ret;
}
/**
* Group the fields by data type.
*
* Where fields don't have a data_type specified, we group
* them as 'internal', since we need them to be grouped into a
* data type for the data processor to work as expected.
*
* @since 1.6.51 Added 'internal' data type as fallback.
*/
$data_type = isset( $field['data_type'] ) ? $field['data_type'] : 'internal';
$ret[ $data_type ][ $key ] = $field['type'];
return $ret;
}
/**
* Returns the submitted values.
*
* Use this method instead of accessing the raw $_POST array to take
* advantage of the filter on the values.
*
* @since 1.0.0
*
* @param array $fields Merged array of all the fields in the form.
* @return array
*/
public function get_submitted_values() {
if ( ! isset( $this->submitted ) ) {
$submitted = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$fields = $this->get_fields_by_data_type();
$options = $this->get_valid_field_options();
/* Process the submitted data. This handles field sanitization. */
$data_processor = new Charitable_Data_Processor( $submitted, $fields, $options );
foreach ( $data_processor->output() as $data_type_data ) {
$submitted = array_merge( $submitted, $data_type_data );
}
/**
* Filter the submitted values.
*
* @since 1.0.0
*
* @param array $data The submitted data.
* @param Charitable_Form $form The form class.
*/
$this->submitted = apply_filters( 'charitable_form_submitted_values', $submitted, $this );
}
return $this->submitted;
}
/**
* Returns the submitted value for a particular field.
*
* @since 1.0.0
*
* @param string $key The key to search for.
* @return mixed Submitted value if set. NULL if value was not set.
*/
public function get_submitted_value( $key ) {
$submitted = $this->get_submitted_values();
return isset( $submitted[ $key ] ) ? $submitted[ $key ] : null;
}
/**
* Uploads a file and attaches it to the given post.
*
* @since 1.0.0
*
* @param string $file_key Key of the file input.
* @param int $post_id Post ID.
* @param array $post_data Overwrite some of the attachment. Optional.
* @param array $overrides Override the wp_handle_upload() behavior. Optional.
* @return int|WP_Error ID of the attachment or a WP_Error object on failure.
*/
public function upload_post_attachment( $file_key, $post_id, $post_data = array(), $overrides = array() ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
$overrides = $this->get_file_overrides( $file_key, $overrides );
return media_handle_upload( $file_key, $post_id, $post_data, $overrides );
}
/**
* Upload a file.
*
* @since 1.0.0
* @version 1.8.9.1
*
* @param string $file_key Reference to a single element of `$_FILES`. Call the
* function once for each uploaded file.
* @param array $overrides Optional. An associative array of names=>values to
* override default variables. Default false.
* @return array|WP_Error On success, returns an associative array of file attributes.
* On failure, returns $overrides['upload_error_handler'](&$file, $message )
* or array( 'error'=>$message ).
*/
public function upload_file( $file_key, $overrides = array() ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( ! isset( $_FILES[ $file_key ] ) ) {
return new WP_Error( 'upload_error', __( 'No file was uploaded.', 'charitable' ) );
}
$overrides = $this->get_file_overrides( $file_key, $overrides );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_FILES array, sanitized by wp_handle_upload
$file = wp_handle_upload( $_FILES[ $file_key ], $overrides );
// phpcs:enable WordPress.Security.NonceVerification.Missing
if ( isset( $file['error'] ) ) {
return new WP_Error( 'upload_error', $file['error'] );
}
return $file;
}
/**
* Return overrides array for use with upload_file() and upload_post_attachment() methods.
*
* @since 1.0.0
*
* @param string $file_key Reference to a single element of `$_FILES`. Call the
* function once for each uploaded file.
* @param array $overrides Optional. An associative array of names=>values to
* override default variables. Default false.
* @return array
*/
protected function get_file_overrides( $file_key, $overrides = array() ) {
$allowed_mimes = array(
'jpg|jpeg|jpe' => 'image/jpeg',
'gif' => 'image/gif',
'png' => 'image/png',
'bmp' => 'image/bmp',
'tif|tiff' => 'image/tiff',
'ico' => 'image/x-icon',
'webp' => 'image/webp',
);
$defaults = array(
'test_form' => false,
'mimes' => apply_filters( 'charitable_file_' . $file_key . '_allowed_mimes', $allowed_mimes ),
);
$overrides = wp_parse_args( $overrides, $defaults );
return $overrides;
}
/**
* Returns all fields as a merged array.
*
* @since 1.6.51
*
* @return array[]
*/
public function get_merged_fields() {
$fields = array();
foreach ( $this->get_fields() as $key => $section ) {
if ( isset( $section['fields'] ) ) {
$fields = array_merge( $fields, $section['fields'] );
} else {
$fields[ $key ] = $section;
}
}
return $fields;
}
/**
* Return the form fields, organized by data type.
*
* @since 1.6.51
*
* @return array
*/
protected function get_fields_by_data_type() {
$fields = array();
foreach ( $this->get_merged_fields() as $key => $field ) {
$fields = $this->sort_field_by_data_type( $key, $field, $fields );
}
return $fields;
}
/**
* Get an array of valid field options for fields in the form.
*
* @since 1.6.51
*
* @return array
*/
protected function get_valid_field_options() {
$options = array();
foreach ( $this->get_merged_fields() as $key => $field ) {
/* The field has a specific set of options. */
if ( isset( $field['options'] ) ) {
$options[ $key ] = array_keys( $field['options'] );
/* Checkbox fields without a value set should be either 1 or nothing. */
} elseif ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) {
$checked_value = isset( $field['value'] ) ? $field['value'] : 1;
$options[ $key ] = array( $checked_value );
}
}
return $options;
}
/**
* Return the template name used for this field.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated.
*
* @param array $field Field definition.
* @return string
*/
public function get_template_name( $field ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::get_template_name()'
);
return $this->view()->get_template_name( $field );
}
/**
* Checks whether a template is valid.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Implemented by Form View.
*
* @param mixed $template Template we're checking.
* @return boolean
*/
protected function is_valid_template( $template ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::is_valid_template()'
);
return $this->view()->is_valid_template( $template );
}
/**
* Whether the given field type can use the default field template.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Implemented by Form View.
*
* @param string $field_type Type of field.
* @return boolean
*/
protected function use_default_field_template( $field_type ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::use_default_field_template()'
);
return $this->view()->use_default_field_template( $field_type );
}
/**
* Set how much the index should be incremented by.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Implemented by Form View.
*
* @param int $increment The number the index should be incremented by.
* @param array $field The field definition.
* @return int
*/
public function increment_index( $increment, $field ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::increment_index()'
);
/**
* Remove form's hooked filter.
*
* Before 1.5, forms used the filter to set the increment level. For
* backwards-compatibility purposes, we still provide this method in the
* form class, but it calls the Form View. This method shoud
* default in the form abstract, but remove it when this function
* is called directly.
*/
remove_filter( 'charitable_form_field_increment', array( $this, 'increment_index' ), 10, 2 );
return $this->view()->increment_index( $field );
}
/**
* Display error notices at the start of the form, if there are any.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Notices are rendered by the Form View now.
*
* @param Charitable_Form $form Form object.
* @return boolean Whether the notices were rendered.
*/
public function render_error_notices( $form ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::render_notices()'
);
if ( ! $form->is_current_form( $this->id ) ) {
return false;
}
return $this->view()->render_notices();
}
/**
* Adds hidden fields to the start of the donation form.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Hidden fields are rendered by the Form View now.
*
* @param Charitable_Form $form The form object.
* @return boolean Whether the output is added.
*/
public function add_hidden_fields( $form ) {
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::render_hidden_fields()'
);
if ( ! $form->is_current_form( $this->id ) ) {
return false;
}
return $this->view()->render_hidden_fields();
}
/**
* Render a form field.
*
* @deprecated 1.8.0
*
* @since 1.0.0
* @since 1.5.0 Deprecated. Use `Charitable_Public_Form_View` instead.
* @since 1.5.4 Added $from_template parameter for backwards compatibility purposes.
* Anytime this hook is called from a template using the pre-Charitable 1.5
* approach of rendering fields (i.e. do_action( 'charitable_form_field' )),
* there will not be a sixth parameter, so this will default to true.
*
* @param array $field Field definition.
* @param string $key Field key.
* @param Charitable_Form $form The form object.
* @param int $index The current index.
* @param string $namespace Namespace for the form field's name attribute.
* @param boolean $from_template Whether the method was called from a template file.
* @return boolean False if the field was not rendered. True otherwise.
*/
public function render_field( $field, $key, $form, $index = 0, $namespace = null, $from_template = true ) {
if ( ! $from_template ) {
return false;
}
charitable_get_deprecated()->deprecated_function(
__METHOD__,
'1.5.0',
'Charitable_Public_Form_View::render_field()'
);
if ( ! $form->is_current_form( $this->id ) ) {
return false;
}
/**
* This was not evoked by the form view so it's likely that notices,
* hidden fields and the honeypot also haven't been rendered.
*/
if ( ! $this->view()->rendered_notices ) {
$this->view()->render_notices();
}
if ( ! $this->view()->rendered_honeypot ) {
$this->view()->render_honeypot();
}
if ( ! $this->view()->rendered_hidden_fields ) {
$this->view()->render_hidden_fields();
}
return $form->view()->render_field(
$field,
$key,
array(
'index' => $index,
'namespace' => $namespace,
)
);
}
}
endif;