<?php /** * The base class for all SiteOrigin_Widget form fields. * * Class SiteOrigin_Widget_Field */ abstract class SiteOrigin_Widget_Field_Base { /* ============================================================================================================== */ /* CORE FIELD PROPERTIES */ /* Properties which are essential to successful rendering of fields and saving of data input into fields. */ /* ============================================================================================================== */ /** * The base name for this field. It is used in the generation of HTML element id and name attributes. * * @var string */ protected $base_name; /** * The rendered HTML element id attribute. * * @var string */ protected $element_id; /** * The rendered HTML element name attribute * * @var string */ protected $element_name; /** * The field configuration options. * * @var array */ protected $field_options; /** * Variables may be added to this array which will be propagated to the front end for use in dynamic rendering. * * @var array */ protected $javascript_variables; /* ============================================================================================================== */ /* BASE FIELD CONFIGURATION PROPERTIES */ /* Common configuration properties used by all fields. */ /* ============================================================================================================== */ /** * The type of the field. * * @var string */ protected $type; /** * Render a label for the field with the given value. * * @var string */ protected $label; /** * The field will be prepopulated with this default value. * * @var mixed */ protected $default; /** * Render small italic text below the field to describe the field's purpose. * * @var string */ protected $description; /** * Append '(Optional)' to this field's label as a small green superscript. * * @var bool */ protected $optional; /** * @var bool|string Is this field required. */ protected $required; /** * Specifies an additional sanitization to be performed. Available sanitizations are `text`, `email` and `url`. If the * specified sanitization isn't recognized it is assumed to be a custom sanitization and a filter is applied using * the pattern `'siteorigin_widgets_sanitize_field_' . $sanitize`, in case the sanitization is defined elsewhere. * * @var string */ protected $sanitize; /** * Reference to the parent widget required for creating child fields. * * @var SiteOrigin_Widget */ protected $for_widget; /** * An array of field names of parent containers. * * @var array */ protected $parent_container; /** * Whether or not this field contains other fields. * * @var bool */ protected $is_container; /** * Additional CSS classes to output in this field's HTML class attribute. It is left up to the field's render_field * function to output these classes. * * @var array */ protected $input_css_classes; /* ============================================================================================================== */ /* FIELD STATES PROPERTIES */ /* Configuration of field state emitters and handlers. */ /* See https://siteorigin.com/docs/widgets-bundle/form-building/state-emitters/ for more detail on the topic of */ /* state emitters and handlers. */ /* ============================================================================================================== */ /** * Specifies the callback type and arguments to use when deciding on the state to be emitted. * * @var array */ protected $state_emitter; /** * Specifies the different possible states to be handled by this field and the resulting effect of the each state. * * @var array */ protected $state_handler; protected $state_handler_initial; /** * @param $base_name string The name of the field. * @param $element_id string The id to be used as the id attribute of the wrapping HTML element. * @param $element_name string The name to be used as the name attribute of the wrapping HTML element. * @param $field_options array Configuration for the field. * @param SiteOrigin_Widget $for_widget * @param array $parent_container * * @throws InvalidArgumentException */ public function __construct( $base_name, $element_id, $element_name, $field_options, $for_widget = null, $parent_container = array() ) { if ( isset( $field_options['type'] ) ) { $this->type = $field_options['type']; } else { throw new InvalidArgumentException( 'SiteOrigin_Widget_Field_Base::__construct: $field_options must contain a \'type\' field.' ); } $this->base_name = $base_name; $this->element_id = $element_id; $this->element_name = $element_name; $this->field_options = $field_options; $this->javascript_variables = array(); $this->for_widget = $for_widget; $this->parent_container = $parent_container; $this->init(); } private function init() { $this->init_options(); $this->initialize(); } /** * Initialization function which may be overridden if required. */ protected function initialize() { } /** * This method ensures that configuration options are set on the corresponding field class instance properties. If * a field has defined default options, those are set first and then can be overwritten by options which were * passed in. */ private function init_options() { // First set properties from default options if any have been set. $default_field_options = $this->get_default_options(); if ( ! empty( $default_field_options ) ) { foreach ( $default_field_options as $key => $value ) { if ( property_exists( $this, $key ) ) { if ( isset( $default_field_options[$key] ) ) { $this->$key = $value; } } } } $field_options = $this->field_options; foreach ( $field_options as $key => $value ) { if ( property_exists( $this, $key ) ) { if ( isset( $field_options[$key] ) ) { $this->$key = $value; } } } } protected function get_default_options() { //Stub: This function may be overridden by subclasses to have default field options. return null; } /** * The CSS classes to be applied to the default label. * This function should be overridden by subclasses when they want to add custom CSS classes to the HTML input label. * * @return array The array of label CSS classes. */ protected function get_label_classes( $value, $instance ) { return array( 'siteorigin-widget-field-label' ); } /** * The CSS classes to be applied to the default description. * This function should be overridden by subclasses when they want to add custom CSS classes to the description text. * * @return array The modified array of description text CSS classes. */ protected function get_description_classes() { return array( 'siteorigin-widget-description' ); } /** * This function is called by the containing SiteOrigin_Widget when rendering it's form. * * @param $value mixed The current instance value of the field. * @param $instance array Optionally pass in the widget instance, if rendering of additional values is required. */ public function render( $value, $instance = array() ) { if ( is_null( $value ) && isset( $this->default ) ) { $value = $this->default; } $wrapper_attributes = array( 'class' => array( 'siteorigin-widget-field', 'siteorigin-widget-field-type-' . $this->type, 'siteorigin-widget-field-' . $this->base_name, ), ); if ( ! empty( $this->optional ) ) { $wrapper_attributes['class'][] = 'siteorigin-widget-field-is-optional'; } if ( ! empty( $this->required ) ) { $wrapper_attributes['class'][] = 'siteorigin-widget-field-is-required'; } $wrapper_attributes['class'] = implode( ' ', array_map( 'sanitize_html_class', $wrapper_attributes['class'] ) ); if ( ! empty( $this->state_emitter ) ) { // State emitters create new states for the form $wrapper_attributes['data-state-emitter'] = json_encode( $this->state_emitter ); } if ( ! empty( $this->state_handler ) ) { // State handlers decide what to do with form states $wrapper_attributes['data-state-handler'] = json_encode( $this->state_handler ); } if ( ! empty( $this->state_handler_initial ) ) { // Initial state handlers are only run when the form is first loaded $wrapper_attributes['data-state-handler-initial'] = json_encode( $this->state_handler_initial ); } ?><div <?php foreach ( $wrapper_attributes as $attr => $attr_val ) { echo siteorigin_sanitize_attribute_key( $attr ) . '="' . esc_attr( $attr_val ) . '" '; } ?>><?php // Allow subclasses and to render something before and after the render_field() function is called. $this->render_before_field( $value, $instance ); $this->render_field( $value, array() ); $this->render_after_field( $value, $instance ); ?></div><?php } /** * This function is called before the main render function. * * @param $value mixed The current value of this field. * @param $instance array The current widget instance. */ protected function render_before_field( $value, $instance ) { $this->render_field_label( $value, $instance ); } /** * Default label rendering implementation. Subclasses should override if necessary to render labels differently. */ protected function render_field_label( $value, $instance ) { ?> <label for="<?php echo esc_attr( $this->element_id ); ?>" <?php $this->render_CSS_classes( $this->get_label_classes( $value, $instance ) ); ?>> <?php echo esc_html( $this->label ); if ( ! empty( $this->optional ) ) { echo '<span class="field-optional">(' . esc_html__( 'Optional', 'so-widgets-bundle' ) . ')</span>'; } if ( ! empty( $this->required ) ) { /* translators: Used to indicate field as required. */ echo '<span class="field-required">' . esc_html__( '*', 'so-widgets-bundle' ) . '</span>'; } ?> </label> <?php } /** * Helper function to render the HTML class attribute with the array of classes. */ protected function render_CSS_classes( $CSS_classes ) { if ( ! empty( $CSS_classes ) ) { ?>class="<?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $CSS_classes ) ) ); ?>"<?php } } /** * The main field rendering function. This function should be overridden by all subclasses and used to render their * specific form field HTML for display. * * @param $value mixed The current value of this field. * @param $instance array The current widget instance. * * @return mixed Should output the desired HTML. */ abstract protected function render_field( $value, $instance ); /** * The default sanitization function. * * @param $value mixed The value to be sanitized. * @param $instance array The widget instance. * @param $old_value mixed The old value of this field. * * @return mixed|string */ public function sanitize( $value, $instance = array(), $old_value = null ) { if ( $value === '' || is_null( $value ) ) { return ''; } $value = $this->sanitize_field_input( $value, $instance ); if ( isset( $this->sanitize ) && ! empty( $value ) ) { // This field also needs some custom sanitization switch( $this->sanitize ) { case 'text': if ( is_user_logged_in() && // Fields can be sanitized for setup purposes during display. // As the data is sanitized during the saving purpose, we can // safely skip this. Not doing so could result in an error. ! current_user_can( 'unfiltered_html' ) && ! apply_filters( 'siteorigin_widgets_field_allow_unfiltered_html', false ) ) { $value = $this->recursive_sanitize( $value ); } break; case 'url': $value = sow_esc_url_raw( $value ); break; case 'email': $value = sanitize_email( $value ); break; case 'number': $value = (int) $value; break; default: // This isn't a built in sanitization. Maybe it's handled elsewhere. if ( is_callable( $this->sanitize ) ) { $value = call_user_func( $this->sanitize, $value, $old_value ); } elseif ( is_string( $this->sanitize ) ) { $value = apply_filters( 'siteorigin_widgets_sanitize_field_' . $this->sanitize, $value ); } break; } } return $value; } /** * Recursively sanitizes and filters the given value using sanitize_text_field(). * * If the value is an array, it recursively applies the sanitization to each element. * * @param mixed $value The value to be sanitized. * @return mixed The sanitized value. */ public function recursive_sanitize( $value ) { if ( is_array( $value ) ) { return array_map( array( $this, 'recursive_sanitize' ), $value ); } return sanitize_text_field( $value ); } /** * This function is called after the main render function. * * @param $value mixed The current value of this field. * @param $instance array The current widget instance. */ protected function render_after_field( $value, $instance ) { $this->render_field_description(); if ( ! empty( $this->required ) && is_string( $this->required ) ) { /* translators: Used to indicate field as required. */ echo '<span class="field-required-message">' . esc_html( $this->required ) . '</span>'; } } /** * Default description rendering implementation. Subclasses should override if necessary to render descriptions * differently. */ protected function render_field_description() { if ( ! empty( $this->description ) ) { ?><div <?php $this->render_CSS_classes( $this->get_description_classes() ); ?>><?php echo wp_kses_post( $this->description ); ?></div><?php } } /** * The main sanitization function. This function should be overridden by all subclasses and used to sanitize the * input received from their HTML form field. * * @param $value mixed The current value of this field. * @param $instance array The widget instance. * * @return mixed The sanitized value. */ abstract protected function sanitize_field_input( $value, $instance ); /** * There are cases where a field may affect values on the widget instance, other than it's own input. It then becomes * necessary to perform additional sanitization on the widget instance, which should be done here. * * @return mixed */ public function sanitize_instance( $instance ) { //Stub: This function may be overridden by subclasses wishing to sanitize additional instance fields. return $instance; } /** * Occasionally it is necessary for a field to set a variable to be used in the front end. Override this function * and set any necessary values on the `javascript_variables` instance property. * * @return array */ public function get_javascript_variables() { return $this->javascript_variables; } /** * Some more complex fields may require some JavaScript in the front end. Enqueue them here. */ public function enqueue_scripts() { } public function __get( $name ) { if ( isset( $this->$name ) ) { return $this->$name; } } }