inc/WPDLib/FieldTypes/Manager.php
<?php
/**
* WPDLib\FieldTypes\Manager class
*
* @package WPDLib
* @subpackage FieldTypes
* @author Felix Arntz <felix-arntz@leaves-and-love.net>
* @since 0.5.0
*/
namespace WPDLib\FieldTypes;
use WPDLib\Components\Manager as ComponentManager;
use WPDLib\Util\Util;
if ( ! defined( 'ABSPATH' ) ) {
die();
}
if ( ! class_exists( 'WPDLib\FieldTypes\Manager' ) ) {
/**
* This class manages all field types and provides some utility functions for fields.
*
* @internal
* @since 0.5.0
*/
final class Manager {
/**
* Status whether the class has been initialized.
*
* @since 0.6.0
* @var boolean
*/
private static $initialized = false;
/**
* Initialization function.
*
* This has to be called by every plugin using WPDLib.
*
* @since 0.6.0
*/
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'wp_ajax_get-attachment-by-url', array( __CLASS__, 'ajax_get_attachment_by_url' ), 15 );
self::$initialized = true;
}
/**
* Returns a new field type instance.
*
* @since 0.5.0
* @param array $args the field arguments
* @param bool $repeatable whether the field will be part of a repeatable field (default is false)
* @return WPDLib\FieldTypes\Base|null the field type instance or null if the field type is invalid or missing
*/
public static function get_instance( $args, $repeatable = false ) {
if ( ! isset( $args['type'] ) ) {
return null;
}
$field_type = self::validate_field_type( $args['type'], $repeatable );
if ( null === $field_type ) {
return null;
}
$field_args = self::validate_field_args( $args );
if ( class_exists( 'WPDLib\FieldTypes\\' . ucfirst( $field_type ) ) ) {
$class_name = '\WPDLib\FieldTypes\\' . ucfirst( $field_type );
return new $class_name( $field_type, $field_args );
}
return new Base( $field_type, $field_args );
}
/**
* Returns a list of available field types.
*
* @since 0.5.0
* @return array an array of available field types
*/
public static function get_field_types() {
return array(
'checkbox',
'radio',
'multibox',
'select',
'multiselect',
'media',
'map',
'textarea',
'wysiwyg',
'datetime',
'date',
'time',
'color',
'range',
'number',
'url',
'email',
'tel',
'text',
'repeatable',
);
}
/**
* Enqueues assets for one or more fields.
*
* This function should be used from all places in the WordPress admin that contain field types from WPDLib.
* All fields active on the screen should be passed to the function.
*
* @since 0.5.0
* @param array $fields the field type objects to enqueue assets for
*/
public static function enqueue_assets( $fields = array() ) {
$assets_url = ComponentManager::get_base_url() . '/assets';
$version = ComponentManager::get_info( 'version' );
list( $dependencies, $script_vars ) = self::get_dependencies_and_script_vars( $fields );
if ( ! in_array( 'jquery', $dependencies ) ) {
$dependencies[] = 'jquery';
}
$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_style( 'wpdlib-fields', $assets_url . '/dist/css/fields' . $min . '.css', array(), $version );
wp_enqueue_script( 'wpdlib-fields', $assets_url . '/dist/js/fields' . $min . '.js', $dependencies, $version, true );
wp_localize_script( 'wpdlib-fields', '_wpdlib_data', $script_vars );
}
/**
* Returns an array of Javascript dependencies and script variables for several field types.
*
* All dependencies must be loaded, otherwise the main WPDLib script will not be loaded.
* The field type classes should handle that automatically though.
*
* The script vars are put into a JSON object '_wpdlib_data' by `wp_localize_script()`.
*
* @since 0.5.0
* @param array $fields the field type objects to get dependencies and script vars for
* @return array an array containing a dependencies array and a script vars array
*/
public static function get_dependencies_and_script_vars( $fields = array() ) {
$dependencies = array();
$script_vars = array();
foreach ( $fields as $field ) {
$asset_data = $field->enqueue_assets();
if ( isset( $asset_data['dependencies'] ) ) {
foreach ( $asset_data['dependencies'] as $dependency ) {
$dependencies[] = $dependency;
}
}
if ( isset( $asset_data['script_vars'] ) ) {
foreach ( $asset_data['script_vars'] as $key => $value ) {
if ( isset( $script_vars[ $key ] ) && is_array( $script_vars[ $key ] ) && is_array( $value ) ) {
$script_vars[ $key ] = array_merge( $script_vars[ $key ], $value );
} else {
$script_vars[ $key ] = $value;
}
}
}
}
$dependencies = array_unique( $dependencies );
return array( $dependencies, $script_vars );
}
/**
* Transforms an array of HTML attributes into an attributes string.
*
* The attributes are printed in a nicely-sorted format.
*
* @since 0.5.0
* @see WPDLib\FieldTypes\Manager::sort_html_attributes()
* @param array $atts array of arguments and their values
* @param bool $html5 whether to output arguments in html5 syntax (default is true)
* @param bool $echo whether to echo the output (default is true)
* @return string the HTML attributes string
*/
public static function make_html_attributes( $atts, $html5 = true, $echo = true ) {
$output = '';
$bool_atts = array_filter( $atts, 'is_bool' );
$atts = array_diff_key( $atts, $bool_atts );
uksort( $atts, array( __CLASS__, 'sort_html_attributes' ) );
foreach ( $atts as $key => $value ) {
if ( is_array( $value ) ) {
continue;
}
if ( is_string( $value ) && empty( $value ) ) {
continue;
}
$output .= ' ' . $key . '="' . esc_attr( $value ) . '"';
}
foreach ( $bool_atts as $key => $active ) {
if ( $active ) {
if ( $html5 ) {
$output .= ' ' . $key;
} else {
$output .= ' ' . $key . '="' . esc_attr( $key ) . '"';
}
}
}
if ( $echo ) {
echo $output;
}
return $output;
}
/**
* Formats a value depending on certain criteria.
*
* Valid types are:
* - string
* - html
* - url
* - boolean / bool
* - integer / int
* - float / double
* - datetime
* - date
* - time
* - byte
*
* @since 0.5.0
* @param mixed $value the value to format
* @param string $type the type of the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @param array $args additional formatting args (optional, they depend on the $type)
* @return mixed the formatted value
*/
public static function format( $value, $type, $mode = 'input', $args = array() ) {
$mode = 'output' === $mode ? 'output' : 'input';
$formatted = $value;
switch ( $type ) {
case 'string':
return self::format_string( $value, $mode );
case 'html':
return self::format_html( $value, $mode );
case 'url':
return self::format_url( $value, $mode );
case 'boolean':
case 'bool':
return self::format_bool( $value, $mode );
case 'integer':
case 'int':
return self::format_int( $value, $mode, $args );
case 'float':
case 'double':
return self::format_float( $value, $mode, $args );
case 'date':
case 'time':
case 'datetime':
return self::format_datetime( $value, $mode, $type, $args );
case 'byte':
return self::format_byte( $value, $mode, $args );
default:
}
return $formatted;
}
/**
* AJAX handler to retrieve an attachment by URL.
*
* WordPress does not have an AJAX function like this, so WPDLib adds it.
* This is required for the media field type to work properly.
*
* @since 0.6.0
*/
public static function ajax_get_attachment_by_url() {
if ( ! isset( $_REQUEST['url'] ) ) {
wp_send_json_error();
}
$id = attachment_url_to_postid( $_REQUEST['url'] );
if ( ! $id ) {
wp_send_json_error();
}
$_REQUEST['id'] = $id;
wp_ajax_get_attachment();
die();
}
/**
* Validates a field type.
*
* If the field type should be part of a repeatable, some additional checks must be made.
*
* @since 0.5.0
* @param string $field_type the field type to validate
* @param bool $repeatable whether the field will be part of a repeatable field (default is false)
* @return string|null the validated field type or null if the field type is invalid
*/
private static function validate_field_type( $field_type, $repeatable = false ) {
$field_types = self::get_field_types();
if ( ! in_array( $field_type, $field_types ) ) {
return null;
}
if ( $repeatable ) {
return self::map_repeatable_type( $field_type );
}
return $field_type;
}
/**
* Checks a field type for inclusion in a repeatable field.
*
* There are a few field types which are not allowed in a repeatable fields.
* Some others are automatically replaced by different field types with a similar data structure.
*
* @since 0.5.0
* @param string $field_type the field type to validate
* @return string|null the validated field type or null if the field type is invalid
*/
private static function map_repeatable_type( $field_type ) {
$non_repeatable_types = array(
'wysiwyg',
'repeatable',
);
if ( in_array( $field_type, $non_repeatable_types ) ) {
return null;
}
$replace_types = array(
'radio' => 'select',
'multibox' => 'multiselect',
'textarea' => 'text',
);
if ( isset( $replace_types[ $field_type ] ) ) {
$field_type = $replace_types[ $field_type ];
}
return $field_type;
}
/**
* Validates arguments for a field type.
*
* The function checks a whitelist of arguments.
* The only other thing it allows are data attributes.
*
* @since 0.5.0
* @param array $args the field type arguments
* @return array the validated field type arguments
*/
private static function validate_field_args( $args ) {
$field_keys = array(
'id',
'name',
'class',
'placeholder',
'required',
'readonly',
'disabled',
'options',
'label',
'rows',
'min',
'max',
'step',
'store',
'mime_types',
'repeatable',
);
$data_args = array();
foreach ( $args as $key => $value ) {
if ( strpos( $key, 'data-' ) === 0 ) {
$data_args[ $key ] = $value;
}
}
return array_merge( $data_args, array_intersect_key( $args, array_flip( $field_keys ) ) );
}
/**
* Callback function to sort HTML attributes.
*
* Attributes have the following order:
* - id
* - name
* - class
* - any data attributes
* - rel
* - type
* - value
* - href
* - any other attributes
*
* Boolean attributes are not handled by this function.
* They are automatically appended as the last attributes.
*
* @since 0.5.0
* @see WPDLib\FieldTypes\Manager::sort_html_data_attributes()
* @see WPDLib\FieldTypes\Manager::sort_html_priority_attributes()
* @param string $a the first attribute to compare
* @param string $b the second attribute to compare
* @return integer -1 if $a < $b, 1 if $a > $b, otherwise 0
*/
private static function sort_html_attributes( $a, $b ) {
if ( $a == $b ) {
return 0;
}
$priorities = array( 'id', 'name', 'class' );
if ( strpos( $a, 'data-' ) === 0 || strpos( $b, 'data-' ) === 0 ) {
return self::sort_html_data_attributes( $a, $b, $priorities );
}
$priorities = array_merge( $priorities, array( 'rel', 'type', 'value', 'href' ) );
return self::sort_html_priority_attributes( $a, $b, $priorities );
}
/**
* Sorts HTML data attributes.
*
* Attributes within the $priorities should show before the data attributes.
* All other attributes should appear after them.
*
* @since 0.5.0
* @param string $a the first attribute to compare
* @param string $b the second attribute to compare
* @param array $priorities array of high-priority attributes
* @return integer -1 if $a < $b, 1 if $a > $b, otherwise 0
*/
private static function sort_html_data_attributes( $a, $b, $priorities = array() ) {
if ( strpos( $a, 'data-' ) === 0 && strpos( $b, 'data-' ) !== 0 ) {
if ( in_array( $b, $priorities ) ) {
return 1;
}
return -1;
} elseif ( strpos( $a, 'data-' ) !== 0 && strpos( $b, 'data-' ) === 0 ) {
if ( in_array( $a, $priorities ) ) {
return -1;
}
return 1;
}
return 0;
}
/**
* Sorts HTML attributes by priority.
*
* Attributes within the $priorities should show before the other attributes (in that particular order).
*
* @since 0.5.0
* @param string $a the first attribute to compare
* @param string $b the second attribute to compare
* @param array $priorities array of high-priority attributes
* @return integer -1 if $a < $b, 1 if $a > $b, otherwise 0
*/
private static function sort_html_priority_attributes( $a, $b, $priorities = array() ) {
if ( in_array( $a, $priorities ) && ! in_array( $b, $priorities ) ) {
return -1;
} elseif ( ! in_array( $a, $priorities ) && in_array( $b, $priorities ) ) {
return 1;
} elseif ( in_array( $a, $priorities ) && in_array( $b, $priorities ) ) {
$key_a = array_search( $a, $priorities );
$key_b = array_search( $b, $priorities );
if ( $key_a < $key_b ) {
return -1;
} elseif ( $key_a > $key_b ) {
return 1;
}
}
return 0;
}
/**
* Formats a string.
*
* @since 0.5.0
* @param string $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @return string the formatted value
*/
private static function format_string( $value, $mode = 'input' ) {
return esc_html( $value );
}
/**
* Formats a HTML string.
*
* @since 0.5.0
* @param string $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @return string the formatted value
*/
private static function format_html( $value, $mode = 'input' ) {
$formatted = wp_kses_post( $value );
if ( 'output' === $mode ) {
$formatted = wpautop( $formatted );
}
return $formatted;
}
/**
* Formats a URL.
*
* @since 0.5.0
* @param string $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @return string the formatted value
*/
private static function format_url( $value, $mode = 'input' ) {
$formatted = esc_html( $value );
if ( 'output' === $mode ) {
return esc_url( $formatted );
}
return esc_url_raw( $formatted );
}
/**
* Formats a boolean.
*
* @since 0.5.0
* @param bool $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @return bool|string the formatted value (if $mode == 'output', the bool is formatted as a string)
*/
private static function format_bool( $value, $mode = 'input' ) {
$formatted = self::parse_bool( $value );
if ( 'output' === $mode ) {
if ( $formatted ) {
$formatted = 'true';
} else {
$formatted = 'false';
}
}
return $formatted;
}
/**
* Formats an integer.
*
* Possible $args:
* - positive_only (bool)
*
* @since 0.5.0
* @param integer $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @param array $args additional formatting args (optional)
* @return integer|string the formatted value (if $mode == 'output', the integer is formatted as a string)
*/
private static function format_int( $value, $mode = 'input', $args = array() ) {
$positive_only = isset( $args['positive_only'] ) ? (bool) $args['positive_only'] : false;
$formatted = self::parse_int( $value );
if ( $positive_only ) {
$formatted = abs( $formatted );
}
if ( 'output' === $mode ) {
$formatted = number_format_i18n( floatval( $formatted ), 0 );
}
return $formatted;
}
/**
* Formats a float.
*
* Possible $args:
* - positive_only (bool)
* - decimals (integer)
*
* @since 0.5.0
* @param float $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @param array $args additional formatting args (optional)
* @return float|string the formatted value (if $mode == 'output', the float is formatted as a string)
*/
private static function format_float( $value, $mode = 'input', $args = array() ) {
$positive_only = isset( $args['positive_only'] ) ? (bool) $args['positive_only'] : false;
$formatted = self::parse_float( $value );
if ( $positive_only ) {
$formatted = abs( $formatted );
}
$decimals = 0;
if ( isset( $args['decimals'] ) ) {
$decimals = absint( $args['decimals'] );
} else {
$detector = explode( '.', '' . $formatted );
if ( isset( $detector[1] ) ) {
$decimals = strlen( $detector[1] );
}
}
if ( 'output' === $mode ) {
$formatted = number_format_i18n( $formatted, $decimals );
} else {
$formatted = round( $formatted, $decimals );
}
return $formatted;
}
/**
* Formats a date.
*
* Possible $args:
* - format (string)
*
* The default format is selected depending on the $type parameter.
*
* @since 0.5.0
* @see WPDLib\FieldTypes\Manager::get_default_datetime_format()
* @param int|string $value the value to format
* @param string $type the formating type; either 'datetime' (default), 'date' or 'time'
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @param array $args additional formatting args (optional)
* @return string the formatted value
*/
private static function format_datetime( $value, $mode = 'input', $type = 'datetime', $args = array() ) {
$timestamp = $value;
if ( ! is_int( $timestamp ) ) {
$timestamp = mysql2date( 'U', $timestamp );
}
$format = isset( $args['format'] ) ? $args['format'] : '';
if ( empty( $format ) ) {
$format = self::get_default_datetime_format( $type, $mode );
}
return date_i18n( $format, $timestamp );
}
/**
* Formats a numeric value as a byte value.
*
* Possible $args:
* - decimals (integer)
* - base_unit (string, either 'B', 'kB', 'MB', 'GB' or 'TB')
*
* @since 0.5.0
* @see WPDLib\Util\Util::format_unit()
* @param integer|float $value the value to format
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @param array $args additional formatting args (optional)
* @return float|string the formatted value (if $mode == 'output', the float is formatted as a string)
*/
private static function format_byte( $value, $mode = 'input', $args = array() ) {
if ( 'output' === $mode ) {
$units = array( 'B', 'kB', 'MB', 'GB', 'TB' );
$decimals = isset( $args['decimals'] ) ? absint( $args['decimals'] ) : 2;
$base_unit = isset( $args['base_unit'] ) && in_array( $args['base_unit'], $units ) ? $args['base_unit'] : 'B';
return Util::format_unit( $value, $units, 1024, $base_unit, $decimals );
}
$formatted = self::parse_float( $value );
$decimals = isset( $args['decimals'] ) ? absint( $args['decimals'] ) : false;
if ( $decimals !== false ) {
$formatted = number_format( $formatted, $decimals );
}
return $formatted;
}
/**
* Parses a value into a boolean.
*
* @since 0.5.0
* @param mixed $value the value to parse
* @return bool the parsed value
*/
private static function parse_bool( $value ) {
if ( is_int( $value ) ) {
if ( $value > 0 ) {
return true;
}
return false;
} elseif ( is_string( $value ) ) {
if ( ! empty( $value ) ) {
if ( strtolower( $value ) == 'false' ) {
return false;
}
return true;
}
return false;
}
return (bool) $value;
}
/**
* Parses a value into an integer.
*
* @since 0.5.0
* @param mixed $value the value to parse
* @return integer the parsed value
*/
private static function parse_int( $value ) {
return intval( $value );
}
/**
* Parses a value into a float.
*
* @since 0.5.0
* @param mixed $value the value to parse
* @return float the parsed value
*/
private static function parse_float( $value ) {
return floatval( $value );
}
/**
* Returns the default date / time format.
*
* @since 0.5.0
* @param string $type the formating type; either 'datetime', 'date' or 'time'
* @param string $mode the formatting mode; either 'input' (default) or 'output'
* @return string date format string
*/
private static function get_default_datetime_format( $type, $mode = 'input' ) {
if ( 'output' === $mode ) {
if ( $type == 'date' ) {
return get_option( 'date_format' );
} elseif ( $type == 'time' ) {
return get_option( 'time_format' );
} else {
return get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
}
}
if ( $type == 'date' ) {
return 'Ymd';
} elseif ( $type == 'time' ) {
return 'His';
} else {
return 'YmdHis';
}
}
}
}