• Resolved riccardo.tasso

    (@riccardotasso)


    Hi, I’m using WordPress with Media Library Assistant and I found them very good for my use case.

    I have some media with att. tags and I’d like to export them in a file which contains a list of media, each one with id and all tags. Something very similar to the export function of wordpress, accessible from the dashboard.

    Which is the simplest way to make the administrator generate this file?

    Thanks,
    Riccardo

    https://www.ads-software.com/plugins/media-library-assistant/

Viewing 7 replies - 1 through 7 (of 7 total)
  • Plugin Author David Lingren

    (@dglingren)

    Thank you for the good words and for your question.

    There is no specific export function in MLA, but I can suggest a semi-automated solution using the Custom Field mapping rules and the hooks/filters they provide.

    If you can give me an example of the output you want I can do some experimenting and give you a more specific solution.

    Thanks for any additional information you can provide, and thanks for your interest in the plugin.

    Thread Starter riccardo.tasso

    (@riccardotasso)

    Let’s say something in XML like:

    <items>
    <item>
       <id>123</id>
       <title>My Media File</title>
       <url>https://www.mydomain.com/?attachment_id=123</url>
       <post_date>2014-06-25 15:41:04</wp:post_date>
       <tag>a</tag>
       <tag>b</tag>
       <tag>c</tag>
    </item>
    ...
    </items>

    Thanks very much for the rapid response.
    Riccardo

    Plugin Author David Lingren

    (@dglingren)

    Thanks for posting the additional detail about the output you need. I have added the code you need to the MLA Mapping Hooks example, along the lines of this similar support topic:

    Quick question on replacing string(s) in image metadata

    You can find more information on the hooks and the example plugin that demonstrates their use in the “MLA Custom Field and IPTC/EXIF Mapping Actions and Filters (Hooks)” section of the Settings/Media Library Assistant Documentation tab. More on that later.

    For your application I decided to use a “custom field” rule to control execution of the exporting code. First, you must define the custom field rule:

    1. Navigate to the Settings/Media Library Assistant “Custom Fields” tab.
    2. Scroll down to the “Add a new Field and Mapping Rule” area.
    3. In the first text box, give your field a name, which must be “Export” to trigger the code.
    4. In the Data Source dropdown list, leave the default setting of “- None (select a value) -“. We don’t want to actually create a WordPress custom field; we are just using the rule to control the export code.
    5. In the text box below the Data Source dropdown, enter the name of the file you want to create, e.g., “attachment.xml”. The name can be anything you want, but you must use “.xml” to trigger the export code.
    6. The remaining part of the rule can be left alone; they will have no effect on the export code.
    7. Click the “Add Field and Map All Attachments” button to save your work.

    With this rule in place, the export function will be run any time you click one of the “map custom fields” controls. There are two of these controls in the Settings/Custom Fields tab:

    1. The “Map All Attachments” button at the bottom of the “Export” rule.
    2. The “Map All Rules, All Attachments Now” button at the bottom of the screen.

    You can also find mapping controls in two other places:

    1. In the Bulk Edit area of the Media/Assistant submenu table. Go to that screen and select one or more items by checking the box at the left of each item. Pull down the Bulk Actions list and select Edit. Click Apply, and the Bulk Edit area will open up. You will find the “Map Custom Field Metadata” in the bottom-right corner of the area.
    2. In the “Save” meta box at the top-right corner of the Media/Edit Media screen for an individual item. From the Media/Assistant submenu table, click on the item thumbnail or on the “Edit” rollover action to get to the Edit Media screen.

    You can pick the mapping control you want to export one item, select a group of items or export all of the items in the Media Library.

    The code required for the export function goes in two parts of the example plugin. Of course, you can add the code to other PHP files in your application if that’s better for you. In the example plugin, a small bit of code that looks for the rule to trigger the export goes in the mla_mapping_updates filter:

    /*
     * Look for the "file export" rule; call the export function if there's a match.
     */
    foreach ($settings as $key => $setting ) {
        if ( 'export' == sanitize_title( $key ) && 'none' == $setting['data_source'] && false !== strpos( $setting['meta_name'], '.xml' ) ) {
            self::_export_this_item( $post_id, $setting['meta_name'] );
        }
    }

    This code looks for a rule named “export” or “Export”, with a Data Source of “none” and a string that includes “.xml”. If all three conditions are met, the item’s ID and the file name are passed to the actual export code.

    All of the work is done in the _export_this_item function. Much of the code deals with the inevitable housekeeping and error checking any file operations need (sigh). Here’s the code for the output format you suggested:

    /**
     * Export data for one or more items to an XML file
     *
     * This function is called from the mla_mapping_updates_filter(),
     * just above, when the "export" rule is defined. It writes information
     * about the item and its tags to an XML file.
     *
     * @since 1.01
     *
     * @param    integer    The ID value of the current item/attachment
     * @param    string    The name of the output file, e.g., "data.xml"
     *
     * @return    void
     */
    private static function _export_this_item( $post_id, $file ) {
        static $filename = NULL, $file_pointer = NULL, $tail = "</items>\n";
    
        // create/open the file on first call
        if ( NULL == $filename ) {
            $filename = MLA_BACKUP_DIR . $file;
    
            // Make sure the directory exists and is writable
            if ( ! file_exists( MLA_BACKUP_DIR ) && ! @mkdir( MLA_BACKUP_DIR ) ) {
                return; // Does not exist and cannot create it
            } elseif ( ! is_writable( MLA_BACKUP_DIR ) && ! @chmod( MLA_BACKUP_DIR , '0777') ) {
                return; // Is not writable and cannot make it so
            }
    
            // Every directory should have an empty index.php file for security
            if ( ! file_exists( MLA_BACKUP_DIR . 'index.php') ) {
                @ touch( MLA_BACKUP_DIR . 'index.php');
            }
    
            // Open the file for write access
            $file_pointer = @fopen( $filename, 'w' );
            if ( ! $file_pointer ) {
                $file_pointer = NULL;
                return; // Cannot open a writable file
            }
    
            // Write the overall header and trailer
            if (false === @fwrite($file_pointer, "<items>\n" . $tail )) {
                fclose($file_pointer);
                $file_pointer = NULL;
                return;
            }
        } // First call
    
        if ( NULL == $file_pointer ) {
            return; // Don't have a writable file
        }
    
        // Back up over the old trailer
        if ( -1 === fseek( $file_pointer, (0 - strlen( $tail ) ), SEEK_END ) ) {
            fclose($file_pointer);
            $file_pointer = NULL;
            return;
        }
    
        $post = get_post( $post_id );
        $terms = wp_get_post_terms( $post_id, 'post_tag' );
    
        // Compose the item, line by line
        $item = array();
        $item[] = "\t<item>";
        $item[] = "\t\t<id>" . absint( $post_id ) . '</id>';
        $item[] = "\t\t<title>" . $post->post_title . '</title>';
        $item[] = "\t\t<url>" . get_attachment_link( $post_id ) . '</url>';
        //$item[] = "\t\t<file>" . wp_get_attachment_url( $post_id ) . '</file>';
        $item[] = "\t\t<post_date>" . $post->post_date . '</post_date>';
    
        foreach( $terms as $term ) {
            $item[] = "\t\t<tag>" . $term->name . '</tag>';
        }
    
        $item[] = "\t</item>";
        $item[] = $tail; // already has "\n" appended
    
        // Write the item to the file
        $item = implode( "\n", $item );
        if (false === @fwrite($file_pointer, $item )) {
            fclose($file_pointer);
            $file_pointer = NULL;
            return;
        }
    } // _export_this_item
    • The MLA_BACKUP_DIR constant puts the file in a safe place. By default, this is the /wp-content/mla-backup/ directory. This code was adapted from the function that exports MLA settings to the same place.
    • The “Back up over the old trailer” logic is needed because there’s no “end of mapping” filter called once at the end of the mapping process. I plan to add one in a future MLA release, but this code works for now.
    • The get_post() and wp_get_post_terms() functions make a lot of information available; you can find details in the Codex. For example, you can record the term_id or slug in addition to the term name, and you can record other item-level information.
    • I think the “url” line in your example is for the item’s “Attachment/Media Page”. There are other possibilities.
    • I think the “tag” lines in your example are the term name, but there are other possibilities.
    • I added some tabs to indent the output.

    You can expand this example in any number of ways, but I think this gets you most of what you need.

    I will add this code to the example plugin in my next MLA version. If you would like a complete copy of the example plugin with the added code before the next version goes out, let me know. You can give me your e-mail address and other contact information by visiting the Contact Us page at our web site:

    Fair Trade Judaica/Contact Us

    I am marking this topic resolved, but please update it if you have any problems or further questions on the approach I’ve outlined. Thanks for an interesting question and for your interest in the plugin.

    Thread Starter riccardo.tasso

    (@riccardotasso)

    Hi David and thank you so much for such a rich explanation!

    I followed carefully each one of your steps:

    1. created and saved the rule (named ExportMLA)
    2. created the plugin, wich is exactly as I found in wp-content/plugins/media-library-assistant/examples/mla-mapping-hooks-example.php.txt plus the snippets you gave me
    3. copied the plugin in the wp-content/plugins/exportMLA/index.php
    4. activated the new plugin from wordpress dashboard
    5. pressed the Map All Rules, All Attachments Now button (it gives me the message: “Custom field mapping completed; 290 attachment(s) examined, no changes detected.”)

    The problem is that i don’t found anything in wp-content/mla-backup except the settings backup (which I generated just to be sure that wordpress had write access to that directory).

    The code of my plugin is as follows:

    <?php
    /**
     * Provides an example of the filters provided by the IPTC/EXIF and Custom Field mapping features
     *
     * In this example...
     *
     * @package MLA Mapping Hooks Example
     * @version 1.02
     */
    
    /*
    Plugin Name: MLA Mapping Hooks Example
    Plugin URI: https://fairtradejudaica.org/media-library-assistant-a-wordpress-plugin/
    Description: Provides an example of the filters provided by the IPTC/EXIF and Custom Field mapping features.
    Author: David Lingren
    Version: 1.02
    Author URI: https://fairtradejudaica.org/our-story/staff/
    
    Copyright 2014 David Lingren
    
    	This program is free software; you can redistribute it and/or modify
    	it under the terms of the GNU General Public License as published by
    	the Free Software Foundation; either version 2 of the License, or
    	(at your option) any later version.
    
    	This program is distributed in the hope that it will be useful,
    	but WITHOUT ANY WARRANTY; without even the implied warranty of
    	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    	GNU General Public License for more details.
    
    	You can get a copy of the GNU General Public License by writing to the
    	Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
    */
    
    /**
     * Class MLA Mapping Hooks Example hooks all of the filters provided by the IPTC/EXIF and Custom Field mapping features
     *
     * Call it anything you want, but give it an unlikely and hopefully unique name. Hiding enerything
     * else inside a class means this is the only name you have to worry about.
     *
     * @package MLA Mapping Hooks Example
     * @since 1.00
     */
    class MLAMappingHooksExample {
    	/**
    	 * Initialization function, similar to __construct()
    	 *
    	 * Installs filters and actions that handle the MLA hooks for uploading and mapping.
    	 *
    	 * @since 1.00
    	 *
    	 * @return	void
    	 */
    	public static function initialize() {
    		/*
    		 * The filters are only useful in the admin section; exit if in the "front-end" posts/pages.
    		 */
    		if ( ! is_admin() )
    			return;
    
    		/*
    		 * add_filter parameters:
    		 * $tag - name of the hook you're filtering; defined by [mla_gallery]
    		 * $function_to_add - function to be called when [mla_gallery] applies the filter
    		 * $priority - default 10; lower runs earlier, higher runs later
    		 * $accepted_args - number of arguments your function accepts
    		 *
    		 * Comment out the filters you don't need; save them for future use
    		 */
    		add_filter( 'mla_upload_prefilter', 'MLAMappingHooksExample::mla_upload_prefilter_filter', 10, 2 );
    		add_filter( 'mla_upload_filter', 'MLAMappingHooksExample::mla_upload_filter_filter', 10, 2 );
    
    		add_action( 'mla_add_attachment', 'MLAMappingHooksExample::mla_add_attachment_action', 10, 1 );
    
    		add_filter( 'mla_update_attachment_metadata_options', 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter', 10, 3 );
    		add_filter( 'mla_update_attachment_metadata_prefilter', 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter', 10, 3 );
    		add_filter( 'mla_update_attachment_metadata_postfilter', 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter', 10, 3 );
    
    		add_filter( 'mla_mapping_settings', 'MLAMappingHooksExample::mla_mapping_settings_filter', 10, 4 );
    		add_filter( 'mla_mapping_rule', 'MLAMappingHooksExample::mla_mapping_rule_filter', 10, 4 );
    		add_filter( 'mla_mapping_custom_value', 'MLAMappingHooksExample::mla_mapping_custom_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_iptc_value', 'MLAMappingHooksExample::mla_mapping_iptc_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_exif_value', 'MLAMappingHooksExample::mla_mapping_exif_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_updates', 'MLAMappingHooksExample::mla_mapping_updates_filter', 10, 5 );
    
    		add_filter( 'mla_get_options_tablist', 'MLAMappingHooksExample::mla_get_options_tablist_filter', 10, 3 );
    	}
    
    	/**
    	 * Save the original image metadata when a file is first uploaded
    	 *
    	 * Array elements are:
    	 * 		'post_id' => 0,
    	 *		'mla_iptc_metadata' => array(),
    	 *		'mla_exif_metadata' => array(),
    	 *		'wp_image_metadata' => array(),
    	 *
    	 * @since 1.00
    	 *
    	 * @var	array
    	 */
    	private static $image_metadata = array();
    	private static $raw_metadata = array();
    
    	/**
    	 * MLA Mapping Upload Prefilter
    	 *
    	 * This filter gives you an opportunity to record the original IPTC, EXIF and
    	 * WordPress image_metadata before the file is stored in the Media Library.
    	 * You can also modify the file name that will be used in the Media Library.
    	 *
    	 * Many plugins and image editing functions alter or destroy this information,
    	 * so this may be your last change to preserve it.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	the file name, type and location
    	 * @param	array	the IPTC, EXIF and WordPress image_metadata
    	 *
    	 * @return	array	updated file name and other information
    	 */
    	public static function mla_upload_prefilter_filter( $file, $image_metadata ) {
    		/*
    		 * Uncomment the error_log statements in any of the filters to see what's passed in
    		 */
    		//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $file = ' . var_export( $file, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $image_metadata = ' . var_export( $image_metadata, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata = $image_metadata;
    		self::$image_metadata['preload_file'] = $file;
    
    		/*
    		 * Save the EXIF, XMP, IPTC and COM data from JPEG files
    		 */
    		if ( 'image/jpeg' == $file['type'] ) {
    			self::$raw_metadata = self::_extract_jpeg_metadata( $file['tmp_name'] );
    			//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $raw_metadata = ' . var_export( self::$raw_metadata, true ), 0 );
    		} else {
    			self::$raw_metadata = array();
    		}
    
    		return $file;
    	} // mla_upload_prefilter_filter
    
    	/**
    	 * MLA Mapping Upload Filter
    	 *
    	 * This filter gives you an opportunity to record some additional metadata
    	 * for audio and video media after the file is stored in the Media Library.
    	 *
    	 * Many plugins and other functions alter or destroy this information,
    	 * so this may be your last change to preserve it.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	the file name, type and location
    	 * @param	array	the ID3 metadata for audio and video files
    	 *
    	 * @return	array	updated file name, type and location
    	 */
    	public static function mla_upload_filter_filter( $file, $id3_data ) {
    		//error_log( 'MLAMappingHooksExample::mla_upload_filter_filter $file = ' . var_export( $file, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_upload_filter_filter $id3_data = ' . var_export( $id3_data, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata['postload_file'] = $file;
    		self::$image_metadata['id3_metadata'] = $id3_data;
    
    		return $file;
    	} // mla_upload_filter_filter
    
    	/**
    	 * MLA Add Attachment Action
    	 *
    	 * This filter is called at the end of the wp_insert_attachment() function,
    	 * after the file is in place and the post object has been created in the database.
    	 *
    	 * By this time, other plugins have probably run their own 'add_attachment' filters
    	 * and done their work/damage to metadata, etc.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	integer	The Post ID of the new attachment
    	 *
    	 * @return	void
    	 */
    	public static function mla_add_attachment_action( $post_ID ) {
    		//error_log( 'MLAMappingHooksExample::mla_add_attachment_action $post_ID = ' . var_export( $post_ID, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata['post_id'] = $post_ID;
    	} // mla_add_attachment_action
    
    	/**
    	 * MLA Update Attachment Metadata Options
    	 *
    	 * This filter lets you inspect or change the processing options that will
    	 * control the MLA mapping rules in the update_attachment_metadata filter.
    	 *
    	 * The options are:
    	 *		is_upload - true if this is part of the original file upload process
    	 *		enable_iptc_exif_mapping - true to apply IPTC/EXIF mapping to file uploads
    	 *		enable_custom_field_mapping - true to apply custom field mapping to file uploads
    	 *		enable_iptc_exif_update - true to apply IPTC/EXIF mapping to updates
    	 *		enable_custom_field_update - true to apply custom field mapping to updates
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 *
    	 * @return	array	updated processing options
    	 */
    	public static function mla_update_attachment_metadata_options_filter( $options, $data, $post_ID ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $options = ' . var_export( $options, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    
    		return $options;
    	} // mla_update_attachment_metadata_prefilter_filter
    
    	/**
    	 * MLA Update Attachment Metadata Prefilter
    	 *
    	 * This filter is called at the end of the wp_update_attachment_metadata() function,
    	 * BEFORE any MLA mapping rules are applied. The prefilter gives you an
    	 * opportunity to record or update the metadata before the mapping.
    	 *
    	 * The wp_update_attachment_metadata() function is called at the end of the file upload process and at
    	 * several later points, such as when an image attachment is edited or by
    	 * plugins that alter the attachment file.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 *
    	 * @return	array	updated attachment metadata
    	 */
    	public static function mla_update_attachment_metadata_prefilter_filter( $data, $post_ID, $options ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $options = ' . var_export( $options, true ), 0 );
    
    		/*
    		 * If the metadata has been stripped, try to replace it
    		 * NOTE: Uncomment/comment the "self::" and "$data = " lines to activate/deactivate
    		 */
    		if ( isset( $data['image_meta']['created_timestamp'] )
    			&& empty( $data['image_meta']['created_timestamp'] ) ) {
    				//self::_replace_jpeg_metadata( self::$image_metadata['postload_file']['file'], self::$raw_metadata );
    				//$data = wp_generate_attachment_metadata( $post_ID, self::$image_metadata['postload_file']['file'] );
    				//error_log( 'regenerated data = ' . var_export( $data, true ), 0 );
    
    		}
    
    		return $data;
    	} // mla_update_attachment_metadata_prefilter_filter
    
    	/**
    	 * MLA Update Attachment Metadata Postfilter
    	 *
    	 * This filter is called AFTER MLA mapping rules are applied during
    	 * wp_update_attachment_metadata() processing. The postfilter gives you
    	 * an opportunity to record or update the metadata after the mapping.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 *
    	 * @return	array	updated attachment metadata
    	 */
    	public static function mla_update_attachment_metadata_postfilter_filter( $data, $post_ID, $options ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $options = ' . var_export( $options, true ), 0 );
    
    		return $data;
    	} // mla_update_attachment_metadata_postfilter_filter
    
    	/**
    	 * MLA Mapping Settings Filter
    	 *
    	 * This filter is called before any mapping rules are executed.
    	 * You can add, change or delete rules from the array.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array 	mapping rules
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against, e.g., custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated mapping rules
    	 */
    	public static function mla_mapping_settings_filter( $settings, $post_ID, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $settings = ' . var_export( $settings, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * $category gives the context in which the rule is applied:
    		 *		'custom_field_mapping' - mapping custom fields for ALL attachments
    		 *		'single_attachment_mapping' - mapping custom fields for ONE attachments
    		 *
    		 *		'iptc_exif_mapping' - mapping ALL IPTC/EXIF rules
    		 *		'iptc_exif_standard_mapping' - mapping standard field rules
    		 *		'iptc_exif_taxonomy_mapping' - mapping taxonomy term rules
    		 *		'iptc_exif_custom_mapping' - mapping IPTC/EXIF custom field rules
    		 *
    		 * NOTE: 'iptc_exif_mapping' will never be passed to the 'mla_mapping_rule' filter.
    		 * There, one of the three more specific values will be passed.
    		 */
    
    		/*
    		 * For Custom Field Mapping, $settings is an array indexed by
    		 * the custom field name.
    		 * Each array element is a mapping rule; an array containing:
    		 *		'name' => custom field name
    		 *		'data_source' => 'none', 'meta', 'template' or data source name
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *		'format' => 'native', 'commas'
    		 *		'mla_column' => boolean; not used
    		 *		'quick_edit' => boolean; not used
    		 *		'bulk_edit' => boolean; not used
    		 *		'meta_name' => attachment metadata element name or content template
    		 *		'option' => 'text', 'single', 'export', 'array', 'multi'
    		 *		'no_null' => boolean; true to delete empty custom field values
    		 *
    		 * For IPTC/EXIF Mapping, $settings is an array indexed by
    		 * the mapping category; 'standard', 'taxonomy' and 'custom'.
    		 * Each category is an array of rules, with slightly different formats.
    		 *
    		 * Each 'standard' category array element is a rule (array) containing:
    		 *		'name' => field slug; 'post_title', 'post_name', 'image_alt', 'post_excerpt', 'post_content'
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *
    		 * Each 'taxonomy' category array element is a rule (array) containing:
    		 *		'name' => taxonomy slug, e.g., 'post_tag', 'attachment_category'
    		 *		'hierarchical' => boolean; true for hierarchical taxonomies
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *		'parent' => zero for none or the term_id of the parent
    		 *		'delimiters' => term separator(s), e.g., ',;'
    		 *
    		 * Each 'custom' category array element is a rule (array) containing:
    		 *		'name' => custom field name
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 */
    		return $settings;
    	} // mla_mapping_settings_filter
    
    	/**
    	 * MLA Mapping Rule Filter
    	 *
    	 * This filter is called once for each mapping rule, before the rule
    	 * is evaluated. You can change the rule parameters, or prevent rule
    	 * evaluation by returning $setting_value['data_source'] = 'none';
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated custom_field_mapping rule
    	 */
    	public static function mla_mapping_rule_filter( $setting_value, $post_ID, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * $setting_value is an array containing a mapping rule; see above
    		 * To stop this rule's evaluation and mapping, return NULL
    		 */
    		return $setting_value;
    	} // mla_mapping_rule_filter
    
    	/**
    	 * MLA Mapping Custom Field Value Filter
    	 *
    	 * This filter is called once for each custom field mapping rule, after the rule
    	 * is evaluated. You can change the new value produced by the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule value
    	 */
    	public static function mla_mapping_custom_value_filter( $new_text, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $new_text = ' . var_export( $new_text, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'size_names',
    			'option' => 'array'
    		);
    		//$size_names = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $size_names = ' . var_export( $size_names, true ), 0 );
    
    		/*
    		 * For "empty" values, return ' '.
    		 */
    		return $new_text;
    	} // mla_mapping_custom_value_filter
    
    	/**
    	 * MLA Mapping IPTC Value Filter
    	 *
    	 * This filter is called once for each IPTC/EXIF mapping rule, after the IPTC
    	 * portion of the rule is evaluated. You can change the new value produced by
    	 * the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	IPTC value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule IPTC value
    	 */
    	public static function mla_mapping_iptc_value_filter( $iptc_value, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $iptc_value = ' . var_export( $iptc_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'template',
    			'meta_name' => '([+iptc:keywords+])',
    			'option' => 'array'
    		);
    		//$keywords = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $keywords = ' . var_export( $keywords, true ), 0 );
    
    		/*
    		 * For "empty" values, return ''.
    		 */
    		return $iptc_value;
    	} // mla_mapping_iptc_value_filter
    
    	/**
    	 * MLA Mapping EXIF Value Filter
    	 *
    	 * This filter is called once for each IPTC/EXIF mapping rule, after the EXIF
    	 * portion of the rule is evaluated. You can change the new value produced by
    	 * the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	EXIF/Template value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule EXIF/Template value
    	 */
    	public static function mla_mapping_exif_value_filter( $exif_value, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $exif_value = ' . var_export( $exif_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'template',
    			'meta_name' => '([+exif:Copyright+])',
    			'option' => 'array'
    		);
    		//$copyright = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $copyright = ' . var_export( $copyright, true ), 0 );
    
    		/*
    		 * For "empty" 'text' values, return ''.
    		 * For "empty" 'array' values, return NULL.
    		 */
    		return $exif_value;
    	} // mla_mapping_exif_value_filter
    
    	/**
    	 * MLA Mapping Updates Filter
    	 *
    	 * This filter is called AFTER all mapping rules are applied.
    	 * You can add, change or remove updates for the attachment's
    	 * standard fields, taxonomies and/or custom fields.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	updates for the attachment's standard fields, taxonomies and/or custom fields
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	mapping rules
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated attachment's updates
    	 */
    	public static function mla_mapping_updates_filter( $updates, $post_ID, $category, $settings, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_updates_filter $updates = ' . var_export( $updates, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_updates_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_updates_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_updates_filter $settings = ' . var_export( $settings, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_updates_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * To stop this rule's updates, return an empty array, i.e., return array();
    		 */
    
    		/*
    		 * Look for the "file ExportMLA" rule; call the export function if there's a match.
    		 */
    		foreach ($settings as $key => $setting ) {
    		    if ( 'ExportMLA' == sanitize_title( $key ) && 'none' == $setting['data_source'] && false !== strpos( $setting['meta_name'], '.xml' ) ) {
    		        self::_export_this_item( $post_id, $setting['meta_name'] );
    		    }
    		}
    
    		return $updates;
    	} // mla_mapping_updates_filter
    
    	/**
    	 * Export data for one or more items to an XML file
    	 *
    	 * This function is called from the mla_mapping_updates_filter(),
    	 * just above, when the "export" rule is defined. It writes information
    	 * about the item and its tags to an XML file.
    	 *
    	 * @since 1.01
    	 *
    	 * @param    integer    The ID value of the current item/attachment
    	 * @param    string    The name of the output file, e.g., "data.xml"
    	 *
    	 * @return    void
    	 */
    	private static function _export_this_item( $post_id, $file ) {
    	    static $filename = NULL, $file_pointer = NULL, $tail = "</items>\n";
    
    	    // create/open the file on first call
    	    if ( NULL == $filename ) {
    	        $filename = MLA_BACKUP_DIR . $file;
    
    	        // Make sure the directory exists and is writable
    	        if ( ! file_exists( MLA_BACKUP_DIR ) && ! @mkdir( MLA_BACKUP_DIR ) ) {
    	            return; // Does not exist and cannot create it
    	        } elseif ( ! is_writable( MLA_BACKUP_DIR ) && ! @chmod( MLA_BACKUP_DIR , '0777') ) {
    	            return; // Is not writable and cannot make it so
    	        }
    
    	        // Every directory should have an empty index.php file for security
    	        if ( ! file_exists( MLA_BACKUP_DIR . 'index.php') ) {
    	            @ touch( MLA_BACKUP_DIR . 'index.php');
    	        }
    
    	        // Open the file for write access
    	        $file_pointer = @fopen( $filename, 'w' );
    	        if ( ! $file_pointer ) {
    	            $file_pointer = NULL;
    	            return; // Cannot open a writable file
    	        }
    
    	        // Write the overall header and trailer
    	        if (false === @fwrite($file_pointer, "<items>\n" . $tail )) {
    	            fclose($file_pointer);
    	            $file_pointer = NULL;
    	            return;
    	        }
    	    } // First call
    
    	    if ( NULL == $file_pointer ) {
    	        return; // Don't have a writable file
    	    }
    
    	    // Back up over the old trailer
    	    if ( -1 === fseek( $file_pointer, (0 - strlen( $tail ) ), SEEK_END ) ) {
    	        fclose($file_pointer);
    	        $file_pointer = NULL;
    	        return;
    	    }
    
    	    $post = get_post( $post_id );
    	    $terms = wp_get_post_terms( $post_id, 'post_tag' );
    
    	    // Compose the item, line by line
    	    $item = array();
    	    $item[] = "\t<item>";
    	    $item[] = "\t\t<id>" . absint( $post_id ) . '</id>';
    	    $item[] = "\t\t<title>" . $post->post_title . '</title>';
    	    $item[] = "\t\t<url>" . get_attachment_link( $post_id ) . '</url>';
    	    //$item[] = "\t\t<file>" . wp_get_attachment_url( $post_id ) . '</file>';
    	    $item[] = "\t\t<post_date>" . $post->post_date . '</post_date>';
    
    	    foreach( $terms as $term ) {
    	        $item[] = "\t\t<tag>" . $term->name . '</tag>';
    	    }
    
    	    $item[] = "\t</item>";
    	    $item[] = $tail; // already has "\n" appended
    
    	    // Write the item to the file
    	    $item = implode( "\n", $item );
    	    if (false === @fwrite($file_pointer, $item )) {
    	        fclose($file_pointer);
    	        $file_pointer = NULL;
    	        return;
    	    }
    	} // _export_this_item
    
    	/**
    	 * MLA Mapping Updates Filter
    	 *
    	 * This filter is called AFTER all mapping rules are applied.
    	 * You can add, change or remove updates for the attachment's
    	 * standard fields, taxonomies and/or custom fields.
    	 *
    	 * @since 1.02
    	 *
    	 * @param	array|false	The entire tablist ( $tab = NULL ), a single tab entry or false if not found/not allowed.
    	 * @param	array		The entire tablist
    	 * @param	string|NULL	tab slug for single-element return or NULL to return entire tablist
    	 *
    	 * @return	array	updated attachment's updates
    	 */
    	public static function mla_get_options_tablist_filter( $results, $mla_tablist, $tab ) {
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $results = ' . var_export( $results, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $mla_tablist = ' . var_export( $mla_tablist, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $tab = ' . var_export( $tab, true ), 0 );
    
    		/*
    		 * Return an updated $mla_tablist ( $tab = NULL ), an updated single element or false
    		 */
    		return $results;
    
    		/*
    		 * Comment out the above return statement to fall through to this example,
    		 * which removes the "Uploads" tab from the Settings/Media Library Assistant submenu
    		 */
    		if ( NULL == $tab ) {
    			unset( $results['upload'] );
    		} elseif ( 'upload' == $tab ) {
    			$results = false;
    		}
    
    		return $results;
    	} // mla_get_options_tablist_filter
    
    	/*
    	 * Selected JPEG Section Markers
    	 */
    	const SOF0	= 0xC0; // Baseline Encoding
    	const SOI	= 0xD8; // Start of image
    	const EOI	= 0xD9; // End of image
    	const SOS	= 0xDA; // Start of scan (image data)
    	const APP0	= 0xE0; // Application segment 0 JFIF Header
    	const APP1	= 0xE1; // Application segment 1 EXIF/XMP
    	const APP2	= 0xE2; // Application segment 2 EXIF Flashpix extensions
    	const APP13	= 0xED; // Application segment 13 IPTC
    	const COM	= 0xFE; // Comment
    
    	/**
    	 * Enumerate the sections of a JPEG file
     	 *
    	 * Returns an array of section descriptors, indexed by the section order, i.e., 0, 1, 2 ...
    	 *
    	 * Each array element is an array, containing:
    	 *		marker => section marker, e.g., 0xD8, 0xE0, 0xED
    	 *		offset => offset in the file of the "0xFF" marker introducing the section
    	 *		length => number of bytes in the section, including the "0xFF", marker byte and length field (if applicable)
    	 *
    	 * @since 1.01
    	 *
    	 * @param	string	File Contents
    	 *
    	 * @return	array	section list ( index => array( 'marker', 'offset', 'length' )
    	 */
    	private static function _enumerate_jpeg_sections( &$file_contents ) {
    		$file_length = strlen( $file_contents );
    		$file_offset = 0;
    		$section_array = array();
    
    		while ( $file_offset < $file_length ) {
    			$section_value = array();
    
    			// Find a marker
    			for ( $i = 0; $i < 7; $i++ ) {
    				if ( 0xFF != ord( $file_contents[ $file_offset + $i ] ) ) {
    					break;
    				}
    			}
    
    			$section_value['marker'] = $marker = ord( $file_contents[ $file_offset + $i ] );
    
    			if ( $marker >= self::SOF0 && $marker <= self::COM ) {
    				$section_value['offset'] = $file_offset + ( $i - 1);
    
    				if ( ( self::SOI == $marker ) || ( self::EOI == $marker ) ) {
    					$file_offset = $file_offset + ( $i + 1 );
    				} elseif ( self::SOS == $marker ) {
    					// Start of Scan precedes image data; skip to end of file/image
    					$file_offset = $file_length - 2;
    
    					// Scan backwards for End of Image marker
    					while ( ( 0xFF != ord( $file_contents[ $file_offset ] ) ) || ( self::EOI != ord( $file_contents[ $file_offset + 1 ] ) ) ) {
    						$file_offset--;
    						if ( $file_offset == $start_of_image ) {
    							// Give up - no End of Image marker
    							$file_offset = $file_length;
    							break;
    						}
    					}
    				} else {
    					// Big Endian length
    					$length = 256 * ord( $file_contents[ $file_offset + ++$i ] );
    					$length += ord( $file_contents[ $file_offset + ++$i ] );
    					$file_offset = $section_value['offset'] + 2 + $length;
    				}
    			}
    			else {
    				// No marker or invalid marker
    				if ( 0 < $i ) {
    					$section_value['offset'] = $file_offset + ( $i - 1 );
    				} else {
    					$section_value['offset'] = $file_offset + $i;
    				}
    				$file_offset = $file_offset + ( $i + 1 );
    
    				while ( $file_offset < $file_length ) {
    					if ( 0xFF == ord( $file_contents[ $file_offset ] ) ) {
    						break;
    					} else {
    						$file_offset++;
    					}
    				}
    			} // invalid marker
    
    			$section_value['length'] = $file_offset - $section_value['offset'];
    			$section_array[] = $section_value;
    			//error_log( 'MLAMappingHooksExample::_enumerate_jpeg_sections $section_value = ' . var_export( $section_value, true ), 0 );
    		} // while offset < length
    
    		return $section_array;
    	} // _enumerate_jpeg_sections
    
    	/**
    	 * Extract IPTC, EXIF/XMP and Comment data from a JPEG file
     	 *
    	 * Returns an array of section content, indexed by the section order
    	 *
    	 * Each array element is an array, containing:
    	 *		marker => section marker, e.g., 0xD8, 0xE0, 0xED
    	 *		content => data bytes in the section
    	 *
    	 * @since 1.01
    	 *
    	 * @param	string	Absolute path to the file
    	 *
    	 * @return	array	section list ( index => array( 'marker', 'content' )
    	 */
    	private static function _extract_jpeg_metadata( $path ) {
    		$metadata = array();
    		$file_contents = file_get_contents( $path, true );
    		 if ( $file_contents ) {
    			 $sections = self::_enumerate_jpeg_sections( $file_contents );
    			 foreach( $sections as $section ) {
    				 if ( in_array( $section['marker'], array( self::APP1, self::APP2, self::APP13, self::COM ) ) ) {
    					$metadata[] = array( 'marker' => $section['marker'],
    					 	'content' => substr( $file_contents, $section['offset'], $section['length'] )
    					);
    				 } // found metadata
    			 } // foreach section
    		 }
    
    		return $metadata;
    	} // _extract_jpeg_metadata
    
    	/**
    	 * Add/replace IPTC, EXIF/XMP and Comment data in a JPEG file
     	 *
    	 * @since 1.01
    	 *
    	 * @param	string	Absolute path to the destination file
    	 * @param	array	Metadata sections from _extract_jpeg_metadata
    	 *
    	 * @return	void
    	 */
    	private static function _replace_jpeg_metadata( $path, $metadata ) {
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $path = ' . var_export( $path, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $marker = ' . var_export( $metadata[0]['marker'], true ), 0 );
    
    		$pathinfo = pathinfo( $path );
    		$temp_path = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '-MLA' . $pathinfo['extension'];
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $temp_path = ' . var_export( $temp_path, true ), 0 );
    
    		/*
    		 * Default to the old COM section if the destination file lacks one
    		 */
    		$COM_section = NULL;
    		foreach ( $metadata as $section ) {
    			if ( self::COM == $section['marker'] ) {
    				$COM_section = $section['content'];
    			}
    		}
    
    		/*
    		 * Strip the destination "APP1, APP2, APP13" sections.
    		 * Separate out the SOI, APP0 and COM sections.
    		 */
    		$SOI_section = NULL;
    		$APP0_section = NULL;
    		$destination_sections = array ();
    		$file_contents = file_get_contents( $path, true );
    		 if ( $file_contents ) {
    			 $destination_sections = self::_enumerate_jpeg_sections( $file_contents );
    			foreach ( $destination_sections as $index => $value ) {
    				if ( self::SOI == $value['marker'] ) {
    					$SOI_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif  ( self::APP0 == $value['marker'] ) {
    					$APP0_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif  ( self::COM == $value['marker'] ) {
    					$COM_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif ( ( self::APP1 == $value['marker'] ) || ( self::APP2 == $value['marker'] ) || ( self::APP13 == $value['marker'] ) ) {
    					unset( $destination_sections[ $index ] );
    				}
    			}
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $SOI_section = ' . var_export( $SOI_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $APP0_section = ' . var_export( $APP0_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $COM_section = ' . var_export( $COM_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $destination_sections = ' . var_export( $destination_sections, true ), 0 );
    
    			if ( ( NULL == $SOI_section ) || ( NULL == $APP0_section ) ) {
    				return;
    			}
    
    			@unlink( $temp_path );
    			$temp_handle = @fopen( $temp_path, 'wb' );
    			if ( false === $temp_handle ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fopen error = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @fwrite( $temp_handle, $SOI_section ) ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite SOI error = ' . var_export( error_get_last(), true ), 0 );
    				@fclose( $temp_handle );
    				@unlink( $temp_path );
    				return;
    			}
    
    			if ( false === @fwrite( $temp_handle, $APP0_section ) ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite APP0 error = ' . var_export( error_get_last(), true ), 0 );
    				@fclose( $temp_handle );
    				@unlink( $temp_path );
    				return;
    			}
    
    			if ( ! empty( $COM_section ) ) {
    				if ( false === @fwrite( $temp_handle, $COM_section ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite COM error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			foreach ( $metadata as $section ) {
    				if ( false === @fwrite( $temp_handle, $section['content'] ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite metadata marker = ' . var_export( $section['marker'], true ), 0 );
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite metadata error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			foreach ( $destination_sections as $section ) {
    				if ( false === @fwrite( $temp_handle, substr( $file_contents, $section['offset'], $section['length'] ) ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite destination_sections marker = ' . var_export( $section['marker'], true ), 0 );
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite destination_sections error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			if ( false === @fclose( $temp_handle ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata fclose = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @unlink( $path ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata unlink = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @rename( $temp_path, $path ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata rename = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    		 } // if $file_contents
    	} // _replace_jpeg_metadata
    } //MLAMappingHooksExample
    
    /*
     * Install the filters at an early opportunity
     */
    add_action('init', 'MLAMappingHooksExample::initialize');
    ?>

    I forgot to mention that I’m using MLA 1.83.

    Do you have any idea?
    Thank you again,
    Riccardo

    Plugin Author David Lingren

    (@dglingren)

    Riccardo,

    Thanks for your update and for all the effort you put into implementing the solution from the snippets I posted. You are almost there!

    The only issue I can see is in the test that looks for your rule to trigger the export. You have coded:

    if ( 'ExportMLA' == sanitize_title( $key ) && 'none' == $setting['data_source'] && false ...

    You need to change 'ExportMLA' to 'exportmla', because the sanitize_title function changes the $key to lowercase (and changes spaces to dashes and a few other cleanup operations). I was forced to add the sanitize_title function because MLA calls the filter with ‘ExportMLA’ if you’re mapping all attachments and ‘exportmla’ if you’re mapping a single attachment. I regret that, but that’s how the plugin works.

    Try making that change and let me know if it solves your problem.

    By the way, the easy way to “disable” the rule without deleting it when you’re done is to change the .xml part of the file name to something like .x m l or anything else that won’t match .xml. That way you can avoid the overhead of doing the export unless you want it.

    Thread Starter riccardo.tasso

    (@riccardotasso)

    Thanks again David, a couple of fixes more were required, but now the plugin works as a charm!

    Here it is my final version:

    <?php
    /**
     * @package MLA Mapping Hooks Example
     * @version 1.02
     */
    
    /*
    Plugin Name: MLA Mapping Hooks Example
    Plugin URI: https://www.cross-library.com
    Description: Provides an example of the filters provided by the IPTC/EXIF and Custom Field mapping features.
    Author: Riccardo Tasso, David Lingren
    Version: 1.02
    Author URI: https://www.cross-library.com
    
    Copyright 2014 David Lingren
    
    	This program is free software; you can redistribute it and/or modify
    	it under the terms of the GNU General Public License as published by
    	the Free Software Foundation; either version 2 of the License, or
    	(at your option) any later version.
    
    	This program is distributed in the hope that it will be useful,
    	but WITHOUT ANY WARRANTY; without even the implied warranty of
    	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    	GNU General Public License for more details.
    
    	You can get a copy of the GNU General Public License by writing to the
    	Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
    */
    
    /**
     * Class MLA Mapping Hooks Example hooks all of the filters provided by the IPTC/EXIF and Custom Field mapping features
     *
     * Call it anything you want, but give it an unlikely and hopefully unique name. Hiding enerything
     * else inside a class means this is the only name you have to worry about.
     *
     * @package MLA Mapping Hooks Example
     * @since 1.00
     */
    class MLAMappingHooksExample {
    	/**
    	 * Initialization function, similar to __construct()
    	 *
    	 * Installs filters and actions that handle the MLA hooks for uploading and mapping.
    	 *
    	 * @since 1.00
    	 *
    	 * @return	void
    	 */
    	public static function initialize() {
    		/*
    		 * The filters are only useful in the admin section; exit if in the "front-end" posts/pages.
    		 */
    		if ( ! is_admin() )
    			return;
    
    		/*
    		 * add_filter parameters:
    		 * $tag - name of the hook you're filtering; defined by [mla_gallery]
    		 * $function_to_add - function to be called when [mla_gallery] applies the filter
    		 * $priority - default 10; lower runs earlier, higher runs later
    		 * $accepted_args - number of arguments your function accepts
    		 *
    		 * Comment out the filters you don't need; save them for future use
    		 */
    		add_filter( 'mla_upload_prefilter', 'MLAMappingHooksExample::mla_upload_prefilter_filter', 10, 2 );
    		add_filter( 'mla_upload_filter', 'MLAMappingHooksExample::mla_upload_filter_filter', 10, 2 );
    
    		add_action( 'mla_add_attachment', 'MLAMappingHooksExample::mla_add_attachment_action', 10, 1 );
    
    		add_filter( 'mla_update_attachment_metadata_options', 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter', 10, 3 );
    		add_filter( 'mla_update_attachment_metadata_prefilter', 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter', 10, 3 );
    		add_filter( 'mla_update_attachment_metadata_postfilter', 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter', 10, 3 );
    
    		add_filter( 'mla_mapping_settings', 'MLAMappingHooksExample::mla_mapping_settings_filter', 10, 4 );
    		add_filter( 'mla_mapping_rule', 'MLAMappingHooksExample::mla_mapping_rule_filter', 10, 4 );
    		add_filter( 'mla_mapping_custom_value', 'MLAMappingHooksExample::mla_mapping_custom_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_iptc_value', 'MLAMappingHooksExample::mla_mapping_iptc_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_exif_value', 'MLAMappingHooksExample::mla_mapping_exif_value_filter', 10, 5 );
    		add_filter( 'mla_mapping_updates', 'MLAMappingHooksExample::mla_mapping_updates_filter', 10, 5 );
    
    		add_filter( 'mla_get_options_tablist', 'MLAMappingHooksExample::mla_get_options_tablist_filter', 10, 3 );
    	}
    
    	/**
    	 * Save the original image metadata when a file is first uploaded
    	 *
    	 * Array elements are:
    	 * 		'post_id' => 0,
    	 *		'mla_iptc_metadata' => array(),
    	 *		'mla_exif_metadata' => array(),
    	 *		'wp_image_metadata' => array(),
    	 *
    	 * @since 1.00
    	 *
    	 * @var	array
    	 */
    	private static $image_metadata = array();
    	private static $raw_metadata = array();
    
    	/**
    	 * MLA Mapping Upload Prefilter
    	 *
    	 * This filter gives you an opportunity to record the original IPTC, EXIF and
    	 * WordPress image_metadata before the file is stored in the Media Library.
    	 * You can also modify the file name that will be used in the Media Library.
    	 *
    	 * Many plugins and image editing functions alter or destroy this information,
    	 * so this may be your last change to preserve it.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	the file name, type and location
    	 * @param	array	the IPTC, EXIF and WordPress image_metadata
    	 *
    	 * @return	array	updated file name and other information
    	 */
    	public static function mla_upload_prefilter_filter( $file, $image_metadata ) {
    		/*
    		 * Uncomment the error_log statements in any of the filters to see what's passed in
    		 */
    		//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $file = ' . var_export( $file, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $image_metadata = ' . var_export( $image_metadata, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata = $image_metadata;
    		self::$image_metadata['preload_file'] = $file;
    
    		/*
    		 * Save the EXIF, XMP, IPTC and COM data from JPEG files
    		 */
    		if ( 'image/jpeg' == $file['type'] ) {
    			self::$raw_metadata = self::_extract_jpeg_metadata( $file['tmp_name'] );
    			//error_log( 'MLAMappingHooksExample::mla_upload_prefilter_filter $raw_metadata = ' . var_export( self::$raw_metadata, true ), 0 );
    		} else {
    			self::$raw_metadata = array();
    		}
    
    		return $file;
    	} // mla_upload_prefilter_filter
    
    	/**
    	 * MLA Mapping Upload Filter
    	 *
    	 * This filter gives you an opportunity to record some additional metadata
    	 * for audio and video media after the file is stored in the Media Library.
    	 *
    	 * Many plugins and other functions alter or destroy this information,
    	 * so this may be your last change to preserve it.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	the file name, type and location
    	 * @param	array	the ID3 metadata for audio and video files
    	 *
    	 * @return	array	updated file name, type and location
    	 */
    	public static function mla_upload_filter_filter( $file, $id3_data ) {
    		//error_log( 'MLAMappingHooksExample::mla_upload_filter_filter $file = ' . var_export( $file, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_upload_filter_filter $id3_data = ' . var_export( $id3_data, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata['postload_file'] = $file;
    		self::$image_metadata['id3_metadata'] = $id3_data;
    
    		return $file;
    	} // mla_upload_filter_filter
    
    	/**
    	 * MLA Add Attachment Action
    	 *
    	 * This filter is called at the end of the wp_insert_attachment() function,
    	 * after the file is in place and the post object has been created in the database.
    	 *
    	 * By this time, other plugins have probably run their own 'add_attachment' filters
    	 * and done their work/damage to metadata, etc.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	integer	The Post ID of the new attachment
    	 *
    	 * @return	void
    	 */
    	public static function mla_add_attachment_action( $post_ID ) {
    		//error_log( 'MLAMappingHooksExample::mla_add_attachment_action $post_ID = ' . var_export( $post_ID, true ), 0 );
    
    		/*
    		 * Save the information for use in the later filters
    		 */
    		self::$image_metadata['post_id'] = $post_ID;
    	} // mla_add_attachment_action
    
    	/**
    	 * MLA Update Attachment Metadata Options
    	 *
    	 * This filter lets you inspect or change the processing options that will
    	 * control the MLA mapping rules in the update_attachment_metadata filter.
    	 *
    	 * The options are:
    	 *		is_upload - true if this is part of the original file upload process
    	 *		enable_iptc_exif_mapping - true to apply IPTC/EXIF mapping to file uploads
    	 *		enable_custom_field_mapping - true to apply custom field mapping to file uploads
    	 *		enable_iptc_exif_update - true to apply IPTC/EXIF mapping to updates
    	 *		enable_custom_field_update - true to apply custom field mapping to updates
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 *
    	 * @return	array	updated processing options
    	 */
    	public static function mla_update_attachment_metadata_options_filter( $options, $data, $post_ID ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $options = ' . var_export( $options, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_options_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    
    		return $options;
    	} // mla_update_attachment_metadata_prefilter_filter
    
    	/**
    	 * MLA Update Attachment Metadata Prefilter
    	 *
    	 * This filter is called at the end of the wp_update_attachment_metadata() function,
    	 * BEFORE any MLA mapping rules are applied. The prefilter gives you an
    	 * opportunity to record or update the metadata before the mapping.
    	 *
    	 * The wp_update_attachment_metadata() function is called at the end of the file upload process and at
    	 * several later points, such as when an image attachment is edited or by
    	 * plugins that alter the attachment file.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 *
    	 * @return	array	updated attachment metadata
    	 */
    	public static function mla_update_attachment_metadata_prefilter_filter( $data, $post_ID, $options ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_prefilter_filter $options = ' . var_export( $options, true ), 0 );
    
    		/*
    		 * If the metadata has been stripped, try to replace it
    		 * NOTE: Uncomment/comment the "self::" and "$data = " lines to activate/deactivate
    		 */
    		if ( isset( $data['image_meta']['created_timestamp'] )
    			&& empty( $data['image_meta']['created_timestamp'] ) ) {
    				//self::_replace_jpeg_metadata( self::$image_metadata['postload_file']['file'], self::$raw_metadata );
    				//$data = wp_generate_attachment_metadata( $post_ID, self::$image_metadata['postload_file']['file'] );
    				//error_log( 'regenerated data = ' . var_export( $data, true ), 0 );
    
    		}
    
    		return $data;
    	} // mla_update_attachment_metadata_prefilter_filter
    
    	/**
    	 * MLA Update Attachment Metadata Postfilter
    	 *
    	 * This filter is called AFTER MLA mapping rules are applied during
    	 * wp_update_attachment_metadata() processing. The postfilter gives you
    	 * an opportunity to record or update the metadata after the mapping.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	attachment metadata
    	 * @param	integer	The Post ID of the new/updated attachment
    	 * @param	array	Processing options, e.g., 'is_upload'
    	 *
    	 * @return	array	updated attachment metadata
    	 */
    	public static function mla_update_attachment_metadata_postfilter_filter( $data, $post_ID, $options ) {
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $data = ' . var_export( $data, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_update_attachment_metadata_postfilter_filter $options = ' . var_export( $options, true ), 0 );
    
    		return $data;
    	} // mla_update_attachment_metadata_postfilter_filter
    
    	/**
    	 * MLA Mapping Settings Filter
    	 *
    	 * This filter is called before any mapping rules are executed.
    	 * You can add, change or delete rules from the array.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array 	mapping rules
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against, e.g., custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated mapping rules
    	 */
    	public static function mla_mapping_settings_filter( $settings, $post_ID, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $settings = ' . var_export( $settings, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_settings_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * $category gives the context in which the rule is applied:
    		 *		'custom_field_mapping' - mapping custom fields for ALL attachments
    		 *		'single_attachment_mapping' - mapping custom fields for ONE attachments
    		 *
    		 *		'iptc_exif_mapping' - mapping ALL IPTC/EXIF rules
    		 *		'iptc_exif_standard_mapping' - mapping standard field rules
    		 *		'iptc_exif_taxonomy_mapping' - mapping taxonomy term rules
    		 *		'iptc_exif_custom_mapping' - mapping IPTC/EXIF custom field rules
    		 *
    		 * NOTE: 'iptc_exif_mapping' will never be passed to the 'mla_mapping_rule' filter.
    		 * There, one of the three more specific values will be passed.
    		 */
    
    		/*
    		 * For Custom Field Mapping, $settings is an array indexed by
    		 * the custom field name.
    		 * Each array element is a mapping rule; an array containing:
    		 *		'name' => custom field name
    		 *		'data_source' => 'none', 'meta', 'template' or data source name
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *		'format' => 'native', 'commas'
    		 *		'mla_column' => boolean; not used
    		 *		'quick_edit' => boolean; not used
    		 *		'bulk_edit' => boolean; not used
    		 *		'meta_name' => attachment metadata element name or content template
    		 *		'option' => 'text', 'single', 'export', 'array', 'multi'
    		 *		'no_null' => boolean; true to delete empty custom field values
    		 *
    		 * For IPTC/EXIF Mapping, $settings is an array indexed by
    		 * the mapping category; 'standard', 'taxonomy' and 'custom'.
    		 * Each category is an array of rules, with slightly different formats.
    		 *
    		 * Each 'standard' category array element is a rule (array) containing:
    		 *		'name' => field slug; 'post_title', 'post_name', 'image_alt', 'post_excerpt', 'post_content'
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *
    		 * Each 'taxonomy' category array element is a rule (array) containing:
    		 *		'name' => taxonomy slug, e.g., 'post_tag', 'attachment_category'
    		 *		'hierarchical' => boolean; true for hierarchical taxonomies
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 *		'parent' => zero for none or the term_id of the parent
    		 *		'delimiters' => term separator(s), e.g., ',;'
    		 *
    		 * Each 'custom' category array element is a rule (array) containing:
    		 *		'name' => custom field name
    		 *		'iptc_value' => IPTC Identifier or friendly name
    		 *		'exif_value' => EXIF element name
    		 *		'iptc_first' => boolean; true to prefer IPTC value over EXIF value
    		 *		'keep_existing' => boolean; true to preserve existing content
    		 */
    		return $settings;
    	} // mla_mapping_settings_filter
    
    	/**
    	 * MLA Mapping Rule Filter
    	 *
    	 * This filter is called once for each mapping rule, before the rule
    	 * is evaluated. You can change the rule parameters, or prevent rule
    	 * evaluation by returning $setting_value['data_source'] = 'none';
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated custom_field_mapping rule
    	 */
    	public static function mla_mapping_rule_filter( $setting_value, $post_ID, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_rule_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * $setting_value is an array containing a mapping rule; see above
    		 * To stop this rule's evaluation and mapping, return NULL
    		 */
    		return $setting_value;
    	} // mla_mapping_rule_filter
    
    	/**
    	 * MLA Mapping Custom Field Value Filter
    	 *
    	 * This filter is called once for each custom field mapping rule, after the rule
    	 * is evaluated. You can change the new value produced by the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule value
    	 */
    	public static function mla_mapping_custom_value_filter( $new_text, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $new_text = ' . var_export( $new_text, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'size_names',
    			'option' => 'array'
    		);
    		//$size_names = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_custom_value_filter $size_names = ' . var_export( $size_names, true ), 0 );
    
    		/*
    		 * For "empty" values, return ' '.
    		 */
    		return $new_text;
    	} // mla_mapping_custom_value_filter
    
    	/**
    	 * MLA Mapping IPTC Value Filter
    	 *
    	 * This filter is called once for each IPTC/EXIF mapping rule, after the IPTC
    	 * portion of the rule is evaluated. You can change the new value produced by
    	 * the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	IPTC value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule IPTC value
    	 */
    	public static function mla_mapping_iptc_value_filter( $iptc_value, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $iptc_value = ' . var_export( $iptc_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'template',
    			'meta_name' => '([+iptc:keywords+])',
    			'option' => 'array'
    		);
    		//$keywords = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_iptc_value_filter $keywords = ' . var_export( $keywords, true ), 0 );
    
    		/*
    		 * For "empty" values, return ''.
    		 */
    		return $iptc_value;
    	} // mla_mapping_iptc_value_filter
    
    	/**
    	 * MLA Mapping EXIF Value Filter
    	 *
    	 * This filter is called once for each IPTC/EXIF mapping rule, after the EXIF
    	 * portion of the rule is evaluated. You can change the new value produced by
    	 * the rule.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	mixed 	EXIF/Template value returned by the rule
    	 * @param	array 	custom_field_mapping rule
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated rule EXIF/Template value
    	 */
    	public static function mla_mapping_exif_value_filter( $exif_value, $setting_value, $post_id, $category, $attachment_metadata ) {
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $exif_value = ' . var_export( $exif_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $setting_value = ' . var_export( $setting_value, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $post_ID = ' . var_export( $post_ID, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $category = ' . var_export( $category, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $attachment_metadata = ' . var_export( $attachment_metadata, true ), 0 );
    
    		/*
    		 * You can use MLAOptions::mla_get_data_source() to get anything available;
    		 * for example:
    		 */
    		$my_setting = array(
    			'data_source' => 'template',
    			'meta_name' => '([+exif:Copyright+])',
    			'option' => 'array'
    		);
    		//$copyright = MLAOptions::mla_get_data_source($post_id, $category, $my_setting, $attachment_metadata);
    		//error_log( 'MLAMappingHooksExample::mla_mapping_exif_value_filter $copyright = ' . var_export( $copyright, true ), 0 );
    
    		/*
    		 * For "empty" 'text' values, return ''.
    		 * For "empty" 'array' values, return NULL.
    		 */
    		return $exif_value;
    	} // mla_mapping_exif_value_filter
    
    	/**
    	 * MLA Mapping Updates Filter
    	 *
    	 * This filter is called AFTER all mapping rules are applied.
    	 * You can add, change or remove updates for the attachment's
    	 * standard fields, taxonomies and/or custom fields.
    	 *
    	 * @since 1.00
    	 *
    	 * @param	array	updates for the attachment's standard fields, taxonomies and/or custom fields
    	 * @param	integer post ID to be evaluated
    	 * @param	string 	category/scope to evaluate against: custom_field_mapping or single_attachment_mapping
    	 * @param	array 	mapping rules
    	 * @param	array 	attachment_metadata, default NULL
    	 *
    	 * @return	array	updated attachment's updates
    	 */
    	public static function mla_mapping_updates_filter( $updates, $post_id, $category, $settings, $attachment_metadata ) {
    		foreach ($settings as $key => $setting ) {
    		    if ( 'exportmla' == sanitize_title( $key ) && 'none' == $setting['data_source'] && false !== strpos( $setting['meta_name'], '.xml' ) ) {
    		        self::_export_this_item( $post_id, $setting['meta_name'] );
    		    }
    		}
    
    		return $updates;
    	} // mla_mapping_updates_filter
    
    	/**
    	 * Export data for one or more items to an XML file
    	 *
    	 * This function is called from the mla_mapping_updates_filter(),
    	 * just above, when the "export" rule is defined. It writes information
    	 * about the item and its tags to an XML file.
    	 *
    	 * @since 1.01
    	 *
    	 * @param    integer    The ID value of the current item/attachment
    	 * @param    string    The name of the output file, e.g., "data.xml"
    	 *
    	 * @return    void
    	 */
    	private static function _export_this_item( $post_id, $file ) {
    	    static $filename = NULL, $file_pointer = NULL, $tail = "</items>\n";
    
    	    // create/open the file on first call
    	    if ( NULL == $filename ) {
    	        $filename = MLA_BACKUP_DIR . $file;
    
    	        // Make sure the directory exists and is writable
    	        if ( ! file_exists( MLA_BACKUP_DIR ) && ! @mkdir( MLA_BACKUP_DIR ) ) {
    	            return; // Does not exist and cannot create it
    	        } elseif ( ! is_writable( MLA_BACKUP_DIR ) && ! @chmod( MLA_BACKUP_DIR , '0777') ) {
    	            return; // Is not writable and cannot make it so
    	        }
    
    	        // Every directory should have an empty index.php file for security
    	        if ( ! file_exists( MLA_BACKUP_DIR . 'index.php') ) {
    	            @ touch( MLA_BACKUP_DIR . 'index.php');
    	        }
    
    	        // Open the file for write access
    	        $file_pointer = @fopen( $filename, 'w' );
    	        if ( ! $file_pointer ) {
    	            $file_pointer = NULL;
    	            return; // Cannot open a writable file
    	        }
    
    	        // Write the overall header and trailer
    	        if (false === @fwrite($file_pointer, "<items>\n" . $tail )) {
    	            fclose($file_pointer);
    	            $file_pointer = NULL;
    	            return;
    	        }
    	    } // First call
    
    	    if ( NULL == $file_pointer ) {
    	        return; // Don't have a writable file
    	    }
    
    	    // Back up over the old trailer
    	    if ( -1 === fseek( $file_pointer, (0 - strlen( $tail ) ), SEEK_END ) ) {
    	        fclose($file_pointer);
    	        $file_pointer = NULL;
    	        return;
    	    }
    
    	    $post = get_post( $post_id );
    	    $terms = wp_get_post_terms( $post_id, 'attachment_tag' );
    
    	    // Compose the item, line by line
    	    $item = array();
    	    $item[] = "\t<item>";
    	    $item[] = "\t\t<id>" . absint( $post_id ) . '</id>';
    	    $item[] = "\t\t<title>" . $post->post_title . '</title>';
    	    $item[] = "\t\t<url>" . get_attachment_link( $post_id ) . '</url>';
    	    //$item[] = "\t\t<file>" . wp_get_attachment_url( $post_id ) . '</file>';
    	    $item[] = "\t\t<post_date>" . $post->post_date . '</post_date>';
    
    	    foreach( $terms as $term ) {
    	        $item[] = "\t\t<tag>" . $term->name . '</tag>';
    	    }
    
    	    $item[] = "\t</item>";
    	    $item[] = $tail; // already has "\n" appended
    
    	    // Write the item to the file
    	    $item = implode( "\n", $item );
    	    if (false === @fwrite($file_pointer, $item )) {
    	        fclose($file_pointer);
    	        $file_pointer = NULL;
    	        return;
    	    }
    	} // _export_this_item
    
    	/**
    	 * MLA Mapping Updates Filter
    	 *
    	 * This filter is called AFTER all mapping rules are applied.
    	 * You can add, change or remove updates for the attachment's
    	 * standard fields, taxonomies and/or custom fields.
    	 *
    	 * @since 1.02
    	 *
    	 * @param	array|false	The entire tablist ( $tab = NULL ), a single tab entry or false if not found/not allowed.
    	 * @param	array		The entire tablist
    	 * @param	string|NULL	tab slug for single-element return or NULL to return entire tablist
    	 *
    	 * @return	array	updated attachment's updates
    	 */
    	public static function mla_get_options_tablist_filter( $results, $mla_tablist, $tab ) {
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $results = ' . var_export( $results, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $mla_tablist = ' . var_export( $mla_tablist, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::mla_get_options_tablist_filter $tab = ' . var_export( $tab, true ), 0 );
    
    		/*
    		 * Return an updated $mla_tablist ( $tab = NULL ), an updated single element or false
    		 */
    		return $results;
    
    		/*
    		 * Comment out the above return statement to fall through to this example,
    		 * which removes the "Uploads" tab from the Settings/Media Library Assistant submenu
    		 */
    		if ( NULL == $tab ) {
    			unset( $results['upload'] );
    		} elseif ( 'upload' == $tab ) {
    			$results = false;
    		}
    
    		return $results;
    	} // mla_get_options_tablist_filter
    
    	/*
    	 * Selected JPEG Section Markers
    	 */
    	const SOF0	= 0xC0; // Baseline Encoding
    	const SOI	= 0xD8; // Start of image
    	const EOI	= 0xD9; // End of image
    	const SOS	= 0xDA; // Start of scan (image data)
    	const APP0	= 0xE0; // Application segment 0 JFIF Header
    	const APP1	= 0xE1; // Application segment 1 EXIF/XMP
    	const APP2	= 0xE2; // Application segment 2 EXIF Flashpix extensions
    	const APP13	= 0xED; // Application segment 13 IPTC
    	const COM	= 0xFE; // Comment
    
    	/**
    	 * Enumerate the sections of a JPEG file
     	 *
    	 * Returns an array of section descriptors, indexed by the section order, i.e., 0, 1, 2 ...
    	 *
    	 * Each array element is an array, containing:
    	 *		marker => section marker, e.g., 0xD8, 0xE0, 0xED
    	 *		offset => offset in the file of the "0xFF" marker introducing the section
    	 *		length => number of bytes in the section, including the "0xFF", marker byte and length field (if applicable)
    	 *
    	 * @since 1.01
    	 *
    	 * @param	string	File Contents
    	 *
    	 * @return	array	section list ( index => array( 'marker', 'offset', 'length' )
    	 */
    	private static function _enumerate_jpeg_sections( &$file_contents ) {
    		$file_length = strlen( $file_contents );
    		$file_offset = 0;
    		$section_array = array();
    
    		while ( $file_offset < $file_length ) {
    			$section_value = array();
    
    			// Find a marker
    			for ( $i = 0; $i < 7; $i++ ) {
    				if ( 0xFF != ord( $file_contents[ $file_offset + $i ] ) ) {
    					break;
    				}
    			}
    
    			$section_value['marker'] = $marker = ord( $file_contents[ $file_offset + $i ] );
    
    			if ( $marker >= self::SOF0 && $marker <= self::COM ) {
    				$section_value['offset'] = $file_offset + ( $i - 1);
    
    				if ( ( self::SOI == $marker ) || ( self::EOI == $marker ) ) {
    					$file_offset = $file_offset + ( $i + 1 );
    				} elseif ( self::SOS == $marker ) {
    					// Start of Scan precedes image data; skip to end of file/image
    					$file_offset = $file_length - 2;
    
    					// Scan backwards for End of Image marker
    					while ( ( 0xFF != ord( $file_contents[ $file_offset ] ) ) || ( self::EOI != ord( $file_contents[ $file_offset + 1 ] ) ) ) {
    						$file_offset--;
    						if ( $file_offset == $start_of_image ) {
    							// Give up - no End of Image marker
    							$file_offset = $file_length;
    							break;
    						}
    					}
    				} else {
    					// Big Endian length
    					$length = 256 * ord( $file_contents[ $file_offset + ++$i ] );
    					$length += ord( $file_contents[ $file_offset + ++$i ] );
    					$file_offset = $section_value['offset'] + 2 + $length;
    				}
    			}
    			else {
    				// No marker or invalid marker
    				if ( 0 < $i ) {
    					$section_value['offset'] = $file_offset + ( $i - 1 );
    				} else {
    					$section_value['offset'] = $file_offset + $i;
    				}
    				$file_offset = $file_offset + ( $i + 1 );
    
    				while ( $file_offset < $file_length ) {
    					if ( 0xFF == ord( $file_contents[ $file_offset ] ) ) {
    						break;
    					} else {
    						$file_offset++;
    					}
    				}
    			} // invalid marker
    
    			$section_value['length'] = $file_offset - $section_value['offset'];
    			$section_array[] = $section_value;
    			//error_log( 'MLAMappingHooksExample::_enumerate_jpeg_sections $section_value = ' . var_export( $section_value, true ), 0 );
    		} // while offset < length
    
    		return $section_array;
    	} // _enumerate_jpeg_sections
    
    	/**
    	 * Extract IPTC, EXIF/XMP and Comment data from a JPEG file
     	 *
    	 * Returns an array of section content, indexed by the section order
    	 *
    	 * Each array element is an array, containing:
    	 *		marker => section marker, e.g., 0xD8, 0xE0, 0xED
    	 *		content => data bytes in the section
    	 *
    	 * @since 1.01
    	 *
    	 * @param	string	Absolute path to the file
    	 *
    	 * @return	array	section list ( index => array( 'marker', 'content' )
    	 */
    	private static function _extract_jpeg_metadata( $path ) {
    		$metadata = array();
    		$file_contents = file_get_contents( $path, true );
    		 if ( $file_contents ) {
    			 $sections = self::_enumerate_jpeg_sections( $file_contents );
    			 foreach( $sections as $section ) {
    				 if ( in_array( $section['marker'], array( self::APP1, self::APP2, self::APP13, self::COM ) ) ) {
    					$metadata[] = array( 'marker' => $section['marker'],
    					 	'content' => substr( $file_contents, $section['offset'], $section['length'] )
    					);
    				 } // found metadata
    			 } // foreach section
    		 }
    
    		return $metadata;
    	} // _extract_jpeg_metadata
    
    	/**
    	 * Add/replace IPTC, EXIF/XMP and Comment data in a JPEG file
     	 *
    	 * @since 1.01
    	 *
    	 * @param	string	Absolute path to the destination file
    	 * @param	array	Metadata sections from _extract_jpeg_metadata
    	 *
    	 * @return	void
    	 */
    	private static function _replace_jpeg_metadata( $path, $metadata ) {
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $path = ' . var_export( $path, true ), 0 );
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $marker = ' . var_export( $metadata[0]['marker'], true ), 0 );
    
    		$pathinfo = pathinfo( $path );
    		$temp_path = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '-MLA' . $pathinfo['extension'];
    		//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $temp_path = ' . var_export( $temp_path, true ), 0 );
    
    		/*
    		 * Default to the old COM section if the destination file lacks one
    		 */
    		$COM_section = NULL;
    		foreach ( $metadata as $section ) {
    			if ( self::COM == $section['marker'] ) {
    				$COM_section = $section['content'];
    			}
    		}
    
    		/*
    		 * Strip the destination "APP1, APP2, APP13" sections.
    		 * Separate out the SOI, APP0 and COM sections.
    		 */
    		$SOI_section = NULL;
    		$APP0_section = NULL;
    		$destination_sections = array ();
    		$file_contents = file_get_contents( $path, true );
    		 if ( $file_contents ) {
    			 $destination_sections = self::_enumerate_jpeg_sections( $file_contents );
    			foreach ( $destination_sections as $index => $value ) {
    				if ( self::SOI == $value['marker'] ) {
    					$SOI_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif  ( self::APP0 == $value['marker'] ) {
    					$APP0_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif  ( self::COM == $value['marker'] ) {
    					$COM_section = substr( $file_contents, $value['offset'], $value['length'] );
    					unset( $destination_sections[ $index ] );
    				} elseif ( ( self::APP1 == $value['marker'] ) || ( self::APP2 == $value['marker'] ) || ( self::APP13 == $value['marker'] ) ) {
    					unset( $destination_sections[ $index ] );
    				}
    			}
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $SOI_section = ' . var_export( $SOI_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $APP0_section = ' . var_export( $APP0_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $COM_section = ' . var_export( $COM_section, true ), 0 );
    			//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata $destination_sections = ' . var_export( $destination_sections, true ), 0 );
    
    			if ( ( NULL == $SOI_section ) || ( NULL == $APP0_section ) ) {
    				return;
    			}
    
    			@unlink( $temp_path );
    			$temp_handle = @fopen( $temp_path, 'wb' );
    			if ( false === $temp_handle ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fopen error = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @fwrite( $temp_handle, $SOI_section ) ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite SOI error = ' . var_export( error_get_last(), true ), 0 );
    				@fclose( $temp_handle );
    				@unlink( $temp_path );
    				return;
    			}
    
    			if ( false === @fwrite( $temp_handle, $APP0_section ) ) {
    				//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite APP0 error = ' . var_export( error_get_last(), true ), 0 );
    				@fclose( $temp_handle );
    				@unlink( $temp_path );
    				return;
    			}
    
    			if ( ! empty( $COM_section ) ) {
    				if ( false === @fwrite( $temp_handle, $COM_section ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite COM error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			foreach ( $metadata as $section ) {
    				if ( false === @fwrite( $temp_handle, $section['content'] ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite metadata marker = ' . var_export( $section['marker'], true ), 0 );
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite metadata error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			foreach ( $destination_sections as $section ) {
    				if ( false === @fwrite( $temp_handle, substr( $file_contents, $section['offset'], $section['length'] ) ) ) {
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite destination_sections marker = ' . var_export( $section['marker'], true ), 0 );
    					//error_log( 'MLAMappingHooksExample::_replace_jpeg_metadata fwrite destination_sections error = ' . var_export( error_get_last(), true ), 0 );
    					@fclose( $temp_handle );
    					@unlink( $temp_path );
    					return;
    				}
    			}
    
    			if ( false === @fclose( $temp_handle ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata fclose = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @unlink( $path ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata unlink = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    
    			if ( false === @rename( $temp_path, $path ) ) {
    				error_log( 'ERROR: MLAMappingHooksExample::_replace_jpeg_metadata rename = ' . var_export( error_get_last(), true ), 0 );
    				return;
    			}
    		 } // if $file_contents
    	} // _replace_jpeg_metadata
    } //MLAMappingHooksExample
    
    /*
     * Install the filters at an early opportunity
     */
    add_action('init', 'MLAMappingHooksExample::initialize');
    ?>
    Plugin Author David Lingren

    (@dglingren)

    Thanks for your update with the complete plugin source. It looks like you corrected the “sanitize_title” comparison and that you changed the taxonomy for your <tags> elements from the WordPress Tags taxonomy to the MLA Att. Tags taxonomy. I did not see any other changes.

    You should note that the next MLA release will have a simplified version of the “mla-mapping-hooks-example.php.txt” example plugin on which you based your work. The new version will incorporate the export rule/function as a new example and remove the “metadata mapping” and JPG parsing code in the original example. The version you have now works great, but you may want to simplify it based on the new copy in the next MLA version.

    Thanks again for your help with an interesting application of the MLA hooks/filters.

Viewing 7 replies - 1 through 7 (of 7 total)
  • The topic ‘Export metadata’ is closed to new replies.