<?php

class Papi_Property_Table extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'array';

	/**
	 * Build table from array data.
	 *
	 * @param  array $arr
	 * @param  bool  $child
	 *
	 * @return stringe
	 */
	protected function build_table( array $arr, $child = false ) {
		$html = '

';
		$allow_html = $this->get_setting( 'allow_html' );

		if ( $child ) {
			$html .= '
';

			foreach ( $arr[0] as $key => $value ) {
				if ( $allow_html ) {
					$key = html_entity_decode( $key );
				}

				$html .= sprintf( '%s	', $key );
			}

			$html .= '
';
		}

		foreach ( $arr as $key => $value ) {
			$html .= '
';

			foreach ( $value as $key2 => $value2 ) {
				if ( is_array( $value2 ) ) {
					$value2 = $this->build_table( $value2, true );
				}

				$value2 = papi_convert_to_string( $value2 );

				if ( $allow_html ) {
					$value2 = html_entity_decode( $value2 );
				}

				$html .= sprintf( '%s	', $value2 );
			}

			$html .= '
';
		}

		return $html . '

';
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'allow_html'  => false,
			'items'       => [],
		];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$value = $this->get_value();
		$data  = $this->get_setting( 'items' );

		if ( ! is_array( $data ) ) {
			return;
		}

		// Convert key/value array to [key, value] value.
		foreach ( $data as $key => $value ) {
			if ( is_array( $value ) ) {
				continue;
			}

			$data[$key] = [$key, $value];
		}

		echo $this->build_table( $data ); // wpcs: xss ok
	}
}


<?php

/**
 * Admin class that handles admin meta boxes.
 */
final class Papi_Admin_Meta_Box {

	/**
	 * The core box.
	 *
	 * @var Papi_Core_Box
	 */
	protected $box;

	/**
	 * The constructor.
	 *
	 * @param Papi_Core_Box $box
	 */
	public function __construct( Papi_Core_Box $box ) {
		// Check if the current user is allowed to view this box.
		if ( ! papi_current_user_is_allowed( $box->capabilities ) ) {
			return;
		}

		if ( $box->display ) {
			$this->box = $box;
			$this->setup_actions();
		}
	}

	/**
	 * Add custom css for hiding boxes with no frame in screen options.
	 *
	 * @return string
	 */
	public function admin_head() {
		if ( ! $this->box->frame ) {
			echo sprintf(
				'<style type="text/css">label[for="%s-hide"],#%s>h2,#%s>button{display:none !important}</style>',
				esc_attr( $this->box->id ),
				esc_attr( $this->box->id ),
				esc_attr( $this->box->id )
			);
		}
	}

	/**
	 * Add css classes to meta box.
	 *
	 * @param  array $classes
	 *
	 * @return string[]
	 */
	public function meta_box_css_classes( array $classes ) {
		return array_merge( $classes, [
			'papi-box'
		] );
	}

	/**
	 * Move meta boxes after title.
	 */
	public function move_meta_box_after_title() {
		global $post, $wp_meta_boxes;
		do_meta_boxes( get_current_screen(), $this->box->context, $post );
		unset( $wp_meta_boxes[get_post_type( $post )][$this->box->context] );
	}

	/**
	 * Get meta post type.
	 *
	 * @return string
	 */
	protected function get_post_type() {
		if ( papi_get_meta_type() === 'post' ) {
			if ( $post_id = papi_get_post_id() ) {
				return get_post_type( $post_id );
			}

			if ( $post_type = papi_get_post_type() ) {
				return $post_type;
			}
		}

		return $this->box->id;
	}

	/**
	 * Get meta box title.
	 *
	 * @return string
	 */
	protected function get_title() {
		$title = $this->box->title;

		if ( $this->box->get_option( 'required' ) ) {
			$title .= papi_property_required_html(
				$this->box->properties[0],
				true
			);
		}

		return $title;
	}

	/**
	 * Render the meta box
	 *
	 * @param array $post
	 * @param array $args
	 */
	public function render_meta_box( $post, array $args ) {
		if ( ! isset( $args['args'] ) ) {
			return;
		}

		// Do a last check before all properties is rendered.
		$args['args'] = array_filter( $args['args'], 'papi_is_property' );

		// Inherit options from the box.
		foreach ( $args['args'] as $index => $property ) {
			if ( $property->layout === 'horizontal' ) {
				$args['args'][$index]->layout = $this->box->layout;
			}
		}

		// Render the properties.
		papi_render_properties( papi_sort_order( array_reverse( $args['args'] ) ) );
	}

	/**
	 * Setup action hooks.
	 */
	protected function setup_actions() {
		if ( post_type_exists( $this->get_post_type() ) && papi_get_meta_type() === 'post' ) {
			add_action( 'add_meta_boxes', [$this, 'setup_meta_box'] );

			if ( $this->box->context === 'after_title' ) {
				add_action( 'edit_form_after_title', [$this, 'move_meta_box_after_title'] );
			}
		} else {
			$this->setup_meta_box();
		}

		// Will be called on when you call do_meta_boxes
		// even without a real post type.
		add_action(
			sprintf(
				'postbox_classes_%s_%s',
				strtolower( $this->get_post_type() ),
				$this->box->id
			),
			[$this, 'meta_box_css_classes']
		);

		add_action( 'admin_head', [$this, 'admin_head'] );
	}

	/**
	 * Setup meta box.
	 */
	public function setup_meta_box() {
		$properties = $this->box->properties;

		// Check all properties and remove them that can't be rendered.
		foreach ( $properties as $index => $property ) {
			if ( $property instanceof Papi_Property && ! $property->can_render() ) {
				unset( $properties[$index] );
			}
		}

		// Bail if properties array is empty,
		// no need to render a empty meta box.
		if ( empty( $properties ) ) {
			return;
		}

		add_meta_box(
			$this->box->id,
			$this->get_title(),
			[$this, 'render_meta_box'],
			$this->get_post_type(),
			$this->box->context,
			$this->box->priority,
			$properties
		);
	}
}


<?php

/**
 * Core class that implements a Papi type.
 */
class Papi_Core_Type {

	/**
	 * The page type class name.
	 *
	 * @var string
	 */
	protected $_class_name = '';

	/**
	 * The file path of the core type file.
	 *
	 * @var string
	 */
	protected $_file_path = '';

	/**
	 * The type id.
	 *
	 * @var string
	 */
	protected $_id = '';

	/**
	 * The name of the core type.
	 *
	 * @var string
	 */
	public $name = '';

	/**
	 * The sort order of the core type.
	 *
	 * @var int
	 */
	public $sort_order = 1000;

	/**
	 * The type name. Used for WP CLI.
	 *
	 * @var string
	 */
	public $type = 'core';

	/**
	 * The constructor.
	 *
	 * Load a core type by the file.
	 *
	 * @param string $file_path
	 */
	public function __construct( $file_path = '' ) {
		if ( is_file( $file_path ) ) {
			$this->setup_file( $file_path );
			$this->setup_meta_data();
		}
	}

	/**
	 * Determine if the entry type is allowed.
	 *
	 * @return bool
	 */
	public function allowed() {
		return true;
	}

	/**
	 * Boot page type.
	 *
	 * @codeCoverageIgnore
	 */
	public function boot() {
		$this->setup_actions();
		$this->setup_filters();
	}

	/**
	 * Get the page type class name with namespace if exists.
	 *
	 * @return string
	 */
	public function get_class_name() {
		return $this->_class_name;
	}

	/**
	 * Get the page type file pat.h
	 *
	 * @return string
	 */
	public function get_file_path() {
		return $this->_file_path;
	}

	/**
	 * Get the page type identifier.
	 *
	 * @return string
	 */
	public function get_id() {
		if ( ! empty( $this->_id ) ) {
			return $this->_id;
		}

		return papi_get_core_type_base_path( $this->_file_path );
	}

	/**
	 * Get meta data from type class and merge
	 * with the parent meta data.
	 *
	 * @return array
	 */
	protected function get_meta() {
		$method = 'meta';

		if ( ! method_exists( $this, $method ) ) {
			return [];
		}

		$child_meta = call_user_func( [$this, $method] );
		$child_meta = is_array( $child_meta ) ? $child_meta : [];

		$parent_class  = get_parent_class( $this );
		$parent_exists = method_exists( $parent_class, $method );
		$parent_meta   = [];

		while ( $parent_exists ) {
			$rc = new ReflectionClass( $parent_class );

			// Bail if not instantiable.
			if ( ! $rc->isInstantiable() ) {
				break;
			}

			$parent        = $rc->newInstance();
			$output        = call_user_func( [$parent, $method] );
			$output        = is_array( $output ) ? $output : [];
			$parent_meta   = array_merge( $parent_meta, $output );
			$parent_class  = get_parent_class( $parent_class );
			$parent_exists = method_exists( $parent_class, $method );
		}

		return array_merge( $parent_meta, $child_meta );
	}

	/**
	 * Get type name.
	 *
	 * @return string
	 */
	public function get_type() {
		return strtolower( $this->type );
	}

	/**
	 * Check so we have a name on the page type.
	 *
	 * @return bool
	 */
	public function has_name() {
		return ! empty( $this->name );
	}

	/**
	 * Check if the the given identifier match the page type identifier.
	 *
	 * @param  string $id
	 *
	 * @return bool
	 */
	public function match_id( $id ) {
		return $this->get_id() === $id;
	}

	/**
	 * Create a new instance of the page type file.
	 *
	 * @return null|object
	 */
	public function new_class() {
		if ( empty( $this->_file_path ) ) {
			return;
		}

		return new $this->_class_name;
	}

	/**
	 * Set custom id.
	 *
	 * @param  string $id
	 */
	public function set_id( $id ) {
		$this->_id = $id;
	}

	/**
	 * Setup actions.
	 *
	 * @codeCoverageIgnore
	 */
	protected function setup_actions() {
	}

	/**
	 * Load the file and setup file path, file name and class name properties.
	 *
	 * @param string $file_path
	 */
	protected function setup_file( $file_path ) {
		$this->_file_path  = $file_path;
		$this->_class_name = papi_get_class_name( $this->_file_path );
	}

	/**
	 * Setup filters.
	 *
	 * @codeCoverageIgnore
	 */
	protected function setup_filters() {
	}

	/**
	 * Setup meta data.
	 */
	protected function setup_meta_data() {
		foreach ( $this->get_meta() as $key => $value ) {
			if ( substr( $key, 0, 1 ) === '_' ) {
				continue;
			}

			$this->$key = papi_esc_html( $value );
		}

		if ( $this->sort_order === 1000 ) {
			$this->sort_order = papi_filter_settings_sort_order();
		}
	}
}


<?php

/**
 * Core class that implements a Papi property.
 */
class Papi_Core_Property implements JsonSerializable {

	/**
	 * The conditional class.
	 *
	 * @var Papi_Core_Conditional
	 */
	protected $conditional;

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'string';

	/**
	 * Default options.
	 *
	 * @var array
	 */
	protected $default_options = [
		'after_class'   => '',
		'after_html'    => '',
		'auth_callback' => '__return_true',
		'before_class'  => '',
		'before_html'   => '',
		'cache'         => true,
		'capabilities'  => [],
		'default'       => null,
		'description'   => '',
		'disabled'      => false,
		'display'       => true,
		'format_cb'     => '',
		'lang'          => false,
		'layout'        => 'horizontal', // or 'vertical'
		'overwrite'     => false,
		'post_type'     => '',
		'raw'           => false,
		'required'      => false,
		'rules'         => [],
		'settings'      => [],
		'sidebar'       => true,
		'site_id'       => 0,
		'show_in_rest'  => true,
		'slug'          => '',
		'sort_order'    => -1,
		'title'         => '',
		'type'          => '',
		'value'         => null
	];

	/**
	 * Option aliases.
	 *
	 * @var array
	 */
	protected $option_aliases = [
		'desc' => 'description',
		'sets' => 'settings',
	];

	/**
	 * Default value.
	 *
	 * @var null
	 */
	public $default_value;

	/**
	 * Display the property in WordPress admin.
	 *
	 * @var bool
	 */
	protected $display = true;

	/**
	 * Current property options object.
	 *
	 * @var stdClass
	 */
	protected $options;

	/**
	 * The post id.
	 *
	 * @var int
	 */
	protected $post_id;

	/**
	 * Determine if the property require a slug or not.
	 *
	 * @var bool
	 */
	protected $slug_required = true;

