<?php
/**
* Utility functions
*
* @package distributor
*/
namespace Distributor\Utils;
use Distributor\DistributorPost;
/**
* Determine if this is a development install of Distributor.
*
* @since 2.0.0
*
* @return bool True if this is a development install, false otherwise.
*/
function is_development_version() {
return file_exists( DT_PLUGIN_PATH . 'composer.lock' );
}
/**
* Determine if we are on VIP
*
* @since 1.0
* @return boolean
*/
function is_vip_com() {
return ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV );
}
/**
* Determine if Gutenberg is being used.
*
* This duplicates the check from `use_block_editor_for_post()` in WordPress
* but removes the check for the `meta-box-loader` querystring parameter as
* it is not required for Distributor.
*
* @since 1.2
* @since 1.7 Update Gutenberg plugin sniff to avoid deprecated function.
* Update Classic Editor sniff to account for mu-plugins.
* @since 2.0 Duplicate the check from WordPress Core's `use_block_editor_for_post()`.
*
* @param int|WP_Post $post The post ID or object.
* @return boolean Whether post is using the block editor/Gutenberg.
*/
function is_using_gutenberg( $post ) {
$post = get_post( $post );
if ( ! $post ) {
return false;
}
// The posts page can't be edited in the block editor.
if ( absint( get_option( 'page_for_posts' ) ) === $post->ID && empty( $post->post_content ) ) {
return false;
}
// Make sure this post type supports Gutenberg
$use_block_editor = dt_use_block_editor_for_post_type( $post->post_type );
/** This filter is documented in wp-admin/includes/post.php */
return apply_filters( 'use_block_editor_for_post', $use_block_editor, $post );
}
/**
* Get Distributor settings with defaults
*
* @since 1.0
* @return array
*/
function get_settings() {
$defaults = [
'override_author_byline' => true,
'media_handling' => 'featured',
'email' => '',
'license_key' => '',
'valid_license' => null,
];
$settings = get_option( 'dt_settings', [] );
$settings = wp_parse_args( $settings, $defaults );
return $settings;
}
/**
* Get Distributor network settings with defaults
*
* @since 1.2
* @return array
*/
function get_network_settings() {
$defaults = [
'email' => '',
'license_key' => '',
'valid_license' => null,
];
$settings = get_site_option( 'dt_settings', [] );
$settings = wp_parse_args( $settings, $defaults );
return $settings;
}
/**
* Hit license API to see if key/email is valid
*
* @param string $email Email address.
* @param string $license_key License key.
* @since 1.2
* @return bool
*/
function check_license_key( $email, $license_key ) {
$request = wp_remote_post(
'https://distributorplugin.com/wp-json/distributor-theme/v1/validate-license',
[
// phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
'timeout' => 10,
'headers' => [
'X-Distributor-Version' => DT_VERSION,
],
'body' => [
'license_key' => $license_key,
'email' => $email,
],
]
);
if ( is_wp_error( $request ) ) {
return false;
}
if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
return true;
}
return false;
}
/**
* Determine if plugin is in debug mode or not
*
* @since 1.0
* @return boolean
*/
function is_dt_debug() {
return ( defined( 'DISTRIBUTOR_DEBUG' ) && DISTRIBUTOR_DEBUG );
}
/**
* Given an array of meta, set meta to another post.
*
* Don't copy in excluded (Distributor) meta.
*
* @param int $post_id Post ID.
* @param array $meta Array of meta as key => value
*/
function set_meta( $post_id, $meta ) {
/**
* Fires before Distributor sets post meta.
*
* All sent meta is included in the `$meta` array, including excluded keys.
* Any excluded keys returned in this filter will be subsequently removed
* from the saved meta data.
*
* @since 2.0.0
* @hook dt_before_set_meta
*
* @param {array} $meta All received meta for the post
* @param {int} $post_id Post ID
*/
$meta = apply_filters( 'dt_before_set_meta', $meta, $post_id );
$existing_meta = get_post_meta( $post_id );
$excluded_meta = excluded_meta();
foreach ( $meta as $meta_key => $meta_values ) {
if ( in_array( $meta_key, $excluded_meta, true ) ) {
continue;
}
foreach ( (array) $meta_values as $meta_placement => $meta_value ) {
$has_prev_value = isset( $existing_meta[ $meta_key ] )
&& is_array( $existing_meta[ $meta_key ] )
&& array_key_exists( $meta_placement, $existing_meta[ $meta_key ] )
? true : false;
if ( $has_prev_value ) {
$prev_value = maybe_unserialize( $existing_meta[ $meta_key ][ $meta_placement ] );
}
if ( ! is_array( $meta_value ) ) {
$meta_value = maybe_unserialize( $meta_value );
}
if ( $has_prev_value ) {
update_post_meta( $post_id, wp_slash( $meta_key ), wp_slash( $meta_value ), $prev_value );
} else {
add_post_meta( $post_id, wp_slash( $meta_key ), wp_slash( $meta_value ) );
}
}
}
/**
* Fires after Distributor sets post meta.
*
* Note: All sent meta is included in the `$meta` array, including excluded keys.
* Take care to continue to filter out excluded keys in any further meta setting.
*
* @since 1.3.8
* @hook dt_after_set_meta
* @tutorial snippets
*
* @param {array} $meta All received meta for the post
* @param {array} $existing_meta Existing meta for the post
* @param {int} $post_id Post ID
*/
do_action( 'dt_after_set_meta', $meta, $existing_meta, $post_id );
}
/**
* Get post types available for pulling.
*
* This will compare the public post types from a remote site
* against the public post types from the origin site and return
* an array of post types supported on both.
*
* @param \Distributor\Connection $connection Connection object
* @param string $type Connection type
* @since 1.3
* @return array
*/
function available_pull_post_types( $connection, $type ) {
$post_types = array();
$local_post_types = array();
$remote_post_types = $connection->get_post_types();
$distributable_post_types = distributable_post_types();
// Return empty array, if the source site is not distributing any post type.
if ( empty( $remote_post_types ) || is_wp_error( $remote_post_types ) ) {
return [];
}
$local_post_types = array_diff_key( get_post_types( [ 'public' => true ], 'objects' ), array_flip( [ 'attachment', 'dt_ext_connection', 'dt_subscription' ] ) );
$available_post_types = array_intersect_key( $remote_post_types, $local_post_types );
if ( ! empty( $available_post_types ) ) {
foreach ( $available_post_types as $post_type ) {
$post_types[] = array(
'name' => 'external' === $type ? $post_type['name'] : $post_type->label,
'slug' => 'external' === $type ? $post_type['slug'] : $post_type->name,
);
}
}
/**
* Filter the post types that should be available for pull.
*
* Helpful for sites that want to pull custom post type content from another site into a different existing post type on the receiving end.
*
* @since 1.3.5
* @hook dt_available_pull_post_types
*
* @param {array} $post_types Post types available for pull with name and slug.
* @param {array} $remote_post_types Post types available from the remote connection.
* @param {array} $local_post_types Post types registered as public on the local site.
* @param {Connection} $connection Distributor connection object.
* @param {string} $type Distributor connection type.
*
* @return {array} Post types available for pull with name and slug.
*/
$pull_post_types = apply_filters( 'dt_available_pull_post_types', $post_types, $remote_post_types, $local_post_types, $connection, $type );
if ( ! empty( $pull_post_types ) ) {
$post_types = array();
foreach ( $pull_post_types as $post_type ) {
if ( in_array( $post_type['slug'], $distributable_post_types, true ) ) {
$post_types[] = $post_type;
}
}
}
return $post_types;
}
/**
* Return post types that are allowed to be distributed
*
* @param string $output Optional. The type of output to return.
* Accepts post type 'names' or 'objects'. Default 'names'.
*
* @since 1.0
* @since 1.7.0 $output parameter introduced.
* @return array
*/
function distributable_post_types( $output = 'names' ) {
$post_types = array_filter( get_post_types(), 'is_post_type_viewable' );
$exclude_post_types = [
'attachment',
'dt_ext_connection',
'dt_subscription',
];
foreach ( $exclude_post_types as $exclude_post_type ) {
unset( $post_types[ $exclude_post_type ] );
}
/**
* Filter post types that are distributable.
*
* @since 1.0.0
* @hook distributable_post_types
* @tutorial snippets
*
* @param {array} Post types that are distributable.
*
* @return {array} Post types that are distributable.
*/
$post_types = apply_filters( 'distributable_post_types', $post_types );
// Remove unregistered post types added via the filter.
$post_types = array_filter( $post_types, 'post_type_exists' );
if ( 'objects' === $output ) {
// Convert to objects.
$post_types = array_map( 'get_post_type_object', $post_types );
}
return $post_types;
}
/**
* Return post statuses that are allowed to be distributed.
*
* @since 1.0
* @return array
*/
function distributable_post_statuses() {
/**
* Filter the post statuses that are allowed to be distributed.
*
* By default only published posts can be distributed.
*
* @hook dt_distributable_post_statuses
*
* @param {array} $statuses Post statuses that are distributable. Default `publish`.
*
* @return {array} Post statuses that are distributable.
*/
return apply_filters( 'dt_distributable_post_statuses', array( 'publish' ) );
}
/**
* Returns list of excluded meta keys
*
* @since 1.2
* @deprecated 1.9.0 Use excluded_meta()
* @return array
*/
function blacklisted_meta() {
_deprecated_function( __FUNCTION__, '1.9.0', '\Distributor\Utils\excluded_meta()' );
return excluded_meta();
}
/**
* Returns list of excluded meta keys
*
* @since 1.9.0
* @return array
*/
function excluded_meta() {
/**
* Filter meta keys that are excluded from distribution.
*
* @since 1.0.0
* @deprecated 1.9.0 Use dt_excluded_meta
*
* @param array $meta_keys Excluded meta keys.
*
* @return array Excluded meta keys.
*/
$excluded_meta = apply_filters_deprecated(
'dt_blacklisted_meta',
[
[
'classic-editor-remember',
'dt_unlinked',
'dt_syndicate_time',
'dt_subscriptions',
'dt_subscription_update',
'dt_subscription_signature',
'dt_original_post_url',
'dt_original_post_id',
'dt_original_blog_id',
'dt_connection_map',
'_wp_old_slug',
'_wp_old_date',
'_wp_attachment_metadata',
'_wp_attached_file',
'_edit_lock',
'_edit_last',
],
],
'1.9.0',
'dt_excluded_meta',
__( 'Please consider writing more inclusive code.', 'distributor' )
);
/**
* Filter meta keys that are excluded from distribution.
*
* @since 1.9.0
* @hook dt_excluded_meta
* @tutorial snippets
*
* @param {array} $meta_keys Excluded meta keys. Default `dt_unlinked, dt_connection_map, dt_subscription_update, dt_subscriptions, dt_subscription_signature, dt_original_post_id, dt_original_post_url, dt_original_blog_id, dt_syndicate_time, _wp_attached_file, _wp_attachment_metadata, _edit_lock, _edit_last, _wp_old_slug, _wp_old_date`.
*
* @return {array} Excluded meta keys.
*/
return apply_filters( 'dt_excluded_meta', $excluded_meta );
}
/**
* Prepare meta for consumption
*
* @param int $post_id Post ID.
* @since 1.0
* @return array
*/
function prepare_meta( $post_id ) {
update_postmeta_cache( array( $post_id ) );
$meta = get_post_meta( $post_id );
if ( false === $meta ) {
return array();
}
$meta = is_array( $meta ) ? $meta : array();
$prepared_meta = array();
$excluded_meta = excluded_meta();
// Transfer all meta
foreach ( $meta as $meta_key => $meta_array ) {
foreach ( $meta_array as $meta_value ) {
if ( ! in_array( $meta_key, $excluded_meta, true ) ) {
$meta_value = maybe_unserialize( $meta_value );
/**
* Filter whether to sync meta.
*
* @hook dt_sync_meta
*
* @param {bool} $sync_meta Whether to sync meta. Default `true`.
* @param {string} $meta_key The meta key.
* @param {mixed} $meta_value The meta value.
* @param {int} $post_id The post ID.
*
* @return {bool} Whether to sync meta.
*/
if ( false === apply_filters( 'dt_sync_meta', true, $meta_key, $meta_value, $post_id ) ) {
continue;
}
$prepared_meta[ $meta_key ][] = $meta_value;
}
}
}
/**
* Filter prepared meta for consumption.
*
* Modify meta data before it is sent for consumption by a distributed
* post. The prepared meta data should not include any excluded meta.
* see `excluded_meta()`.
*
* @since 2.0.0
* @hook dt_prepared_meta
*
* @param {array} $prepared_meta Prepared meta.
* @param {int} $post_id Post ID.
*
* @return {array} Prepared meta.
*/
$prepared_meta = apply_filters( 'dt_prepared_meta', $prepared_meta, $post_id );
return $prepared_meta;
}
/**
* Format media items for consumption
*
* @param int $post_id Post ID.
* @since 1.0
* @return array
*/
function prepare_media( $post_id ) {
$dt_post = new DistributorPost( $post_id );
if ( ! $dt_post ) {
return array();
}
return $dt_post->get_media();
}
/**
* Format taxonomy terms for consumption
*
* @since 1.0
*
* @param int $post_id Post ID.
* @param array $args Taxonomy query arguments. See get_taxonomies().
* @return array[] Array of taxonomy terms.
*/
function prepare_taxonomy_terms( $post_id, $args = array() ) {
$post = get_post( $post_id );
if ( ! $post ) {
return array();
}
// Warm the term cache for the post.
update_object_term_cache( array( $post->ID ), $post->post_type );
if ( empty( $args ) ) {
$args = array( 'publicly_queryable' => true );
}
$taxonomy_terms = [];
$taxonomies = get_taxonomies( $args );
/**
* Filters the taxonomies that should be synced.
*
* @since 1.0
* @hook dt_syncable_taxonomies
*
* @param {array} $taxonomies Associative array list of taxonomies supported by current post in the format of `$taxonomy => $terms`.
* @param {WP_Post} $post The post object.
*
* @return {array} Associative array list of taxonomies supported by current post in the format of `$taxonomy => $terms`.
*/
$taxonomies = apply_filters( 'dt_syncable_taxonomies', $taxonomies, $post );
foreach ( $taxonomies as $taxonomy ) {
$taxonomy_terms[ $taxonomy ] = wp_get_object_terms( $post_id, $taxonomy );
}
/**
* Filters the taxonomy terms for consumption.
*
* Modify taxonomies and terms prior to distribution. The array should be
* keyed by taxonomy. The returned data by filters should only return
* taxonomies permitted for distribution. See the `dt_syncable_taxonomies` hook.
*
* @since 2.0.0
* @hook dt_prepared_taxonomy_terms
*
* @param {array} $taxonomy_terms Associative array of terms keyed by taxonomy.
* @param {int} $post_id Post ID.
*
* @param {array} $args Modified array of terms keyed by taxonomy.
*/
$taxonomy_terms = apply_filters( 'dt_prepared_taxonomy_terms', $taxonomy_terms, $post_id );
return $taxonomy_terms;
}
/**
* Given an array of terms by taxonomy, set those terms to another post. This function will cleverly merge
* terms into the post and create terms that don't exist.
*
* @param int $post_id Post ID.
* @param array $taxonomy_terms Array with taxonomy as key and array of terms as values.
* @since 1.0
*/
function set_taxonomy_terms( $post_id, $taxonomy_terms ) {
// Now let's add the taxonomy/terms to syndicated post
foreach ( $taxonomy_terms as $taxonomy => $terms ) {
// Continue if taxonomy doesn't exist
if ( ! taxonomy_exists( $taxonomy ) ) {
continue;
}
$term_ids = [];
$term_id_mapping = [];
foreach ( $terms as $term_array ) {
if ( ! is_array( $term_array ) ) {
$term_array = (array) $term_array;
}
$term = get_term_by( 'slug', $term_array['slug'], $taxonomy );
// Create terms on remote site if they don't exist
/**
* Filter whether missing terms should be created.
*
* @since 1.0.0
* @hook dt_create_missing_terms
*
* @param {bool} true Whether missing terms should be created. Default `true`.
* @param {string} $taxonomy The taxonomy name.
* @param {array} $term_array Term data.
* @param {WP_Term|array|false} $term `WP_Term` object or `array` if found, `false` if not.
*
* @return {bool} Whether missing terms should be created.
*/
$create_missing_terms = apply_filters( 'dt_create_missing_terms', true, $taxonomy, $term_array, $term );
if ( empty( $term ) ) {
// Bail if terms shouldn't be created
if ( false === $create_missing_terms ) {
continue;
}
$term = wp_insert_term(
$term_array['name'],
$taxonomy,
[
'slug' => $term_array['slug'],
'description' => $term_array['description'],
]
);
if ( ! is_wp_error( $term ) ) {
$term_id_mapping[ $term_array['term_id'] ] = $term['term_id'];
$term_ids[] = $term['term_id'];
}
} else {
$term_id_mapping[ $term_array['term_id'] ] = $term->term_id;
$term_ids[] = $term->term_id;
}
}
// Handle hierarchical terms if they exist
/**
* Filter whether term hierarchy should be updated.
*
* @since 1.0.0
* @hook dt_update_term_hierarchy
*
* @param {bool} true Whether term hierarchy should be updated. Default `true`.
* @param {string} $taxonomy The taxonomy slug for the current term.
*
* @return {bool} Whether term hierarchy should be updated.
*/
$update_term_hierarchy = apply_filters( 'dt_update_term_hierarchy', true, $taxonomy );
if ( ! empty( $update_term_hierarchy ) ) {
foreach ( $terms as $term_array ) {
if ( ! is_array( $term_array ) ) {
$term_array = (array) $term_array;
}
if ( empty( $term_array['parent'] ) ) {
$term = wp_update_term(
$term_id_mapping[ $term_array['term_id'] ],
$taxonomy,
[
'parent' => '',
]
);
} elseif ( isset( $term_id_mapping[ $term_array['parent'] ] ) ) {
$term = wp_update_term(
$term_id_mapping[ $term_array['term_id'] ],
$taxonomy,
[
'parent' => $term_id_mapping[ $term_array['parent'] ],
]
);
}
}
}
wp_set_object_terms( $post_id, $term_ids, $taxonomy );
}
}
/**
* Given an array of media, set the media to a new post. This function will cleverly merge media into the
* new post deleting duplicates. Meta and featured image information for each image will be copied as well.
*
* @param int $post_id Post ID.
* @param array $media Array of media posts.
* @param array $args Additional args for set_media.
* @since 1.0
*/
function set_media( $post_id, $media, $args = [] ) {
$settings = get_settings(); // phpcs:ignore
$current_media_posts = get_attached_media( get_allowed_mime_types(), $post_id );
$current_media = [];
$args = wp_parse_args(
$args,
[
'use_filesystem' => false,
]
);
/**
* Allow filtering of the set_media args.
*
* @since 1.6.0
* @hook dt_set_media_args
*
* @param {array} $args List of args.
* @param {int} $post_id Post ID.
* @param {array} $media Array of media posts.
*
* @return {array} set_media args.
*/
$args = apply_filters( 'dt_set_media_args', $args, $post_id, $media );
// Create mapping so we don't create duplicates
foreach ( $current_media_posts as $media_post ) {
$original = get_post_meta( $media_post->ID, 'dt_original_media_url', true );
$current_media[ $original ] = $media_post->ID;
}
$found_featured_image = false;
// If we only want to process the featured image, remove all other media
if ( 'featured' === $settings['media_handling'] ) {
$featured_keys = wp_list_pluck( $media, 'featured' );
// Note: this is not a strict search because of issues with typecasting in some setups
$featured_key = array_search( true, $featured_keys ); // @codingStandardsIgnoreLine Ignore strict search requirement.
$media = ( false !== $featured_key ) ? array( $media[ $featured_key ] ) : array();
}
foreach ( $media as $media_item ) {
$args['source_file'] = $media_item['source_file'];
// Delete duplicate if it exists (unless filter says otherwise)
/**
* Filter whether media should be deleted and replaced if it already exists.
*
* @since 1.0.0
* @hook dt_sync_media_delete_and_replace
*
* @param {bool} true Whether pre-existing media should be deleted and replaced. Default `true`.
* @param {int} $post_id The post ID.
*
* @return {bool} Whether pre-existing media should be deleted and replaced.
*/
if ( apply_filters( 'dt_sync_media_delete_and_replace', true, $post_id ) ) {
if ( ! empty( $current_media[ $media_item['source_url'] ] ) ) {
wp_delete_attachment( $current_media[ $media_item['source_url'] ], true );
}
$image_id = process_media( $media_item['source_url'], $post_id, $args );
} else {
if ( ! empty( $current_media[ $media_item['source_url'] ] ) ) {
$image_id = $current_media[ $media_item['source_url'] ];
} else {
$image_id = process_media( $media_item['source_url'], $post_id, $args );
}
}
// Exit if the image ID is not valid.
if ( ! $image_id ) {
continue;
}
update_post_meta( $image_id, 'dt_original_media_url', $media_item['source_url'] );
update_post_meta( $image_id, 'dt_original_media_id', $media_item['id'] );
if ( $media_item['featured'] ) {
$found_featured_image = true;
set_post_thumbnail( $post_id, $image_id );
}
// Transfer all meta
if ( isset( $media_item['meta'] ) ) {
set_meta( $image_id, $media_item['meta'] );
}
// Transfer post properties
wp_update_post(
[
'ID' => $image_id,
'post_title' => $media_item['title'],
'post_content' => $media_item['description']['raw'],
'post_excerpt' => $media_item['caption']['raw'],
]
);
}
if ( ! $found_featured_image ) {
delete_post_meta( $post_id, '_thumbnail_id' );
}
}
/**
* This is a helper function for transporting/formatting data about a media post
*
* @param \WP_Post $media_post Media post.
* @param int $post_id Post ID.
* @since 1.0
* @return array
*/
function format_media_post( $media_post, $post_id = 0 ) {
$media_item = array(
'id' => $media_post->ID,
'title' => $media_post->post_title,
);
$media_item['featured'] = false;
if ( $post_id && (int) get_post_thumbnail_id( $post_id ) === $media_post->ID ) {
$media_item['featured'] = true;
}
$media_item['description'] = array(
'raw' => $media_post->post_content,
'rendered' => get_processed_content( $media_post->post_content ),
);
$media_item['caption'] = array(
'raw' => $media_post->post_excerpt,
);
$media_item['alt_text'] = get_post_meta( $media_post->ID, '_wp_attachment_image_alt', true );
$media_item['media_type'] = wp_attachment_is_image( $media_post->ID ) ? 'image' : 'file';
$media_item['mime_type'] = $media_post->post_mime_type;
/**
* Filter media details retrieved by `wp_get_attachment_metadata()`.
*
* @hook dt_get_media_details
*
* @param {array|false} $metadata Array of media metadata. `false` on failure.
* @param {int} $media_post->ID The media post ID.
*
* @return {array} Array of media metadata.
*/
$media_item['media_details'] = apply_filters( 'dt_get_media_details', wp_get_attachment_metadata( $media_post->ID ), $media_post->ID );
$media_item['post'] = $media_post->post_parent;
$media_item['source_url'] = wp_get_attachment_url( $media_post->ID );
$media_item['source_file'] = get_attached_file( $media_post->ID );
$media_item['meta'] = \Distributor\Utils\prepare_meta( $media_post->ID );
/**
* Filter formatted media item.
*
* @hook dt_media_item_formatted
*
* @param {array} $media_item Array of media item details.
* @param {int} $media_post->ID The media post ID.
*
* @return {array} Array of media item details.
*/
return apply_filters( 'dt_media_item_formatted', $media_item, $media_post->ID );
}
/**
* Simple function for sideloading media and returning the media id
*
* @param string $url URL of media.
* @param int $post_id Post ID that the media will be assigned to.
* @param array $args Additional args for process_media.
* @since 1.0
* @return int|bool
*/
function process_media( $url, $post_id, $args = [] ) {
global $wp_filesystem;
$args = wp_parse_args(
$args,
[
'use_filesystem' => false,
'source_file' => '',
]
);
/**
* Allow filtering of the process_media args.
*
* @since 1.6.0
* @hook dt_process_media_args
*
* @param array $args List of args.
* @param string $url URL of media.
* @param int $post_id Post ID.
*
* @return array Process media arguments.
*/
$args = apply_filters( 'dt_process_media_args', $args, $url, $post_id );
/**
* Filter allowed media extensions to be processed
*
* @since 1.3.7
* @hook dt_allowed_media_extensions
*
* @param {array} $allowed_extensions Allowed extensions array.
* @param {string} $url Media url.
* @param {int} $post_id Post ID.
*
* @return {array} Media extensions to be processed.
*/
$allowed_extensions = apply_filters( 'dt_allowed_media_extensions', array( 'jpg', 'jpeg', 'jpe', 'gif', 'png' ), $url, $post_id );
preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $url, $matches );
if ( ! $matches ) {
$media_name = null;
} else {
$media_name = basename( $matches[0] );
}
/**
* Filter name of the processing media.
*
* @since 1.3.7
* @hook dt_media_processing_filename
*
* @param {string} $media_name Filename of the media being processed.
* @param {string} $url Media url.
* @param {int} $post_id Post ID.
*
* @return {string} Filename of the media being processed.
*/
$media_name = apply_filters( 'dt_media_processing_filename', $media_name, $url, $post_id );
if ( is_null( $media_name ) ) {
return false;
}
$file_array = array();
$file_array['name'] = $media_name;
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
$download_url = true;
$source_file = false;
$save_source_file_path = false;
if ( $args['use_filesystem'] && isset( $args['source_file'] ) && ! empty( $args['source_file'] ) ) {
$source_file = $args['source_file'];
if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) {
$credentials = request_filesystem_credentials( site_url() );
wp_filesystem( $credentials );
}
// Copy the source file so we don't mess with the original file.
if ( $wp_filesystem->exists( $source_file ) ) {
$temp_name = wp_tempnam( $source_file );
$copied = $wp_filesystem->copy( $source_file, $temp_name, true );
if ( $copied ) {
/**
* Allow filtering whether to save the source file path.
*
* @since 1.6.0
* @hook dt_process_media_save_source_file_path
*
* @param {boolean} $save_file Whether to save the source file path. Default `false`.
*
* @return {boolean} Whether to save the source file path or not.
*/
$save_source_file_path = apply_filters( 'dt_process_media_save_source_file_path', false );
$file_array['tmp_name'] = $temp_name;
$download_url = false;
}
}
}
// Default for external or if a local file copy failed.
if ( $download_url ) {
// Set the scheme to http: if a relative URL is specified.
if ( str_starts_with( $url, '//' ) ) {
$url = 'http:' . $url;
}
// Allows to pull media from local IP addresses
// Uses a "magic number" for priority so we only unhook our call, just in case.
add_filter( 'http_request_host_is_external', '__return_true', 88 );
// Download file to temp location.
$file_array['tmp_name'] = download_url( $url );
remove_filter( 'http_request_host_is_external', '__return_true', 88 );
}
// If error storing temporarily, return the error.
if ( is_wp_error( $file_array['tmp_name'] ) ) {
// Distributor is in debug mode, display the issue, could be storage related.
if ( is_dt_debug() ) {
error_log( sprintf( 'Distributor: %s', $file_array['tmp_name']->get_error_message() ) ); // @codingStandardsIgnoreLine
set_media_errors( $post_id, $file_array['tmp_name']->get_error_message() );
}
return false;
}
// Do the validation and storage stuff.
$result = media_handle_sideload( $file_array, $post_id );
if ( is_wp_error( $result ) ) {
// Distributor is in debug mode, display the issue, could be storage related.
if ( is_dt_debug() ) {
error_log( sprintf( 'Distributor: %s', $result->get_error_message() ) ); // @codingStandardsIgnoreLine
set_media_errors( $post_id, $result->get_error_message() );
}
return false;
}
// Make sure we clean up.
//phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_unlink
@unlink( $file_array['tmp_name'] );
if ( $save_source_file_path ) {
update_post_meta( $result, 'dt_original_file_path', sanitize_text_field( $source_file ) );
}
return (int) $result;
}
/**
* Return whether a post type is compatible with the block editor.
*
* The block editor depends on the REST API, and if the post type is not shown in the
* REST API, then it won't work with the block editor.
*
* This duplicates the function use_block_editor_for_post_type() in WordPress Core
* to ensure the function is always available in Distributor. The function is not
* available in some WordPress contexts.
*
* @source WordPress 5.0.0
*
* @param string $post_type The post type.
* @return bool Whether the post type can be edited with the block editor.
*/
function dt_use_block_editor_for_post_type( $post_type ) {
// In some contexts this function doesn't exist so we can't reliably use it.
if ( function_exists( 'use_block_editor_for_post_type' ) ) {
return use_block_editor_for_post_type( $post_type );
}
if ( ! post_type_exists( $post_type ) ) {
return false;
}
if ( ! post_type_supports( $post_type, 'editor' ) ) {
return false;
}
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object && ! $post_type_object->show_in_rest ) {
return false;
}
/**
* Filters whether an item is able to be edited in the block editor.
*
* @since 1.6.9
* @hook dt_use_block_editor_for_post_type
*
* @param {bool} $use_block_editor Whether the post type uses the block editor. Default true.
* @param {string} $post_type The post type being checked.
*
* @return {bool} Whether the post type uses the block editor.
*/
return apply_filters( 'dt_use_block_editor_for_post_type', true, $post_type );
}
/**
* Helper function to process post content.
*
* @param string $post_content The post content.
*
* @return string $post_content The processed post content.
*/
function get_processed_content( $post_content ) {
global $wp_embed;
/**
* Remove autoembed filter so that actual URL will be pushed and not the generated markup.
*/
remove_filter( 'the_content', [ $wp_embed, 'autoembed' ], 8 );
// Filter documented in WordPress core.
$post_content = apply_filters( 'the_content', $post_content );
add_filter( 'the_content', [ $wp_embed, 'autoembed' ], 8 );
return $post_content;
}
/**
* Gets the REST URL for a post.
*
* @param int $blog_id The blog ID.
* @param int $post_id The post ID.
* @return string
*/
function get_rest_url( $blog_id, $post_id ) {
if ( ! is_multisite() ) {
// Filter documented below.
return apply_filters( 'dt_get_rest_url', false, $blog_id, $post_id );
}
switch_to_blog( $blog_id );
$post = get_post( $post_id );
if ( ! is_a( $post, '\WP_Post' ) ) {
restore_current_blog();
// Filter documented below.
return apply_filters( 'dt_get_rest_url', false, $blog_id, $post_id );
}
$obj = get_post_type_object( $post->post_type );
$rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$base = sprintf( '%s/%s', 'wp/v2', $rest_base );
$rest_url = rest_url( trailingslashit( $base ) . $post->ID );
restore_current_blog();
/**
* Allow filtering of the REST API URL used for pulling post content.
*
* @hook dt_get_rest_url
*
* @param {string} $rest_url The default REST URL to the post.
* @param {int} $blog_id The blog ID.
* @param {int} $post_id The post ID being retrieved.
*
* @return {string} REST API URL for pulling post content.
*/
return apply_filters( 'dt_get_rest_url', $rest_url, $blog_id, $post_id );
}
/**
* Setup additional properties on a post object to enable them to be
* fetched once and manipulated by filters.
*
* @param WP_Post $post WP_Post object.
* @since 1.2.2
* @return WP_Post
*/
function prepare_post( $post ) {
$post->link = get_permalink( $post->ID );
$post->meta = prepare_meta( $post->ID );
$post->terms = prepare_taxonomy_terms( $post->ID );
$post->media = prepare_media( $post->ID );
return $post;
}
/**
* Use transient to store media errors temporarily.
*
* @param int $post_id Post ID where the media attaches to.
* @param array|string $data Error message.
*/
function set_media_errors( $post_id, $data ) {
$errors = get_transient( "dt_media_errors_$post_id" );
if ( ! $errors ) {
$errors = [];
}
if ( is_array( $data ) ) {
$errors += $data;
} else {
$errors[] = $data;
}
set_transient( "dt_media_errors_$post_id", $errors, HOUR_IN_SECONDS );
}
/**
* Reduce arguments passed to wp_insert_post to approved arguments only.
*
* @since 1.7.0
*
* @link http://developer.wordpress.org/reference/functions/wp_insert_post/ wp_insert_post() documentation.
*
* @param array $post_args Arguments used for wp_insert_post() or wp_update_post().
*
* @return array Arguments cleaned of any not expected by the core function.
*/
function post_args_allow_list( $post_args ) {
$allowed_post_keys = array(
'ID',
'post_author',
'post_date',
'post_date_gmt',
'post_content',
'post_content_filtered',
'post_title',
'post_excerpt',
'post_status',
'post_type',
'comment_status',
'ping_status',
'post_password',
'post_name',
'to_ping',
'pinged',
'post_modified',
'post_modified_gmt',
'post_parent',
'menu_order',
'post_mime_type',
'guid',
'import_id',
'post_category',
'tags_input',
'tax_input',
'meta_input',
);
return array_intersect_key( $post_args, array_flip( $allowed_post_keys ) );
}
/**
* Make a remote HTTP request.
*
* Wrapper function for wp_remote_request() and vip_safe_wp_remote_request(). The order
* of parameters differs from vip_safe_wp_remote_request() to promote the arguments array
* to the second parameter.
*
* The default request type is a GET request although the function can be used for other
* HTTP methods by setting the method in the $args array.
*
* See {@see http://developer.wordpress.org/reference/classes/WP_Http/request/ WP_Http::request} for $args defaults.
*
* @param string $url The URL to request.
* @param array $args Optional. An array of arguments to pass to wp_remote_get()/vip_safe_wp_remote_get().
* @param mixed $fallback Optional. Fallback value to return if the request fails. Default ''. VIP only.
* @param int $threshold Optional. The number of fails required before subsequent requests automatically
* return the fallback value. Defaults to 3, with a maximum of 10. VIP only.
* @param int $timeout Optional. The timeout for WP VIP requests. Use $args['timeout'] for others. VIP only.
* All requests have a maximum of 5 seconds except:
* - `POST` requests made via WP CLI have a maximum of 30 seconds.
* - `POST` requests within the WP Admin have a maximum of 15 seconds.
* @param int $retries Optional. The number of retries to attempt. Minimum and default is 10,
* lower values will be increased to 10. VIP only.
*
* @return mixed The response from the remote request. On VIP if the request fails, the fallback value is returned.
*/
function remote_http_request( $url, $args = array(), $fallback = '', $threshold = 3, $timeout = 3, $retries = 10 ) {
if ( function_exists( 'vip_safe_wp_remote_request' ) && is_vip_com() ) {
return vip_safe_wp_remote_request( $url, $fallback, $threshold, $timeout, $retries, $args );
}
return wp_remote_request( $url, $args );
}
/**
* Determines if a post is distributed.
*
* @since 2.0.0
*
* @param int|\WP_Post $post The post object or ID been checked.
* @return bool True if the post is distributed, false otherwise.
*/
function is_distributed_post( $post ) {
$post = get_post( $post );
if ( ! $post ) {
return false;
}
$post_id = $post->ID;
$original_post_id = get_post_meta( $post_id, 'dt_original_post_id', true );
return ! empty( $original_post_id );
}
/**
* Returns the admin icon in data URL base64 format.
*
* @since 2.0.1
*
* @param string $color The hex color if changing the color of the icon. Default `#a0a5aa`.
* @return string Data URL base64 encoded SVG icon string.
*/
function get_admin_icon( $color = '#a0a5aa' ) {
$svg_icon = sprintf( '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="13.4 8.8 573.2 573.2"><path fill="%1$s" d="M195.113 411.033c45.835 46.692 119.124 58.488 178.387 24.273 70.262-40.566 94.371-130.544 53.806-200.806-40.566-70.262-130.544-94.371-200.806-53.806-19.873 11.474-36.055 26.899-48.124 44.715l64.722 33.186c22.201-25.593 59.796-33.782 91.279-17.639 37.002 18.973 51.64 64.418 32.667 101.421-18.973 37.002-64.418 51.64-101.421 32.667-31.483-16.143-46.776-51.45-38.951-84.415l-81.702-41.892c-8.838-4.532-12.335-15.367-7.814-24.211 15.514-30.346 39.658-56.715 71.344-75.009 87.469-50.5 199.482-20.486 249.983 66.983 50.5 87.469 20.486 199.482-66.983 249.983-75.235 43.437-168.63 27.307-225.419-33.717-17.809 3.778-36.797-4.055-46.387-20.666-11.922-20.648-4.837-47.091 15.812-59.012 20.648-11.922 47.091-4.836 59.012 15.812 7.77 13.458 7.466 29.377.595 42.133Z"/><path fill="%1$s" d="M262.237 72.985C148.8 91.101 62 189.494 62 308c0 131.356 106.644 238 238 238s238-106.644 238-238c0-34.059-7.168-66.458-20.08-95.766-15.121.99-30.323-6.014-39.137-19.626-12.959-20.014-7.231-46.783 12.783-59.742 20.014-12.958 46.783-7.231 59.742 12.783 10.095 15.592 8.849 35.284-1.657 49.352C565.288 229.461 574 267.721 574 308c0 151.225-122.775 274-274 274S26 459.225 26 308C26 170.539 127.443 56.584 259.487 36.98 265.594 20.533 281.438 8.8 300 8.8c23.843 0 43.2 19.357 43.2 43.2 0 23.843-19.357 43.2-43.2 43.2-16.229 0-30.38-8.968-37.763-22.215Z"/></svg>', $color );
return sprintf( 'data:image/svg+xml;base64,%s', base64_encode( $svg_icon ) );
}