";
return $return;
}
/**
* Find mime type for file being imported
*
* @since 1.2.3
*
* @param string $absolute_path Absolute path to the file being imported
* @return string|array
*/
static public function media_sync_get_mime_type($absolute_path)
{
try {
$mime_type = '';
if (function_exists('mime_content_type')) {
$mime_type = mime_content_type($absolute_path);
}
if (!empty($mime_type)) {
return $mime_type;
}
$file_type = wp_check_filetype(basename($absolute_path), null);
if(!$file_type['type']) {
return array(
'errorMessage' => sprintf(__('Invalid mime type for file: %s.', 'media-sync'), $absolute_path),
);
}
return $file_type['type'];
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error getting mime type for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
}
/**
* Prepare data to be imported to `wp_posts` table
*
* @since 1.2.2
*
* @param string $absolute_path Absolute path to the file being imported
* @param string $relative_path Path relative to the site URL
* @param string $post_date_type How to generate post date
* @return array
*/
static public function media_sync_prepare_attachment_data($absolute_path, $relative_path, $post_date_type)
{
try {
$mime_type = self::media_sync_get_mime_type($absolute_path);
if (isset($mime_type['error']) || isset($mime_type['errorMessage'])) {
return $mime_type;
}
// Prepare an array of post data for the attachment.
$attachment = array(
'guid' => get_site_url() . $relative_path,
'post_mime_type' => $mime_type,
'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $relative_path ) ),
'post_content' => '',
'post_status' => 'inherit'
);
// Try to get post date based on settings - "Default" option will not set anything, so WP can use defaults
$post_date = self::media_sync_post_date($absolute_path, $post_date_type);
if(!empty($post_date)) {
$attachment['post_date'] = $post_date;
$attachment['post_date_gmt'] = $post_date;
}
return $attachment;
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error preparing attachment data for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
}
/**
* Import file to database (`wp_posts` and `wp_postmeta`)
*
* @since 1.2.1
*
* @param array $attachment Data for `wp_posts` table
* @param string $absolute_path Absolute path to the file being imported
* @return array|true
*/
static public function media_sync_update_import_to_database($attachment, $absolute_path)
{
if (!stream_resolve_include_path($absolute_path)) {
return array(
'errorMessage' => sprintf(__('File to import not found at path: %s.', 'media-sync'), $absolute_path)
);
}
// Insert the attachment (`wp_posts` table)
try {
$max_execution_time = ini_get('max_execution_time');
if ($max_execution_time) {
// Reset execution time so that each file can be processed
set_time_limit($max_execution_time);
}
$attach_id = wp_insert_attachment($attachment, $absolute_path);
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error inserting attachment (`wp_posts` table) for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
if (!($attach_id > 0)) {
// TODO: Revert wp_insert_attachment()?
return array(
'errorMessage' => sprintf(__('Attach ID not received for inserted attachment (`wp_posts` table) for file: %s.', 'media-sync'), $absolute_path)
);
}
try {
// Create custom meta tag (in `wp_postmeta` table) so that we later know this file was imported using this plugin
$media_sync_metadata = 1;
$media_sync_metadata = apply_filters('media_sync_filter_before_update_msc_metadata', $media_sync_metadata, $attach_id, $absolute_path);
if ($media_sync_metadata) {
add_post_meta($attach_id, '_msc', $media_sync_metadata, true);
}
} catch (Exception $e) {}
try {
// Generate the metadata for the attachment
$attach_data = wp_generate_attachment_metadata($attach_id, $absolute_path);
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error generating attachment metadata (`wp_postmeta` table) for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
if (!$attach_data || empty($attach_data)) {
return array(
'errorMessage' => sprintf(__('Attachment metadata could not be generated from %s.', 'media-sync'), $absolute_path),
'error' => json_encode($attachment)
);
}
try {
/**
* Apply this filter to collect additional metadata and/or to run some additional actions,
* e.g. to "auto-connect" items to pages, posts, and WooCommerce products.
* Returning empty data will skip updating metadata (wp_update_attachment_metadata),
* so this filter can also be used to totally overwrite updating attachment metadata.
*
* This filter can be used in a number of different ways:
* 1. to collect additional metadata,
* 2. to run some additional custom actions
* (e.g. to "auto-connect" items to pages, posts, and WooCommerce products),
* 3. to skip or completely overwrite updating the metadata (`wp_update_attachment_metadata` function),
* when this filter returns empty data (null).
*
* NOTE:
* Things could break if the filter doesn't return proper attach data
* and it hasn't created/updated the `wp_postmeta` table record (meta_key _wp_attachment_metadata).
*
* @since 1.2.5
*
* @param array $attach_data Data received from WP function: wp_generate_attachment_metadata
* @param int $attach_id
* @return array|null
*/
$attach_data = apply_filters('media_sync_filter_before_update_metadata', $attach_data, $attach_id);
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error processing filter media_sync_filter_before_update_metadata for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
// There is this same validation (if) before "media_sync_filter_before_update_metadata" filter,
// so if we end up here, it's because of that filter. Which either already updated metadata,
// or doesn't want to update it (basically telling us not to run wp_update_attachment_metadata).
if (!$attach_data || empty($attach_data)) {
return true;
}
try {
// Update `wp_postmeta` database record
wp_update_attachment_metadata($attach_id, $attach_data);
} catch (Exception $e) {
return array(
'errorMessage' => sprintf(__('Error updating attachment metadata (`wp_postmeta` table) for file: %s.', 'media-sync'), $absolute_path),
'error' => $e->getMessage()
);
}
return true;
}
/**
* Returns post date for file being imported based on selected option
*
* @since 1.1.0
* @param string $file_path Absolute path to file being imported
* @param string $type Selected option telling which date to find [default|file_time|smart_file_time]
* @return string
*/
static public function media_sync_post_date($file_path, $type)
{
// Default is empty string so that WordPress can set its default value (current date)
$post_date = '';
try {
// Take "time modified" of file being imported
$file_timestamp = filemtime( $file_path );
if($file_timestamp) {
// Convert to datetime
$file_time = date( 'Y-m-d H:i:s', $file_timestamp );
if($file_time) {
// For "File time" option - return file time
if($type === 'file_time') {
$post_date = $file_time;
}
// For "Smart file time" option - determine post date by comparing file's modified date with folder in which it is
if($type === 'smart_file_time') {
// Use file time as a fallback
$post_date = $file_time;
// Compare the file date with the folder date structure
// Try to extract year and month from file path (e.g. "2020/02" or "2020\02")
// But this path is now always using forward slash (even on Windows Server), so we can again use just '/(\d{4})\/(\d{2})/'
preg_match('/(\d{4})[\/|\\\\](\d{2})/', $file_path, $matches);
if ( !empty($matches) ) {
$year_and_month = $matches[1] . '-' . $matches[2];
$folder_time = $year_and_month . '-' . '01' . ' 00:00:00';
// If what we found for folder date is a date (double check for regex above)
if ( date('Y-m-d H:i:s', strtotime( $folder_time )) == $folder_time ) {
// If file's year and month values do not match with folder's year and month
if ( date( 'Y-m', $file_timestamp ) != $year_and_month ) {
// Use the folder date instead
$post_date = $folder_time;
}
}
}
}
}
}
// Receive external function for setting custom post date
$external = apply_filters('media_sync_filter_post_date', $post_date, $file_path, $type );
// Return received custom post date if it's empty string or a date
if( is_string($external) && ($external === '' || ( date('Y-m-d H:i:s', strtotime( $external )) == $external) ) ) {
$post_date = $external;
}
return $post_date;
} catch ( Exception $e ) {
return $post_date;
}
}
/**
* Get path of directory used by this plugin as main import directory (uploads dir).
*
* Since PHP on Windows Server recognizes both backslash ("\") or forward slash ("/"),
* and WordPress always uses forward slashes (in it's built in functions), we'll also always use forward slashes.
*
* So "C:\www\WP/wp-content/uploads" becomes "C:/www/WP/wp-content/uploads"
*
* Returning for example:
* * /var/www/WP/wp-content/uploads
* * C:/www/WP/wp-content/uploads
*
* @since 1.1.3
* @param boolean $append_sub_dir Should we append sub dir from settings
* @return string
*/
static private function media_sync_get_uploads_basedir($append_sub_dir = true)
{
$upload_dir = wp_upload_dir();
if(!($upload_dir && isset($upload_dir['basedir']) && !empty($upload_dir['basedir']))) {
return null;
}
// Convert backslashes ("\") to forward slashes ("/")
$basedir = wp_normalize_path($upload_dir['basedir']);
// Append sub dir from settings
$sub_dir = get_option( 'ms_sg_scan_sub_dir' );
if($append_sub_dir === true && $sub_dir && !empty($sub_dir)) {
// Add one forward slash between base dir and sub dir
$base_with_sub = $basedir . self::MEDIA_SYNC_DS . trim($sub_dir);
// Return only if exists
if( stream_resolve_include_path($base_with_sub) ) {
return $base_with_sub;
} else {
return null;
}
}
return $basedir;
}
/**
* Get path absolute to WP root. Always using forward slashes ("/").
*
* e.g. /var/www/WP/wp-content/uploads -> /wp-content/uploads
* e.g. C:/www/WP/wp-content/uploads -> /wp-content/uploads
*
* @since 1.1.3
* @param string $absolute_path Absolute file path. Will be convert to forward slashes ("/").
* @return string
*/
static private function media_sync_get_relative_path($absolute_path)
{
// Since get_home_path() and WP in general always use forward slashes, we need to convert it as well
$absolute_path = wp_normalize_path($absolute_path);
// Always using forward slash ("/")
return str_replace(get_home_path(), self::MEDIA_SYNC_DS, $absolute_path);
}
/**
* Scan "uploads" directory and return recursive list of files and directories
*
* @since 0.1.0
* @return Generator
*/
static private function media_sync_get_list_of_uploads()
{
self::$start_time = microtime(true);
self::$upload_dir_path = self::media_sync_get_uploads_basedir();
if(!self::$upload_dir_path) {
return array();
}
// Limit scanning to specific sub folder or encoded path (e.g. &sub_dir=2020%2F01)
if(isset($_GET['sub_dir']) && !empty($_GET['sub_dir'])) {
// Since this path is always using forward slashes, we're also using forward slash ("/")
self::$upload_dir_path = self::$upload_dir_path . self::MEDIA_SYNC_DS . urldecode($_GET['sub_dir']);
}
if(empty(self::$files_in_db)) {
self::$files_in_db = self::media_sync_get_files_in_db();
}
$associated_filter = isset($_GET['associated-filter']) && !empty($_GET['associated-filter']) ? explode(':', urldecode($_GET['associated-filter'])) : null;
// Clear cached files (affecting file_exists, stream_resolve_include_path, etc.)
clearstatcache();
// Show debugging stats if debugging is turned on
self::media_sync_render_debug_stats('before uploads directory scan');
// Get all files - returning Generator (not Array)
$tree = self::media_sync_get_list_of_files(self::$upload_dir_path, self::$files_in_db, $associated_filter);
// Show debugging stats if debugging is turned on
self::media_sync_render_debug_stats('after uploads directory scan');
return $tree;
}
/**
* Scan directory (passed as first value) and return recursive list of files and directories
*
* @since 0.1.0
* @param string $current_dir_path Changing recursively for each directory that gets iterated
* @param array $files_in_db List of files that are already in database
* @param string $associated_filter Filter by "association", for now it can only be "file missing from media library" (solved by "Import Selected")
* @return Generator
*/
static private function media_sync_get_list_of_files($current_dir_path, $files_in_db, $associated_filter)
{
if(!self::$upload_dir_path) {
yield null;
}
// "stream_resolve_include_path" is like "file_exists" but should be quicker
if( !stream_resolve_include_path($current_dir_path) ) {
yield null;
}
foreach (scandir($current_dir_path) as $key => $file_name) {
// Skip ".", "..", etc.
if ($file_name == '.' || $file_name == '..' || $file_name == ".DS_Store" || $file_name == ".htaccess" || $file_name == "index.php") {
continue;
}
// Convert backslashes ("\") to forward slashes ("/")
$full_path = wp_normalize_path($current_dir_path . "/" . $file_name);
$isDir = is_dir($full_path);
// Default file skipping rules:
// 1. contains image size at the end (e.g. -100x100.jpg), or
// 2. ends with "-scaled" (also WP generated), or
// 3. retina thumbnail (e.g. -100x100@2x.jpg), or
// 4. has .webp after another file type (e.g. .jpg.webp).
$is_ignored = preg_match('/(-scaled|[_-]\d+x\d+)|@[2-6]\x(?=\.[a-z]{3,4}$)|\.[a-z]{3,4}.webp/im', $file_name) == true;
// Receive external rules for skipping files/folders
$is_ignored_external = apply_filters('media_sync_filter_is_scan_object_ignored', $is_ignored, $full_path, $file_name);
// Take custom rule for skipping files/folders from external hook/filter function
if (is_bool($is_ignored_external)) {
$is_ignored = $is_ignored_external;
}
if ($is_ignored) {
continue;
}
$relative_path = self::media_sync_get_relative_path($full_path);
$file_in_db = isset($files_in_db[$relative_path]) && !empty($files_in_db[$relative_path]) ?
$files_in_db[$relative_path] : false;
$file_id = $file_in_db && !empty($file_in_db['id']) ? $file_in_db['id'] : false;
$file_status = $file_in_db && !empty($file_in_db['status']) ? $file_in_db['status'] : false;
if (!$isDir && $associated_filter && $associated_filter[0] == 'missing_from' && $associated_filter[1] == 'media_library' && $file_id !== false) {
continue;
}
// Ugly, but efficient
$children = array();
if ($isDir) {
$children = self::media_sync_get_list_of_files($full_path, $files_in_db, $associated_filter);
if (empty($children)) {
continue;
}
}
$file_uid = uniqid('', true);
// Get current parents, to get for example: "/2012/03"
$parents_path = str_replace(self::$upload_dir_path, '', $current_dir_path);
// Trim first slash, to get for example: "2012/03"
$parents_path = ltrim($parents_path, self::MEDIA_SYNC_DS);
// Since this path is always using forward slashes, that's what we'll use here
$parents = !empty($parents_path) ? explode(self::MEDIA_SYNC_DS, $parents_path) : array();
// Replace forward slash with "_" and do other cleanup since this will be used as HTML attribute
$parent_alias = !empty($parents_path) ? sanitize_title(str_replace(self::MEDIA_SYNC_DS, '_', $parents_path), $file_uid) : '';
$alias = sanitize_title($file_name, $file_uid);
$item = array(
'alias' => (!empty($parent_alias) ? $parent_alias . '_' : '') . $alias,
'display_name' => $file_name,
'is_dir' => !!$isDir,
'level' => count($parents) + 1,
'parent_alias' => $parent_alias,
'parents' => $parents,
'absolute_path' => self::media_sync_url_encode($full_path)
);
if ($isDir) {
$item['url'] = 'javascript:;';
$item['children'] = $children;
// TODO: Figure out how to count children when using generators (yield)
$item['count_children'] = null;
} else {
// Encode just file name
$item['url'] = get_site_url() . self::media_sync_url_encode($relative_path);
$item['file_id'] = $file_id;
$item['file_status'] = $file_status;
$item['children'] = [];
$item['count_children'] = 0;
}
yield $item;
}
}
/**
* Convert special characters to safe characters but keeping forward slash.
*
* @param $url
* @return string
* @since 1.2.4
*/
static private function media_sync_url_encode($url)
{
return str_replace('%2F', '/', rawurlencode($url));
}
/**
* Get list of files that are already in database
*
* Caching does not seem to work, disabled for now
*
* @since 0.1.0
* @return array
*/
static private function media_sync_get_files_in_db()
{
$media_query = new WP_Query(array(
'post_type' => 'attachment',
'post_status' => array('inherit', 'trash'),
'posts_per_page' => -1
));
$files = array();
foreach ($media_query->posts as $post) {
$file_url = '';
if(function_exists('wp_get_original_image_url')) {
// First try to find original image URL, to get proper URL without "-scaled" part for bigger images
// https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
$file_url = wp_get_original_image_url($post->ID);
}
// If not image, get attachment URL
if(!$file_url || empty($file_url)) {
$file_url = wp_get_attachment_url($post->ID);
}
// Should already have forward slashes since it's URL
$relative_path = parse_url($file_url, PHP_URL_PATH);
$files[$relative_path] = array(
'id' => $post->ID,
'name' => $post->post_title,
'status' => $post->post_status
);
}
return $files;
}
/**
* Show debugging stats.
*
* @since 1.2.0
* @param string $event When are we showing these stats
* @return void
*/
static public function media_sync_render_debug_stats($event)
{
if(!$event || !self::media_sync_is_debug()) {
return null;
}
$mem_usage = memory_get_usage();
$mem_limit = self::media_sync_get_memory_limit();
$limit_percentage = $mem_usage * 100 / $mem_limit;
?>