	/**
	 * The store that the property works with to get data.
	 *
	 * @var Papi_Core_Meta_Store
	 */
	protected $store;

	/**
	 * Parent property.
	 *
	 * @var Papi_Core_Property
	 */
	protected $parent_property = null;

	/**
	 * The constructor.
	 */
	public function __construct() {
		$this->setup_actions();
		$this->setup_filters();
		$this->setup_properties();
	}

	/**
	 * Get option value dynamic.
	 *
	 * @param  string $key
	 *
	 * @return mixed
	 */
	public function __get( $key ) {
		return $this->get_option( $key );
	}

	/**
	 * Check if options value exists or not.
	 *
	 * @param  string $key
	 *
	 * @return bool
	 */
	public function __isset( $key ) {
		return $this->get_option( $key ) !== null;
	}

	/**
	 * Set options value dynamic.
	 *
	 * @param string $key
	 * @param mixed  $value
	 */
	public function __set( $key, $value ) {
		$this->set_option( $key, $value );
	}

	/**
	 * Determine if the current user has capabilities rights.
	 *
	 * @return bool
	 */
	public function current_user_can() {
		return papi_current_user_is_allowed( $this->capabilities );
	}

	/**
	 * Delete value from the database.
	 *
	 * @param  string $slug
	 * @param  int    $post_id
	 * @param  string $type
	 *
	 * @return bool
	 */
	public function delete_value( $slug, $post_id, $type ) {
		return papi_data_delete( $post_id, $slug, $type );
	}

	/**
	 * Determine if the property is disabled or not.
	 *
	 * @return bool
	 */
	public function disabled() {
		// Return true if site id isn't zero and don't match the current site id.
		if ( $this->site_id !== 0 && $this->site_id !== get_current_blog_id() ) {
			return true;
		}

		// If the post type don't match the current one
		// the property should not be rendered.
		if ( papi_get_meta_type() === 'post' && ! empty( $this->post_type ) && $this->post_type !== papi_get_post_type() ) {
			return true;
		}

		return $this->disabled;
	}

	/**
	 * Determine if the property should be displayed.
	 *
	 * @return bool
	 */
	public function display() {
		// If the property display is true it can be changed with
		// the display option.
		return $this->display ? $this->options->display : false;
	}

	/**
	 * Create a new instance of the given type or a
	 * empty core property if no type is given.
	 *
	 * @return null|object
	 */
	public static function factory() {
		if ( count( func_get_args() ) === 0 ) {
			return new static;
		} else {
			$type = func_get_arg( 0 );
		}

		if ( is_array( $type ) ) {
			$type = (object) $type;
		}

		if ( ! is_string( $type ) && ! is_object( $type ) ) {
			return;
		}

		if ( is_object( $type ) && ! isset( $type->type ) ) {
			$property = new static;
			$property->set_options( $type );

			return $property;
		}

		if ( is_subclass_of( $type, __CLASS__ ) ) {
			return $type;
		}

		$options = null;

		if ( is_object( $type ) ) {
			if ( ! isset( $type->type ) || ! is_string( $type->type ) ) {
				return;
			}

			$options = $type;
			$type    = $type->type;
		}

		// Old types, 'PropertyString' => 'String'.
		$type = preg_replace( '/^Property/', '', $type );

		if ( empty( $type ) ) {
			return;
		}

		$class_name = papi_get_property_class_name( $type );

		if ( ! class_exists( $class_name ) || ! is_subclass_of( $class_name, __CLASS__ ) ) {
			return;
		}

		if ( ! papi()->exists( $class_name ) ) {
			papi()->bind( $class_name, new $class_name() );
		}

		$class = papi()->make( $class_name );

		// @codeCoverageIgnoreStart
		if ( ! is_object( $class ) || $class instanceof Papi_Core_Property === false ) {
			$class = new $class_name();
			papi()->bind( $class_name, $class );
		}
		// @codeCoverageIgnoreEnd

		$property = clone $class;

		if ( is_object( $options ) ) {
			$property->set_options( (array) $options );
		}

		return $property;
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return mixed
	 */
	public function format_value( $value, $slug, $post_id ) {
		$value = maybe_unserialize( $value );

		return papi_maybe_json_decode( $value, $this->convert_type === 'array' );
	}

	/**
	 * Get child property.
	 *
	 * @param  string $slug
	 * @param  array  $items
	 *
	 * @return Papi_Core_Property|null
	 */
	public function get_child_property( $slug, array $items = [] ) {
		$items = empty( $items ) ? $this->get_child_properties() : $items;

		foreach ( $items as $property ) {
			if ( is_array( $property ) && isset( $property['items'] ) ) {
				$property = $this->get_child_property( $slug, $property['items'] );
			}

			$property = papi_get_property_type( $property );

			if ( papi_is_property( $property ) && $property->match_slug( $slug ) ) {
				return $property;
			}
		}
	}

	/**
	 * Get child properties from `items` in the settings array.
	 *
	 * @return array
	 */
	public function get_child_properties() {
		$items = $this->get_setting( 'items', [] );

		return papi_to_array( $items );
	}

	/**
	 * Get convert type.
	 *
	 * @return string
	 */
	public function get_convert_type() {
		return $this->convert_type;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [];
	}

	/**
	 * Get meta type from the store or the default one.
	 *
	 * @return string
	 */
	public function get_meta_type() {
		$store = $this->get_store();

		return $store ? $store->get_type() : papi_get_meta_type();
	}

	/**
	 * Get option value.
	 *
	 * @param  string $key
	 * @param  mixed  $default
	 *
	 * @return mixed
	 */
	public function get_option( $key, $default = null ) {
		$value = $default;

		if ( isset( $this->options->$key ) ) {
			$value = $this->options->$key;
		}

		if ( papi_is_empty( $value ) && isset( $this->default_options[$key] ) ) {
			$value = $this->default_options[$key];
		}

		if ( $key === 'settings' && is_array( $value ) ) {
			$value = (object) $value;
		}

		return $value;
	}

	/**
	 * Get the current property options object.
	 *
	 * @return stdClass
	 */
	public function get_options() {
		if ( is_null( $this->options ) ) {
			$this->set_options();
		}

		return $this->options;
	}

	/**
	 * Get parent property.
	 *
	 * @return Papi_Core_Property
	 */
	public function get_parent_property() {
		return $this->parent_property;
	}

	/**
	 * Get post id.
	 *
	 * @return int
	 */
	public function get_post_id() {
		if ( ! papi_is_empty( $this->post_id ) ) {
			return $this->post_id;
		}

		if ( $this->store instanceof Papi_Core_Meta_Store ) {
			return $this->store->id;
		}

		return papi_get_post_id();
	}

	/**
	 * Get conditional rules.
	 *
	 * @return array
	 */
	public function get_rules() {
		return $this->rules;
	}

	/**
	 * Get setting value.
	 *
	 * @param  string $key
	 * @param  mixed  $default
	 *
	 * @return stdClass
	 */
	public function get_setting( $key, $default = null ) {
		if ( ! is_string( $key ) ) {
			return $default;
		}

		$settings = $this->get_settings();

		if ( isset( $settings->$key ) ) {
			return $settings->$key;
		}

		return $default;
	}

	/**
	 * Get custom property settings.
	 *
	 * @return stdClass
	 */
	public function get_settings() {
		return (object) array_merge( $this->get_default_settings(), (array) $this->settings );
	}

	/**
	 * Get property slug.
	 *
	 * @param  bool $remove_prefix
	 *
	 * @return string
	 */
	public function get_slug( $remove_prefix = false ) {
		if ( $remove_prefix ) {
			return unpapify( $this->slug );
		}

		return $this->slug;
	}

	/**
	 * Get the store that the property will get data from.
	 *
	 * @return Papi_Core_Meta_Store|null
	 */
	public function get_store() {
		if ( $this->store instanceof Papi_Core_Meta_Store ) {
			return $this->store;
		}

		return papi_get_meta_store( $this->get_post_id() );
	}

	/**
	 * Get value, no database connections here.
	 *
	 * @return mixed
	 */
	public function get_value() {
		return papi_is_empty( $this->value ) ? $this->default : $this->value;
	}

	/**
	 * Get the html id attribute value.
	 *
	 * @param  object|string $suffix
	 * @param  int           $row
	 *
	 * @return string
	 */
	public function html_id( $suffix = '', $row = null ) {
		if ( is_array( $suffix ) || is_object( $suffix ) ) {
			return papi_f( $this->html_name( $suffix, $row ) );
		} else {
			$suffix = empty( $suffix ) || ! is_string( $suffix ) ? '' : '_' . $suffix;
			$suffix = papi_underscorify( papi_slugify( $suffix ) );
		}

		$name = $this->html_name();
		$len  = strlen( $name );

		if ( isset( $name[$len - 1] ) && $name[$len - 1] === ']' ) {
			return papi_f( sprintf( '%s%s]', substr( $name, 0, $len - 1 ), $suffix ) );
		}

		return papi_f( sprintf( '%s%s', $this->html_name(), $suffix ) );
	}

	/**
	 * Get html name for property with or without sub property and row number.
	 *
	 * @param  array|object $sub_property
	 * @param  int          $row
	 *
	 * @return string
	 */
	public function html_name( $sub_property = null, $row = null ) {
		$base_slug = $this->slug;

		if ( is_null( $sub_property ) ) {
			return $base_slug;
		}

		if ( is_numeric( $row ) ) {
			$base_slug = sprintf( '%s[%d]', $base_slug, intval( $row ) );
		}

		if ( ! papi_is_property( $sub_property ) ) {
			if ( is_array( $sub_property ) || is_object( $sub_property ) ) {
				$sub_property = self::factory( $sub_property );
			} else {
				return $base_slug;
			}
		}

		return sprintf( '%s[%s]', $base_slug, unpapify( $sub_property->get_slug() ) );
	}

	/**
	 * Change value after it's loaded from the database.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return mixed
	 */
	public function load_value( $value, $slug, $post_id ) {
		$value = maybe_unserialize( $value );

		return papi_maybe_json_decode( $value, $this->convert_type === 'array' );
	}

	/**
	 * Match property slug with given slug value.
	 *
	 * @param  string $slug
	 *
	 * @return bool
	 */
	public function match_slug( $slug ) {
		if ( ! is_string( $slug ) ) {
			return false;
		}

		return $this->get_slug( ! preg_match( '/^papi\_/', $slug ) ) === $slug;
	}

	/**
	 * Prepare value before database.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	protected function prepare_value( $value ) {
		if ( papi_is_empty( $value ) ) {
			return;
		}

		$value = papi_santize_data( $value );

		if ( is_array( $value ) ) {
			$value = array_filter( $value, function ( $val ) {
				return ! papi_is_empty( $val );
			} );

			if ( ! count( array_filter( array_keys( $value ), 'is_string' ) ) ) {
				$value = array_values( $value );
			}
		}

		return $value;
	}

	/**
	 * Register property with:
	 *
	 * - `register_meta` (WP 4.6+)
	 *
	 * @param  string $type
	 *
	 * @return bool
	 */
	public function register( $type = 'post' ) {
		if ( version_compare( get_bloginfo( 'version' ), '4.6', '<' ) ) {
			return false;
		}

		$type = papi_get_meta_type( $type );

		// Register option fields with the new `register_setting` function and only for WordPress 4.7.
		if ( $type === 'option' && version_compare( get_bloginfo( 'version' ), '4.7', '>=' ) ) {
			// The `type` will be the same for each fields, this is just to get it out
			// to the REST API, the output will be different for different fields and are
			// handled later on.
			return register_setting( 'papi', $this->get_slug( true ), [
				'sanitize_callback' => [$this, 'register_meta_sanitize_callback'],
				'show_in_rest'      => $this->get_option( 'show_in_rest' ),
				'type'              => 'string'
			] );
		}

		// Register meta fields with the new `register_meta` function.
		// The `type` will be the same for each fields, this is just to get it out
		// to the REST API, the output will be different for different fields and are
		// handled later on.
		return register_meta( $type, $this->get_slug( true ), [
			'auth_callback'     => $this->get_option( 'auth_callback' ),
			'description'       => $this->get_option( 'description' ),
			'sanitize_callback' => [$this, 'register_meta_sanitize_callback'],
			'show_in_rest'      => $this->get_option( 'show_in_rest' ),
			'single'            => $this->get_convert_type() !== 'array',
			'type'              => 'string'
		] );
	}

	/**
	 * No need for this in Papi, since this is handle different.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	public function register_meta_sanitize_callback( $value ) {
		return $value;
	}

	/**
	 * Prepare property value for REST API response.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	public function rest_prepare_value( $value ) {
		return $value;
	}

	/**
	 * Render AJAX request.
	 */
	public function render_ajax_request() {
		papi_render_property( $this );
	}

	/**
	 * Check if the property is allowed
	 * to render by the conditional rules.
	 *
	 * @param  array $rules
	 *
	 * @return bool
	 */
	public function render_is_allowed_by_rules( array $rules = [] ) {
		if ( empty( $rules ) ) {
			$rules = $this->get_rules();
		}

		return $this->conditional->display( $rules, $this );
	}

	/**
	 * Set the store that the property will get data from.
	 *
	 * @param Papi_Core_Meta_Store $store
	 */
	public function set_store( Papi_Core_Meta_Store $store ) {
		$this->store = $store;
	}

	/**
	 * Set post id.
	 *
	 * @param int $post_id
	 */
	public function set_post_id( $post_id ) {
		if ( ! is_numeric( $post_id ) ) {
			return;
		}

		$this->post_id = (int) $post_id;
	}

	/**
	 * Set the current property options object.
	 *
	 * @param array|object $options
	 */
	public function set_options( $options = [] ) {
		$this->options = $this->setup_options( $options );
	}

	/**
	 * Set property option value.
	 *
	 * @param string $key
	 * @param mixed  $value
	 */
	public function set_option( $key, $value ) {
		if ( ! is_object( $this->options ) ) {
			$this->options = (object) $this->default_options;
		}

		$this->options->$key = $value;
	}

	/**
	 * Set property setting value.
	 *
	 * @param string $key
	 * @param mixed  $value
	 */
	public function set_setting( $key, $value ) {
		if ( isset( $this->options->settings, $this->options->settings->$key ) ) {
			$this->options->settings->$key = $value;
		}
	}

	/**
	 * Set parent property.
	 *
	 * @param Papi_Core_Property $parent_property
	 */
	public function set_parent_property( Papi_Core_Property $parent_property ) {
		$this->parent_property = $parent_property;
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
	}

	/**
	 * Setup filters.
	 */
	protected function setup_filters() {
	}

	/**
	 * Setup properties.
	 */
	protected function setup_properties() {
		$this->conditional = new Papi_Core_Conditional();

		if ( $this->default_options['sort_order'] === -1 ) {
			$this->default_options['sort_order'] = papi_filter_settings_sort_order();
		}

		if ( papi_is_empty( $this->default_options['post_type'] ) ) {
			$this->default_options['post_type'] = papi_get_post_type();
		}
	}

	/**
	 * Setup property options.
	 *
	 * @param  mixed $options
	 *
	 * @return mixed
	 */
	protected function setup_options( $options = [] ) {
		// When a object is sent in, just return it.
		if ( is_object( $options ) ) {
			return $options;
		}

		// Only arrays can be handled.
		if ( ! is_array( $options ) ) {
			$options = [];
		}

		// Merge default options with the given options array.
		$options = array_merge( $this->default_options, $options );
		$options = (object) $options;

		// Setup aliases.
		$option_aliases = apply_filters( 'papi/option_aliases', $this->option_aliases );
		$option_aliases = is_array( $option_aliases ) ? $option_aliases : [];
		$option_aliases = array_merge( $this->option_aliases, $option_aliases );

		foreach ( $option_aliases as $alias => $option ) {
			if ( isset( $options->$alias ) && ! papi_is_empty( $options->$alias ) ) {
				$options->$option = $options->$alias;
				unset( $options->$alias );
			}
		}

		// Capabilities should be a array.
		$options->capabilities = papi_to_array( $options->capabilities );

		// Setup property slug.
		$options->slug = strtolower( $this->setup_options_slug( $options ) );

		// Setup property settings.
		$options->settings = $this->setup_options_settings( $options );

		// Type should always be lowercase.
		$options->type = strtolower( $options->type );

		// Escape all options except those that are send it as second argument.
		return papi_esc_html( $options, ['before_html', 'html', 'after_html'] );
	}

	/**
	 * Setup options slug.
	 *
	 * @param  stdClass $options
	 *
	 * @return string
	 */
	protected function setup_options_slug( $options ) {
		$slug = $options->slug;

		// When `slug` is false or not required a unique slug should be generated.
		if ( $slug === false || ( empty( $slug ) && $this->slug_required === false ) ) {
			return '_' . papi_html_name( md5( uniqid( rand(), true ) ) );
		}

		// If `slug` is empty, check if `title` is not empty
		// and generate a slug from the `title` or if empty
		// use the `type`.
		if ( empty( $slug ) ) {
			if ( empty( $options->title ) ) {
				$slug = papi_slugify( $options->type );
			} else {
				$slug = papi_slugify( $options->title );
			}
		}

		// Create a html friendly name from the `slug`.
		return papi_html_name( $slug );
	}

	/**
	 * Setup options settings.
	 *
	 * @param  stdClass $options
	 *
	 * @return stdClass
	 */
	protected function setup_options_settings( $options ) {
		$property_class = self::factory( $options->type );

		if ( papi_is_property( $property_class ) ) {
			$options->settings = array_merge( (array) $property_class->get_default_settings(), (array) $options->settings );
		}

		return (object) $options->settings;
	}

	/**
	 * Update value before it's saved to the database.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return mixed
	 */
	public function update_value( $value, $slug, $post_id ) {
		if ( ! ( $value = $this->prepare_value( $value ) ) ) {
			return;
		}

		return papi_maybe_json_encode( $value );
	}

	/**
	 * Get a string representation of the object.
	 *
	 * @return string
	 */
	public function __toString() {
		return $this->get_slug( true );
	}

	/**
	 * Serialize property to json.
	 *
	 * @return mixed
	 */
	public function jsonSerialize() {
		return $this->get_options();
	}
}


<?php

/**
 * Papi type that handle attachment post type, post data
 * and rendering. All attachment types should extend this
 * class.
 */
class Papi_Attachment_Type extends Papi_Page_Type {

