attachments ) && !empty( $instance->attachments ) ) {
foreach( $instance->attachments as $attachment ) {
// we need to give our Attachment a uid to carry through to all the fields
$attachment->uid = uniqid();
// we'll create the attachment
$this->create_attachment( $instance->name, $attachment );
}
}
?>
ATTACHMENTS_DIR . 'classes/fields/class.field.text.php',
'textarea' => ATTACHMENTS_DIR . 'classes/fields/class.field.textarea.php',
'select' => ATTACHMENTS_DIR . 'classes/fields/class.field.select.php',
'wysiwyg' => ATTACHMENTS_DIR . 'classes/fields/class.field.wysiwyg.php',
);
// support custom field types
// $field_types = apply_filters( 'attachments_fields', $field_types );
$field_index = 0;
foreach ( $field_types as $type => $path ) {
// proceed with inclusion
if ( file_exists( $path ) ) {
// include the file
include_once( $path );
// store the registered classes so we can single out what gets added
$existing_classes = get_declared_classes();
// we're going to use our Attachments class as a reference because
// during subsequent instantiations of Attachments (e.g. within template files)
// these field classes WILL NOT be added to the array again because
// we're using include_once() so that strategy is no longer useful
// determine it's class
$flag = array_search( 'Attachments_Field', $existing_classes );
// the field's class is next
$field_class = $existing_classes[$flag + $field_index + 1];
// create our link using our new field class
$field_types[ $type ] = $field_class;
$field_index++;
}
}
// send it back
return $field_types;
}
/**
* Registers a field type for use within an instance
*
* @since 3.0
*/
function register_field( $params = array() ) {
$defaults = array(
'name' => 'title',
'type' => 'text',
'label' => __( 'Title', 'attachments' ),
'meta' => array(),
);
$params = array_merge( $defaults, $params );
// ensure it's a valid type
if ( ! isset( $this->fields[$params['type']] ) ) {
return false;
}
// sanitize
if ( isset( $params['name'] ) ) {
$params['name'] = str_replace( '-', '_', sanitize_title( $params['name'] ) );
}
if ( isset( $params['label'] ) ) {
$params['label'] = __( esc_html( $params['label'] ) );
}
if ( !isset( $params['meta'] ) || !is_array( $params['meta'] ) ) {
$params['meta'] = array();
}
// instantiate the class for this field and send it back
return new $this->fields[ $params['type'] ]( $params['name'], $params['label'], $params['meta'] );
}
/**
* Registers an Attachments instance
*
* @since 3.0
*/
function register( $name = 'attachments', $params = array() ) {
$defaults = array(
// title of the meta box (string)
'label' => __( 'Attachments', 'attachments' ),
// all post types to utilize (string|array)
'post_type' => array( 'post', 'page' ),
// meta box position (string) (normal, side or advanced)
'position' => 'normal',
// meta box priority (string) (high, default, low, core)
'priority' => 'high',
// maximum number of Attachments (int) (-1 is unlimited)
'limit' => -1,
// allowed file type(s) (array) (image|video|text|audio|application)
'filetype' => null, // no filetype limit
// include a note within the meta box (string)
'note' => null, // no note
// by default new Attachments will be appended to the list
// but you can have then prepend if you set this to false
'append' => true,
// text for 'Attach' button (string)
'button_text' => __( 'Attach', 'attachments' ),
// text for modal 'Attach' button (string)
'modal_text' => __( 'Attach', 'attachments' ),
// which tab should be the default in the modal (string) (browse|upload)
'router' => 'browse',
// fields for this instance (array)
'fields' => array(
array(
'name' => 'title', // unique field name
'type' => 'text', // registered field type
'label' => __( 'Title', 'attachments' ), // label to display
'default' => 'title', // default value upon selection
),
array(
'name' => 'caption', // unique field name
'type' => 'wysiwyg', // registered field type
'label' => __( 'Caption', 'attachments' ), // label to display
'default' => 'caption', // default value upon selection
),
),
);
$params = array_merge( $defaults, $params );
// sanitize
if ( ! is_array( $params['post_type'] ) ) {
$params['post_type'] = array( $params['post_type'] ); // we always want an array
}
if ( ! is_array( $params['filetype'] ) ) {
$params['filetype'] = array( $params['filetype'] ); // we always want an array
}
$params['label'] = esc_html( $params['label'] );
$params['limit'] = intval( $params['limit'] );
$params['note'] = esc_html( $params['note'] );
$params['button_text'] = esc_attr( $params['button_text'] );
$params['modal_text'] = esc_attr( $params['modal_text'] );
// make sure we've got valid filetypes
if ( is_array( $params['filetype'] ) ) {
foreach ( $params['filetype'] as $key => $filetype ) {
if ( ! in_array( $filetype, $this->valid_filetypes ) ) {
unset( $params['filetype'][ $key ] );
}
}
}
// WordPress sanitizes post type names when registering, so we will too
foreach ( $params['post_type'] as $key => $post_type ) {
$params['post_type'][ $key ] = sanitize_key( $post_type );
}
// make sure the instance name is proper
$instance = str_replace( '-', '_', sanitize_title( $name ) );
// register the fields
if ( isset( $params['fields'] ) && is_array( $params['fields'] ) && count( $params['fields'] ) ) {
foreach( $params['fields'] as $field ) {
// register the field
$this->register_field( $field );
}
}
// set the instance
$this->instances[ $instance ] = $params;
// set the Attachments for this instance
$this->instances[ $instance ]['attachments'] = $this->get_attachments( $instance );
}
/**
* Gets the applicable Attachments instances for the current post type
*
* @since 3.0
*/
function get_instances_for_post_type( $post_type = null ) {
$post_type = ( ! is_null( $post_type ) && post_type_exists( $post_type ) ) ? $post_type : $this->get_post_type();
$instances = array();
if ( ! empty( $this->instances ) ) {
foreach ( $this->instances as $name => $params ) {
if ( in_array( $post_type, $params['post_type'] ) ) {
$instances[] = $name;
}
}
}
return $instances;
}
/**
* Our own implementation of WordPress' get_post_type() as it's not
* functional when we need it
*
* @since 3.0
*/
function get_post_type() {
global $post;
// TODO: Retrieving the post_type at this point is ugly to say the least. This needs major cleanup.
if ( empty( $post->ID ) && isset( $_GET['post_type'] ) ) {
$post_type = sanitize_text_field( $_GET['post_type'] );
} elseif( !empty( $post->ID ) ) {
$post_type = get_post_type( $post->ID );
} elseif( isset( $_GET['post'] ) ) {
$post_type = get_post_type( intval( $_GET['post'] ) );
} else {
$post_type = 'post';
}
return $post_type;
}
/**
* Sets the applicable Attachments instances for the current post type
*
* @since 3.0
*/
function set_instances_for_current_post_type() {
// store the applicable instances for this post type
$this->instances_for_post_type = $this->get_instances_for_post_type( $this->get_post_type() );
}
/**
* Outputs HTML for a single Attachment within an instance
*
* @since 3.0
*/
function create_attachment_field( $instance, $field, $attachment = null ) {
// the $field at this point is just the user-declared array
// we need to make it a field object
$type = $field['type'];
if( isset( $this->fields[ $type ] ) ) {
$name = sanitize_title( $field['name'] );
$label = esc_html( $field['label'] );
$default = isset( $field['default'] ) ? $field['default'] : false; // validated in the class
$meta = isset( $field['meta'] ) ? $field['meta'] : array();
$value = isset( $attachment->fields->$name ) ? $attachment->fields->$name : null;
$field = new $this->fields[ $type ]( $name, $label, $value, $meta );
$field->value = $field->format_value_for_input( $field->value );
// does this field already have a unique ID?
$uid = ( isset( $attachment->uid ) ) ? $attachment->uid : null;
// TODO: make sure we've got a registered instance
$field->set_field_instance( $instance, $field );
$field->set_field_identifiers( $field, $uid );
$field->set_field_type( $type );
$field->set_field_default( $default );
?>
create_field( $instance, $field ); ?>
html( $field );
}
/**
* Outputs all the necessary markup for an Attachment
*
* @since 3.0
*/
function create_attachment( $instance, $attachment = null ) {
?>
uid ) ) ? $attachment->uid : '{{ attachments.attachment_uid }}'; ?>
id ) ) {
// we'll just use the full size since that's what Media in 3.5 uses
$attachment_meta = wp_get_attachment_metadata( $attachment->id );
// only images return the 'file' key
if ( ! isset( $attachment_meta['file'] ) ) {
$attachment_meta['file'] = get_attached_file( $attachment->id );
$filename = explode( "/", $attachment_meta['file'] );
}
$attachment->width = isset( $attachment_meta['width'] ) ? $attachment_meta['width'] : null;
$attachment->height = isset( $attachment_meta['height'] ) ? $attachment_meta['height'] : null;
$attachment->filename = basename( $attachment_meta['file'] );
$attachment_mime = explode( '/', get_post_mime_type( $attachment->id ) );
$attachment->type = isset( $attachment_mime[0] ) ? $attachment_mime[0] : null;
$attachment->subtype = isset( $attachment_mime[1] ) ? $attachment_mime[1] : null;
}
?>
instances_for_post_type ) || empty( $post ) ) {
return;
}
// all metaboxes have been put in place, we can now determine which field assets need to be included
// first we'll get a list of the field types on screen
$fieldtypes = array();
foreach ( $this->instances_for_post_type as $instance ) {
foreach ( $this->instances[$instance]['fields'] as $field ) {
$fieldtypes[] = $field['type'];
}
}
// we only want to dump out assets once for each field type
$fieldtypes = array_unique( $fieldtypes );
// loop through and dump out all the assets
foreach ( $fieldtypes as $fieldtype ) {
$field = new $this->fields[ $fieldtype ];
$field->assets();
}
}
/**
* Callback to fire the init() function for reach registered field
*
* @since 3.1
*/
function field_inits() {
global $post;
// we only want to enqueue if we're on an edit screen and it's applicable
if ( empty( $this->instances_for_post_type ) || empty( $post ) ) {
return;
}
// all metaboxes have been put in place, we can now determine which field assets need to be included
// first we'll get a list of the field types on screen
$fieldtypes = array();
foreach ( $this->instances_for_post_type as $instance ) {
foreach( $this->instances[ $instance ]['fields'] as $field ) {
$fieldtypes[] = $field['type'];
}
}
// we only want to dump out assets once for each field type
$fieldtypes = array_unique( $fieldtypes );
// loop through and dump out all the assets
foreach( $fieldtypes as $fieldtype ) {
$field = new $this->fields[ $fieldtype ];
$field->init();
}
}
/**
* Outputs all necessary Backbone templates
* Each Backbone template includes each field present in an instance
*
* @since 3.0
*/
function admin_footer() {
if ( ! empty( $this->instances_for_post_type ) ) { ?>
instances_for_post_type as $instance ) : ?>
save_metadata( $post_id, $attachments_meta );
return $post_id;
}
/**
* Processes submitted fields and saves Attachments' post metadata
*
* @param int $post_id The post ID
* @param array $attachments_meta Multidimenaional array containing Attachments data
* @return bool
* @since 3.4.3
*/
function save_metadata( $post_id = 0, $attachments_meta = null ) {
if ( ! is_array( $attachments_meta ) || ! is_int( $post_id ) || intval( $post_id ) < 1 ) {
return false;
}
// final data store
$attachments = array();
// loop through each submitted instance
foreach ( $attachments_meta as $instance => $instance_attachments ) {
// loop through each Attachment of this instance
foreach( $instance_attachments as $key => $attachment ) {
// see if it was pulled as JSON from a delete cleanup
if ( is_object( $attachment ) ) {
$attachment = get_object_vars( $attachment );
if ( is_array( $attachment ) && !empty( $attachment ) ) {
if ( isset( $attachment['fields'] ) && is_object( $attachment['fields'] ) ) {
$attachment['fields'] = get_object_vars( $attachment['fields'] );
}
}
}
$attachment_exists = isset( $attachment['id'] ) ? get_post( absint( $attachment['id'] ) ) : false;
// make sure the attachment exists
if ( $attachment_exists ) {
// since we're using JSON for storage in the database, we need
// to make sure that characters are encoded that would otherwise
// break the JSON
if ( isset( $attachment['fields'] ) && is_array( $attachment['fields'] ) ) {
foreach( $attachment['fields'] as $key => $field_value ) {
// take care of our returns
$field_value = str_replace( "\r\n", "\n", $field_value );
$field_value = str_replace( "\r", "\n", $field_value );
// we don't want to strip out our newlines so we're going to flag them
$field_value = str_replace("\n", "%%ATTACHMENTS_NEWLINE%%", $field_value );
// slashes were already added so we're going to strip them
$field_value = stripslashes_deep( $field_value );
// put back our newlines
$field_value = str_replace("%%ATTACHMENTS_NEWLINE%%", "\\n", $field_value );
// encode the whole thing
$field_value = $this->encode_field_value( $field_value );
// encode things properly
$attachment['fields'][ $key ] = $field_value;
}
}
// set the post parent if applicable
// need to first check to make sure we're not overwriting a native Attach
$attach_post_ref = $attachment_exists;
if ( $attach_post_ref->post_parent == 0 && ! empty( $this->instances[ $instance ]['post_parent'] ) ) {
// no current Attach, we can add ours
$attach_post = array(
'ID' => absint( $attachment['id'] ),
'post_parent' => $post_id,
);
wp_update_post( $attach_post );
}
$attachments[ $instance ][] = $attachment;
}
}
}
if ( ! empty( $attachments ) ) {
// we're going to store JSON (JSON_UNESCAPED_UNICODE is PHP 5.4+)
$attachments = version_compare( PHP_VERSION, '5.4.0', '>=' ) ? json_encode( $attachments, JSON_UNESCAPED_UNICODE ) : json_encode( $attachments );
// fix potentially encoded Unicode
$attachments = str_replace( '\\', '\\\\', $attachments );
// we're going to wipe out any existing Attachments meta (because we'll put it back)
return update_post_meta( $post_id, $this->meta_key, $attachments );
} else {
// there are no attachments so we'll clean up the record
return delete_post_meta( $post_id, $this->meta_key );
}
}
/**
* Recursive function to encode a field type before saving
* @param mixed $field_value The field value
* @return mixed The encoded field value
*
* @since 3.3
*/
function encode_field_value( $field_value = null ) {
if ( is_null( $field_value ) ) {
return false;
}
if ( is_object( $field_value ) ) {
$input = get_object_vars( $field_value );
foreach ( $input as $key => $val ) {
$field_value[ $key ] = $this->encode_field_value( $val );
}
$field_value = (object) $field_value;
} elseif ( is_array( $field_value ) ) {
foreach ( $field_value as $key => $val ) {
$field_value[$key] = $this->encode_field_value( $val );
}
} else {
$field_value = htmlentities( $field_value, ENT_QUOTES, 'UTF-8' );
}
return $field_value;
}
/**
* Retrieves all Attachments for the submitted instance and post ID
*
* @since 3.0
*/
function get_attachments( $instance = null, $post_id = null ) {
$post_id = $this->determine_post_id( $post_id );
if ( ! $post_id ) {
return false;
}
$attachments = array();
$attachments_raw = $this->get_attachments_metadata( $post_id );
// we need to decode the fields (that were encoded during save) and run them through
// their format_value_for_input as defined in it's class
if ( ! is_null( $instance ) && is_string( $instance ) && isset( $attachments_raw->$instance ) ) {
foreach( $attachments_raw->$instance as $attachment ) {
$attachments[] = $this->process_attachment( $attachment, $instance );
}
} elseif ( is_null( $instance ) ) {
// return them all, regardless of instance
if ( ( is_array( $attachments_raw ) && count( $attachments_raw ) ) || is_object( $attachments_raw ) ) {
// cast an object if necessary
if ( is_object( $attachments_raw ) ) $attachments_raw = (array) $attachments_raw;
foreach( $attachments_raw as $instance => $attachments_unprocessed ) {
foreach( $attachments_unprocessed as $unprocessed_attachment ) {
$attachments[] = $this->process_attachment( $unprocessed_attachment, $instance );
}
}
}
}
// tack on the post ID for each attachment
for ( $i = 0; $i < count( $attachments ); $i++ ) {
$attachments[ $i ]->post_id = $post_id;
}
// we don't want the filter to run on the admin side of things
if ( ! is_admin() ) {
$attachments = apply_filters( "attachments_get_{$instance}", $attachments );
}
return $attachments;
}
/**
* Retrieves the post_meta record (saved as JSON) and processes it
* @param int $post_id The post ID
* @return object Attachments for the post
*
* @since 3.3
*/
function get_attachments_metadata( $post_id ) {
$post_id = intval( $post_id );
// grab our JSON and decode it
$attachments_json = get_post_meta( $post_id, $this->meta_key, true );
$attachments_raw = is_string( $attachments_json ) ? json_decode( $attachments_json ) : false;
// convert field newline characters properly
if ( ! empty( $attachments_raw ) ) {
foreach ( $attachments_raw as $instanceKey => $instance ) {
foreach( $instance as $attachmentKey => $attachment ) {
if( isset( $attachment->fields ) ) {
foreach( $attachment->fields as $fieldKey => $fieldValue ) {
$attachment->fields->$fieldKey = str_replace( '\\n', "\n", $fieldValue );
}
}
}
}
}
return $attachments_raw;
}
/**
* Determines a proper post ID based on whether one was passed, the global $post object, or a GET variable
* @param int $post_id Desired post ID
* @return mixed The understood post ID
*/
function determine_post_id( $post_id = null ) {
global $post;
// if a post id was passed, we'll use it
if ( !is_null( $post_id ) ) {
$post_id = intval( $post_id );
} elseif( is_null( $post_id ) && is_object( $post ) && isset( $post->ID ) ) {
$post_id = $post->ID;
} elseif( isset( $_GET['post'] ) ) {
$post_id = intval( $_GET['post'] );
} else {
// no post ID, nothing to do...
$post_id = false;
}
return $post_id;
}
/**
* Processes Attachment (including fields) in preparation for return
* @param object $attachment An Attachment object as it was stored as post metadata
* @param string $instance The applicable instance
* @return object Post-processed Attachment data
*
* @since 3.3
*/
function process_attachment( $attachment, $instance ) {
if ( ! is_object( $attachment ) || !is_string( $instance ) ) {
return $attachment;
}
if ( isset( $attachment->fields ) && is_object( $attachment->fields ) ) {
foreach ( $attachment->fields as $key => $value ) {
// loop through the instance fields to get the type
if ( isset( $this->instances[ $instance ]['fields'] ) ) {
$type = '';
foreach ( $this->instances[$instance]['fields'] as $field ) {
if ( isset( $field['name'] ) && $field['name'] == $key ) {
$type = isset( $field['type'] ) ? $field['type'] : false;
break;
}
}
if ( isset( $this->fields[$type] ) ) {
// we need to decode the html entities that were encoded for the save
$attachment->fields->$key = $this->decode_field_value( $attachment->fields->$key );
} else {
// the type doesn't exist
$attachment->fields->$key = false;
}
} else {
// this was a theme file request, just grab it
$attachment->fields->$key = $this->decode_field_value( $attachment->fields->$key );
}
}
}
return $attachment;
}
/**
* Recursive function to decode a field value when retrieving it
* @param mixed $field_value The field value
* @return mixed The encoded field value
*
* @since 3.3
*/
function decode_field_value( $field_value = null ) {
if ( is_null( $field_value ) ) {
return false;
}
if ( is_object( $field_value ) ) {
$input = get_object_vars( $field_value );
foreach ( $input as $key => $val ) {
$field_value[$key] = $this->decode_field_value( $val );
}
$field_value = (object) $field_value;
} elseif( is_array( $field_value ) ) {
foreach ( $field_value as $key => $val ) {
$field_value[$key] = $this->decode_field_value( $val );
}
} else {
$field_value = html_entity_decode( $field_value, ENT_QUOTES, 'UTF-8' );
}
return $field_value;
}
/**
* Callback to implement our Settings page
*
* @since 3.0
*/
function admin_page() {
if ( ! ( defined( 'ATTACHMENTS_SETTINGS_SCREEN' ) && ATTACHMENTS_SETTINGS_SCREEN === false ) ) {
if ( apply_filters( 'attachments_settings_screen', true ) ) {
add_options_page( 'Settings', __( 'Attachments', 'attachments' ), 'manage_options', 'attachments', array( $this, 'options_page' ) );
}
}
}
/**
* Callback to output our Settings page markup
*
* @since 3.0
*/
function options_page()
{
include_once( ATTACHMENTS_DIR . '/views/options.php' );
}
}
endif; // class_exists check
new Attachments();