<?php

/**
 * Admin class that handle meta data, like post or term.
 */
final class Papi_Admin_Meta_Handler extends Papi_Core_Data_Handler {

	/**
	 * Get meta type so we know where the data should be saved.
	 *
	 * @return string
	 */
	protected function get_meta_type() {
		if ( $current_filter = current_filter() ) {
			return papi_get_meta_type( explode( '_', $current_filter )[1] );
		}

		return papi_get_meta_type();
	}

	/**
	 * Overwrite post data in `posts` table.
	 *
	 * @param int $post_id
	 */
	protected function overwrite_post_data( $post_id ) {
		global $wpdb;

		if ( empty( $post_id ) || empty( $this->overwrite ) ) {
			return;
		}

		$wpdb->update( $wpdb->posts, $this->overwrite, [
			'ID' => $post_id
		] );

		// Delete cache since it can be cached even if not saved in the database.
		foreach ( array_keys( $this->overwrite ) as $key ) {
			papi_cache_delete( $key, $post_id );
		}

		clean_post_cache( $post_id );
	}

	/**
	 * Pre save page template and page type.
	 *
	 * @param int $id
	 */
	protected function pre_save( $id ) {
		if ( empty( $id ) ) {
			return;
		}

		$data = $this->get_pre_data();

		foreach ( $data as $key => $value ) {
			if ( empty( $value ) ) {
				continue;
			}

			if ( is_array( $value ) ) {
				list( $keys, $value ) = $this->get_pre_deep_keys_value( $value );
				$key = sprintf( '%s_%s', $key, implode( '_', $keys ) );
			}

			update_metadata( $this->get_meta_type(), $id, $key, $value );
		}
	}

	/**
	 * Save meta boxes.
	 *
	 * @param int     $id
	 * @param object  $post
	 */
	public function save_meta_boxes( $id, $post = null ) {
		// Check if there was a multisite switch before.
		if ( is_multisite() && ms_is_switched() ) {
			return;
		}

		// Can't proceed without a id.
		if ( empty( $id ) ) {
			return;
		}

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

		$meta_type = $this->get_meta_type();
		$post      = is_array( $post ) ? (object) $post : $post;

		if ( $meta_type === 'post' && $post_type = get_post_type_object( $post->post_type ) ) {
			// Check so the id is a post id and not a autosave post.
			if ( ! $this->valid_post_id( $id ) ) {
				return;
			}

			// Check the `edit_posts` capability before we continue.
			if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
				return;
			}

			// Delete all oEmbed caches.
			if ( class_exists( 'WP_Embed' ) ) {
				global $wp_embed;

				if ( $wp_embed instanceof WP_Embed ) {
					$wp_embed->delete_oembed_caches( $id );
				}
			}
		}

		if ( $meta_type === 'term' && $taxonomy = get_taxonomy( papi_get_taxonomy() ) ) {
			// Check the `edit_terms` capability before we continue.
			if ( $taxonomy && ! current_user_can( $taxonomy->cap->edit_terms ) ) {
				return;
			}
		}

		$this->save_properties( $id );
	}

	/**
	 * Save custom fields for revision post.
	 *
	 * @param int $post_id
	 */
	public function save_revision( $revision_id ) {
		// Check if our nonce is vailed.
		if ( ! wp_verify_nonce( papi_get_sanitized_post( 'papi_meta_nonce' ), 'papi_save_data' ) ) {
			return;
		}

		// Fetch parent id from revision.
		if ( ! $parent_id = wp_is_post_revision( $revision_id ) ) {
			return;
		}

		// Bail if a entry type don't exists on parent post.
		if ( papi_is_empty( papi_get_entry_type_by_meta_id( $parent_id, 'post' ) ) ) {
			return;
		}

		$meta = get_post_meta( $parent_id );

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

			papi_data_update( $revision_id, $key, array_shift( $value ) );
		}
	}

	/**
	 * Save properties.
	 *
	 * @param int $id
	 */
	public function save_properties( $id ) {
		// Pre save page template, page type and some others dynamic values.
		$this->pre_save( $id );

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

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

		// Get meta type.
		$meta_type = $this->get_meta_type();

		// Overwrite post data if any.
		if ( $meta_type === 'post' ) {
			$this->overwrite_post_data( $id );
		}

		// Save all properties value
		foreach ( $data as $key => $value ) {
			papi_data_update( $id, $key, $value, $this->get_meta_type() );
		}

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

	/**
	 * Restore to post revision.
	 *
	 * @param int $post_id
	 * @param int $revision_id
	 */
	public function restore_post_revision( $post_id, $revision_id ) {
		if ( papi_is_empty( papi_get_entry_type_by_meta_id( $post_id, 'post' ) ) ) {
			return;
		}

		$meta = get_post_meta( $revision_id );

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

			papi_data_update( $post_id, $key, array_shift( $value ) );
		}
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( '_wp_put_post_revision', [$this, 'save_revision'] );
		add_action( 'save_post', [$this, 'save_meta_boxes'], 1, 2 );
		add_action( 'created_term', [$this, 'save_meta_boxes'], 1 );
		add_action( 'edit_term', [$this, 'save_meta_boxes'], 1 );
		add_action( 'wp_restore_post_revision', [$this, 'restore_post_revision'], 10, 2 );
	}

	/**
	 * Check if post id is valid or not.
	 *
	 * @param  int $post_id
	 *
	 * @return bool
	 */
	protected function valid_post_id( $post_id ) {
		$key = papi_get_sanitized_post( 'action' ) === 'save-attachment-compat' ? 'id' : 'post_ID';
		$val = papi_get_sanitized_post( $key );

		// When autosave is in place the post id is located deeper in the post data array, the ids should not match.
		if ( isset( $_POST['data']['wp_autosave'], $_POST['data']['wp_autosave']['post_id'] ) ) {
			// But if it's a auto draft the ids should match.
			if ( isset( $_POST['data']['wp_autosave']['auto_draft'] ) && ! empty( $_POST['data']['wp_autosave']['auto_draft'] ) ) {
				return sanitize_text_field( $_POST['data']['wp_autosave']['post_id'] ) === strval( $post_id );
			}

			return sanitize_text_field( $_POST['data']['wp_autosave']['post_id'] ) !== strval( $post_id );
		}

		// Should not be the same id when `wp-preview` equals `dopreview`.
		if ( isset( $_POST['wp-preview'] ) && strtolower( $_POST['wp-preview'] ) === 'dopreview' ) {
			return $val !== strval( $post_id );
		}

		return $val === strval( $post_id );
	}
}

if ( papi_is_admin() ) {
	new Papi_Admin_Meta_Handler;
}


<?php

/**
 * Core class that handle if a conditional rule
 * should display the property or not.
 */
class Papi_Core_Conditional {

	/**
	 * Available relations.
	 *
	 * @var array
	 */
	protected $relations = [
		'AND',
		'OR'
	];

	/**
	 * Check if the property should be displayed by the rules.
	 *
	 * @param  array $rules
	 * @param  Papi_Core_Property $property
	 *
	 * @return bool
	 */
	public function display( array $rules, $property = null ) {
		if ( empty( $rules ) ) {
			return true;
		}

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

		if ( in_array( $rules['relation'], $this->relations, true ) ) {
			return $this->display_by_relation( $rules );
		}

		return true;
	}

	/**
	 * Get the display by relation.
	 *
	 * @param  array $rules
	 *
	 * @return bool
	 */
	protected function display_by_relation( array $rules ) {
		if ( $rules['relation'] === 'AND' ) {
			$display = true;

			foreach ( $rules as $rule ) {
				if ( ! $display ) {
					break;
				}

				if ( papi_is_rule( $rule ) ) {
					/**
					 * Modify rule allowed.
					 *
					 * @param  bool $result
					 * @param  Papi_Core_Conditional_Rule $rule
					 *
					 * @return bool
					 */
					$display = apply_filters( 'papi/conditional/rule_allowed', papi_filter_conditional_rule_allowed( $rule ), $rule );
				}
			}

			return $display;
		}

		$empty = array_filter( $rules, function ( $rule ) {
			return papi_is_rule( $rule ) ? true : null;
		} );

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

		$result = [];

		foreach ( $rules as $rule ) {
			if ( papi_is_rule( $rule ) ) {
				/**
				 * Modify rule allowed.
				 *
				 * @param  bool $result
				 * @param  Papi_Core_Conditional_Rule $rule
				 *
				 * @return bool
				 */
				$result[] = apply_filters( 'papi/conditional/rule_allowed', papi_filter_conditional_rule_allowed( $rule ), $rule );
			}
		}

		$result = array_filter( $result, function ( $res ) {
			return $res === true ? true : null;
		} );

		return ! empty( $result );
	}

	/**
	 * Get rule slug.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 * @param  Papi_Core_Property $property
	 *
	 * @return string
	 */
	protected function get_rule_slug( $rule, $property ) {
		$arr_reg = '/\[\d+\](\[\w+\])$/';
		$slug    = $property->get_slug();

		$page_type = papi_get_entry_type_by_meta_id();

		if ( $page_type instanceof Papi_Page_Type === false ) {
			return $rule->slug;
		}

		if ( preg_match( $arr_reg, $slug, $out ) ) {
			$slug     = str_replace( $out[1], '[' . unpapify( $rule->slug ) . ']', $slug );
			$property = $page_type->get_property( $slug );

			if ( papi_is_property( $property ) ) {
				return $slug;
			}
		}

		return $rule->slug;
	}

	/**
	 * Prepare rules.
	 *
	 * @param  array $rules
	 * @param  Papi_Core_Property $property
	 *
	 * @return array
	 */
	public function prepare_rules( array $rules, $property = null ) {
		if ( ! isset( $rules['relation'] ) ) {
			$rules['relation'] = 'OR';
		} else {
			$rules['relation'] = strtoupper( $rules['relation'] );
		}

		foreach ( $rules as $index => $value ) {
			if ( is_string( $index ) ) {
				continue;
			}

			if ( is_array( $value ) ) {
				$rules[$index] = new Papi_Core_Conditional_Rule( $value );

				if ( strpos( $rules[$index]->slug, '.' ) === false && papi_is_property( $property ) ) {
					$rules[$index]->slug = $this->get_rule_slug(
						$rules[$index],
						$property
					);
				}
			}
		}

		return $rules;
	}
}


<?php

/**
 * Papi type that handle all post types except attachment.
 * All page types should extend this class.
 */
class Papi_Page_Type extends Papi_Entry_Type {

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

	/**
	 * The page types that lives under this page type.
	 *
	 * @var array
	 */
	public $child_types = [];

	/**
	 * The description of the page type.
	 *
	 * @var string
	 */
	public $description = '';

	/**
	 * Fill labels.
	 *
	 * @var bool
	 */
	public $fill_labels = false;

	/**
	 * Labels, the same labels that post type object uses.
	 *
	 * Plus some special:
	 * - `title_placeholder` that can change main title input placeholder.
	 *
	 * @var array
	 */
	public $labels = [];

	/**
	 * The post types to register the page type with.
	 *
	 * @var array
	 */
	public $post_type = ['page'];

	/**
	 * Remove meta boxes.
	 *
	 * @var array
	 */
	protected $remove_meta_boxes = [];

	/**
	 * Show standard page type or not.
	 *
	 * @var bool
	 */
	public $standard_type = false;

	/**
	 * Show permalink edit box.
	 *
	 * @var bool
	 */
	public $show_permalink = true;

