version = '3.1.1'; $this->url = ATTACHMENTS_URL; $this->dir = ATTACHMENTS_DIR; // includes include_once( ATTACHMENTS_DIR . 'upgrade.php' ); include_once( ATTACHMENTS_DIR . '/classes/class.field.php' ); // deal with our legacy issues if the user hasn't dismissed or migrated already if( false == get_option( 'attachments_migrated' ) && false == get_option( 'attachments_ignore_migration' ) ) { // TODO: this will not retrieve posts that have exclude_from_search = true // TODO: make this reusable elsewhere $legacy = new WP_Query( 'post_type=any&post_status=any&posts_per_page=1&meta_key=_attachments' ); $this->legacy = empty( $legacy->found_posts ) ? false : true; } else { $this->legacy = false; } // set our image sizes $this->image_sizes = array_merge( $this->image_sizes, get_intermediate_image_sizes() ); // include our fields $this->fields = $this->get_field_types(); // hook into WP add_action( 'admin_enqueue_scripts', array( $this, 'assets' ), 999, 1 ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_pointer' ), 999 ); // register our user-defined instances add_action( 'init', array( $this, 'do_actions_filters' ) ); // determine which instances apply to the current post type add_action( 'init', array( $this, 'set_instances_for_current_post_type' ) ); add_action( 'add_meta_boxes', array( $this, 'meta_box_init' ) ); add_action( 'admin_footer', array( $this, 'admin_footer' ) ); add_action( 'save_post', array( $this, 'save' ) ); add_action( 'admin_menu', array( $this, 'admin_page' ) ); // with version 3 we'll be giving at least one admin notice add_action( 'admin_notices', array( $this, 'admin_notice' ) ); add_action( 'admin_print_footer_scripts', array( $this, 'field_assets' ) ); // set our attachments if necessary if( !is_null( $instance ) ) $this->attachments = $this->get_attachments( $instance, $post_id ); } /** * Returns whether or not the current object has any Attachments * * @since 3.0 */ function exist() { return !empty( $this->attachments ); } /** * Returns the number of Attachments * * @since 3.0.6 */ function total() { return count( $this->attachments ); } /** * Returns the next Attachment for the current object and increments the index * * @since 3.0 */ function get() { $this->attachments_ref++; if( !count( $this->attachments ) || $this->attachments_ref >= count( $this->attachments ) ) return false; return $this->attachments[$this->attachments_ref]; } /** * Returns the asset (array) for the current Attachment * * @since 3.0.6 */ function asset( $size = 'thumbnail' ) { // do we have our meta yet? if( !isset( $this->attachments[$this->attachments_ref]->meta ) ) $this->attachments[$this->attachments_ref]->meta = wp_get_attachment_metadata( $this->attachments[$this->attachments_ref]->id ); // is it an image? if( isset( $this->attachments[$this->attachments_ref]->meta['sizes'] ) && // is it an image? in_array( $size, $this->image_sizes ) ) // do we have the right size? { $asset = wp_get_attachment_image_src( $this->attachments[$this->attachments_ref]->id, $size ); } else { // either it's not an image or we don't have the proper size, so we'll use the icon $asset = $this->icon(); } return $asset; } /** * Returns the icon (array) for the current Attachment * * @since 3.0.6 */ function icon() { $asset = wp_get_attachment_image_src( $this->attachments[$this->attachments_ref]->id, null, true ); return $asset; } /** * Returns an appropriate for the current Attachment if it's an image * * @since 3.0 */ function image( $size = 'thumbnail' ) { $asset = $this->asset( $size ); $image_src = $asset[0]; $image_width = $asset[1]; $image_height = $asset[2]; $image_alt = get_post_meta( $this->attachments[$this->attachments_ref]->id, '_wp_attachment_image_alt', true ); $image = '' . $image_alt . ''; return $image; } /** * Returns the URL for the current Attachment if it's an image * * @since 3.0 */ function src( $size = 'thumbnail' ) { $asset = $this->asset( $size ); return $asset[0]; } /** * Returns the formatted filesize of the current Attachment * * @since 3.0 */ function filesize() { if( !isset( $this->attachments[$this->attachments_ref]->id ) ) return false; $url = wp_get_attachment_url( $this->attachments[$this->attachments_ref]->id ); $uploads = wp_upload_dir(); $file_path = str_replace( $uploads['baseurl'], $uploads['basedir'], $url ); $formatted = '0 bytes'; if( file_exists( $file_path ) ) { $formatted = size_format( @filesize( $file_path ) ); } return $formatted; } /** * Returns the type of the current Attachment * * @since 3.0 */ function type() { if( !isset( $this->attachments[$this->attachments_ref]->id ) ) return false; $attachment_mime = explode( '/', get_post_mime_type( $this->attachments[$this->attachments_ref]->id ) ); return isset( $attachment_mime[0] ) ? $attachment_mime[0] : null; } /** * Returns the subtype of the current Attachment * * @since 3.0 */ function subtype() { if( !isset( $this->attachments[$this->attachments_ref]->id ) ) return false; $attachment_mime = explode( '/', get_post_mime_type( $this->attachments[$this->attachments_ref]->id ) ); return isset( $attachment_mime[1] ) ? $attachment_mime[1] : null; } /** * Returns the id of the current Attachment * * @since 3.0 */ function id() { return isset( $this->attachments[$this->attachments_ref]->id ) ? $this->attachments[$this->attachments_ref]->id : null; } /** * Returns the URL for the current Attachment * * @since 3.0 */ function url() { if( !isset( $this->attachments[$this->attachments_ref]->id ) ) return false; return wp_get_attachment_url( $this->attachments[$this->attachments_ref]->id ); } /** * Returns the field value for the submitted field name * * @since 3.0 */ function field( $name = 'title' ) { return isset( $this->attachments[$this->attachments_ref]->fields->$name ) ? $this->attachments[$this->attachments_ref]->fields->$name : false; } /** * Fires all of our actions * * @since 3.0 */ function do_actions_filters() { // implement our default instance if appropriate if( !defined( 'ATTACHMENTS_DEFAULT_INSTANCE' ) ) $this->register(); // facilitate user-defined instance registration do_action( 'attachments_register', $this ); } /** * Enqueues our necessary assets * * @since 3.0 */ function assets( $hook ) { global $post; // we only want our assets on edit screens if( !empty( $this->instances_for_post_type ) && 'edit.php' != $hook && 'post.php' != $hook && 'post-new.php' != $hook ) return; // we only want to enqueue if appropriate if( empty( $this->instances_for_post_type ) ) return; $post_id = isset( $post->ID ) ? $post->ID : null; wp_enqueue_media( array( 'post' => $post_id ) ); wp_enqueue_style( 'attachments', trailingslashit( $this->url ) . 'css/attachments.css', null, $this->version, 'screen' ); wp_enqueue_script( 'attachments', trailingslashit( $this->url ) . 'js/attachments.js', array( 'jquery', 'jquery-ui-sortable' ), $this->version, true ); } /** * Registers meta box(es) for the current edit screen * * @since 3.0 */ function meta_box_init() { $nonce_sent = false; if( !empty( $this->instances_for_post_type ) ) { foreach( $this->instances_for_post_type as $instance ) { $instance_name = $instance; $instance = (object) $this->instances[$instance]; $instance->name = $instance_name; add_meta_box( 'attachments-' . $instance_name, __( esc_attr( $instance->label ) ), array( $this, 'meta_box_markup' ), $this->get_post_type(), 'normal', 'high', array( 'instance' => $instance, 'setup_nonce' => !$nonce_sent ) ); $nonce_sent = true; } } } /** * Callback that outputs the meta box markup * * @since 3.0 */ function meta_box_markup( $post, $metabox ) { // single out our $instance $instance = (object) $metabox['args']['instance']; if( $metabox['args']['setup_nonce'] ) wp_nonce_field( 'attachments_save', 'attachments_nonce' ); ?>
note ) ) : ?>
note ); ?>
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 ); } } ?>
button_text ), 'attachments' ); ?>
ATTACHMENTS_DIR . 'classes/fields/class.field.text.php', 'textarea' => ATTACHMENTS_DIR . 'classes/fields/class.field.textarea.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' ), ); $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'] ) ); // instantiate the class for this field and send it back return new $this->fields[ $params['type'] ]( $params['name'], $params['label'] ); } /** * 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' ), // 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 // text for 'Attach' button (string) 'button_text' => __( 'Attach', 'attachments' ), // text for modal 'Attach' button (string) 'modal_text' => __( 'Attach', 'attachments' ), // fields for this instance (array) 'fields' => array( array( 'name' => 'title', // unique field name 'type' => 'text', // registered field type 'label' => __( 'Title', 'attachments' ), // label to display ), array( 'name' => 'caption', // unique field name 'type' => 'text', // registered field type 'label' => __( 'Caption', 'attachments' ), // label to display ), ), ); $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_sql( $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 = str_replace( '-', '_', sanitize_title( $_GET['post_type'] ) ); // TODO: Better sanitization } 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'] ); $value = ( isset( $attachment->fields->$name ) ) ? $attachment->fields->$name : null; $field = new $this->fields[$type]( $name, $label, $value ); $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 ); ?>
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 ); $attachment->width = isset( $attachment_meta['width'] ) ? $attachment_meta['width'] : null; $attachment->height = isset( $attachment_meta['height'] ) ? $attachment_meta['height'] : null; $attachment->filename = end( explode( "/", $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; } ?>
id ) ? wp_get_attachment_image_src( $attachment->id, 'thumbnail', true ) : false; $image = $thumbnail ? $thumbnail[0] : '{{ attachments.attachment_thumb }}'; ?>
Thumbnail
filename ) ? $attachment->filename : '{{ attachments.filename }}' ; ?>
id ) && isset( $attachment->width ) ) || !isset( $attachment->id ) ) : ?>
width ) ? $attachment->width : '{{ attachments.width }}' ; ?> × height ) ? $attachment->height : '{{ attachments.height }}' ; ?>
Handle
instances[$instance]['fields'] as $field ) $field_ref = $this->create_attachment_field( $instance, $field, $attachment ); ?>
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(); } } /** * 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 ) : ?> $instance_attachments ) { // loop through each Attachment of this instance foreach( $instance_attachments as $key => $attachment ) { // 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 dont 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( $field_value ); // put back our newlines $field_value = str_replace("%%ATTACHMENTS_NEWLINE%%", "\\n", $field_value ); // encode the whole thing $field_value = htmlentities( $field_value, ENT_QUOTES, 'UTF-8' ); // encode things properly $attachment['fields'][$key] = $field_value; } } $attachments[$instance][] = $attachment; } } // 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 ); // we're going to wipe out any existing Attachments meta (because we'll put it back) update_post_meta( $post_id, $this->meta_key, $attachments ); } /** * Retrieves all Attachments for the submitted instance and post ID * * @since 3.0 */ function get_attachments( $instance = '', $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... return; } // 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; // 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( isset( $attachments_raw->$instance ) ) { foreach( $attachments_raw->$instance as $attachment ) { if( 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 = html_entity_decode( $attachment->fields->$key, ENT_QUOTES, 'UTF-8' ); } else { // the type doesn't exist $attachment->fields->$key = false; } } else { // this was a theme file request, just grab it $attachment->fields->$key = html_entity_decode( $attachment->fields->$key, ENT_QUOTES, 'UTF-8' ); } } } $attachments[] = $attachment; } } else { $attachments = false; } return $attachments; } /** * Determines whether or not there is 'active' legacy data the user may not know about * * @since 3.0 */ function has_outstanding_legacy_data() { if( // migration has not taken place and we have legacy data ( false == get_option( 'attachments_migrated' ) && !empty( $this->legacy ) ) && // we're not intentionally ignoring the message ( false == get_option( 'attachments_ignore_migration' ) ) ) { return true; } else { return false; } } /** * Outputs a WordPress message to notify user of legacy data * * @since 3.0 */ function admin_notice() { if( $this->has_outstanding_legacy_data() && ( isset( $_GET['page'] ) && $_GET['page'] !== 'attachments' || !isset( $_GET['page'] ) ) ) : ?>

Attachments has detected legacy Attachments data. A lot has changed since Attachments 1.x.' ,'attachments' ); ?> .

legacy && !in_array( 'attachments_legacy', $dismissed_pointers ) ) { $enqueue_pointer_script_style = true; // Add footer scripts using callback function add_action( 'admin_print_footer_scripts', array( $this, 'pointer_legacy' ) ); } // Enqueue pointer CSS and JS files, if needed if( $enqueue_pointer_script_style ) { wp_enqueue_style( 'wp-pointer' ); wp_enqueue_script( 'wp-pointer' ); } } /** * Pointer that calls attention to legacy data * * @since 3.0 */ function pointer_legacy() { $pointer_content = "

". __( esc_html( 'Attachments 3.0 brings big changes!' ), 'attachments' ) ."

"; $pointer_content .= "

". __( esc_html( 'It is very important that you take a few minutes to see what has been updated. The changes will affect your themes/plugins.' ), 'attachments' ) ."

"; ?>