	/**
	 * The post type to register the type with.
	 *
	 * @var string
	 */
	public $post_type = 'attachment';

	/**
	 * The type name.
	 *
	 * @var string
	 */
	public $type = 'attachment';

	/**
	 * Get post type.
	 *
	 * @return string
	 */
	public function get_post_type() {
		return $this->post_type[0];
	}

	/**
	 * Setup filters.
	 */
	protected function setup_filters() {
		// Don't add any filters on post.php page.
		if ( ! isset( $_GET['post'] ) ) {
			add_filter( 'attachment_fields_to_edit', [$this, 'edit_attachment'], 10, 2 );
			add_filter( 'attachment_fields_to_save', [$this, 'save_attachment'], 10 );
		}
	}

	/**
	 * Add attachment fields.
	 *
	 * @param  array   $form_fields
	 * @param  WP_Post $post
	 *
	 * @return array
	 */
	public function edit_attachment( $form_fields, $post ) {
		foreach ( $this->get_boxes() as $box ) {
			if ( ! empty( $box->title ) ) {
				$form_fields['papi-media-title-' . uniqid()] = [
					'label' => '',
					'input' => 'html',
					'html'  => '
' . $box->title . '
'
				];
			}

			foreach ( $box->properties as $prop ) {
				// Raw output is required.
				$prop->raw = true;

				// Set post id to the property.
				$prop->set_post_id( $post->ID );

				// Add property to form fields.
				$form_fields[$prop->get_slug()] = [
					'label' => $prop->title,
					'input' => 'html',
					'helps' => $prop->description,
					'html'  => papi_maybe_get_callable_value(
						'papi_render_property',
						$prop
					)
				];
			}
		}

		// Add nonce field.
		$form_fields['papi_meta_nonce'] = [
			'label' => '',
			'input' => 'html',
			'html'  => sprintf(
				'<input name="papi_meta_nonce" type="hidden" value="%s" />',
				wp_create_nonce( 'papi_save_data' )
			)
		];

		return $form_fields;
	}

	/**
	 * Save attachment post data.
	 *
	 * @param  array $post
	 *
	 * @return array
	 */
	public function save_attachment( $post ) {
		update_post_meta( $post['ID'], papi_get_page_type_key(), $this->get_id() );

		$handler = new Papi_Admin_Meta_Handler();
		$handler->save_meta_boxes( $post['ID'], $post );

		return $post;
	}

	/**
	 * Check if the entry type is a singleton.
	 *
	 * @return bool
	 */
	public function singleton() {
		$key = sprintf( 'entry_type_id.post_type.%s', $this->get_post_type() );

		if ( papi()->exists( $key ) ) {
			return true;
		}

		papi()->singleton( $key, $this->get_id() );

		return false;
	}
}


<?php

/**
 * Core class that implements a Papi meta store.
 */
abstract class Papi_Core_Meta_Store {

	/**
	 * The WordPress meta id if it exists.
	 *
	 * @var int
	 */
	public $id;

	/**
	 * Current properties.
	 *
	 * @var array
	 */
	protected $properties = [];

	/**
	 * Properties meta values.
	 *
	 * @var array
	 */
	protected $meta_values = [];

	/**
	 * The meta type.
	 *
	 * @var string
	 */
	protected $type = 'meta';

	/**
	 * The type class.
	 *
	 * @var Papi_Core_Type
	 */
	protected $type_class;

	/**
	 * Get property meta value.
	 *
	 * @param  string $slug
	 *
	 * @return mixed
	 */
	public function get_property_meta_value( $slug ) {
		$slug = strtolower( $slug );

		if ( isset( $this->meta_values[$slug] ) ) {
			return $this->meta_values[$slug];
		}

		return papi_data_get( $this->id, $slug, $this->get_type() );
	}

	/**
	 * Get meta type.
	 *
	 * @return string
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Get type class.
	 *
	 * @return Papi_Core_Type
	 */
	public function get_type_class() {
		return $this->type_class;
	}

	/**
	 * Get value.
	 *
	 * @param  int    $id
	 * @param  string $slug
	 * @param  mixed  $default
	 * @param  string $type
	 *
	 * @return mixed
	 */
	public function get_value( $id = null, $slug = null, $default = null, $type = 'post' ) {
		if ( ! is_numeric( $id ) && is_string( $id ) ) {
			$type    = empty( $default ) ? $type : $default;
			$default = $slug;
			$slug    = $id;
			$id      = null;
		}

		$slug = strtolower( $slug );

		// Determine if we should use the cache or not.
		$cache = $this->get_property_option( $slug, 'cache', true );

		// Get the raw value from the cache.
		$raw_value = $cache ? papi_cache_get( $slug, $id, $type ) : false;

		// Load raw value if not cached.
		if ( $raw_value === null || $raw_value === false ) {
			$raw_value = $this->load_value( $slug );

			if ( papi_is_empty( $raw_value ) ) {
				return $default;
			}

			if ( $cache ) {
				papi_cache_set( $slug, $id, $raw_value, $type );
			} else {
				papi_cache_delete( $slug, $id, $type );
			}
		}

		if ( papi_is_empty( $raw_value ) ) {
			return $default;
		}

		// Format raw value.
		$value = $this->format_value( $slug, $raw_value );

		return papi_is_empty( $value ) ? $default : $value;
	}

	/**
	 * Format property value from the property.
	 *
	 * @param  string $slug
	 * @param  mixed  $value
	 *
	 * @return mixed
	 */
	public function format_value( $slug, $value ) {
		$slug     = strtolower( unpapify( $slug ) );
		$property = $this->property( $slug );

		// If no property type is found, just return null.
		if ( ! papi_is_property( $property ) ) {
			return;
		}

		// Format the value from the property class.
		$value = $property->format_value( $value, $slug, $this->id );

		// Only fired when not in admin.
		if ( ! papi_is_admin() ) {
			$value = papi_filter_format_value(
				$property->type,
				$value,
				$slug,
				$this->id,
				papi_get_meta_type()
			);
		}

		// Modify value before it's return to the theme via a defined format callback on the property.
		if ( ! papi_is_admin() && is_callable( $property->format_cb ) ) {
			$value = call_user_func( $property->format_cb, $value );
		}

		// Remove empty values from arrays.
		if ( is_array( $value ) ) {
			foreach ( $value as $index => $val ) {
				if ( papi_is_empty( $val ) ) {
					unset( $value[$index] );
				}
			}
		}

		return $value;
	}