	/**
	 * Show page attributes box.
	 *
	 * @var bool
	 */
	public $show_page_attributes = true;

	/**
	 * Show page template dropdown.
	 *
	 * @var bool
	 */
	public $show_page_template = false;

	/**
	 * Show page type switcher.
	 *
	 * @var boolean
	 */
	public $switcher = true;

	/**
	 * The template of the page type.
	 *
	 * @var string
	 */
	public $template = '';

	/**
	 * The page type thumbnail.
	 *
	 * @var string
	 */
	public $thumbnail = '';

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

	/**
	 * The constructor.
	 *
	 * Load a page type by the file.
	 *
	 * @param string $file_path
	 */
	public function __construct( $file_path = '' ) {
		parent::__construct( $file_path );
		$this->setup_post_types();
		$this->setup_page_templates();
	}

	/**
	 * Determine if the page type is allowed by capabilities and post type.
	 *
	 * @return bool
	 */
	public function allowed() {
		$args = func_get_args();

		if ( empty( $args ) ) {
			return parent::allowed();
		}

		return papi_current_user_is_allowed( $this->capabilities ) && isset( $args[0] ) && in_array( $args[0], $this->post_type, true );
	}

	/**
	 * Should the Page Type be displayed in WordPress admin or not?
	 *
	 * @param  string $post_type
	 *
	 * @return bool
	 */
	public function display( $post_type ) {
		return true;
	}

	/**
	 * Get body css classes.
	 *
	 * @return array
	 */
	public function get_body_classes() {
		$classes = parent::get_body_classes();

		if ( ! $this->show_permalink ) {
			$classes[] = 'papi-hide-edit-slug-box';
		}

		if ( ! $this->show_page_attributes ) {
			$classes[] = 'papi-hide-pageparentdiv';
		}

		return $classes;
	}

	/**
	 * Get child page types that lives under the current page type.
	 *
	 * @return array
	 */
	public function get_child_types() {
		$child_types = [];

		foreach ( papi_to_array( $this->child_types ) as $id ) {
			$child_type = papi_get_entry_type_by_id( $id );

			if ( $child_type instanceof Papi_Page_Type ) {
				$child_types[] = $child_type;
			}
		}

		return $child_types;
	}

	/**
	 * Get labels that should be changed
	 * when using `fill_labels` option.
	 *
	 * @return array
	 */
	public function get_labels() {
		if ( ! $this->fill_labels ) {
			return $this->labels;
		}

		return array_merge( $this->labels, [
			'add_new_item' => sprintf( '%s %s', __( 'Add New', 'papi' ), $this->name ),
			'edit_item'    => sprintf( '%s %s', __( 'Edit', 'papi' ), $this->name ),
			'view_item'    => sprintf( '%s %s', __( 'View', 'papi' ), $this->name )
		] );
	}

	/**
	 * Get post type.
	 *
	 * @return string
	 */
	public function get_post_type() {
		return papi_get_post_type();
	}

	/**
	 * Get post type supports that will be removed.
	 *
	 * @return array
	 */
	protected function get_post_type_supports() {
		$supports = ['custom-fields'];

		if ( method_exists( $this, 'remove' ) ) {
			$output   = $this->remove();
			$output   = is_string( $output ) ? [$output] : $output;
			$output   = is_array( $output ) ? $output : [];
			$output   = array_filter( $output, 'is_string' );
			$supports = array_merge( $supports, $output );
		}

		$parent_class  = get_parent_class( $this );
		$parent_remove = method_exists( $parent_class, 'remove' );

		while ( $parent_remove ) {
			$parent        = new $parent_class();
			$output        = $parent->remove();
			$output        = is_string( $output ) ? [$output] : $output;
			$output        = is_array( $output ) ? $output : [];
			$output        = array_filter( $output, 'is_string' );
			$supports      = array_merge( $supports, $output );
			$parent_class  = get_parent_class( $parent_class );
			$parent_remove = method_exists( $parent_class, 'remove' );
		}

		return $supports;
	}

	/**
	 * Get page type image thumbnail.
	 *
	 * @return string
	 */
	public function get_thumbnail() {
		if ( empty( $this->thumbnail ) ) {
			return '';
		}

		return $this->thumbnail;
	}

	/**
	 * Check if the given post is allowed to use the page type.
	 *
	 * @param string $post_type
	 *
	 * @return bool
	 */
	public function has_post_type( $post_type ) {
		return in_array( $post_type, $this->post_type, true );
	}

	/**
	 * Remove post type support action.
	 */
	public function remove_post_type_support() {
		global $_wp_post_type_features;

		$post_type = $this->get_post_type();

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

		$post_type_supports = $this->get_post_type_supports();

		foreach ( $post_type_supports as $key => $value ) {
			if ( is_numeric( $key ) ) {
				$key = $value;
				$value = '';
			}

			if ( isset( $_wp_post_type_features[$post_type], $_wp_post_type_features[$post_type][$key] ) ) {
				unset( $_wp_post_type_features[$post_type][$key] );
				continue;
			}

			if ( in_array( strtolower( $key ), ['all', 'normal', 'side', 'advanced'], true ) ) {
				$value = strtolower( $key );
			} else if ( empty( $value ) ) {
				$value = 'normal';
			}

			$this->remove_meta_boxes[] = [$key, $value];
		}

		add_action( 'add_meta_boxes', [$this, 'remove_meta_boxes'], 999 );
	}

	/**
	 * Remove meta boxes.
	 */
	public function remove_meta_boxes() {
		global $wp_meta_boxes;

		$post_type = $this->get_post_type();
		$context = [];

		foreach ( $this->remove_meta_boxes as $item ) {
			if ( $item[0] !== $item[1] ) {
				remove_meta_box( $item[0], $post_type, $item[1] );
				continue;
			}

			$context = $item[0];

			// Our special context.
			if ( $context === 'all' ) {
				$context = ['normal', 'side', 'advanced'];
			} else {
				$context = [strtolower( $context )];
			}

			foreach ( $context as $ctx ) {
				if ( ! isset( $wp_meta_boxes[$post_type], $wp_meta_boxes[$post_type][$ctx] ) ) {
					continue;
				}

				$meta_boxes = $wp_meta_boxes[$post_type][$ctx];

				if ( ! is_array( $meta_boxes ) ) {
					continue;
				}

				foreach ( $meta_boxes as $level ) {
					foreach ( $level as $id => $box ) {
						// Don't allow removing of papi boxes.
						if ( strpos( $id, '_papi' ) !== false ) {
							continue;
						}

						// Don't allow removing of submitdiv.
						if ( $id === 'submitdiv' ) {
							continue;
						}

						remove_meta_box( $id, $post_type, $ctx );
					}
				}
		}
		}

		// Special for editor in normal context.
		if ( in_array( 'normal', $context, true ) ) {
			remove_post_type_support( 'page', 'editor' );
		}
	}

	/**
	 * Setup page type.
	 */
	public function setup() {
		parent::setup();

		// Remove post type support and meta boxes.
		$this->remove_post_type_support();

		// Add support for displaying information in publish box from a page type.
		if ( method_exists( $this, 'publish_box' ) ) {
			add_action( 'post_submitbox_misc_actions', [$this, 'publish_box'] );
		}

		// Hide page template dropdown if it shouldn't be showed.
		if ( ! $this->show_page_template ) {
			add_filter( 'theme_page_templates', '__return_empty_array' );
		}

		// Main title input placeholder.
		if ( ! empty( $this->labels['title_placeholder'] ) ) {
			add_filter( 'enter_title_here', function () {
				return $this->labels['title_placeholder'];
			} );
		}
	}

	/**
	 * Setup post types array.
	 */
	protected function setup_post_types() {
		$this->post_type = papi_to_array( $this->post_type );

		// Set a default value to post types array
		// if we don't have a array or a empty array.
		if ( empty( $this->post_type ) ) {
			$this->post_type = ['page'];
		}

		if ( count( $this->post_type ) === 1 && $this->post_type[0] === 'any' ) {
			$this->post_type = get_post_types( '', 'names' );
			$this->post_type = array_values( $this->post_type );
		}
	}

	/**
	 * Setup page templates if templates is a array.
	 */
	protected function setup_page_templates() {
		if ( ! is_array( $this->template ) || ! $this->has_post_type( papi_get_post_type() ) ) {
			return;
		}

		$this->show_page_template = true;
		foreach ( $this->post_type as $post_type ) {
			// Can't use `theme_templates` filter since it's added in 4.9.6.
			add_filter( "theme_{$post_type}_templates", function( $templates ) {
				foreach ( $this->template as $template ) {
					if ( ! is_array( $template ) || ! isset( $template['template'], $template['label'] ) ) {
						continue;
					}

					$templates[$template['template']] = $template['label'];
				}

				return $templates;
			} );
		}
	}
}


<?php

/**
 * WordPress editor property.
 */
class Papi_Property_Editor extends Papi_Property {

	/**
	 * Add filters to filter TinyMCE buttons.
	 */
	protected function add_mce_buttons() {
		for ( $i = 0; $i < 4; $i++ ) {
			$num = $i === 0 ? '' : '_' . ( $i + 1 );
			add_filter( 'mce_buttons' . $num, [$this, 'mce_buttons'] );
		}
	}

	/**
	 * 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 ) {
		return papi_is_admin() ? $value : apply_filters( 'the_content', $value );
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'mce_buttons'      => [],
			'mce_buttons_2'    => [],
			'mce_buttons_3'    => [],
			'mce_buttons_4'    => [],
			'media_buttons'    => true,
			'teeny'            => false,
			'drag_drop_upload' => true
		];
	}

	/**
	 * Filter TinyMCE buttons (Visual tab).
	 *
	 * @param  array $buttons
	 *
	 * @return array
	 */
	public function mce_buttons( array $buttons = [] ) {
		if ( $new = papi_to_array( $this->get_setting( current_filter(), [] ) ) ) {
			return $new;
		}

		return $buttons;
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$value = $this->get_value();
		$value = html_entity_decode( $value );
		$id    = str_replace(
			'[',
			'',
			str_replace( ']', '', $this->html_name() )
		) . '-' . uniqid();

		// Add `mce_buttons` filters.
		$this->add_mce_buttons();

		wp_editor( $value, $id, [
			'textarea_name'    => esc_attr( $this->html_name() ),
			'media_buttons'    => $this->get_setting( 'media_buttons', true ),
			'teeny'            => $this->get_setting( 'teeny', false ),
			'drag_drop_upload' => $this->get_setting( 'drag_drop_upload', true ),
		] );

		// Remove `mce_buttons` filters.
		$this->reove_mce_buttons();

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			add_filter( 'mce_external_plugins', '__return_empty_array' );
		}
	}

	/**
	 * Remove filters that filter TinyMCE buttons.
	 */
	protected function reove_mce_buttons() {
		for ( $i = 0; $i < 4; $i++ ) {
			$num = $i === 0 ? '' : '_' . ( $i + 1 );
			remove_filter( 'mce_buttons' . $num, [$this, 'mce_buttons'] );
		}
	}
}


<?php

/**
 * Repeater property that can repeat multiple properties.
 */
class Papi_Property_Repeater extends Papi_Property {

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

	/**
	 * Repeater counter number.
	 *
	 * @var int
	 */
	protected $counter = 0;

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * Exclude properties that is not allowed in a repeater.
	 *
	 * @var array
	 */
	protected $exclude_properties = ['flexible'];

