Files
nurinda.co.id---old/wp-content/plugins/media-sync/includes/MediaSync.class.php
T

1379 lines
62 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Media Sync
*
* This class is used for generating main content and also to import files to database
*
* @package MediaSync
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 0.1.0
* @author Erol Živina
*/
if ( !class_exists( 'MediaSync' ) ) :
class MediaSync
{
/**
* 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 to solve various issues when using Windows Server.
*/
const MEDIA_SYNC_DS = '/';
/**
* Absolute path to main "uploads" directory.
*
* @var null
*/
static $upload_dir_path = null;
/**
* Files already in Media Library.
*
* @var null|array
*/
static $files_in_db = null;
/**
* For showing passed time when debugging.
*
* @since 1.2.0
* @var null|string
*/
static $start_time = null;
/**
* Render main plugin content
*
* @since 0.1.0
* @return void
*/
static public function media_sync_main_page()
{
if(!MediaSync::media_sync_user_has_general_access()) {
wp_die(__('You do not have sufficient permissions to access this page.', 'media-sync'));
}
$is_debug = self::media_sync_is_debug();
if($is_debug) {
@ini_set('display_errors', 1);
// Enable WP_DEBUG mode
if ( !defined('WP_DEBUG') ) {
define('WP_DEBUG', true);
}
// Enable Debug logging to the /wp-content/debug.log file
if ( !defined('WP_DEBUG_LOG') ) {
define('WP_DEBUG_LOG', true);
}
// Enable display of errors and warnings
if ( !defined('WP_DEBUG_DISPLAY') ) {
define('WP_DEBUG_DISPLAY', true);
}
// Use dev versions of core JS and CSS files (only needed if you are modifying these core files)
if ( !defined('SCRIPT_DEBUG') ) {
define('SCRIPT_DEBUG', true);
}
// 512 MB
@ini_set('memory_limit', '512M');
// 10 min
@ini_set('max_execution_time', '600');
}
$scan_files = isset($_GET['scan_files']) && $_GET['scan_files'] == true;
$associated_filter = isset($_GET['associated-filter']) && !empty($_GET['associated-filter']) ? explode(':', urldecode($_GET['associated-filter'])) : null;
$missing_from_ml = $associated_filter && $associated_filter[0] == 'missing_from' && $associated_filter[1] == 'media_library';
$here = esc_url(get_admin_url(null, 'upload.php?page=media-sync-page'));
$upload_dir_path = self::media_sync_get_uploads_basedir();
$relative_path = self::media_sync_get_relative_path($upload_dir_path);
$no_upload_dir_message = __('Scan directory not found. Please check settings.', 'media-sync');
?>
<div class="wrap main-media-sync-page" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<h1><?= __('Media Sync', 'media-sync') ?></h1>
<?php do_action('media_sync_extended_initial_notice'); ?>
<div class="notice notice-error hidden media-sync-global-errors js-media-sync-error-placeholder"></div>
<?php if ($scan_files) : ?>
<div class="notice notice-error">
<p><?= __('Please backup your database! This plugin makes database changes.', 'media-sync') ?></p>
</div>
<div class="notice notice-success notice-files-imported">
<p><?= sprintf(__('Done! Highlighted files were successfully imported. %s to see changes.', 'media-sync'),
'<a href="'.add_query_arg('scan_files', 1, $here).'">'.__('Re-scan', 'media-sync').'</a>') ?></p>
</div>
<?php endif; ?>
<div class="media-sync-list-files">
<form action="<?= $here ?>" method="GET">
<input type="hidden" name="page" value="media-sync-page"/>
<input type="hidden" name="scan_files" value="<?= $scan_files ?>"/>
<input type="hidden" name="is_debug" value="<?= $is_debug ?>"/>
<div class="media-sync-buttons-holder">
<?php if (!$scan_files) : ?>
<div class="card">
<h2 class="title"><?= __('Sync - uploads directory', 'media-sync') ?></h2>
<?php if($upload_dir_path) : ?>
<a class="button button-primary" href="<?= add_query_arg('scan_files', 1, $here) ?>">
<?= __('Scan Files', 'media-sync') ?>
</a>
<p class="media-sync-scan-files-message">
<?= sprintf(__('Use this to see content of upload dir: %s and import files to Media Library.', 'media-sync'),
'<code title="'.$upload_dir_path.'">'.$relative_path.'</code>') ?>
</p>
<?php else: ?>
<p><i><?= $no_upload_dir_message ?></i></p>
<?php endif; ?>
</div>
<div class="card">
<h2 class="title"><?= __('Sync - Media Library', 'media-sync') ?></h2>
<a class="button button-primary" href="<?= add_query_arg(array('mode' => 'list', 'media_sync_missing_files' => 'yes'), get_admin_url(null, 'upload.php')) ?>">
<?= __('Filter Media Library', 'media-sync') ?>
</a>
<p class="media-sync-scan-files-message">
<?= __('Use this to see Media Library items that are missing actual files. This takes you to Media Library but with custom filter.', 'media-sync') ?>
</p>
</div>
<?php endif; ?>
<?php if ($scan_files) : ?>
<div class="media-sync-button-holder">
<a class="button button-primary" href="<?= add_query_arg('scan_files', 1, $here) ?>">
<?= __('Re-scan', 'media-sync') ?>
</a>
</div>
<div class="media-sync-button-holder">
<button class="button button-primary js-import-selected"><?= __('Import Selected', 'media-sync') ?></button>
<span class="spinner import-spinner"></span>
</div>
<div class="import-options">
<?php if ( !!get_option( 'ms_sg_use_dry_run', 1 ) ) : ?>
<div class="media-sync-dry-run-holder">
<input type="checkbox" id="dry-run" name="dry_run" checked="checked" />
<label for="dry-run"><?= __('Dry Run (test without making database changes)', 'media-sync') ?></label>
</div>
<?php else: ?>
<input type="hidden" name="dry_run" value="" />
<?php endif; ?>
<div class="import-option-date-type">
<label><?= __('Date/time to set for newly imported files', 'media-sync') ?>:</label>
<?php self::media_sync_render_post_date_option( 'file_post_date' ) ?>
</div>
<div class="import-option-batch-size">
<label for="batch-size">
<?= __('Batch Size:', 'media-sync') ?>
<input type="number" value="10" step="1" min="1" class="small-text" name="batch_size" id="batch-size" />
</label>
<p class="description">
<?= __('The number of files to process per network request.', 'media-sync') ?>
<?= __('A lower value might avoid some limits of your server (e.g. max_execution_time). ', 'media-sync') ?>
<br />
<?= __('If you still get errors, please try disabling other plugins related to files/images.', 'media-sync') ?>
<?= __('To reduce additional actions related to file uploads (e.g. generating additional thumbnails).', 'media-sync') ?>
</p>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($scan_files) : ?>
<p class="media-sync-state-holder">
<?php if (!$is_debug) : ?>
<span class="media-sync-progress-holder">
<span class="media-sync-progress"></span>
</span>
<?php endif; ?>
<span class="media-sync-state">
<?php if (!$is_debug) : ?>
<span class="media-sync-state-text">
<?= __('Imported', 'media-sync') ?>
</span>
<span class="media-sync-state-number media-sync-imported-count js-media-sync-imported-count">0</span>
<span class="media-sync-state-text">
<?= __('out of', 'media-sync') ?>
</span>
<?php endif; ?>
<span class="media-sync-state-number media-sync-selected-count js-media-sync-selected-count">0</span>
<span class="media-sync-state-text">
<?= __('selected items', 'media-sync') ?>
</span>
</span>
<span class="media-sync-state media-sync-state-note">
<?= __('Files already in Media Library will be skipped during import', 'media-sync') ?>
</span>
</p>
<div class="wp-filter">
<div class="filter-items">
<label for="associated-filter" class="screen-reader-text"><?= __('Filter by type', 'media-sync') ?></label>
<select class="associated-filters" name="associated-filter" id="associated-filter">
<option value=""><?= __('All files', 'media-sync') ?></option>
<option value="missing_from:media_library"<?= $missing_from_ml ? 'selected="selected"' : '' ?>>
<?= __('Only files missing from Media Library', 'media-sync') ?>
</option>
</select>
<div class="actions">
<input type="submit" name="filter_action" id="post-query-submit" class="button" value="<?= __('Filter', 'media-sync') ?>">
</div>
</div>
</div>
<?php if ($is_debug) : ?>
<div class="media-sync-html-response-holder">
<div class="media-sync-html-response-title"><?= __('Raw import results', 'media-sync') ?>:</div>
<div class="media-sync-html-responses js-media-sync-html-responses"></div>
</div>
<?php endif; ?>
<?php
$tree = $upload_dir_path ? self::media_sync_get_list_of_uploads() : array();
$current_dir = get_option( 'ms_sg_scan_sub_dir' ) ? $relative_path : null;
?>
<?php if (!empty($tree)) : ?>
<div class="media-sync-table-holder">
<table class="wp-list-table widefat fixed media">
<?php self::media_sync_render_thead_tfoot_row('thead', $current_dir) ?>
<tbody id="the-list">
<?php foreach ($tree as $item) : ?>
<?php self::media_sync_render_row($item) ?>
<?php endforeach; ?>
</tbody>
<?php self::media_sync_render_thead_tfoot_row('tfoot') ?>
</table>
<span class="spinner is-active table-spinner"></span>
</div>
<?php
self::media_sync_render_debug_stats('after table render');
?>
<?php else : ?>
<p class="media-sync-no-results">
<?php if ($missing_from_ml) : ?>
<?= __('Everything seems fine here, there are no files that are not already in your Media Library.', 'media-sync') ?>
<?php elseif(!$upload_dir_path) : ?>
<?= $no_upload_dir_message ?>
<?php else : ?>
<?= __('No Results', 'media-sync') ?>
<?php endif; ?>
</p>
<?php endif; ?>
<?php endif; ?>
</form>
</div>
</div>
<?php
}
/**
* Prepare plugin settings
*
* @since 1.1.0
* @return void|boolean
*/
static public function media_sync_options_setup()
{
if ( !current_user_can( 'manage_options' ) ) {
return false;
}
// From this plugin
add_option( 'ms_sg_scan_sub_dir', '' );
add_option( 'ms_sg_use_dry_run', 1 );
add_option( 'ms_sg_file_post_date', 'default' );
add_option( 'ms_sg_use_debug', 0 );
register_setting( 'media-sync-settings-group', 'ms_sg_scan_sub_dir' );
register_setting( 'media-sync-settings-group', 'ms_sg_use_dry_run' );
register_setting( 'media-sync-settings-group', 'ms_sg_file_post_date' );
register_setting( 'media-sync-settings-group', 'ms_sg_use_debug' );
// From Add-On
do_action('media_sync_extended_advanced_options_register');
}
/**
* Render plugin settings
*
* @since 1.1.0
* @return void|boolean
*/
static public function media_sync_options_page()
{
if ( !current_user_can( 'manage_options' ) ) {
return false;
}
?>
<div class="wrap media-sync-page-settings">
<h1><?= __('Media Sync Settings', 'media-sync') ?></h1>
<form method="post" action="options.php">
<?php
// From this plugin
settings_fields( 'media-sync-settings-group' );
do_settings_sections( 'media-sync-settings-group' );
?>
<table class="form-table">
<tbody>
<?php
// From this plugin
?>
<tr>
<th scope="row">
<label for="ms-sg-scan-sub-dir"><?= __('Scan directory', 'media-sync') ?></label>
</th>
<td>
<fieldset>
<?php
$upload_dir_path = self::media_sync_get_uploads_basedir(false);
// Since this path is always using forward slashes, we're also adding forward slash ("/")
$relative_path = self::media_sync_get_relative_path($upload_dir_path) . self::MEDIA_SYNC_DS;
?>
<span><?= $relative_path ?></span>
<input type="text" value="<?= get_option( 'ms_sg_scan_sub_dir' ) ?>" name="ms_sg_scan_sub_dir" id="ms-sg-scan-sub-dir" />
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<label><?= __('Dry Run', 'media-sync') ?></label>
</th>
<td>
<fieldset>
<label for="ms-sg-use-dry-run">
<?php $checked = checked( 1, get_option( 'ms_sg_use_dry_run' ), false ); ?>
<input type="checkbox" name="ms_sg_use_dry_run" id="ms-sg-use-dry-run" value="1" <?= $checked ?>>
<?= __('Show "Dry Run" option', 'media-sync') ?>
</label>
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<label><?= __('Preselected / default value for file date', 'media-sync') ?></label>
</th>
<td>
<?php self::media_sync_render_post_date_option( 'ms_sg_file_post_date' ) ?>
</td>
</tr>
<tr>
<th scope="row">
<label><?= __('Turn on debugging', 'media-sync') ?></label>
</th>
<td>
<fieldset>
<label for="ms-sg-use-debugging">
<?php $checked = checked( 1, get_option( 'ms_sg_use_debug' ), false ); ?>
<input type="checkbox" name="ms_sg_use_debug" id="ms-sg-use-debugging" value="1" <?= $checked ?>>
</label>
</fieldset>
</td>
</tr>
<?php
// From Add-On
do_action('media_sync_extended_advanced_options_render');
?>
</tbody>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
/**
* Render option for selecting post date
*
* @since 1.1.0
* @param string $name Field name
* @return void
*/
static public function media_sync_render_post_date_option($name)
{
$selected = get_option( 'ms_sg_file_post_date' );
if(isset($_GET['file_post_date'])) {
$selected = $_GET['file_post_date'];
}
?>
<fieldset>
<div class="import-option-date-type-item">
<label>
<input type="radio" name="<?= $name ?>" value="default" <?= checked( 'default', $selected ) ?>/>
<span><?= __('Default', 'media-sync') ?></span>
</label>
<p class="description"><?= __('Defaults to what WordPress uses - current time.', 'media-sync') ?></p>
</div>
<div class="import-option-date-type-item">
<label>
<input type="radio" name="<?= $name ?>" value="file_time" <?= checked( 'file_time', $selected ) ?>/>
<span><?= __('File time', 'media-sync') ?></span>
</label>
<p class="description"><?= __('File modification timestamp - when it was last modified.', 'media-sync') ?></p>
</div>
<div class="import-option-date-type-item">
<label>
<input type="radio" name="<?= $name ?>" value="smart_file_time" <?= checked( 'smart_file_time', $selected ) ?>/>
<span><?= __('Smart file time', 'media-sync') ?></span>
</label>
<p class="description"><?= sprintf(__('If file timestamp does not match the folder in which it is - it will construct the date/time based on the folder: %s.', 'media-sync'), '{year}-{month}-01 00:00:00') ?></p>
</div>
</fieldset>
<?php
}
/**
* Render table header and footer
*
* @since 0.1.0
* @param string $tag [thead|tfoot]
* @param string $dir
* @return void
*/
static public function media_sync_render_thead_tfoot_row($tag, $dir = null)
{
$cb_id = 'cb-select-all-' . ($tag == 'thead' ? '1' : '2');
?>
<<?= $tag ?>>
<tr>
<td class="manage-column check-column check-column-all"<?= $tag == 'thead' ? ' id="cb"':''?>>
<label class="screen-reader-text" for="<?= $cb_id ?>"><?= __('Select All', 'media-sync') ?></label>
<input id="<?= $cb_id ?>" type="checkbox">
</td>
<th scope="col" class="manage-column column-title column-primary"<?= $tag == 'thead' ? ' id="title"':''?>>
<span><?= __('File', 'media-sync') ?> <?= $dir ? (__('at', 'media-sync') . ' ' . $dir) : '' ?></span>
</th>
</tr>
</<?= $tag ?>>
<?php
}
/**
* Render table row for each file or directory
*
* @since 0.1.0
* @param array $item
* @return void
*/
static public function media_sync_render_row($item)
{
$has_file_id = isset($item['file_id']) && $item['file_id'] !== false;
$url = $has_file_id ? esc_url(add_query_arg(array('post' => $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'];
?>
<tr class="<?= $cls ?>" id="<?= $row_id ?>" data-parent-id="media-sync-item-<?= $item['parent_alias'] ?>">
<th scope="row" class="check-column">
<label class="screen-reader-text" for="cb-select-<?= $item['alias'] ?>"></label>
<input type="checkbox" class="js-checkbox" id="cb-select-<?= $item['alias'] ?>"
value="<?= $item['absolute_path'] ?>" data-row-id="<?= $row_id ?>">
</th>
<td class="title column-title has-row-actions column-primary" data-colname="<?= __('File', 'media-sync') ?>">
<?php if (!empty($item['parents'])) : ?>
<span class="media-sync-parents">
<?php foreach ($item['parents'] as $parent_key => $parent) : ?>
<?php
$parent_cls = 'media-sync-parent';
$parent_cls .= ' is-first-' . ($parent_key == 0 ? 'yes' : 'no');
$parent_cls .= ' is-last-' . ($parent_key + 1 == count($item['parents']) ? 'yes' : 'no');
?>
<span class="<?= $parent_cls ?>"><i></i></span>
<?php endforeach; ?>
<span class="clearfix"></span>
</span>
<?php endif; ?>
<?php if ($toggle_arrows && $item['is_dir'] === true) : ?>
<span class="js-toggle-row media-sync-toggle-row dashicons"></span>
<?php endif; ?>
<?= $is_link ? '<a href="' . $item['url'] . '"' . $url_attr . '>' : '' ?>
<?php if ($item['is_dir'] === true) : ?>
<span class="dashicons dashicons-category"></span>
<?php endif; ?>
<span class="media-sync-file-name">
<?= $item['display_name'] ?>
</span>
<?= $is_link ? '</a>' : '' ?>
<?php if ($item['is_dir'] === true && $count_children !== null) : ?>
<span class="media-sync-num-items"><?= sprintf('(%u %s)', $count_children, $count_children == 1 ? __('item', 'media-sync') : __('items', 'media-sync')) ?></span>
<?php endif; ?>
<?php if ($has_file_id) : ?>
<span class="media-sync-already-in-db"> - <?= __('Already in', 'media-sync') ?>
<a href="<?= $url ?>" class="dashicons dashicons-admin-media" target="_blank"></a>
<?= $is_trash ? ' (' . __('In Trash', 'media-sync') . ')' : '' ?>
</span>
<?php endif; ?>
</td>
</tr>
<?php
if (!empty($item['children'])) :
foreach ($item['children'] as $child_item) :
self::media_sync_render_row($child_item);
endforeach;
endif;
}
/**
* Ajax action to import selected files
*
* @since 0.1.0
* @return string
*/
static public function media_sync_import_files()
{
if(!MediaSync::media_sync_user_has_general_access()) {
wp_die(__('You do not have sufficient permissions to access this page.', 'media-sync'));
}
check_ajax_referer( 'media_sync_import_files', 'security' );
// Get database stuff
global $wpdb;
$results = array();
if(isset($_POST['media_items']) && !empty($_POST['media_items'])) {
// Make sure that this file is included, as wp_generate_attachment_metadata() depends on it.
require_once( ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'image.php' );
$files_in_db = self::media_sync_get_files_in_db();
$post_date_type = isset($_POST['file_post_date']) ? $_POST['file_post_date'] : get_option( 'ms_sg_file_post_date', 'default' );
$dry_run = isset($_POST['dry_run']) && json_decode($_POST['dry_run']) === true;
foreach ($_POST['media_items'] as $media_item) {
if(isset($media_item['file']) && !empty($media_item['file'])) {
// This comes from JS and it's taken from checkbox value, which is $item['absolute_path'] from media_sync_get_list_of_files()
$absolute_path = urldecode($media_item['file']);
$relative_path = self::media_sync_get_relative_path($absolute_path);
// It's quicker to get all files already in db and check that array, than to do this query for each file
// $query = "SELECT COUNT(*) FROM {$wpdb->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) ? "<br />" : "") . " [Error Message]: {$result['errorMessage']} ";
}
$return = "<div class='media-sync-html-response-item-" . (!empty($errors) ? 'error' : 'success') . "'>";
$return .= "{$icon} {$row_id}" . (!empty($errors) ? '<div>' . $errors . '</div>' : '');
$return .= "</div>";
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;
?>
<div class="media-sync-debugging-stats">
<div class="media-sync-debugging-title">Debugging <strong><?= $event ?></strong>:</div>
<div class="media-sync-debugging-stat-row">
Peak memory usage: <?= memory_get_peak_usage(true) / 1024 / 1024 ?> MB / <?= memory_get_peak_usage(true) / 1024 ?> KB
</div>
<div class="media-sync-debugging-stat-row">
Memory usage: <?= round($mem_usage / 1024 / 1024) ?> MB / <?= round($mem_usage / 1024) ?> KB
(<?= round($limit_percentage) ?>% of the available memory: <?= round($mem_limit / 1024 / 1024) ?> MB / <?= round($mem_limit / 1024) ?> KB)
</div>
<div class="media-sync-debugging-stat-row">
Time since start: <?= self::$start_time > 0 ? microtime(true) - self::$start_time : 0 ?> seconds
</div>
</div>
<?php
}
/**
* Parse the memory_limit variable from the php.ini file.
*
* @since 1.2.0
* @return int
*/
static private function media_sync_get_memory_limit()
{
$limit_string = ini_get('memory_limit');
$unit = strtolower(mb_substr($limit_string, -1 ));
$bytes = intval(mb_substr($limit_string, 0, -1), 10);
switch ($unit)
{
case 'k':
$bytes *= 1024;
break 1;
case 'm':
$bytes *= 1024 * 1024;
break 1;
case 'g':
$bytes *= 1024 * 1024 * 1024;
break 1;
default:
break 1;
}
return $bytes;
}
/**
* Should debugging be used.
*
* @since 1.2.0
* @return boolean
*/
static public function media_sync_is_debug()
{
if(isset($_GET['debug'])) {
return $_GET['debug'] !== "0";
}
if(!!get_option('ms_sg_use_debug', 1)) {
return true;
}
// return defined('WP_DEBUG') && WP_DEBUG;
return false;
}
/**
* Check if logged in user has access
*
* @since 1.1.0
* @return boolean
*/
static public function media_sync_user_has_general_access()
{
if ( !current_user_can( 'upload_files' ) ) {
return false;
}
return true;
}
}
endif; // End if class_exists check.
?>