	/**
	 * Get current property.
	 *
	 * @param  string $slug
	 *
	 * @return Papi_Core_Property
	 */
	protected function property( $slug = '' ) {
		if ( isset( $this->properties[$slug] ) && papi_is_property( $this->properties[$slug] ) ) {
			$property = $this->properties[$slug];
		} else {
			$property = $this->properties[$slug] = $this->get_property( $slug );
		}

		/**
		 * Modify property.
		 *
		 * @param  Papi_Core_Property $property
		 */
		return apply_filters( 'papi/get_property', $property );
	}

	/**
	 * Load property value from the property.
	 *
	 * @param  string $slug
	 *
	 * @return mixed
	 */
	public function load_value( $slug ) {
		$slug     = strtolower( unpapify( $slug ) );
		$property = $this->property( $slug );

		// If no property type is found, just return null.
		if ( ! papi_is_property( $property ) ) {
			return;
		}

		// Get raw property meta value.
		$value = $this->get_property_meta_value( $slug );

		// Prepare load value, when you have `overwrite => true`
		// this value will not exist in the database and that's
		// why we need to prepare (change) the value.
		$value = $this->prepare_load_value( $property, $value );

		// Bail if value is empty and option value is empty.
		if ( papi_is_empty( $value ) ) {
			if ( ! papi_is_empty( $property->get_option( 'value' ) ) ) {
				return $property->get_option( 'value' );
			}

			return;
		}

		// A property need to know about the store.
		$this->property( $slug )->set_store( $this );

		// Run load value method right after the value has been loaded from the database.
		$value = $property->load_value( $value, $slug, $this->id );
		$value = papi_filter_load_value(
			$property->type,
			$value,
			$slug,
			$this->id,
			papi_get_meta_type()
		);

		// Remove empty values from arrays.
		if ( is_array( $value ) ) {
			foreach ( $value as $index => $val ) {
				if ( papi_is_empty( $val ) ) {
					unset( $value[$index] );
				}
			}
		}

		return $value;
	}

	/**
	 * Get store from factory.
	 *
	 * @param  int    $post_id
	 * @param  string $type
	 *
	 * @return mixed
	 */
	public static function factory( $post_id, $type = 'post' ) {
		$type         = papi_get_meta_type( $type );
		$class_suffix = '_' . ucfirst( $type ) . '_Store';
		$class_name   = 'Papi' . $class_suffix;

		if ( ! class_exists( $class_name ) ) {
			return;
		}

		$post_id = papi_get_post_id( $post_id );
		$page    = new $class_name( $post_id );

		if ( ! $page->valid() ) {
			return;
		}

		return $page;
	}

	/**
	 * Get property from entry type.
	 *
	 * @param  string $slug
	 * @param  string $child_slug
	 *
	 * @return Papi_Core_Property
	 */
	abstract public function get_property( $slug, $child_slug = '' );

	/**
	 * Get property option or default value.
	 *
	 * @param  string $slug
	 * @param  string $option
	 * @param  mixed  $default
	 *
	 * @return bool
	 */
	public function get_property_option( $slug, $option, $default = null ) {
		$slug     = unpapify( $slug );
		$property = $this->property( $slug );

		// If no property type is found, return default
		// value since we don't have a property.
		if ( ! papi_is_property( $property ) ) {
			return $default;
		}

		$value = $property->get_option( $option );

		if ( papi_is_empty( $value ) ) {
			return $default;
		}

		return $value;
	}

	/**
	 * Prepare load value.
	 *
	 * @param  Papi_Core_Property $property
	 * @param  mixed              $value
	 *
	 * @return mixed
	 */
	protected function prepare_load_value( Papi_Core_Property $property, $value ) {
		return $value;
	}

	/**
	 * Prepare property before returning it.
	 *
	 * @param  Papi_Core_Property $property
	 *
	 * @return Papi_Core_Property
	 */
	protected function prepare_property( Papi_Core_Property $property ) {
		$property->set_store( $this );

		return $property;
	}

	/**
	 * Set property meta value.
	 *
	 * @param string $slug
	 * @param mixed  $value
	 */
	public function set_property_meta_value( $slug, $value ) {
		$this->meta_values[strtolower( $slug )] = $value;
	}

	/**
	 * Check if it's a valid store.
	 *
	 * @return bool
	 */
	abstract public function valid();
}


<?php

/**
 * Divider property.
 */
class Papi_Property_Divider extends Papi_Property {

	/**
	 * Determine if the property require a slug or not.
	 *
	 * @var bool
	 */
	protected $slug_required = false;

	/**
	 * Render property html.
	 */
	public function html() {
		$options = $this->get_options();
		$title   = '';
		$text    = '';

		if ( ! papi_is_empty( $options->title ) ) {
			$title = sprintf( '
%s
', esc_html( $options->title ) );
		}

		if ( ! papi_is_empty( $options->description ) ) {
			$text = sprintf( '
%s
', $options->description );
		}

		papi_render_html_tag( 'div', [
			'class'          => 'papi-property-divider',
			'data-papi-rule' => esc_attr( $this->html_name() ),
			sprintf( '%s%s', $title, $text )
		] );
	}

	/**
	 * Render the final html that is displayed in the table.
	 */
	protected function render_row_html() {
		if ( $this->get_option( 'raw' ) ) {
			parent::render_row_html();
		} else {
			?>
			<tr class="<?php echo $this->display ? '' : 'papi-hide'; ?>">

					<?php $this->render_property_html(); ?>

			<?php
		}
	}
}


<?php

/**
 * Base property that implements the rendering of
 * a property in WordPress admin.
 */
class Papi_Property extends Papi_Core_Property {

	/**
	 * Get value.
	 *
	 * @return mixed
	 */
	public function get_value() {
		$value = $this->get_option( 'value' );

		if ( papi_is_empty( $value ) ) {
			$type        = papi_get_meta_type();
			$slug        = $this->get_slug( true );
			$value       = papi_get_field( $slug, null, $type );
			$post_status = get_post_status( $this->get_post_id() );

			if ( papi_is_empty( $value ) && ( $post_status === false || $post_status === 'auto-draft' ) ) {
				$value = $this->get_option( 'default' );
			}
		}

		if ( papi_is_empty( $value ) ) {
			return $this->default_value;
		}

		if ( $this->convert_type === 'string' ) {
			$value = papi_convert_to_string( $value );
		}

		return papi_santize_data( $value );
	}

	/**
	 * Render property html.
	 */
	public function html() {
	}

	/**
	 * Determine if the property can be rendered or not.
	 *
	 * @return bool
	 */
	public function can_render() {
		// Check if current user can view the property.
		if ( ! $this->current_user_can() ) {
			return false;
		}

		// A disabled property should not be rendered.
		if ( $this->disabled() ) {
			return false;
		}

		// Check language option, so we don't render properties on a different language.
		if ( $lang = $this->get_option( 'lang' ) ) {
			return in_array( papi_get_lang(), papi_to_array( $lang ), true );
		}

		// If no valid lang query string exists we have to override the display property.
		return $this->get_option( 'lang' ) === false;
	}

	/**
	 * Render the property.
	 */
	public function render() {
		// Bail if we can't render the property.
		if ( ! $this->can_render() ) {
			return;
		}

		// Override display with rules check.
		if ( $this->display() ) {
			$this->display = $this->render_is_allowed_by_rules();
		}

		// Render property.
		$this->render_row_html();
	}

	/**
	 * Render AJAX request.
	 */
	public function render_ajax_request() {
		papi_render_property( $this );
	}

	/**
	 * Render the property description.
	 */
	protected function render_description_html() {
		if ( papi_is_empty( $this->get_option( 'description' ) ) ) {
			return;
		}

		papi_render_html_tag( 'p', [
			papi_nl2br( $this->get_option( 'description' ) )
		] );
	}

	/**
	 * Output hidden input field that cointains which property is used.
	 */
	protected function render_hidden_html() {
		$slug = $this->get_option( 'slug' );

		if ( substr( $slug, - 1 ) === ']' ) {
			$slug = substr( $slug, 0, - 1 );
			$slug = papi_get_property_type_key( $slug );
			$slug .= ']';
		} else {
			$slug = papi_get_property_type_key( $slug );
		}

		$slug          = papify( $slug );
		$options       = $this->get_options();
		$property_json = base64_encode( papi_maybe_json_encode( $options ) );

		papi_render_html_tag( 'input', [
			'data-property' => strtolower( $this->get_option( 'type' ) ),
			'name'          => $slug,
			'type'          => 'hidden',
			'value'         => $property_json
		] );
	}

	/**
	 * Render label for the property.
	 */
	protected function render_label_html() {
		$title = $this->get_option( 'title' );

		papi_render_html_tag( 'label', [
			'for'   => $this->html_id(),
			'title' => trim( $title . ' ' . papi_property_require_text( $this->get_options() ) ),
			$title,
			papi_property_required_html( $this )
		] );
	}

	/**
	 * Render property html.
	 */
	protected function render_property_html() {
		papi_render_html_tag( 'div', [
			'class'         => 'papi-before-html ' . $this->get_option( 'before_class' ),
			'data-property' => $this->get_option( 'type' ),
			papi_maybe_get_callable_value( $this->get_option( 'before_html' ) )
		] );

		$this->html();

		papi_render_html_tag( 'div', [
			'class'         => 'papi-after-html ' . $this->get_option( 'after_class' ),
			'data-property' => $this->get_option( 'type' ),
			papi_maybe_get_callable_value( $this->get_option( 'after_html' ) )
		] );
	}

	/**
	 * Render the final html that is displayed in the table.
	 */
	protected function render_row_html() {
		$display_class = $this->display() ? '' : ' papi-hide';
		$rules_class   = papi_is_empty( $this->get_rules() ) ? '' : ' papi-rules-exists';
		$css_class     = trim( $display_class . $rules_class );
		$css_class    .= sprintf( ' papi-layout-%s', $this->get_option( 'layout' ) );

		if ( $this->get_option( 'raw' ) ) {
			echo sprintf( '
', esc_attr( $css_class ) );
			$this->render_property_html();
			$this->render_hidden_html();
			$this->render_rules_json();
			echo '
';
		} else {
			?>
			<tr class="<?php echo esc_attr( $css_class ); ?>">
				<?php if ( $this->get_option( 'sidebar' ) && $this->get_option( 'layout' ) === 'horizontal' ): ?>

						<?php
							$this->render_label_html();
							$this->render_description_html();
						?>

				<?php endif; ?>
				<td <?php echo $this->get_option( 'sidebar' ) && $this->get_option( 'layout' ) === 'horizontal' ? '' : 'colspan="2"'; ?>>
					<?php
					// Render vertical layout where title and description is above the property.
					if ( $this->get_option( 'layout' ) === 'vertical' && $this->get_option( 'sidebar' ) ) {
						$this->render_label_html();
						$this->render_description_html();
					}

					$this->render_property_html();
					$this->render_hidden_html();
					$this->render_rules_json();
					?>

			<?php
		}
	}

	/**
	 * Render Conditional rules as script tag with JSON.
	 */
	protected function render_rules_json() {
		$rules = $this->get_rules();

		if ( empty( $rules ) ) {
			return;
		}

		$rules = $this->conditional->prepare_rules( $rules, $this );

		papi_render_html_tag( 'script', [
			'data-papi-rule-source-slug' => $this->html_name(),
			'data-papi-rules'            => 'true',
			'type'                       => 'application/json',
			papi_maybe_json_encode( $rules )
		] );
	}
}


<?php

/**
 * HTML text input property.
 */
class Papi_Property_String extends Papi_Property {

	/**
	 * The input type to use.
	 *
	 * @var string
	 */
	public $input_type = 'text';

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( ! $this->get_setting( 'allow_html' ) && $this->input_type === 'text' ) {
			$value = papi_maybe_json_decode( maybe_unserialize( $value ) );

			if ( ! is_string( $value ) ) {
				$value = '';
			}

			$value = wp_strip_all_tags( $value );
		}

		return $value;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'allow_html'  => false,
			'placeholder' => ''
		];
	}

	/**
	 * Get value from the database.
	 *
	 * @return string
	 */
	public function get_value() {
		$value = $this->format_value(
			parent::get_value(),
			$this->get_slug(),
			papi_get_post_id()
		);

		return $this->get_setting( 'allow_html' ) ? $value : esc_html( $value );
	}

	/**
	 * Render property html.
	 */
	public function html() {
		papi_render_html_tag( 'input', [
			'id'          => esc_attr( $this->html_id() ),
			'name'        => esc_attr( $this->html_name() ),
			'type'        => esc_attr( $this->input_type ),
			'value'       => $this->get_value(),
			'placeholder' => esc_attr( $this->get_setting( 'placeholder' ) )
		] );
	}
}


<?php

/**
 * Core class that handle core post data.
 */
class Papi_Core_Data_Handler {