	/**
	 * 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 ) {
		$rows   = intval( papi_data_get( $post_id, $slug, $type ) );
		$value  = $this->load_value( $rows, $slug, $post_id );
		$value  = papi_property_to_array_slugs( $value, $slug );
		$result = true;

		foreach ( array_keys( $value ) as $key ) {
			$out    = papi_data_delete( $post_id, $key, $type );
			$result = $out ? $result : $out;
		}

		return $result;
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $values
	 * @param  string $repeater_slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $values, $repeater_slug, $post_id ) {
		if ( ! is_array( $values ) ) {
			return [];
		}

		$top_property = new Papi_Core_Property;

		foreach ( $values as $index => $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$top_property->slug = $repeater_slug . '_' . $index;

			foreach ( $row as $slug => $value ) {
				if ( papi_is_property_type_key( $slug ) ) {
					continue;
				}

				// Property type slug may have a prefixed underscore.
				$property_type_slug = papi_get_property_type_key_f( $slug );

				if ( ! isset( $row[$property_type_slug] ) ) {
					// If property type slug isn't prefixed we should try without.
					$property_type_slug = papi_get_property_type_key( $slug );

					if ( ! isset( $row[$property_type_slug] ) ) {
						continue;
					}
				}

				// Get property type.
				$property_type_value = $row[$property_type_slug];
				$property_type       = papi_get_property_type( $property_type_value );

				if ( ! is_object( $property_type ) ) {
					continue;
				}

				// Get property child slug.
				$child_slug = $this->get_child_slug( $repeater_slug, $slug );

				// Set top property.
				$property_type->set_parent_property( $top_property );

				// Create cache key.
				$cache_key = sprintf( '%s_%s', $top_property->slug, $slug );
				if ( $parent_property = $this->get_parent_property() ) {
					$cache_key = sprintf( '%s_%s', $parent_property->get_slug( true ), $cache_key );
				}

				// Get raw value from cache if enabled.
				if ( $this->cache ) {
					$raw_value = papi_cache_get( $cache_key, $post_id, $this->get_meta_type() );
				} else {
					$raw_value = false;
				}

				// Load the value.
				if ( $raw_value === null || $raw_value === false ) {
					$row[$slug] = $property_type->load_value( $value, $child_slug, $post_id );
					$row[$slug] = papi_filter_load_value( $property_type->type, $row[$slug], $child_slug, $post_id, papi_get_meta_type() );

					if ( ! papi_is_empty( $row[$slug] ) && $this->cache ) {
						papi_cache_set( $cache_key, $post_id, $row[$slug], $this->get_meta_type() );
					}
				} else {
					$row[$slug] = $raw_value;
				}

				// Format the value from the property class.
				$row[$slug] = $property_type->format_value( $row[$slug], $child_slug, $post_id );

				if ( ! papi_is_admin() ) {
					$row[$slug] = papi_filter_format_value( $property_type->type, $row[$slug], $child_slug, $post_id, papi_get_meta_type() );
				}

				$row[$property_type_slug] = $property_type_value;

				if ( papi_is_empty( $row[$slug] ) ) {
					$row[$slug] = $property_type_value->get_option( 'default', $property_type_value->default_value );
				}
			}

			if ( ! papi_is_admin() ) {
				foreach ( $row as $slug => $value ) {
					if ( papi_is_property_type_key( $slug ) ) {
						unset( $row[$slug] );
					}
				}
			}

			$values[$index] = $row;
		}

		return $values;
	}

	/**
	 * Get child slug from the repeater slug.
	 *
	 * @param  string $repeater_slug
	 * @param  string $child_slug
	 *
	 * @return string
	 */
	protected function get_child_slug( $repeater_slug, $child_slug ) {
		return preg_replace( '/^\_/', '', preg_replace( '/\_\d+\_/', '', preg_replace( sprintf( '/^%s/', $repeater_slug ), '', $child_slug ) ) );
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'add_new_label' => __( 'Add new row', 'papi' ),
			'closed_rows'   => false,
			'items'         => [],
			'layout'        => 'table',
			'limit'         => - 1
		];
	}

	/**
	 * Get results from the database.
	 *
	 * @param  int    $value
	 * @param  string $repeater_slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	protected function get_results( $value, $repeater_slug, $post_id ) {
		global $wpdb;

		if ( $this->get_meta_type() === 'option' ) {
			$table = $wpdb->prefix . 'options';

			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE `option_name` LIKE '%s' ORDER BY `option_id` ASC",
				$repeater_slug . '_%'
			);
			// @codingStandardsIgnoreEnd
		} else {
			$table  = sprintf( '%s%smeta', $wpdb->prefix, $this->get_meta_type() );
			$column = papi_get_meta_id_column( $this->get_meta_type() );
			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE `meta_key` LIKE '%s' AND `$column` = %s ORDER BY `meta_id` ASC", $repeater_slug . '_%',
				$post_id
			);
			// @codingStandardsIgnoreEnd
		}

		$dbresults = $wpdb->get_results( $query ); // WPCS: unprepared SQL
		$value     = intval( $value );

		// Do not proceed with empty value.
		if ( empty( $value ) ) {
			return [[], []];
		}

		$values  = [];
		$results = [];
		$trash   = [];

		// Get row results.
		$rows = array_values( $this->get_row_results( $dbresults ) );

		// Add repeater slug with number of rows to the values array.
		$values[$repeater_slug] = $value;

		for ( $i = 0; $i < $value; $i ++ ) {
			$no_trash = [];

			if ( ! isset( $no_trash[$i] ) ) {
				$no_trash[$i] = [];
			}

			if ( ! isset( $rows[$i] ) ) {
				continue;
			}

			foreach ( $rows[$i] as $slug => $meta ) {
				if ( ! is_string( $slug ) || ! isset( $rows[$i][$slug] ) ) {
					continue;
				}

				// Add meta object to the no trash array.
				// so it won't be deleted.
				$no_trash[$slug] = $meta;

				// Add property value.
				$values[$meta->meta_key] = papi_maybe_json_decode( maybe_unserialize( $meta->meta_value ) );
			}

			// Get the meta keys to delete.
			$trash_diff = array_diff( array_keys( $rows[$i] ), array_keys( $no_trash[$i] ) );

			if ( ! empty( $trash_diff ) ) {
				// Find all trash meta objects from results array.
				foreach ( $trash_diff as $slug ) {
					if ( ! isset( $results[$i] ) || ! isset( $rows[$i][$slug] ) ) {
						continue;
					}

					$trash[$results[$i][$slug]->meta_key] = $rows[$i][$slug];
				}
			}
		}

		$properties = $this->get_settings_properties();

		// Add empty rows that isn't saved to database.
		for ( $i = 0; $i < $value; $i ++ ) {
			foreach ( $properties as $prop ) {
				if ( ! isset( $prop->slug ) ) {
					continue;
				}

				$slug = sprintf( '%s_%d_%s', $repeater_slug, $i, unpapify( $prop->slug ) );

				if ( ! isset( $values[$slug] ) ) {
					$values[$slug] = null;
				}
			}
		}

		return [$values, $trash];
	}

	/**
	 * Get row results.
	 *
	 * @param  array $dbresults
	 *
	 * @return array
	 */
	protected function get_row_results( $dbresults ) {
		$results   = [];
		$is_option = $this->get_meta_type() === 'option';

		foreach ( $dbresults as $meta ) {
			if ( $is_option ) {
				preg_match( '/^[^\d]*(\d+)/', $meta->option_name, $matches );
			} else {
				preg_match( '/^[^\d]*(\d+)/', $meta->meta_key, $matches );
			}

			if ( count( $matches ) < 2 ) {
				continue;
			}
			$i = intval( $matches[1] );

			if ( ! isset( $results[$i] ) ) {
				$results[$i] = [];
			}

			if ( $is_option ) {
				$results[$i][$meta->option_name] = (object) [
					'meta_key'   => $meta->option_name,
					'meta_value' => $meta->option_value
				];
			} else {
				$results[$i][$meta->meta_key] = $meta;
			}
		}

		return $results;
	}

	/**
	 * Get settings properties.
	 *
	 * @return array
	 */
	protected function get_settings_properties() {
		$settings = $this->get_settings();

		if ( is_null( $settings ) ) {
			return [];
		}

		return $this->prepare_properties( papi_to_array( $settings->items ) );
	}

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

		// Reset list counter number.
		$this->counter = 0;

		// Fix so repeater is not render over the title and description.
		if ( $this->get_option( 'layout' ) === 'vertical' ) {
			echo '
';
		}

		// Render repeater html.
		$this->render_repeater( $options );

		// Render JSON template that is used for Papi ajax.
		$this->render_json_template( $options->slug );
	}

	/**
	 * Check if the given layout is the layouted used.
	 *
	 * @param  string $layout
	 *
	 * @return bool
	 */
	protected function layout( $layout ) {
		return $this->get_setting( 'layout' ) === $layout;
	}

	/**
	 * Change value after it's loaded from the database
	 * and populate every property in the repeater with the right property type.
	 *
	 * @param int    $value
	 * @param string $repeater_slug
	 * @param int    $post_id
	 *
	 * @return array
	 */
	public function load_value( $value, $repeater_slug, $post_id ) {
		if ( is_array( $value ) ) {
			return $value;
		}

		list( $results, $trash ) = $this->get_results( $value, $repeater_slug, $post_id );

		// Will not need this array.
		unset( $trash );

		$results = papi_from_property_array_slugs( $results, unpapify( $repeater_slug ) );

		if ( empty( $results ) ) {
			return $this->default_value;
		}

		return $this->load_child_properties( $results, $this );
	}

	/**
	 * Load child properties.
	 *
	 * @param  array              $results
	 * @param  Papi_Core_Property $property
	 *
	 * @return array
	 */
	protected function load_child_properties( array $results, $property = null ) {
		foreach ( $results as $index => $row ) {
			foreach ( $row as $slug => $value ) {
				if ( is_array( $value ) && isset( $value[$slug] ) ) {
					$child_property = $this->get_store()->get_property( $this->get_slug( true ), $slug );

					if ( papi_is_property( $child_property ) && ! empty( $child_property->get_child_properties() ) ) {
						$value                  = papi_from_property_array_slugs( $value, unpapify( $slug ) );
						$results[$index][$slug] = $this->load_child_properties( $value, $child_property );
					}
				}

				$type_key = papi_get_property_type_key_f( $slug );

				if ( $property->match_slug( $slug ) ) {
					$results[$index][$type_key] = $property;
				} else {
					$results[$index][$type_key] = $property->get_child_property( $slug );
				}
			}
		}

		return $results;
	}

	/**
	 * Prepare properties, get properties options object,
	 * check which properties that are allowed to use.
	 *
	 * @param  array $items
	 *
	 * @return array
	 */
	protected function prepare_properties( $items ) {
		$key   = isset( $this->layout_key ) &&
		$this->layout_key === '_layout' ? 'flexible' : 'repeater';
		$items = array_map( 'papi_property', $items );

		$exclude_properties = $this->exclude_properties;
		$exclude_properties = array_merge( $exclude_properties, apply_filters( 'papi/property/' . $key . '/exclude', [] ) );

		return array_filter( $items, function ( $item ) use ( $exclude_properties ) {
			if ( ! is_object( $item ) ) {
				return false;
			}

			if ( empty( $item->type ) ) {
				return false;
			}

			return ! in_array( $item->type, $exclude_properties, true );
		} );
	}

	/**
	 * Prepare property for JSON.
	 *
	 * @param  Papi_Property $property
	 *
	 * @return bool|object
	 */
	protected function prepare_property_for_json( $property ) {
		// Only real property objects and not properties that are disabled.
		if ( ! papi_is_property( $property ) || $property->disabled() ) {
			return false;
		}

		$options = clone $property->get_options();

		if ( isset( $options->settings->items ) ) {
			foreach ( $options->settings->items as $index => $property ) {
				if ( is_array( $property ) && isset( $property['items'] ) ) {
					foreach ( $property['items'] as $child_index => $child_property ) {
						$options->settings->items[$index]['items'][$child_index] = $this->prepare_property_for_json( $child_property );
					}
				}

				if ( $property = $this->prepare_property_for_json( $property ) ) {
					$options->settings->items[$index] = $property;
				}
			}
		}

		return $options;
	}

	/**
	 * Remove all repeater rows from the database.
	 *
	 * @param int    $post_id
	 * @param string $repeater_slug
	 */
	protected function remove_repeater_rows( $post_id, $repeater_slug ) {
		global $wpdb;

		$is_option     = $this->get_meta_type() === 'option';
		$repeater_slug = $repeater_slug . '_%';

		if ( $is_option ) {
			$table = $wpdb->prefix . 'options';

			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE (`option_name` LIKE %s OR `option_name` LIKE %s AND NOT `option_name` = %s)",
				$repeater_slug,
				papi_f( $repeater_slug ),
				papi_get_property_type_key_f( $repeater_slug )
			);
			// @codingStandardsIgnoreEnd
		} else {
			$table = $wpdb->prefix . 'postmeta';
			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE `post_id` = %d AND (`meta_key` LIKE %s OR `meta_key` LIKE %s AND NOT `meta_key` = %s)",
				$post_id,
				$repeater_slug,
				papi_f( $repeater_slug ),
				papi_get_property_type_key_f( $repeater_slug )
			);
			// @codingStandardsIgnoreEnd
		}

		$results = $wpdb->get_results( $query ); // WPCS: unprepared sql

		foreach ( $results as $res ) {
			if ( $is_option ) {
				$key = $res->option_name;
			} else {
				$key = $res->meta_key;
			}

			papi_data_delete( $post_id, $key, $this->get_meta_type() );
		}
	}

	/**
	 * Render AJAX request.
	 */
	public function render_ajax_request() {
		$items = $this->get_settings_properties();

		if ( papi_doing_ajax() ) {
			$counter       = papi_get_qs( 'counter' );
			$this->counter = intval( $counter );
		}

		$this->render_properties( $items, false );
	}

	/**
	 * Render property JSON template.
	 *
	 * @param string $slug
	 */
	protected function render_json_template( $slug ) {
		$options = $this->get_options();

		$options->settings->items = papi_to_array( $options->settings->items );

		foreach ( $options->settings->items as $key => $value ) {
			$property = $this->prepare_property_for_json( papi_property( $value ) );

			if ( $property === false ) {
				unset( $options->settings->items[$key] );
				continue;
			}

			$options->settings->items[$key] = $property;
		}

		papi_render_html_tag( 'script', [
			'data-papi-json' => esc_attr( sprintf( '%s_repeater_json', $slug ) ),
			'type'           => 'application/json',
			papi_maybe_json_encode( [$options] )
		] );
	}

	/**
	 * Render properties.
	 *
	 * @param array      $row
	 * @param array|bool $value
	 */
	protected function render_properties( $row, $value ) {
		$layout = $this->get_setting( 'layout' );

		if ( $layout === 'row' ): ?>

		<?php endif;

		$has_value = $value !== false;

		foreach ( $row as $property ) {
			// Don't show the property if it's disabled.
			if ( $property->disabled() ) {
				continue;
			}

			$render_property = clone $property->get_options();
			$value_slug      = $property->get_slug( true );

			if ( $has_value ) {
				if ( array_key_exists( $value_slug, $value ) ) {
					$render_property->value = $value[$value_slug];
				} else {
					if ( array_key_exists( $property->get_slug(), $value ) ) {
						$render_property->value = $property->default_value;
					} else {
						continue;
					}
				}
			}

			$render_property->slug = $this->html_name( $property, $this->counter );
			$render_property->raw  = $layout === 'table';

			if ( $layout === 'table' ) {
				echo 'display() ? '' : 'papi-hide' ) . '">';
				echo '
';
				echo sprintf(
					'%s',
					esc_attr( $this->html_id( $property, $this->counter ) ),
					esc_html( $property->title )
				);
			}

			papi_render_property( $render_property );

			if ( $layout === 'table' ) {
				echo '
';
				echo '	';
			}
		}

		if ( $layout === 'row' ): ?>

		<?php endif;
	}

	/**
	 * Render repeater html.
	 *
	 * @param stdClass $options
	 */
	protected function render_repeater( $options ) {
		?>
		<div class="papi-property-repeater papi-property-repeater-top" data-limit="<?php echo esc_attr( $this->get_setting( 'limit' ) ); ?>">

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

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

				<?php
				papi_render_html_tag( 'button', [
					'class'          => 'button button-primary',
					'data-papi-json' => sprintf( '%s_repeater_json', $options->slug ),
					'type'           => 'button',
					esc_html( $this->get_setting( 'add_new_label' ) )
				] );
				?>

			<?php /* Default repeater value */ ?>

			<input type="hidden" data-papi-rule="<?php echo esc_attr( $options->slug ); ?>" name="<?php echo esc_attr( $options->slug ); ?>[]" />

		<?php
	}

	/**
	 * Render repeater head.
	 */
	protected function render_repeater_head() {
		$properties = $this->get_settings_properties();
		?>

		<?php if ( ! $this->layout( 'row' ) ): ?>

				<?php
				foreach ( $properties as $property ):
					// Don't show the property if it's disabled.
					if ( $property->disabled() ) {
						continue;
					}
					?>
					<th class="repeater-column <?php echo $property->display() ? '' : 'papi-hide'; ?>">
						<?php echo esc_html( $property->title ); ?>

				<?php endforeach; ?>

		<?php endif; ?>

		<?php
	}

	/**
	 * Render repeater rows.
	 */
	protected function render_repeater_rows() {
		$items  = $this->get_settings_properties();
		$values = $this->get_value();
		$values = is_array( $values ) ? $values : [];
		$slugs  = wp_list_pluck( $items, 'slug' );

		// Remove values that don't exists in the slugs array.
		foreach ( $values as $index => $value ) {
			if ( ! is_array( $value ) ) {
				continue;
			}

			$keys = array_keys( $value );

			foreach ( $slugs as $slug ) {
				if ( in_array( $slug, $keys, true ) ) {
					continue;
				}

				$values[$index][$slug] = '';
			}
		}

		$values      = array_filter( $values );
		$closed_rows = $this->get_setting( 'closed_rows', true );

		foreach ( $values as $row ):
			?>
			<tr <?php echo $closed_rows ? 'class="closed"' : ''; ?>>

					<?php echo esc_html( $this->counter + 1 ); ?>

				<?php
				$this->render_properties( $items, $row );
				$this->counter ++;
				?>

						<a title="<?php esc_attr_e( 'Remove', 'papi' ); ?>" href="#" class="repeater-remove-item">x

			<?php
		endforeach;
	}

	/**
	 * Render repeater row template.
	 */
	public function render_repeater_rows_template() {
		?>

		<?php
	}

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

	/**
	 * Update value before it's saved to the database.
	 *
	 * @param mixed  $values
	 * @param string $repeater_slug
	 * @param int    $post_id
	 *
	 * @return array
	 */
	public function update_value( $values, $repeater_slug, $post_id ) {
		$rows = intval( papi_data_get( $post_id, $repeater_slug, $this->get_meta_type() ) );

		if ( ! is_array( $values ) ) {
			$values = [];
		}

		list( $results, $trash ) = $this->get_results( $rows, $repeater_slug, $post_id );

		// Delete trash values.
		foreach ( $trash as $meta ) {
			papi_data_delete( $post_id, $meta->meta_key, $this->get_meta_type() );
		}

		$top_property = new Papi_Core_Property;

		foreach ( $values as $index => $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$top_property->slug = $repeater_slug . '_' . $index;

			foreach ( $row as $slug => $value ) {
				if ( papi_is_property_type_key( $slug ) ) {
					continue;
				}

				// Property type slug may have a prefixed underscore.
				$property_type_slug = papi_get_property_type_key_f( $slug );

				if ( ! isset( $row[$property_type_slug] ) ) {
					// If property type slug isn't prefixed we should try without.
					$property_type_slug = papi_get_property_type_key( $slug );

					if ( ! isset( $row[$property_type_slug] ) ) {
						continue;
					}
				}

				if ( ! isset( $row[$property_type_slug] ) ) {
					continue;
				}

				// Get real property slug
				$property_slug = $this->get_child_slug( $repeater_slug, $slug );

				// Get property type
				$property_type_value = $row[$property_type_slug]->type;
				$property_type       = papi_get_property_type( $row[$property_type_slug] );

				if ( ! is_object( $property_type ) ) {
					continue;
				}

				// Set top property.
				$property_type->set_parent_property( $top_property );

				// Create cache key.
				$cache_key = sprintf( '%s_%s', $top_property->slug, $slug );
				if ( $parent_property = $this->get_parent_property() ) {
					$cache_key = sprintf( '%s_%s', $parent_property->get_slug( true ), $cache_key );
				}

				// Delete cache if any.
				papi_cache_delete( $cache_key, $post_id, papi_get_meta_type() );

				// Unserialize if needed.
				$value = papi_maybe_json_decode( maybe_unserialize( $value ) );

				// Run update value on each property type class.
				$value = $property_type->update_value( $value, $property_slug, $post_id );

				// Run update value on each property type filter.
				$row[$slug] = papi_filter_update_value( $property_type_value, $value, $property_slug, $post_id, papi_get_meta_type() );

				if ( is_array( $row[$slug] ) ) {
					foreach ( $row[$slug] as $key => $val ) {
						if ( ! is_string( $key ) ) {
							continue;
						}

						unset( $row[$slug][$key] );

						$key = preg_replace( '/^\_/', '', $key );

						$row[$slug][$key] = $val;
					}
				}
			}

			// Remove unnecessary property type keys if any is left.
			foreach ( $row as $slug => $value ) {
				if ( papi_is_property_type_key( $slug ) ) {
					unset( $row[$slug] );
				}
			}

			$values[$index] = $row;
		}

		$values = papi_property_to_array_slugs( $values, $repeater_slug );

		// Find out which keys that should be deleted.
		$trash = array_diff( array_keys( papi_to_array( $results ) ), array_keys( papi_to_array( $values ) ) );

		// Delete unwanted (trash) values.
		foreach ( array_keys( $trash ) as $trash_key ) {
			papi_data_delete( $post_id, $trash_key, $this->get_meta_type() );
		}

		// It's safe to remove all rows in the database here.
		$this->remove_repeater_rows( $post_id, $repeater_slug );

		return $values;
	}
}


