| Total Complexity | 76 |
| Total Lines | 666 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like AdminMediaController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AdminMediaController, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 42 | class AdminMediaController extends AbstractBaseController { |
||
| 43 | // How many files to upload on one form. |
||
| 44 | const MAX_UPLOAD_FILES = 10; |
||
| 45 | |||
| 46 | protected $layout = 'layouts/administration'; |
||
| 47 | |||
| 48 | /** |
||
| 49 | * @param Request $request |
||
| 50 | * |
||
| 51 | * @return Response |
||
| 52 | */ |
||
| 53 | public function index(Request $request): Response { |
||
| 78 | ]); |
||
| 79 | } |
||
| 80 | |||
| 81 | /** |
||
| 82 | * @param Request $request |
||
| 83 | * |
||
| 84 | * @return Response |
||
| 85 | */ |
||
| 86 | public function delete(Request $request): Response { |
||
| 87 | $delete_file = $request->get('file', ''); |
||
| 88 | $media_folder = $request->get('folder', ''); |
||
| 89 | |||
| 90 | // Only delete valid (i.e. unused) media files |
||
| 91 | $disk_files = $this->allDiskFiles($media_folder, '', 'include', ''); |
||
| 92 | |||
| 93 | // Check file exists? Maybe it was already deleted or renamed. |
||
| 94 | if (in_array($delete_file, $disk_files)) { |
||
| 95 | $tmp = WT_DATA_DIR . $media_folder . $delete_file; |
||
| 96 | try { |
||
| 97 | unlink($tmp); |
||
| 98 | FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', Html::filename($tmp)), 'info'); |
||
| 99 | } catch (Throwable $ex) { |
||
| 100 | DebugBar::addThrowable($ex); |
||
| 101 | |||
| 102 | FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', Html::filename($tmp)) . '<hr><samp dir="ltr">' . $ex->getMessage() . '</samp>', 'danger'); |
||
| 103 | } |
||
| 104 | } |
||
| 105 | |||
| 106 | return new Response; |
||
| 107 | } |
||
| 108 | |||
| 109 | /** |
||
| 110 | * @param Request $request |
||
| 111 | * |
||
| 112 | * @return JsonResponse |
||
| 113 | */ |
||
| 114 | public function data(Request $request): JsonResponse { |
||
| 115 | $files = $request->get('files'); // local|external|unused |
||
| 116 | $search = $request->get('search'); |
||
| 117 | $search = $search['value']; |
||
| 118 | $start = (int) $request->get('start'); |
||
| 119 | $length = (int) $request->get('length'); |
||
| 120 | |||
| 121 | // family tree setting MEDIA_DIRECTORY |
||
| 122 | $media_folders = $this->allMediaFolders(); |
||
| 123 | $media_folder = $request->get('media_folder', ''); |
||
| 124 | // User folders may contain special characters. Restrict to actual folders. |
||
| 125 | if (!array_key_exists($media_folder, $media_folders)) { |
||
| 126 | $media_folder = reset($media_folders); |
||
| 127 | } |
||
| 128 | |||
| 129 | // prefix to filename |
||
| 130 | $media_paths = $this->mediaPaths($media_folder); |
||
| 131 | $media_path = $request->get('media_path', ''); |
||
| 132 | // User paths may contain special characters. Restrict to actual paths. |
||
| 133 | if (!array_key_exists($media_path, $media_paths)) { |
||
| 134 | $media_path = reset($media_paths); |
||
| 135 | } |
||
| 136 | |||
| 137 | // subfolders within $media_path |
||
| 138 | $subfolders = $request->get('subfolders'); // include|exclude |
||
| 139 | |||
| 140 | switch ($files) { |
||
| 141 | case 'local': |
||
| 142 | // Filtered rows |
||
| 143 | $SELECT1 = |
||
| 144 | "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM multimedia_file_refn) AS media_path, m_id AS xref, descriptive_title, m_file AS gedcom_id, m_gedcom AS gedcom" . |
||
| 145 | " FROM `##media`" . |
||
| 146 | " JOIN `##media_file` USING (m_file, m_id)" . |
||
| 147 | " JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" . |
||
| 148 | " JOIN `##gedcom` USING (gedcom_id)" . |
||
| 149 | " WHERE setting_value = :media_folder" . |
||
| 150 | " AND multimedia_file_refn LIKE CONCAT(:media_path_2, '%')" . |
||
| 151 | " AND (SUBSTRING_INDEX(multimedia_file_refn, '/', -1) LIKE CONCAT('%', :search_1, '%')" . |
||
| 152 | " OR descriptive_title LIKE CONCAT('%', :search_2, '%'))" . |
||
| 153 | " AND multimedia_file_refn NOT LIKE 'http://%'" . |
||
| 154 | " AND multimedia_file_refn NOT LIKE 'https://%'"; |
||
| 155 | $ARGS1 = [ |
||
| 156 | 'media_path_1' => $media_path, |
||
| 157 | 'media_folder' => $media_folder, |
||
| 158 | 'media_path_2' => Database::escapeLike($media_path), |
||
| 159 | 'search_1' => Database::escapeLike($search), |
||
| 160 | 'search_2' => Database::escapeLike($search), |
||
| 161 | ]; |
||
| 162 | // Unfiltered rows |
||
| 163 | $SELECT2 = |
||
| 164 | "SELECT SQL_CACHE COUNT(*)" . |
||
| 165 | " FROM `##media`" . |
||
| 166 | " JOIN `##media_file` USING (m_file, m_id)" . |
||
| 167 | " JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" . |
||
| 168 | " WHERE setting_value = :media_folder" . |
||
| 169 | " AND multimedia_file_refn LIKE CONCAT(:media_path_3, '%')" . |
||
| 170 | " AND multimedia_file_refn NOT LIKE 'http://%'" . |
||
| 171 | " AND multimedia_file_refn NOT LIKE 'https://%'"; |
||
| 172 | $ARGS2 = [ |
||
| 173 | 'media_folder' => $media_folder, |
||
| 174 | 'media_path_3' => $media_path, |
||
| 175 | ]; |
||
| 176 | |||
| 177 | if ($subfolders == 'exclude') { |
||
| 178 | $SELECT1 .= " AND multimedia_file_refn NOT LIKE CONCAT(:media_path_4, '%/%')"; |
||
| 179 | $ARGS1['media_path_4'] = Database::escapeLike($media_path); |
||
| 180 | $SELECT2 .= " AND multimedia_file_refn NOT LIKE CONCAT(:media_path_4, '%/%')"; |
||
| 181 | $ARGS2['media_path_4'] = Database::escapeLike($media_path); |
||
| 182 | } |
||
| 183 | |||
| 184 | $order = $request->get('order', []); |
||
| 185 | $SELECT1 .= " ORDER BY "; |
||
| 186 | if ($order) { |
||
| 187 | foreach ($order as $key => $value) { |
||
| 188 | if ($key > 0) { |
||
| 189 | $SELECT1 .= ','; |
||
| 190 | } |
||
| 191 | // Columns in datatables are numbered from zero. |
||
| 192 | // Columns in MySQL are numbered starting with one. |
||
| 193 | switch ($value['dir']) { |
||
| 194 | case 'asc': |
||
| 195 | $SELECT1 .= ":col_" . $key . " ASC"; |
||
| 196 | break; |
||
| 197 | case 'desc': |
||
| 198 | $SELECT1 .= ":col_" . $key . " DESC"; |
||
| 199 | break; |
||
| 200 | } |
||
| 201 | $ARGS1['col_' . $key] = 1 + $value['column']; |
||
| 202 | } |
||
| 203 | } else { |
||
| 204 | $SELECT1 = " 1 ASC"; |
||
| 205 | } |
||
| 206 | |||
| 207 | if ($length > 0) { |
||
| 208 | $SELECT1 .= " LIMIT :length OFFSET :start"; |
||
| 209 | $ARGS1['length'] = $length; |
||
| 210 | $ARGS1['start'] = $start; |
||
| 211 | } |
||
| 212 | |||
| 213 | $rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll(); |
||
| 214 | // Total filtered/unfiltered rows |
||
| 215 | $recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne(); |
||
| 216 | $recordsTotal = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne(); |
||
| 217 | |||
| 218 | $data = []; |
||
| 219 | foreach ($rows as $row) { |
||
| 220 | $media = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom); |
||
| 221 | $media_files = $media->mediaFiles(); |
||
| 222 | $media_files = array_map(function (MediaFile $media_file) { |
||
| 223 | return $media_file->displayImage(150, 150, '', []); |
||
| 224 | }, $media_files); |
||
| 225 | $data[] = [ |
||
| 226 | $this->mediaFileInfo($media_folder, $media_path, $row->media_path), |
||
| 227 | implode('', $media_files), |
||
| 228 | $this->mediaObjectInfo($media), |
||
| 229 | ]; |
||
| 230 | } |
||
| 231 | break; |
||
| 232 | |||
| 233 | case 'external': |
||
| 234 | // Filtered rows |
||
| 235 | $SELECT1 = |
||
| 236 | "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS multimedia_file_refn, m_id AS xref, descriptive_title, m_file AS gedcom_id, m_gedcom AS gedcom" . |
||
| 237 | " FROM `##media`" . |
||
| 238 | " JOIN `##media_file` USING (m_id, m_file)" . |
||
| 239 | " WHERE (multimedia_file_refn LIKE 'http://%' OR multimedia_file_refn LIKE 'https://%')" . |
||
| 240 | " AND (multimedia_file_refn LIKE CONCAT('%', :search_1, '%') OR descriptive_title LIKE CONCAT('%', :search_2, '%'))"; |
||
| 241 | $ARGS1 = [ |
||
| 242 | 'search_1' => Database::escapeLike($search), |
||
| 243 | 'search_2' => Database::escapeLike($search), |
||
| 244 | ]; |
||
| 245 | // Unfiltered rows |
||
| 246 | $SELECT2 = |
||
| 247 | "SELECT SQL_CACHE COUNT(*)" . |
||
| 248 | " FROM `##media`" . |
||
| 249 | " JOIN `##media_file` USING (m_id, m_file)" . |
||
| 250 | " WHERE (multimedia_file_refn LIKE 'http://%' OR multimedia_file_refn LIKE 'https://%')"; |
||
| 251 | $ARGS2 = []; |
||
| 252 | |||
| 253 | $order = $request->get('order', []); |
||
| 254 | $SELECT1 .= " ORDER BY "; |
||
| 255 | if ($order) { |
||
| 256 | foreach ($order as $key => $value) { |
||
| 257 | if ($key > 0) { |
||
| 258 | $SELECT1 .= ','; |
||
| 259 | } |
||
| 260 | // Columns in datatables are numbered from zero. |
||
| 261 | // Columns in MySQL are numbered starting with one. |
||
| 262 | switch ($value['dir']) { |
||
| 263 | case 'asc': |
||
| 264 | $SELECT1 .= ":col_" . $key . " ASC"; |
||
| 265 | break; |
||
| 266 | case 'desc': |
||
| 267 | $SELECT1 .= ":col_" . $key . " DESC"; |
||
| 268 | break; |
||
| 269 | } |
||
| 270 | $ARGS1['col_' . $key] = 1 + $value['column']; |
||
| 271 | } |
||
| 272 | } else { |
||
| 273 | $SELECT1 = " 1 ASC"; |
||
| 274 | } |
||
| 275 | |||
| 276 | if ($length > 0) { |
||
| 277 | $SELECT1 .= " LIMIT :length OFFSET :start"; |
||
| 278 | $ARGS1['length'] = $length; |
||
| 279 | $ARGS1['start'] = $start; |
||
| 280 | } |
||
| 281 | |||
| 282 | $rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll(); |
||
| 283 | |||
| 284 | // Total filtered/unfiltered rows |
||
| 285 | $recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne(); |
||
| 286 | $recordsTotal = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne(); |
||
| 287 | |||
| 288 | $data = []; |
||
| 289 | foreach ($rows as $row) { |
||
| 290 | $media = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom); |
||
| 291 | $data[] = [ |
||
| 292 | GedcomTag::getLabelValue('URL', $row->multimedia_file_refn), |
||
| 293 | $media->displayImage(150, 150, '', []), |
||
| 294 | $this->mediaObjectInfo($media), |
||
| 295 | ]; |
||
| 296 | } |
||
| 297 | break; |
||
| 298 | |||
| 299 | case 'unused': |
||
| 300 | // Which trees use this media folder? |
||
| 301 | $media_trees = Database::prepare( |
||
| 302 | "SELECT gedcom_name, gedcom_name" . |
||
| 303 | " FROM `##gedcom`" . |
||
| 304 | " JOIN `##gedcom_setting` USING (gedcom_id)" . |
||
| 305 | " WHERE setting_name='MEDIA_DIRECTORY' AND setting_value = :media_folder AND gedcom_id > 0" |
||
| 306 | )->execute([ |
||
| 307 | 'media_folder' => $media_folder, |
||
| 308 | ])->fetchAssoc(); |
||
| 309 | |||
| 310 | $disk_files = $this->allDiskFiles($media_folder, $media_path, $subfolders, $search); |
||
| 311 | $db_files = $this->allMediaFiles($media_folder, $media_path, $search); |
||
| 312 | |||
| 313 | // All unused files |
||
| 314 | $unused_files = array_diff($disk_files, $db_files); |
||
| 315 | $recordsTotal = count($unused_files); |
||
| 316 | |||
| 317 | // Filter unused files |
||
| 318 | if ($search) { |
||
| 319 | $unused_files = array_filter($unused_files, function ($x) use ($search) { |
||
| 320 | return strpos($x, $search) !== false; |
||
| 321 | }); |
||
| 322 | } |
||
| 323 | $recordsFiltered = count($unused_files); |
||
| 324 | |||
| 325 | // Sort files - only option is column 0 |
||
| 326 | sort($unused_files); |
||
| 327 | $order = $request->get('order', []); |
||
| 328 | if ($order && $order[0]['dir'] === 'desc') { |
||
| 329 | $unused_files = array_reverse($unused_files); |
||
| 330 | } |
||
| 331 | |||
| 332 | // Paginate unused files |
||
| 333 | $unused_files = array_slice($unused_files, $start, $length); |
||
| 334 | |||
| 335 | $data = []; |
||
| 336 | foreach ($unused_files as $unused_file) { |
||
| 337 | $imgsize = getimagesize(WT_DATA_DIR . $media_folder . $media_path . $unused_file); |
||
| 338 | // We can’t create a URL (not in public_html) or use the media firewall (no such object) |
||
| 339 | if ($imgsize === false) { |
||
| 340 | $img = '-'; |
||
| 341 | } else { |
||
| 342 | $url = route('unused-media-thumbnail', ['folder' => $media_folder, 'file' => $media_path . $unused_file, 'w' => 100, 'h' => 100]); |
||
| 343 | $img = '<img src="' . e($url) . '">'; |
||
| 344 | } |
||
| 345 | |||
| 346 | // Is there a pending record for this file? |
||
| 347 | $exists_pending = Database::prepare( |
||
| 348 | "SELECT 1 FROM `##change` WHERE status='pending' AND new_gedcom LIKE CONCAT('%\n1 FILE ', :unused_file, '\n%')" |
||
| 349 | )->execute([ |
||
| 350 | 'unused_file' => Database::escapeLike($unused_file), |
||
| 351 | ])->fetchOne(); |
||
| 352 | |||
| 353 | // Form to create new media object in each tree |
||
| 354 | $create_form = ''; |
||
| 355 | if (!$exists_pending) { |
||
| 356 | foreach ($media_trees as $media_tree) { |
||
| 357 | $create_form .= |
||
| 358 | '<p><a href="#" data-toggle="modal" data-target="#modal-create-media-from-file" data-file="' . e($unused_file) . '" data-tree="' . e($media_tree) . '" onclick="document.getElementById(\'file\').value=this.dataset.file; document.getElementById(\'ged\').value=this.dataset.tree;">' . I18N::translate('Create') . '</a> — ' . e($media_tree) . '<p>'; |
||
| 359 | } |
||
| 360 | } |
||
| 361 | |||
| 362 | $delete_link = '<p><a data-confirm="' . I18N::translate('Are you sure you want to delete “%s”?', e($unused_file)) . '" data-url="' . e(route('admin-media-delete', ['file' => $media_path . $unused_file, 'folder' => $media_folder])) . '" onclick="if (confirm(this.dataset.confirm)) jQuery.post(this.dataset.url, function (){location.reload();})" href="#">' . I18N::translate('Delete') . '</a></p>'; |
||
| 363 | |||
| 364 | $data[] = [ |
||
| 365 | $this->mediaFileInfo($media_folder, $media_path, $unused_file) . $delete_link, |
||
| 366 | $img, |
||
| 367 | $create_form, |
||
| 368 | ]; |
||
| 369 | } |
||
| 370 | break; |
||
| 371 | |||
| 372 | default: |
||
| 373 | throw new BadRequestHttpException; |
||
| 374 | } |
||
| 375 | |||
| 376 | // See http://www.datatables.net/usage/server-side |
||
| 377 | return new JsonResponse([ |
||
| 378 | 'draw' => $request->get('draw'), |
||
| 379 | 'recordsTotal' => $recordsTotal, |
||
| 380 | 'recordsFiltered' => $recordsFiltered, |
||
| 381 | 'data' => $data, |
||
| 382 | ]); |
||
| 383 | } |
||
| 384 | |||
| 385 | /** |
||
| 386 | * @param Request $request |
||
| 387 | * |
||
| 388 | * @return Response |
||
| 389 | */ |
||
| 390 | public function upload(Request $request): Response { |
||
| 391 | $media_folders = $this->folderListAll(); |
||
| 392 | |||
| 393 | $filesize = ini_get('upload_max_filesize'); |
||
| 394 | if (empty($filesize)) { |
||
| 395 | $filesize = '2M'; |
||
| 396 | } |
||
| 397 | |||
| 398 | $title = I18N::translate('Upload media files'); |
||
| 399 | |||
| 400 | return $this->viewResponse('admin/media-upload', [ |
||
| 401 | 'max_upload_files' => self::MAX_UPLOAD_FILES, |
||
| 402 | 'filesize' => $filesize, |
||
| 403 | 'media_folders' => $media_folders, |
||
| 404 | 'title' => $title, |
||
| 405 | ]); |
||
| 406 | } |
||
| 407 | |||
| 408 | /** |
||
| 409 | * @param Request $request |
||
| 410 | * |
||
| 411 | * @return RedirectResponse |
||
| 412 | */ |
||
| 413 | public function uploadAction(Request $request): RedirectResponse { |
||
| 414 | $all_folders = $this->folderListAll(); |
||
| 415 | |||
| 416 | for ($i = 1; $i < self::MAX_UPLOAD_FILES; $i++) { |
||
| 417 | if (!empty($_FILES['mediafile' . $i]['name'])) { |
||
| 418 | $folder = $request->get('folder' . $i, ''); |
||
| 419 | $filename = $request->get('filename' . $i, ''); |
||
| 420 | |||
| 421 | // If no filename specified, use the original filename. |
||
| 422 | if ($filename === '') { |
||
| 423 | $filename = $_FILES['mediafile' . $i]['name']; |
||
| 424 | } |
||
| 425 | |||
| 426 | // Validate the folder |
||
| 427 | if (!in_array($folder, $all_folders)) { |
||
| 428 | break; |
||
| 429 | } |
||
| 430 | |||
| 431 | // Validate the filename. |
||
| 432 | $filename = str_replace('\\', '/', $filename); |
||
| 433 | $filename = trim($filename, '/'); |
||
| 434 | |||
| 435 | if (strpos('/' . $filename, '/../') !== false) { |
||
| 436 | FlashMessages::addMessage('Folder names are not allowed to include “../”'); |
||
| 437 | continue; |
||
| 438 | } elseif (preg_match('/([:])/', $filename, $match)) { |
||
| 439 | // Local media files cannot contain certain special characters, especially on MS Windows |
||
| 440 | FlashMessages::addMessage(I18N::translate('Filenames are not allowed to contain the character “%s”.', $match[1])); |
||
| 441 | continue; |
||
| 442 | } elseif (preg_match('/(\.(php|pl|cgi|bash|sh|bat|exe|com|htm|html|shtml))$/i', $filename, $match)) { |
||
| 443 | // Do not allow obvious script files. |
||
| 444 | FlashMessages::addMessage(I18N::translate('Filenames are not allowed to have the extension “%s”.', $match[1])); |
||
| 445 | continue; |
||
| 446 | } |
||
| 447 | |||
| 448 | // The new filename may have created a new sub-folder. |
||
| 449 | $full_path = WT_DATA_DIR . $folder . $filename; |
||
| 450 | $folder = dirname($full_path); |
||
| 451 | |||
| 452 | // Make sure the media folder exists |
||
| 453 | if (!is_dir($folder)) { |
||
| 454 | if (File::mkdir($folder)) { |
||
| 455 | FlashMessages::addMessage(I18N::translate('The folder %s has been created.', Html::filename($folder)), 'info'); |
||
| 456 | } else { |
||
| 457 | FlashMessages::addMessage(I18N::translate('The folder %s does not exist, and it could not be created.', Html::filename($folder)), 'danger'); |
||
| 458 | continue; |
||
| 459 | } |
||
| 460 | } |
||
| 461 | |||
| 462 | if (file_exists($full_path)) { |
||
| 463 | FlashMessages::addMessage(I18N::translate('The file %s already exists. Use another filename.', $full_path, 'error')); |
||
| 464 | continue; |
||
| 465 | } |
||
| 466 | |||
| 467 | // Now copy the file to the correct location. |
||
| 468 | if (move_uploaded_file($_FILES['mediafile' . $i]['tmp_name'], $full_path)) { |
||
| 469 | FlashMessages::addMessage(I18N::translate('The file %s has been uploaded.', Html::filename($full_path)), 'success'); |
||
| 470 | Log::addMediaLog('Media file ' . $full_path . ' uploaded'); |
||
| 471 | } else { |
||
| 472 | FlashMessages::addMessage(I18N::translate('There was an error uploading your file.') . '<br>' . Functions::fileUploadErrorText($_FILES['mediafile' . $i]['error']), 'danger'); |
||
| 473 | } |
||
| 474 | } |
||
| 475 | } |
||
| 476 | |||
| 477 | $url = route('admin-media-upload'); |
||
| 478 | |||
| 479 | return new RedirectResponse($url); |
||
| 480 | } |
||
| 481 | |||
| 482 | /** |
||
| 483 | * Generate a list of all folders from all the trees. |
||
| 484 | * |
||
| 485 | * @return string[] |
||
| 486 | */ |
||
| 487 | private function folderListAll(): array { |
||
| 488 | $folders = Database::prepare( |
||
| 489 | "SELECT SQL_CACHE CONCAT(setting_value, LEFT(multimedia_file_refn, CHAR_LENGTH(multimedia_file_refn) - CHAR_LENGTH(SUBSTRING_INDEX(multimedia_file_refn, '/', -1))))" . |
||
| 490 | " FROM `##gedcom_setting` AS gs" . |
||
| 491 | " JOIN `##media_file` AS m ON m.m_file = gs.gedcom_id AND gs.setting_name = 'MEDIA_DIRECTORY'" . |
||
| 492 | " WHERE multimedia_file_refn NOT LIKE 'http://%'" . |
||
| 493 | " AND multimedia_file_refn NOT LIKE 'https://%'" . |
||
| 494 | " AND gs.gedcom_id > 0" . |
||
| 495 | " GROUP BY 1" . |
||
| 496 | " UNION" . |
||
| 497 | " SELECT setting_value FROM `##gedcom_setting` WHERE setting_name = 'MEDIA_DIRECTORY'" . |
||
| 498 | " ORDER BY 1" |
||
| 499 | )->execute()->fetchOneColumn(); |
||
| 500 | |||
| 501 | return $folders; |
||
| 502 | } |
||
| 503 | |||
| 504 | /** |
||
| 505 | * A unique list of media folders, from all trees. |
||
| 506 | * |
||
| 507 | * @return string[] |
||
| 508 | */ |
||
| 509 | private function allMediaFolders(): array { |
||
| 510 | return Database::prepare( |
||
| 511 | "SELECT SQL_CACHE setting_value, setting_value" . |
||
| 512 | " FROM `##gedcom_setting`" . |
||
| 513 | " WHERE setting_name='MEDIA_DIRECTORY' AND gedcom_id > 0" . |
||
| 514 | " GROUP BY 1" . |
||
| 515 | " ORDER BY 1" |
||
| 516 | )->execute([])->fetchAssoc(); |
||
| 517 | } |
||
| 518 | |||
| 519 | /** |
||
| 520 | * Generate a list of media paths (within a media folder) used by all media objects. |
||
| 521 | * |
||
| 522 | * @param string $media_folder |
||
| 523 | * |
||
| 524 | * @return string[] |
||
| 525 | */ |
||
| 526 | private function mediaPaths(string $media_folder): array { |
||
| 527 | $media_paths = Database::prepare( |
||
| 528 | "SELECT SQL_CACHE LEFT(multimedia_file_refn, CHAR_LENGTH(multimedia_file_refn) - CHAR_LENGTH(SUBSTRING_INDEX(multimedia_file_refn, '/', -1))) AS media_path" . |
||
| 529 | " FROM `##media`" . |
||
| 530 | " JOIN `##media_file` USING (m_file, m_id)" . |
||
| 531 | " JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" . |
||
| 532 | " WHERE setting_value = :media_folder" . |
||
| 533 | " AND multimedia_file_refn NOT LIKE 'http://%'" . |
||
| 534 | " AND multimedia_file_refn NOT LIKE 'https://%'" . |
||
| 535 | " GROUP BY 1" . |
||
| 536 | " ORDER BY 1" |
||
| 537 | )->execute([ |
||
| 538 | 'media_folder' => $media_folder, |
||
| 539 | ])->fetchOneColumn(); |
||
| 540 | |||
| 541 | if (empty($media_paths) || $media_paths[0] !== '') { |
||
| 542 | // Always include a (possibly empty) top-level folder |
||
| 543 | array_unshift($media_paths, ''); |
||
| 544 | } |
||
| 545 | |||
| 546 | return array_combine($media_paths, $media_paths); |
||
| 547 | } |
||
| 548 | |||
| 549 | /** |
||
| 550 | * Search a folder (and optional subfolders) for filenames that match a search pattern. |
||
| 551 | * |
||
| 552 | * @param string $dir |
||
| 553 | * @param bool $recursive |
||
| 554 | * @param string $filter |
||
| 555 | * |
||
| 556 | * @return string[] |
||
| 557 | */ |
||
| 558 | private function scanFolders(string $dir, bool $recursive, string $filter): array { |
||
| 559 | $files = []; |
||
| 560 | |||
| 561 | // $dir comes from the database. The actual folder may not exist. |
||
| 562 | if (is_dir($dir)) { |
||
| 563 | foreach (scandir($dir) as $path) { |
||
| 564 | if (is_dir($dir . $path)) { |
||
| 565 | // What if there are user-defined subfolders “thumbs” or “watermarks”? |
||
| 566 | if ($path != '.' && $path != '..' && $path != 'thumbs' && $path != 'watermark' && $recursive) { |
||
| 567 | foreach ($this->scanFolders($dir . $path . '/', $recursive, $filter) as $subpath) { |
||
| 568 | $files[] = $path . '/' . $subpath; |
||
| 569 | } |
||
| 570 | } |
||
| 571 | } elseif (!$filter || stripos($path, $filter) !== false) { |
||
| 572 | $files[] = $path; |
||
| 573 | } |
||
| 574 | } |
||
| 575 | } |
||
| 576 | |||
| 577 | return $files; |
||
| 578 | } |
||
| 579 | |||
| 580 | /** |
||
| 581 | * Fetch a list of all files on disk |
||
| 582 | * |
||
| 583 | * @param string $media_folder Location of root folder |
||
| 584 | * @param string $media_path Any subfolder |
||
| 585 | * @param string $subfolders Include or exclude subfolders |
||
| 586 | * @param string $filter Filter files whose name contains this test |
||
| 587 | * |
||
| 588 | * @return string[] |
||
| 589 | */ |
||
| 590 | private function allDiskFiles(string $media_folder, string $media_path, string $subfolders, string $filter): array { |
||
| 591 | return $this->scanFolders(WT_DATA_DIR . $media_folder . $media_path, $subfolders == 'include', $filter); |
||
| 592 | } |
||
| 593 | |||
| 594 | /** |
||
| 595 | * Fetch a list of all files on in the database. |
||
| 596 | * |
||
| 597 | * @param string $media_folder |
||
| 598 | * @param string $media_path |
||
| 599 | * @param string $filter |
||
| 600 | * |
||
| 601 | * @return string[] |
||
| 602 | */ |
||
| 603 | private function allMediaFiles(string $media_folder, string $media_path, string $filter): array { |
||
| 604 | return Database::prepare( |
||
| 605 | "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM multimedia_file_refn) AS media_path, 'OBJE' AS type, descriptive_title, m_id AS xref, m_file AS ged_id, m_gedcom AS gedrec, multimedia_file_refn" . |
||
| 606 | " FROM `##media`" . |
||
| 607 | " JOIN `##media_file` USING (m_file, m_id)" . |
||
| 608 | " JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" . |
||
| 609 | " JOIN `##gedcom` USING (gedcom_id)" . |
||
| 610 | " WHERE setting_value = :media_folder" . |
||
| 611 | " AND multimedia_file_refn LIKE CONCAT(:media_path_2, '%')" . |
||
| 612 | " AND (SUBSTRING_INDEX(multimedia_file_refn, '/', -1) LIKE CONCAT('%', :filter_1, '%')" . |
||
| 613 | " OR descriptive_title LIKE CONCAT('%', :filter_2, '%'))" . |
||
| 614 | " AND multimedia_file_refn NOT LIKE 'http://%'" . |
||
| 615 | " AND multimedia_file_refn NOT LIKE 'https://%'" |
||
| 616 | )->execute([ |
||
| 617 | 'media_path_1' => $media_path, |
||
| 618 | 'media_folder' => $media_folder, |
||
| 619 | 'media_path_2' => Database::escapeLike($media_path), |
||
| 620 | 'filter_1' => Database::escapeLike($filter), |
||
| 621 | 'filter_2' => Database::escapeLike($filter), |
||
| 622 | ])->fetchOneColumn(); |
||
| 623 | } |
||
| 624 | |||
| 625 | /** |
||
| 626 | * Generate some useful information and links about a media file. |
||
| 627 | * |
||
| 628 | * @param string $media_folder |
||
| 629 | * @param string $media_path |
||
| 630 | * @param string $file |
||
| 631 | * |
||
| 632 | * @return string |
||
| 633 | */ |
||
| 634 | private function mediaFileInfo(string $media_folder, string $media_path, string $file): string { |
||
| 635 | $html = '<dl>'; |
||
| 636 | $html .= '<dt>' . I18N::translate('Filename') . '</dt>'; |
||
| 637 | $html .= '<dd>' . e($file) . '</dd>'; |
||
| 638 | |||
| 639 | $full_path = WT_DATA_DIR . $media_folder . $media_path . $file; |
||
| 640 | try { |
||
| 641 | $size = filesize($full_path); |
||
| 642 | $size = (int) (($size + 1023) / 1024); // Round up to next KB |
||
| 643 | $size = /* I18N: size of file in KB */ |
||
| 644 | I18N::translate('%s KB', I18N::number($size)); |
||
| 645 | $html .= '<dt>' . I18N::translate('File size') . '</dt>'; |
||
| 646 | $html .= '<dd>' . $size . '</dd>'; |
||
| 647 | |||
| 648 | try { |
||
| 649 | $imgsize = getimagesize($full_path); |
||
| 650 | $html .= '<dt>' . I18N::translate('Image dimensions') . '</dt>'; |
||
| 651 | $html .= '<dd>' . /* I18N: image dimensions, width × height */ |
||
| 652 | I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1'])) . '</dd>'; |
||
| 653 | } catch (Throwable $ex) { |
||
| 654 | DebugBar::addThrowable($ex); |
||
| 655 | |||
| 656 | // Not an image, or not a valid image? |
||
| 657 | } |
||
| 658 | |||
| 659 | $html .= '</dl>'; |
||
| 660 | } catch (Throwable $ex) { |
||
| 661 | DebugBar::addThrowable($ex); |
||
| 662 | |||
| 663 | // Not a file? Not an image? |
||
| 664 | } |
||
| 665 | |||
| 666 | return $html; |
||
| 667 | } |
||
| 668 | |||
| 669 | /** |
||
| 670 | * Generate some useful information and links about a media object. |
||
| 671 | * |
||
| 672 | * @param Media $media |
||
| 673 | * |
||
| 674 | * @return string HTML |
||
| 675 | */ |
||
| 676 | private function mediaObjectInfo(Media $media) { |
||
| 708 | } |
||
| 709 | } |
||
| 710 |