	/**
	 * The fields that should be overwritten.
	 *
	 * @var array
	 */
	protected $overwrite = [];

	/**
	 * The constructor.
	 */
	public function __construct() {
		$this->setup_actions();
	}

	/**
	 * Decode property.
	 *
	 * @param string $key
	 * @param string $value
	 *
	 * @return mixed
	 */
	protected function decode_property( $key, $value ) {
		if ( papi_is_property_type_key( $key ) && is_string( $value ) ) {
			$value = base64_decode( $value );
			$value = papi_maybe_json_decode( $value );
		}

		return $value;
	}

	/**
	 * Get post data.
	 *
	 * @param  string $pattern
	 *
	 * @return array
	 */
	protected function get_post_data( $pattern = '/^papi\_.*/' ) {
		$data = [];
		$keys = preg_grep( $pattern, array_keys( $_POST ) );

		foreach ( $keys as $key ) {
			// Remove page type keys with suffix. This should not be saved.
			if (
				strpos( $key, papi_get_page_type_key() ) === 0
				&&
				strlen( papi_get_page_type_key() ) !== strlen( $key )
			) {
				continue;
			}

			// Fix for input fields that should be true on `on` value.
			if ( $_POST[$key] === 'on' ) {
				$data[$key] = true;
			} else {
				$value = $this->decode_property( $key, $_POST[$key] );
				$data[$key] = $this->prepare_post_data( $value );
				$data[$key] = $this->santize_data( $data[$key] );
			}
		}

		// Don't wont to save meta nonce field.
		if ( isset( $data['papi_meta_nonce'] ) ) {
			unset( $data['papi_meta_nonce'] );
		}

		return $data;
	}

	/**
	 * Get pre data that should be saved before all properties data.
	 */
	protected function get_pre_data() {
		return $this->get_post_data( '/^\_papi\_.*/' );
	}

	/**
	 * Pre get deep keys and value.
	 *
	 * Used for saving pre data when properties are in a flexible or repeater.
	 *
	 * @param  array $arr
	 *
	 * @return array
	 */
	protected function get_pre_deep_keys_value( array $arr ) {
		$keys  = [];
		$value = null;

		foreach ( $arr as $key => $v ) {
			if ( is_array( $v ) ) {
				$keys[] = $key;
				list( $ks, $val ) = $this->get_pre_deep_keys_value( $v );
				$keys   = array_merge( $keys, $ks );
				$value  = $val;
			} else {
				$keys[] = $key;
				$value  = $v;
			}
		}

		return [$keys, $value];
	}

	/**
	 * Prepare post data.
	 * Will decode property options recursive.
	 *
	 * @param  mixed $data
	 *
	 * @return mixed
	 */
	protected function prepare_post_data( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		foreach ( $data as $key => $value ) {
			if ( is_array( $value ) ) {
				$data[$key] = $this->prepare_post_data( $value );
			} else {
				$data[$key] = $this->decode_property( $key, $value );
			}
		}

		return $data;
	}