<?php

class Papi_REST_API_Settings {

	/**
	 * The post store class.
	 *
	 * @var Papi_Post_Store
	 */
	protected $entries;

	/**
	 * REST API Settings construct.
	 */
	public function __construct() {
		// Hook into the REST API.
		add_action( 'rest_api_init', [$this, 'register'] );
		add_filter( 'rest_pre_get_setting', [$this, 'pre_get_setting'] );
	}

	/**
	 * Register options properties.
	 */
	public function register() {
		// Fetch all options entries.
		$this->entries = papi_get_all_entry_types( [
			'types' => 'option'
		] );

		foreach ( $this->entries as $entry ) {
			foreach ( $entry->get_properties() as $property ) {
				$property->register( 'option' );
			}
		}
	}

	/**
	 * Setup request after callbacks filter so it's only runs on settings endpoint.
	 *
	 * When the filter is added the `rest_pre_get_setting` will be removed since it's
	 * not used to anything good.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	public function pre_get_setting( $value ) {
		if ( ! has_filter( 'rest_request_after_callbacks', [$this, 'prepare_response'] ) ) {
			add_filter( 'rest_request_after_callbacks', [$this, 'prepare_response'] );
			remove_filter( 'rest_pre_get_setting', [$this, 'pre_get_setting'] );
		}

		return $value;
	}

	/**
	 * Get setting value for a property.
	 *
	 * @param  string $key
	 * @param  string $value
	 *
	 * @return mixed
	 */
	public function get_setting( $key, $value ) {
		$property = null;

		foreach ( (array) $this->entries as $entry ) {
			if ( $property = $entry->get_property( $key ) ) {
				break;
			}
		}

		if ( is_null( $property ) ) {
			return $value;
		}

		$value = papi_get_option( $key );

		return $property->rest_prepare_value( $value );
	}

