.
*/
namespace Fisharebest\Webtrees;
use Fisharebest\Webtrees\Controller\AjaxController;
use Fisharebest\Webtrees\Controller\PageController;
use Fisharebest\Webtrees\Functions\FunctionsEdit;
define('WT_SCRIPT_NAME', 'admin_media.php');
require './includes/session.php';
// type of file/object to include
$files = Filter::get('files', 'local|external|unused', 'local');
// family tree setting MEDIA_DIRECTORY
$media_folders = all_media_folders();
$media_folder = Filter::get('media_folder', null, ''); // MySQL needs an empty string, not NULL
// User folders may contain special characters. Restrict to actual folders.
if (!array_key_exists($media_folder, $media_folders)) {
$media_folder = reset($media_folders);
}
// prefix to filename
$media_paths = media_paths($media_folder);
$media_path = Filter::get('media_path', null, ''); // MySQL needs an empty string, not NULL
// User paths may contain special characters. Restrict to actual paths.
if (!array_key_exists($media_path, $media_paths)) {
$media_path = reset($media_paths);
}
// subfolders within $media_path
$subfolders = Filter::get('subfolders', 'include|exclude', 'include');
$action = Filter::get('action');
////////////////////////////////////////////////////////////////////////////////
// POST callback for file deletion
////////////////////////////////////////////////////////////////////////////////
$delete_file = Filter::post('delete');
if ($delete_file) {
$controller = new AjaxController;
// Only delete valid (i.e. unused) media files
$media_folder = Filter::post('media_folder', null, ''); // MySQL needs an empty string, not NULL
$disk_files = all_disk_files($media_folder, '', 'include', '');
if (in_array($delete_file, $disk_files)) {
$tmp = WT_DATA_DIR . $media_folder . $delete_file;
try {
unlink($tmp);
FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', Html::filename($tmp)), 'success');
} catch (\ErrorException $ex) {
FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', Html::filename($tmp)) . '
' . $ex->getMessage() . '', 'danger');
}
// Delete any corresponding thumbnail
$tmp = WT_DATA_DIR . $media_folder . 'thumbs/' . $delete_file;
if (file_exists($tmp)) {
try {
unlink($tmp);
FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', Html::filename($tmp)), 'success');
} catch (\ErrorException $ex) {
FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', Html::filename($tmp)) . '' . $ex->getMessage() . '', 'danger');
}
}
} else {
// File no longer exists? Maybe it was already deleted or renamed.
}
$controller->pageHeader();
return;
}
////////////////////////////////////////////////////////////////////////////////
// GET callback for server-side pagination
////////////////////////////////////////////////////////////////////////////////
switch ($action) {
case 'load_json':
$search = Filter::get('search');
$search = $search['value'];
$start = Filter::getInteger('start');
$length = Filter::getInteger('length');
switch ($files) {
case 'local':
// Filtered rows
$SELECT1 =
"SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM m_filename) AS media_path, m_id AS xref, m_titl, m_file AS gedcom_id, m_gedcom AS gedcom" .
" FROM `##media`" .
" JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
" JOIN `##gedcom` USING (gedcom_id)" .
" WHERE setting_value = :media_folder" .
" AND m_filename LIKE CONCAT(:media_path_2, '%')" .
" AND (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', :search_1, '%')" .
" OR m_titl LIKE CONCAT('%', :search_2, '%'))" .
" AND m_filename NOT LIKE 'http://%'" .
" AND m_filename NOT LIKE 'https://%'";
$ARGS1 = array(
'media_path_1' => $media_path,
'media_folder' => $media_folder,
'media_path_2' => Filter::escapeLike($media_path),
'search_1' => Filter::escapeLike($search),
'search_2' => Filter::escapeLike($search),
);
// Unfiltered rows
$SELECT2 =
"SELECT SQL_CACHE COUNT(*)" .
" FROM `##media`" .
" JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
" WHERE setting_value = :media_folder" .
" AND m_filename LIKE CONCAT(:media_path_3, '%')" .
" AND m_filename NOT LIKE 'http://%'" .
" AND m_filename NOT LIKE 'https://%'";
$ARGS2 = array(
'media_folder' => $media_folder,
'media_path_3' => $media_path,
);
if ($subfolders == 'exclude') {
$SELECT1 .= " AND m_filename NOT LIKE CONCAT(:media_path_4, '%/%')";
$ARGS1['media_path_4'] = Filter::escapeLike($media_path);
$SELECT2 .= " AND m_filename NOT LIKE CONCAT(:media_path_4, '%/%')";
$ARGS2['media_path_4'] = Filter::escapeLike($media_path);
}
$order = Filter::getArray('order');
$SELECT1 .= " ORDER BY ";
if ($order) {
foreach ($order as $key => $value) {
if ($key > 0) {
$SELECT1 .= ',';
}
// Datatables numbers columns 0, 1, 2
// MySQL numbers columns 1, 2, 3
switch ($value['dir']) {
case 'asc':
$SELECT1 .= ":col_" . $key . " ASC";
break;
case 'desc':
$SELECT1 .= ":col_" . $key . " DESC";
break;
}
$ARGS1['col_' . $key] = 1 + $value['column'];
}
} else {
$SELECT1 = " 1 ASC";
}
if ($length > 0) {
$SELECT1 .= " LIMIT :length OFFSET :start";
$ARGS1['length'] = $length;
$ARGS1['start'] = $start;
}
$rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll();
// Total filtered/unfiltered rows
$recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
$recordsTotal = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne();
$data = array();
foreach ($rows as $row) {
$media = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
$data[] = array(
mediaFileInfo($media_folder, $media_path, $row->media_path),
$media->displayImage(),
mediaObjectInfo($media),
);
}
break;
case 'external':
// Filtered rows
$SELECT1 =
"SELECT SQL_CACHE SQL_CALC_FOUND_ROWS m_filename, m_id AS xref, m_titl, m_file AS gedcom_id, m_gedcom AS gedcom" .
" FROM `##media`" .
" WHERE (m_filename LIKE 'http://%' OR m_filename LIKE 'https://%')" .
" AND (m_filename LIKE CONCAT('%', :search_1, '%') OR m_titl LIKE CONCAT('%', :search_2, '%'))";
$ARGS1 = array(
'search_1' => Filter::escapeLike($search),
'search_2' => Filter::escapeLike($search),
);
// Unfiltered rows
$SELECT2 =
"SELECT SQL_CACHE COUNT(*)" .
" FROM `##media`" .
" WHERE (m_filename LIKE 'http://%' OR m_filename LIKE 'https://%')";
$ARGS2 = array();
$order = Filter::getArray('order');
$SELECT1 .= " ORDER BY ";
if ($order) {
foreach ($order as $key => $value) {
if ($key > 0) {
$SELECT1 .= ',';
}
// Datatables numbers columns 0, 1, 2
// MySQL numbers columns 1, 2, 3
switch ($value['dir']) {
case 'asc':
$SELECT1 .= ":col_" . $key . " ASC";
break;
case 'desc':
$SELECT1 .= ":col_" . $key . " DESC";
break;
}
$ARGS1['col_' . $key] = 1 + $value['column'];
}
} else {
$SELECT1 = " 1 ASC";
}
if ($length > 0) {
$SELECT1 .= " LIMIT :length OFFSET :start";
$ARGS1['length'] = $length;
$ARGS1['start'] = $start;
}
$rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll();
// Total filtered/unfiltered rows
$recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
$recordsTotal = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne();
$data = array();
foreach ($rows as $row) {
$media = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
$data[] = array(
GedcomTag::getLabelValue('URL', $row->m_filename),
$media->displayImage(),
mediaObjectInfo($media),
);
}
break;
case 'unused':
// Which trees use this media folder?
$media_trees = Database::prepare(
"SELECT gedcom_name, gedcom_name" .
" FROM `##gedcom`" .
" JOIN `##gedcom_setting` USING (gedcom_id)" .
" WHERE setting_name='MEDIA_DIRECTORY' AND setting_value = :media_folder AND gedcom_id > 0"
)->execute(array(
'media_folder' => $media_folder,
))->fetchAssoc();
$disk_files = all_disk_files($media_folder, $media_path, $subfolders, $search);
$db_files = all_media_files($media_folder, $media_path, $subfolders, $search);
// All unused files
$unused_files = array_diff($disk_files, $db_files);
$recordsTotal = count($unused_files);
// Filter unused files
if ($search) {
$unused_files = array_filter($unused_files, function ($x) use ($search) { return strpos($x, $search) !== false; });
}
$recordsFiltered = count($unused_files);
// Sort files - only option is column 0
sort($unused_files);
$order = Filter::get('order');
if ($order && $order[0]['dir'] === 'desc') {
$unused_files = array_reverse($unused_files);
}
// Paginate unused files
$unused_files = array_slice($unused_files, $start, $length);
$data = array();
foreach ($unused_files as $unused_file) {
$full_path = WT_DATA_DIR . $media_folder . $media_path . $unused_file;
$thumb_path = WT_DATA_DIR . $media_folder . 'thumbs/' . $media_path . $unused_file;
if (!file_exists($thumb_path)) {
$thumb_path = $full_path;
}
try {
$imgsize = getimagesize($thumb_path);
// We can’t create a URL (not in public_html) or use the media firewall (no such object)
// so just the base64-encoded image inline.
if ($imgsize === false) {
// not an image
$img = '-';
} else {
$img = '';
}
} catch (\ErrorException $ex) {
// Not an image, or not a valid image?
$img = '-';
}
// Is there a pending record for this file?
$exists_pending = Database::prepare(
"SELECT 1 FROM `##change` WHERE status='pending' AND new_gedcom LIKE CONCAT('%\n1 FILE ', :unused_file, '\n%')"
)->execute(array(
'unused_file' => Filter::escapeLike($unused_file),
))->fetchOne();
// Form to create new media object in each tree
$create_form = '';
if (!$exists_pending) {
foreach ($media_trees as $media_tree) {
$create_form .=
'
';
$data[] = array(
mediaFileInfo($media_folder, $media_path, $unused_file) . $delete_link,
$img,
$create_form,
);
}
break;
default:
throw new \DomainException('Invalid action');
}
header('Content-type: application/json');
// See http://www.datatables.net/usage/server-side
echo json_encode(array(
'draw' => Filter::getInteger('draw'), // String, but always an integer
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $data,
));
return;
}
/**
* A unique list of media folders, from all trees.
*
* @return string[]
*/
function all_media_folders() {
return Database::prepare(
"SELECT SQL_CACHE setting_value, setting_value" .
" FROM `##gedcom_setting`" .
" WHERE setting_name='MEDIA_DIRECTORY' AND gedcom_id > 0" .
" GROUP BY 1" .
" ORDER BY 1"
)->execute(array())->fetchAssoc();
}
/**
* Generate a list of media paths (within a media folder) used by all media objects.
*
* @param string $media_folder
*
* @return string[]
*/
function media_paths($media_folder) {
$media_paths = Database::prepare(
"SELECT SQL_CACHE LEFT(m_filename, CHAR_LENGTH(m_filename) - CHAR_LENGTH(SUBSTRING_INDEX(m_filename, '/', -1))) AS media_path" .
" FROM `##media`" .
" JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
" WHERE setting_value = :media_folder" .
" AND m_filename NOT LIKE 'http://%'" .
" AND m_filename NOT LIKE 'https://%'" .
" GROUP BY 1" .
" ORDER BY 1"
)->execute(array(
'media_folder' => $media_folder,
))->fetchOneColumn();
if (!$media_paths || reset($media_paths) != '') {
// Always include a (possibly empty) top-level folder
array_unshift($media_paths, '');
}
return array_combine($media_paths, $media_paths);
}
/**
* Search a folder (and optional subfolders) for filenames that match a search pattern.
*
* @param string $dir
* @param bool $recursive
* @param string $filter
*
* @return string[]
*/
function scan_dirs($dir, $recursive, $filter) {
$files = array();
// $dir comes from the database. The actual folder may not exist.
if (is_dir($dir)) {
foreach (scandir($dir) as $path) {
if (is_dir($dir . $path)) {
// What if there are user-defined subfolders “thumbs” or “watermarks”?
if ($path != '.' && $path != '..' && $path != 'thumbs' && $path != 'watermark' && $recursive) {
foreach (scan_dirs($dir . $path . '/', $recursive, $filter) as $subpath) {
$files[] = $path . '/' . $subpath;
}
}
} elseif (!$filter || stripos($path, $filter) !== false) {
$files[] = $path;
}
}
}
return $files;
}
/**
* Fetch a list of all files on disk
*
* @param string $media_folder Location of root folder
* @param string $media_path Any subfolder
* @param string $subfolders Include or exclude subfolders
* @param string $filter Filter files whose name contains this test
*
* @return string[]
*/
function all_disk_files($media_folder, $media_path, $subfolders, $filter) {
return scan_dirs(WT_DATA_DIR . $media_folder . $media_path, $subfolders == 'include', $filter);
}
/**
* Fetch a list of all files on in the database.
*
* The subfolders parameter is not implemented. However, as we
* currently use this function as an exclusion list, it is harmless
* to always include sub-folders.
*
* @param string $media_folder
* @param string $media_path
* @param string $subfolders
* @param string $filter
*
* @return string[]
*/
function all_media_files($media_folder, $media_path, $subfolders, $filter) {
return Database::prepare(
"SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM m_filename) AS media_path, 'OBJE' AS type, m_titl, m_id AS xref, m_file AS ged_id, m_gedcom AS gedrec, m_filename" .
" FROM `##media`" .
" JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
" JOIN `##gedcom` USING (gedcom_id)" .
" WHERE setting_value = :media_folder" .
" AND m_filename LIKE CONCAT(:media_path_2, '%')" .
" AND (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', :filter_1, '%')" .
" OR m_titl LIKE CONCAT('%', :filter_2, '%'))" .
" AND m_filename NOT LIKE 'http://%'" .
" AND m_filename NOT LIKE 'https://%'"
)->execute(array(
'media_path_1' => $media_path,
'media_folder' => $media_folder,
'media_path_2' => Filter::escapeLike($media_path),
'filter_1' => Filter::escapeLike($filter),
'filter_2' => Filter::escapeLike($filter),
))->fetchOneColumn();
}
/**
* Generate some useful information and links about a media file.
*
* @param string $media_folder
* @param string $media_path
* @param string $file
*
* @return string
*/
function mediaFileInfo($media_folder, $media_path, $file) {
$html = '
';
$html .= '
' . I18N::translate('Filename') . '
';
$html .= '
' . Filter::escapeHtml($file) . '
';
$full_path = WT_DATA_DIR . $media_folder . $media_path . $file;
if ($file && file_exists($full_path)) {
try {
$size = filesize($full_path);
$size = (int) (($size + 1023) / 1024); // Round up to next KB
$size = /* I18N: size of file in KB */ I18N::translate('%s KB', I18N::number($size));
$html .= '
' . I18N::translate('This media file exists, but cannot be accessed.') . '
';
}
} else {
$html .= '';
$html .= '
' . I18N::translate('This media file does not exist.') . '
';
}
return $html;
}
/**
* Generate some useful information and links about a media object.
*
* @param Media $media
*
* @return string HTML
*/
function mediaObjectInfo(Media $media) {
$xref = $media->getXref();
$gedcom = $media->getTree()->getName();
$html =
'