	/**
	 * Prepare properties data for saving.
	 *
	 * @param  array $data
	 * @param  int   $post_id
	 *
	 * @return array
	 */
	protected function prepare_properties_data( array $data = [], $post_id = 0 ) {
		// Since we are storing witch property it is in the `$data` array
		// we need to remove that and set the property type to the property
		// and make a array of the property type and the value.
		foreach ( $data as $key => $value ) {
			if ( papi_is_property_type_key( $key ) ) {
				continue;
			}

			$property_type_key = papify( papi_get_property_type_key( $key ) );

			// Check if value exists.
			if ( ! isset( $data[$key] ) || ! isset( $data[$property_type_key] ) ) {
				continue;
			}

			// Pair property value with property type object.
			$data[$key] = [
				'type'  => $data[$property_type_key],
				'value' => $value
			];

			// Remove property type object since it's not needed anymore.
			unset( $data[$property_type_key] );
		}

		foreach ( $data as $key => $item ) {
			if ( papi_is_property_type_key( $key ) ) {
				if ( isset( $data[$key] ) ) {
						unset( $data[$key] );
				}
				continue;
			}

			if ( empty( $item['type'] ) ) {
				continue;
			}

			$property = papi_get_property_type( $item['type'] );

			unset( $data[ $key ] );

			if ( papi_is_property( $property ) ) {
				// Run `update_value` method on the property class.
				$data[$key] = $property->update_value(
					$item['value'],
					unpapify( $key ),
					$post_id
				);

				// Apply `update_value` filter so this can be changed from
				// the theme for specified property type.
				$data[$key] = papi_filter_update_value(
					$item['type']->type,
					$data[$key],
					unpapify( $key ),
					$post_id,
					papi_get_meta_type()
				);

				if ( $item['type']->overwrite ) {
					$slug = unpapify( $key );
					$this->overwrite[$slug] = $data[$key];
					unset( $data[$key] );
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitize data before saving it.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	protected function santize_data( $value ) {
		if ( is_array( $value ) ) {
			foreach ( $value as $k => $v ) {
				if ( is_string( $v ) ) {
					$value[$k] = $this->santize_data( $v );
				}
			}
		} else if ( is_string( $value ) ) {
			$value = wp_unslash( $value );
		}

		return $value;
	}

	/**
	 * Setup actions.
	 *
	 * @codeCoverageIgnore
	 */
	protected function setup_actions() {
	}
}


<?php

class Papi_Core_Data {

	/**
	 * Does the meta use id?
	 *
	 * @var bool
	 */
	protected $id = false;

	/**
	 * Meta type.
	 *
	 * @var
	 */
	protected $type;

	/**
	 * Property data constructor.
	 *
	 * @param $type
	 */
	public function __construct( $type = 'post' ) {
		$this->type = is_string( $type ) ? papi_get_meta_type( $type ) : 'post';
		$this->id   = in_array( $this->type, ['post', 'term'], true );
	}

	/**
	 * Delete property value.
	 *
	 * @param  int    $id
	 * @param  string $slug
	 *
	 * @return bool
	 */
	public function delete( $id, $slug ) {
		$fn = $this->get_function( 'delete' );

		// Check so the function is callable before using it.
		if ( ! is_callable( $fn ) ) {
			return false;
		}

		// Delete cached value.
		papi_cache_delete( $slug, $id, $this->type );

		if ( $this->id ) {
			return call_user_func_array( $fn, [$this->type, $id, unpapify( $slug )] );
		}

		return call_user_func_array( $fn, [unpapify( $slug )] );
	}

	/**
	 * Get right meta function for right type and context.
	 *
	 * @param  string $context
	 *
	 * @return string
	 */
	public function get_function( $context = 'get' ) {
		switch ( $this->type ) {
			case 'option':
				return sprintf( '%s_option', $context );
			case 'site':
			case 'network':
				return sprintf( '%s_site_option', $context );
			case 'post':
			case 'term':
				return sprintf( '%s_metadata', $context );
			default:
				break;
		}
	}

	/**
	 * Geta property value for right meta type.
	 *
	 * @param  int    $id
	 * @param  string $slug
	 *
	 * @return mixed
	 */
	public function get( $id, $slug ) {
		$fn = $this->get_function( 'get' );

		if ( ! is_callable( $fn ) ) {
			return;
		}

		if ( $this->id ) {
			$value = call_user_func_array( $fn, [$this->type, $id, unpapify( $slug ), true] );
		} else {
			$value = call_user_func_array( $fn, [unpapify( $slug ), null] );
		}

		if ( papi_is_empty( $value ) ) {
			return;
		}

		return $value;
	}

	/**
	 * Update property meta.
	 *
	 * @param  int    $id
	 * @param  string $slug
	 * @param  mixed  $value
	 *
	 * @return bool
	 */
	public function update( $id, $slug, $value ) {
		$save_value = true;

		// Get right update function to use.
		$fn = $this->get_function( 'update' );

		if ( ! is_callable( $fn ) ) {
			return false;
		}

		// Check for string keys in the array if any.
		foreach ( array_keys( papi_to_array( $value ) ) as $key ) {
			if ( is_string( $key ) ) {
				$save_value = false;
				break;
			}
		}

		// If main value shouldn't be saved it should be array.
		if ( ! $save_value && is_array( $value ) ) {
			$value = [$value];
		}

		// Delete saved value if empty.
		if ( papi_is_empty( $value ) ) {
			return $this->delete( $id, $slug );
		}

		$result = true;

		foreach ( papi_to_array( $value ) as $key => $val ) {
			// Delete saved value if value is empty.
			if ( papi_is_empty( $val ) || $val === '[]' || $val === '{}' ) {
				return $this->delete( $id, $slug );
			}

			// Delete main value cache.
			papi_cache_delete( $slug, $id, $this->type );

			// If not a array we can save the value.
			if ( ! is_array( $val ) ) {
				if ( $save_value ) {
					$val = $value;
				}

				if ( $this->id ) {
					$out    = call_user_func_array( $fn, [$this->type, $id, unpapify( $slug ), $val] );
					$result = $out ? $result : $out;
				} else {
					$out    = call_user_func_array( $fn, [unpapify( $slug ), $val] );
					$result = $out ? $result : $out;
				}

				continue;
			}

			// Clear cache for all child values.
			$this->update_clear_cache( $id, $value );

			// Update metadata or option value for all child values.
			foreach ( $val as $child_key => $child_value ) {
				if ( papi_is_empty( $child_value ) ) {
					$this->delete( $id, $child_key );
				} else {
					if ( $this->id ) {
						call_user_func_array( $fn, [$this->type, $id, unpapify( $child_key ), $child_value] );
					} else {
						call_user_func_array( $fn, [unpapify( $child_key ), $child_value] );
					}
				}
			}
		}

		return $result;
	}

	/**
	 * Clear cache values on update property meta.
	 *
	 * @param  int   $id
	 * @param  mixed $value
	 */
	public function update_clear_cache( $id, $value ) {
		$value = is_array( $value ) ? $value : [];

		foreach ( $value as $child_key => $child_value ) {
			papi_cache_delete( $child_key, $id, $this->type );

			if ( is_array( $child_value ) ) {
				$this->update_clear_cache( $id, $child_value );
			}
		}
	}
}


<?php

class Papi_Admin_Entry_Post extends Papi_Admin_Entry {

	/**
	 * The post type.
	 *
	 * @var string
	 */
	protected $post_type;

	/**
	 * The construct.
	 */
	public function __construct() {
		$this->post_type = papi_get_post_type();

		$this->setup_actions();
		$this->setup_filters();
	}

	/**
	 * Filters the fields displayed in the post revision diff UI.
	 *
	 * @param  array   $return
	 * @param  WP_Post $compare_from
	 * @param  WP_Post $compare_to
	 *
	 * @return array
	 */
	public function get_revision_ui_diff( $return, $compare_from, $compare_to ) {
		$meta = get_post_meta( $compare_from->ID );
		$meta = is_array( $meta ) ? $meta : [];

		foreach ( $meta as $key => $value ) {
			if ( $key[0] === '_' && $key !== papi_get_page_type_key() ) {
				continue;
			}

			$content_from = papi_data_get( $compare_from->ID, $key );
			$content_to = papi_data_get( $compare_to->ID, $key );

			$diff = wp_text_diff( $content_from, $content_to, [
				'show_split_view' => true
			] );

			if ( $diff ) {
				$return[] = [
					'id'   => $key,
					'name' => $key,
					'diff' => $diff,
				];
			}
		}

		return $return;
	}

	/**
	 * Output hidden meta boxes.
	 */
	public function hidden_meta_boxes() {
		global $_wp_post_type_features;

		if ( isset( $_wp_post_type_features[$this->post_type]['editor'] ) ) {
			return;
		}

		add_meta_box( 'papi-hidden-editor', 'Papi hidden editor', [$this, 'hidden_meta_box_editor'], $this->post_type );
	}

	/**
	 * Output hidden WordPress editor.
	 */
	public function hidden_meta_box_editor() {
		wp_editor( '', 'papiHiddenEditor' );
	}

	/**
	 * Load post new action
	 * Redirect to right url if no page type is set.
	 */
	public function load_post_new() {
		$request_uri = $_SERVER['REQUEST_URI'];
		$post_types  = papi_get_post_types();

		if ( in_array( $this->post_type, $post_types, true ) && strpos( $request_uri, 'page_type=' ) === false ) {
			$parsed_url = parse_url( $request_uri );

			$only_page_type = papi_filter_settings_only_page_type( $this->post_type );
			$page_types     = papi_get_all_page_types( $this->post_type );
			$show_standard  = false;

			if ( count( $page_types ) === 1 && empty( $only_page_type ) ) {
				$show_standard  = $page_types[0]->standard_type;
				$show_standard  = $show_standard ? papi_filter_settings_show_standard_page_type( $this->post_type ) : $show_standard;
				$only_page_type = $show_standard ? '' : $page_types[0]->get_id();
			}

			// Check if we should show one post type or not and
			// create the right url for that.
			if ( ! empty( $only_page_type ) && ! $show_standard ) {
				$url = papi_get_page_new_url( $only_page_type, false );
			} else {
				$page = 'page=papi-add-new-page,' . $this->post_type;

				if ( $this->post_type !== 'post' ) {
					$page = '&' . $page;
				}

				$url = 'edit.php?' . $parsed_url['query'] . $page;
			}

			wp_safe_redirect( $url );
			papi_is_admin() && exit;
		}
	}

	/**
	 * Redirect post location when post is in iframe mode.
	 *
	 * @param  string $location
	 *
	 * @return string
	 */
	public function redirect_post_location( $location ) {
		if ( ! isset( $_SERVER['HTTP_REFERER'] ) ) {
			return $location;
		}

		$referer = $_SERVER['HTTP_REFERER'];
		$referer = strtolower( $referer );

		if ( strpos( $referer, 'papi-iframe-mode' ) === false ) {
			return $location;
		}

		return sprintf( '%s&papi_css[]=papi-iframe-mode', $location );
	}

	/**
	 * Setup admin entry.
	 */
	public function setup() {
		// Preload all page types.
		foreach ( papi_get_post_types() as $post_type ) {
			papi_get_all_entry_types( [
				'args' => $post_type
			] );
		}

		return ! in_array( $this->post_type, ['revision', 'nav_menu_item'], true );
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'load-post-new.php', [$this, 'load_post_new'] );
		add_action( 'add_meta_boxes', [$this, 'hidden_meta_boxes'], 10 );
		add_action( 'redirect_post_location', [$this, 'redirect_post_location'] );
	}

	/**
	 * Setup filters.
	 */
	protected function setup_filters() {
		add_filter( 'wp_get_revision_ui_diff', [$this, 'get_revision_ui_diff'], 10, 3 );
	}
}

if ( papi_is_admin() ) {
	Papi_Admin_Entry_Post::instance();
}


<?php

/**
 * Manage posts.
 */
class Papi_CLI_Post_Command extends Papi_CLI_Command {

	/**
	 * Get default fields for formatter.
	 *
	 * @return array
	 */
	protected function get_default_format_fields() {
		return ['slug', 'type', 'has value', 'box'];
	}

	/**
	 * Get fields that exists on a post.
	 *
	 * ## OPTIONS
	 *
	 * <post>
	 * : Post ID
	 *
	 * [--field=<field>]
	 * : Instead of returning the whole post fields, returns the value of a single fields.
	 *
	 * [--fields=<fields>]
	 * : Get a specific subset of the post's fields.
	 *
	 * [--format=<format>]
	 * : Accepted values: table, json, csv. Default: table.
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields are available for get command:
	 *
	 * * slug
	 * * type
	 * * exists
	 *
	 * ## EXAMPLES
	 *
	 *     wp papi post get 123 --format=json
	 *
	 *     wp papi post get 123 --field=slug
	 *
	 * @param  array $args
	 * @param  array $assoc_args
	 */
	public function get( $args, $assoc_args ) {
		try {
			// Set query string that we need.
			$_GET['post'] = $args[0];

			// Get the page type that the post has.
			$entry_type = papi_get_entry_type_by_meta_id( $args[0] );

			if ( empty( $entry_type ) || $entry_type instanceof Papi_Page_Type === false ) {
				WP_CLI::error( 'No page type exists on the post' );
			}

			$properties = [];

			foreach ( $entry_type->get_boxes() as $box ) {
				foreach ( $box->properties as $property ) {
					$properties[] = [
						'slug'      => $property->get_slug( true ),
						'type'      => $property->type,
						'has value' => $property->get_value() !== null ? 'true' : 'false',
						'box'       => $box->title
					];
				}
			}

			// Render types as a table.
			$formatter = $this->get_formatter( $assoc_args );
			$formatter->display_items( $properties );
		} catch ( WC_CLI_Exception $e ) {
			WP_CLI::error( $e->getMessage() );
		}
	}

	/**
	 * Rename meta key for page type.
	 *
	 * ## OPTIONS
	 *
	 * <page_type>
	 * : Page type id
	 *
	 * <old_key>
	 * : Old meta key
	 *
	 * <new_key>
	 * : New meta key
	 *
	 * ## EXAMPLES
	 *
	 *     wp papi post rename about-page-type name title
	 *
	 * @param  array $args
	 * @param  array $assoc_args
	 */
	public function rename( $args, $assoc_args ) {
		$type    = $args[0];
		$old_key = $args[1];
		$new_key = $args[2];

		$posts = ( new Papi_Query( [
			'entry_type' => $type,
			'fields'     => 'ids'
		] ) )->get_result();

		if ( empty( $posts ) ) {
			WP_CLI::error( 'No posts found' );
		}

		foreach ( $posts as $post ) {
			$meta = get_post_meta( $post, $old_key, true );

			if ( papi_is_empty( $meta ) ) {
				continue;
			}

			if ( delete_post_meta( $post, $old_key ) === false ) {
				WP_CLI::error( 'Could not delete post meta with key: ' . $old_key );

			}

			if ( update_post_meta( $post, $new_key, $meta ) === false ) {
				WP_CLI::error( 'Could not update post meta with key: ' . $new_key );
			}
		}

		WP_CLI::success( 'Done' );
	}
}


<?php

/**
 * Core class that implements a conditional rule.
 */
class Papi_Core_Conditional_Rule {

	/**
	 * The operator.
	 *
	 * @var string
	 */
	public $operator = '=';

	/**
	 * The slug.
	 *
	 * @var string
	 */
	public $slug;

	/**
	 * The source value.
	 *
	 * @var mixed
	 */
	public $source;

	/**
	 * The value.
	 *
	 * @var mixed
	 */
	public $value;

	/**
	 * The constructor.
	 *
	 * @param array $rule
	 */
	public function __construct( array $rule ) {
		$this->setup( $rule );
	}

	/**
	 * Get field slug.
	 *
	 * @return string
	 */
	public function get_field_slug() {
		if ( preg_match( '/\[|\]/', $this->slug ) ) {
			$slug = preg_replace( '/\[|\]/', '.', $this->slug );
			$slug = str_replace( '..', '.', $slug );
			return substr( $slug, 0, -1 );
		}

		return $this->slug;
	}

	/**
	 * Get the source value.
	 *
	 * @return mixed
	 */
	public function get_source() {
		if ( is_callable( $this->source ) ) {
			return call_user_func_array( $this->source, [$this->slug] );
		}

		if ( is_string( $this->source ) && strpos( $this->source, '#' ) !== false ) {
			$source = explode( '#', $this->source );

			if ( empty( $source[0] ) || empty( $source[1] ) ) {
				return $this->source;
			}

			$source[0] = new $source[0]();

			if ( method_exists( $source[0], $source[1] ) ) {
				return call_user_func_array( $source, [$this->slug] );
			}

			return;
		}

		return $this->source;
	}

	/**
	 * Setup source callable.
	 *
	 * @param  string $value
	 *
	 * @return string
	 */
	public function setup_source( $value ) {
		if ( is_array( $value ) && count( $value ) === 2 && is_object( $value[0] ) && is_string( $value[1] ) ) {
			return sprintf( '%s#%s', get_class( $value[0] ), $value[1] );
		}

		if ( is_string( $value ) && is_callable( $value ) ) {
			return $value;
		}

		// No support for closure.
		if ( is_object( $value ) && $value instanceof Closure ) {
			return '';
		}

		return $value;
	}

	/**
	 * Setup the rule and assign properties with values.
	 *
	 * @param array  $rule
	 */
	protected function setup( array $rule ) {
		foreach ( $rule as $key => $value ) {
			if ( $key === 'operator' ) {
				$value = strtoupper( $value );
				$value = html_entity_decode( $value );
			} else if ( $key === 'slug' ) {
				$value = papify( $value );
			} else if ( $key === 'source' ) {
				$value = $this->setup_source( $value );
			}

			$this->$key = $value;
		}
	}
}


<?php

/**
 * Admin class that handle loading of Papi types,
 * columns and loading of posts.
 */
final class Papi_Admin {

	/**
	 * The entry type.
	 *
	 * @var Papi_Entry_Type
	 */
	protected $entry_type;

	/**
	 * The post type.
	 *
	 * @var string
	 */
	protected $meta_type;

	/**
	 * The construct.
	 */
	public function __construct() {
		$this->load_files();
		$this->setup_actions();
		$this->setup_filters();
	}

	/**
	 * Cloning is forbidden.
	 *
	 * @codeCoverageIgnore
	 */
	public function __clone() {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'papi' ), '3.0' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 *
	 * @codeCoverageIgnore
	 */
	public function __wakeup() {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'papi' ), '3.0' );
	}

	/**
	 * Preboot all types and setup the current type if any.
	 */
	public function admin_init() {
		$meta_type  = papi_get_meta_type();
		$meta_type  = ucfirst( $meta_type );
		$class_name = 'Papi_Admin_Entry_' . $meta_type;

		// A custom class is not required, e.g
		// options don't have one.
		if ( class_exists( $class_name ) ) {
			$class = call_user_func( [$class_name, 'instance'] );

			if ( ! $class->setup() ) {
				return;
			}
		}

		// Setup entry type.
		if ( $entry_type = $this->get_entry_type() ) {
			$entry_type->setup();
		}
	}

	/**
	 * Add custom body class when it's a page type.
	 *
	 * @param  string $classes
	 *
	 * @return string
	 */
	public function admin_body_class( $classes ) {
		if ( $entry_type = $this->get_entry_type() ) {
			$classes .= sprintf( ' papi-body papi-meta-type-%s', papi_get_meta_type() );

			// Add custom css classes from entry type.
			$arr = $entry_type->get_body_classes();
			$arr = is_string( $arr ) ? [ $arr ] : $arr;
			$arr = is_array( $arr ) ? $arr : [];

			$classes .= ' ' . implode( ' ', $arr );
		}

		// Add custom css classes from query string.
		if ( $css = papi_get_qs( 'papi_css' ) ) {
			$css = is_array( $css ) ? $css : [];
			$css = array_map( 'sanitize_text_field', $css );
			$classes .= ' ' . implode( ' ', $css );
		}

		return $classes;
	}

	/**
	 * Output Papi page type hidden field.
	 */
	public function edit_form_after_title() {
		wp_nonce_field( 'papi_save_data', 'papi_meta_nonce' );

		if ( $value = esc_attr( papi_get_entry_type_id() ) ) {
			papi_render_html_tag( 'input', [
				'data-papi-page-type-key' => true,
				'name'                    => esc_attr( papi_get_page_type_key() ),
				'type'                    => 'hidden',
				'value'                   => $value
			] );
		}
	}

	/**
	 * Get Entry Type instance.
	 *
	 * @return Papi_Entry_Type|false
	 */
	protected function get_entry_type() {
		if ( $this->entry_type instanceof Papi_Entry_Type ) {
			return $this->entry_type;
		}

		$entry_type_id = papi_get_entry_type_id();

		// If the entry type id is empty try to load
		// the entry type id from `page` query string.
		//
		// Example:
		//   /wp-admin/options-general.php?page=papi/option/site-option-type
		if ( empty( $entry_type_id ) ) {
			$entry_type_id = preg_replace( '/^papi\/\w+\//', '', papi_get_qs( 'page' ) );
		}

		// Use the default entry type id if empty.
		if ( empty( $entry_type_id ) ) {
			$entry_type_id = papi_get_entry_type_id();
		}

		// If no entry type id exists Papi can't setup a entry type.
		if ( empty( $entry_type_id ) ) {
			return false;
		}

		$entry_type = papi_get_entry_type_by_id( $entry_type_id );

		if ( $entry_type instanceof Papi_Entry_Type === false ) {
			return false;
		}

		$this->entry_type = $entry_type;

		return $entry_type;
	}

	/**
	 * Load admin files that are not loaded by the autoload.
	 */
	protected function load_files() {
		require_once __DIR__ . '/class-papi-admin-meta-handler.php';
		require_once __DIR__ . '/class-papi-admin-option-handler.php';
		require_once __DIR__ . '/class-papi-admin-entry-post.php';
		require_once __DIR__ . '/class-papi-admin-entry-taxonomy.php';
		require_once __DIR__ . '/class-papi-admin-columns.php';
		require_once __DIR__ . '/class-papi-admin-page-type-switcher.php';
	}

	/**
	 * Add docs links to plugin row meta.
	 *
	 * @param  array  $links
	 * @param  string $file
	 *
	 * @return array
	 */
	public function plugin_row_meta( array $links, $file ) {
		if ( $file === PAPI_PLUGIN_BASENAME ) {
			return array_merge( $links, [
				'docs' => '' . __( 'Docs', 'papi' ) . '',
			] );
		}

		return $links;
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_init', [$this, 'admin_init'] );
		add_action( 'edit_form_after_title', [$this, 'edit_form_after_title'] );

		if ( $taxonomy = papi_get_taxonomy() ) {
			add_action( $taxonomy . '_add_form', [$this, 'edit_form_after_title'] );
			add_action( $taxonomy . '_edit_form', [$this, 'edit_form_after_title'] );
		}
	}

	/**
	 * Setup filters.
	 */
	protected function setup_filters() {
		add_filter( 'admin_body_class', [$this, 'admin_body_class'] );
		add_filter( 'plugin_row_meta', [$this, 'plugin_row_meta'], 10, 2 );
		add_filter( 'wp_link_query', [$this, 'wp_link_query'] );
		add_filter( 'wp_refresh_nonces', [$this, 'wp_refresh_nonces'], 11 );
		add_filter( 'pre_update_option', [$this, 'update_front_page'], 10, 2 );
	}

	/**
	 * Update page type to front page type when updating
	 * which page that should be used as front page.
	 *
	 * @param  mixed  $value
	 * @param  string $option
	 *
	 * @return mixed
	 */
	public function update_front_page( $value, $option ) {
		if ( $option !== 'page_on_front' ) {
			return $value;
		}

		$front_pages = papi_get_all_entry_types( [
			'types' => ['front-page'],
		] );

		if ( count( $front_pages ) === 0 ) {
			return $value;
		}

		$old_page_type_id = papi_get_page_type_id( $value );
		$new_page_type_id = $front_pages[0]->get_id();

		// Delete all old fields from old page type if any.
		if ( strtolower( $old_page_type_id ) !== strtolower( $new_page_type_id ) ) {
			if ( $page_type = papi_get_entry_type_by_id( $old_page_type_id ) ) {
				$slugs = array_map( function( $property ) {
					return $property->get_slug( true );
				}, $page_type->get_properties() );

				foreach ( $slugs as $slug ) {
					papi_delete_field( $value, $slug );
				}
			}
		}

		papi_set_page_type_id( $value, $new_page_type_id );

		return $value;
	}

	/**
	 * Filter the link query results.
	 *
	 * @param  array $results
	 *
	 * @return array
	 */
	public function wp_link_query( array $results ) {
		foreach ( $results as $index => $value ) {
			$post_type = papi_get_post_type( $value['ID'] );
			$name      = papi_get_page_type_name( $value['ID'] );

			if ( empty( $name ) ) {
				$name = papi_filter_settings_standard_page_type_name( $post_type );
			}

			$results[$index]['info'] = esc_html( $name );
		}

		return $results;
	}

	/**
	 * Check nonce expiration on the New/Edit Post screen and refresh if needed.
	 *
	 * @param  array $response
	 *
	 * @return array
	 */
	public function wp_refresh_nonces( array $response ) {
		if ( ! array_key_exists( 'wp-refresh-post-nonces', $response ) ) {
			return $response;
		}

		$response['wp-refresh-post-nonces']['replace']['papi_meta_nonce'] = wp_create_nonce( 'papi_save_data' );

		return $response;
	}
}

if ( papi_is_admin() ) {
	new Papi_Admin;
}


<?php

/**
 * Module property that can handle relationship
 * between modules.
 */
class Papi_Property_Module extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'object';