	/**
	 * Prepare settings response.
	 *
	 * @param  WP_HTTP_Response $response
	 *
	 * @return array
	 */
	public function prepare_response( $response ) {
		$response = (array) $response;

		foreach ( $response as $key => $value ) {
			$setting = $this->get_setting( $key, $value );

			if ( $setting !== $value ) {
				$response[$key] = $setting;
			}
		}

		return $response;
	}
}

new Papi_REST_API_Settings;


<?php

/**
 * User property that populates a dropdown.
 */
class Papi_Property_User extends Papi_Property_Dropdown {

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

	/**
	 * 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 null|WP_User
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( is_object( $value ) && isset( $value->ID ) ) {
			$value = $value->ID;
		}

		if ( is_numeric( $value ) ) {
			return $value === 0 ? null : new WP_User( $value );
		}

		return $value;
	}

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

	/**
	 * Get value from database.
	 *
	 * @return int
	 */
	public function get_value() {
		$user = parent::get_value();

		if ( is_object( $user ) && isset( $user->ID ) ) {
			return $user->ID;
		}

		return 0;
	}

	/**
	 * Get users as dropdown items.
	 *
	 * @return array
	 */
	public function get_items() {
		$capabilities = papi_to_array( $this->get_setting( 'capabilities' ) );
		$users        = get_users();
		$items        = [];

		foreach ( $users as $user ) {
			$allcaps = $user->allcaps;

			if ( count( array_diff( $capabilities, array_keys( $allcaps ) ) ) === 0 ) {
				$items[$user->display_name] = $user->ID;
			}
		}

		ksort( $items );

		return $items;
	}

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

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

		return (int) $value;
	}
}


<?php

// Load Papi Container.
require_once __DIR__ . '/core/class-papi-core-container.php';

/**
 * Papi loader class that handle the loading
 * of the plugin.
 */
final class Papi_Loader extends Papi_Core_Container {

	/**
	 * The instance of Papi loader class.
	 *
	 * @var Papi_Loader
	 */
	protected static $instance;

	/**
	 * The plugin name.
	 *
	 * @var string
	 */
	public $name = 'Papi';

	/**
	 * Papi loader instance.
	 *
	 * @return Papi_Loader
	 */
	public static function instance() {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new self;
		}

		return self::$instance;
	}

	/**
	 * Call function in the container.
	 *
	 * @param  string $name
	 * @param  array  $arguments
	 *
	 * @return mixed
	 */
	public function __call( $name, $arguments ) {
		return $this->make( $name, $arguments );
	}

	/**
	 * The constructor.
	 */
	protected function __construct() {
		$this->constants();
		$this->init();

		// Fires the loaded action.
		// Deprecated action. `papi/loaded` should be used instead.
		if ( has_action( 'papi/include' ) ) {
			did_action( 'papi/include' ) || do_action( 'papi/include' );
			_deprecated_function( 'The papi/include action', '3.0.0', 'papi/loaded' );
		}

		// Fires the loaded action.
		did_action( 'papi/loaded' ) || do_action( 'papi/loaded' );
	}

	/**
	 * 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' );
	}

	/**
	 * Bootstrap constants
	 */
	protected function constants() {
		// Path to Papi plugin directory
		if ( ! defined( 'PAPI_PLUGIN_DIR' ) ) {
			$mu_dir = trailingslashit( sprintf( '%s/%s/src', WPMU_PLUGIN_DIR, basename( dirname( __DIR__ ) ) ) );

			if ( is_dir( $mu_dir ) ) {
				define( 'PAPI_PLUGIN_DIR', $mu_dir );
			} else {
				define( 'PAPI_PLUGIN_DIR', trailingslashit( __DIR__ ) );
			}
		}

		// URL to Papi plugin directory
		if ( ! defined( 'PAPI_PLUGIN_URL' ) ) {
			$plugin_url = plugin_dir_url( __FILE__ );

			if ( is_ssl() ) {
				$plugin_url = str_replace( 'http://', 'https://', $plugin_url );
			}

			define( 'PAPI_PLUGIN_URL', $plugin_url );
		}

		// The meta key that page type value is using
		$this->define( 'PAPI_PAGE_TYPE_KEY', '_papi_page_type' );

		// The plugin basename that is used in actions to match so right plugin is modified.
		$this->define( 'PAPI_PLUGIN_BASENAME', basename( dirname( __DIR__ ) ) . '/papi-loader.php' );
	}

	/**
	 * Define constant if not already set.
	 *
	 * @param  string      $name
	 * @param  string|bool $value
	 */
	protected function define( $name, $value ) {
		if ( ! defined( $name ) ) {
			define( $name, $value );
		}
	}

	/**
	 * Initialise Papi.
	 */
	protected function init() {
		// Fires the before init action.
		did_action( 'papi/before_init' ) || do_action( 'papi/before_init' );

		// Set up localisation.
		$this->load_textdomain();

		// Load all required files.
		$this->require_files();

		// Setup the container.
		$this->setup_container();

		// Fires the init action.
		did_action( 'papi/init' ) || do_action( 'papi/init' );
	}

	/**
	 * Load Localisation files.
	 *
	 * Locales found in:
	 * - WP_LANG_DIR/papi/papi-LOCALE.mo
	 * - WP_CONTENT_DIR/[mu-]plugins/papi/languages/papi-LOCALE.mo
	 */
	protected function load_textdomain() {
		$locale = function_exists( 'get_user_local' ) ? get_user_local() : get_locale();
		$locale = apply_filters( 'plugin_locale', $locale, 'papi' );

		load_textdomain( 'papi', WP_LANG_DIR . '/papi/papi-' . $locale . '.mo' );
		load_textdomain( 'papi', PAPI_PLUGIN_DIR . '../languages/papi-' . $locale . '.mo' );
	}

	/**
	 * Require files.
	 */
	protected function require_files() {
		// Require the autoload class.
		require_once __DIR__ . '/core/class-papi-core-autoload.php';

		$lib_path     = __DIR__ . '/lib/';
		$lib_includes = [
			'core/cache.php',
			'core/conditional.php',
			'core/data.php',
			'core/deprecated.php',
			'core/meta.php',
			'core/store.php',
			'core/post.php',
			'core/io.php',
			'core/property.php',
			'core/slug.php',
			'core/tabs.php',
			'core/taxonomy.php',
			'core/template.php',
			'core/url.php',
			'core/utilities.php',
			'hooks/actions.php',
			'hooks/filters.php',
			'hooks/filters-page-type.php',
			'hooks/filters-taxonomy-type.php',
			'fields/page.php',
			'fields/option.php',
			'fields/taxonomy.php',
			'types/entry.php',
			'types/page.php',
			'types/option.php',
			'types/taxonomy.php'
		];

		// Require function files.
		foreach ( $lib_includes as $file ) {
			if ( file_exists( $lib_path . $file ) ) {
				require_once $lib_path . $file;
			}
		}

		unset( $file );

		// Require admin classes that should not be loaded by the autoload.
		require_once __DIR__ . '/admin/class-papi-admin.php';
		require_once __DIR__ . '/admin/class-papi-admin-ajax.php';
		require_once __DIR__ . '/admin/class-papi-admin-assets.php';
		require_once __DIR__ . '/admin/class-papi-admin-menu.php';

		// Require conditional rules that should not be loaded by the autoload.
		require_once __DIR__ . '/core/class-papi-core-conditional-rules.php';

		// Require REST API classes.
		require_once __DIR__ . '/rest-api/class-papi-rest-api.php';

		// Load Papi CLI class if WP CLI is used.
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			require_once __DIR__ . '/cli/class-papi-cli.php';
		}
	}

	/**
	 * Deactivate Papi if the WordPress version is lower then 4.6.
	 */
	public static function deactivate() {
		// Remove Papi from plugins_loaded action.
		remove_action( 'plugins_loaded', 'papi' );

		// Load deactivate_plugins if it don't exists.
		if ( ! function_exists( 'deactivate_plugins' ) ) {
			include_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		deactivate_plugins( PAPI_PLUGIN_BASENAME );

		wp_die( esc_html__( 'WordPress 4.6 and higher required to run Papi! The plugin has now disabled itself.', 'papi' ) );

		// Remove instance.
		self::$instance = null;
	}

	/**
	 * Reset container.
	 */
	public function reset() {
		parent::reset();

		$this->setup_container();
	}

	/**
	 * Setup container.
	 */
	protected function setup_container() {
		$this->singleton( 'data', function ( $type = 'post' ) {
			return new Papi_Core_Data( $type );
		} );
	}
}

/**
 * Return the instance of Papi to everyone.
 *
 * @return Papi_Loader
 */
function papi() {
	if ( version_compare( get_bloginfo( 'version' ), '4.6', '<' ) ) {
		Papi_Loader::deactivate();
	}

	return Papi_Loader::instance();
}

add_action( 'plugins_loaded', 'papi' );


<?php

/**
 * Dropdown property.
 */
class Papi_Property_Dropdown extends Papi_Property {

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

