pageHeader(); exit; } //////////////////////////////////////////////////////////////////////////////// // GET callback for server-side pagination //////////////////////////////////////////////////////////////////////////////// switch($action) { case 'load_json': Zend_Session::writeClose(); $search = WT_Filter::get('search'); $search = $search['value']; $start = WT_Filter::getInteger('start'); $length = WT_Filter::getInteger('length'); switch ($files) { case 'local': // Filtered rows $SELECT1 = "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING ? 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=?" . " AND m_filename LIKE CONCAT(?, '%')" . " AND (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', ?, '%')" . " OR m_titl LIKE CONCAT('%', ?, '%'))" . " AND m_filename NOT LIKE 'http://%'" . " AND m_filename NOT LIKE 'https://%'"; $ARGS1 = array( $media_path, $media_folder, WT_Filter::escapeLike($media_path), WT_Filter::escapeLike($search), WT_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=?" . " AND m_filename LIKE CONCAT(?, '%')" . " AND m_filename NOT LIKE 'http://%'" . " AND m_filename NOT LIKE 'https://%'"; $ARGS2 = array( $media_folder, $media_path ); if ($subfolders=='exclude') { $SELECT1 .= " AND m_filename NOT LIKE CONCAT(?, '%/%')"; $ARGS1[] = WT_Filter::escapeLike($media_path); $SELECT2 .= " AND m_filename NOT LIKE CONCAT(?, '%/%')"; $ARGS2[] = WT_Filter::escapeLike($media_path); } if ($length > 0) { $LIMIT = " LIMIT " . $start . ',' . $length; } else { $LIMIT = ""; } $order = WT_Filter::get('order'); if ($order) { $ORDER_BY = " ORDER BY "; for ($i = 0; $i < count($order); ++$i) { if ($i > 0) { $ORDER_BY .= ','; } // Datatables numbers columns 0, 1, 2, ... // MySQL numbers columns 1, 2, 3, ... switch ($order[$i]['dir']) { case 'asc': $ORDER_BY .= (1 + $order[$i]['column']) . ' ASC '; break; case 'desc': $ORDER_BY .= (1 + $order[$i]['column']) . ' DESC '; break; } } } else { $ORDER_BY = " ORDER BY 1 ASC"; } $rows = WT_DB::prepare($SELECT1.$ORDER_BY.$LIMIT)->execute($ARGS1)->fetchAll(); // Total filtered/unfiltered rows $recordsFiltered = WT_DB::prepare("SELECT FOUND_ROWS()")->fetchOne(); $recordsTotal = WT_DB::prepare($SELECT2)->execute($ARGS2)->fetchOne(); $data = array(); foreach ($rows as $row) { $media = WT_Media::getInstance($row->xref, $row->gedcom_id); $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('%', ?, '%') OR m_titl LIKE CONCAT('%', ?, '%'))"; $ARGS1 = array( WT_Filter::escapeLike($search), WT_Filter::escapeLike($search) ); // Unfiltered rows $SELECT2 = "SELECT SQL_CACHE COUNT(*)" . " FROM `##media`" . " WHERE (m_filename LIKE 'http://%' OR m_filename LIKE 'https://%')"; $ARGS2 = array(); if ($length>0) { $LIMIT = " LIMIT " . $start . ',' . $length; } else { $LIMIT = ""; } $order = WT_Filter::get('order'); if ($order) { $ORDER_BY = " ORDER BY "; for ($i=0; $i < count($order); ++$i) { if ($i > 0) { $ORDER_BY .= ','; } // Datatables numbers columns 0, 1, 2, ... // MySQL numbers columns 1, 2, 3, ... switch ($order[$i]['dir']) { case 'asc': $ORDER_BY .= (1 + $order[$i]['column']).' ASC '; break; case 'desc': $ORDER_BY .= (1 + $order[$i]['column']).' DESC '; break; } } } else { $ORDER_BY = " ORDER BY 1 ASC"; } $rows = WT_DB::prepare($SELECT1.$ORDER_BY.$LIMIT)->execute($ARGS1)->fetchAll(); // Total filtered/unfiltered rows $recordsFiltered = WT_DB::prepare("SELECT FOUND_ROWS()")->fetchOne(); $recordsTotal = WT_DB::prepare($SELECT2)->execute($ARGS2)->fetchOne(); $data = array(); foreach ($rows as $row) { $media = WT_Media::getInstance($row->xref, $row->gedcom_id, $row->gedcom); $data[] = array( WT_Gedcom_Tag::getLabelValue('URL', $row->m_filename), $media->displayImage(), mediaObjectInfo($media), ); } break; case 'unused': // Which trees use this media folder? $media_trees = WT_DB::prepare( "SELECT gedcom_name, gedcom_name" . " FROM `##gedcom`" . " JOIN `##gedcom_setting` USING (gedcom_id)" . " WHERE setting_name='MEDIA_DIRECTORY' AND setting_value=?" )->execute(array($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 = WT_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; } $imgsize=@getimagesize($thumb_path); if ($imgsize && $imgsize[0] && $imgsize[1]) { // 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. $img = ''; } else { $img = '-'; } // Is there a pending record for this file? $exists_pending = WT_DB::prepare( "SELECT 1 FROM `##change` WHERE status='pending' AND new_gedcom LIKE CONCAT('%\n1 FILE ', ?, '\n%')" )->execute(array(WT_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 .= '

