Files
old-v2-backend/wordpress/wp-content/plugins/wp-migrate-db-pro-media-files/class/wpmdbpro-media-files-base.php
2018-06-25 00:00:37 +02:00

845 lines
24 KiB
PHP

<?php
/**
* Class WPMDBPro_Media_Files_Base
*
* Base class that holds common functionality required by both WPMDBPro_Media_Files_Local and
* WPMDBPro_Media_Files_Remote. Extends WPMDBPro_Addon as we require functionality included in the WPMDB base classes.
* Never instantiated on its own.
*/
class WPMDBPro_Media_Files_Base extends WPMDBPro_Addon {
/**
* The number of seconds a batch should run for when queueing or comparing attachments
*
* @var int $media_diff_batch_time
*/
protected $media_diff_batch_time;
/**
* The max number of attachments in a batch
*
* @var int $media_diff_batch_limit
*/
protected $media_diff_batch_limit;
/**
* The number of seconds a batch should run for when scanning for files
*
* @var int $media_files_batch_time_limit
*/
protected $media_files_batch_time_limit;
protected $accepted_fields;
public function __construct( $plugin_file_path ) {
parent::__construct( $plugin_file_path );
$this->media_diff_batch_time = apply_filters( 'wpmdb_media_diff_batch_time', 10 );
$this->media_diff_batch_limit = apply_filters( 'wpmdb_media_diff_batch_limit', 300 );
$this->media_files_batch_time_limit = apply_filters( 'wpmdb_media_files_batch_time_limit', 15 );
$this->accepted_fields = array(
'media_files',
'remove_local_media',
'media_migration_option',
'mf_select_subsites',
'mf_selected_subsites',
);
add_filter( 'wpmdb_accepted_profile_fields', array( $this, 'accepted_profile_fields' ) );
add_filter( 'wpmdbmf_include_subsite', array( $this, 'include_subsite' ), 10, 2 );
}
/**
* Whitelist media setting fields for use in AJAX save in core
*
* @param array $profile_fields Array of profile fields
*
* @return array Updated array of profile fields
*/
function accepted_profile_fields( $profile_fields ) {
return array_merge( $profile_fields, $this->accepted_fields );
}
/**
* Return total number of local attachments
*
* For multisite returns the total number of attachments for all blogs
*
* @return int Total number of local attachments
*/
function get_local_attachments_count() {
global $wpdb;
$count = 0;
if ( is_multisite() ) {
$blogs = $this->get_blog_ids();
foreach ( $blogs as $blog ) {
$blog_prefix = $wpdb->get_blog_prefix( $blog );
$count += $this->get_attachments_count( $blog_prefix );
}
} else {
$count += $this->get_attachments_count( $wpdb->base_prefix );
}
return $count;
}
/**
* Retrieve the count of attachments for a blog
*
* @param string $prefix Blog db prefix
*
* @return int Number of attachments
*/
function get_attachments_count( $prefix ) {
return $this->get_attachment_results( $prefix, 'count' );
}
/**
* Get all attachments for a blog
*
* @param string $prefix Blog db prefix
* @param int $blog Blog ID
* @param int $limit Limit passed to SQL query (for batching)
* @param array $offset Offset (blog ID, post ID) passed to SQL query (for batching)
*
* @return array Attachments
*/
function get_attachments( $prefix, $blog, $limit, $offset ) {
return $this->get_attachment_results( $prefix, 'rows', array( $blog, $offset, $limit ) );
}
/**
* Utility function for retrieving attachments
*
* @param string $prefix Blog db prefix
* @param string $result_type Type of result we want to retrieve (count / rows / row)
* @param array $args Dependant of $result_type.
* 'count' - none
* 'rows' - $blog_id, $offset, $limit
* 'row' - $blog_id, $filename
*
* @return array Attachments
*/
function get_attachment_results( $prefix, $result_type = 'rows', $args = array() ) {
global $wpdb;
$core = " FROM `{$prefix}posts`
INNER JOIN `{$prefix}postmeta` pm1 ON `{$prefix}posts`.`ID` = pm1.`post_id` AND pm1.`meta_key` = '_wp_attached_file'
LEFT OUTER JOIN `{$prefix}postmeta` pm2 ON `{$prefix}posts`.`ID` = pm2.`post_id` AND pm2.`meta_key` = '_wp_attachment_metadata'
WHERE `{$prefix}posts`.`post_type` = 'attachment' ";
if ( 'count' == $result_type ) {
$sql = 'SELECT COUNT(*)' . $core;
return $wpdb->get_var( $sql );
}
$select = "SELECT `{$prefix}posts`.`ID` AS 'ID', `{$prefix}posts`.`post_modified_gmt` AS 'date', pm1.`meta_value` AS 'file', pm2.`meta_value` AS 'metadata', %d AS 'blog_id'";
$sql = $select . $core;
if ( 'rows' == $result_type ) {
$action = 'get_results';
$sql .= "AND `{$prefix}posts`.`ID` > %d
ORDER BY `{$prefix}posts`.`ID`
LIMIT %d";
} else {
$action = 'get_row';
$sql .= 'AND pm1.`meta_value` = %s';
}
$sql = $wpdb->prepare( $sql, $args );
$results = $wpdb->$action( $sql, ARRAY_A );
return $results;
}
/**
* Return a batch of attachments across all blogs
*
* @param mixed $blogs Blogs
* @param int $limit Max attachments limit (for batching)
* @param array $offset Optional offset (blog ID, post ID) to use instead of $blog['last_post']
*
* @return array Local attachments and blogs
*/
function get_local_attachments_batch( $blogs, $limit, $offset = null ) {
$all_limit = $limit;
if ( ! is_array( $blogs ) ) {
$blogs = unserialize( stripslashes( $blogs ) );
}
$all_attachments = array();
$all_count = 0;
foreach ( $blogs as $blog_id => $blog ) {
if ( 1 == $blog['processed'] ) {
continue;
}
$blog_offset = $blog['last_post'];
if ( is_array( $offset ) ) {
if ( $offset[0] > $blog_id ) {
$blogs[ $blog_id ]['processed'] = 1;
continue;
} elseif ( $blog_id == $offset[0] ) {
$blog_offset = $offset[1];
}
}
$attachments = $this->get_attachments( $blog['prefix'], $blog_id, $limit, $blog_offset );
$count = count( $attachments );
if ( 0 == $count ) {
// no more attachments, record the blog ID to skip next time
$blogs[ $blog_id ]['processed'] = 1;
} else {
$all_count += $count;
// process attachments for sizes files
$attachments = array_map( array( $this, 'process_attachment_data' ), $attachments );
$attachments = array_filter( $attachments );
$all_attachments[ $blog_id ] = $attachments;
}
if ( $all_count >= $all_limit ) {
break;
}
$limit = $limit - $count;
}
$return = array(
'attachments' => $all_attachments,
'blogs' => $blogs,
);
return $return;
}
/**
* Return all attachment files across all blogs
*
* @param array $offset (blog ID, attachment ID) offset
*
* @return array Local media attachment files and last attachment ID
*/
function get_local_media_attachment_files_batch( $offset ) {
$local_media_attachment_files = array();
$last_attachment_id = 0;
$blogs = $this->get_blogs();
$local_media_attachments = $this->get_local_attachments_batch( $blogs, $this->media_diff_batch_limit, $offset );
$last_blog_id = 1;
// Get file paths from attachments
foreach ( $local_media_attachments['attachments'] as $blog_id => $attachments ) {
foreach ( $attachments as $attachment ) {
if ( ! empty( $attachment['file_size'] ) ) {
$local_media_attachment_files[] = $attachment['file'];
$last_blog_id = $blog_id;
$last_attachment_id = $attachment['ID'];
}
if ( isset( $attachment['sizes'] ) && ! empty( $attachment['sizes'] ) ) {
foreach ( $attachment['sizes'] as $size ) {
if ( ! empty( $size['file_size'] ) ) {
$local_media_attachment_files[] = $size['file'];
}
}
}
}
}
return array(
'files' => $local_media_attachment_files,
'last_blog_id' => $last_blog_id,
'last_attachment_id' => $last_attachment_id,
);
}
/**
* Return a batch of local media files
*
* Scans the uploads directory for physical files
*
* @param string $start_file The file or directory to start at
*
* @return array Local media files
*/
function get_local_media_files_batch( $start_file ) {
$local_media_files = array();
$upload_dir = $this->uploads_dir();
if ( ! $this->filesystem->file_exists( $upload_dir ) ) {
return $local_media_files;
}
// Check if we're just kicking off with the root uploads dir
if ( empty( $start_file ) ) {
$this->get_local_media_files_batch_recursive( '', '', $local_media_files );
} else {
$dir = dirname( $start_file );
$start_filename = basename( $start_file );
$this->get_local_media_files_batch_recursive( trailingslashit( $dir ), $start_filename, $local_media_files );
$dirs = explode( '/', $dir );
while ( $dirs ) {
$start_filename = array_pop( $dirs );
$dir = trailingslashit( implode( '/', $dirs ) );
$this->get_local_media_files_batch_recursive( $dir, $start_filename, $local_media_files );
}
}
return $local_media_files;
}
/**
* Recursively go through uploads directories and get a batch of media files.
* Stops when it has scanned all files/directories or after it has run for
* $this->media_files_batch_time_limit seconds, whichever comes first.
*
* @param string $dir The directory to start in
* @param string $start_filename The file or directory to start at within $dir
* @param array $local_media_files Array to populate with media files found
*/
function get_local_media_files_batch_recursive( $dir, $start_filename, &$local_media_files ) {
$upload_dir = $this->uploads_dir();
static $allowed_mime_types;
if ( is_null( $allowed_mime_types ) ) {
$allowed_mime_types = array_flip( get_allowed_mime_types() );
}
static $finish_time;
if ( is_null( $finish_time ) ) {
$finish_time = microtime( true ) + $this->media_files_batch_time_limit;
}
$dir = ( '/' == $dir ) ? '' : $dir;
$dir_path = $upload_dir . $dir;
$sub_paths = glob( $dir_path . '*', GLOB_MARK );
// Get all the files except the one we use to store backups.
$wpmdb_upload_folder = $this->get_upload_info();
$pattern = '/' . preg_quote( $wpmdb_upload_folder, '/' ) . '/';
$files = preg_grep( $pattern, $sub_paths ? $sub_paths : array(), PREG_GREP_INVERT );
$reached_start_file = false;
foreach ( $files as $file_path ) {
if ( microtime( true ) >= $finish_time ) {
break;
}
// Are we starting from a certain file within the directory?
// If so, we skip all the files that come before it.
if ( $start_filename ) {
if ( basename( $file_path ) == $start_filename ) {
$reached_start_file = true;
continue;
} elseif ( ! $reached_start_file ) {
continue;
}
}
$short_file_path = str_replace( array( $upload_dir, '\\' ), array( '', '/' ), $file_path );
// Is directory? We use this instead of is_dir() to save us an I/O call
if ( substr( $file_path, -1 ) == DIRECTORY_SEPARATOR ) {
$this->get_local_media_files_batch_recursive( $short_file_path, '', $local_media_files );
continue;
}
// ignore files that we shouldn't touch, e.g. .php, .sql, etc
$filetype = wp_check_filetype( $short_file_path );
if ( ! isset( $allowed_mime_types[ $filetype['type'] ] ) ) {
continue;
}
if ( apply_filters( 'wpmdbmf_exclude_local_media_file_from_removal', false, $upload_dir, $short_file_path, $this ) ) {
continue;
}
$local_media_files[] = $short_file_path;
}
}
/**
* Queues attachment file and image size files for migration if they exist on the source filesystem
*
* @param array $files_to_migrate List of files to migrate
* @param array $attachment Attachment data
* @param bool $local_attachment Used to compare if files actually exist locally
*/
function maybe_queue_attachment( &$files_to_migrate, $attachment, $local_attachment = false ) {
if ( isset( $attachment['file_size'] ) && ( ! $local_attachment || ( $local_attachment && ! isset( $local_attachment['file_size'] ) ) ) ) {
// if the remote attachment exists on the remote file system
// and if a local attachment is supplied, if the file doesn't exist on local file system
$files_to_migrate[ $attachment['file'] ] = $attachment['file_size'];
}
// check other image sizes of the attachment
if ( empty( $attachment['sizes'] ) || apply_filters( 'wpmdb_exclude_resized_media', false ) ) {
return;
}
foreach ( $attachment['sizes'] as $size ) {
$original_file_name = $size['file'];
// if dir_prefix is set, then the remote is a multisite and we need to compare the file without the subsite directory prefix
if ( ! is_multisite() && $attachment['dir_prefix'] ) {
$size['file'] = str_replace( $attachment['dir_prefix'], '', $size['file'] );
}
if ( isset( $size['file_size'] ) && ( ! $local_attachment || ( $local_attachment && ! $this->local_image_size_file_exists( $size, $local_attachment ) ) ) ) {
// if the remote image size file exists on the remote file system
$files_to_migrate[ $original_file_name ] = $size['file_size'];
}
}
}
/**
* Compare a batch of remote attachments with those on local site
*
* @param mixed $blogs Blogs
* @param mixed $all_attachments Batch of attachments
* @param int $progress Progress count
*
* @return array Data to return to AJAX response
*/
function compare_remote_attachments( $blogs, $all_attachments, $progress ) {
if ( ! is_array( $blogs ) ) {
$blogs = unserialize( stripslashes( $blogs ) );
}
if ( ! is_array( $all_attachments ) ) {
$all_attachments = unserialize( stripslashes( $all_attachments ) );
}
$files_to_migrate = array();
$finish = time() + $this->media_diff_batch_time;
foreach ( $all_attachments as $blog_id => $attachments ) {
foreach ( $attachments as $remote_attachment ) {
if ( time() >= $finish ) {
break;
}
// find local attachment
$local_attachment = $this->find_attachment( $remote_attachment );
if ( false === $local_attachment ) {
// local attachment doesn't exist, definitely migrate remote
$this->maybe_queue_attachment( $files_to_migrate, $remote_attachment );
} else {
// local attachment already exists
// check the timestamps on the attachment
$remote_timestamp = strtotime( $remote_attachment['date'] );
$local_timestamp = strtotime( $local_attachment['date'] );
if ( $remote_timestamp != $local_timestamp ) {
// timestamps are different, let's migrate remote
$this->maybe_queue_attachment( $files_to_migrate, $remote_attachment );
} else {
// only migrate if the local files are missing
$this->maybe_queue_attachment( $files_to_migrate, $remote_attachment, $local_attachment );
}
}
$blogs[ $blog_id ]['last_post'] = $remote_attachment['ID'];
$progress++;
}
}
$return = array(
'files_to_migrate' => $files_to_migrate,
'blogs' => $blogs,
'determine_progress' => $progress,
);
return $return;
}
/**
* Find an attachment in a specific blog
*
* @param array $attachment
*
* @return array|bool Attachment, false if not found
*/
function find_attachment( $attachment ) {
global $wpdb;
$prefix = $wpdb->base_prefix;
if ( is_multisite() ) {
$blog_ids = $this->get_blog_ids();
// check the blog exists
if ( ! in_array( $attachment['blog_id'], $blog_ids ) ) {
return false;
}
$prefix = $wpdb->get_blog_prefix( $attachment['blog_id'] );
}
$filename = $attachment['file'];
$dir_prefix = ( isset( $attachment['dir_prefix'] ) && strlen( $attachment['dir_prefix'] ) ) ? $attachment['dir_prefix'] : $this->get_dir_prefix( $attachment );
// file names are stored in DB without dir prefix, so if the file has one then we need to remove it
$filename = str_replace( $dir_prefix, '', $filename );
$local_attachment = $this->get_attachment_results( $prefix, 'row', array( $attachment['blog_id'], $filename ) );
if ( empty( $local_attachment ) ) {
return false;
}
$local_attachment = $this->process_attachment_data( $local_attachment );
return $local_attachment;
}
/**
* Process an attachment
*
* Adds the physical file size to an attachment including file sizes for all resized images
*
* @param array $attachment The attachment to process
*
* @return array The updated attachment
*/
function process_attachment_data( $attachment ) {
// prepend site directory prefix for multisite blogs
$attachment['dir_prefix'] = $this->get_dir_prefix( $attachment );
if ( is_multisite() && $attachment['dir_prefix'] ) {
$attachment['file'] = $attachment['dir_prefix'] . $attachment['file'];
}
// use the correct directory for image size files
$upload_dir = str_replace( basename( $attachment['file'] ), '', $attachment['file'] );
if ( ! empty( $attachment['metadata'] ) ) {
$attachment['metadata'] = @unserialize( $attachment['metadata'] );
if ( ! empty( $attachment['metadata']['sizes'] ) && is_array( $attachment['metadata']['sizes'] ) ) {
foreach ( $attachment['metadata']['sizes'] as $size ) {
if ( empty( $size['file'] ) ) {
continue;
}
$size_data = array( 'file' => $upload_dir . $size['file'] );
$size_data = $this->apply_file_size( $size_data );
$attachment['sizes'][] = $size_data;
}
}
}
unset( $attachment['metadata'] );
// get size of image on disk
$attachment = $this->apply_file_size( $attachment );
return $attachment;
}
/**
* Remove local files from the uploads directory
*
* @param mixed $local_files Files to remove
*
* @return array $errors Array of errors
*/
function remove_local_media_files( $local_files ) {
if ( ! is_array( $local_files ) ) {
$local_files = @unserialize( $local_files );
}
$errors = array();
if ( empty( $local_files ) ) {
return $errors;
}
$upload_dir = $this->uploads_dir();
foreach ( $local_files as $local_file ) {
if ( false === $this->filesystem->unlink( $upload_dir . $local_file ) && $this->filesystem->file_exists( $upload_dir . $local_file ) ) {
$errors[] = sprintf( __( 'Could not delete "%s"', 'wp-migrate-db-pro-media-files' ), $upload_dir . $local_file ) . ' (#122mf)';
}
}
return $errors;
}
/**
* Compare a set of files with those on the local filesystem
*
* @param mixed $files Files to compare
* @param string $intent
*
* @return array $files_to_remove Files that do not exist locally
*/
function get_files_not_on_local( $files, $intent ) {
if ( ! is_array( $files ) ) {
$files = @unserialize( $files );
}
$upload_dir = $this->uploads_dir();
$files_to_remove = array();
foreach ( $files as $file ) {
if ( ! $this->filesystem->file_exists( $upload_dir . apply_filters( 'wpmdbmf_file_not_on_local', $file, $intent, $this ) ) ) {
$files_to_remove[] = $file;
}
}
return $files_to_remove;
}
/**
* Check if a remote image size file exists locally
*
* @param array $remote_size Remote attachment size
* @param array $local_attachment Local attachment
*
* @return bool
*/
function local_image_size_file_exists( $remote_size, $local_attachment ) {
if ( empty( $local_attachment['sizes'] ) ) {
return false;
}
foreach ( $local_attachment['sizes'] as $size ) {
if ( $size['file'] == $remote_size['file'] ) {
if ( isset( $size['file_size'] ) ) {
return true;
}
}
}
return false;
}
/**
* Store the physical file size in an attachment
*
* Used for attachments files and resized images files
*
* @param array $attachment Attachment
*
* @return array Updated attachment
*/
function apply_file_size( $attachment ) {
if ( ! isset( $attachment['file'] ) ) {
return $attachment;
}
// get size of image on disk
$size = $this->get_file_size( $attachment['file'], ( isset( $attachment['dir_prefix'] ) ? $attachment['dir_prefix'] : '' ) );
if ( false !== $size ) {
$attachment['file_size'] = $size;
}
return $attachment;
}
/**
* Calculate size on disk of a file
*
* @param string $file File path
* @param string $dir_prefix Multisite blog specific directory
*
* @return int|bool File size if exists, false otherwise
*/
function get_file_size( $file, $dir_prefix = '' ) {
$upload_dir = untrailingslashit( $this->uploads_dir() );
if ( ! $this->filesystem->file_exists( $upload_dir ) ) {
return false;
}
if ( ! is_multisite() && $dir_prefix ) {
$file = str_replace( $dir_prefix, '', $file );
}
$file = $upload_dir . DIRECTORY_SEPARATOR . $file;
if ( ! $this->filesystem->file_exists( $file ) ) {
return false;
}
return $this->filesystem->filesize( $file );
}
/**
* Get the directory prefix for an attachment in a multisite blog
*
* @param array $attachment Attachment
*
* @return string Directory prefix
*/
function get_dir_prefix( $attachment ) {
$dir_prefix = ''; // nothing for default blogs
if ( isset( $attachment['blog_id'] ) && ! $this->is_current_blog( $attachment['blog_id'] ) ) {
if ( defined( 'UPLOADBLOGSDIR' ) ) {
$dir_prefix = sprintf( '%s/files/', $attachment['blog_id'] );
} else {
$dir_prefix = sprintf( 'sites/%s/', $attachment['blog_id'] );
}
}
return $dir_prefix;
}
/**
* Return the base uploads directory
*
* @return string Path to uploads directory
*/
function uploads_dir() {
static $upload_dir;
if ( ! is_null( $upload_dir ) ) {
return $upload_dir;
}
if ( defined( 'UPLOADBLOGSDIR' ) ) {
$upload_dir = trailingslashit( ABSPATH ) . UPLOADBLOGSDIR;
} else {
$upload_dir = wp_upload_dir();
$upload_dir = $upload_dir['basedir'];
}
if ( is_multisite() ) {
// Remove multisite postfix
$upload_dir = untrailingslashit( $upload_dir );
$upload_dir = preg_replace( '/\/sites\/(\d)+$/', '', $upload_dir );
}
$upload_dir = trailingslashit( $upload_dir );
return $upload_dir;
}
/**
* Get an array of the blogs in the site to be processed by the addon
*
* @return array Blogs to be processed
*/
function get_blogs() {
global $wpdb;
$blogs = array();
if ( is_multisite() ) {
$blog_ids = $this->get_blog_ids();
foreach ( $blog_ids as $blog_id ) {
$blogs[ $blog_id ] = array(
'prefix' => $wpdb->get_blog_prefix( $blog_id ),
'last_post' => 0,
'processed' => 0,
);
}
} else {
$blogs[1] = array(
'prefix' => $wpdb->base_prefix,
'last_post' => 0, // record last post id process to be used as an offset in the next batch for the blog
'processed' => 0, // flag to record if we have processed all attachments for the blog
);
}
return $blogs;
}
/**
* Get all the IDs of the blogs for the multisite
*
* @return array Blog ID's
*/
function get_blog_ids() {
$blog_ids = array();
if ( ! is_multisite() ) {
return $blog_ids;
}
$args = array(
'spam' => 0,
'deleted' => 0,
'archived' => 0,
'number' => false
);
if ( version_compare( $GLOBALS['wp_version'], '4.6', '>=' ) ) {
$blogs = get_sites( $args );
} else {
$blogs = wp_get_sites( $args );
}
foreach ( $blogs as $blog ) {
$blog = (array) $blog;
if ( apply_filters( 'wpmdbmf_include_subsite', true, $blog['blog_id'], $this ) ) {
$blog_ids[] = $blog['blog_id'];
}
}
return $blog_ids;
}
/**
* Compares the blog ID with the current site specified in wp-config.php
*
* @param int $blog_id Blog ID
*
* @return bool
*/
function is_current_blog( $blog_id ) {
$default = defined( 'BLOG_ID_CURRENT_SITE' ) ? BLOG_ID_CURRENT_SITE : 1;
if ( $default === (int) $blog_id ) {
return true;
}
return false;
}
/**
* Handler for "wpmdbmf_include_subsite" filter to disallow subsite's media to be migrated if not selected.
*
* @param bool $value
* @param int $blog_id
*
* @return bool
*/
public function include_subsite( $value, $blog_id ) {
$this->set_post_data();
if ( is_null( $this->form_data ) && ! empty( $this->state_data['form_data'] ) ) {
$this->form_data = $this->parse_migration_form_data( $this->state_data['form_data'] );
}
if ( false === $value || empty( $this->form_data['mf_select_subsites'] ) || empty( $this->form_data['mf_selected_subsites'] ) ) {
return $value;
}
if ( ! in_array( $blog_id, $this->form_data['mf_selected_subsites'] ) ) {
$value = false;
}
return $value;
}
/**
* Returns validated and sanitized form data.
*
* @param array|string $data
*
* @return array|string
*/
function parse_migration_form_data( $data ) {
$form_data = parent::parse_migration_form_data( $data );
$form_data = array_intersect_key( $form_data, array_flip( $this->accepted_fields ) );
return $form_data;
}
}