	/**
	 * 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 ) {
		if ( ! $this->get_setting( 'multiple' ) ) {
			return $this->is_string_items() ? $value : papi_cast_string_value( $value );
		}

		$value = is_array( $value ) ? $value : [];

		if ( ! $this->is_string_items() ) {
			$value = array_map( 'papi_cast_string_value', $value );
		}

		return $value;
	}

	/**
	 * Determine if items are strings or not.
	 *
	 * @return bool
	 */
	public function is_string_items() {
		$items = $this->get_items();

		if ( empty( $items ) ) {
			return false;
		}

		$items = array_values( $items );

		return is_string( $items[0] );
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'allow_clear' => true,
			'placeholder' => null,
			'items'       => [],
			'multiple'    => false,
			'selected'    => [],
			'select2'     => true
		];
	}

	/**
	 * Get dropdown items.
	 *
	 * @return array
	 */
	protected function get_items() {
		return papi_to_array( $this->get_setting( 'items', [] ) );
	}

	/**
	 * Render property html.
	 */
	public function html() {
		// Setup variables needed.
		$settings     = $this->get_settings();
		$value        = $this->get_value();
		$options_html = [];

		// Properties that extends dropdown property
		// maybe don't have this setting.
		if ( ! isset( $settings->selected ) ) {
			$settings->selected = [];
		}

		// Properties that extends dropdown property
		// maybe don't have this setting.
		if ( ! isset( $settings->multiple ) ) {
			$settings->multiple = false;
		}

		// Override selected setting with
		// database value if not empty.
		if ( ! papi_is_empty( $value ) ) {
			$settings->selected = $value;
		}

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

		// Add placeholder if any.
		if ( ! is_null( $settings->placeholder ) ) {
			$options_html[] = sprintf( '<option value="%s">%s</option>', esc_attr( $this->get_option( 'default', '' ) ), esc_html( $placeholder ) );
		}

		$classes = 'papi-fullwidth';

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

		// Create option html tags for all items.
		foreach ( $this->get_items() as $key => $value ) {
			$key = is_numeric( $key ) ? $value : $key;

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

			// When a multiple we need to check with
			// `in_array` since the value is a array.
			if ( $settings->multiple ) {
				$selected = in_array( $value, $settings->selected, true );
			} else {
				$selected = papi_convert_to_string( $value ) === papi_convert_to_string( $settings->selected );
			}

			$options_html[] = papi_html_tag( 'option', [
				'data-edit-url' => false,
				'data-new-url'  => false,
				'selected'      => $selected ? 'selected' : null,
				'value'         => $value,
				esc_html( papi_convert_to_string( $key ) )
			] );
		}

		// When selectize is empty this will be used.
		papi_render_html_tag( 'input', [
			'name' => $this->html_name() . ( $settings->multiple ? '[]' : '' ),
			'type' => 'hidden'
		] );

		papi_render_html_tag( 'select', [
			'class'            => $classes,
			'data-allow-clear' => $settings->allow_clear,
			'data-placeholder' => $placeholder,
			'data-width'       => '100%',
			'id'               => $this->html_id() . ( $settings->multiple ? '[]' : '' ),
			'multiple'         => $settings->multiple ? 'multiple' : null,
			'name'             => $this->html_name() . ( $settings->multiple ? '[]' : '' ),
			$options_html
		] );
	}

	/**
	 * 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->get_setting( 'multiple' ) );
	}

	/**
	 * 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 );
	}
}


<?php

/**
 * Group property that render child properties in a table.
 */
class Papi_Property_Group extends Papi_Property_Repeater {

	/**
	 * 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_array( $value ) ) {
			return [];
		}

		// Fixes issue when flexible and repeater are used with group property
		// cache since we use repeaters format value function that cache values.
		// At this point the value array don't know about the parent structure.
		if ( $this->get_parent_property() instanceof Papi_Property_Repeater ) {
			$this->cache = false;
		}

		$value = parent::format_value( $value, $slug, $post_id );

		return array_shift( $value );
	}

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

	/**
	 * Get settings properties.
	 *
	 * @return array
	 */
	protected function get_settings_properties() {
		$settings = $this->get_settings();

		if ( is_null( $settings ) ) {
			return [];
		}

		return array_filter( array_map( 'papi_property', papi_to_array( $settings->items ) ), 'papi_is_property' );
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$properties = $this->get_settings_properties();
		$properties = $this->prepare_properties( $properties );

		// Fix so group is not render over the title and description.
		if ( $this->get_option( 'layout' ) === 'vertical' ) {
			echo '
';
		}

		echo '
html_name() ) . '">';
			papi_render_properties( $properties );
		echo '
';
	}

	/**
	 * Prepare properties and set right slug and value.
	 *
	 * @param  array $properties
	 *
	 * @return array
	 */
	protected function prepare_properties( $properties ) {
		$result = [];
		$value  = $this->get_value();
		$value  = is_array( $value ) ? $value : [];

		foreach ( $properties as $property ) {
			$render_property = clone $property->get_options();
			$value_slug      = $property->get_slug( true );

			if ( array_key_exists( $value_slug, $value ) ) {
				$render_property->value = $value[$value_slug];
			} else {
				$render_property->value = null;
			}

			$render_property->slug = $this->html_name( $property );

			$result[] = $render_property;
		}

		return $result;
	}

	/**
	 * Update value before it's saved to the database.
	 *
	 * @param mixed  $values
	 * @param string $slug
	 * @param int    $post_id
	 *
	 * @return array
	 */
	public function update_value( $values, $slug, $post_id ) {
		if ( ! isset( $values[0] ) && ! empty( $values ) ) {
			$values = [$values];
		}

		return parent::update_value( $values, $slug, $post_id );
	}
}


<?php

/**
 * WordPress media file property.
 */
class Papi_Property_File extends Papi_Property {

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

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * File type.
	 *
	 * @var string
	 */
	protected $file_type  = 'file';

	/**
	 * 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 ) {
		if ( ! is_array( $value ) ) {
			$value = $this->get_file( $value );
		}

		// Single file.
		if ( is_numeric( $value ) ) {
			$meta = wp_get_attachment_metadata( $value );

			if ( isset( $meta ) && ! empty( $meta ) ) {
				$att  = get_post( $value );
				$mine = [
					'alt'         => trim( strip_tags( get_post_meta( $value, '_wp_attachment_image_alt', true ) ) ),
					'caption'     => trim( strip_tags( $att->post_excerpt ) ),
					'description' => trim( strip_tags( $att->post_content ) ),
					'id'          => intval( $value ),
					'is_image'    => (bool) wp_attachment_is_image( $value ),
					'title'       => esc_html( $att->post_title ),
					'url'         => wp_get_attachment_url( $value ),
				];

				$meta = is_array( $meta ) ? $meta : ['file' => $meta];

				if ( isset( $meta['sizes'] ) ) {
					foreach ( array_keys( $meta['sizes'] ) as $size ) {
						if ( $src = wp_get_attachment_image_src( $mine['id'], $size ) ) {
							$meta['sizes'][$size]['url'] = $src[0];
						}
					}
				}

				$value = (object) array_merge( $meta, $mine );

				if ( ! papi_is_admin() && $this->get_setting( 'fields' ) === 'ids' ) {
					return $this->get_file_value( $value );
				}

				return $value;
			}

			return (int) $value;
		}

		// Multiple files.
		if ( is_array( $value ) ) {
			foreach ( $value as $k => $v ) {
				$value[$k] = $this->format_value( $v, $slug, $post_id );
			}

			return $value;
		}

		if ( is_object( $value ) && ! isset( $value->url ) ) {
			return;
		}

		return $value;
	}

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

	/**
	 * Get labels.
	 *
	 * @return array
	 */
	public function get_labels() {
		return [
			'add'     => __( 'Add file', 'papi' ),
			'no_file' => __( 'No file selected', 'papi' )
		];
	}

	/**
	 * Get file by id or meta value.
	 *
	 * @param  mixed $value
	 *
	 * @return int
	 */
	public function get_file( $value ) {
		$meta_key = $this->get_setting( 'meta_key' );

		if ( empty( $meta_key ) ) {
			if ( is_numeric( $value ) && intval( $value ) !== 0 ) {
				$post = get_post( $value );
			}
		} else if ( ! empty( $value ) ) {
			$args = [
				'fields'         => 'ids',
				'meta_key'       => $meta_key,
				'meta_value'     => $value,
				'posts_per_page' => 1,
				'post_type'      => 'attachment',
				'post_status'    => 'any'
			];

			$query = new WP_Query( $args );

			if ( ! empty( $query->posts ) ) {
				$post = get_post( $query->posts[0] );
			}
		}

		if ( empty( $post ) ) {
			return $value;
		}

		return $post->ID;
	}

	/**
	 * Get matching value based on key from a file.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	protected function get_file_value( $value ) {
		$meta_key = $this->get_setting( 'meta_key' );

		if ( ! is_object( $value ) ) {
			return 0;
		}

		if ( empty( $meta_key ) ) {
			return $value->id;
		}

		if ( $value = get_post_meta( $value->id, $meta_key, true ) ) {
			return $value;
		}

		return 0;
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$css_classes = '';
		$labels      = $this->get_labels();
		$settings    = $this->get_settings();
		$slug        = $this->html_name();
		$value       = papi_to_array( $this->get_value() );

		// Keep only valid objects.
		$value = array_filter( $value, function ( $item ) {
			return is_object( $item ) && isset( $item->id ) && ! empty( $item->id );
		} );

		$show_button = empty( $value );

		if ( $settings->multiple ) {
			$css_classes .= ' multiple ';
			$slug .= '[]';
			$show_button = true;
		}
		?>

		<div class="papi-property-file <?php echo esc_attr( $css_classes ); ?>" data-file-type="<?php echo esc_attr( $this->file_type ); ?>">
			<p class="papi-file-select <?php echo $show_button ? '' : 'papi-hide'; ?>">
				<?php
				if ( ! $settings->multiple ) {
					echo esc_html( $labels['no_file'] ) . ' ';
				}

				papi_render_html_tag( 'input', [
					'name'  => esc_attr( $slug ),
					'type'  => 'hidden',
					'value' => ''
				] );

				papi_render_html_tag( 'button', [
					'data-slug' => esc_attr( $slug ),
					'class'     => 'button',
					'type'      => 'button',

					esc_html( $labels['add'] )
				] );
				?>

				<?php
				if ( is_array( $value ) ):
					foreach ( $value as $file ):
						if ( ! is_object( $file ) ) {
							continue;
						}

						$url = wp_get_attachment_thumb_url( $file->id );

						if ( empty( $url ) ) {
							$url = wp_mime_type_icon( $file->id );
						}
						?>

							×

										<?php
										papi_render_html_tag( 'img', [
											'alt' => esc_attr( $file->alt ),
											'src' => esc_attr( $url )
										] );

										papi_render_html_tag( 'input', [
											'name'  => esc_attr( $slug ),
											'type'  => 'hidden',
											'value' => $file->id
										] );

										if ( ! isset( $file->file ) && isset( $file->url ) ) {
											$file->file = $file->url;
										}
										?>

									<?php if ( $this->file_type === 'file' ): ?>

<?php echo esc_html( basename( $file->file ) ); ?>

									<?php endif; ?>

					<?php
					endforeach;
				endif;
				?>

	<?php
	}

	/**
	 * Check if the given id is a attachment post type or not.
	 *
	 * @param  int $id
	 *
	 * @return bool
	 */
	protected function is_attachment( $id ) {
		return get_post_type( (int) $id ) === 'attachment';
	}

	/**
	 * Render file template.
	 */
	public function render_file_template() {
		?>

		<?php
	}

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

	/**
	 * Setup filters.
	 */
	protected function setup_filters() {
		add_action( 'wp_get_attachment_metadata', [$this, 'wp_get_attachment_metadata'], 10, 2 );
	}

	/**
	 * Save file with a meta value or id.
	 *
	 * @param  mixed  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return string
	 */
	public function update_value( $values, $slug, $post_id ) {
		if ( ! is_array( $values ) ) {
			$values = $this->get_file_value( (object) [
				'id' => $values
			] );

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

			return $values;
		}

		foreach ( $values as $index => $value ) {
			if ( ! is_numeric( $value ) ) {
				continue;
			}

			$values[$index] = $this->get_file_value( (object) [
				'id' => $value
			] );
		}

		return array_filter( $values );
	}

	/**
	 * Get attachment metadata.
	 *
	 * @param  mixed $data
	 * @param  int   $post_id
	 *
	 * @return mixed
	 */
	public function wp_get_attachment_metadata( $data, $post_id ) {
		if ( papi_is_empty( $data ) ) {
			return get_post_meta( $post_id, '_wp_attached_file', true );
		}

		return $data;
	}
}