' . WT_I18N::translate('Create') . ' — ' . WT_Filter::escapeHtml($media_tree) . '

'; } } $conf = WT_I18N::translate('Are you sure you want to delete “%s”?', $unused_file); $delete_link = '

' . WT_I18N::Translate('Delete') . '

'; $data[] = array( mediaFileInfo($media_folder, $media_path, $unused_file) . $delete_link, $img, $create_form, ); } break; } header('Content-type: application/json'); echo json_encode(array( // See http://www.datatables.net/usage/server-side 'draw' => WT_Filter::getInteger('draw'), // String, but always an integer 'recordsTotal' => $recordsTotal, 'recordsFiltered' => $recordsFiltered, 'data' => $data )); exit; } /** * A unique list of media folders, from all trees. * * @return string[] */ function all_media_folders() { return WT_DB::prepare( "SELECT SQL_CACHE setting_value, setting_value" . " FROM `##gedcom_setting`" . " WHERE setting_name='MEDIA_DIRECTORY'" . " GROUP BY 1" . " ORDER BY 1" )->execute(array(WT_GED_ID))->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 = WT_DB::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=?" . " AND m_filename NOT LIKE 'http://%'" . " AND m_filename NOT LIKE 'https://%'" . " GROUP BY 1" . " ORDER BY 1" )->execute(array($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 boolean $recursive * @param string $filter * * @return array */ 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)) { // TODO - but 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 array */ 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. * * @todo 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 WT_DB::prepare( "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING ? 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=?" . " AND m_filename LIKE CONCAT(?, '%')" . " AND (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', ?, '%')" . " OR m_titl LIKE CONCAT('%', ?, '%'))" . " AND m_filename NOT LIKE 'http://%'" . " AND m_filename NOT LIKE 'https://%'" )->execute(array( $media_path, $media_folder, WT_Filter::escapeLike($media_path), WT_Filter::escapeLike($filter), WT_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 = '' . WT_Filter::escapeHtml($file). ''; $full_path = WT_DATA_DIR . $media_folder . $media_path . $file; if ($file && file_exists($full_path)) { $size = @filesize($full_path); if ($size!==false) { $size = (int)(($size+1023)/1024); // Round up to next KB $size = /* I18N: size of file in KB */ WT_I18N::translate('%s KB', WT_I18N::number($size)); $html .= WT_Gedcom_Tag::getLabelValue('__FILE_SIZE__', $size); $imgsize = @getimagesize($full_path); if (is_array($imgsize)) { $imgsize = /* I18N: image dimensions, width × height */ WT_I18N::translate('%1$s × %2$s pixels', WT_I18N::number($imgsize['0']), WT_I18N::number($imgsize['1'])); $html .= WT_Gedcom_Tag::getLabelValue('__IMAGE_SIZE__', $imgsize); } } else { $html .= '
' . WT_I18N::translate('This media file exists, but cannot be accessed.') . '
' ; } } else { $html .= '
' . WT_I18N::translate('This media file does not exist.') . '
' ; } return $html; } /** * Generate some useful information and links about a media object. * * @param WT_Media $media * * @return string HTML */ function mediaObjectInfo(WT_Media $media) { $xref = $media->getXref(); $gedcom = WT_Tree::getNameFromId($media->getGedcomId()); $name = $media->getFullName(); $html = '' . $name . '' . '
' . WT_Filter::escapeHtml($media->getNote()) . '
' . '
' . '' . WT_I18N::translate('View') . ''; $html .= ' - ' . '' . WT_I18N::Translate('Edit') . '' . ' - ' . '' . WT_I18N::Translate('Delete') . '' . ' - '; if (array_key_exists('GEDFact_assistant', WT_Module::getActiveModules())) { $html .= '' . WT_I18N::Translate('Manage links') . ''; } else { global $TEXT_DIRECTION; $classSuffix = $TEXT_DIRECTION=='rtl' ? '_rtl' : ''; $menu = new WT_Menu(); $menu->addLabel(WT_I18N::translate('Set link')); $menu->addClass('', 'submenu'); $submenu = new WT_Menu(WT_I18N::translate('To individual')); $submenu->addClass("submenuitem".$classSuffix); $submenu->addOnClick("return ilinkitem('$xref', 'person', '$gedcom')"); $menu->addSubMenu($submenu); $submenu = new WT_Menu(WT_I18N::translate('To family')); $submenu->addClass("submenuitem".$classSuffix); $submenu->addOnClick("return ilinkitem('$xref', 'family', '$gedcom')"); $menu->addSubMenu($submenu); $submenu = new WT_Menu(WT_I18N::translate('To source')); $submenu->addClass("submenuitem".$classSuffix); $submenu->addOnClick("return ilinkitem('$xref', 'source', '$gedcom')"); $menu->addSubMenu($submenu); $html .= '
' . $menu->getMenu() . '
'; } $html .= '

