corvée(dépendances) ajoute Carbon Fields

This commit is contained in:
gcch 2024-08-09 18:45:01 +02:00
commit 62368587e5
459 changed files with 72750 additions and 26 deletions

View file

@ -0,0 +1,16 @@
<?php
namespace Carbon_Fields;
/**
* Block proxy factory class.
* Used for shorter namespace access when creating a block.
*/
class Block extends Container {
/**
* {@inheritDoc}
*/
public static function make() {
return call_user_func_array( array( parent::class, 'make' ), array_merge( array( 'block' ), func_get_args() ) );
}
}

View file

@ -0,0 +1,389 @@
<?php
namespace Carbon_Fields;
use Carbon_Fields\Pimple\Container as PimpleContainer;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Loader\Loader;
use Carbon_Fields\Container\Repository as ContainerRepository;
use Carbon_Fields\Toolset\Key_Toolset;
use Carbon_Fields\Toolset\WP_Toolset;
use Carbon_Fields\Service\Meta_Query_Service;
use Carbon_Fields\Service\Legacy_Storage_Service_v_1_5;
use Carbon_Fields\Service\Revisions_Service;
use Carbon_Fields\Service\REST_API_Service;
use Carbon_Fields\Libraries\Sidebar_Manager\Sidebar_Manager;
use Carbon_Fields\REST_API\Router as REST_API_Router;
use Carbon_Fields\REST_API\Decorator as REST_API_Decorator;
use Carbon_Fields\Event\Emitter;
use Carbon_Fields\Event\PersistentListener;
use Carbon_Fields\Event\SingleEventListener;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Holds a static reference to the ioc container
*/
final class Carbon_Fields {
/**
* An event emitter to facilitate events before the WordPress environment is guaranteed to be loaded
*
* @var Emitter
*/
protected $emitter;
/**
* Flag if Carbon Fields has been booted
*
* @var bool
*/
public $booted = false;
/**
* Inversion of Control container instance
*
* @var PimpleContainer
*/
public $ioc = null;
/**
* Singleton implementation
*
* @return Carbon_Fields
*/
public static function instance() {
static $instance = null;
if ( $instance === null ) {
$instance = new static();
}
return $instance;
}
/**
* Constructor
*/
private function __construct() {
$this->install( $this->get_default_ioc() );
}
/**
* Resolve a dependency through IoC
*
* @param string $key
* @param string|null $subcontainer Subcontainer to look into
* @return mixed
*/
public static function resolve( $key, $subcontainer = null ) {
$ioc = static::instance()->ioc;
if ( $subcontainer !== null ) {
if ( ! isset( $ioc[ $subcontainer ] ) ) {
return null;
}
$ioc = $ioc[ $subcontainer ];
}
return $ioc[ $key ];
}
/**
* Resolve a dependency through IoC with arguments
*
* @param string $identifier Key to resolve from the container
* @param array $arguments Key-value array of arguments
* @param string $subcontainer The container to resolve from
* @return mixed
*/
public static function resolve_with_arguments( $identifier, $arguments, $subcontainer = null ) {
$local_container = $subcontainer ? static::resolve( $subcontainer ) : static::instance()->ioc;
$container = new PimpleContainer();
$container['root_container'] = static::instance()->ioc;
$container['local_container'] = $local_container;
$container['arguments'] = $arguments;
$container['object'] = $local_container->raw( $identifier );
return $container['object'];
}
/**
* Resolve a service through IoC
*
* @param string $service_name
* @return mixed
*/
public static function service( $service_name ) {
return static::resolve( $service_name, 'services' );
}
/**
* Check if a dependency is registered
*
* @param string $key
* @param string|null $subcontainer Subcontainer to look into
* @return bool
*/
public static function has( $key, $subcontainer = null ) {
$ioc = static::instance()->ioc;
if ( $subcontainer !== null ) {
if ( ! isset( $ioc[ $subcontainer ] ) ) {
return false;
}
$ioc = $ioc[ $subcontainer ];
}
return isset( $ioc[ $key ] );
}
/**
* Extend Carbon Fields by adding a new entity (container condition etc.)
*
* @param string $class Extension class name
* @param string $extender Extending callable
*/
public static function extend( $class, $extender ) {
$type_dictionary = array(
'_Container' => 'containers',
'_Field' => 'fields',
'_Condition' => 'container_conditions',
);
$extension_suffix = '';
$extension_subcontainer = '';
foreach ( $type_dictionary as $suffix => $subcontainer ) {
if ( substr( $class, -strlen( $suffix ) ) === $suffix ) {
$extension_suffix = $suffix;
$extension_subcontainer = $subcontainer;
}
}
if ( empty( $extension_suffix ) ) {
Incorrect_Syntax_Exception::raise( 'Could not determine "' . $class . '" extension type. Extension classes must have one of the following suffixes: ' . implode( ', ', array_keys( $type_dictionary ) ) );
return;
}
$identifier = Helper::class_to_type( $class, $extension_suffix );
$ioc = static::instance()->ioc[ $extension_subcontainer ];
$ioc[ $identifier ] = $ioc->factory( $extender );
}
/**
* Replace the ioc container for Carbon_Fields\Carbon_Fields
*
* @param PimpleContainer $ioc
*/
public function install( PimpleContainer $ioc ) {
$this->ioc = $ioc;
}
/**
* Boot Carbon Fields with default IoC dependencies
*/
public static function boot() {
if ( static::is_booted() ) {
return;
}
if ( defined( __NAMESPACE__ . '\VERSION' ) ) {
return; // Possibly attempting to load multiple versions of Carbon Fields; bail in favor of already loaded version
}
static::resolve( 'loader' )->boot();
static::instance()->booted = true;
static::instance()->get_emitter()->emit( 'loaded' );
do_action( 'carbon_fields_loaded' );
}
/**
* Check if Carbon Fields has booted
*/
public static function is_booted() {
return static::instance()->booted;
}
/**
* Throw exception if Carbon Fields has not been booted
*/
public static function verify_boot() {
if ( ! static::is_booted() ) {
throw new \Exception( 'You must call Carbon_Fields\Carbon_Fields::boot() in a suitable WordPress hook before using Carbon Fields.' );
}
}
/**
* Throw exception if fields have not been registered yet
*/
public static function verify_fields_registered() {
$register_action = 'carbon_fields_register_fields';
$registered_action = 'carbon_fields_fields_registered';
if ( ! doing_action( $register_action ) && ! doing_action( $registered_action ) && did_action( $registered_action ) === 0 ) {
Incorrect_Syntax_Exception::raise( 'Attempted to access a field before the ' . $register_action . ' and ' . $registered_action . ' actions have fired yet.' );
}
}
/**
* Resolve the public url of a directory inside WordPress
*
* @param string $directory
* @return string
*/
public static function directory_to_url( $directory ) {
$url = \trailingslashit( $directory );
$count = 0;
# Sanitize directory separator on Windows
$url = str_replace( '\\' ,'/', $url );
$possible_locations = array(
WP_PLUGIN_DIR => \plugins_url(), # If installed as a plugin
WP_CONTENT_DIR => \content_url(), # If anywhere in wp-content
ABSPATH => \site_url( '/' ), # If anywhere else within the WordPress installation
);
foreach ( $possible_locations as $test_dir => $test_url ) {
$test_dir_normalized = str_replace( '\\' ,'/', $test_dir );
$url = str_replace( $test_dir_normalized, $test_url, $url, $count );
if ( $count > 0 ) {
return \untrailingslashit( $url );
}
}
return ''; // return empty string to avoid exposing half-parsed paths
}
/**
* Get the event emitter
*
* @return Emitter
*/
public function get_emitter() {
if ( $this->emitter === null ) {
$this->emitter = static::resolve( 'event_emitter' );
}
return $this->emitter;
}
/**
* Add a listener to an event
*
* @param string $event
* @param Event\Listener $listener
* @return Event\Listener $listener
*/
public static function add_listener( $event, $listener ) {
return static::instance()->get_emitter()->add_listener( $event, $listener );
}
/**
* Remove a listener from any event
*
* @param Event\Listener $listener
*/
public static function remove_listener( $listener ) {
static::instance()->get_emitter()->remove_listener( $listener );
}
/**
* Add a persistent listener to an event
*
* @param string $event The event to listen for
* @param string $callable The callable to call when the event is broadcasted
* @return Event\Listener
*/
public static function on( $event, $callable ) {
return static::instance()->get_emitter()->on( $event, $callable );
}
/**
* Add a one-time listener to an event
*
* @param string $event The event to listen for
* @param string $callable The callable to call when the event is broadcasted
* @return Event\Listener
*/
public static function once( $event, $callable ) {
return static::instance()->get_emitter()->once( $event, $callable );
}
/**
* Get default IoC container dependencies
*
* @return PimpleContainer
*/
protected static function get_default_ioc() {
$ioc = new PimpleContainer();
$ioc['loader'] = function( $ioc ) {
return new Loader( $ioc['sidebar_manager'], $ioc['container_repository'] );
};
$ioc['container_repository'] = function() {
return new ContainerRepository();
};
$ioc['containers'] = function() {
return new PimpleContainer();
};
$ioc['fields'] = function() {
return new PimpleContainer();
};
$ioc['key_toolset'] = function() {
return new Key_Toolset();
};
$ioc['wp_toolset'] = function() {
return new WP_Toolset();
};
$ioc['sidebar_manager'] = function() {
return new Sidebar_Manager();
};
$ioc['rest_api_router'] = function( $ioc ) {
return new REST_API_Router( $ioc['container_repository'] );
};
$ioc['rest_api_decorator'] = function( $ioc ) {
return new REST_API_Decorator( $ioc['container_repository'] );
};
/* Services */
$ioc['services'] = function() {
return new PimpleContainer();
};
$ioc['services']['revisions'] = function() use ( $ioc ) {
return new Revisions_Service();
};
$ioc['services']['meta_query'] = function() use ( $ioc ) {
return new Meta_Query_Service( $ioc['container_repository'], $ioc['key_toolset'] );
};
$ioc['services']['legacy_storage'] = function() use ( $ioc ) {
return new Legacy_Storage_Service_v_1_5( $ioc['container_repository'], $ioc['key_toolset'] );
};
$ioc['services']['rest_api'] = function() use ( $ioc ) {
return new REST_API_Service( $ioc['rest_api_router'], $ioc['rest_api_decorator'] );
};
/* Events */
$ioc['event_emitter'] = function() {
return new Emitter();
};
$ioc['event_persistent_listener'] = function() {
return new PersistentListener();
};
$ioc['event_single_event_listener'] = function() {
return new SingleEventListener();
};
$ioc->register( new \Carbon_Fields\Provider\Container_Condition_Provider() );
return $ioc;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Carbon_Fields;
/**
* Container proxy factory class.
* Used for shorter namespace access when creating a container.
*
* @method static \Carbon_Fields\Container\Comment_Meta_Container make_comment_meta( string $id, string $name = null )
* @method static \Carbon_Fields\Container\Nav_Menu_Item_Container make_nav_menu_item( string $id, string $name = null )
* @method static \Carbon_Fields\Container\Network_Container make_network( string $id, string $name = null )
* @method static \Carbon_Fields\Container\Post_Meta_Container make_post_meta( string $id, string $name = null )
* @method static \Carbon_Fields\Container\Term_Meta_Container make_term_meta( string $id, string $name = null )
* @method static \Carbon_Fields\Container\Theme_Options_Container make_theme_options( string $id, string $name = null )
* @method static \Carbon_Fields\Container\User_Meta_Container make_user_meta( string $id, string $name = null )
*/
class Container {
/**
* A proxy for the abstract container factory method.
*
* @see \Carbon_Fields\Container\Container::factory()
* @return \Carbon_Fields\Container\Container
*/
public static function factory() {
return call_user_func_array( array( '\Carbon_Fields\Container\Container', 'factory' ), func_get_args() );
}
/**
* An alias of factory().
*
* @see \Carbon_Fields\Container\Container::factory()
* @return \Carbon_Fields\Container\Container
*/
public static function make() {
return call_user_func_array( array( static::class, 'factory' ), func_get_args() );
}
/**
* @param string $method
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic( $method, $arguments ) {
if ( strpos( $method, 'make_' ) === 0 ) {
$raw_type = substr_replace( $method, '', 0, 5 );
array_unshift( $arguments, $raw_type );
return call_user_func_array( array( static::class, 'factory' ), $arguments );
} else {
trigger_error( sprintf( 'Call to undefined function: %s::%s().', static::class, $method ), E_USER_ERROR );
}
}
}

View file

@ -0,0 +1,499 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Block_Container extends Container {
/**
* {@inheritDoc}
*/
public $settings = array(
'mode' => 'edit',
'preview' => true,
'parent' => null,
'icon' => 'block-default',
'inner_blocks' => array(
'enabled' => false,
'position' => 'above',
'template' => null,
'template_lock' => null,
'allowed_blocks' => null,
),
'category' => array(
'slug' => 'common',
),
);
/**
* Mode map for settings
*
* @see set_mode()
* @var array
*/
protected $mode_map = array(
'both' => array(
'mode' => 'edit',
'preview' => true,
),
'edit' => array(
'mode' => 'edit',
'preview' => false,
),
'preview' => array(
'mode' => 'preview',
'preview' => false,
),
);
/***
* Block type render callback.
*
* @var callable
*/
protected $render_callback;
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'empty' ), $this->has_default_datastore() );
}
}
/**
* {@inheritDoc}
*/
public function init() {
add_action( 'init', array( $this, '_attach' ) );
}
/**
* {@inheritDoc}
*/
public function is_valid_save() {
// Return false because Gutenberg
// will handle saving.
return false;
}
/**
* {@inheritDoc}
*/
public function save( $data = null ) {
// Nothing to do here because
// the data is saved by Gutenberg.
}
/**
* {@inheritDoc}
*/
protected function get_environment_for_request() {
return array();
}
/**
* {@inheritDoc}
*/
public function is_valid_attach_for_request() {
return function_exists( 'register_block_type' );
}
/**
* {@inheritDoc}
*/
protected function get_environment_for_object( $object_id ) {
return array();
}
/**
* {@inheritDoc}
*/
public function is_valid_attach_for_object( $object_id = null ) {
return function_exists( 'register_block_type' );
}
/**
* {@inheritDoc}
*/
public function attach() {
add_filter( 'block_categories_all', array( $this, 'attach_block_category' ), 10, 2 );
$this->register_block();
}
/**
* Attach the category of the block type.
*
* @param array $categories
* @return array
*/
public function attach_block_category( $categories ) {
foreach ( $categories as $category ) {
if ( $category[ 'slug' ] === $this->settings[ 'category' ][ 'slug' ] ) {
return $categories;
}
}
return array_merge( $categories, array( $this->settings[ 'category' ] ) );
}
/**
* Set the description of the block type.
*
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#description-optional
*
* @param string $description
* @return Block_Container
*/
public function set_description( $description ) {
$this->settings[ 'description' ] = $description;
return $this;
}
/**
* Set the category of the block type.
*
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#category
*
* @param string $slug
* @param string $title
* @param string $icon
* @return Block_Container
*/
public function set_category( $slug, $title = null, $icon = null ) {
$this->settings[ 'category' ][ 'slug' ] = $slug;
$this->settings[ 'category' ][ 'icon' ] = $icon;
$this->settings[ 'category' ][ 'title' ] = $title ?: Helper::normalize_label( $slug );
return $this;
}
/**
* Set the icon of the block type.
*
* @see https://developer.wordpress.org/resource/dashicons
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#icon-optional
*
* @param string $icon
* @return Block_Container
*/
public function set_icon( $icon ) {
$this->settings[ 'icon' ] = $icon;
return $this;
}
/**
* Set the keywords of the block type.
*
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#keywords-optional
*
* @param array $keywords
* @return Block_Container
*/
public function set_keywords( $keywords = array() ) {
$this->settings[ 'keywords' ] = array_slice( $keywords, 0, 3 );
return $this;
}
/**
* Set a style handle.
*
* @param string $key
* @param string $handle
* @return Block_Container
*/
protected function set_style_handle( $key, $handle ) {
if ( ! wp_style_is( $handle, 'registered' ) ) {
throw new \Exception( __( "Style '$handle' is not registered.", 'crb' ) );
}
$this->settings[ $key ] = $handle;
return $this;
}
/**
* Set the style of the block type.
*
* @param string $handle
* @return Block_Container
*/
public function set_style( $handle ) {
return $this->set_style_handle( 'style', $handle );
}
/**
* Set the editor style of the block type.
*
* @param string $handle
* @return Block_Container
*/
public function set_editor_style( $handle ) {
return $this->set_style_handle( 'editor_style', $handle );
}
/**
* Set whether the preview mode is available for the block type.
*
* @param boolean $preview
* @return Block_Container
*/
public function set_preview_mode( $preview = true ) {
_deprecated_function( __FUNCTION__, '3.0', 'set_mode()' );
$mode = $preview ? 'both' : 'edit';
$this->set_mode( $mode );
return $this;
}
/**
* Set the mode for the block type.
*
* @param string $mode
* @return Block_Container
*/
public function set_mode( $mode ) {
$modes = array_keys( $this->mode_map );
if ( ! in_array( $mode, $modes ) ) {
Incorrect_Syntax_Exception::raise( 'The mode must be one of the following: ' . implode( ', ', $modes ) );
}
$this->settings[ 'mode' ] = $this->mode_map[ $mode ][ 'mode' ];
$this->settings[ 'preview' ] = $this->mode_map[ $mode ][ 'preview' ];
return $this;
}
/**
* Set the parent block(s) in which the block type can be inserted.
*
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#parent-optional
*
* @param string|string[]|null $parent
* @return Block_Container
*/
public function set_parent( $parent = null ) {
if ( ! is_array( $parent ) && ! is_string( $parent ) && ! is_null( $parent ) ) {
throw new \Exception( __( "The parent must be 'array', 'string' or 'null'.", 'crb' ) );
}
$this->settings[ 'parent' ] = is_string( $parent ) ? array( $parent ) : $parent;
return $this;
}
/**
* Set whether the inner blocks are available for the block type.
*
* @param boolean $inner_blocks
* @return Block_Container
*/
public function set_inner_blocks( $inner_blocks = true ) {
$this->settings[ 'inner_blocks' ][ 'enabled' ] = $inner_blocks;
return $this;
}
/**
* Set the position of the inner blocks to be rendered
* above or below the fields.
*
* @param string $position
* @return Block_Container
*/
public function set_inner_blocks_position( $position = 'above' ) {
if ( ! in_array( $position, [ 'above', 'below' ] ) ) {
throw new \Exception( __( "The position of inner blocks must be 'above' or 'below'.", 'crb' ) );
}
$this->settings[ 'inner_blocks' ][ 'position' ] = $position;
return $this;
}
/**
* Set the default template that should be rendered in inner blocks.
*
* @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#template
*
* @param array[]|null $template
* @return Block_Container
*/
public function set_inner_blocks_template( $template = null ) {
if ( ! is_array( $template ) && ! is_null( $template ) ) {
throw new \Exception( __( "The template must be an 'array' or 'null'.", 'crb' ) );
}
$this->settings[ 'inner_blocks' ][ 'template' ] = $template;
return $this;
}
/**
* Set the lock mode used by template of inner blocks.
*
* @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#templatelock
*
* @param string|boolean|null $lock
* @return Block_Container
*/
public function set_inner_blocks_template_lock( $lock = null ) {
if ( is_string( $lock ) && ! in_array( $lock, [ 'all', 'insert' ] ) ) {
throw new \Exception( __( "The template lock must be 'all', 'insert', 'false' or 'null'.", 'crb' ) );
}
$this->settings[ 'inner_blocks' ][ 'template_lock' ] = $lock;
return $this;
}
/**
* Set the list of allowed blocks that can be inserted.
*
* @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#allowedblocks
*
* @param string[]|null $blocks
* @return Block_Container
*/
public function set_allowed_inner_blocks( $blocks = null ) {
if ( ! is_array( $blocks ) && ! is_null( $blocks ) ) {
throw new \Exception( __( "The allowed blocks must be an 'array' or 'null'.", 'crb' ) );
}
if ( is_array( $blocks ) ) {
$this->settings[ 'inner_blocks' ][ 'allowed_blocks' ] = array_map( function ( $block ) {
if ( $block instanceof self ) {
return $block->get_block_type_name();
}
return $block;
}, $blocks );
} else {
$this->settings[ 'inner_blocks' ][ 'allowed_blocks' ] = $blocks;
}
return $this;
}
/**
* Set the render callback of the block type.
*
* @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks/
*
* @param callable $render_callback
* @return Block_Container
*/
public function set_render_callback( $render_callback ) {
$this->render_callback = $render_callback;
return $this;
}
/**
* Get the post id where the block is used in.
* Try with the GET param, and if this is an
* AJAX request get previuos (admin) edit page.
*
* @return int $post_id
*/
public function get_post_id() {
$post_id = isset( $_GET['post'] ) && (int) $_GET['post'] > 0 ? (int) $_GET['post'] : null;
if ( ! empty( $post_id ) ) {
return $post_id;
}
$admin_url = isset( $_SERVER['HTTP_REFERER'] ) && ! empty( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null;
if ( ! empty( $admin_url ) ) {
$parsed_url = parse_url( $admin_url );
if ( isset( $parsed_url['query']) && ! empty( $parsed_url['query'] ) ) {
$params = explode( '&', $parsed_url['query'] );
foreach ( $params as $param ) {
$param = explode( '=', $param );
if ( $param[0] === 'post' ) {
$post_id = $param[1];
}
}
}
}
if ( empty( $post_id ) ) {
$post_id = get_the_ID();
}
return $post_id;
}
/**
* Render the block type.
*
* @param array $attributes
* @param string $content
* @return string
*/
public function render_block( $attributes, $content ) {
$fields = $attributes['data'];
$post_id = $this->get_post_id();
// Unset the "data" property because we
// pass it as separate argument to the callback.
unset($attributes['data']);
ob_start();
call_user_func( $this->render_callback , $fields, $attributes, $content, $post_id );
return ob_get_clean();
}
/**
* Returns the block type name, e.g. "carbon-fields/testimonial"
*/
private function get_block_type_name() {
return str_replace( 'carbon-fields-container-', 'carbon-fields/', str_replace( '_', '-', $this->id ) );
}
/**
* Register the block type.
*
* @return void
*/
protected function register_block() {
if ( is_null( $this->render_callback ) ) {
throw new \Exception( __( "'render_callback' is required for the blocks.", 'crb' ) );
}
if ( ! is_callable( $this->render_callback ) ) {
throw new \Exception( __( "'render_callback' must be a callable.", 'crb' ) );
}
$style = isset( $this->settings[ 'style' ] ) ? $this->settings[ 'style' ] : null;
$editor_style = isset( $this->settings[ 'editor_style' ] ) ? $this->settings[ 'editor_style' ] : null;
$attributes = array_reduce( $this->get_fields(), function( $attributes, $field ) {
$attributes[ 'data' ][ 'default' ][ $field->get_base_name() ] = $field->get_default_value();
return $attributes;
}, array(
'data' => array(
'type' => 'object',
'default' => array(),
),
) );
register_block_type( $this->get_block_type_name(), array(
'style' => $style,
'editor_style' => $editor_style,
'attributes' => $attributes,
'render_callback' => array( $this, 'render_block' ),
) );
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container;
/**
* Broken container class.
* Used when a container gets misconfigured.
*/
class Broken_Container extends Container {
public function add_fields( $fields ) {}
public function init() {}
protected function is_valid_save() { return false; }
protected function get_environment_for_request() { return array(); }
public function is_valid_attach_for_request() { return false; }
protected function get_environment_for_object( $object_id ) { return array(); }
public function is_valid_attach_for_object( $object_id = null ) { return false; }
}

View file

@ -0,0 +1,159 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Datastore\Meta_Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Comment meta container class.
*/
class Comment_Meta_Container extends Container {
protected $comment_id;
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'comment_meta' ), $this->has_default_datastore() );
}
}
/**
* Perform instance initialization
*/
public function init() {
if ( isset( $_GET['c'] ) && $comment_id = absint( $_GET['c'] ) ) { // Input var okay.
$this->set_comment_id( $comment_id );
}
add_action( 'admin_init', array( $this, '_attach' ) );
add_action( 'edit_comment', array( $this, '_save' ) );
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
$params = func_get_args();
return $this->is_valid_attach_for_object( $params[0] );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*
* @param int $comment_id ID of the comment against which save() is ran
*/
public function save( $comment_id = null ) {
// Unhook action to guarantee single save
remove_action( 'edit_comment', array( $this, '_save' ) );
$this->set_comment_id( $comment_id );
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
$input = stripslashes_deep( $_GET );
$environment = array(
'comment_id' => isset( $input['c'] ) ? intval( $input['c'] ) : 0,
);
return $environment;
}
/**
* Check container attachment rules against current page request (in admin)
*
* @return bool
*/
public function is_valid_attach_for_request() {
global $pagenow;
if ( $pagenow !== 'comment.php' ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
$environment = array(
'comment_id' => intval( $object_id ),
);
return $environment;
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
return $this->all_conditions_pass( intval( $object_id ) );
}
/**
* Add meta box to the comment
*/
public function attach() {
add_meta_box(
$this->get_id(),
$this->title,
array( $this, 'render' ),
'comment',
'normal',
'high'
);
}
/**
* Output the container markup
*/
public function render() {
include \Carbon_Fields\DIR . '/templates/Container/comment_meta.php';
}
/**
* Set the comment ID the container will operate with.
*
* @param int $comment_id
*/
protected function set_comment_id( $comment_id ) {
$this->comment_id = $comment_id;
$this->get_datastore()->set_object_id( $comment_id );
foreach ( $this->fields as $field ) {
$datastore = $field->get_datastore();
if ( $datastore->get_object_id() === 0 ) {
$datastore->set_object_id( $comment_id );
}
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if the current blog has a specific id
*/
class Blog_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$blog_id = get_current_blog_id();
return $this->compare(
$blog_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Internal boolean (always true) condition
*/
class Boolean_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
return $this->compare(
true,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Any_Contain_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( 'IN', 'NOT IN' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
if ( ! is_array( $b ) ) {
Incorrect_Syntax_Exception::raise( 'Supplied comparison value is not an array: ' . print_r( $b, true ) );
return false;
}
$intersection = array_intersect( $a, $b );
switch ( $comparison_operator ) {
case 'IN':
return ! empty( $intersection );
case 'NOT IN':
return empty( $intersection );
}
return false;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
class Any_Equality_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( '=', '!=' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
switch ( $comparison_operator ) {
case '=':
return in_array( $b, $a );
case '!=':
return ! in_array( $b, $a );
}
return false;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
abstract class Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array();
/**
* Check if comparer supports the specified comparison sign
*
* @param string $comparison_operator
* @return bool
*/
public function supports_comparison_operator( $comparison_operator ) {
return in_array( $comparison_operator, $this->supported_comparison_operators );
}
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
abstract public function is_correct( $a, $comparison_operator, $b );
}

View file

@ -0,0 +1,38 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Contain_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( 'IN', 'NOT IN' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
if ( ! is_array( $b ) ) {
Incorrect_Syntax_Exception::raise( 'Supplied comparison value is not an array: ' . print_r( $b, true ) );
return false;
}
switch ( $comparison_operator ) {
case 'IN':
return in_array( $a, $b );
case 'NOT IN':
return ! in_array( $a, $b );
}
return false;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Custom_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( 'CUSTOM' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
if ( ! is_callable( $b ) ) {
Incorrect_Syntax_Exception::raise( 'Supplied comparison value is not a callable: ' . print_r( $b, true ) );
return false;
}
switch ( $comparison_operator ) {
case 'CUSTOM':
return (bool) $b( $a );
}
return false;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
class Equality_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( '=', '!=' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
switch ( $comparison_operator ) {
case '=':
return $a == $b;
case '!=':
return $a != $b;
}
return false;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Carbon_Fields\Container\Condition\Comparer;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Scalar_Comparer extends Comparer {
/**
* Supported comparison signs
*
* @var array<string>
*/
protected $supported_comparison_operators = array( '>', '>=', '<', '<=' );
/**
* Check if comparison is true for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
public function is_correct( $a, $comparison_operator, $b ) {
if ( ! is_scalar( $a ) ) {
Incorrect_Syntax_Exception::raise( 'Environment value for comparison is not scalar: ' . print_r( $a, true ) );
return false;
}
if ( ! is_scalar( $b ) ) {
Incorrect_Syntax_Exception::raise( 'Supplied comparison value is not scalar: ' . print_r( $b, true ) );
return false;
}
switch ( $comparison_operator ) {
case '>':
return $a > $b;
case '>=':
return $a >= $b;
case '<':
return $a < $b;
case '<=':
return $a <= $b;
}
return false;
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Carbon_Fields\Container\Condition;
use Carbon_Fields\Container\Fulfillable\Fulfillable;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Base container condition class
*/
abstract class Condition implements Fulfillable {
/**
* Condition value to check
*
* @var mixed
*/
protected $value;
/**
* Comparers to use for condition checking
*
* @var \Carbon_Fields\Container\Condition\Comparer\Comparer[]
*/
protected $comparers = array();
/**
* Comparison string to use
*
* @var string
*/
protected $comparison_operator = '';
/**
* Get the condition value
*
* @return mixed
*/
public function get_value() {
if ( $this->get_comparison_operator() !== 'CUSTOM' && $this->value instanceof \Closure ) {
return call_user_func( $this->value );
}
return $this->value;
}
/**
* Set the condition value
*
* @param mixed $value
* @return Condition $this
*/
public function set_value( $value ) {
$this->value = $value;
return $this;
}
/**
* Get the condition comparers
*
* @return \Carbon_Fields\Container\Condition\Comparer\Comparer[]
*/
protected function get_comparers() {
return $this->comparers;
}
/**
* Set the condition comparers
*
* @param \Carbon_Fields\Container\Condition\Comparer\Comparer[] $comparers
* @return Condition $this
*/
public function set_comparers( $comparers ) {
$this->comparers = $comparers;
return $this;
}
/**
* Find the first operator which supports $comparison_operator and check if it is correct for $a and $b
*
* @param mixed $a
* @param string $comparison_operator
* @param mixed $b
* @return bool
*/
protected function compare( $a, $comparison_operator, $b ) {
$comparers = $this->get_comparers();
foreach ( $comparers as $comparer ) {
if ( ! $comparer->supports_comparison_operator( $comparison_operator ) ) {
continue;
}
return $comparer->is_correct( $a, $comparison_operator, $b );
}
Incorrect_Syntax_Exception::raise( 'Unsupported container condition comparison operator used: ' . $comparison_operator );
return false;
}
/**
* Get comparison sign used
*
* @return string
*/
public function get_comparison_operator() {
return $this->comparison_operator;
}
/**
* Set comparison sign
*
* @param string $comparison_operator
* @return $this
*/
public function set_comparison_operator( $comparison_operator ) {
$this->comparison_operator = $comparison_operator;
return $this;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if user has a specific capability
*
* Operator "CUSTOM" is passed the user id
*/
class Current_User_Capability_Condition extends User_Capability_Condition {
/**
* Get user id from environment
*
* @param array $environment
* @return integer
*/
protected function get_user_id( $environment ) {
return get_current_user_id();
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if the currently logged in user has a specific id
*/
class Current_User_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$user_id = get_current_user_id();
return $this->compare(
$user_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if the currently logged in user has a specific role
*
* Operator "CUSTOM" is passed an array of all user roles
*/
class Current_User_Role_Condition extends User_Role_Condition {
/**
* Get roles for a user from the environment
*
* @param array $environment
* @return array<string>
*/
protected function get_user_roles( $environment ) {
$user = wp_get_current_user();
$roles = $user ? $user->roles : array();
return $roles;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Carbon_Fields\Container\Condition;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Factory {
/**
* Container to resolve conditions from
*/
protected $ioc;
/**
* Constructor
*
* @param \Carbon_Fields\Pimple\Container $ioc
*/
public function __construct( $ioc ) {
$this->ioc = $ioc;
}
/**
* Get the type for the specified class
*
* @param string $class
* @return string
*/
public function get_type( $class ) {
return Helper::class_to_type( $class, '_Condition' );
}
/**
* Get an instance of the specified type
*
* @param string $type
* @return mixed
*/
public function make( $type ) {
$normalized_type = Helper::normalize_type( $type );
if ( isset( $this->ioc[ $normalized_type ] ) ) {
return $this->ioc[ $normalized_type ];
}
Incorrect_Syntax_Exception::raise( 'Unknown condition type "' . $type . '".' );
return null;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if post has a specific ancestor
*
* Operator "CUSTOM" is passed an array of ancestor post ids
*/
class Post_Ancestor_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
$post = $environment['post'];
$ancestors = array();
if ( $post ) {
$ancestors = array_map( 'intval', get_ancestors( $post_id, $post->post_type ) );
}
return $this->compare(
$ancestors,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check is post is of specific format
*
* Pass an empty string as the value for this condition in order to test if the post has no format assigned
*/
class Post_Format_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
$format = get_post_format( $post_id );
$format = ( $format ) ? $format : ''; // force an empty string for falsy values to ensure strict comparisons work
return $this->compare(
$format,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check for a specific post id
*/
class Post_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
return $this->compare(
$post_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if a post is on a specific hierarchy level
*
* Level 1 is considered the root level. Passed values have a forced minimum value of 1.
*/
class Post_Level_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
$post_level = count( get_post_ancestors( $post_id ) ) + 1;
$value = $this->get_value();
if ( is_numeric( $value ) ) {
$value = max( 1, intval( $this->get_value() ) );
}
return $this->compare(
$post_level,
$this->get_comparison_operator(),
$value
);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if post has a specific parent
*/
class Post_Parent_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post = $environment['post'];
$post_parent_id = is_object( $post ) ? intval( $post->post_parent ) : 0;
return $this->compare(
$post_parent_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if post has a specific template
*
* Pass "default" as the value for the default post template
*/
class Post_Template_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
$is_page_for_posts = intval( $post_id ) === intval( get_option( 'page_for_posts' ) );
$post_template = get_post_meta( $post_id, '_wp_page_template', true );
if ( ! $post_template || $is_page_for_posts ) {
$post_template = 'default';
}
return $this->compare(
$post_template,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Carbon_Fields\Container\Condition;
use Carbon_Fields\Toolset\WP_Toolset;
/**
* Check if a post has a specific term
*
* Accepts the following values:
* Operators "=" and "!=":
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...] // "slug", "term_id" etc. - see get_term_by()
* )
*
* Operators "IN" and "NOT IN":
* array(
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...]
* ),
* ...
* )
*
* Operator "CUSTOM" is passed the post id
*/
class Post_Term_Condition extends Term_Condition {
/**
* Check if a post has a term
*
* @param integer $post_id
* @param array $full_term_desriptor
* @return boolean
*/
protected function post_has_term( $post_id, $full_term_desriptor ) {
$term = $full_term_desriptor['term_object'];
return has_term( intval( $term->term_id ), $term->taxonomy, intval( $post_id ) );
}
/**
* Check if a post has any of the supplied terms
*
* @param integer $post_id
* @param array<array> $full_term_desriptors
* @return boolean
*/
protected function post_has_any_term( $post_id, $full_term_desriptors ) {
foreach ( $full_term_desriptors as $full_term_desriptor ) {
if ( $this->post_has_term( $post_id, $full_term_desriptor ) ) {
return true;
}
}
return false;
}
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_id = $environment['post_id'];
switch ( $this->get_comparison_operator() ) {
case '=':
return $this->post_has_term( $post_id, $this->get_value() );
break;
case '!=':
return ! $this->post_has_term( $post_id, $this->get_value() );
break;
case 'IN':
return $this->post_has_any_term( $post_id, $this->get_value() );
break;
case 'NOT IN':
return ! $this->post_has_any_term( $post_id, $this->get_value() );
break;
}
return $this->compare(
$post_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check is post is of specific type
*/
class Post_Type_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$post_type = $environment['post_type'];
return $this->compare(
$post_type,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if term has a specific ancestor
*
* Accepts the following values:
* Operators "=" and "!=":
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...] // "slug", "term_id" etc. - see get_term_by()
* )
*
* Operators "IN" and "NOT IN":
* array(
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...]
* ),
* ...
* )
*
* Operator "CUSTOM" is passed an array of ancestor term ids
*/
class Term_Ancestor_Condition extends Term_Condition {
public function is_fulfilled( $environment ) {
$term_id = $environment['term_id'];
$term = $environment['term'];
$ancestors = array();
if ( $term ) {
$ancestors = array_map( 'intval', get_ancestors( $term_id, $term->taxonomy ) );
}
$value = $this->get_value();
switch ( $this->get_comparison_operator() ) {
case '=': // fallthrough intended
case '!=':
$value = $this->get_term_id_from_full_term_descriptor( $value );
break;
case 'IN': // fallthrough intended
case 'NOT IN':
$value = $this->get_term_ids_from_full_term_descriptors( $value );
break;
}
return $this->compare(
$ancestors,
$this->get_comparison_operator(),
$value
);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Carbon_Fields\Container\Condition;
use Carbon_Fields\Toolset\WP_Toolset;
/**
* Check for a specific term
*
* Accepts the following values:
* Operators "=" and "!=":
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...] // "slug", "term_id" etc. - see get_term_by()
* )
*
* Operators "IN" and "NOT IN":
* array(
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...]
* ),
* ...
* )
*
* Operator "CUSTOM" is passed the term_id
*/
class Term_Condition extends Condition {
/**
* WP_Toolset to fetch term data with
*
* @var WP_Toolset
*/
protected $wp_toolset;
/**
* Constructor
*/
public function __construct( $wp_toolset ) {
$this->wp_toolset = $wp_toolset;
}
/**
* Get the condition value
*
* @return mixed
*/
public function get_value() {
$value = parent::get_value();
if ( is_array( $value ) ) {
if ( isset( $value['value'] ) ) {
$value = $this->wp_toolset->wildcard_term_descriptor_to_full_term_descriptor( $value );
} else {
$value = array_map( array( $this->wp_toolset, 'wildcard_term_descriptor_to_full_term_descriptor' ), $value );
}
}
return $value;
}
/**
* Pluck term id from a full term descriptor
*
* @param array $full_term_descriptor
* @return integer
*/
protected function get_term_id_from_full_term_descriptor( $full_term_descriptor ) {
return intval( $full_term_descriptor['term_object']->term_id );
}
/**
* Pluck term ids from array of full term descriptors
*
* @param array $full_term_descriptors
* @return array<integer>
*/
protected function get_term_ids_from_full_term_descriptors( $full_term_descriptors ) {
return array_map( array( $this, 'get_term_id_from_full_term_descriptor' ), $full_term_descriptors );
}
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$term_id = $environment['term_id'];
switch ( $this->get_comparison_operator() ) {
case '=':
$value_term_id = $this->get_term_id_from_full_term_descriptor( $this->get_value() );
return $term_id == $value_term_id;
break;
case '!=':
$value_term_id = $this->get_term_id_from_full_term_descriptor( $this->get_value() );
return $term_id != $value_term_id;
break;
case 'IN':
$value_term_ids = $this->get_term_ids_from_full_term_descriptors( $this->get_value() );
return in_array( $term_id, $value_term_ids );
break;
case 'NOT IN':
$value_term_ids = $this->get_term_ids_from_full_term_descriptors( $this->get_value() );
return ! in_array( $term_id, $value_term_ids );
break;
}
return $this->compare(
$term_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if a term is on a specific hierarchy level
*
* Level 1 is considered the root level. Passed values have a forced minimum value of 1.
*/
class Term_Level_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$term = $environment['term'];
$term_level = 1;
if ( $term ) {
$term_level = count( get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) ) + 1;
}
$value = $this->get_value();
if ( is_numeric( $value ) ) {
$value = max( 1, intval( $this->get_value() ) );
}
return $this->compare(
$term_level,
$this->get_comparison_operator(),
$value
);
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if term has a specific parent
*
* Accepts the following values:
* Operators "=" and "!=":
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...] // "slug", "term_id" etc. - see get_term_by()
* )
*
* Operators "IN" and "NOT IN":
* array(
* array(
* 'value'=>...,
* 'taxonomy'=>...,
* ['field'=>...]
* ),
* ...
* )
*
* Operator "CUSTOM" is passed the parent term_id
*/
class Term_Parent_Condition extends Term_Condition {
public function is_fulfilled( $environment ) {
$term = $environment['term'];
$parent_term_id = $term ? intval( $term->parent ) : 0;
$value = $this->get_value();
switch ( $this->get_comparison_operator() ) {
case '=': // fallthrough intended
case '!=':
$value = $this->get_term_id_from_full_term_descriptor( $value );
break;
case 'IN': // fallthrough intended
case 'NOT IN':
$value = $this->get_term_ids_from_full_term_descriptors( $value );
break;
}
return $this->compare(
$parent_term_id,
$this->get_comparison_operator(),
$value
);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if term is of a specific taxonomy
*/
class Term_Taxonomy_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$taxonomy = $environment['taxonomy'];
return $this->compare(
$taxonomy,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if user has a specific capability
*
* Operator "CUSTOM" is passed the user id
*/
class User_Capability_Condition extends Condition {
/**
* Get user id from environment
*
* @param array $environment
* @return integer
*/
protected function get_user_id( $environment ) {
return $environment['user_id'];
}
/**
* Check if a user has any of the supplied capabilities
*
* @param integer $user_id
* @param array<string> $capabilities
* @return boolean
*/
protected function user_can_any( $user_id, $capabilities ) {
foreach ( $capabilities as $cap ) {
if ( user_can( $user_id, $cap ) ) {
return true;
}
}
return false;
}
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$user_id = $this->get_user_id( $environment );
switch ( $this->get_comparison_operator() ) {
case '=':
return user_can( $user_id, $this->get_value() );
break;
case '!=':
return ! user_can( $user_id, $this->get_value() );
break;
case 'IN':
return $this->user_can_any( $user_id, $this->get_value() );
break;
case 'NOT IN':
return ! $this->user_can_any( $user_id, $this->get_value() );
break;
}
return $this->compare(
$user_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check for a specific user id
*/
class User_ID_Condition extends Condition {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$user_id = $environment['user_id'];
return $this->compare(
$user_id,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Carbon_Fields\Container\Condition;
/**
* Check if user has a specific role
*
* Operator "CUSTOM" is passed an array of all user roles
*/
class User_Role_Condition extends Condition {
/**
* Get roles for a user from the environment
*
* @param array $environment
* @return array<string>
*/
protected function get_user_roles( $environment ) {
return $environment['roles'];
}
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$roles = $this->get_user_roles( $environment );
return $this->compare(
$roles,
$this->get_comparison_operator(),
$this->get_value()
);
}
}

View file

@ -0,0 +1,918 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Carbon_Fields;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Field\Field;
use Carbon_Fields\Field\Group_Field;
use Carbon_Fields\Container\Fulfillable\Fulfillable_Collection;
use Carbon_Fields\Datastore\Datastore_Interface;
use Carbon_Fields\Datastore\Datastore_Holder_Interface;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Base container class.
* Defines the key container methods and their default implementations.
*/
abstract class Container implements Datastore_Holder_Interface {
/**
* Where to put a particular tab -- at the head or the tail. Tail by default
*/
const TABS_TAIL = 1;
const TABS_HEAD = 2;
/**
* Separator signifying field hierarchy relation
* Used when searching for fields in a specific complex field
*/
const HIERARCHY_FIELD_SEPARATOR = '/';
/**
* Separator signifying complex_field->group relation
* Used when searching for fields in a specific complex field group
*/
const HIERARCHY_GROUP_SEPARATOR = ':';
/**
* Visual layout type constants
*/
const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal';
const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical';
/**
* Stores if the container is active on the current page
*
* @see activate()
* @var bool
*/
protected $active = false;
/**
* List of registered unique field names for this container instance
*
* @see register_field_name()
* @var array
*/
protected $registered_field_names = array();
/**
* Complex field layout
*
* @var string static::LAYOUT_* constant
*/
protected $layout = self::LAYOUT_TABBED_HORIZONTAL;
/**
* Tabs available
*/
protected $tabs = array();
/**
* List of default container settings
*
* @see init()
* @var array
*/
public $settings = array();
/**
* Unique ID of the container
*
* @var string
*/
public $id;
/**
* Title of the container
*
* @var string
*/
public $title = '';
/**
* Type of the container
*
* @var string
*/
public $type;
/**
* List of notification messages to be displayed on the front-end
*
* @var array
*/
protected $notifications = array();
/**
* List of error messages to be displayed on the front-end
*
* @var array
*/
protected $errors = array();
/**
* List of container fields
*
* @see add_fields()
* @var array
*/
protected $fields = array();
/**
* Array of custom CSS classes.
*
* @see set_classes()
* @see get_classes()
* @var array<string>
*/
protected $classes = array();
/**
* Container datastores. Propagated to all container fields
*
* @see set_datastore()
* @see get_datastore()
* @var Datastore_Interface
*/
protected $datastore;
/**
* Flag whether the datastore is the default one or replaced with a custom one
*
* @see set_datastore()
* @see get_datastore()
* @var boolean
*/
protected $has_default_datastore = true;
/**
* Fulfillable_Collection to use when checking attachment/saving conditions
*
* @var Fulfillable_Collection
*/
protected $condition_collection;
/**
* Translator to use when translating conditions to json
*
* @var \Carbon_Fields\Container\Fulfillable\Translator\Translator
*/
protected $condition_translator;
/**
* Create a new container of type $type and name $name.
*
* @param string $raw_type
* @param string $id Unique id for the container. Optional
* @param string $name Human-readable name of the container
* @return Container $container
*/
public static function factory( $raw_type, $id, $name = '' ) {
// no name provided - switch input around as the id is optionally generated based on the name
if ( $name === '' ) {
$name = $id;
$id = '';
}
$type = Helper::normalize_type( $raw_type );
$repository = Carbon_Fields::resolve( 'container_repository' );
$id = $repository->get_unique_container_id( ( $id !== '' ) ? $id : $name );
if ( ! Helper::is_valid_entity_id( $id ) ) {
Incorrect_Syntax_Exception::raise( 'Container IDs can only contain lowercase alphanumeric characters, dashes and underscores ("' . $id . '" passed).' );
return null;
}
if ( ! $repository->is_unique_container_id( $id ) ) {
Incorrect_Syntax_Exception::raise( 'The passed container id is already taken ("' . $id . '" passed).' );
return null;
}
$container = null;
if ( Carbon_Fields::has( $type, 'containers' ) ) {
$container = Carbon_Fields::resolve_with_arguments( $type, array(
'id' => $id,
'name' => $name,
'type' => $type,
), 'containers' );
} else {
// Fallback to class name-based resolution
$class = Helper::type_to_class( $type, __NAMESPACE__, '_Container' );
if ( ! class_exists( $class ) ) {
Incorrect_Syntax_Exception::raise( 'Unknown container "' . $raw_type . '".' );
$class = __NAMESPACE__ . '\\Broken_Container';
}
$fulfillable_collection = Carbon_Fields::resolve( 'container_condition_fulfillable_collection' );
$condition_translator = Carbon_Fields::resolve( 'container_condition_translator_json' );
$container = new $class( $id, $name, $type, $fulfillable_collection, $condition_translator );
}
$repository->register_container( $container );
return $container;
}
/**
* An alias of factory().
*
* @see Container::factory()
* @return Container
*/
public static function make() {
return call_user_func_array( array( static::class, 'factory' ), func_get_args() );
}
/**
* Create a new container
*
* @param string $id Unique id of the container
* @param string $title Title of the container
* @param string $type Type of the container
* @param Fulfillable_Collection $condition_collection
* @param \Carbon_Fields\Container\Fulfillable\Translator\Translator $condition_translator
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
Carbon_Fields::verify_boot();
if ( empty( $title ) ) {
Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
}
$this->id = $id;
$this->title = $title;
$this->type = $type;
$this->condition_collection = $condition_collection;
$this->condition_collection->set_condition_type_list(
array_merge( $this->get_condition_types( true ), $this->get_condition_types( false ) ),
true
);
$this->condition_translator = $condition_translator;
}
/**
* Return the container id
*
* @return string
*/
public function get_id() {
return $this->id;
}
/**
* Get array of all static condition types
*
* @param boolean $static
* @return array<string>
*/
protected function get_condition_types( $static ) {
$group = $static ? 'static' : 'dynamic';
$container_type = Helper::class_to_type( get_class( $this ), '_Container' );
$condition_types = array();
$condition_types = apply_filters( 'carbon_fields_' . $container_type . '_container_' . $group . '_condition_types', $condition_types, $container_type, $this );
$condition_types = apply_filters( 'carbon_fields_container_' . $group . '_condition_types', $condition_types, $container_type, $this );
return $condition_types;
}
/**
* Return whether the container is active
*/
public function is_active() {
return $this->active;
}
/**
* Activate the container and trigger an action
*/
protected function activate() {
$this->active = true;
$this->boot();
do_action( 'carbon_fields_container_activated', $this );
$fields = $this->get_fields();
foreach ( $fields as $field ) {
$field->activate();
}
}
/**
* Perform instance initialization
*/
abstract public function init();
/**
* Boot the container once it's attached.
*/
protected function boot() {
}
/**
* Load the value for each field in the container.
* Could be used internally during container rendering
*/
public function load() {
foreach ( $this->fields as $field ) {
$field->load();
}
}
/**
* Called first as part of the container save procedure.
* Responsible for checking the request validity and
* calling the container-specific save() method
*
* @see save()
* @see is_valid_save()
*/
public function _save() {
$param = func_get_args();
if ( call_user_func_array( array( $this, '_is_valid_save' ), $param ) ) {
call_user_func_array( array( $this, 'save' ), $param );
}
}
/**
* Load submitted data and save each field in the container
*
* @see is_valid_save()
*/
public function save( $data = null ) {
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
final protected function _is_valid_save() {
$params = func_get_args();
$is_valid_save = call_user_func_array( array( $this, 'is_valid_save' ), $params );
return apply_filters( 'carbon_fields_container_is_valid_save', $is_valid_save, $this );
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
abstract protected function is_valid_save();
/**
* Called first as part of the container attachment procedure.
* Responsible for checking it's OK to attach the container
* and if it is, calling the container-specific attach() method
*
* @see attach()
* @see is_valid_attach()
*/
public function _attach() {
if ( ! $this->is_valid_attach() ) {
return;
}
$param = func_get_args();
call_user_func_array( array( $this, 'attach' ), $param );
// Allow containers to initialize but not activate (useful in cases such as theme options)
if ( $this->should_activate() ) {
$this->activate();
}
}
/**
* Attach the container rendering and helping methods
* to concrete WordPress Action hooks
*/
public function attach() {}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
final public function is_valid_attach() {
$is_valid_attach = $this->is_valid_attach_for_request();
return apply_filters( 'carbon_fields_container_is_valid_attach', $is_valid_attach, $this );
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
abstract protected function get_environment_for_request();
/**
* Check container attachment rules against current page request (in admin)
*
* @return bool
*/
abstract protected function is_valid_attach_for_request();
/**
* Check if conditions pass for request
*
* @return bool
*/
protected function static_conditions_pass() {
$environment = $this->get_environment_for_request();
$static_condition_collection = $this->condition_collection->evaluate(
$this->get_condition_types( false ),
true
);
return $static_condition_collection->is_fulfilled( $environment );
}
/**
* Get environment array for object id
*
* @param integer $object_id
* @return array
*/
abstract protected function get_environment_for_object( $object_id );
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
abstract public function is_valid_attach_for_object( $object_id );
/**
* Check if all conditions pass for object
*
* @param integer $object_id
* @return bool
*/
protected function all_conditions_pass( $object_id ) {
$environment = $this->get_environment_for_object( $object_id );
return $this->condition_collection->is_fulfilled( $environment );
}
/**
* Whether this container is currently viewed.
*/
public function should_activate() {
return true;
}
/**
* Perform a check whether the current container has fields
*
* @return bool
*/
public function has_fields() {
return (bool) $this->fields;
}
/**
* Returns the private container array of fields.
* Use only if you are completely aware of what you are doing.
*
* @return Field[]
*/
public function get_fields() {
return $this->fields;
}
/**
* Return root field from container with specified name
*
* @example crb_complex
*
* @param string $field_name
* @return Field
*/
public function get_root_field_by_name( $field_name ) {
$fields = $this->get_fields();
foreach ( $fields as $field ) {
if ( $field->get_base_name() === $field_name ) {
return $field;
}
}
return null;
}
/**
* Get a regex to match field name patterns used to fetch specific fields
*
* @return string
*/
protected function get_field_pattern_regex() {
$field_name_characters = Helper::get_field_name_characters_pattern();
// matches:
// field_name
// field_name[0]
// field_name[0]:group_name
// field_name:group_name
$regex = '/
\A
(?P<field_name>[' . $field_name_characters . ']+)
(?:\[(?P<group_index>\d+)\])?
(?:' . preg_quote( static::HIERARCHY_GROUP_SEPARATOR, '/' ). '(?P<group_name>[' . $field_name_characters . ']+))?
\z
/x';
return $regex;
}
/**
* Return field from container with specified name
*
* @example $field_name = 'crb_complex/text_field'
* @example $field_name = 'crb_complex/complex_2'
* @example $field_name = 'crb_complex/complex_2:text_group/text_field'
* @example $field_name = 'crb_complex[3]/complex_2[1]:text_group/text_field'
*
* @param string $field_name
* @return Field
*/
public function get_field_by_name( $field_name ) {
$hierarchy = array_filter( explode( static::HIERARCHY_FIELD_SEPARATOR, $field_name ) );
$field = null;
$field_group = $this->get_fields();
$hierarchy_left = $hierarchy;
$field_pattern_regex = $this->get_field_pattern_regex();
$hierarchy_index = array();
while ( ! empty( $hierarchy_left ) ) {
$segment = array_shift( $hierarchy_left );
$segment_pieces = array();
if ( ! preg_match( $field_pattern_regex, $segment, $segment_pieces ) ) {
return null;
}
$segment_field_name = $segment_pieces['field_name'];
$segment_group_index = isset( $segment_pieces['group_index'] ) ? $segment_pieces['group_index'] : 0;
$segment_group_name = isset( $segment_pieces['group_name'] ) ? $segment_pieces['group_name'] : Group_Field::DEFAULT_GROUP_NAME;
foreach ( $field_group as $f ) {
if ( $f->get_base_name() !== $segment_field_name ) {
continue;
}
if ( empty( $hierarchy_left ) ) {
$field = clone $f;
$field->set_hierarchy_index( $hierarchy_index );
} else {
if ( ! ( $f instanceof \Carbon_Fields\Field\Complex_Field ) ) {
return null;
}
$group = $f->get_group_by_name( $segment_group_name );
if ( ! $group ) {
return null;
}
$field_group = $group->get_fields();
$hierarchy_index[] = $segment_group_index;
}
break;
}
}
return $field;
}
/**
* Perform checks whether there is a field registered with the name $name.
* If not, the field name is recorded.
*
* @param string $name
* @return boolean
*/
protected function register_field_name( $name ) {
if ( in_array( $name, $this->registered_field_names ) ) {
Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
return false;
}
$this->registered_field_names[] = $name;
return true;
}
/**
* Return whether the datastore instance is the default one or has been overriden
*
* @return boolean
*/
public function has_default_datastore() {
return $this->has_default_datastore;
}
/**
* Set datastore instance
*
* @param Datastore_Interface $datastore
* @param bool $set_as_default (optional)
* @return Container $this
*/
public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
if ( $set_as_default && ! $this->has_default_datastore() ) {
return $this; // datastore has been overriden with a custom one - abort changing to a default one
}
$this->datastore = $datastore;
$this->has_default_datastore = $set_as_default;
foreach ( $this->fields as $field ) {
$field->set_datastore( $this->get_datastore(), true );
}
return $this;
}
/**
* Get the DataStore instance
*
* @return Datastore_Interface $datastore
*/
public function get_datastore() {
return $this->datastore;
}
/**
* Return WordPress nonce name used to identify the current container instance
*
* @return string
*/
protected function get_nonce_name() {
return $this->get_id() . '_nonce';
}
/**
* Return WordPress nonce name used to identify the current container instance
*
* @return string
*/
protected function get_nonce_value() {
return wp_create_nonce( $this->get_nonce_name() );
}
/**
* Check if the nonce is present in the request and that it is verified
*
* @return bool
*/
protected function verified_nonce_in_request() {
$input = Helper::input();
$nonce_name = $this->get_nonce_name();
$nonce_value = isset( $input[ $nonce_name ] ) ? $input[ $nonce_name ] : '';
return wp_verify_nonce( $nonce_value, $nonce_name );
}
/**
* Internal function that creates the tab and associates it with particular field set
*
* @param string $tab_name
* @param array $fields
* @param int $queue_end
* @return object $this
*/
private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
if ( isset( $this->tabs[ $tab_name ] ) ) {
Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
}
if ( $queue_end === static::TABS_TAIL ) {
$this->tabs[ $tab_name ] = array();
} else if ( $queue_end === static::TABS_HEAD ) {
$this->tabs = array_merge(
array( $tab_name => array() ),
$this->tabs
);
}
foreach ( $fields as $field ) {
$field_name = $field->get_name();
$this->tabs[ $tab_name ][ $field_name ] = $field;
}
$this->settings['tabs'] = $this->get_tabs_json();
}
/**
* Whether the container is tabbed or not
*
* @return bool
*/
public function is_tabbed() {
return (bool) $this->tabs;
}
/**
* Retrieve all fields that are not defined under a specific tab
*
* @return array
*/
protected function get_untabbed_fields() {
$tabbed_fields_names = array();
foreach ( $this->tabs as $tab_fields ) {
$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
}
$untabbed_fields = array_filter( $this->fields, function( $field ) use ( $tabbed_fields_names ) {
return ! in_array( $field->get_name(), $tabbed_fields_names );
} );
return $untabbed_fields;
}
/**
* Retrieve all tabs.
* Create a default tab if there are any untabbed fields.
*
* @return array
*/
protected function get_tabs() {
$untabbed_fields = $this->get_untabbed_fields();
if ( ! empty( $untabbed_fields ) ) {
$this->create_tab(
apply_filters( 'carbon_fields_untabbed_fields_tab_title', __( 'General', 'carbon-fields' ), $this ),
$untabbed_fields,
static::TABS_HEAD
);
}
return $this->tabs;
}
/**
* Build the tabs JSON
*
* @return array
*/
protected function get_tabs_json() {
$tabs_json = array();
$tabs = $this->get_tabs();
foreach ( $tabs as $tab_name => $fields ) {
foreach ( $fields as $field_name => $field ) {
$tabs_json[ $tab_name ][] = $field_name;
}
}
return $tabs_json;
}
/**
* Get custom CSS classes.
*
* @return array<string>
*/
public function get_classes() {
return $this->classes;
}
/**
* Set CSS classes that the container should use.
*
* @param string|array<string> $classes
* @return Container $this
*/
public function set_classes( $classes ) {
$this->classes = Helper::sanitize_classes( $classes );
return $this;
}
/**
* Returns an array that holds the container data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$conditions = $this->condition_collection->evaluate( $this->get_condition_types( true ), $this->get_environment_for_request(), array( 'CUSTOM' ) );
$conditions = $this->condition_translator->fulfillable_to_foreign( $conditions );
$container_data = array(
'id' => $this->get_id(),
'type' => $this->type,
'title' => $this->title,
'layout' => $this->layout,
'classes' => $this->get_classes(),
'settings' => $this->settings,
'conditions' => $conditions,
'fields' => array(),
'nonce' => array(
'name' => $this->get_nonce_name(),
'value' => $this->get_nonce_value(),
),
);
$fields = $this->get_fields();
foreach ( $fields as $field ) {
$field_data = $field->to_json( $load );
$container_data['fields'][] = $field_data;
}
return $container_data;
}
/**
* COMMON USAGE METHODS
*/
/**
* Append array of fields to the current fields set. All items of the array
* must be instances of Field and their names should be unique for all
* Carbon containers.
* If a field does not have DataStore already, the container datastore is
* assigned to them instead.
*
* @param array $fields
* @return Container $this
*/
public function add_fields( $fields ) {
foreach ( $fields as $field ) {
if ( ! ( $field instanceof Field ) ) {
Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
return $this;
}
$unique = $this->register_field_name( $field->get_name() );
if ( ! $unique ) {
return $this;
}
$field->set_context( $this->type );
if ( ! $field->get_datastore() ) {
$field->set_datastore( $this->get_datastore(), $this->has_default_datastore() );
}
}
$this->fields = array_merge( $this->fields, $fields );
return $this;
}
/**
* Configuration function for adding tab with fields
*
* @param string $tab_name
* @param array $fields
* @return Container $this
*/
public function add_tab( $tab_name, $fields ) {
$this->add_fields( $fields );
$this->create_tab( $tab_name, $fields );
return $this;
}
/**
* Proxy function to set attachment conditions
*
* @see Fulfillable_Collection::where()
* @return Container $this
*/
public function where() {
call_user_func_array( array( $this->condition_collection, 'where' ), func_get_args() );
return $this;
}
/**
* Proxy function to set attachment conditions
*
* @see Fulfillable_Collection::or_where()
* @return Container $this
*/
public function or_where() {
call_user_func_array( array( $this->condition_collection, 'or_where' ), func_get_args() );
return $this;
}
/**
* Modify the layout of this field.
*
* @param string $layout
* @return self $this
*/
public function set_layout( $layout ) {
$available_layouts = array(
static::LAYOUT_TABBED_HORIZONTAL,
static::LAYOUT_TABBED_VERTICAL,
);
if ( ! in_array( $layout, $available_layouts ) ) {
$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
'Available layouts: ' . implode( ', ', $available_layouts );
Incorrect_Syntax_Exception::raise( $error_message );
return $this;
}
$this->layout = $layout;
return $this;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Carbon_Fields\Container\Fulfillable;
interface Fulfillable {
/**
* Check if the condition is fulfilled
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment );
}

View file

@ -0,0 +1,378 @@
<?php
namespace Carbon_Fields\Container\Fulfillable;
use Carbon_Fields\Container\Condition\Factory;
use Carbon_Fields\Container\Fulfillable\Translator\Array_Translator;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Fulfillable_Collection implements Fulfillable {
/**
* Condition factory used to translated condition types
*
* @var Factory
*/
protected $condition_factory;
/**
* Array translator used to support array representations of fulfillables
*
* @var Array_Translator
*/
protected $array_translator;
/**
* Array of fulfillables in this collection
*
* @var array<array>
*/
protected $fulfillables = array();
/**
* Array of supported fulfillable comparisons
*
* @var array<string>
*/
protected $supported_fulfillable_comparisons = array( 'AND', 'OR' );
/**
* Array of allowed condition types which propagate to child collections
*
* @var array<string>
*/
protected $condition_type_list = array();
/**
* Whether the condition type list is a whitelist or a blacklist
*
* @var bool
*/
protected $condition_type_list_whitelist = false;
/**
* Constructor
*
* @param Factory $condition_factory
* @param Array_Translator $array_translator
*/
public function __construct( Factory $condition_factory, Array_Translator $array_translator ) {
$this->condition_factory = $condition_factory;
$this->array_translator = $array_translator;
}
/**
* Create a new collection
*
* @return Fulfillable_Collection
*/
protected function create_collection() {
return \Carbon_Fields\Carbon_Fields::resolve( 'container_condition_fulfillable_collection' );
}
/**
* Get an array of the fulfillables in this collection
*
* @return array<Fulfillable>
*/
public function get_fulfillables() {
return $this->fulfillables;
}
/**
* Get array of allowed condition types
*
* @return array<string>
*/
public function get_condition_type_list() {
return $this->condition_type_list;
}
/**
* Set array of allowed condition types
* WARNING: this will NOT remove already added conditions which are no longer allowed
*
* @param array<string> $condition_type_list
* @param bool $whitelist
* @return Fulfillable_Collection $this
*/
public function set_condition_type_list( $condition_type_list, $whitelist ) {
$this->condition_type_list_whitelist = $whitelist;
$this->condition_type_list = $condition_type_list;
$this->propagate_condition_type_list();
return $this;
}
/**
* Check if conditions types list is a whitelist
*
* @return bool
*/
public function is_condition_type_list_whitelist() {
return $this->condition_type_list_whitelist;
}
/**
* Check if condition type is allowed
*
* @param string $condition_type
* @return bool
*/
public function is_condition_type_allowed( $condition_type ) {
$in_list = in_array( $condition_type, $this->get_condition_type_list() );
if ( $this->is_condition_type_list_whitelist() ) {
return $in_list;
}
return ! $in_list;
}
/**
* Propagate allowed condition types to child collections
*/
protected function propagate_condition_type_list() {
$condition_type_list = $this->get_condition_type_list();
$fulfillables = $this->get_fulfillables();
foreach ( $fulfillables as $fulfillable ) {
if ( $fulfillable['fulfillable'] instanceof Fulfillable_Collection ) {
$fulfillable['fulfillable']->set_condition_type_list( $condition_type_list, $this->is_condition_type_list_whitelist() );
}
}
}
/**
* Shorthand for where with OR comparison
*
* @param string|array|callable $condition_type
* @param string $comparison_operator Can be skipped. Defaults to "="
* @param mixed $value
* @return Fulfillable_Collection $this
*/
public function or_where( $condition_type, $comparison_operator = '=', $value = null ) {
$this->where( $condition_type, $comparison_operator, $value, 'OR' );
return $this;
}
/**
* Add fulfillable with optional comparison_operator
* This method assumes there is no fulfillable that can be compared with literal NULL
*
* @param string|array|callable $condition_type
* @param string $comparison_operator Can be skipped. Defaults to "="
* @param mixed $value
* @param string $fulfillable_comparison
* @return Fulfillable_Collection $this
*/
public function where( $condition_type, $comparison_operator = '=', $value = null, $fulfillable_comparison = 'AND' ) {
if ( is_array( $condition_type ) ) {
return $this->where_array( $condition_type, $fulfillable_comparison );
}
if ( $condition_type instanceof \Closure ) {
return $this->where_collection( $condition_type, $fulfillable_comparison );
}
if ( ! $this->is_condition_type_allowed( $condition_type ) ) {
Incorrect_Syntax_Exception::raise( 'Unsupported container condition used: ' . $condition_type );
return $this;
}
if ( $value === null ) {
// We do not have a supplied comparison_operator so we default to "="
$value = $comparison_operator;
$comparison_operator = '=';
}
$condition = $this->condition_factory->make( $condition_type );
$condition->set_comparison_operator( $comparison_operator );
$condition->set_value( $value );
$this->add_fulfillable( $condition, $fulfillable_comparison );
return $this;
}
/**
* Add a Fulfillable through array representation
*
* @param array $fulfillable_as_array
* @param string $fulfillable_comparison
* @return Fulfillable_Collection $this
*/
protected function where_array( $fulfillable_as_array, $fulfillable_comparison) {
$fulfillable = $this->array_translator->foreign_to_fulfillable( $fulfillable_as_array );
$this->add_fulfillable( $fulfillable, $fulfillable_comparison );
return $this;
}
/**
* Add a Fulfillable_Collection for nested logic
*
* @param callable $collection_callable
* @param string $fulfillable_comparison
* @return Fulfillable_Collection $this
*/
protected function where_collection( $collection_callable, $fulfillable_comparison) {
$collection = $this->create_collection();
$collection->set_condition_type_list( $this->get_condition_type_list(), $this->is_condition_type_list_whitelist() );
$collection_callable( $collection );
$this->add_fulfillable( $collection, $fulfillable_comparison );
return $this;
}
/**
* Add fulfillable to collection
*
* @param Fulfillable $fulfillable
* @param string $fulfillable_comparison See static::$supported_fulfillable_comparisons
*/
public function add_fulfillable( Fulfillable $fulfillable, $fulfillable_comparison ) {
if ( ! in_array( $fulfillable_comparison, $this->supported_fulfillable_comparisons ) ) {
Incorrect_Syntax_Exception::raise( 'Invalid fulfillable comparison passed: ' . $fulfillable_comparison );
return;
}
$this->fulfillables[] = array(
'fulfillable_comparison' => $fulfillable_comparison,
'fulfillable' => $fulfillable,
);
}
/**
* Remove fulfillable from collection
*
* @param Fulfillable $fulfillable
* @return bool Fulfillable found and removed
*/
public function remove_fulfillable( Fulfillable $fulfillable ) {
$fulfillables = $this->get_fulfillables();
foreach ( $fulfillables as $index => $fulfillable_tuple ) {
if ( $fulfillable_tuple['fulfillable'] === $fulfillable ) {
$fulfillables_copy = $fulfillables; // introduce a copy array to highlight array_splice mutation
array_splice( $fulfillables_copy, $index, 1 );
$this->fulfillables = array_values( $fulfillables_copy ); // make sure our array is indexed cleanly
return true;
}
}
return false;
}
/**
* Get a copy of the collection with conditions not in the whitelist filtered out
*
* @param array<string> $condition_whitelist
* @return Fulfillable_Collection
*/
public function filter( $condition_whitelist ) {
$fulfillables = $this->get_fulfillables();
$collection = $this->create_collection();
foreach ( $fulfillables as $fulfillable_tuple ) {
$fulfillable = $fulfillable_tuple['fulfillable'];
$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
if ( $fulfillable instanceof Fulfillable_Collection ) {
$filtered_collection = $fulfillable->filter( $condition_whitelist );
$filtered_collection_fulfillables = $filtered_collection->get_fulfillables();
if ( empty( $filtered_collection_fulfillables ) ) {
continue; // skip empty collections to reduce clutter
}
$collection->add_fulfillable( $filtered_collection, $fulfillable_comparison );
} else {
$type = $this->condition_factory->get_type( get_class( $fulfillable ) );
if ( ! in_array( $type, $condition_whitelist ) ) {
continue;
}
$fulfillable_clone = clone $fulfillable;
$collection->add_fulfillable( $fulfillable_clone, $fulfillable_comparison );
}
}
return $collection;
}
/**
* Get a copy of the collection with passed conditions evaluated into boolean conditions
* Useful when evaluating only certain condition types but preserving the rest
* or when passing dynamic conditions to the front-end
*
* @param array<string> $condition_types
* @param array|boolean $environment Environment array or a boolean value to force on conditions
* @param array<string> $comparison_operators Array of comparison operators to evaluate regardless of condition type
* @param boolean $condition_types_blacklist Whether the condition list should act as a blacklist
* @param boolean $comparison_operators_blacklist Whether the comparison operators list should act as a blacklist
* @return Fulfillable_Collection
*/
public function evaluate( $condition_types, $environment, $comparison_operators = array(), $condition_types_blacklist = false, $comparison_operators_blacklist = false ) {
$fulfillables = $this->get_fulfillables();
$collection = $this->create_collection();
foreach ( $fulfillables as $fulfillable_tuple ) {
$fulfillable = $fulfillable_tuple['fulfillable'];
$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
if ( $fulfillable instanceof Fulfillable_Collection ) {
$evaluated_collection = $fulfillable->evaluate( $condition_types, $environment, $comparison_operators, $condition_types_blacklist, $comparison_operators_blacklist );
$collection->add_fulfillable( $evaluated_collection, $fulfillable_comparison );
} else {
$type = $this->condition_factory->get_type( get_class( $fulfillable ) );
$comparison_operator = $fulfillable->get_comparison_operator();
$condition_type_match = in_array( $type, $condition_types );
if ( $condition_types_blacklist ) {
$condition_type_match = ! $condition_type_match;
}
$comparison_operator_match = in_array( $comparison_operator, $comparison_operators );
if ( $comparison_operators_blacklist ) {
$comparison_operator_match = ! $comparison_operator_match;
}
if ( $condition_type_match || $comparison_operator_match ) {
$boolean_condition = $this->condition_factory->make( 'boolean' );
$boolean_condition->set_comparison_operator( '=' );
$value = is_bool( $environment ) ? $environment : $fulfillable->is_fulfilled( $environment );
$boolean_condition->set_value( $value );
$collection->add_fulfillable( $boolean_condition, $fulfillable_comparison );
} else {
$collection->add_fulfillable( clone $fulfillable, $fulfillable_comparison );
}
}
}
return $collection;
}
/**
* Check if all fulfillables are fulfilled taking into account their fulfillable comparison
*
* @param array $environment
* @return bool
*/
public function is_fulfilled( $environment ) {
$fulfilled = true; // return true for empty collections
$fulfillables = $this->get_fulfillables();
foreach ( $fulfillables as $i => $fulfillable_tuple ) {
$fulfillable = $fulfillable_tuple['fulfillable'];
$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
if ( $i === 0 ) {
// Ignore first comparison as we need a base fulfillment value
$fulfilled = $fulfillable->is_fulfilled( $environment );
continue;
}
// minor optimization - avoid unnecessary AND check if $fulfilled is currently false
// false && whatever is always false
if ( $fulfillable_comparison == 'AND' && $fulfilled ) {
$fulfilled = $fulfillable->is_fulfilled( $environment );
}
// minor optimization - avoid unnecessary OR check if $fulfilled is currently true
// true || whatever is always true
if ( $fulfillable_comparison == 'OR' && ! $fulfilled ) {
$fulfilled = $fulfillable->is_fulfilled( $environment );
}
}
return $fulfilled;
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Carbon_Fields\Container\Fulfillable\Translator;
use Carbon_Fields\Container\Condition\Factory;
use Carbon_Fields\Container\Fulfillable\Fulfillable_Collection;
use Carbon_Fields\Container\Condition\Condition;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Array_Translator extends Translator {
/**
* Condition factory used to translated condition types
*
* @var Factory
*/
protected $condition_factory;
/**
* Constructor
*
* @param Factory $condition_factory
*/
public function __construct( Factory $condition_factory ) {
$this->condition_factory = $condition_factory;
}
/**
* {@inheritDoc}
*/
protected function condition_to_foreign( Condition $condition ) {
return array(
'type' => $this->condition_factory->get_type( get_class( $condition ) ),
'compare' => $condition->get_comparison_operator(),
'value' => $condition->get_value(),
);
}
/**
* {@inheritDoc}
*/
protected function fulfillable_collection_to_foreign( Fulfillable_Collection $fulfillable_collection ) {
$fulfillables = $fulfillable_collection->get_fulfillables();
if ( empty( $fulfillables ) ) {
return array();
}
$collection = array(
'relation' => 'AND',
);
$relations = array();
foreach ( $fulfillables as $fulfillable_tuple ) {
$comparison = $fulfillable_tuple['fulfillable_comparison'];
$fulfillable = $fulfillable_tuple['fulfillable'];
if ( ! isset( $relations[ $comparison ] ) ) {
$relations[ $comparison ] = array();
}
$relations[ $comparison ][] = $this->fulfillable_to_foreign( $fulfillable );
}
if ( ! empty( $relations['OR'] ) ) {
$collection['relation'] = 'OR';
}
foreach ( $relations as $relation => $fulfillables ) {
$collection[] = array( 'relation' => $relation ) + $fulfillables;
}
if ( count( $relations ) === 1 ) {
// we only have one relation group so we simplify the fulfillables with 1 level
$collection = $collection[0];
}
return array_filter( $collection );
}
/**
* {@inheritDoc}
*/
public function foreign_to_fulfillable( $foreign ) {
if ( ! is_array( $foreign ) ) {
Incorrect_Syntax_Exception::raise( 'Invalid data passed to array condition translator: ' . print_r( $foreign, true ) );
return null;
}
if ( isset( $foreign['type'] ) ) {
return $this->foreign_to_native_condition( $foreign );
}
return $this->foreign_to_native_fulfillable_collection( $foreign );
}
/**
* Translate a Condition
*
* @param array $foreign
* @return Condition
*/
protected function foreign_to_native_condition( $foreign ) {
$condition_type = $foreign['type'];
$comparison_operator = isset( $foreign['compare'] ) ? $foreign['compare'] : '=';
$value = isset( $foreign['value'] ) ? $foreign['value'] : '';
$condition = $this->condition_factory->make( $condition_type );
$condition->set_comparison_operator( $comparison_operator );
$condition->set_value( $value );
return $condition;
}
/**
* Translate a Fulfillable_Collection
*
* @param array $foreign
* @return Fulfillable_Collection
*/
protected function foreign_to_native_fulfillable_collection( $foreign ) {
$fulfillable_comparison = isset( $foreign['relation'] ) ? $foreign['relation'] : 'AND';
$collection = \Carbon_Fields\Carbon_Fields::resolve( 'container_condition_fulfillable_collection' );
foreach ( $foreign as $key => $value ) {
if ( $key === 'relation' ) {
continue; // ignore the relation key - we are only interested in condition definitions
}
$collection->add_fulfillable( $this->foreign_to_fulfillable( $value ), $fulfillable_comparison );
}
return $collection;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Carbon_Fields\Container\Fulfillable\Translator;
use Carbon_Fields\Container\Fulfillable\Fulfillable;
class Json_Translator extends Array_Translator {
/**
* {@inheritDoc}
*/
public function fulfillable_to_foreign( Fulfillable $fulfillable ) {
$result = parent::fulfillable_to_foreign( $fulfillable );
return $this->foreign_to_json( $result );
}
/**
* Make conditions friendly for json-based frontend.
*
* @param array $foreign
* @return array
*/
protected function foreign_to_json( $foreign ) {
if ( empty( $foreign ) ) {
return array(
'relation' => 'AND',
'conditions' => array(),
);
}
if ( ! isset( $foreign['relation'] ) ) {
return $foreign;
}
$conditions = array();
foreach ( $foreign as $key => $value ) {
if ( $key === 'relation' ) {
continue;
}
if ( isset( $value['relation'] ) ) {
$conditions[] = $this->foreign_to_json( $value );
} else {
if ( isset( $value['type'] ) ) {
$conditions[] = $value;
} else {
$conditions = array_merge( $conditions, $value );
}
}
}
return array(
'relation' => $foreign['relation'],
'conditions' => $conditions,
);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Carbon_Fields\Container\Fulfillable\Translator;
use Carbon_Fields\Container\Fulfillable\Fulfillable;
use Carbon_Fields\Container\Fulfillable\Fulfillable_Collection;
use Carbon_Fields\Container\Condition\Condition;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
abstract class Translator {
/**
* Translate a Fulfillable to foreign data
*
* @param Fulfillable $fulfillable
* @return mixed
*/
public function fulfillable_to_foreign( Fulfillable $fulfillable ) {
if ( $fulfillable instanceof Condition ) {
return $this->condition_to_foreign( $fulfillable );
}
if ( $fulfillable instanceof Fulfillable_Collection ) {
return $this->fulfillable_collection_to_foreign( $fulfillable );
}
Incorrect_Syntax_Exception::raise( 'Attempted to translate an unsupported object: ' . print_r( $fulfillable, true ) );
return null;
}
/**
* Translate a Condition to foreign data
*
* @param Condition $condition
* @return mixed
*/
abstract protected function condition_to_foreign( Condition $condition );
/**
* Translate a Fulfillable_Collection to foreign data
*
* @param Fulfillable_Collection $fulfillable_collection
* @return mixed
*/
abstract protected function fulfillable_collection_to_foreign( Fulfillable_Collection $fulfillable_collection );
/**
* Translate foreign data to a Fulfillable
*
* @param mixed $foreign
* @return Fulfillable
*/
abstract public function foreign_to_fulfillable( $foreign );
}

View file

@ -0,0 +1,229 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Nav menu item fields container class.
*/
class Nav_Menu_Item_Container extends Container {
/**
* Array of container clones for every menu item
*
* @see init()
* @var int
*/
protected $menu_item_instances = array();
/**
* The menu item id this container is for
*
* @var int
*/
protected $menu_item_id = 0;
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'nav_menu_item' ), $this->has_default_datastore() );
}
// Register the custom edit walker only once
$callable = array( static::class, 'edit_walker' );
if ( ! has_filter( 'wp_edit_nav_menu_walker', $callable ) ) {
add_filter( 'wp_edit_nav_menu_walker', $callable, 10, 2 );
}
}
/**
* Perform instance initialization
*
* @param int $menu_item_id Used to pass the correct menu_item_id to the Container object
*/
public function init( $menu_item_id = 0 ) {
$this->menu_item_id = $menu_item_id;
$this->get_datastore()->set_object_id( $this->menu_item_id );
$this->_attach();
// Only the base container should register for updating/rendering
if ( $this->menu_item_id === 0 ) {
add_action( 'wp_update_nav_menu_item', array( $this, 'update' ), 10, 3 );
add_action( 'carbon_fields_print_nav_menu_item_container_fields', array( $this, 'form' ), 10, 5 );
}
return $this;
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
$params = func_get_args();
return $this->is_valid_attach_for_object( $params[0] );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*/
public function save( $data = null ) {
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
do_action( 'carbon_fields_nav_menu_item_container_saved', $this );
}
/**
* {@inheritDoc}
*/
public function is_active() {
return ( $this->active && $this->menu_item_id !== 0 );
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
return array();
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
global $pagenow;
$input = Helper::input();
$ajax = defined( 'DOING_AJAX' ) ? DOING_AJAX : false;
$ajax_action = isset( $input['action'] ) ? $input['action'] : '';
$is_on_menu_page = ( $pagenow === 'nav-menus.php' );
$is_menu_ajax_request = ( $ajax && $ajax_action === 'add-menu-item' );
if ( ! $is_on_menu_page && ! $is_menu_ajax_request ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
return array();
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
$post = get_post( $object_id );
if ( ! $post ) {
return false;
}
if ( $post->post_type !== 'nav_menu_item' ) {
return false;
}
return $this->all_conditions_pass( intval( $post->ID ) );
}
/**
* Output the container markup
*/
public function render() {
include \Carbon_Fields\DIR . '/templates/Container/nav_menu_item.php';
}
/**
* Trigger Save for all instances
*/
public function update( $menu_id, $current_menu_item_id ) {
if ( ! $this->is_valid_attach_for_request() ) {
return;
}
$clone = $this->get_clone_for_menu_item( $current_menu_item_id, false );
$clone->_save( $current_menu_item_id );
}
/**
* Render custom fields inside each Nav Menu entry
*/
public function form( $item ) {
if ( ! $this->is_valid_attach_for_request() ) {
return;
}
$clone = $this->get_clone_for_menu_item( $item->ID );
$clone->render();
}
/**
* Create a clone of this container with its own datastore for every menu item
*/
protected function get_clone_for_menu_item( $menu_item_id, $load = true ) {
if ( ! isset( $this->menu_item_instances[ $menu_item_id ] ) ) {
$menu_item_datastore = Datastore::make( 'nav_menu_item' );
$menu_item_datastore->set_object_id( $menu_item_id );
$menu_item_field_prefix = $menu_item_datastore->get_garbage_prefix();
$custom_fields = array();
$fields = $this->get_fields();
foreach ( $fields as $field ) {
$tmp_field = clone $field;
$tmp_field->set_id( $menu_item_field_prefix . $tmp_field->get_id() );
$tmp_field->set_name( $menu_item_field_prefix . $tmp_field->get_name() );
$tmp_field->set_datastore( $menu_item_datastore, true );
$custom_fields[] = $tmp_field;
}
$container = Container::factory( $this->type, $menu_item_field_prefix . $this->get_id() )
->set_datastore( $menu_item_datastore, true )
->add_fields( $custom_fields )
->init( $menu_item_id );
if ( $load ) {
$container->load();
}
$this->menu_item_instances[ $menu_item_id ] = $container;
}
return $this->menu_item_instances[ $menu_item_id ];
}
/**
* Setup custom walker for the Nav Menu entries
*/
public static function edit_walker() {
return 'Carbon_Fields\\Walker\\Nav_Menu_Item_Edit_Walker';
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Theme options container class.
*/
class Network_Container extends Theme_Options_Container {
/**
* ID of the site the container is operating with
*
* @see init()
* @var int
*/
protected $site_id;
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! is_multisite() ) {
Incorrect_Syntax_Exception::raise( 'The "' . $title . '" container will not be available because your site is not a multisite.' );
return;
}
$this->set_datastore( Datastore::make( 'network' ), $this->has_default_datastore() );
$this->set_site_id( SITE_ID_CURRENT_SITE );
}
/**
* {@inheritDoc}
*/
public function init() {
$registered = $this->register_page();
if ( $registered ) {
add_action( 'network_admin_menu', array( $this, '_attach' ) );
}
}
/**
* Check if a site exists by id
*
* @param integer $id
* @return boolean
*/
protected function site_exists( $id ) {
if ( ! function_exists( 'get_blog_status' ) ) {
return false;
}
$blog_domain = get_blog_status( $id, 'domain' );
return ! empty( $blog_domain );
}
/**
* Get the site ID the container is operating with.
*
* @return integer
*/
public function get_site_id() {
return $this->site_id;
}
/**
* Set the site ID the container will operate with.
*
* @param int $id
* @return self $this
*/
public function set_site_id( $id ) {
$id = intval( $id );
if ( ! $this->site_exists( $id ) ) {
Incorrect_Syntax_Exception::raise( 'The specified site id #' . $id . ' does not exist' );
return $this;
}
$this->site_id = $id;
$this->datastore->set_object_id( $this->get_site_id() );
return $this;
}
}

View file

@ -0,0 +1,463 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Field container designed to extend WordPress custom fields functionality,
* providing easier user interface to add, edit and delete text, media files,
* location information and more.
*/
class Post_Meta_Container extends Container {
/**
* ID of the post the container is working with
*
* @see init()
* @var int
*/
protected $post_id;
/**
* Post Types
*
*/
protected $post_types;
/**
* Determines whether revisions are disabled for this container
*
* @var bool
*/
protected $revisions_disabled = false;
/**
* List of default container settings
*
* @see init()
* @var array
*/
public $settings = array(
'meta_box_context' => 'normal',
'meta_box_priority' => 'high',
);
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'post_meta' ), $this->has_default_datastore() );
}
}
/**
* Create DataStore instance, set post ID to operate with (if such exists).
* Bind attach() and save() to the appropriate WordPress actions.
*/
public function init() {
$input = stripslashes_deep( $_GET );
$request_post_id = isset( $input['post'] ) ? intval( $input['post'] ) : 0;
if ( $request_post_id > 0 ) {
$this->set_post_id( $request_post_id );
}
add_action( 'admin_init', array( $this, '_attach' ) );
add_action( 'save_post', array( $this, '_save' ) );
// support for attachments
add_action( 'add_attachment', array( $this, '_save' ) );
add_action( 'edit_attachment', array( $this, '_save' ) );
}
/**
* Checks whether the current save request is valid
* Possible errors are triggering save() for autosave requests
* or performing post save outside of the post edit page (like Quick Edit)
*
* @return bool
*/
public function is_valid_save() {
$params = func_get_args();
$post_id = $params[0];
$post_type = get_post_type( $post_id );
$wp_preview = ( ! empty( $_POST['wp-preview'] ) ) ? $_POST['wp-preview'] : '';
$in_preview = $wp_preview === 'dopreview';
$doing_autosave = defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE;
$is_revision = $post_type === 'revision';
if ( ( $doing_autosave || $is_revision ) && ( ! $in_preview || $this->revisions_disabled ) ) {
return false;
}
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
return $this->is_valid_attach_for_object( $post_id );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*
* @param int $post_id ID of the post against which save() is ran
*/
public function save( $post_id = null ) {
// Unhook action to garantee single save
remove_action( 'save_post', array( $this, '_save' ) );
$this->set_post_id( $post_id );
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
do_action( 'carbon_fields_post_meta_container_saved', $post_id, $this );
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
global $pagenow;
$input = stripslashes_deep( $_GET );
$request_post_type = isset( $input['post_type'] ) ? $input['post_type'] : '';
$post_type = '';
if ( $this->post_id ) {
$post_type = get_post_type( $this->post_id );
} elseif ( ! empty( $request_post_type ) ) {
$post_type = $request_post_type;
} elseif ( $pagenow === 'post-new.php' ) {
$post_type = 'post';
}
$post = get_post( $this->post_id );
$post = $post ? $post : null;
$environment = array(
'post_id' => $post ? $post->ID : 0,
'post_type' => $post ? $post->post_type : $post_type,
'post' => $post,
);
return $environment;
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
global $pagenow;
if ( $pagenow !== 'post.php' && $pagenow !== 'post-new.php' ) {
return false;
}
$environment = $this->get_environment_for_request();
if ( ! $environment['post_type'] ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
$post = get_post( intval( $object_id ) );
$post_type = $post->post_type;
if ( wp_is_post_revision( $post ) !== false ) {
$post = get_post( intval( $post->post_parent ) );
$post_type = $post->post_type;
}
$environment = array(
'post_id' => $post->ID,
'post' => $post,
'post_type' => $post_type,
);
return $environment;
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
$post = get_post( intval( $object_id ) );
if ( ! $post ) {
return false;
}
return $this->all_conditions_pass( intval( $post->ID ) );
}
/**
* Add meta box for each of the container post types
*/
public function attach() {
$this->post_types = $this->get_post_type_visibility();
foreach ( $this->post_types as $post_type ) {
add_meta_box(
$this->get_id(),
$this->title,
array( $this, 'render' ),
$post_type,
$this->settings['meta_box_context'],
$this->settings['meta_box_priority']
);
$container_id = $this->get_id();
add_filter( "postbox_classes_{$post_type}_{$container_id}", array( $this, 'add_postbox_classes' ) );
}
}
/**
* Classes to add to the post meta box
*/
public function add_postbox_classes( $classes ) {
$classes[] = 'carbon-box';
return $classes;
}
/**
* Output the container markup
*/
public function render() {
include \Carbon_Fields\DIR . '/templates/Container/post_meta.php';
}
/**
* Set the post ID the container will operate with.
*
* @param int $post_id
*/
public function set_post_id( $post_id ) {
$this->post_id = $post_id;
$this->get_datastore()->set_object_id( $post_id );
foreach ( $this->fields as $field ) {
$datastore = $field->get_datastore();
if ( $datastore->get_object_id() === 0 ) {
$datastore->set_object_id( $post_id );
}
}
}
/**
* Get array of post types this container can appear on conditionally
*
* @return array<string>
*/
public function get_post_type_visibility() {
$all_post_types = get_post_types();
$evaluated_collection = $this->condition_collection->evaluate( array( 'post_type' ), true, array(), true );
$shown_on = array();
foreach ( $all_post_types as $post_type ) {
$environment = array(
'post_type' => $post_type,
);
if ( $evaluated_collection->is_fulfilled( $environment ) ) {
$shown_on[] = $post_type;
}
}
return $shown_on;
}
/**
* COMMON USAGE METHODS
*/
/**
* Show the container only on particular page referenced by its path.
*
* @deprecated
* @param int|string $page page ID or page path
* @return object $this
*/
public function show_on_page( $page ) {
$page_id = absint( $page );
if ( $page_id && $page_id == $page ) {
$page_obj = get_post( $page_id );
} else {
$page_obj = get_page_by_path( $page );
}
$page_id = ( $page_obj ) ? $page_obj->ID : -1;
$this->where( 'post_id', '=', $page_id );
return $this;
}
/**
* Show the container only on pages whose parent is referenced by $parent_page_path.
*
* @deprecated
* @param string $parent_page_path
* @return object $this
*/
public function show_on_page_children( $parent_page_path ) {
$page = get_page_by_path( $parent_page_path );
$page_id = ( $page ) ? $page->ID : -1;
$this->where( 'post_parent_id', '=', $page_id );
return $this;
}
/**
* Show the container only on pages whose template has filename $template_path.
*
* @deprecated
* @param string|array $template_path
* @return object $this
*/
public function show_on_template( $template_path ) {
// Backwards compatibility where only pages support templates
if ( version_compare( get_bloginfo( 'version' ), '4.7', '<' ) ) {
$this->show_on_post_type( 'page' );
}
$template_paths = is_array( $template_path ) ? $template_path : array( $template_path );
$this->where( 'post_template', 'IN', $template_paths );
return $this;
}
/**
* Hide the container from pages whose template has filename $template_path.
*
* @deprecated
* @param string|array $template_path
* @return object $this
*/
public function hide_on_template( $template_path ) {
$template_paths = is_array( $template_path ) ? $template_path : array( $template_path );
$this->where( 'post_template', 'NOT IN', $template_paths );
return $this;
}
/**
* Show the container only on hierarchical posts of level $level.
* Levels start from 1 (top level post)
*
* @deprecated
* @param int $level
* @return object $this
*/
public function show_on_level( $level ) {
$this->where( 'post_level', '=', intval( $level ) );
return $this;
}
/**
* Show the container only on posts from the specified format.
* Learn more about {@link http://codex.wordpress.org/Post_Formats Post Formats (Codex)}
*
* @deprecated
* @param string|array $post_format Name of the format as listed on Codex
* @return object $this
*/
public function show_on_post_format( $post_format ) {
$post_formats = is_array( $post_format ) ? $post_format : array( $post_format );
$this->where( 'post_format', 'IN', $post_formats );
return $this;
}
/**
* Show the container only on posts from the specified type(s).
*
* @deprecated
* @param string|array $post_types
* @return object $this
*/
public function show_on_post_type( $post_types ) {
$post_types = is_array( $post_types ) ? $post_types : array( $post_types );
$this->where( 'post_type', 'IN', $post_types );
return $this;
}
/**
* Show the container only on posts from the specified category.
*
* @see show_on_taxonomy_term()
*
* @deprecated
* @param string $category_slug
* @return object $this
*/
public function show_on_category( $category_slug ) {
$this->where( 'post_term', '=', array(
'value' => $category_slug,
'field' => 'slug',
'taxonomy' => 'category',
) );
return $this;
}
/**
* Show the container only on posts which have term $term_slug from the $taxonomy_slug taxonomy.
*
* @deprecated
* @param string $taxonomy_slug
* @param string $term_slug
* @return object $this
*/
public function show_on_taxonomy_term( $term_slug, $taxonomy_slug ) {
$this->where( 'post_term', '=', array(
'value' => $term_slug,
'field' => 'slug',
'taxonomy' => $taxonomy_slug,
) );
return $this;
}
/**
* Sets the meta box container context
*
* @see https://codex.wordpress.org/Function_Reference/add_meta_box
* @param string $context ('normal', 'advanced', 'side' or the custom `carbon_fields_after_title`)
*/
public function set_context( $context ) {
$this->settings['meta_box_context'] = $context;
return $this;
}
/**
* Sets the meta box container priority
*
* @see https://codex.wordpress.org/Function_Reference/add_meta_box
* @param string $priority ('high', 'core', 'default' or 'low')
*/
public function set_priority( $priority ) {
$this->settings['meta_box_priority'] = $priority;
return $this;
}
public function set_revisions_disabled( $revisions_disabled ) {
$this->revisions_disabled = $revisions_disabled;
return $this;
}
public function get_revisions_disabled() {
return $this->revisions_disabled;
}
}

View file

@ -0,0 +1,246 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Keeps track of all instantiated containers
*/
class Repository {
/**
* List of registered unique container ids
*
* @see get_unique_container_id()
* @see register_unique_container_id()
* @see unregister_unique_container_id()
* @var array
*/
protected $registered_container_ids = array();
/**
* List of registered containers that should be initialized
*
* @see initialize_containers()
* @var array
*/
protected $pending_containers = array();
/**
* List of all containers
*
* @see _attach()
* @var array
*/
protected $containers = array();
/**
* Container id prefix
*
* @var string
*/
protected $container_id_prefix = 'carbon_fields_container_';
/**
* Container id prefix
*
* @var string
*/
protected $widget_id_wildcard_suffix = '-__i__';
/**
* Register a container with the repository
*
* @param Container $container
*/
public function register_container( Container $container ) {
$this->register_unique_container_id( $container->get_id() );
$this->containers[] = $container;
$this->pending_containers[] = $container;
}
/**
* Initialize registered containers
*
* @return Container[]
*/
public function initialize_containers() {
$initialized_containers = array();
while ( ( $container = array_shift( $this->pending_containers ) ) ) {
$container->init();
$initialized_containers[] = $container;
}
return $initialized_containers;
}
/**
* Return all containers
*
* @param string $type Container type to filter for
* @return Container[]
*/
public function get_containers( $type = null ) {
$raw_containers = $this->containers;
$containers = array();
if ( $type === null ) {
$containers = $raw_containers;
} else {
$normalized_type = Helper::normalize_type( $type );
foreach ( $raw_containers as $container ) {
if ( $container->type === $normalized_type ) {
$containers[] = $container;
}
}
}
return $containers;
}
/**
* Return field in a container with supplied id
*
* @param string $field_name
* @param string $container_id
* @param bool $include_nested_fields
* @return \Carbon_Fields\Field\Field
*/
public function get_field_in_container( $field_name, $container_id, $include_nested_fields = true ) {
$containers = $this->get_containers();
$field = null;
foreach ( $containers as $container ) {
if ( $container->get_id() !== $container_id ) {
continue;
}
if ( $include_nested_fields ) {
$field = $container->get_field_by_name( $field_name );
} else {
$field = $container->get_root_field_by_name( $field_name );
}
break;
}
return $field;
}
/**
* Return field in containers
*
* @param string $field_name
* @param string $container_type
* @param bool $include_nested_fields
* @return \Carbon_Fields\Field\Field
*/
public function get_field_in_containers( $field_name, $container_type = null, $include_nested_fields = true ) {
$containers = $this->get_containers( $container_type );
$field = null;
foreach ( $containers as $container ) {
if ( $include_nested_fields ) {
$field = $container->get_field_by_name( $field_name );
} else {
$field = $container->get_root_field_by_name( $field_name );
}
if ( $field ) {
break;
}
}
return $field;
}
/**
* Return all currently active containers
*
* @return Container[]
*/
public function get_active_containers() {
return array_filter( $this->containers, function( $container ) {
/** @var Container $container */
return $container->is_active();
} );
}
/**
* Check if container identificator id is unique
*
* @param string $id
* @return bool
*/
public function is_unique_container_id( $id ) {
return ! in_array( $id, $this->registered_container_ids );
}
/**
* Generate a unique container identificator id based on container title
*
* @param string $title
* @return string
*/
public function get_unique_container_id( $title ) {
$id = remove_accents( $title );
$id = strtolower( $id );
$id_prefix = $this->container_id_prefix;
if ( substr( $id, 0, strlen( $id_prefix ) ) === $id_prefix ) {
$id_prefix = '';
}
$wids = $this->widget_id_wildcard_suffix;
$id_suffix = '';
if ( substr( $id, -strlen( $wids ) ) === $wids ) {
$id_suffix = $wids;
$id = substr( $id, 0, -strlen( $wids ) );
}
$id = preg_replace( '~[\s]+~', '_', $id );
$id = preg_replace( '~[^\w\-\_]+~', '', $id );
// Remove multiple sequential underscores from the slug
$id = preg_replace( '~_+~', '_', $id );
// Sometimes we're unable to produce slug because the
// source language isn't latin; in those cases
// we just produce stable hash from the title
if (empty($id) || $id === '_') {
$id = substr( md5( $title ), 0, 8 );
}
$id = $id_prefix . $id . $id_suffix;
$base = $id;
$suffix = 0;
while ( ! $this->is_unique_container_id( $id ) ) {
$suffix++;
$id = $base . strval( $suffix );
}
return $id;
}
/**
* Add container identificator id to the list of unique container ids
*
* @param string $id
*/
protected function register_unique_container_id( $id ) {
if ( $this->is_unique_container_id( $id ) ) {
$this->registered_container_ids[] = $id;
}
}
/**
* Remove container identificator id from the list of unique container ids
*
* @param string $id
*/
protected function unregister_unique_container_id( $id ) {
if ( ! $this->is_unique_container_id( $id ) ) {
unset( $this->registered_container_ids[ array_search( $id, $this->registered_container_ids ) ] );
}
}
}

View file

@ -0,0 +1,231 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore;
/**
* Term meta container class.
*/
class Term_Meta_Container extends Container {
protected $term_id;
public $settings = array();
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'term_meta' ), $this->has_default_datastore() );
}
}
/**
* Bind attach() and save() to the appropriate WordPress actions.
*/
public function init() {
add_action( 'admin_init', array( $this, '_attach' ) );
add_action( 'init', array( $this, 'hook_to_taxonomies' ), 999999 );
}
/**
* Hook to relevant taxonomies
*/
public function hook_to_taxonomies() {
$taxonomies = $this->get_taxonomy_visibility();
foreach ( $taxonomies as $taxonomy ) {
add_action( 'edited_' . $taxonomy, array( $this, '_save' ), 10, 2 );
add_action( 'created_' . $taxonomy, array( $this, '_save' ), 10, 2 );
}
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
$params = func_get_args();
return $this->is_valid_attach_for_object( $params[0] );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*
* @param int $term_id ID of the term against which save() is ran
*/
public function save( $term_id = null ) {
$this->set_term_id( $term_id );
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
do_action( 'carbon_fields_term_meta_container_saved', $term_id, $this );
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
$input = stripslashes_deep( $_GET );
$request_term_id = isset( $input['tag_ID'] ) ? intval( $input['tag_ID'] ) : 0;
$request_taxonomy = isset( $input['taxonomy'] ) ? $input['taxonomy'] : '';
$term = get_term( $request_term_id );
$term = ( $term && ! is_wp_error( $term ) ) ? $term : null;
$environment = array(
'term_id' => $term ? intval( $term->term_id ) : 0,
'term' => $term,
'taxonomy' => $term ? $term->taxonomy : $request_taxonomy,
);
return $environment;
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
global $pagenow;
if ( $pagenow !== 'edit-tags.php' && $pagenow !== 'term.php' ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
$term = get_term( intval( $object_id ) );
$environment = array(
'term_id' => intval( $term->term_id ),
'term' => $term,
'taxonomy' => $term->taxonomy,
);
return $environment;
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
$term = get_term( $object_id );
$term = ( $term && ! is_wp_error( $term ) ) ? $term : null;
if ( ! $term ) {
return false;
}
return $this->all_conditions_pass( intval( $term->term_id ) );
}
/**
* Add term meta for each of the container taxonomies
*/
public function attach() {
$taxonomies = $this->get_taxonomy_visibility();
foreach ( $taxonomies as $taxonomy ) {
add_action( $taxonomy . '_edit_form_fields', array( $this, 'render' ), 10, 2 );
add_action( $taxonomy . '_add_form_fields', array( $this, 'render' ), 10, 2 );
}
}
/**
* Output the container markup
*/
public function render( $term = null ) {
if ( is_object( $term ) ) {
$this->set_term_id( $term->term_id );
}
include \Carbon_Fields\DIR . '/templates/Container/term_meta.php';
}
/**
* Set the term ID the container will operate with.
*
* @param int $term_id
*/
protected function set_term_id( $term_id ) {
$this->term_id = $term_id;
$this->get_datastore()->set_object_id( $term_id );
foreach ( $this->fields as $field ) {
$datastore = $field->get_datastore();
if ( $datastore->get_object_id() === 0 ) {
$datastore->set_object_id( $term_id );
}
}
}
/**
* Get array of taxonomies this container can appear on conditionally
*
* @return array<string>
*/
public function get_taxonomy_visibility() {
$all_taxonomies = get_taxonomies();
$evaluated_collection = $this->condition_collection->evaluate( array( 'term_taxonomy' ), true, array(), true );
$shown_on = array();
foreach ( $all_taxonomies as $taxonomy ) {
$environment = array(
'taxonomy' => $taxonomy,
);
if ( $evaluated_collection->is_fulfilled( $environment ) ) {
$shown_on[] = $taxonomy;
}
}
return $shown_on;
}
/**
* Show the container only on terms from the specified taxonomies.
*
* @deprecated
* @param string|array $taxonomies
* @return object $this
*/
public function show_on_taxonomy( $taxonomies ) {
$taxonomies = is_array( $taxonomies ) ? $taxonomies : array( $taxonomies );
$this->where( 'term_taxonomy', 'IN', $taxonomies );
return $this;
}
/**
* Show the container only on particular term level.
*
* @deprecated
* @param int $term_level
* @return object $this
*/
public function show_on_level( $term_level ) {
$this->where( 'term_level', '=', intval( $term_level ) );
return $this;
}
}

View file

@ -0,0 +1,329 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Theme options container class.
*/
class Theme_Options_Container extends Container {
/**
* Array of registered page slugs to verify uniqueness with
*
* @var array
*/
protected static $registered_pages = array();
/**
* Array of container settings
*
* @var array
*/
public $settings = array(
'parent' => '',
'file' => '',
'icon' => '',
'menu_position' => null,
'menu_title' => null,
);
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'theme_options' ), $this->has_default_datastore() );
}
if ( apply_filters( 'carbon_fields_' . $type . '_container_admin_only_access', true, $title, $this ) ) {
$this->where( 'current_user_capability', '=', 'manage_options' );
}
}
/**
* Sanitize a title to a filename
*
* @param string $title
* @param string $extension
* @return string
*/
protected function title_to_filename( $title, $extension ) {
$title = sanitize_file_name( $title );
$title = strtolower( $title );
$title = remove_accents( $title );
$title = preg_replace( array(
'~\s+~',
'~[^\w\d-]+~u',
'~-+~',
), array(
'-',
'-',
'-',
), $title );
return $title . $extension;
}
/**
* Attach container as a theme options page/subpage.
*/
public function init() {
$registered = $this->register_page();
if ( $registered ) {
add_action( 'admin_menu', array( $this, '_attach' ) );
}
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
return $this->is_valid_attach_for_object( null );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*
* @param mixed $user_data
*/
public function save( $user_data = null ) {
try {
parent::save( $user_data );
} catch ( Incorrect_Syntax_Exception $e ) {
$this->errors[] = $e->getMessage();
}
do_action( 'carbon_fields_' . $this->type . '_container_saved', $user_data, $this );
if ( ! headers_sent() ) {
wp_redirect( add_query_arg( array( 'settings-updated' => 'true' ) ) );
}
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
return array();
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
return array();
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
return $this->all_conditions_pass( intval( $object_id ) );
}
/**
* Add theme options container pages.
* Hook the container saving action.
*/
public function attach() {
// use the "read" capability because conditions will handle actual access and save capability checking
// before the attach() method is called
// Add menu page
if ( ! $this->settings['parent'] ) {
add_menu_page(
$this->title,
$this->settings['menu_title'] ? $this->settings['menu_title'] : $this->title,
'read',
$this->get_page_file(),
array( $this, 'render' ),
$this->settings['icon'],
$this->settings['menu_position']
);
}
add_submenu_page(
$this->settings['parent'],
$this->title,
$this->settings['menu_title'] ? $this->settings['menu_title'] : $this->title,
'read',
$this->get_page_file(),
array( $this, 'render' )
);
$page_hook = get_plugin_page_hookname( $this->get_page_file(), '' );
add_action( 'load-' . $page_hook, array( $this, '_save' ) );
}
/**
* Whether this container is currently viewed.
*
* @return boolean
*/
public function should_activate() {
$input = stripslashes_deep( $_GET );
$request_page = isset( $input['page'] ) ? $input['page'] : '';
if ( ! empty( $request_page ) && $request_page === $this->get_page_file() ) {
return true;
}
return false;
}
/**
* Output the container markup
*/
public function render() {
$input = stripslashes_deep( $_GET );
$request_settings_updated = isset( $input['settings-updated'] ) ? $input['settings-updated'] : '';
if ( $request_settings_updated === 'true' ) {
$this->notifications[] = __( 'Settings saved.', 'carbon-fields' );
}
include \Carbon_Fields\DIR . '/templates/Container/' . $this->type . '.php';
}
/**
* Register the page while making sure it is unique.
*
* @return boolean
*/
protected function register_page() {
$file = $this->get_page_file();
$parent = $this->settings['parent'];
if ( ! $parent ) {
// Register top level page
if ( isset( static::$registered_pages[ $file ] ) ) {
Incorrect_Syntax_Exception::raise( 'Page "' . $file . '" already registered' );
return false;
}
static::$registered_pages[ $file ] = array();
return true;
}
// Register sub-page
if ( ! isset( static::$registered_pages[ $parent ] ) ) {
static::$registered_pages[ $parent ] = array();
}
if ( in_array( $file, static::$registered_pages[ $parent ] ) ) {
Incorrect_Syntax_Exception::raise( 'Page "' . $file . '" with parent "' . $parent . '" is already registered. Please set a name for the container.' );
return false;
}
static::$registered_pages[ $parent ][] = $file;
return true;
}
/**
* Change the parent theme options page of this container
*
* @param string|Theme_Options_Container $parent
* @return Container $this
*/
public function set_page_parent( $parent ) {
if ( is_a( $parent, static::class ) ) {
$this->settings['parent'] = $parent->get_page_file();
return $this;
}
$this->settings['parent'] = $parent;
return $this;
}
/**
* Get the theme options file name of this container.
*
* @return string
*/
public function get_page_file() {
if ( ! empty( $this->settings['file'] ) ) {
return $this->settings['file'];
}
return $this->title_to_filename( 'crb_' . $this->get_id(), '.php' );
}
/**
* Set the theme options file name of this container.
*
* @param string $file
* @return Container $this
*/
public function set_page_file( $file ) {
$this->settings['file'] = $file;
return $this;
}
/**
* Set the title of this container in the administration menu.
*
* @param string $title
* @return Container $this
*/
public function set_page_menu_title( $title ) {
$this->settings['menu_title'] = $title;
return $this;
}
/**
* Alias of the set_page_menu_position() method for backwards compatibility
*
* @param integer $position
* @return Container $this
*/
public function set_page_position( $position ) {
return $this->set_page_menu_position( $position );
}
/**
* Set the page position of this container in the administration menu.
*
* @param integer $position
* @return Container $this
*/
public function set_page_menu_position( $position ) {
$this->settings['menu_position'] = $position;
return $this;
}
/**
* Set the icon of this theme options page.
* Applicable only for parent theme option pages.
*
* @param string $icon
* @return Container $this
*/
public function set_icon( $icon ) {
$this->settings['icon'] = $icon;
return $this;
}
}

View file

@ -0,0 +1,212 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class User_Meta_Container extends Container {
protected $user_id;
public $settings = array();
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
if ( ! $this->get_datastore() ) {
$this->set_datastore( Datastore::make( 'user_meta' ), $this->has_default_datastore() );
}
if ( apply_filters( 'carbon_fields_' . $type . '_container_admin_only_access', true, $title, $this ) ) {
$this->where( 'current_user_capability', '=', 'manage_options' );
}
}
/**
* Bind attach() and save() to the appropriate WordPress actions.
*/
public function init() {
add_action( 'admin_init', array( $this, '_attach' ) );
add_action( 'profile_update', array( $this, '_save' ), 10, 1 );
add_action( 'user_register', array( $this, '_save' ), 10, 1 );
}
/**
* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->is_profile_page() ) {
return false;
}
if ( ! $this->verified_nonce_in_request() ) {
return false;
}
$params = func_get_args();
return $this->is_valid_attach_for_object( $params[0] );
}
/**
* Perform save operation after successful is_valid_save() check.
* The call is propagated to all fields in the container.
*
* @param int $user_id ID of the user against which save() is ran
*/
public function save( $user_id = null ) {
// Unhook action to garantee single save
remove_action( 'profile_update', array( $this, '_save' ) );
$this->set_user_id( $user_id );
foreach ( $this->fields as $field ) {
$field->set_value_from_input( Helper::input() );
$field->save();
}
do_action( 'carbon_fields_user_meta_container_saved', $user_id, $this );
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
global $pagenow;
$input = stripslashes_deep( $_GET );
$user_id = 0;
if ( $pagenow === 'profile.php' ) {
$user_id = get_current_user_id();
}
if ( isset( $input['user_id'] ) ) {
$user_id = intval( $input['user_id'] );
}
$user = get_userdata( $user_id );
$environment = array(
'user_id' => $user ? intval( $user->ID ) : 0,
'user' => $user ? $user : null,
'roles' => $user ? $user->roles : array(),
);
return $environment;
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
if ( ! $this->is_profile_page() ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
$user = get_userdata( $object_id );
$environment = array(
'user_id' => intval( $user->ID ),
'user' => $user,
'roles' => $user->roles,
);
return $environment;
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
$user = get_userdata( $object_id );
if ( ! $user ) {
return false;
}
return $this->all_conditions_pass( intval( $user->ID ) );
}
/**
* Add the container to the user
*/
public function attach() {
add_action( 'show_user_profile', array( $this, 'render' ), 10, 1 );
add_action( 'edit_user_profile', array( $this, 'render' ), 10, 1 );
add_action( 'user_new_form', array( $this, 'render' ), 10, 1 );
}
/**
* Whether we're on the user profile page
*/
protected function is_profile_page() {
global $pagenow;
return $pagenow === 'profile.php' || $pagenow === 'user-new.php' || $pagenow === 'user-edit.php';
}
/**
* Output the container markup
*/
public function render( $user_profile = null ) {
$profile_role = '';
if ( is_object( $user_profile ) ) {
$this->set_user_id( $user_profile->ID );
// array_shift removed the returned role from the $user_profile->roles
// $roles_to_shift prevents changing of the $user_profile->roles variable
$roles_to_shift = $user_profile->roles;
$profile_role = array_shift( $roles_to_shift );
}
include \Carbon_Fields\DIR . '/templates/Container/user_meta.php';
}
/**
* Set the user ID the container will operate with.
*
* @param int $user_id
*/
protected function set_user_id( $user_id ) {
$this->user_id = $user_id;
$this->get_datastore()->set_object_id( $user_id );
foreach ( $this->fields as $field ) {
$datastore = $field->get_datastore();
if ( $datastore->get_object_id() === 0 ) {
$datastore->set_object_id( $user_id );
}
}
}
/**
* Show the container only on users who have the $role role.
*
* @deprecated
* @param string|array $role
* @return object $this
*/
public function show_on_user_role( $role ) {
$roles = is_array( $role ) ? $role : array( $role );
$this->where( 'user_role', 'IN', $roles );
return $this;
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Carbon_Fields\Container;
use Carbon_Fields\Helper\Helper;
/**
* Widget container class
*/
class Widget_Container extends Container {
/**
* {@inheritDoc}
*/
public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
parent::__construct( $id, $title, $type, $condition_collection, $condition_translator );
$this->title = '';
}
/**
* Perform instance initialization
*/
public function init() {
$this->_attach();
return $this;
}
/**
* Get environment array for page request (in admin)
*
* @return array
*/
protected function get_environment_for_request() {
return array();
}
/**
* Perform checks whether the container should be attached during the current request
*
* @return bool True if the container is allowed to be attached
*/
public function is_valid_attach_for_request() {
if ( ( did_action( 'rest_api_init' ) || doing_action( 'rest_api_init' ) ) && ! defined( 'WP_ADMIN' ) ) {
// Widgets should be attached in REST API only outside the Administration area
// Several 3rd party plugins register REST API in the Administration area, which causes issues
// with the widgets initialization
return true;
}
$screen = get_current_screen();
$input = Helper::input();
$request_action = isset( $input['action'] ) ? $input['action'] : '';
$is_widget_save = ( $request_action === 'save-widget' );
if ( ( ! $screen || ! in_array( $screen->id, array( 'widgets', 'customize' ) ) ) && ! $is_widget_save ) {
return false;
}
return $this->static_conditions_pass();
}
/**
* Get environment array for object id
*
* @return array
*/
protected function get_environment_for_object( $object_id ) {
return array();
}
/**
* Check container attachment rules against object id
*
* @param int $object_id
* @return bool
*/
public function is_valid_attach_for_object( $object_id = null ) {
return $this->all_conditions_pass( intval( $object_id ) );
}
/* Checks whether the current save request is valid
*
* @return bool
*/
public function is_valid_save() {
if ( ! $this->is_valid_attach_for_request() ) {
return false;
}
$params = func_get_args();
return $this->is_valid_attach_for_object( $params[0] );
}
/**
* Output the container markup
*/
public function render() {
include \Carbon_Fields\DIR . '/templates/Container/widget.php';
}
/**
* Returns an array that holds the container data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
return parent::to_json( false );
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Comment meta datastore class.
*/
class Comment_Meta_Datastore extends Meta_Datastore {
/**
* Retrieve the type of meta data.
*
* @return string
*/
public function get_meta_type() {
return 'comment';
}
/**
* Retrieve the meta table name to query.
*
* @return string
*/
public function get_table_name() {
global $wpdb;
return $wpdb->commentmeta;
}
/**
* Retrieve the meta table field name to query by.
*
* @return string
*/
public function get_table_field_name() {
return 'comment_id';
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Base datastore.
* Defines the key datastore methods and their default implementations.
*/
abstract class Datastore implements Datastore_Interface {
/**
* The related object id
*
* @var integer
*/
protected $object_id = 0;
/**
* Initialize the datastore.
*/
public function __construct() {
$this->init();
}
/**
* Initialization tasks for concrete datastores.
*
* @abstract
*/
abstract public function init();
/**
* Get the related object id
*
* @return integer
*/
public function get_object_id() {
return $this->object_id;
}
/**
* Set the related object id
*
* @param integer $object_id
*/
public function set_object_id( $object_id ) {
$this->object_id = $object_id;
}
/**
* Create a new datastore of type $raw_type.
*
* @param string $raw_type
* @return Datastore_Interface
*/
public static function factory( $raw_type ) {
$type = Helper::normalize_type( $raw_type );
$class = Helper::type_to_class( $type, __NAMESPACE__, '_Datastore' );
if ( ! class_exists( $class ) ) {
Incorrect_Syntax_Exception::raise( 'Unknown datastore type "' . $raw_type . '".' );
return null;
}
$datastore = new $class();
return $datastore;
}
/**
* An alias of factory().
*
* @see Datastore::factory()
* @return Datastore_Interface
*/
public static function make() {
return call_user_func_array( array( static::class, 'factory' ), func_get_args() );
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Carbon_Fields\Datastore;
interface Datastore_Holder_Interface {
/**
* Return whether the datastore instance is the default one or has been overriden
*
* @return boolean
*/
public function has_default_datastore();
/**
* Set datastore instance
*
* @param Datastore_Interface $datastore
* @param boolean $set_as_default
* @return object $this
*/
public function set_datastore( Datastore_Interface $datastore, $set_as_default );
/**
* Get the DataStore instance
*
* @return Datastore_Interface $datastore
*/
public function get_datastore();
}

View file

@ -0,0 +1,47 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Interface for data storage management.
*/
interface Datastore_Interface {
/**
* Get the related object id
*
* @return integer
*/
public function get_object_id();
/**
* Set the related object id
*
* @param integer $object_id
*/
public function set_object_id( $object_id );
/**
* Load the field value(s)
*
* @param Field $field The field to load value(s) in.
* @return array
*/
public function load( Field $field );
/**
* Save the field value(s)
*
* @param Field $field The field to save.
*/
public function save( Field $field );
/**
* Delete the field value(s)
*
* @param Field $field The field to delete.
*/
public function delete( Field $field );
}

View file

@ -0,0 +1,30 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Empty datastore class.
*/
class Empty_Datastore extends Datastore {
/**
* {@inheritDoc}
*/
public function init() {}
/**
* {@inheritDoc}
*/
public function load( Field $field ) {}
/**
* {@inheritDoc}
*/
public function save( Field $field ) {}
/**
* {@inheritDoc}
*/
public function delete( Field $field ) {}
}

View file

@ -0,0 +1,200 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Field\Field;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Key Value Datastore
* Provides basic functionality to save in a key-value storage
*/
abstract class Key_Value_Datastore extends Datastore {
/**
* Key Toolset for key generation and comparison utilities
*
* @var \Carbon_Fields\Toolset\Key_Toolset
*/
protected $key_toolset;
/**
* Initialize the datastore.
*/
public function __construct() {
$this->key_toolset = \Carbon_Fields\Carbon_Fields::resolve( 'key_toolset' );
parent::__construct();
}
/**
* Get array of ancestors (ordered top-bottom) with the field name appended to the end
*
* @param Field $field
* @return array<string>
*/
protected function get_full_hierarchy_for_field( Field $field ) {
$full_hierarchy = array_merge( $field->get_hierarchy(), array( $field->get_base_name() ) );
return $full_hierarchy;
}
/**
* Get array of ancestor group indexes (ordered top-bottom)
* Indexes show which entry/group this field belongs to in a Complex_Field
*
* @param Field $field
* @return array<int>
*/
protected function get_full_hierarchy_index_for_field( Field $field ) {
$hierarchy_index = $field->get_hierarchy_index();
$full_hierarchy_index = ! empty( $hierarchy_index ) ? $hierarchy_index : array();
return $full_hierarchy_index;
}
/**
* Convert a cascading storage array to a value tree array
*
* @see Internal Glossary in DEVELOPMENT.MD
* @param \stdClass[] $storage_array
* @return array
*/
protected function cascading_storage_array_to_value_tree_array( $storage_array ) {
$tree = array();
$found_keepalive = false;
foreach ( $storage_array as $row ) {
$parsed_storage_key = $this->key_toolset->parse_storage_key( $row->key );
if ( $parsed_storage_key['property'] === $this->key_toolset->get_keepalive_property() ) {
$found_keepalive = true;
continue;
}
$level = &$tree;
foreach ( $parsed_storage_key['full_hierarchy'] as $i => $field_name ) {
$index = isset( $parsed_storage_key['hierarchy_index'][ $i ] ) ? $parsed_storage_key['hierarchy_index'][ $i ] : 0;
if ( ! isset( $level[ $field_name ] ) ) {
$level[ $field_name ] = array();
}
$level = &$level[ $field_name ];
if ( $i < count( $parsed_storage_key['full_hierarchy'] ) - 1 ) {
if ( ! isset( $level[ $index ] ) ) {
$level[ $index ] = array();
}
$level = &$level[ $index ];
} else {
if ( ! isset( $level[ $parsed_storage_key['value_index'] ] ) ) {
$level[ $parsed_storage_key['value_index'] ] = array();
}
$level = &$level[ $parsed_storage_key['value_index'] ];
$level[ $parsed_storage_key['property'] ] = $row->value;
}
}
$level = &$tree;
}
Helper::ksort_recursive( $tree );
if ( empty( $tree ) && ! $found_keepalive ) {
return null;
}
return $tree;
}
/**
* Convert a value set tree to a value tree for the specified field
* (get a single value tree from the collection)
*
* @see Internal Glossary in DEVELOPMENT.MD
* @param array $value_tree_array
* @param Field $field
* @return array
*/
protected function value_tree_array_to_value_tree( $value_tree_array, Field $field ) {
$value_tree = $value_tree_array;
$hierarchy = $field->get_hierarchy();
$hierarchy_index = $field->get_hierarchy_index();
foreach ( $hierarchy as $index => $parent_field ) {
$hierarchy_index_value = isset( $hierarchy_index[ $index ] ) ? $hierarchy_index[ $index ] : 0;
if ( isset( $value_tree[ $parent_field ][ $hierarchy_index_value ] ) ) {
$value_tree = $value_tree[ $parent_field ][ $hierarchy_index_value ];
}
}
if ( isset( $value_tree[ $field->get_base_name() ] ) ) {
$value_tree = $value_tree[ $field->get_base_name() ];
}
return $value_tree;
}
/**
* Get a raw database query results array for a field
*
* @param Field $field The field to retrieve value for.
* @param array $storage_key_patterns
* @return \stdClass[] Array of {key, value} objects
*/
abstract protected function get_storage_array( Field $field, $storage_key_patterns );
/**
* Get the field value(s)
*
* @param Field $field The field to get value(s) for
* @return null|array<array>
*/
public function load( Field $field ) {
$storage_key_patterns = $this->key_toolset->get_storage_key_getter_patterns( $field->is_simple_root_field(), $this->get_full_hierarchy_for_field( $field ) );
$cascading_storage_array = $this->get_storage_array( $field, $storage_key_patterns );
$value_tree_array = $this->cascading_storage_array_to_value_tree_array( $cascading_storage_array );
if ( $value_tree_array === null ) {
return $value_tree_array;
}
$value_tree = $this->value_tree_array_to_value_tree( $value_tree_array, $field );
return $value_tree;
}
/**
* Save a single key-value pair to the database
*
* @param string $key
* @param string $value
*/
abstract protected function save_key_value_pair( $key, $value );
/**
* Save the field value(s)
*
* @param Field $field The field to save.
*/
public function save( Field $field ) {
$value_set = $field->get_full_value();
if ( empty( $value_set ) && $field->get_value_set()->keepalive() ) {
$storage_key = $this->key_toolset->get_storage_key(
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field ),
0,
$this->key_toolset->get_keepalive_property()
);
$this->save_key_value_pair( $storage_key, '' );
}
foreach ( $value_set as $value_group_index => $values ) {
foreach ( $values as $property => $value ) {
$storage_key = $this->key_toolset->get_storage_key(
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field ),
$value_group_index,
$property
);
$this->save_key_value_pair( $storage_key, $value );
}
}
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Abstract meta datastore class.
*/
abstract class Meta_Datastore extends Key_Value_Datastore {
/**
* Initialization tasks.
*/
public function init() {}
/**
* Get a raw database query results array for a field
*
* @param Field $field The field to retrieve value for.
* @param array $storage_key_patterns
* @return \stdClass[] Array of {key, value} objects
*/
protected function get_storage_array( Field $field, $storage_key_patterns ) {
global $wpdb;
$storage_key_comparisons = $this->key_toolset->storage_key_patterns_to_sql( '`meta_key`', $storage_key_patterns );
$storage_array = $wpdb->get_results( '
SELECT `meta_key` AS `key`, `meta_value` AS `value`
FROM ' . $this->get_table_name() . '
WHERE `' . $this->get_table_field_name() . '` = ' . intval( $this->get_object_id() ) . '
AND ' . $storage_key_comparisons . '
ORDER BY `meta_key` ASC
' );
$storage_array = apply_filters( 'carbon_fields_datastore_storage_array', $storage_array, $this, $storage_key_patterns );
return $storage_array;
}
/**
* Save a single key-value pair to the database
*
* @param string $key
* @param string $value
*/
protected function save_key_value_pair( $key, $value ) {
$value = wp_slash( $value );
if ( ! update_metadata( $this->get_meta_type(), $this->get_object_id(), $key, $value ) ) {
add_metadata( $this->get_meta_type(), $this->get_object_id(), $key, $value, true );
}
}
/**
* Delete the field value(s)
*
* @param Field $field The field to delete.
*/
public function delete( Field $field ) {
global $wpdb;
$storage_key_patterns = $this->key_toolset->get_storage_key_deleter_patterns(
( $field instanceof \Carbon_Fields\Field\Complex_Field ),
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field )
);
$storage_key_comparisons = $this->key_toolset->storage_key_patterns_to_sql( '`meta_key`', $storage_key_patterns );
$meta_keys = $wpdb->get_col( '
SELECT `meta_key`
FROM `' . $this->get_table_name() . '`
WHERE `' . $this->get_table_field_name() . '` = ' . intval( $this->get_object_id() ) . '
AND ' . $storage_key_comparisons . '
' );
foreach ( $meta_keys as $meta_key ) {
delete_metadata( $this->get_meta_type(), $this->get_object_id(), $meta_key );
}
}
/**
* Get the type of meta data.
*/
abstract public function get_meta_type();
/**
* Get the meta table name to query.
*/
abstract public function get_table_name();
/**
* Get the meta table field name to query by.
*/
abstract public function get_table_field_name();
}

View file

@ -0,0 +1,78 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
class Nav_Menu_Item_Datastore extends Post_Meta_Datastore {
public function get_garbage_prefix() {
if ( ! $this->get_object_id() ) {
return '';
}
return '_menu-item-' . $this->get_object_id() . '_';
}
public function get_clean_field_name( $field ) {
$name = ( is_object( $field ) && is_subclass_of( $field, 'Carbon_Fields\\Field\\Field' ) ) ? $field->get_name() : $field;
$garbage_prefix = $this->get_garbage_prefix();
$garbage_prefix_length = strlen( $garbage_prefix );
if ( substr( $name, 0, $garbage_prefix_length ) === $garbage_prefix ) {
$name = substr( $name, $garbage_prefix_length );
}
return $name;
}
public function get_dirty_field_name( $field ) {
$name = ( is_object( $field ) && is_subclass_of( $field, 'Carbon_Fields\\Field\\Field' ) ) ? $field->get_name() : $field;
$garbage_prefix = $this->get_garbage_prefix();
$garbage_prefix_length = strlen( $garbage_prefix );
if ( substr( $name, 0, $garbage_prefix_length ) !== $garbage_prefix ) {
$name = $garbage_prefix . $name;
}
return $name;
}
/**
* Load the field value(s)
*
* @param Field $field The field to load value(s) in.
*/
public function load( Field $field ) {
if ( ! $this->get_object_id() ) {
return;
}
$old_name = $field->get_name();
$field->set_name( $this->get_clean_field_name( $field ) );
$result = parent::load( $field );
$field->set_name( $old_name );
return $result;
}
/**
* Save the field value(s)
*
* @param Field $field The field to save.
*/
public function save( Field $field ) {
if ( ! $this->get_object_id() ) {
return;
}
$clone = clone $field;
$clone->set_name( $this->get_clean_field_name( $field ) );
parent::save( $clone );
}
/**
* Delete the field value(s)
*
* @param Field $field The field to delete.
*/
public function delete( Field $field ) {
$clone = clone $field;
$clone->set_name( $this->get_clean_field_name( $field ) );
parent::delete( $clone );
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Theme options datastore class.
*/
class Network_Datastore extends Meta_Datastore {
/**
* {@inheritDoc}
*/
public function get_meta_type() {
return 'site';
}
/**
* {@inheritDoc}
*/
public function get_table_name() {
global $wpdb;
return $wpdb->sitemeta;
}
/**
* {@inheritDoc}
*/
public function get_table_field_name() {
return 'site_id';
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Post meta (custom fields) datastore class.
*/
class Post_Meta_Datastore extends Meta_Datastore {
/**
* Retrieve the type of meta data.
*
* @return string
*/
public function get_meta_type() {
return 'post';
}
/**
* Retrieve the meta table name to query.
*
* @return string
*/
public function get_table_name() {
global $wpdb;
return $wpdb->postmeta;
}
/**
* Retrieve the meta table field name to query by.
*
* @return string
*/
public function get_table_field_name() {
return 'post_id';
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Term meta datastore class.
*/
class Term_Meta_Datastore extends Meta_Datastore {
/**
* Initialization tasks.
*/
public function init() {
global $wpdb;
// Setup termmeta table and hooks only once
if ( ! empty( $wpdb->termmeta ) ) {
return;
}
$wpdb->termmeta = $wpdb->prefix . 'termmeta';
static::create_table();
// Delete all meta associated with the deleted term
add_action( 'delete_term', array( __CLASS__, 'on_delete_term' ), 10, 3 );
}
/**
* Create term meta database table (for WP < 4.4)
*/
public static function create_table() {
global $wpdb;
$tables = $wpdb->get_results( 'SHOW TABLES LIKE "' . $wpdb->prefix . 'termmeta"' );
if ( ! empty( $tables ) ) {
return;
}
$charset_collate = '';
if ( ! empty( $wpdb->charset ) ) {
$charset_collate = 'DEFAULT CHARACTER SET ' . $wpdb->charset;
}
if ( ! empty( $wpdb->collate ) ) {
$charset_collate .= ' COLLATE ' . $wpdb->collate;
}
$wpdb->query( 'CREATE TABLE ' . $wpdb->prefix . 'termmeta (
meta_id bigint(20) unsigned NOT NULL auto_increment,
term_id bigint(20) unsigned NOT NULL default "0",
meta_key varchar(255) default NULL,
meta_value longtext,
PRIMARY KEY (meta_id),
KEY term_id (term_id),
KEY meta_key (meta_key)
) ' . $charset_collate . ';' );
}
/**
* Delete term meta on term deletion.
* Useful for WP < 4.4.
*
* @param int $term_id Term ID.
* @return bool Result of the deletion operation.
*/
public static function on_delete_term( $term_id ) {
global $wpdb;
return $wpdb->query( '
DELETE FROM ' . $wpdb->termmeta . '
WHERE `term_id` = "' . intval( $term_id ) . '"
' );
}
/**
* Retrieve the type of meta data.
*
* @return string
*/
public function get_meta_type() {
return 'term';
}
/**
* Retrieve the meta table name to query.
*
* @return string
*/
public function get_table_name() {
global $wpdb;
return $wpdb->termmeta;
}
/**
* Retrieve the meta table field name to query by.
*
* @return string
*/
public function get_table_field_name() {
return 'term_id';
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* Theme options datastore class.
*/
class Theme_Options_Datastore extends Key_Value_Datastore {
/**
* Initialization tasks.
*/
public function init() {}
/**
* Get a raw database query results array for a field
*
* @param Field $field The field to retrieve value for.
* @param array $storage_key_patterns
* @return array<stdClass> Array of {key, value} objects
*/
protected function get_storage_array( Field $field, $storage_key_patterns ) {
global $wpdb;
$storage_key_comparisons = $this->key_toolset->storage_key_patterns_to_sql( '`option_name`', $storage_key_patterns );
$storage_array = $wpdb->get_results( '
SELECT `option_name` AS `key`, `option_value` AS `value`
FROM ' . $wpdb->options . '
WHERE ' . $storage_key_comparisons . '
ORDER BY `option_name` ASC
' );
$storage_array = apply_filters( 'carbon_fields_datastore_storage_array', $storage_array, $this, $storage_key_patterns );
return $storage_array;
}
/**
* Save a single key-value pair to the database
*
* @param string $key
* @param string $value
*/
protected function save_key_value_pair( $key, $value ) {
$this->save_key_value_pair_with_autoload( $key, $value, false );
}
/**
* Save a single key-value pair to the database with autoload
*
* @param string $key
* @param string $value
* @param bool $autoload
*/
protected function save_key_value_pair_with_autoload( $key, $value, $autoload = true ) {
$autoload = $autoload ? 'yes': 'no';
$notoptions = wp_cache_get( 'notoptions', 'options' );
$notoptions[ $key ] = '';
wp_cache_set( 'notoptions', $notoptions, 'options' );
if ( ! add_option( $key, $value, null, $autoload ) ) {
update_option( $key, $value, $autoload );
}
}
/**
* Save the field value(s)
*
* @param Field $field The field to save.
*/
public function save( Field $field ) {
$value_set = $field->get_full_value();
if ( empty( $value_set ) && $field->get_value_set()->keepalive() ) {
$storage_key = $this->key_toolset->get_storage_key(
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field ),
0,
$this->key_toolset->get_keepalive_property()
);
$this->save_key_value_pair_with_autoload( $storage_key, '', $field->get_autoload() );
}
foreach ( $value_set as $value_group_index => $values ) {
foreach ( $values as $property => $value ) {
$storage_key = $this->key_toolset->get_storage_key(
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field ),
$value_group_index,
$property
);
$this->save_key_value_pair_with_autoload( $storage_key, $value, $field->get_autoload() );
}
}
}
/**
* Delete the field value(s)
*
* @param Field $field The field to delete.
*/
public function delete( Field $field ) {
global $wpdb;
$storage_key_patterns = $this->key_toolset->get_storage_key_deleter_patterns(
( $field instanceof \Carbon_Fields\Field\Complex_Field ),
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field )
);
$storage_key_comparisons = $this->key_toolset->storage_key_patterns_to_sql( '`option_name`', $storage_key_patterns );
$option_names = $wpdb->get_col( '
SELECT `option_name`
FROM `' . $wpdb->options . '`
WHERE ' . $storage_key_comparisons . '
' );
foreach ( $option_names as $option_name ) {
delete_option( $option_name );
}
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
/**
* User meta datastore class.
*/
class User_Meta_Datastore extends Meta_Datastore {
/**
* Retrieve the type of meta data.
*
* @return string
*/
public function get_meta_type() {
return 'user';
}
/**
* Retrieve the meta table name to query.
*
* @return string
*/
public function get_table_name() {
global $wpdb;
return $wpdb->usermeta;
}
/**
* Retrieve the meta table field name to query by.
*
* @return string
*/
public function get_table_field_name() {
return 'user_id';
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Carbon_Fields\Datastore;
use Carbon_Fields\Field\Field;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Widget datastore
*/
class Widget_Datastore extends Key_Value_Datastore {
/**
* Flat key-value array acting as storage
*
* @var array
*/
protected $storage = array();
/**
* Initialization tasks.
*/
public function init() {}
/**
* Override storage array
*
* @param array $storage
*/
public function import_storage( $storage ) {
$this->storage = $storage;
}
/**
* Get storage array
*
* @return array
*/
public function export_storage() {
return $this->storage;
}
/**
* Get a raw database query results array for a field
*
* @param Field $field The field to retrieve value for.
* @param array $storage_key_patterns
* @return array<stdClass> Array of {key, value} objects
*/
protected function get_storage_array( Field $field, $storage_key_patterns ) {
$storage_array = array();
foreach ( $this->storage as $storage_key => $value ) {
if ( $this->key_toolset->storage_key_matches_any_pattern( $storage_key, $storage_key_patterns ) ) {
$storage_array[] = (object) array(
'key' => $storage_key,
'value' => $value,
);
}
}
return $storage_array;
}
/**
* Save a single key-value pair to the database
*
* @param string $key
* @param string $value
*/
protected function save_key_value_pair( $key, $value ) {
$this->storage[ $key ] = $value;
}
/**
* Delete the field value(s)
*
* @param Field $field The field to delete.
*/
public function delete( Field $field ) {
$storage_key_patterns = $this->key_toolset->get_storage_key_deleter_patterns(
( $field instanceof \Carbon_Fields\Field\Complex_Field ),
$field->is_simple_root_field(),
$this->get_full_hierarchy_for_field( $field ),
$this->get_full_hierarchy_index_for_field( $field )
);
foreach ( $this->storage as $storage_key => $value ) {
if ( $this->key_toolset->storage_key_matches_any_pattern( $storage_key, $storage_key_patterns ) ) {
unset( $this->storage[ $storage_key ] );
}
}
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Carbon_Fields\Event;
class Emitter {
/**
* @var Listener[]
*/
protected $listeners = array();
/**
* Broadcast an event
*/
public function emit() {
$args = func_get_args();
$event = $args[0];
$args = array_slice( $args, 1 );
$listeners = $this->get_listeners( $event );
foreach ( $listeners as $listener ) {
if ( $listener->is_valid() ) {
call_user_func_array( array( $listener, 'notify' ), $args );
}
}
$this->remove_invalid_listeners( $event );
}
/**
* Get array of events with registered listeners
*
* @return array<string>
*/
protected function get_events() {
$events_with_listeners = array_filter( $this->listeners );
$events = array_keys( $events_with_listeners );
return $events;
}
/**
* Get array of listenrs for a specific event
*
* @param string $event Event to get listeners for
* @return array<Listener>
*/
protected function get_listeners( $event ) {
$listeners = isset( $this->listeners[ $event ] ) ? $this->listeners[ $event ] : array();
return $listeners;
}
/**
* Remove invalid listeners from an event
*
* @param string $event
*/
protected function remove_invalid_listeners( $event ) {
$listeners = $this->get_listeners( $event );
if ( empty( $listeners ) ) {
return;
}
$this->listeners[ $event ] = array_filter( $listeners, function( $listener ) {
/** @var Listener $listener */
return $listener->is_valid();
} );
}
/**
* Add a listener to an event
*
* @param string $event
* @param Listener $listener
* @return Listener $listener
*/
public function add_listener( $event, $listener ) {
if ( ! isset( $this->listeners[ $event ] ) ) {
$this->listeners[ $event ] = array();
}
$this->listeners[ $event ][] = $listener;
return $listener;
}
/**
* Remove a listener from any event
*
* @param Listener $removed_listener
*/
public function remove_listener( $removed_listener ) {
$events = $this->get_events();
foreach ( $events as $event ) {
$listeners = $this->get_listeners( $event );
$filtered_listeners = array_filter( $listeners, function( $listener ) use ( $removed_listener ) {
return ( $listener !== $removed_listener );
} );
$this->listeners[ $event ] = $filtered_listeners;
}
}
/**
* Add a persistent listener to an event
*
* @param string $event The event to listen for
* @param string $callable The callable to call when the event is broadcasted
* @return Listener
*/
public function on( $event, $callable ) {
$listener = \Carbon_Fields\Carbon_Fields::resolve( 'event_persistent_listener' );
$listener->set_callable( $callable );
return $this->add_listener( $event, $listener );
}
/**
* Add a one-time listener to an event
*
* @param string $event The event to listen for
* @param string $callable The callable to call when the event is broadcasted
* @return Listener
*/
public function once( $event, $callable ) {
$listener = \Carbon_Fields\Carbon_Fields::resolve( 'event_single_event_listener' );
$listener->set_callable( $callable );
return $this->add_listener( $event, $listener );
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Carbon_Fields\Event;
interface Listener {
/**
* Get the listener's callable
*
* @return callable
*/
public function get_callable();
/**
* Set the listener's callable
*
* @param callable $callable
*/
public function set_callable( $callable );
/**
* Get if the listener is valid
*
* @return boolean
*/
public function is_valid();
/**
* Notify the listener that the event has been broadcasted
*
* @return mixed
*/
public function notify();
}

View file

@ -0,0 +1,41 @@
<?php
namespace Carbon_Fields\Event;
class PersistentListener implements Listener {
/**
* Callable to call when the event is broadcasted
*
* @var callable
*/
protected $callable;
/**
* {@inheritDoc}
*/
public function get_callable() {
return $this->callable;
}
/**
* {@inheritDoc}
*/
public function set_callable( $callable ) {
$this->callable = $callable;
}
/**
* {@inheritDoc}
*/
public function is_valid() {
return true;
}
/**
* {@inheritDoc}
*/
public function notify() {
return call_user_func_array( $this->callable, func_get_args() );
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Carbon_Fields\Event;
class SingleEventListener extends PersistentListener {
/**
* Flag if the event has been called
*
* @var boolean
*/
protected $called = false;
/**
* {@inheritDoc}
*/
public function is_valid() {
return ! $this->called;
}
/**
* {@inheritDoc}
*/
public function notify() {
$this->called = true;
return call_user_func_array( array( $this, 'parent::notify' ), func_get_args() );
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Carbon_Fields\Exception;
class Incorrect_Syntax_Exception extends \Exception {
public static $errors = array();
public static $throw_errors = WP_DEBUG;
/**
* Throw an exception when WP_DEBUG is enabled, and show a friendly admin notice otherwise
*
* @param string $message
* @param int $code (optional)
*/
public static function raise( $message, $code = 0 ) {
if ( empty( static::$errors ) ) {
add_action( 'admin_notices', array( __NAMESPACE__ . '\\Incorrect_Syntax_Exception', 'print_errors' ) );
add_action( 'network_admin_notices', array( __NAMESPACE__ . '\\Incorrect_Syntax_Exception', 'print_errors' ) );
}
$exception = new self( $message, $code );
if ( static::$throw_errors ) {
throw $exception;
} else {
static::$errors[] = $exception;
}
}
public static function print_errors() {
$hideErrorsCookieName = 'crbErrHide';
// Disable cookies
if ( isset( $_COOKIE[ $hideErrorsCookieName ] ) ) {
return;
}
$errors = static::$errors;
$plural = count( $errors ) === 1 ? '' : 's';
include \Carbon_Fields\DIR . '/templates/Exception/incorrect-syntax.php';
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Carbon_Fields;
/**
* Field proxy factory class.
* Used for shorter namespace access when creating a field.
*
* @method static \Carbon_Fields\Field\Association_Field make_association( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Checkbox_Field make_checkbox( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Color_Field make_color( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Complex_Field make_complex( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Date_Field make_date( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Date_Time_Field make_date_time( string $name, string $label = null )
* @method static \Carbon_Fields\Field\File_Field make_file( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Footer_Scripts_Field make_footer_scripts( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Gravity_Form_Field make_gravity_form( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Header_Scripts_Field make_header_scripts( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Hidden_Field make_hidden( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Html_Field make_html( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Image_Field make_image( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Map_Field make_map( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Media_Gallery_Field make_media_gallery( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Multiselect_Field make_multiselect( string $name, string $label = null )
* @method static \Carbon_Fields\Field\OEmbed_Field make_oembed( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Radio_Field make_radio( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Radio_Image_Field make_radio_image( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Rich_Text_Field make_rich_text( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Select_Field make_select( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Separator_Field make_separator( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Set_Field make_set( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Sidebar_Field make_sidebar( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Text_Field make_text( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Textarea_Field make_textarea( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Time_Field make_time( string $name, string $label = null )
* @method static \Carbon_Fields\Field\Block_Preview_Field make_html( string $name, string $label = null )
*/
class Field {
/**
* A proxy for the abstract field factory method.
*
* @see \Carbon_Fields\Field\Field::factory()
* @return \Carbon_Fields\Field\Field
*/
public static function factory() {
return call_user_func_array( array( '\Carbon_Fields\Field\Field', 'factory' ), func_get_args() );
}
/**
* An alias of factory().
*
* @see \Carbon_Fields\Field\Field::factory()
* @return \Carbon_Fields\Field\Field
*/
public static function make() {
return call_user_func_array( array( static::class, 'factory' ), func_get_args() );
}
/**
* @param string $method
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic( $method, $arguments ) {
if ( strpos( $method, 'make_' ) === 0 ) {
$raw_type = substr_replace( $method, '', 0, 5 );
array_unshift( $arguments, $raw_type );
return call_user_func_array( array( static::class, 'factory' ), $arguments );
} else {
trigger_error( sprintf( 'Call to undefined function: %s::%s().', static::class, $method ), E_USER_ERROR );
}
}
}

View file

@ -0,0 +1,851 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Value_Set\Value_Set;
use WP_Query;
use WP_Term_Query;
use WP_User_Query;
use WP_Comment_Query;
/**
* Association field class.
* Allows selecting and manually sorting entries from various types:
* - Posts
* - Terms
* - Users
* - Comments
*/
class Association_Field extends Field {
/**
* WP_Toolset instance for WP data loading
*
* @var \Carbon_Fields\Toolset\WP_Toolset
*/
protected $wp_toolset;
/**
* Min number of selected items allowed. -1 for no limit
*
* @var integer
*/
protected $min = -1;
/**
* Max number of selected items allowed. -1 for no limit
*
* @var integer
*/
protected $max = -1;
/**
* Max items per page. -1 for no limit
*
* @var integer
*/
protected $items_per_page = 20;
/**
* Allow items to be added multiple times
*
* @var boolean
*/
protected $duplicates_allowed = false;
/**
* Default field value
*
* @var array
*/
protected $default_value = array();
/**
* Types of entries to associate with.
* @var array
*/
protected $types = array(
array(
'type' => 'post',
'post_type' => 'post',
),
);
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->wp_toolset = \Carbon_Fields\Carbon_Fields::resolve( 'wp_toolset' );
$this->set_value_set( new Value_Set( Value_Set::TYPE_VALUE_SET, array( 'type' => '', 'subtype' => '', 'id' => 0 ) ) );
parent::__construct( $type, $name, $label );
}
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
$value = array();
if ( isset( $input[ $this->get_name() ] ) ) {
$value = stripslashes_deep( $input[ $this->get_name() ] );
if ( is_array( $value ) ) {
$value = array_values( $value );
}
}
$this->set_value( $value );
return $this;
}
/**
* {@inheritDoc}
*/
public function set_value( $value ) {
$value = $this->value_string_array_to_value_set( $value );
return parent::set_value( $value );
}
/**
* Get value string for legacy value
*
* @param string $legacy_value
* @return string
*/
protected function get_value_string_for_legacy_value( $legacy_value ) {
$entry_type = 'post';
$entry_subtype = 'post';
// attempt to find a suitable type that is registered to this field as post type is not stored for legacy data
foreach ( $this->types as $type ) {
if ( $type['type'] === $entry_type ) {
$entry_subtype = $type['post_type'];
break;
}
}
return $entry_type . ':' . $entry_subtype . ':' . $legacy_value;
}
/**
* Convert a colon:separated:string into its expected components
* Used for backwards compatibility to CF 1.5
*
* @param string $value_string
* @return array
*/
protected function value_string_to_property_array( $value_string ) {
if ( is_numeric( $value_string ) ) {
// we are dealing with legacy data that only contains a post ID
$value_string = $this->get_value_string_for_legacy_value( $value_string );
}
$value_pieces = explode( ':', $value_string );
$type = isset( $value_pieces[0] ) ? $value_pieces[0] : 'post';
$subtype = isset( $value_pieces[1] ) ? $value_pieces[1] : 'post';
$id = isset( $value_pieces[2] ) ? $value_pieces[2] : 0;
$property_array = array(
Value_Set::VALUE_PROPERTY => $value_string,
'type' => $type,
'subtype' => $subtype,
'id' => intval( $id ),
);
return $property_array;
}
/**
* Convert a colon:separated:string into its expected components
* Used for backwards compatibility to CF 1.5
*
* @param array $value_string_array
* @return array<array>
*/
protected function value_string_array_to_value_set( $value_string_array ) {
$value_set = array();
foreach ( $value_string_array as $raw_value_entry ) {
$value_string = $raw_value_entry;
if ( is_array( $raw_value_entry ) ) {
if ( isset( $raw_value_entry['type'] ) ) {
// array is already in suitable format
$value_set[] = $raw_value_entry;
continue;
}
$value_string = $raw_value_entry[ Value_Set::VALUE_PROPERTY ];
}
$value_string = trim( $value_string );
if ( empty( $value_string ) ) {
continue;
}
$property_array = $this->value_string_to_property_array( $value_string );
$value_set[] = $property_array;
}
return $value_set;
}
/**
* Generate the item options.
*
* @access public
*
* @param array $args
* @return array $options The selectable options of the association field.
*/
public function get_options( $args = array() ) {
global $wpdb;
$args = wp_parse_args( $args, array(
'page' => 1,
'term' => '',
) );
$sql_queries = array();
foreach ( $this->types as $type ) {
$type_args = array_merge( $type, array(
'term' => $args['term'],
) );
$callback = "get_{$type['type']}_options_sql";
$sql_statement = $this->$callback( $type_args );
$sql_queries[] = $sql_statement;
}
$sql_queries = implode( " UNION ", $sql_queries );
$per_page = $this->get_items_per_page();
$offset = ($args['page'] - 1) * $per_page;
$sql_queries .= " ORDER BY `title` ASC LIMIT {$per_page} OFFSET {$offset}";
$results = $wpdb->get_results( $sql_queries );
$options = array();
foreach ( $results as $result ) {
$callback = "format_{$result->type}_option";
$options[] = $this->$callback( $result );
}
/**
* Filter the final list of options, available to a certain association field.
*
* @param array $options Unfiltered options items.
* @param string $name Name of the association field.
*/
$options = apply_filters( 'carbon_fields_association_field_options', $options, $this->get_base_name() );
return array(
'total_options' => $wpdb->get_var( "SELECT COUNT(*) FROM (" . preg_replace( '~(LIMIT .*)$~', '', $sql_queries ) . ") as t" ),
'options' => $options,
);
}
/**
* Get the types.
*
* @access public
*
* @return array
*/
public function get_types() {
return $this->types;
}
/**
* Modify the types.
*
* @param array $types New types
* @return self $this
*/
public function set_types( $types ) {
$this->types = $types;
return $this;
}
/**
* Get the minimum allowed number of selected entries.
*
* @return int
*/
public function get_min() {
return $this->min;
}
/**
* Set the minimum allowed number of selected entries.
*
* @param int $min
* @return self $this
*/
public function set_min( $min ) {
$this->min = intval( $min );
return $this;
}
/**
* Get the maximum allowed number of selected entries.
*
* @return int
*/
public function get_max() {
return $this->max;
}
/**
* Set the maximum allowed number of selected entries.
*
* @param int $max
* @return self $this
*/
public function set_max( $max ) {
$this->max = intval( $max );
return $this;
}
/**
* Set the items per page.
*
* @param int $items_per_page
* @return self $this
*/
public function set_items_per_page( $items_per_page ) {
$this->items_per_page = intval( $items_per_page );
return $this;
}
/**
* Get the items per page.
*
* @return int
*/
public function get_items_per_page() {
return $this->items_per_page;
}
/**
* Get whether entry duplicates are allowed.
*
* @return boolean
*/
public function get_duplicates_allowed() {
return $this->duplicates_allowed;
}
/**
* Set whether entry duplicates are allowed.
*
* @param boolean $allowed
* @return self $this
*/
public function set_duplicates_allowed( $allowed ) {
$this->duplicates_allowed = $allowed;
return $this;
}
/**
* Specify whether to allow each entry to be selected multiple times.
* Backwards-compatibility alias.
*
* @param boolean $allow
* @return self $this
*/
public function allow_duplicates( $allow = true ) {
return $this->set_duplicates_allowed( $allow );
}
/**
* Converts the field values into a usable associative array.
*
* The association data is saved in the database in the following format:
* array (
* 0 => 'post:page:4',
* 1 => 'term:category:2',
* 2 => 'user:user:1',
* )
* where the value of each array item contains:
* - Type of data (post, term, user or comment)
* - Subtype of data (the particular post type or taxonomy)
* - ID of the item (the database ID of the item)
*/
protected function value_to_json() {
$value_set = $this->get_value();
$value = array();
foreach ( $value_set as $entry ) {
$item = array(
'type' => $entry['type'],
'subtype' => $entry['subtype'],
'id' => intval( $entry['id'] ),
'title' => $this->get_title_by_type( $entry['id'], $entry['type'], $entry['subtype'] ),
'label' => $this->get_item_label( $entry['id'], $entry['type'], $entry['subtype'] ),
'is_trashed' => ( $entry['type'] == 'post' && get_post_status( $entry['id'] ) === 'trash' ),
);
$value[] = $item;
}
return $value;
}
/**
* Convert the field data into JSON representation.
* @param bool $load Whether to load data from the datastore.
* @return mixed The JSON field data.
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'value' => $this->value_to_json(),
'options' => $this->get_options(),
'min' => $this->get_min(),
'max' => $this->get_max(),
'duplicates_allowed' => $this->duplicates_allowed,
) );
return $field_data;
}
/**
* Helper method to prepare the SQL needed to search for options of type 'post'.
*
* Creates a 'fake' WP_Query with only one result in order to catch the SQL
* that it will construct in order to support all of the WP_Query arguments.
*
* @access public
*
* @param array $args
* @return string
*/
public function get_post_options_sql( $args = array() ) {
$type = $args['type'];
$post_type = $args['post_type'];
$search_term = $args['term'];
unset( $args['type'], $args['post_type'], $args['term'] );
/**
* Filter the default query when fetching posts for a particular field.
*
* @param array $args The parameters, passed to WP_Query::__construct().
*/
$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type . '_' . $post_type;
$args = apply_filters( $filter_name, array(
'post_type' => $post_type,
'posts_per_page' => 1,
'fields' => 'ids',
'suppress_filters' => false,
's' => $search_term,
) );
add_filter( 'posts_fields_request', array( $this, 'get_post_options_sql_select_clause' ) );
add_filter( 'posts_groupby_request', '__return_empty_string' );
add_filter( 'posts_orderby_request', '__return_empty_string' );
add_filter( 'post_limits_request', '__return_empty_string' );
$posts_query = new WP_Query( $args );
remove_filter( 'posts_fields_request', array( $this, 'get_post_options_sql_select_clause' ) );
remove_filter( 'posts_groupby_request', '__return_empty_string' );
remove_filter( 'posts_orderby_request', '__return_empty_string' );
remove_filter( 'post_limits_request', '__return_empty_string' );
return $posts_query->request;
}
/**
* Modify the "SELECT" columns for the WP_Query.
*
* @access public
*
* @param string $fields
* @return string
*/
public function get_post_options_sql_select_clause( $fields ) {
global $wpdb;
return $fields . " , `{$wpdb->posts}`.`post_title` AS `title`, 'post' AS `type`, `{$wpdb->posts}`.`post_type` AS `subtype` ";
}
/**
* Helper method to prepare the SQL needed to search for options of type 'term'.
*
* Creates a 'fake' WP_Term_Query with only one result in order to catch the SQL
* that it will construct in order to support all of the WP_Term_Query arguments.
*
* @access public
*
* @param array $args
* @return string
*/
public function get_term_options_sql( $args = array() ) {
$type = $args['type'];
$taxonomy = $args['taxonomy'];
$search_term = $args['term'];
unset( $args['type'], $args['taxonomy'], $args['term'] );
/**
* Filter the default parameters when fetching terms for a particular field.
*
* @param array $args The parameters, passed to WP_Term_Query::__construct().
*/
$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type . '_' . $taxonomy;
$args = apply_filters( $filter_name, array(
'hide_empty' => 0,
'taxonomy' => $taxonomy,
'fields' => 'count',
'number' => 1,
'search' => $search_term,
'update_term_meta_cache' => false,
) );
add_filter( 'get_terms_fields', array( $this, 'get_term_options_sql_select_clause' ) );
add_filter( 'terms_clauses', array( $this, 'get_term_options_sql_clauses' ) );
$terms_query = new WP_Term_Query( $args );
remove_filter( 'get_terms_fields', array( $this, 'get_term_options_sql_select_clause' ) );
remove_filter( 'terms_clauses', array( $this, 'get_term_options_sql_clauses' ) );
return $terms_query->request;
}
/**
* Modify the "SELECT" columns for the WP_Term_Query.
*
* @access public
*
* @param array $fields
* @return array
*/
public function get_term_options_sql_select_clause( $fields ) {
return array( '`t`.`term_id` AS `ID`', '`t`.`name` AS `title`', '\'term\' as `type`', '`tt`.`taxonomy` AS `subtype`' );
}
/**
* Modify the clauses for the SQL request of the WP_Term_Query.
*
* @access public
*
* @param array $clauses
* @return array
*/
public function get_term_options_sql_clauses( $clauses ) {
unset( $clauses['orderby'], $clauses['order'], $clauses['limits'] );
return $clauses;
}
/**
* Helper method to prepare the SQL needed to search for options of type 'user'.
*
* Creates a 'fake' WP_User_Query with only one result in order to catch the SQL
* that it will construct in order to support all of the WP_User_Query arguments.
*
* @access public
*
* @param array $args
* @return string
*/
public function get_user_options_sql( $args = array() ) {
global $wpdb;
$type = $args['type'];
$search_term = $args['term'];
unset( $args['type'], $args['term'], $args['subtype'] );
/**
* Filter the default parameters when fetching terms for a particular field.
*
* @param array $args The parameters, passed to WP_User_Query::__construct().
*/
$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type;
$args = apply_filters( $filter_name, array(
'fields' => 'ID',
'number' => 1,
'search' => $search_term,
) );
$users_query = new WP_User_Query;
$users_query->prepare_query( $args );
return "SELECT `{$wpdb->users}`.`ID`, '' AS `title`, 'user' AS `type`, 'user' AS `subtype` {$users_query->query_from} {$users_query->query_where}";
}
/**
* Helper method to prepare the SQL needed to search for options of type 'comment'.
*
* Creates a 'fake' WP_Comment_Query with only one result in order to catch the SQL
* that it will construct in order to support all of the WP_Comment_Query arguments.
*
* @access public
*
* @param array $args
* @return string
*/
public function get_comment_options_sql( $args = array() ) {
$type = $args['type'];
$search_term = $args['term'];
unset( $args['type'], $args['term'], $args['subtype'] );
/**
* Filter the default parameters when fetching comments for a particular field.
*
* @param array $args The parameters, passed to get_comments().
*/
$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type;
$args = apply_filters( $filter_name, array(
'fields' => 'ids',
'number' => 1,
'search' => $search_term,
) );
add_filter( 'comments_clauses', array( $this, 'get_comments_clauses' ) );
$comments_query = new WP_Comment_Query;
$comments_query->query( $args );
remove_filter( 'comments_clauses', array( $this, 'get_comments_clauses' ) );
return $comments_query->request;
}
/**
* Modify the "SELECT" columns and the clauses for the SQL request
* performed by the WP_Comment_Query.
*
* @access public
*
* @param array $clauses
* @return array
*/
public function get_comments_clauses( $clauses ) {
global $wpdb;
$clauses['fields'] = " {$wpdb->comments}.`comment_ID` AS `ID`, '' AS `title`, 'comment' AS `type`, 'comment' AS `subtype` ";
unset( $clauses['orderby'], $clauses['limits'], $clauses['groupby'] );
return $clauses;
}
/**
* Used to get the thumbnail of an item.
*
* Can be overriden or extended by the `carbon_fields_association_field_option_thumbnail` filter.
*
* @param int $id The database ID of the item.
* @param string $type Item type (post, term, user, comment, or a custom one).
* @param string $subtype The subtype - "page", "post", "category", etc.
* @return string $title The title of the item.
*/
public function get_thumbnail_by_type( $id, $type, $subtype = '' ) {
$thumbnail_url = '';
if ( $type === 'post' ) {
$thumbnail_url = get_the_post_thumbnail_url( $id, 'thumbnail' );
}
return apply_filters( 'carbon_fields_association_field_option_thumbnail', $thumbnail_url, $id, $type, $subtype );
}
/**
* Used to get the title of an item.
*
* Can be overriden or extended by the `carbon_association_title` filter.
*
* @param int $id The database ID of the item.
* @param string $type Item type (post, term, user, comment, or a custom one).
* @param string $subtype The subtype - "page", "post", "category", etc.
* @return string $title The title of the item.
*/
public function get_title_by_type( $id, $type, $subtype = '' ) {
$title = '';
$method = 'get_' . $type . '_title';
$callable = array( $this->wp_toolset, $method );
if ( is_callable( $callable ) ) {
$title = call_user_func( $callable, $id, $subtype );
}
if ( $type === 'comment' ) {
$max = apply_filters( 'carbon_fields_association_field_comment_length', 30, $this->get_base_name() );
if ( strlen( $title ) > $max ) {
$title = substr( $title, 0, $max ) . '...';
}
}
/**
* Filter the title of the association item.
*
* @param string $title The unfiltered item title.
* @param string $name Name of the association field.
* @param int $id The database ID of the item.
* @param string $type Item type (post, term, user, comment, or a custom one).
* @param string $subtype Subtype - "page", "post", "category", etc.
*/
$title = apply_filters( 'carbon_fields_association_field_title', $title, $this->get_base_name(), $id, $type, $subtype );
if ( ! $title ) {
$title = '(no title) - ID: ' . $id;
}
return $title;
}
/**
* Used to get the label of an item.
*
* Can be overriden or extended by the `carbon_association_item_label` filter.
*
* @param int $id The database ID of the item.
* @param string $type Item type (post, term, user, comment, or a custom one).
* @param string $subtype Subtype - "page", "post", "category", etc.
* @return string $label The label of the item.
*/
public function get_item_label( $id, $type, $subtype = '' ) {
$label = $subtype ? $subtype : $type;
if ( $type === 'post' ) {
$post_type_object = get_post_type_object( $subtype );
$label = $post_type_object->labels->singular_name;
} elseif ( $type === 'term' ) {
$taxonomy_object = get_taxonomy( $subtype );
$label = $taxonomy_object->labels->singular_name;
}
/**
* Filter the label of the association item.
*
* @param string $label The unfiltered item label.
* @param string $name Name of the association field.
* @param int $id The database ID of the item.
* @param string $type Item type (post, term, user, comment, or a custom one).
* @param string $subtype Subtype - "page", "post", "category", etc.
*/
return apply_filters( 'carbon_fields_association_field_item_label', $label, $this->get_base_name(), $id, $type, $subtype );
}
/**
* Retrieve the edit link of a particular object.
*
* @param array $type Object type.
* @param int $id ID of the object.
* @return string URL of the edit link.
*/
protected function get_object_edit_link( $type, $id ) {
switch ( $type['type'] ) {
case 'post':
$edit_link = get_edit_post_link( $id, '' );
break;
case 'term':
$edit_link = get_edit_term_link( $id, '', $type['type'] );
break;
case 'comment':
$edit_link = get_edit_comment_link( $id );
break;
case 'user':
$edit_link = get_edit_user_link( $id );
break;
default:
$edit_link = false;
}
return $edit_link;
}
/**
* Prepares an option of type 'post' for JS usage.
*
* @param \stdClass $data
* @return array
*/
public function format_post_option( $data ) {
return array(
'id' => intval( $data->ID ),
'title' => $this->get_title_by_type( $data->ID, $data->type, $data->subtype ),
'thumbnail' => get_the_post_thumbnail_url( $data->ID, 'thumbnail' ),
'type' => $data->type,
'subtype' => $data->subtype,
'label' => $this->get_item_label( $data->ID, $data->type, $data->subtype ),
'is_trashed' => ( get_post_status( $data->ID ) == 'trash' ),
'edit_link' => $this->get_object_edit_link( get_object_vars( $data ), $data->ID ),
);
}
/**
* Prepares an option of type 'term' for JS usage.
*
* @param \stdClass $data
* @return array
*/
public function format_term_option( $data ) {
return array(
'id' => intval( $data->ID ),
'title' => $this->get_title_by_type( $data->ID, $data->type, $data->subtype ),
'thumbnail' => '',
'type' => $data->type,
'subtype' => $data->subtype,
'label' => $this->get_item_label( $data->ID, $data->type, $data->subtype ),
'is_trashed' => false,
'edit_link' => $this->get_object_edit_link( get_object_vars( $data ), $data->ID ),
);
}
/**
* Prepares an option of type 'comment' for JS usage.
*
* @param \stdClass $data
* @return array
*/
public function format_comment_option( $data ) {
return array(
'id' => intval( $data->ID ),
'title' => $this->get_title_by_type( $data->ID, 'comment' ),
'thumbnail' => '',
'type' => 'comment',
'subtype' => 'comment',
'label' => $this->get_item_label( $data->ID, 'comment' ),
'is_trashed' => false,
'edit_link' => $this->get_object_edit_link( get_object_vars( $data ), $data->ID ),
);
}
/**
* Prepares an option of type 'user' for JS usage.
*
* @param \stdClass $data
* @return array
*/
public function format_user_option( $data ) {
return array(
'id' => intval( $data->ID ),
'title' => $this->get_title_by_type( $data->ID, 'user' ),
'thumbnail' => get_avatar_url( $data->ID, array( 'size' => 150 ) ),
'type' => 'user',
'subtype' => 'user',
'label' => $this->get_item_label( $data->ID, 'user' ),
'is_trashed' => false,
'edit_link' => $this->get_object_edit_link( get_object_vars( $data ), $data->ID ),
);
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Block Preview field class.
* Allows to create a field that displays any HTML in a
* Block Preview (visible when clicking on the "plus" sign in the
* Gutenberg Editor). This type of fields would be printed only inside the preview
* and would be hidden inside all other containers.
*/
class Block_Preview_Field extends Field {
/**
* HTML contents to display
*
* @var string
*/
public $field_html = '';
/**
* Set the field HTML or callback that returns the HTML.
*
* @param string|callable $callback_or_html HTML or callable that returns the HTML.
* @return self $this
*/
public function set_html( $callback_or_html ) {
if ( ! is_callable( $callback_or_html ) && ! is_string( $callback_or_html ) ) {
Incorrect_Syntax_Exception::raise( 'Only strings and callbacks are allowed in the <code>set_html()</code> method.' );
return $this;
}
$this->field_html = '<div class="cf-preview">' . $callback_or_html . '</div><!-- /.cf-preview -->';
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_html = is_callable( $this->field_html ) ? call_user_func( $this->field_html ) : $this->field_html;
$field_data = array_merge( $field_data, array(
'html' => $field_html,
'default_value' => $field_html,
) );
return $field_data;
}
/**
* Whether this field is required.
* The Block Preview field is non-required by design.
*
* @return false
*/
public function is_required() {
return false;
}
/**
* Load the field value.
* Skipped, no value to be loaded.
*/
public function load() {
// skip;
}
/**
* Save the field value.
* Skipped, no value to be saved.
*/
public function save() {
// skip;
}
/**
* Delete the field value.
* Skipped, no value to be deleted.
*/
public function delete() {
// skip;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Carbon_Fields\Field;
/**
* Broken field class.
*/
class Broken_Field extends Field {
}

View file

@ -0,0 +1,100 @@
<?php
namespace Carbon_Fields\Field;
/**
* Single checkbox field class.
*/
class Checkbox_Field extends Field {
/**
* @{inheritDoc}
*/
protected $default_value = false;
/**
* The value that is saved in the database when
* this checkbox field is enabled.
*
* @var string
*/
protected $option_value = 'yes';
/**
* Get the option value.
*
* @return string
*/
public function get_option_value() {
return $this->option_value;
}
/**
* Set the option value.
*
* @param string $value New value
* @return self $this
*/
public function set_option_value( $value ) {
$this->option_value = $value;
return $this;
}
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
parent::set_value_from_input( $input );
if ( $this->get_value() !== $this->get_option_value() ) {
$this->set_value( '' );
}
return $this;
}
/**
* {@inheritDoc}
*/
public function set_value( $value ) {
if ( is_bool( $value ) ) {
$value = $value ? $this->get_option_value() : '';
}
return parent::set_value( $value );
}
/**
* Return a differently formatted value for end-users
*
* @return mixed
*/
public function get_formatted_value() {
return ( $this->get_value() === $this->get_option_value() );
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
* In addition to default data, option value and label are added.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'option_value' => $this->get_option_value(),
'option_label' => parent::get_label(),
) );
return $field_data;
}
/**
* Get the field label.
* Label here is empty because it is displayed in the front-end.
*
* @return string Label of the field.
*/
public function get_label() {
return '';
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Carbon_Fields\Field;
/**
* Color picker field class.
*/
class Color_Field extends Field {
/**
* Flag whether to enable alpha selection
*
* @var boolean
*/
protected $alpha_enabled = false;
/**
* Array of hex colors to show in the color picker
*
* @var array<string>
*/
protected $palette = array();
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'value' => $this->get_value(),
'alphaEnabled' => $this->get_alpha_enabled(),
'palette' => $this->get_palette(),
) );
return $field_data;
}
/**
* Get color presets
*
* @return array<string>
*/
public function get_palette() {
return $this->palette;
}
/**
* Set color presets
*
* @param array<string> $palette
* @return self $this
*/
public function set_palette( $palette ) {
$this->palette = $palette;
return $this;
}
/**
* Get whether alpha is enabled
*
* @return boolean
*/
public function get_alpha_enabled() {
return $this->alpha_enabled;
}
/**
* Set whether alpha is enabled
*
* @param boolean $enabled
* @return self $this
*/
public function set_alpha_enabled( $enabled = true ) {
$this->alpha_enabled = $enabled;
return $this;
}
}

View file

@ -0,0 +1,698 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Datastore\Datastore_Interface;
use Carbon_Fields\Value_Set\Value_Set;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Complex field class.
* Allows nested repeaters with multiple field groups to be created.
*/
class Complex_Field extends Field {
/**
* Visual layout type constants
*/
const LAYOUT_GRID = 'grid'; // default
const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal';
const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical';
const TYPE_PROPERTY = '_type';
/**
* Default field value
*
* @var array
*/
protected $default_value = array();
/**
* Complex field layout
*
* @var string static::LAYOUT_* constant
*/
protected $layout = self::LAYOUT_GRID;
/**
* Value tree describing the complex values and all groups with their child fields
*
* @var array
*/
protected $value_tree = array();
/**
* Array of groups registered for this complex field
*
* @var array
*/
protected $groups = array();
/**
* Minimum number of entries. -1 for no limit
*
* @var integer
*/
protected $values_min = -1;
/**
* Maximum number of entries. -1 for no limit
*
* @var integer
*/
protected $values_max = -1;
/**
* Default entry state - collapsed or not
*
* @var boolean
*/
protected $collapsed = false;
/**
* Defines whether duplicate groups are allowed or not
*
* @var boolean
*/
protected $duplicate_groups_allowed = true;
/**
* Entry labels
* These are translated in init()
*
* @var array
*/
public $labels = array(
'singular_name' => 'Entry',
'plural_name' => 'Entries',
);
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
parent::__construct( $type, $name, $label );
}
/**
* Initialization tasks.
*/
public function init() {
$this->labels = array(
'singular_name' => __( $this->labels['singular_name'], 'carbon-fields' ),
'plural_name' => __( $this->labels['plural_name'], 'carbon-fields' ),
);
parent::init();
}
/**
* Set array of hierarchy field names
*
* @param array $hierarchy
* @return self $this
*/
public function set_hierarchy( $hierarchy ) {
parent::set_hierarchy( $hierarchy );
$this->update_child_hierarchy();
return $this;
}
/**
* Propagate hierarchy to child fields
*/
public function update_child_hierarchy() {
$hierarchy = array_merge( $this->get_hierarchy(), array( $this->get_base_name() ) );
$fields = $this->get_fields();
foreach ( $fields as $field ) {
$field->set_hierarchy( $hierarchy );
}
}
/**
* Activate the field once the container is attached.
*/
public function activate() {
parent::activate();
$fields = $this->get_fields();
foreach ( $fields as $field ) {
$field->activate();
}
}
/**
* Set the datastore of this field and propagate it to children
*
* @param Datastore_Interface $datastore
* @param boolean $set_as_default
* @return self $this
*/
public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
if ( $set_as_default && ! $this->has_default_datastore() ) {
return $this; // datastore has been overriden with a custom one - abort changing to a default one
}
$this->datastore = $datastore;
$this->has_default_datastore = $set_as_default;
$this->update_child_datastore( $this->get_datastore(), true );
return $this;
}
/**
* Propagate the datastore down the hierarchy
*
* @param Datastore_Interface $datastore
* @param boolean $set_as_default
*/
protected function update_child_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
foreach ( $this->groups as $group ) {
$group->set_datastore( $datastore, $set_as_default );
}
}
/**
* Retrieve all groups of fields.
*
* @return array $fields
*/
public function get_fields() {
$fields = array();
foreach ( $this->groups as $group ) {
$group_fields = $group->get_fields();
$fields = array_merge( $fields, $group_fields );
}
return $fields;
}
/**
* Add a set/group of fields.
*
* Accepted param variations:
* - array<Field> $fields
* - string $group_name, array<Field> $fields
* - string $group_name, string $group_label, array<Field> $fields
*
* @return $this
*/
public function add_fields() {
$argv = func_get_args();
$argc = count( $argv );
$fields = $argv[ $argc - 1 ];
$name = '';
$label = null;
if ( $argc >= 2 ) {
$name = $argv[0];
}
if ( $argc >= 3 ) {
$label = $argv[1];
}
$name = ! empty( $name ) ? $name : Group_Field::DEFAULT_GROUP_NAME;
if ( array_key_exists( $name, $this->groups ) ) {
Incorrect_Syntax_Exception::raise( 'Group with name "' . $name . '" in Complex Field "' . $this->get_label() . '" already exists.' );
return $this;
}
$reserved_names = array( Value_Set::VALUE_PROPERTY, static::TYPE_PROPERTY );
foreach ( $fields as $field ) {
/** @var Field $field */
if ( in_array( $field->get_base_name(), $reserved_names ) ) {
Incorrect_Syntax_Exception::raise( '"' . $field->get_base_name() . '" is a reserved keyword for Complex fields and cannot be used for a field name.' );
return $this;
}
}
$group = new Group_Field( $name, $label, $fields );
$this->groups[ $group->get_name() ] = $group;
$this->update_child_hierarchy();
if ( $this->get_datastore() !== null ) {
$this->update_child_datastore( $this->get_datastore(), true );
}
return $this;
}
/**
* Retrieve the groups of this field.
*
* @return array
*/
public function get_group_names() {
return array_keys( $this->groups );
}
/**
* Retrieve a group by its name.
*
* @param string $group_name Group name
* @return Group_Field $group_object Group object
*/
public function get_group_by_name( $group_name ) {
$group_object = null;
foreach ( $this->groups as $group ) {
if ( $group->get_name() == $group_name ) {
$group_object = $group;
}
}
return $group_object;
}
/**
* Set the group label Underscore template.
*
* @param string|callable $template
* @return self $this
*/
public function set_header_template( $template ) {
$template = is_callable( $template ) ? call_user_func( $template ) : $template;
// Assign the template to the group that was added last
$values = array_values( $this->groups );
$group = end( $values );
if ( $group ) {
$group->set_label_template( $template );
$this->groups[ $group->get_name() ] = $group;
}
return $this;
}
/**
* Set the field labels.
* Currently supported values:
* - singular_name - the singular entry label
* - plural_name - the plural entries label
*
* @param array $labels Labels
* @return Complex_Field
*/
public function setup_labels( $labels ) {
$this->labels = array_merge( $this->labels, $labels );
return $this;
}
/**
* Return a clone of a field with hierarchy settings applied
*
* @param Field $field
* @param Field $parent_field
* @param int $group_index
* @return Field
*/
public function get_clone_under_field_in_hierarchy( $field, $parent_field, $group_index = 0 ) {
$clone = clone $field;
$clone->set_hierarchy( array_merge( $parent_field->get_hierarchy(), array( $parent_field->get_base_name() ) ) );
$clone->set_hierarchy_index( array_merge( $parent_field->get_hierarchy_index(), array( $group_index ) ) );
return $clone;
}
protected function get_prefilled_group_fields( $group_fields, $group_values, $group_index ) {
$fields = array();
foreach ( $group_fields as $field ) {
$clone = $this->get_clone_under_field_in_hierarchy( $field, $this, $group_index );
if ( isset( $group_values[ $clone->get_base_name() ] ) ) {
$clone->set_value( $group_values[ $clone->get_base_name() ] );
}
$fields[] = $clone;
}
return $fields;
}
protected function get_prefilled_groups( $groups, $value_tree ) {
$fields = array();
foreach ( $value_tree as $group_index => $value ) {
$group_name = $groups[ $group_index ];
$group = $this->get_group_by_name( $group_name );
if ( ! $group ) {
// Failed to find group - sombody has been messing with the database or group definitions
continue;
}
$group_fields = $group->get_fields();
$group_values = array();
if ( isset( $value_tree[ $group_index ] ) ) {
$group_values = $value_tree[ $group_index ];
}
$fields[ $group_index ] = array( Value_Set::VALUE_PROPERTY => $group->get_name() ) + $this->get_prefilled_group_fields( $group_fields, $group_values, $group_index );
}
return $fields;
}
/**
* Load the field value from an input array based on its name.
*
* @param array $input Array of field names and values.
* @return self $this
*/
public function set_value_from_input( $input ) {
if ( ! isset( $input[ $this->get_name() ] ) ) {
return $this;
}
$value_tree = array();
$input_groups = $input[ $this->get_name() ];
$input_group_index = 0;
foreach ( $input_groups as $values ) {
if ( ! isset( $values[ Value_Set::VALUE_PROPERTY ] ) || ! isset( $this->groups[ $values[ Value_Set::VALUE_PROPERTY ] ] ) ) {
continue;
}
$group = $this->get_group_by_name( $values[ Value_Set::VALUE_PROPERTY ] );
$group_fields = $group->get_fields();
$group_field_names = array_flip( $group->get_field_names() );
$value_group = array( Value_Set::VALUE_PROPERTY => $values[ Value_Set::VALUE_PROPERTY ] );
unset( $values[ Value_Set::VALUE_PROPERTY ] );
// trim input values to those used by the field
$values = array_intersect_key( $values, $group_field_names );
foreach ( $group_fields as $field ) {
$tmp_field = $this->get_clone_under_field_in_hierarchy( $field, $this, $input_group_index );
$tmp_field->set_value_from_input( $values );
if ( $tmp_field instanceof Complex_Field ) {
$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_value_tree();
} else {
$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_full_value();
}
}
$value_tree[] = $value_group;
$input_group_index++;
}
$this->set_value( $value_tree );
return $this;
}
/**
* Save all contained groups of fields.
*/
public function save() {
// Only delete root field values as nested field values should be deleted in a cascading manner by the datastore
$hierarchy = $this->get_hierarchy();
$delete_on_save = empty( $hierarchy );
$delete_on_save = apply_filters( 'carbon_fields_should_delete_field_value_on_save', $delete_on_save, $this );
if ( $delete_on_save ) {
$this->delete();
}
$save = apply_filters( 'carbon_fields_should_save_field_value', true, $this->get_value(), $this );
if ( $save ) {
$this->get_datastore()->save( apply_filters( 'carbon_fields_before_complex_field_save', $this ) );
$field_groups = $this->get_prefilled_groups( $this->get_value(), $this->get_value_tree() );
foreach ( $field_groups as $group_index => $fields ) {
foreach ( $fields as $field ) {
if ( ! ( $field instanceof Field ) ) {
continue;
}
$field->save();
}
}
}
}
/**
* {@inheritDoc}
*/
public function get_formatted_value() {
$field_groups = $this->get_prefilled_groups( $this->get_value(), $this->get_value_tree() );
$value = array();
foreach ( $field_groups as $group_index => $field_group ) {
$value[ $group_index ] = array();
foreach ( $field_group as $key => $field ) {
if ( $field instanceof Field ) {
$value[ $group_index ][ $field->get_base_name() ] = $field->get_formatted_value();
} else {
if ( $key === Value_Set::VALUE_PROPERTY ) {
$value[ $group_index ][ static::TYPE_PROPERTY ] = $field;
} else {
$value[ $group_index ][ $key ] = $field;
}
}
}
}
return $value;
}
/**
* Convert an externally-keyed value array ('_type' => ...)
* to an internally-keyed one ('value' => ...)
*
* @param mixed $value
* @return mixed
*/
protected function external_to_internal_value( $value ) {
if ( ! is_array( $value ) ) {
return $value;
}
if ( ! isset( $value[ static::TYPE_PROPERTY ] ) ) {
return $value;
}
$value = array_map( array( $this, 'external_to_internal_value' ), $value );
$value[ Value_Set::VALUE_PROPERTY ] = $value[ static::TYPE_PROPERTY ];
unset( $value[ static::TYPE_PROPERTY ] );
return $value;
}
/**
* {@inheritDoc}
*/
public function set_value( $value ) {
$value = array_map( array( $this, 'external_to_internal_value' ), $value );
$groups = array();
foreach ( $value as $values ) {
$groups[] = isset( $values[ Value_Set::VALUE_PROPERTY ] ) ? $values[ Value_Set::VALUE_PROPERTY ] : Group_Field::DEFAULT_GROUP_NAME;
}
parent::set_value( $groups );
$this->set_value_tree( $value );
return $this;
}
/**
* {@inheritDoc}
*/
public function set_default_value( $default_value ) {
foreach ( $default_value as $index => $group ) {
if ( ! isset( $group[ static::TYPE_PROPERTY ] ) ) {
$default_value[ $index ][ static::TYPE_PROPERTY ] = Group_Field::DEFAULT_GROUP_NAME;
}
}
$this->default_value = $default_value;
return $this;
}
/**
* Return the full value tree of all groups and their fields
*
* @return mixed
*/
public function get_value_tree() {
return (array) $this->value_tree;
}
/**
* Set the full value tree of all groups and their fields
*
* @see Internal Glossary in DEVELOPMENT.MD
* @param array $value_tree
* @return self $this
*/
public function set_value_tree( $value_tree ) {
$this->value_tree = $value_tree;
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$complex_data = parent::to_json( $load );
$groups_data = array();
foreach ( $this->groups as $group ) {
$group_data = $group->to_json( false );
$group_data['collapsed'] = $this->get_collapsed();
$groups_data[] = $group_data;
}
$field_groups = $this->get_prefilled_groups( $this->get_value(), $this->get_value_tree() );
$value_data = array();
foreach ( $field_groups as $group_index => $fields ) {
$group = $this->get_group_by_name( $fields[ Value_Set::VALUE_PROPERTY ] );
$data = array(
'name' => $group->get_name(),
'label' => $group->get_label(),
'label_template' => $group->get_label_template(),
'group_id' => $group->get_group_id(),
'collapsed' => $this->get_collapsed(),
'fields' => array(),
);
foreach ( $fields as $field ) {
if ( ! ( $field instanceof Field ) ) {
continue;
}
$data['fields'][] = $field->to_json( false );
}
$value_data[] = $data;
}
$group_types = array();
foreach ( $this->groups as $group ) {
$group_types[] = array(
'name' => $group->get_name(),
'label' => $group->get_label(),
);
}
$complex_data = array_merge( $complex_data, array(
'duplicate_groups_allowed' => $this->get_duplicate_groups_allowed(),
'group_types' => $group_types,
'layout' => $this->layout,
'labels' => $this->labels,
'min' => $this->get_min(),
'max' => $this->get_max(),
'multiple_groups' => count( $groups_data ) > 1,
'groups' => $groups_data,
'value' => $value_data,
'collapsed' => $this->get_collapsed(),
) );
return $complex_data;
}
/**
* Modify the layout of this field.
*
* @param string $layout
* @return self $this
*/
public function set_layout( $layout ) {
$available_layouts = array(
static::LAYOUT_GRID,
static::LAYOUT_TABBED_HORIZONTAL,
static::LAYOUT_TABBED_VERTICAL,
);
if ( ! in_array( $layout, $available_layouts ) ) {
$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
'Available layouts: ' . implode( ', ', $available_layouts );
Incorrect_Syntax_Exception::raise( $error_message );
return $this;
}
$this->layout = $layout;
return $this;
}
/**
* Get the minimum number of entries.
*
* @return int $min
*/
public function get_min() {
return $this->values_min;
}
/**
* Set the minimum number of entries.
*
* @param int $min
* @return self $this
*/
public function set_min( $min ) {
$this->values_min = intval( $min );
return $this;
}
/**
* Get the maximum number of entries.
*
* @return int $max
*/
public function get_max() {
return $this->values_max;
}
/**
* Set the maximum number of entries.
*
* @param int $max
* @return self $this
*/
public function set_max( $max ) {
$this->values_max = intval( $max );
return $this;
}
/**
* Get collapsed state
*
* @return bool
*/
public function get_collapsed() {
return $this->collapsed;
}
/**
* Change the groups initial collapse state.
* This state relates to the state of which the groups are rendered.
*
* @param bool $collapsed
* @return self $this
*/
public function set_collapsed( $collapsed = true ) {
$this->collapsed = $collapsed;
return $this;
}
/**
* Get whether duplicate groups are allowed.
*
* @return bool
*/
public function get_duplicate_groups_allowed() {
return $this->duplicate_groups_allowed;
}
/**
* Set whether duplicate groups are allowed.
*
* @param bool $allowed
* @return self $this
*/
public function set_duplicate_groups_allowed( $allowed ) {
$this->duplicate_groups_allowed = $allowed;
return $this;
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Carbon_Fields\Field;
/**
* Date picker field class.
*/
class Date_Field extends Field {
/**
* {@inheritDoc}
*/
protected $allowed_attributes = array( 'placeholder', 'autocomplete' );
/**
* The storage format for use in PHP
*
* @var string
*/
protected $storage_format = 'Y-m-d';
/**
* The expected input format for use in PHP
*
* @var string
*/
protected $input_format_php = 'Y-m-d';
/**
* The expected input format for use in Flatpickr JS
*
* @var string
*/
protected $input_format_js = 'Y-m-d';
/**
* Picker options.
*
* @var array
*/
protected $picker_options = array(
'allowInput' => true,
'altInput' => true,
'altFormat' => "j M Y",
);
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
if ( isset( $input[ $this->get_name() ] ) ) {
$date = \DateTime::createFromFormat( $this->input_format_php, $input[ $this->get_name() ] );
$value = ( $date instanceof \DateTime ) ? $date->format( $this->storage_format ) : '';
$this->set_value( $value );
} else {
$this->clear_value();
}
return $this;
}
/**
* {@inheritDoc}
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$value = $this->get_value();
if ( ! empty( $value ) ) {
$date = \DateTime::createFromFormat( $this->storage_format, $value );
$value = ( $date instanceof \DateTime ) ? $date->format( $this->input_format_php ) : '';
}
$field_data = array_merge( $field_data, array(
'value' => $value,
'storage_format' => $this->get_storage_format(),
'picker_options' => array_merge( $this->get_picker_options(), array(
'dateFormat' => $this->input_format_js,
) ),
) );
return $field_data;
}
/**
* Get storage format
*
* @return string
*/
public function get_storage_format() {
if ( $this->get_context() === 'block' ) {
$this->input_format_js = "Y-m-d h:i:S K";
return "Y-m-d H:i:s";
}
return $this->storage_format;
}
/**
* Set storage format
*
* @param string $storage_format
* @return self $this
*/
public function set_storage_format( $storage_format ) {
$this->storage_format = $storage_format;
return $this;
}
/**
* Get the expected input format in php and js variants
*
* @param string $php_format
* @param string $js_format
* @return self $this
*/
public function get_input_format( $php_format, $js_format ) {
$this->input_format_php = $php_format;
$this->input_format_js = $js_format;
return $this;
}
/**
* Set a format for use on the front-end in both PHP and Flatpickr formats
* The formats should produce identical results (i.e. they are translations of each other)
*
* @param string $php_format
* @param string $js_format
* @return self $this
*/
public function set_input_format( $php_format, $js_format ) {
$this->input_format_php = $php_format;
$this->input_format_js = $js_format;
return $this;
}
/**
* Returns the picker options.
*
* @return array
*/
public function get_picker_options() {
return $this->picker_options;
}
/**
* Set datepicker options
*
* @param array $options
* @return self $this
*/
public function set_picker_options( $options ) {
$this->picker_options = array_replace( $this->picker_options, $options );
return $this;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Carbon_Fields\Field;
/**
* Date and time picker field class.
*/
class Date_Time_Field extends Time_Field {
/**
* {@inheritDoc}
*/
protected $picker_options = array(
'allowInput' => true,
'enableTime' => true,
'enableSeconds' => true,
);
/**
* {@inheritDoc}
*/
protected $storage_format = 'Y-m-d H:i:s';
/**
* {@inheritDoc}
*/
protected $input_format_php = 'Y-m-d g:i:s A';
/**
* {@inheritDoc}
*/
protected $input_format_js = 'Y-m-d h:i:S K';
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
<?php
namespace Carbon_Fields\Field;
/**
* File upload field class.
*
* Allows selecting and saving a media attachment file,
* where the file ID is saved in the database.
*/
class File_Field extends Field {
// empty for all types. available types: audio, video, image and all WordPress-recognized mime types
public $field_type = '';
// alt, author, caption, dateFormatted, description, editLink, filename, height, icon, id, link, menuOrder, mime, name, status, subtype, title, type, uploadedTo, url, width
public $value_type = 'id';
/**
* Change the type of the field
*
* @param string $type
* @return File_Field
*/
public function set_type( $type ) {
$this->field_type = $type;
return $this;
}
/**
* Change the value type of the field.
*
* @param string $value_type
* @return File_Field
*/
public function set_value_type( $value_type ) {
$this->value_type = $value_type;
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @access public
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'type_filter' => $this->field_type,
'value_type' => $this->value_type,
) );
return $field_data;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Carbon_Fields\Field;
/**
* Footer scripts field class.
* Intended only for use in theme options container.
*/
class Footer_Scripts_Field extends Scripts_Field {
/**
* {@inheritDoc}
*/
protected $hook_name = 'wp_footer';
/**
* {@inheritDoc}
*/
protected function get_default_help_text() {
return __( 'If you need to add scripts to your footer (like Google Analytics tracking code), you should enter them in this box.', 'carbon-fields' );
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Carbon_Fields\Field;
/**
* Gravity Form selection field class
*/
class Gravity_Form_Field extends Select_Field {
/**
* Whether the Gravity Forms plugin is installed and activated.
*
* @return bool
*/
public function is_plugin_active() {
if ( class_exists( '\RGFormsModel' ) && method_exists( '\RGFormsModel', 'get_forms' ) ) {
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
protected function load_options() {
return $this->get_gravity_form_options();
}
/**
* Set the available forms as field options
*
* @return array
*/
protected function get_gravity_form_options() {
if ( ! $this->is_plugin_active() ) {
return array();
}
$forms = \RGFormsModel::get_forms( null, 'title' );
if ( ! is_array( $forms ) || empty( $forms ) ) {
return array();
}
$options = array(
'' => __( 'No form', 'carbon-fields' ),
);
foreach ( $forms as $form ) {
$options[ $form->id ] = $form->title;
}
return apply_filters( 'carbon_fields_gravity_form_options', $options );
}
/**
* {@inheritDoc}
*/
public function to_json( $load ) {
$this->set_options( $this->get_options() );
return parent::to_json( $load );
}
}

View file

@ -0,0 +1,261 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Datastore\Datastore_Interface;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
class Group_Field {
/**
* Default name to use for groups which have no name defined by the user
*/
const DEFAULT_GROUP_NAME = '_';
/**
* Unique group identificator. Generated randomly.
*
* @var string
*/
protected $group_id;
/**
* Sanitized group name.
*
* @var string
*/
protected $name;
/**
* Group label, used during rendering.
*
* @var string
*/
protected $label;
/**
* Group label template.
*
* @var string
*/
protected $label_template;
/**
* Group fields.
*
* @var array
*/
protected $fields = array();
/**
* List of registered unique field names
*
* @see register_field_name()
* @var array
*/
protected $registered_field_names = array();
/**
* Create a group field with the specified name and label.
*
* @param string $name
* @param string $label
* @param array $fields
*/
public function __construct( $name, $label, $fields ) {
$this->set_name( $name );
$this->set_label( $label );
$this->add_fields( $fields );
// Pick random ID
$random_string = md5( mt_rand() . $this->get_name() . $this->get_label() );
$random_string = substr( $random_string, 0, 5 ); // 5 chars should be enough
$this->group_id = 'carbon-group-' . $random_string;
}
/**
* Add a group of fields.
*
* @param array $fields
*/
public function add_fields( $fields ) {
foreach ( $fields as $field ) {
if ( ! ( $field instanceof Field ) ) {
Incorrect_Syntax_Exception::raise( 'Object must be of type ' . __NAMESPACE__ . '\\Field' );
}
$this->register_field_name( $field->get_name() );
}
$this->fields = array_merge( $this->fields, $fields );
}
/**
* Fields attribute getter.
*
* @return array
*/
public function get_fields() {
return $this->fields;
}
/**
* Return the names of all fields.
*
* @return array
*/
public function get_field_names() {
$names = array();
foreach ( $this->fields as $field ) {
$names[] = $field->get_name();
}
return $names;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$fields_data = array();
foreach ( $this->get_fields() as $field ) {
$fields_data[] = $field->to_json( $load );
}
$group_data = array(
'group_id' => $this->get_group_id(),
'name' => $this->get_name(),
'label' => $this->get_label(),
'label_template' => $this->get_label_template(),
'fields' => $fields_data,
);
return $group_data;
}
/**
* Group ID attribute getter.
*
* @return string
*/
public function get_group_id() {
return $this->group_id;
}
/**
* Set the group label.
*
* @param string $label If null, the label will be generated from the group name
* @return self $this
*/
public function set_label( $label ) {
if ( is_null( $label ) ) {
// Try to guess field label from its name
$label = Helper::normalize_label( $this->get_name() );
}
$this->label = $label;
return $this;
}
/**
* Label attribute getter.
*
* @return string
*/
public function get_label() {
return $this->label;
}
/**
* Set the label template.
*
* @param string $template
* @return self $this
*/
public function set_label_template( $template ) {
$this->label_template = $template;
return $this;
}
/**
* Get the label template.
*
* @return string
*/
public function get_label_template() {
return $this->label_template;
}
/**
* Print the label template.
*/
public function template_label() {
echo $this->label_template;
}
/**
* Set the group name.
*
* @param string $name Group name, either sanitized or not
* @return self $this
*/
public function set_name( $name ) {
if ( ! $name ) {
$name = static::DEFAULT_GROUP_NAME;
}
if ( ! Helper::is_valid_entity_id( $name ) ) {
Incorrect_Syntax_Exception::raise( 'Group names can only contain lowercase alphanumeric characters, dashes and underscores ("' . $name . '" passed).' );
return $this;
}
$this->name = $name;
return $this;
}
/**
* Return the group name.
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Assign a DataStore instance for all group fields.
*
* @param Datastore_Interface $datastore
* @param boolean $set_as_default
* @return self $this
*/
public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
foreach ( $this->fields as $field ) {
$field->set_datastore( $datastore, $set_as_default );
}
return $this;
}
/**
* Perform checks whether there is a field registered with the name $name.
* If not, the field name is recorded.
*
* @param string $name
* @return boolean
*/
public function register_field_name( $name ) {
if ( in_array( $name, $this->registered_field_names ) ) {
Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
return false;
}
$this->registered_field_names[] = $name;
return true;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Carbon_Fields\Field;
/**
* Header scripts field class.
* Intended only for use in theme options container.
*/
class Header_Scripts_Field extends Scripts_Field {
/**
* {@inheritDoc}
*/
protected $hook_name = 'wp_head';
/**
* {@inheritDoc}
*/
protected function get_default_help_text() {
return __( 'If you need to add scripts to your header, you should enter them here.', 'carbon-fields' );
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Carbon_Fields\Field;
/**
* Hidden field class.
*/
class Hidden_Field extends Field {
protected $hidden = false;
/**
* Get hidden state
*
* @return bool
*/
public function get_hidden() {
return $this->hidden;
}
/**
* This states configures if the field is shown in the backend.
*
* @param bool $hidden
* @return self $this
*/
public function set_hidden( $hidden = true ) {
$this->hidden = $hidden;
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'hidden' => $this->get_hidden()
) );
return $field_data;
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* HTML field class.
* Allows to create a field that displays any HTML in a container.
*/
class Html_Field extends Field {
/**
* HTML contents to display
*
* @var string
*/
public $field_html = '';
/**
* Set the field HTML or callback that returns the HTML.
*
* @param string|callable $callback_or_html HTML or callable that returns the HTML.
* @return self $this
*/
public function set_html( $callback_or_html ) {
if ( ! is_callable( $callback_or_html ) && ! is_string( $callback_or_html ) ) {
Incorrect_Syntax_Exception::raise( 'Only strings and callbacks are allowed in the <code>set_html()</code> method.' );
return $this;
}
$this->field_html = $callback_or_html;
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_html = is_callable( $this->field_html ) ? call_user_func( $this->field_html ) : $this->field_html;
$field_data = array_merge( $field_data, array(
'html' => $field_html,
'default_value' => $field_html,
) );
return $field_data;
}
/**
* Whether this field is required.
* The HTML field is non-required by design.
*
* @return false
*/
public function is_required() {
return false;
}
/**
* Load the field value.
* Skipped, no value to be loaded.
*/
public function load() {
// skip;
}
/**
* Save the field value.
* Skipped, no value to be saved.
*/
public function save() {
// skip;
}
/**
* Delete the field value.
* Skipped, no value to be deleted.
*/
public function delete() {
// skip;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Carbon_Fields\Field;
/**
* Image field class.
*
* Allows selecting and saving a media attachment file,
* where the image ID is saved in the database.
*/
class Image_Field extends File_Field {
public $field_type = 'image';
}

View file

@ -0,0 +1,132 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Value_Set\Value_Set;
/**
* Google Maps with Address field class.
* Allows to manually select a pin, or to position a pin based on a specified address.
* Coords (lat, lng), address and zoom are saved in the database.
*/
class Map_Field extends Field {
/**
* {@inheritDoc}
*/
protected $default_value = array(
Value_Set::VALUE_PROPERTY => '40.346544,-101.645507',
'lat' => 40.346544,
'lng' => -101.645507,
'zoom' => 10,
'address' => '',
);
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_PROPERTIES, array( 'lat' => '', 'lng' => '', 'zoom' => '', 'address' => '' ) ) );
parent::__construct( $type, $name, $label );
}
/**
* Enqueue scripts and styles in admin
* Called once per field type
*/
public static function admin_enqueue_scripts() {
$api_key = apply_filters( 'carbon_fields_map_field_api_key', false );
$url = apply_filters( 'carbon_fields_map_field_api_url', '//maps.googleapis.com/maps/api/js?' . ( $api_key ? 'key=' . $api_key : '' ), $api_key );
wp_enqueue_script( 'carbon-google-maps', $url, array(), null );
}
/**
* Convert lat and lng to a comma-separated list
*/
protected function lat_lng_to_latlng( $lat, $lng ) {
return ( ! empty( $lat ) && ! empty( $lng ) ) ? $lat . ',' . $lng : '';
}
/**
* Load the field value from an input array based on its name
*
* @param array $input Array of field names and values.
* @return self $this
*/
public function set_value_from_input( $input ) {
if ( ! isset( $input[ $this->get_name() ] ) ) {
$this->set_value( null );
return $this;
}
$value_set = array(
'lat' => '',
'lng' => '',
'zoom' => '',
'address' => '',
);
foreach ( $value_set as $key => $v ) {
if ( isset( $input[ $this->get_name() ][ $key ] ) ) {
$value_set[ $key ] = $input[ $this->get_name() ][ $key ];
}
}
$value_set['lat'] = (float) $value_set['lat'];
$value_set['lng'] = (float) $value_set['lng'];
$value_set['zoom'] = (int) $value_set['zoom'];
$value_set[ Value_Set::VALUE_PROPERTY ] = $this->lat_lng_to_latlng( $value_set['lat'], $value_set['lng'] );
$this->set_value( $value_set );
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$value_set = $this->get_value();
$field_data = array_merge( $field_data, array(
'value' => array(
'lat' => floatval( $value_set['lat'] ),
'lng' => floatval( $value_set['lng'] ),
'zoom' => intval( $value_set['zoom'] ),
'address' => $value_set['address'],
'value' => $value_set[ Value_Set::VALUE_PROPERTY ],
),
) );
return $field_data;
}
/**
* Set the coords and zoom of this field.
*
* @param string $lat Latitude
* @param string $lng Longitude
* @param int $zoom Zoom level
* @return $this
*/
public function set_position( $lat, $lng, $zoom ) {
return $this->set_default_value( array_merge(
$this->get_default_value(),
array(
Value_Set::VALUE_PROPERTY => $this->lat_lng_to_latlng( $lat, $lng ),
'lat' => $lat,
'lng' => $lng,
'zoom' => $zoom,
)
) );
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Value_Set\Value_Set;
use Carbon_Fields\Helper\Helper;
/**
* Set field class.
*
* Allows selecting multiple attachments and stores
* their IDs in the Database.
*/
class Media_Gallery_Field extends Field {
/**
* File type filter. Leave a blank string for any file type.
* Available types: audio, video, image and all WordPress-recognized mime types
*
* @var string|array
*/
protected $file_type = '';
/**
* What value to store
*
* @var string
*/
protected $value_type = 'id';
/**
* Default field value
*
* @var array
*/
protected $default_value = array();
/**
* Allow items to be added multiple times
*
* @var boolean
*/
protected $duplicates_allowed = true;
/**
* Toggle the inline edit functionality
*
* @var boolean
*/
protected $can_edit_inline = true;
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
parent::__construct( $type, $name, $label );
}
/**
* Change the type of the field
*
* @param string $type
* @return Media_Gallery_Field
*/
public function set_type( $type ) {
$this->file_type = $type;
return $this;
}
/**
* Get whether entry duplicates are allowed.
*
* @return boolean
*/
public function get_duplicates_allowed() {
return $this->duplicates_allowed;
}
/**
* Set whether entry duplicates are allowed.
*
* @param boolean $allowed
* @return self $this
*/
public function set_duplicates_allowed( $allowed ) {
$this->duplicates_allowed = $allowed;
return $this;
}
/**
* Set wether the edit functionality will open inline or in the media popup
*
* @param boolean $can_edit_inline
* @return self $this
*/
public function set_edit_inline( $can_edit_inline ) {
$this->can_edit_inline = $can_edit_inline;
return $this;
}
/**
* Load the field value from an input array based on its name
*
* @param array $input Array of field names and values.
* @return self $this
*/
public function set_value_from_input( $input ) {
if ( ! isset( $input[ $this->name ] ) ) {
$this->set_value( array() );
} else {
$value = stripslashes_deep( $input[ $this->name ] );
if ( is_array( $value ) ) {
$value = array_values( $value );
}
$this->set_value( $value );
}
return $this;
}
/**
* Converts the field values into a usable associative array.
*
* @access protected
*
* @return array
*/
protected function value_to_json() {
$value_set = $this->get_value();
return array(
'value' => array_map( 'absint', $value_set ),
);
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, $this->value_to_json(), array(
'value_type' => $this->value_type,
'type_filter' => $this->file_type,
'can_edit_inline' => $this->can_edit_inline,
'duplicates_allowed' => $this->get_duplicates_allowed(),
) );
return $field_data;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Delimiter;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Value_Set\Value_Set;
/**
* Multiselect field class.
* Allows to create a select where multiple values can be selected.
*/
class Multiselect_Field extends Predefined_Options_Field {
/**
* Default field value
*
* @var array
*/
protected $default_value = array();
/**
* Value delimiter
*
* @var string
*/
protected $value_delimiter = '|';
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
parent::__construct( $type, $name, $label );
}
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
if ( ! isset( $input[ $this->get_name() ] ) ) {
return $this->set_value( array() );
}
$value_delimiter = $this->value_delimiter;
$options_values = $this->get_options_values();
$value = stripslashes_deep( $input[ $this->get_name() ] );
$value = Delimiter::split( $value, $this->value_delimiter );
$value = array_map( function( $val ) use ( $value_delimiter ) {
return Delimiter::unquote( $val, $value_delimiter );
}, $value );
$value = Helper::get_valid_options( $value, $options_values );
return $this->set_value( $value );
}
/**
* {@inheritDoc}
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$value_delimiter = $this->value_delimiter;
$options = $this->parse_options( $this->get_options(), true );
$options = array_map( function( $option ) use ( $value_delimiter ) {
$option['value'] = Delimiter::quote( $option['value'], $value_delimiter );
return $option;
}, $options );
$value = array_map( function( $value ) use ( $value_delimiter ) {
return Delimiter::quote( $value, $value_delimiter );
}, $this->get_formatted_value() );
$field_data = array_merge( $field_data, array(
'options' => $options,
'value' => $value,
'valueDelimiter' => $this->value_delimiter,
) );
return $field_data;
}
/**
* {@inheritDoc}
*/
public function get_formatted_value() {
$value = $this->get_value();
$value = $this->get_values_from_options( $value );
return $value;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Carbon_Fields\Field;
/**
* Oembed field class.
*/
class Oembed_Field extends Field {
/**
* Enqueue scripts and styles in admin
* Called once per field type
*/
public static function admin_enqueue_scripts() {
wp_enqueue_script( 'wp-api' );
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$field_data = array_merge( $field_data, array(
'value' => $this->get_value(),
) );
return $field_data;
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
/**
* Base class for fields with predefined options.
* Mainly used to reduce the bloat on the base Field class.
*/
abstract class Predefined_Options_Field extends Field {
/**
* Stores the raw, unprocessed field options
*
* @var array(array|callable)
*/
protected $option_collections = array();
/**
* Check if an array is indexed
*
* @param array $array
* @return boolean
*/
protected function is_indexed_array( $array ) {
return array_keys( $array ) === range( 0, count( $array ) - 1 );
}
/**
* Set the options of this field.
* Accepts either array of data or a callback that returns the data.
*
* @param array|callable $options
* @return self $this
*/
public function set_options( $options ) {
if ( ! is_callable( $options ) && ! is_array( $options ) ) {
Incorrect_Syntax_Exception::raise( 'Only arrays and callbacks are allowed in the <code>set_options()</code> method.' );
return $this;
}
$this->option_collections = array();
return $this->add_options( $options );
}
/**
* Add new options to this field.
* Accepts either array of data or a callback that returns the data.
*
* @param array|callable $options
* @return self $this
*/
public function add_options( $options ) {
if ( ! is_callable( $options ) && ! is_array( $options ) ) {
Incorrect_Syntax_Exception::raise( 'Only arrays and callbacks are allowed in the <code>add_options()</code> method.' );
return $this;
}
$this->option_collections[] = $options;
return $this;
}
/**
* Get a populated array of options executing any callbacks in the process
*
* @return array
*/
protected function load_options() {
$options = array();
foreach ( $this->option_collections as $collection ) {
$collection_items = array();
if ( is_callable( $collection ) ) {
$collection_items = call_user_func( $collection );
if ( ! is_array( $collection_items ) ) {
continue;
}
} else {
$collection_items = $collection;
}
if ( $this->is_indexed_array( $options ) && $this->is_indexed_array( $collection_items ) ) {
$options = array_merge( $options, $collection_items );
} else {
$options = array_replace( $options, $collection_items );
}
}
return $options;
}
/**
* Retrieve the current options.
*
* @return array
*/
public function get_options() {
return $this->load_options();
}
/**
* Retrieve the current options' values only.
*
* @return array $options
*/
protected function get_options_values() {
$options = $this->parse_options( $this->get_options() );
return wp_list_pluck( $options, 'value' );
}
/**
* Changes the options array structure. This is needed to keep the array items order when it is JSON encoded.
* Will also work with a callable that returns an array.
*
* @param array|callable $options
* @param bool $stringify_value (optional)
* @return array
*/
protected function parse_options( $options, $stringify_value = false ) {
$parsed = array();
if ( is_callable( $options ) ) {
$options = call_user_func( $options );
}
foreach ( $options as $key => $value ) {
$parsed[] = array(
'value' => $stringify_value ? strval( $key ) : $key,
'label' => strval( $value ),
);
}
return $parsed;
}
/**
* Get an array of all values that are both in the passed array and the predefined list of options
*
* @param array $values
* @return array
*/
protected function get_values_from_options( $values ) {
$options_values = $this->get_options_values();
$values = Helper::get_valid_options( $values, $options_values );
return $values;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Carbon_Fields\Field;
/**
* Radio buttons field class.
*/
class Radio_Field extends Select_Field {
}

View file

@ -0,0 +1,10 @@
<?php
namespace Carbon_Fields\Field;
/**
* Radio buttons field class.
*/
class Radio_Image_Field extends Select_Field {
}

View file

@ -0,0 +1,153 @@
<?php
namespace Carbon_Fields\Field;
/**
* WYSIWYG rich text area field class.
*/
class Rich_Text_Field extends Textarea_Field {
/**
* All Rich Text Fields settings references.
* Used to prevent duplicated wp_editor initialization with the same settings.
*
* @var array
*/
protected static $settings_references = [];
/**
* WP Editor settings
*
* @link https://developer.wordpress.org/reference/classes/_wp_editors/parse_settings/
* @var array
*/
protected $settings = array(
'media_buttons' => true,
'tinymce' => array(
'resize' => true,
'wp_autoresize_on' => true,
),
);
/**
* MD5 Hash of the instance settings
*
* @var string
*/
protected $settings_hash = null;
/**
* WP Editor settings reference
*
* @var string
*/
protected $settings_reference;
/**
* Set the editor settings
*
* @param array $settings
* @return self $this
*/
public function set_settings( $settings ) {
$this->settings = array_merge( $this->settings, $settings );
return $this;
}
/**
* Calc the settings hash
*
* @return string
*/
protected function calc_settings_hash() {
return md5( json_encode( $this->settings ) );
}
/**
* Get the editor settings reference
*
* @return string
*/
protected function get_settings_reference() {
if ( is_null( $this->settings_hash ) ) {
$this->settings_hash = $this->calc_settings_hash();
}
return 'carbon_fields_settings_' . $this->settings_hash;
}
/**
* {@inheritDoc}
*/
public function activate() {
parent::activate();
add_action( 'admin_footer', array( $this, 'editor_init' ) );
}
/**
* Display the editor.
*
* Instead of enqueueing all required scripts and stylesheets and setting up TinyMCE,
* wp_editor() automatically enqueues and sets up everything.
*/
public function editor_init() {
if( in_array( $this->get_settings_reference(), self::$settings_references ) ) {
return;
}
self::$settings_references[] = $this->get_settings_reference();
?>
<div style="display:none;">
<?php
add_filter( 'user_can_richedit', '__return_true' );
wp_editor( '', $this->get_settings_reference(), $this->settings );
remove_filter( 'user_can_richedit', '__return_true' );
?>
</div>
<?php
}
/**
* Display Upload Image Button
*
*/
public function upload_image_button_html() {
$upload_image_button = '<a href="#" class="button insert-media add_media" data-editor="<%- id %>" title="Add Media">
<span class="wp-media-buttons-icon"></span> Add Media
</a>';
echo apply_filters( 'crb_upload_image_button_html', $upload_image_button, $this->base_name );
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$media_buttons = '';
if ( $this->settings['media_buttons'] ) {
ob_start();
remove_action( 'media_buttons', 'media_buttons' );
$this->upload_image_button_html();
do_action( 'media_buttons' );
add_action( 'media_buttons', 'media_buttons' );
$media_buttons = apply_filters( 'crb_media_buttons_html', ob_get_clean(), $this->base_name );
}
$field_data = array_merge( $field_data, array(
'rich_editing' => user_can_richedit() && ! empty( $this->settings['tinymce'] ),
'media_buttons' => $media_buttons,
'settings_reference' => $this->get_settings_reference(),
) );
return $field_data;
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Carbon_Fields\Field;
/**
* Abstract scripts field class.
* Intended only for use in theme options container.
*/
abstract class Scripts_Field extends Textarea_Field {
/**
* Hook to putput scripts in
*
* @var string
*/
protected $hook_name = '';
/**
* Hook priority to use
*
* @var integer
*/
protected $hook_priority = 10;
/**
* Initialization actions
*/
public function init() {
$this->set_help_text( $this->get_default_help_text() );
add_action( 'wp', array( $this, 'attach_hook' ) );
parent::init();
}
/**
* Attach the assigned hook
*/
public function attach_hook() {
if ( strlen( $this->get_hook_name() ) > 0 ) {
add_action( $this->get_hook_name(), array( $this, 'print_scripts' ), $this->get_hook_priority() );
}
}
/**
* Display the field value in the front-end header.
*/
public function print_scripts() {
$is_valid_datastore = ( $this->get_datastore() instanceof \Carbon_Fields\Datastore\Theme_Options_Datastore || $this->get_datastore() instanceof \Carbon_Fields\Datastore\Network_Datastore );
if ( ! $this->get_datastore() || ! $is_valid_datastore ) {
return;
}
$this->load();
echo $this->get_formatted_value();
}
/**
* Get the hook name
*
* @return string
*/
public function get_hook_name() {
return $this->hook_name;
}
/**
* Set the hook name
*
* @param string $hook_name
* @return self $this
*/
public function set_hook_name( $hook_name ) {
$this->hook_name = $hook_name;
return $this;
}
/**
* Get the hook priority
*
* @return integer
*/
public function get_hook_priority() {
return $this->hook_priority;
}
/**
* Set the hook priority
*
* @param integer $hook_priority
* @return self $this
*/
public function set_hook_priority( $hook_priority ) {
$this->hook_priority = $hook_priority;
return $this;
}
/**
* Set the hook name and priority
*
* @param string $hook_name
* @param integer $hook_priority
* @return self $this
*/
public function set_hook( $hook_name, $hook_priority = 10 ) {
$this->set_hook_name( $hook_name );
$this->set_hook_priority( $hook_priority );
return $this;
}
/**
* Get the default help text to be displayed for this field type.
*
* @return string
*/
abstract protected function get_default_help_text();
}

View file

@ -0,0 +1,65 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Helper;
/**
* Select dropdown field class.
*/
class Select_Field extends Predefined_Options_Field {
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
$options_values = $this->get_options_values();
$value = null;
if ( isset( $input[ $this->get_name() ] ) ) {
$raw_value = stripslashes_deep( $input[ $this->get_name() ] );
$raw_value = Helper::get_valid_options( array( $raw_value ), $options_values );
if ( ! empty( $raw_value ) ) {
$value = $raw_value[0];
}
}
if ( $value === null ) {
$value = $options_values[0];
}
return $this->set_value( $value );
}
/**
* {@inheritDoc}
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$options = $this->parse_options( $this->get_options(), true );
$value = strval( $this->get_formatted_value() );
$field_data = array_merge( $field_data, array(
'value' => strval( $value ),
'options' => $options,
) );
return $field_data;
}
/**
* {@inheritDoc}
*/
public function get_formatted_value() {
$options_values = $this->get_options_values();
if ( empty( $options_values ) ) {
$options_values[] = '';
}
$value = $this->get_value();
$value = $this->get_values_from_options( array( $value ) );
$value = ! empty( $value ) ? $value[0] : $options_values[0];
return $value;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Carbon_Fields\Field;
/**
* Separator field class.
* Used for presentation purposes to create sections between fields.
*/
class Separator_Field extends Field {
/**
* Load the field value.
* Skipped, no value to be loaded.
*/
public function load() {
// skip;
}
/**
* Save the field value.
* Skipped, no value to be saved.
*/
public function save() {
// skip;
}
/**
* Delete the field value.
* Skipped, no value to be deleted.
*/
public function delete() {
// skip;
}
/**
* Whether this field is required.
* The Separator field is non-required by design.
*
* @return false
*/
public function is_required() {
return false;
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Helper;
use Carbon_Fields\Value_Set\Value_Set;
/**
* Set field class.
* Allows to create a set of checkboxes where multiple can be selected.
*/
class Set_Field extends Predefined_Options_Field {
/**
* Default field value
*
* @var array
*/
protected $default_value = array();
/**
* The options limit.
*
* @var int
*/
protected $limit_options = 0;
/**
* Create a field from a certain type with the specified label.
*
* @param string $type Field type
* @param string $name Field name
* @param string $label Field label
*/
public function __construct( $type, $name, $label ) {
$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
parent::__construct( $type, $name, $label );
}
/**
* {@inheritDoc}
*/
public function set_value_from_input( $input ) {
if ( ! isset( $input[ $this->get_name() ] ) ) {
return $this->set_value( array() );
}
$options_values = $this->get_options_values();
$value = stripslashes_deep( $input[ $this->get_name() ] );
$value = Helper::get_valid_options( $value, $options_values );
return $this->set_value( $value );
}
/**
* {@inheritDoc}
*/
public function to_json( $load ) {
$field_data = parent::to_json( $load );
$options = $this->parse_options( $this->get_options(), true );
$value = array_map( 'strval', $this->get_formatted_value() );
$field_data = array_merge( $field_data, array(
'options' => $options,
'value' => $value,
'limit_options' => $this->limit_options,
) );
return $field_data;
}
/**
* {@inheritDoc}
*/
public function get_formatted_value() {
$value = $this->get_value();
$value = $this->get_values_from_options( $value );
return $value;
}
/**
* Set the number of the options to be displayed at the initial field display.
*
* @param int $limit
*/
public function limit_options( $limit ) {
$this->limit_options = $limit;
return $this;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Carbon_Fields\Field;
use Carbon_Fields\Helper\Helper;
class Sidebar_Field extends Select_Field {
/**
* Allow the user to add new sidebars
*
* @var boolean
*/
private $enable_add_new = true;
/**
* Array of sidebars to exclude from the select menu
*
* @var array
*/
private $excluded_sidebars = array();
/**
* {@inheritDoc}
*/
protected function load_options() {
$sidebars = Helper::get_active_sidebars();
$options = array();
foreach ( $sidebars as $sidebar ) {
if ( in_array( $sidebar[ 'id' ], $this->excluded_sidebars ) ) {
continue;
}
$options[ $sidebar['id'] ] = $sidebar['name'];
}
return $options;
}
/**
* Disable adding new sidebars.
*
* @return self $this
*/
public function disable_add_new() {
$this->enable_add_new = false;
return $this;
}
/**
* Specify sidebars to be excluded.
*
* @param array $sidebars
* @return self $this
*/
public function set_excluded_sidebars( $sidebars ) {
$this->excluded_sidebars = is_array( $sidebars ) ? $sidebars : array( $sidebars );
return $this;
}
/**
* Returns an array that holds the field data, suitable for JSON representation.
*
* @param bool $load Should the value be loaded from the database or use the value from the current instance.
* @return array
*/
public function to_json( $load ) {
$options = array();
if ( $this->enable_add_new ) {
$options[] = array(
'value' => '__add_new',
'label' => _x( 'Add New', 'sidebar', 'carbon-fields' ),
);
}
$field_data = parent::to_json( $load );
// override default value and options behavior since sidebars are
// loaded separately and not as a part of the field options
$field_data = array_merge( $field_data, array(
'value' => $this->get_formatted_value(),
) );
$field_data['options'] = array_merge( $field_data['options'], $options );
if ( ! empty( $this->excluded_sidebars ) ) {
$field_data = array_merge( $field_data, array(
'excluded_sidebars' => $this->excluded_sidebars,
) );
}
return $field_data;
}
}

Some files were not shown because too many files have changed in this diff Show more