	/**
	 * Default value.
	 *
	 * @var array
	 */
	public $default_value = [
		'module'   => null,
		'template' => null
	];

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( is_numeric( $value ) && intval( $value ) !== 0 ) {
			$value = [
				'module'   => get_post( $value ),
				'template' => papi_data_get( $post_id, sprintf( '%s_template', unpapify( $this->html_name() ) ) )
			];

			if ( papi_is_admin() ) {
				return (object) $value;
			}

			// Return the template value instead of index when not in admin.
			if ( $value['module'] instanceof WP_Post ) {
				$templates = $this->get_templates( $value['module']->ID );

				// Check if index exists.
				if ( isset( $templates[$value['template']] ) ) {
					$value['template'] = $templates[$value['template']];
				}

				// Supports label and template array.
				if ( is_array( $value['template'] ) && isset( $value['template']['template'] ) ) {
					$value['template'] = $value['template']['template'];
				}
			}

			return (object) $value;
		}

		return (object) $this->default_value;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'edit_url'      => true,
			'labels'        => [
				'select_template' => __( 'Select Template', 'papi' ),
				'select_module'   => __( 'Select Module', 'papi' )
			],
			'new_url'       => true,
			'placeholder'   => '',
			'post_type'     => 'module',
			'select2'       => true,
			'query'         => [],
		];
	}

	/**
	 * Get post types.
	 *
	 * @return array
	 */
	protected function get_post_type() {
		$arr = papi_to_array( $this->get_setting( 'post_type' ) );
		return array_shift( $arr );
	}

	/**
	 * Get posts.
	 *
	 * @param  mixed $post_type
	 *
	 * @return array
	 */
	protected function get_posts( $post_type = null ) {
		$query = $this->get_setting( 'query' );

		// By default we add posts per page key with the value -1 (all).
		if ( ! isset( $query['posts_per_page'] ) ) {
			$query['posts_per_page'] = -1;
		}

		// Prepare arguments for WP_Query.
		$args = array_merge( $query, [
			'post_status'            => 'any',
			'post_type'              => $post_type,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false
		] );

		return ( new WP_Query( $args ) )->posts;
	}

	/**
	 * Get module templates by post id.
	 *
	 * @param  int $id
	 *
	 * @return array
	 */
	protected function get_templates( $id ) {
		if ( empty( $id ) && ! is_numeric( $id ) ) {
			return [];
		}

		if ( $data = papi_get_entry_type_by_meta_id( $id ) ) {
			$templates = papi_to_array( $data->template );

			ksort( $templates );

			return $templates;
		}

		return [];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$layout             = $this->get_setting( 'layout' );
		$post_type          = $this->get_post_type();
		$settings           = $this->get_settings();
		$value              = $this->get_value();
		$posts              = $this->get_posts( $post_type );
		$selected_post_id   = is_object( $value ) ? $value->module : 0;
		$selected_post_id   = is_object( $selected_post_id ) ? $selected_post_id->ID : 0;
		$templates          = $this->get_templates( $selected_post_id );
		$selected_template  = is_object( $value ) ? intval( $value->template ) : null;
		$classes            = '';

		if ( $settings->select2 ) {
			$classes .= ' papi-component-select2';
		}
		?>

						<label for="<?php echo esc_attr( $this->html_id() ); ?>_modules">
							<?php echo esc_html( $settings->labels['select_module'] ); ?>

						<?php
							$placeholder = ! is_null( $settings->placeholder ) ? $settings->placeholder : '';
						?>

						<select
							class="<?php echo esc_attr( $classes ); ?>  papi-property-module-right"
							id="<?php echo esc_attr( $this->html_id() ); ?>_modules"
							name="<?php echo esc_attr( $this->html_name() ); ?>"
							data-allow-clear="<?php echo empty( $settings->placeholder ) ? 'false' : 'true'; ?>"
							data-select-item="<?php echo esc_attr( $settings->labels['select_module'] ); ?>"
							data-placeholder="<?php echo esc_attr( $settings->placeholder ); ?>"
							data-width="100%"
							>

							<?php if ( ! empty( $settings->placeholder ) ): ?>
								<?php if ( $settings->new_url ): ?>
									<option data-placeholder data-new-url="<?php echo esc_attr( admin_url( 'post-new.php?post_type=' . $post_type ) ); ?>"></option>
								<?php else: ?>
									<option></option>
								<?php endif; ?>
							<?php endif; ?>

							<?php
							foreach ( $posts as $post ) {
								if ( papi_is_empty( $post->post_title ) ) {
									continue;
								}

								papi_render_html_tag( 'option', [
									'data-entry-type' => get_post_meta( $post->ID, papi_get_page_type_key(), true ),
									'data-edit-url'   => get_edit_post_link( $value ),
									'data-new-url'    => $settings->new_url ? admin_url( 'post-new.php?post_type=' . $post->post_type ) : '',
									'selected'        => $selected_post_id === $post->ID,
									'value'           => $post->ID,
									$post->post_title
								] );
							}
							?>
						</select>

						<label for="<?php echo esc_attr( $this->html_id() ); ?>_template">
							<?php echo esc_html( $settings->labels['select_template'] ); ?>

						<select
							id="<?php echo esc_attr( $this->html_id() ); ?>_template"
							name="<?php echo esc_attr( $this->html_name() ); ?>_template"
							class="<?php echo esc_attr( $classes ); ?> papi-property-module-left"
							data-post-query='<?php echo esc_attr( papi_maybe_json_encode( $settings->query ) ); ?>'
							data-width="100%"
							>
							<?php
							foreach ( $templates as $index => $item ) {
								// Make string to a array.
								if ( is_string( $item ) ) {
									$item = [
										'label'    => $item,
										'template' => $item,
										'default'  => false
									];
								}

								// Bail if no array.
								if ( ! is_array( $item ) ) {
									continue;
								}

								// Bail if no label or template exists.
								if ( ! isset( $item['label'], $item['template'] ) ) {
									continue;
								}

								// Set missing default value.
								if ( ! isset( $item['default'] ) ) {
									$item['default'] = false;
								}

								papi_render_html_tag( 'option', [
									'value'    => $index,
									'selected' => ! papi_is_empty( $selected_template ) ? $index === $selected_template : $item['default'],
									$item['label']
								] );
							}
							?>
						</select>

		<?php
	}

	/**
	 * Render option template.
	 */
	public function render_option_template() {
		$settings  = $this->get_settings();
		$post_type = $this->get_post_type();

		?>

		<?php
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_head', [$this, 'render_option_template'] );
	}
}


<?php

/**
 * HTML property that can display html in the admin.
 */
class Papi_Property_Html extends Papi_Property {

	/**
	 * Determine if the property require a slug or not.
	 *
	 * @var bool
	 */
	protected $slug_required = false;

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'html' => '',
			'save' => false
		];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings = $this->get_settings();
		$html     = papi_maybe_get_callable_value( $settings->html );

		if ( $settings->save ) {
			$value = $this->get_value();

			if ( ! empty( $value ) && is_string( $value ) ) {
				$html = $value;
			}

			papi_render_html_tag( 'input', [
				'name'  => esc_attr( $this->html_name() ),
				'type'  => 'hidden',
				'value' => $html
			] );
		}

		papi_render_html_tag( 'div', [
			'data-papi-rule' => esc_attr( $this->html_name() ),
			'class'          => 'property-html',
			$html
		] );
	}
}


<?php

class Papi_Core_Tab {

	/**
	 * The background of the tab.
	 *
	 * Possible values are: `white` or `grey`.
	 *
	 * By default if empty the background will be automatic,
	 * if first property has no sidebar it'll be white and
	 * if it has a sidebar it'll be grey.
	 *
	 * @var string
	 */
	public $background = '';