<?php

/**
 * Container class that implements a container.
 */
class Papi_Core_Container implements ArrayAccess {

	/**
	 * The classes holder.
	 *
	 * @var array
	 */
	protected $classes = [];

	/**
	 * The keys holder.
	 *
	 * @var array
	 */
	protected $keys = [];

	/**
	 * The values holder.
	 *
	 * @var array
	 */
	protected $values = [];

	/**
	 * Set a parameter or an object.
	 *
	 * @param string $id
	 * @param mixed  $value
	 * @param bool   $singleton
	 *
	 * @throws Exception when singleton cannot be rebind.
	 *
	 * @return mixed
	 */
	public function bind( $id, $value = null, $singleton = false ) {
		if ( is_string( $id ) && $this->is_singleton( $id ) ) {
			throw new Exception( sprintf( 'Identifier `%s` is a singleton and cannot be rebind', $id ) );
		}

		if ( is_object( $id ) && get_class( $id ) !== false ) {
			$value              = $id;
			$id                 = $this->get_class_prefix( get_class( $id ), false );
			$this->classes[$id] = true;
		}

		if ( $value instanceof Closure ) {
			$closure = $value;
		} else {
			$closure = $this->get_closure( $value, $singleton );
		}

		$this->values[$id] = compact( 'closure', 'singleton' );
		$this->keys[$id]   = true;

		return $value;
	}

	/**
	 * Call closure.
	 *
	 * @param mixed $closure
	 * @param array $parameters
	 *
	 * @return Closure
	 */
	protected function call_closure( $closure, array $parameters = [] ) {
		if ( $closure instanceof Closure ) {
			$rc      = new ReflectionFunction( $closure );
			$args    = $rc->getParameters();
			$params  = $parameters;
			$classes = [
				$this->get_class_prefix( get_class( $this ) ),
				get_class( $this ),
				get_parent_class( $this )
			];

			foreach ( $args as $index => $arg ) {
				if ( $arg->getClass() === null ) {
					continue;
				}

				if ( in_array( $arg->getClass()->name, $classes, true ) ) {
					$parameters[$index] = $this;
				} else if ( $this->exists( $arg->getClass()->name ) ) {
					$parameters[$index] = $this->make( $arg->getClass()->name );
				}
			}

			if ( ! empty( $args ) && empty( $parameters ) ) {
				$parameters[0] = $this;
			}

			if ( count( $args ) > count( $parameters ) ) {
				$parameters = array_merge( $parameters, $params );
			}

			return $this->call_closure( call_user_func_array( $closure, $parameters ), $parameters );
		}

		return $closure;
	}

	/**
	 * Check if identifier is set or not.
	 *
	 * @param string $id
	 *
	 * @return bool
	 */
	public function exists( $id ) {
		return isset( $this->keys[$this->get_class_prefix( $id )] );
	}

	/**
	 * Get closure function.
	 *
	 * @param mixed $value
	 * @param bool  $singleton
	 *
	 * @return Closure
	 */
	protected function get_closure( $value, $singleton = false ) {
		return function () use ( $value, $singleton ) {
			return $value;
		};
	}

	/**
	 * Get class prefix.
	 *
	 * @param string $id
	 * @param bool   $check
	 *
	 * @return string
	 */
	protected function get_class_prefix( $id, $check = true ) {
		if ( strpos( $id, '\\' ) !== false && $id[0] !== '\\' ) {
			$class = '\\' . $id;

			if ( $check ) {
				return isset( $this->classes[$class] ) ? $class : $id;
			}

			return $class;
		}

		return $id;
	}

	/**
	 * Determine if a given type is a singleton or not.
	 *
	 * @param string $id
	 *
	 * @throws InvalidArgumentException if argument is not string.
	 *
	 * @return bool
	 */
	public function is_singleton( $id ) {
		if ( ! is_string( $id ) ) {
			throw new InvalidArgumentException( 'Invalid argument. Must be string.' );
		}

		if ( ! $this->exists( $id ) ) {
			return false;
		}

		$id = $this->get_class_prefix( $id );

		return $this->values[$id]['singleton'] === true;
	}

	/**
	 * Resolve the given type from the container.
	 *
	 * @param string $id
	 * @param array  $parameters
	 *
	 * @throws Exception if identifier is not defined.
	 *
	 * @return Closure
	 */
	public function make( $id, array $parameters = [] ) {
		if ( ! $this->exists( $id ) ) {
			throw new Exception( sprintf( 'Identifier `%s` is not defined', $id ) );
		}

		$id      = $this->get_class_prefix( $id );
		$value   = $this->values[$id];
		$closure = $value['closure'];

		return $this->call_closure( $closure, $parameters );
	}

	/**
	 * Add key and value to the container once.
	 *
	 * @param  string $key
	 * @param  mixed  $callback
	 *
	 * @return mixed
	 */
	public function once( $key, $callback ) {
		if ( ! is_string( $key ) && ! is_callable( $callback ) ) {
			return;
		}

		if ( ! $this->exists( $key ) ) {
			$result = $callback();
			$this->singleton( $key, $result );
		}

		return $this->make( $key );
	}

	/**
	 * Unset value by identifier.
	 *
	 * @param string $id
	 */
	public function remove( $id ) {
		$id = $this->get_class_prefix( $id );
		unset( $this->keys[$id], $this->values[$id] );
	}

	/**
	 * Reset container values.
	 */
	public function reset() {
		unset( $this->keys, $this->values, $this->classes );
	}

	/**
	 * Set a parameter or an object.
	 *
	 * @param string $id
	 * @param mixed  $value
	 *
	 * @return mixed
	 */
	public function singleton( $id, $value = null ) {
		return $this->bind( $id, $value, true );
	}

	/**
	 * Check if identifier is set or not.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $id
	 *
	 * @return bool
	 */
	public function offsetExists( $id ) {
		return $this->exists( $id );
	}

	/**
	 * Get value by identifier.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $id
	 *
	 * @return mixed
	 */
	public function offsetGet( $id ) {
		return $this->make( $id );
	}

	/**
	 * Set a parameter or an object.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $id
	 * @param mixed  $value
	 */
	public function offsetSet( $id, $value ) {
		$this->bind( $id, $value );
	}

	/**
	 * Unset value by identifier.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $id
	 */
	public function offsetUnset( $id ) {
		$this->remove( $id );
	}
}


<?php

/**
 * Admin class that handles admin menus.
 */
final class Papi_Admin_Menu {

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

	/**
	 * Fill labels on admin bar.
	 */
	public function admin_bar_menu() {
		if ( $entry_type = $this->get_entry_type() ) {
			$this->override_labels( $entry_type );
		}
	}

	/**
	 * Get current type class.
	 *
	 * @return Papi_Entry_Type
	 */
	protected function get_entry_type() {
		if ( $entry_type = papi_get_entry_type_by_id( papi_get_entry_type_id() ) ) {
			return $entry_type;
		}
	}

	/**
	 * Override labels with labels from the entry type.
	 *
	 * @param Papi_Entry_Type $entry_type
	 */
	protected function override_labels( Papi_Entry_Type $entry_type ) {
		global $wp_post_types, $wp_taxonomies;

		if ( $entry_type->get_type() === 'taxonomy' ) {
			$meta_type_value = papi_get_taxonomy();
		} else {
			$meta_type_value = papi_get_post_type();
		}

		if ( empty( $meta_type_value ) || ( ! isset( $wp_post_types[$meta_type_value] ) && ! isset( $wp_taxonomies[$meta_type_value] ) ) ) {
			return;
		}

		foreach ( $entry_type->get_labels() as $key => $value ) {
			if ( empty( $value ) ) {
				continue;
			}

			if ( $entry_type->get_type() === 'taxonomy' && isset( $wp_taxonomies[$meta_type_value]->labels->$key ) ) {
				$wp_taxonomies[$meta_type_value]->labels->$key = $value;
			} else if ( isset( $wp_post_types[$meta_type_value]->labels->$key ) ) {
				$wp_post_types[$meta_type_value]->labels->$key = $value;
			}
		}
	}

	/**
	 * Page items menu.
	 *
	 * This function will register all entry types
	 * that has a fake post type. Like option types.
	 */
	public function page_items_menu() {
		$entry_types = papi_get_all_entry_types( [
			'mode'  => 'exclude',
			'types' => 'page'
		] );

		foreach ( $entry_types as $entry_type ) {
			if ( empty( $entry_type->get_menu() ) || empty( $entry_type->name ) ) {
				continue;
			}

			$slug = sprintf(
				'papi/%s/%s',
				$entry_type->get_type(),
				$entry_type->get_id()
			);

			add_submenu_page(
				$entry_type->get_menu(),
				$entry_type->name,
				$entry_type->name,
				$entry_type->capability,
				$slug,
				[$entry_type, 'render']
			);
		}
	}

	/**
	 * Setup menu items for real post types.
	 */
	public function post_types_menu() {
		global $submenu;

		$post_types = papi_get_post_types();

		foreach ( $post_types as $post_type ) {
			if ( ! post_type_exists( $post_type ) ) {
				continue;
			}

			if ( $post_type === 'post' ) {
				$edit_url = 'edit.php';
			} else {
				$edit_url = 'edit.php?post_type=' . $post_type;
			}

			if ( ! isset( $submenu[$edit_url], $submenu[$edit_url][10], $submenu[$edit_url][10][2] ) ) {
				$post_type_object = get_post_type_object( $post_type );

				if ( $post_type_object->show_in_menu !== true ) {
					$submenu[$edit_url] = [
						10 => [
							__( 'Add New', 'papi' ),
							'edit_posts',
							'post-new.php'
						]
					];
				} else {
					continue;
				}
			}

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

			// Don't change menu item when no page types is found.
			if ( empty( $page_types ) ) {
				continue;
			}

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

			if ( ! empty( $only_page_type ) && ! $show_standard ) {
				$submenu[$edit_url][10][2] = papi_get_page_new_url(
					$only_page_type,
					false,
					$post_type,
					[
						'post_parent',
						'lang'
					]
				);
			} else {
				$page  = 'papi-add-new-page,' . $post_type;
				$start = strpos( $edit_url, 'post_type' ) === false ? '?' : '&';

				$submenu[$edit_url][10][2] = sprintf(
					'%s%spage=%s',
					$edit_url,
					$start,
					$page
				);

				// Add menu item.
				add_menu_page(
					__( 'Add New', 'papi' ),
					__( 'Add New', 'papi' ),
					'read',
					$page,
					[$this, 'render_view']
				);

				// Remove the menu item so it's hidden.
				remove_menu_page( $page );
			}
		}
	}

	/**
	 * Menu callback that loads right view depending on what the `page` query string says.
	 */
	public function render_view() {
		if ( strpos( papi_get_qs( 'page' ), 'papi' ) !== false ) {
			$page = str_replace( 'papi-', '', papi_get_qs( 'page' ) );
			$res  = preg_replace( '/\,.*/', '', $page );

			if ( is_string( $res ) ) {
				$page_view = $res;
			}
		}

		if ( ! isset( $page_view ) ) {
			$page_view = null;
		}

		if ( ! is_null( $page_view ) ) {
			$view = new Papi_Admin_View;
			$view->render( $page_view );
		} else {
			echo '
Papi - 404
';
		}
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		if ( papi_is_admin() ) {
			add_action( 'admin_init', [$this, 'admin_bar_menu'] );
			add_action( 'admin_menu', [$this, 'page_items_menu'] );
			add_action( 'admin_menu', [$this, 'post_types_menu'] );
		} else {
			add_action( 'admin_bar_menu', [$this, 'admin_bar_menu'] );
		}
	}
}

