'.__('Re-scan', 'media-sync').'') ?>

'.$relative_path.'') ?>


0 0

:

<> > > > $item['file_id'], 'action' => 'edit'), get_admin_url(null, 'post.php'))) : $item['url']; $url_attr = $item['is_dir'] !== true ? ' target="_blank"' : ''; $count_children = $item['count_children']; $cls = 'media-sync-list-file'; $cls .= ' is-' . ($item['is_dir'] === true ? 'dir' : 'file'); $cls .= ' level-' . $item['level']; $cls .= ' is-first-level-' . ($item['level'] === 1 ? 'yes' : 'no'); if ($item['is_dir'] !== true) { $cls .= ' is-in-db-' . ($has_file_id ? 'yes' : 'no'); } // This can be made optional, for a bit different UI $toggle_arrows = true; if ($toggle_arrows) { $is_link = $item['is_dir'] !== true; $cls .= ' toggle-arrows-yes'; } else { $is_link = $item['is_dir'] !== true || $count_children > 0; $url_attr .= ' class="js-toggle-row"'; } $is_trash = isset($item['file_status']) && $item['file_status'] === 'trash'; $row_id = "media-sync-item-" . $item['alias']; ?> $parent) : ?> ' : '' ?> ' : '' ?> - posts} WHERE guid LIKE '%{$relative_path}'"; // $is_in_db = intval($wpdb->get_var($query)) > 0; $is_in_db = isset($files_in_db[$relative_path]) && !empty($files_in_db[$relative_path]); // Check if file is already in database if(!$is_in_db) { // Prepare data to be saved to `wp_posts` and `wp_postmeta` $attachment = self::media_sync_prepare_attachment_data($absolute_path, $relative_path, $post_date_type); if (!$attachment || isset($attachment['error']) || isset($attachment['errorMessage'])) { // It's probably better to just continue importing other files // echo json_encode($attachment); // wp_die(); // This will mark this file as failed and show error below file name $results[] = array( 'row_id' => $media_item['row_id'], 'inserted' => false, 'error' => isset($attachment['error']) ? $attachment['error'] : null, 'errorMessage' => isset($attachment['errorMessage']) ? $attachment['errorMessage'] : null ); continue; } // If we're actually importing (creating database records) if(!$dry_run) { // Import file to database (`wp_posts` and `wp_postmeta`) $import_response = self::media_sync_update_import_to_database($attachment, $absolute_path); // Response is an array if there are errors or true if success $is_inserted = $import_response === true; // It's probably better to just continue importing other files //if (!$is_inserted) { // echo json_encode($import_response); // wp_die(); //} $results[] = array( 'row_id' => $media_item['row_id'], 'inserted' => $is_inserted, 'error' => isset($import_response['error']) ? $import_response['error'] : null, 'errorMessage' => isset($import_response['errorMessage']) ? $import_response['errorMessage'] : null ); } else { $results[] = array( 'row_id' => $media_item['row_id'], 'inserted' => true ); } } } } } if (self::media_sync_is_debug()) { foreach ($results as $result) { echo self::media_sync_render_html_import_result($result); } } else { echo json_encode(array( 'results' => $results )); } wp_die(); // Must have for Ajax calls } /** * Render HTML response for each item. * Used only when debugging is turned on. * * @since 1.2.6 * @param array $result * @return void */ static public function media_sync_render_html_import_result($result) { $icon = isset($result['inserted']) && $result['inserted'] === true ? "✅" : "❌"; $row_id = isset($result['row_id']) ? $result['row_id'] : ""; $errors = ""; if (isset($result['error']) && !empty($result['error'])) { $errors .= " [Error]: {$result['error']} "; } if (isset($result['errorMessage']) && !empty($result['errorMessage'])) { $errors .= (!empty($errors) ? "
" : "") . " [Error Message]: {$result['errorMessage']} "; } $return = "
"; $return .= "{$icon} {$row_id}" . (!empty($errors) ? '
' . $errors . '
' : ''); $return .= "
"; 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; ?>
Debugging :
Peak memory usage: MB / KB
Memory usage: MB / KB (% of the available memory: MB / KB)
Time since start: 0 ? microtime(true) - self::$start_time : 0 ?> seconds