	/**
	 * Capabilities list.
	 *
	 * @var array
	 */
	public $capabilities = [];

	/**
	 * Tab icon.
	 *
	 * @var string
	 */
	public $icon = '';

	/**
	 * The core tab identifier.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Box properties.
	 *
	 * @var array
	 */
	public $properties = [];

	/**
	 * The sort order of the core box.
	 *
	 * @var int
	 */
	public $sort_order = 1000;

	/**
	 * Because of old code for tabs this
	 * property is required to exists on
	 * core tab class.
	 *
	 * @var bool
	 */
	public $tab = true;

	/**
	 * The title of the box.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * The constructor.
	 *
	 * @param array $args
	 * @param array $properties
	 */
	public function __construct( array $args = [], array $properties = [] ) {
		$this->setup_args( $args );
		$this->setup_properties( $properties );
	}

	/**
	 * Setup arguments.
	 *
	 * @param  array $args
	 */
	protected function setup_args( array $args ) {
		foreach ( $args as $key => $value ) {
			if ( isset( $this->$key ) ) {
				$this->$key = papi_esc_html( $value );
			}
		}

		if ( empty( $this->id ) ) {
			$this->id = strtolower( papi_f( papi_underscorify( papify( $this->title ) ) ) );
		}
	}

	/**
	 * Setup properties.
	 *
	 * @param  array $properties
	 */
	protected function setup_properties( array $properties ) {
		$this->properties = array_merge( $this->properties, papi_populate_properties( $properties ) );
	}
}


<?php

/**
 * HTML textarea property.
 */
class Papi_Property_Text extends Papi_Property {

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( ! $this->get_setting( 'allow_html' ) ) {
			$value = maybe_unserialize( $value );

			if ( ! is_string( $value ) ) {
				$value = '';
			}

			$value = wp_strip_all_tags( $value );

			if ( ! papi_is_admin() ) {
				$value = $this->get_setting( 'nl2br' ) ? nl2br( $value ) : $value;
			}
		}

		return $value;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'allow_html' => false,
			'nl2br'      => true
		];
	}

	/**
	 * Get value from the database.
	 *
	 * @return string
	 */
	public function get_value() {
		$value = $this->format_value(
			parent::get_value(),
			$this->get_slug(),
			papi_get_post_id()
		);

		return $this->get_setting( 'allow_html' ) ? $value : esc_html( $value );
	}

	/**
	 * Render property html.
	 */
	public function html() {
		papi_render_html_tag( 'textarea', [
			'class' => 'papi-property-text',
			'id'    => esc_attr( $this->html_id() ),
			'name'  => esc_attr( $this->html_name() ),
			$this->get_value()
		] );
	}
}


<?php

/**
 * Post store implementation of Papi meta store.
 */
class Papi_Post_Store extends Papi_Core_Meta_Store {

	/**
	 * The WordPress post.
	 *
	 * @var object
	 */
	protected $post;

	/**
	 * The meta type.
	 *
	 * @var string
	 */
	protected $type = 'post';

	/**
	 * The constructor.
	 *
	 * @param int $id
	 */
	public function __construct( $id = 0 ) {
		$this->id         = papi_get_post_id( $id );
		$this->post       = get_post( $this->id );
		$id               = papi_get_page_type_id( $this->id );
		$this->type_class = papi_get_entry_type_by_id( $id );
	}

	/**
	 * Get the permalink for the page.
	 *
	 * @return string
	 */
	public function get_permalink() {
		return get_permalink( $this->id );
	}

	/**
	 * Get the WordPress post object.
	 *
	 * @return WP_Post
	 */
	public function get_post() {
		return $this->post;
	}

	/**
	 * Get the post status of a page.
	 *
	 * @return string
	 */
	public function get_status() {
		return get_post_status( $this->id );
	}

	/**
	 * Load property from page type.
	 *
	 * @param  string $slug
	 * @param  string $child_slug
	 *
	 * @return null|Papi_Core_Property
	 */
	public function get_property( $slug, $child_slug = '' ) {
		$page_type_id = papi_get_page_type_id( $this->id );
		$page_type    = papi_get_entry_type_by_id( $page_type_id );

		if ( $page_type instanceof Papi_Page_Type === false ) {
			return;
		}

		if ( $property = $page_type->get_property( $slug, $child_slug ) ) {
			return $this->prepare_property( $property );
		}
	}

	/**
	 * Prepare load value.
	 *
	 * @param  Papi_Core_Property $property
	 * @param  mixed              $value
	 *
	 * @return mixed
	 */
	protected function prepare_load_value( Papi_Core_Property $property, $value ) {
		if ( $property->overwrite ) {
			// Clear post cache to solve issue with cached post objects
			// when selecting post field.
			clean_post_cache( $this->id );

			$slug    = $property->get_slug( true );
			$context = papi_is_admin() ? 'edit' : 'display';
			$value   = get_post_field( $slug, $this->id, $context );
		}

		return $value;
	}

	/**
	 * Check if the page has the post object and that it's not null.
	 *
	 * @return bool
	 */
	public function valid() {
		return ! is_null( $this->post );
	}
}


<?php

/**
 * Admin class that handle option post data.
 */
final class Papi_Admin_Option_Handler extends Papi_Core_Data_Handler {

	/**
	 * Save properties with a post id of zero.
	 */
	public function save_properties() {
		if ( $_SERVER ['REQUEST_METHOD'] !== 'POST' || papi_get_meta_type() !== 'option' ) {
			return;
		}

		// Check if our nonce is vailed.
		if ( ! wp_verify_nonce( papi_get_sanitized_post( 'papi_meta_nonce' ), 'papi_save_data' ) ) {
			return;
		}

		// Get properties data.
		$data = $this->get_post_data();

		// Prepare properties data.
		$data = $this->prepare_properties_data( $data, 0 );

		foreach ( $data as $key => $value ) {
			papi_data_update( 0, $key, $value, 'option' );
		}

		/**
		 * Fire `save_properties` action when all is done.
		 *
		 * @param int    $id
		 * @param string $meta_type
		 */
		do_action( 'papi/save_properties', 0, 'option' );
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_init', [$this, 'save_properties'] );
	}
}

if ( papi_is_admin() ) {
	new Papi_Admin_Option_Handler;
}


<?php

class Papi_Admin_Page_Type_Switcher {

	/**
	 * The construct.
	 */
	public function __construct() {
		add_action( 'admin_init', [$this, 'admin_init'] );
	}

	/**
	 * Admin init hook callback.
	 */
	public function admin_init() {
		add_action( 'post_submitbox_misc_actions', [$this, 'metabox'] );
		add_action( 'save_post', [$this, 'save_post'], 10, 2 );
	}

	/**
	 * Render metabox.
	 */
	public function metabox() {
		$post_type     = papi_get_post_type();
		$page_type     = papi_get_entry_type_by_id( papi_get_page_type_id() );
		$page_type_key = papi_get_page_type_key( 'switch' );
		$page_types    = papi_get_all_page_types( $post_type );
		$show_standard = papi_filter_settings_show_standard_page_type( $post_type );

		if ( $show_standard ) {
			$standard_page_type = papi_get_standard_page_type( $post_type );
			$page_types[]       = $standard_page_type;

			if ( empty( $page_type ) ) {
				$page_type = $standard_page_type;
			}
		}

		usort( $page_types, function ( $a, $b ) {
			return strcmp( $a->name, $b->name );
		} );

		$page_types = papi_sort_order( array_reverse( $page_types ) );

		// Don't do anything without any page types.
		if ( empty( $page_type ) || empty( $page_types ) ) {
			return;
		} ?>

			<label for="<?php echo esc_attr( $page_type_key ); ?>"><?php esc_html_e( 'Page Type:', 'papi' ); ?>
			<?php echo esc_html( $page_type->name ); ?>

			<?php if ( papi_current_user_is_allowed( $page_type->capabilities ) && $page_type->switcher ): ?>
				<?php esc_html_e( 'Edit', 'papi' ); ?>

					<select name="<?php echo esc_attr( $page_type_key ); ?>" id="<?php echo esc_attr( $page_type_key ); ?>">
						<?php
						foreach ( $page_types as $pt ) {
							if ( ! papi_current_user_is_allowed( $pt->capabilities ) ) {
								continue;
							}

							papi_render_html_tag( 'option', [
								'selected' => $page_type->match_id( $pt->get_id() ),
								'value'    => esc_attr( $pt->get_id() ),

								esc_html( $pt->name )
							] );
						}
						?>
					</select>
					<?php esc_html_e( 'OK', 'papi' ); ?>
					<?php esc_html_e( 'Cancel', 'papi' ); ?>

			<?php endif; ?>

		<?php
	}

	/**
	 * Switch page type if all checks pass.
	 *
	 * @param  int     $post_id
	 * @param  WP_post $post
	 *
	 * @return bool
	 */
	public function save_post( $post_id, $post ) {
		// Check if post id and post object is empty or not.
		if ( empty( $post_id ) || empty( $post ) ) {
			return false;
		}

		// Check if our nonce is vailed.
		if ( ! wp_verify_nonce( papi_get_sanitized_post( 'papi_meta_nonce' ), 'papi_save_data' ) ) {
			return false;
		}

		// Check if so both page type keys exists.
		if ( empty( $_POST[papi_get_page_type_key()] ) || empty( $_POST[papi_get_page_type_key( 'switch' )] ) ) {
			return false;
		}

		// Page type information.
		$page_type_id        = sanitize_text_field( $_POST[papi_get_page_type_key()] );
		$page_type_switch_id = sanitize_text_field( $_POST[papi_get_page_type_key( 'switch' )] );

		// Don't update if the same ids.
		if ( $page_type_id === $page_type_switch_id ) {
			return false;
		}

		// Fetch right page type if standard page type id.
		if ( papi_get_standard_page_type_id( $post->post_type ) === $page_type_id ) {
			$page_type = papi_get_standard_page_type( $post->post_type );
		} else {
			$page_type = papi_get_entry_type_by_id( $page_type_id );
		}

		// Fetch right page type switch if standard page type id.
		if ( papi_get_standard_page_type_id( $post->post_type ) === $page_type_switch_id ) {
			$page_type_switch    = papi_get_standard_page_type( $post->post_type );
			$page_type_switch_id = '';
		} else {
			$page_type_switch = papi_get_entry_type_by_id( $page_type_switch_id );
		}

		$post_type_object = get_post_type_object( $post->post_type );

		// Check if page type and post type is not empty.
		if ( empty( $page_type_switch ) || empty( $post_type_object ) ) {
			return false;
		}

		// Check if autosave.
		if ( wp_is_post_autosave( $post_id ) ) {
			return false;
		}

		// Check if revision.
		if ( wp_is_post_revision( $post_id ) ) {
			return false;
		}

		// Check if revision post type.
		if ( in_array( $post->post_type, ['revision', 'nav_menu_item'], true ) ) {
			return false;
		}

		// Check so page type has the post type.
		if ( ! $page_type->has_post_type( $post->post_type ) || ! $page_type_switch->has_post_type( $post->post_type ) ) {
			return false;
		}

		// Check page type capabilities.
		if ( ! papi_current_user_is_allowed( $page_type_switch->capabilities ) ) {
			return false;
		}

		// Check so user can edit posts and that the user can publish posts on the post type.
		if ( ! current_user_can( 'edit_post', $post_id ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
			return false;
		}

		// Get properties.
		$properties        = $page_type->get_properties();
		$properties_switch = $page_type_switch->get_properties();

		// Delete only properties that don't have the same type and slug.
		foreach ( $properties as $property ) {
			$delete = true;

			// Check if the properties are the same or not.
			foreach ( $properties_switch as $property_switch ) {
				if ( $property_switch->type === $property->type && $property_switch->match_slug( $property->get_slug() ) ) {
					$delete = false;
					break;
				}
			}

			if ( ! $delete ) {
				continue;
			}

			// Delete property values.
			$property->delete_value( $property->get_slug( true ), $post_id, papi_get_meta_type() );
		}

		// Delete page type switch id.
		if ( empty( $page_type_switch_id ) ) {
			return delete_post_meta( $post_id, papi_get_page_type_key() );
		}

		// Update page type id.
		return papi_set_page_type_id( $post_id, $page_type_switch_id );
	}
}

if ( papi_is_admin() ) {
	new Papi_Admin_Page_Type_Switcher;
}