new Papi_Admin_Menu;


<?php

/**
 * Autoload class that autoload classes.
 */
final class Papi_Core_Autoload {

	/**
	 * The Constructor.
	 *
	 * @codeCoverageIgnore
	 */
	public function __construct() {
		spl_autoload_register( [$this, 'autoload'] );
	}

	/**
	 * Autoload Papi classes.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $class
	 */
	public function autoload( $class ) {
		$class = strtolower( $class );
		$file  = 'class-' . str_replace( '_', '-', strtolower( $class ) ) . '.php';
		$path  = PAPI_PLUGIN_DIR;

		if ( strpos( $class, 'papi_admin' ) === 0 ) {
			$path .= 'admin/';
		} else if ( strpos( $class, 'papi_core_' ) === 0 ) {
			$path .= 'core/';
		} else if ( strpos( $class, 'papi_cli_' ) === 0 ) {
			$path .= 'cli/';
		} else if ( preg_match( '/^papi\_\w+\_store$/', $class ) ) {
			$path .= 'stores/';
		} else if ( strpos( $class, 'papi_property' ) === 0 ) {
			$path .= 'properties/';
		} else if ( strpos( $class, 'papi_query' ) === 0 ) {
			$path .= 'query/';
		} else if ( preg_match( '/^papi\_\w+\_type/', $class ) ) {
			$path .= 'types/';
		}

		if ( is_readable( $path . $file ) ) {
			require_once $path . $file;
		}
	}
}

new Papi_Core_Autoload;


<?php

/**
 * Term store implementation of Papi meta store.
 */
class Papi_Term_Store extends Papi_Core_Meta_Store {

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

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

	/**
	 * The constructor.
	 *
	 * @param int $id
	 */
	public function __construct( $id = 0 ) {
		$this->id         = papi_get_term_id( $id );
		$this->term       = get_term( $this->id, '' );
		$id               = papi_get_taxonomy_type_id( $this->id );
		$this->type_class = papi_get_entry_type_by_id( $id );
	}

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

	/**
	 * Get the WordPress term object.
	 *
	 * @return WP_Term
	 */
	public function get_term() {
		return $this->term;
	}

	/**
	 * Check if the term is a valid term object.
	 *
	 * @return bool
	 */
	public function valid() {
		return $this->term instanceof WP_Term;
	}

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

		if ( $taxonomy_type instanceof Papi_Taxonomy_Type === false ) {
			return;
		}

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


<?php

/**
 * Sidebar property that populates a dropdown.
 */
class Papi_Property_Sidebar extends Papi_Property_Dropdown {

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

	/**
	 * 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 string
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( is_admin() || ! $this->settings->render ) {
			return $value;
		}

		ob_start();
		if ( is_active_sidebar( $value ) ) {
			dynamic_sidebar( $value );
		}

		return ob_get_clean();
	}

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

	/**
	 * Get registered sidebars as dropdown items.
	 *
	 * @return array
	 */
	public function get_items() {
		global $wp_registered_sidebars;
		$items = [];

		foreach ( $wp_registered_sidebars as $item ) {
			$items[$item['name']] = $item['id'];
		}

		ksort( $items );

		return $items;
	}
}


<?php

/**
 * HTML5 url input property.
 */
class Papi_Property_Url extends Papi_Property {

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

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

		papi_render_html_tag( 'input', [
			'class'   => $settings->mediauploader ? 'papi-url-media-input' : null,
			'id'      => esc_attr( $this->html_id() ),
			'name'    => esc_attr( $this->html_name() ),
			'type'    => 'url',
			'value'   => $this->get_value()
		] );

		if ( $settings->mediauploader ) {
			echo ' ';

			papi_render_html_tag( 'input', [
				'class'            => 'button papi-url-media-button',
				'data-papi-action' => 'mediauploader',
				'id'               => esc_attr( $this->html_id() ),
				'name'             => esc_attr( $this->html_name() . '_button' ),
				'type'             => 'button',
				'value'            => esc_attr__( 'Select file', 'papi' )
			] );
		}
	}

	/**
	 * 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 ) {
		if ( filter_var( $value, FILTER_VALIDATE_URL ) ) {
			return $value;
		}
	}

	/**
	 * 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 ) {
		return $this->load_value( $value, $slug, $post_id );
	}
}


<?php

/**
 * Post property that can handle relationship
 * between posts.
 */
class Papi_Property_Post extends Papi_Property {

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

	/**
	 * 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 ) {
		$meta_key = $this->get_setting( 'meta_key' );

		if ( ! empty( $meta_key ) ) {
			$args = [
				'fields'         => 'ids',
				'meta_key'       => $meta_key,
				'meta_value'     => $value,
				'posts_per_page' => 1,
				'post_type'      => $this->get_setting( 'post_type' ),
			];

			$query = new WP_Query( $args );

			if ( ! empty( $query->posts ) ) {
				$value = $query->posts[0];
			}
		}

		$post = $this->default_value;

		// Allow only id to be returned.
		if ( ! papi_is_admin() && $this->get_setting( 'fields' ) === 'ids' ) {
			$post = $this->get_post_value( $value );
		} else if ( ! empty( $value ) ) {
			$post = get_post( $value );
		}

		if ( empty( $post ) ) {
			return $this->default_value;
		}

		if ( is_object( $post ) && empty( $post->ID ) ) {
			return $this->default_value;
		}

		return $post;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'allow_clear'   => true,
			'edit_url'      => true,
			'fields'        => '',
			'labels'        => [
				'select_post_type' => __( 'Select Post Type', 'papi' ),
				'select_item'      => __( 'Select %s', 'papi' )
			],
			'layout'        => 'single', // single or advanced
			'meta_key'      => '',
			'new_url'       => true,
			'placeholder'   => null,
			'post_type'     => 'post',
			'select2'       => true,
			'query'         => []
		];
	}

	/**
	 * Get labels from post types.
	 *
	 * @return array
	 */
	protected function get_labels() {
		$results = [];

		foreach ( $this->get_post_types() as $post_type ) {
			if ( post_type_exists( $post_type ) ) {
				$post_type_object    = get_post_type_object( $post_type );
				$results[$post_type] = $post_type_object->labels->menu_name;
			}
		}

		return $results;
	}

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

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

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

		if ( $layout !== 'advanced' ) {
			$post_type = $this->get_post_types();
		} else if ( empty( $post_type ) ) {
			$post_type = $this->get_post_types();
			$post_type = array_shift( $post_type );
		}

		// 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
		] );

		$posts = ( new WP_Query( $args ) )->posts;

		// Keep only objects.
		$posts   = papi_get_only_objects( $posts );
		$results = [];

		foreach ( $posts as $post ) {
			$obj = get_post_type_object( $post->post_type );

			if ( empty( $obj ) ) {
				continue;
			}

			if ( ! isset( $results[$obj->labels->menu_name] ) ) {
				$results[$obj->labels->menu_name] = [];
			}

			$results[$obj->labels->menu_name][] = $post;
		}

		ksort( $results );

		return $results;
	}

	/**
	 * Get matching value based on key from a post.
	 *
	 * @param  mixed $post
	 *
	 * @return mixed
	 */
	protected function get_post_value( $post ) {
		$meta_key = $this->get_setting( 'meta_key' );

		if ( is_numeric( $post ) ) {
			$post_id = $post;
		} else if ( ! empty( $post ) ) {
			$post = get_post( $post );
			if ( $post instanceof WP_Post === false ) {
				return 0;
			}

			$post_id = $post->ID;
		}

		if ( empty( $post_id ) ) {
			return 0;
		}

		if ( ! empty( $meta_key ) ) {
			$value = get_post_meta( $post_id, $meta_key, true );
		} else {
			$value = $post_id;
		}

		return $value;
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings           = $this->get_settings();
		$layout             = $settings->layout;
		$labels             = $this->get_labels();
		$post_types         = $this->get_post_types();
		$render_label       = count( $post_types ) > 1;
		$advanced           = $render_label && $layout === 'advanced';
		$single             = $render_label && $layout !== 'advanced';
		$classes            = count( $post_types ) > 1 ? '' : 'papi-fullwidth';
		$value              = $this->get_value();
		$value              = $this->get_post_value( $value );
		$selected_label     = is_array( $labels ) && ! empty( $labels ) ? array_values( $labels )[0] : '';
		$selected_post_type = empty( $value ) ? '' : get_post_type( $value );
		$selected_post_type = empty( $selected_post_type ) ? '' : $selected_post_type;
		$posts              = $this->get_posts( $selected_post_type );

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

		// When new url is true and we have more than one post type
		// we need to use the advanced layout so the placeholder option
		// will get the right post type in the new url.
		if ( $settings->new_url && $render_label ) {
			$advanced = true;
			$single = false;
		}
		?>

		<div class="papi-property-post <?php echo $advanced ? 'advanced' : ''; ?>">
			<?php if ( $advanced ): ?>

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

						<select
							id="<?php echo esc_attr( $this->html_id() ); ?>_post_type"
							class="<?php echo esc_attr( $classes ); ?> papi-property-post-left"
							data-select-item="<?php echo esc_attr( $settings->labels['select_item'] ); ?>"
							data-post-query='<?php echo esc_attr( papi_maybe_json_encode( $settings->query ) ); ?>'
							data-width="100%"
						>
							<?php
							foreach ( $labels as $post_type => $label ) {
								$selected = $post_type === $selected_post_type;

								papi_render_html_tag( 'option', [
									'value'    => $post_type,
									'selected' => $selected,
									$label
								] );

								if ( $selected ) {
									$selected_label = $label;
								}
							}
							?>
						</select>

						<label for="<?php echo esc_attr( $this->html_id() ); ?>_posts">
							<?php echo esc_html( sprintf( $settings->labels['select_item'], $selected_label ) ); ?>

						<?php endif; ?>

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

						<select
							class="<?php echo esc_attr( $classes ); ?>  papi-property-post-right"
							id="<?php echo esc_attr( $this->html_id() ); ?>_posts"
							name="<?php echo esc_attr( $this->html_name() ); ?>"
							data-allow-clear="<?php echo is_null( $settings->placeholder ) ? 'false' : 'true'; ?>"
							data-placeholder="<?php echo esc_attr( $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_types[0] ) ); ?>"></option>
								<?php else: ?>
									<option></option>
								<?php endif; ?>
							<?php endif; ?>

							<?php foreach ( $posts as $label => $items ) : ?>

								<?php if ( $single ): ?>
									<optgroup label="<?php echo esc_attr( $label ); ?>">
								<?php endif; ?>

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

									papi_render_html_tag( 'option', [
										'data-allow-clear' => $settings->allow_clear,
										'data-edit-url'    => $settings->edit_url ? get_edit_post_link( $post ) : '',
										'data-new-url'     => $settings->new_url ? admin_url( 'post-new.php?post_type=' . $post->post_type ) : '',
										'selected'         => $value === $this->get_post_value( $post ),
										'value'            => $this->get_post_value( $post ),
										$post->post_title
									] );
								}
								?>

								<?php if ( $single ): ?>
									</optgroup>
								<?php endif; ?>

							<?php endforeach; ?>
						</select>

						<?php if ( $advanced ): ?>

		<?php endif; ?>

		<?php
	}

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

		?>

		<?php
	}

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