'; $linked = array(); foreach ($media->linkedIndividuals('OBJE') as $link) { $linked[] = '' . $link->getFullName() . ''; } foreach ($media->linkedFamilies('OBJE') as $link) { $linked[] = '' . $link->getFullName() . ''; } foreach ($media->linkedSources('OBJE') as $link) { $linked[] = '' . $link->getFullName() . ''; } foreach ($media->linkedNotes('OBJE') as $link) { // Invalid GEDCOM - you cannot link a NOTE to an OBJE $linked[] = '' . $link->getFullName() . ''; } foreach ($media->linkedRepositories('OBJE') as $link) { // Invalid GEDCOM - you cannot link a REPO to an OBJE $linked[] = '' . $link->getFullName() . ''; } if ($linked) { $html .= ''; } else { $html .= '
' . WT_I18N::translate('This media object is not linked to any other record.') . '
'; } return $html; } //////////////////////////////////////////////////////////////////////////////// // Start here //////////////////////////////////////////////////////////////////////////////// // Preserver the pagination/filtering/sorting between requests, so that the // browser’s back button works. Pagination is dependent on the currently // selected folder. $table_id=md5($files.$media_folder.$media_path.$subfolders); $controller = new WT_Controller_Page(); $controller ->restrictAccess(Auth::isAdmin()) ->setPageTitle(WT_I18N::translate('Media')) ->addExternalJavascript(WT_JQUERY_DATATABLES_URL) ->pageHeader() ->addInlineJavascript(' jQuery("#media-table-' . $table_id . '").dataTable({ dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\', processing: true, serverSide: true, ajax: "'.WT_SERVER_NAME.WT_SCRIPT_PATH.WT_SCRIPT_NAME.'?action=load_json&files='.$files.'&media_folder='.$media_folder.'&media_path='.$media_path.'&subfolders='.$subfolders.'", ' . WT_I18N::datatablesI18N(array(5, 10, 20, 50, 100, 500, 1000, -1)) . ', jQueryUI: true, autoWidth:false, pageLength: 10, pagingType: "full_numbers", stateSave: true, stateDuration: 300, columns: [ {}, { sortable: false }, { sortable: ' . ($files=='unused' ? 'false' : 'true') . ' } ] }); '); ?>
onchange="this.form.submit();">
onchange="this.form.submit();">
onchange="this.form.submit();">
', // The full path will be LTR or mixed LTR/RTL. Force LTR. WT_DATA_DIR; // Don’t show a list of media folders if it just contains one folder if (count($media_folders)>1) { echo ' ', select_edit_control('media_folder', $media_folders, null, $media_folder, $extra); } else { echo $media_folder, ''; } // Don’t show a list of subfolders if it just contains one subfolder if (count($media_paths)>1) { echo ' ', select_edit_control('media_path', $media_paths, null, $media_path, $extra); } else { echo $media_path, ''; } echo '', '
', '', WT_I18N::translate('Include subfolders'), '
', '', WT_I18N::translate('Exclude subfolders'), '
'; break; case 'external': echo WT_I18N::translate('External media files have a URL instead of a filename.'); echo ''; echo ''; break; } ?>