1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Fisharebest\Webtrees\FlashMessages; 23use Fisharebest\Webtrees\GedcomTag; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Tree; 26use Illuminate\Database\Capsule\Manager as DB; 27use Illuminate\Database\Query\Expression; 28use Illuminate\Support\Collection; 29use InvalidArgumentException; 30use League\Flysystem\FilesystemInterface; 31use Psr\Http\Message\ServerRequestInterface; 32use Psr\Http\Message\UploadedFileInterface; 33use RuntimeException; 34use Symfony\Component\HttpFoundation\File\UploadedFile; 35 36use function array_combine; 37use function array_diff; 38use function array_filter; 39use function array_map; 40use function assert; 41use function dirname; 42use function intdiv; 43use function pathinfo; 44use function preg_match; 45use function sha1; 46use function sort; 47use function str_replace; 48use function strpos; 49use function strtolower; 50use function trim; 51 52use const PATHINFO_EXTENSION; 53use const UPLOAD_ERR_OK; 54 55/** 56 * Managing media files. 57 */ 58class MediaFileService 59{ 60 public const EDIT_RESTRICTIONS = [ 61 'locked', 62 ]; 63 64 public const PRIVACY_RESTRICTIONS = [ 65 'none', 66 'privacy', 67 'confidential', 68 ]; 69 70 /** 71 * What is the largest file a user may upload? 72 */ 73 public function maxUploadFilesize(): string 74 { 75 $bytes = UploadedFile::getMaxFilesize(); 76 $kb = intdiv($bytes + 1023, 1024); 77 78 return I18N::translate('%s KB', I18N::number($kb)); 79 } 80 81 /** 82 * A list of key/value options for media types. 83 * 84 * @param string $current 85 * 86 * @return array 87 */ 88 public function mediaTypes($current = ''): array 89 { 90 $media_types = GedcomTag::getFileFormTypes(); 91 92 $media_types = ['' => ''] + [$current => $current] + $media_types; 93 94 return $media_types; 95 } 96 97 /** 98 * A list of media files not already linked to a media object. 99 * 100 * @param Tree $tree 101 * @param FilesystemInterface $data_filesystem 102 * 103 * @return array 104 */ 105 public function unusedFiles(Tree $tree, FilesystemInterface $data_filesystem): array 106 { 107 $used_files = DB::table('media_file') 108 ->where('m_file', '=', $tree->id()) 109 ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 110 ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 111 ->pluck('multimedia_file_refn') 112 ->all(); 113 114 $disk_files = $tree->mediaFilesystem($data_filesystem)->listContents('', true); 115 116 $disk_files = array_filter($disk_files, static function (array $item) { 117 // Older versions of webtrees used a couple of special folders. 118 return 119 $item['type'] === 'file' && 120 strpos($item['path'], '/thumbs/') === false && 121 strpos($item['path'], '/watermarks/') === false; 122 }); 123 124 $disk_files = array_map(static function (array $item): string { 125 return $item['path']; 126 }, $disk_files); 127 128 $unused_files = array_diff($disk_files, $used_files); 129 130 sort($unused_files); 131 132 return array_combine($unused_files, $unused_files); 133 } 134 135 /** 136 * Store an uploaded file (or URL), either to be added to a media object 137 * or to create a media object. 138 * 139 * @param ServerRequestInterface $request 140 * 141 * @return string The value to be stored in the 'FILE' field of the media object. 142 */ 143 public function uploadFile(ServerRequestInterface $request): string 144 { 145 $tree = $request->getAttribute('tree'); 146 assert($tree instanceof Tree); 147 148 $data_filesystem = $request->getAttribute('filesystem.data'); 149 assert($data_filesystem instanceof FilesystemInterface); 150 151 $params = $request->getParsedBody(); 152 $file_location = $params['file_location']; 153 154 switch ($file_location) { 155 case 'url': 156 $remote = $params['remote']; 157 158 if (strpos($remote, '://') !== false) { 159 return $remote; 160 } 161 162 return ''; 163 164 case 'unused': 165 $unused = $params['unused']; 166 167 if ($tree->mediaFilesystem($data_filesystem)->has($unused)) { 168 return $unused; 169 } 170 171 return ''; 172 173 case 'upload': 174 default: 175 $folder = $params['folder']; 176 $auto = $params['auto']; 177 $new_file = $params['new_file']; 178 179 /** @var UploadedFileInterface|null $uploaded_file */ 180 $uploaded_file = $request->getUploadedFiles()['file']; 181 if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) { 182 return ''; 183 } 184 185 // The filename 186 $new_file = str_replace('\\', '/', $new_file); 187 if ($new_file !== '' && strpos($new_file, '/') === false) { 188 $file = $new_file; 189 } else { 190 $file = $uploaded_file->getClientFilename(); 191 } 192 193 // The folder 194 $folder = str_replace('\\', '/', $folder); 195 $folder = trim($folder, '/'); 196 if ($folder !== '') { 197 $folder .= '/'; 198 } 199 200 // Generate a unique name for the file? 201 if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->has($folder . $file)) { 202 $folder = ''; 203 $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION); 204 $file = sha1((string) $uploaded_file->getStream()) . '.' . $extension; 205 } 206 207 try { 208 $tree->mediaFilesystem($data_filesystem)->writeStream($folder . $file, $uploaded_file->getStream()->detach()); 209 210 return $folder . $file; 211 } catch (RuntimeException | InvalidArgumentException $ex) { 212 FlashMessages::addMessage(I18N::translate('There was an error uploading your file.')); 213 214 return ''; 215 } 216 } 217 } 218 219 /** 220 * Convert the media file attributes into GEDCOM format. 221 * 222 * @param string $file 223 * @param string $type 224 * @param string $title 225 * 226 * @return string 227 */ 228 public function createMediaFileGedcom(string $file, string $type, string $title): string 229 { 230 if (preg_match('/\.([a-z0-9]+)/i', $file, $match)) { 231 $extension = strtolower($match[1]); 232 $extension = str_replace('jpg', 'jpeg', $extension); 233 $extension = ' ' . $extension; 234 } else { 235 $extension = ''; 236 } 237 238 $gedcom = '1 FILE ' . $file; 239 if ($type !== '') { 240 $gedcom .= "\n2 FORM" . $extension . "\n3 TYPE " . $type; 241 } 242 if ($title !== '') { 243 $gedcom .= "\n2 TITL " . $title; 244 } 245 246 return $gedcom; 247 } 248 249 /** 250 * Fetch a list of all files on disk (in folders used by any tree). 251 * 252 * @param FilesystemInterface $data_filesystem Fileystem to search 253 * @param string $media_folder Root folder 254 * @param bool $subfolders Include subfolders 255 * 256 * @return Collection 257 */ 258 public function allFilesOnDisk(FilesystemInterface $data_filesystem, string $media_folder, bool $subfolders): Collection 259 { 260 $array = $data_filesystem->listContents($media_folder, $subfolders); 261 262 return Collection::make($array) 263 ->filter(static function (array $metadata): bool { 264 return 265 $metadata['type'] === 'file' && 266 strpos($metadata['path'], '/thumbs/') === false && 267 strpos($metadata['path'], '/watermark/') === false; 268 }) 269 ->map(static function (array $metadata): string { 270 return $metadata['path']; 271 }); 272 } 273 274 /** 275 * Fetch a list of all files on in the database. 276 * 277 * @param string $media_folder Root folder 278 * @param bool $subfolders Include subfolders 279 * 280 * @return Collection 281 */ 282 public function allFilesInDatabase(string $media_folder, bool $subfolders): Collection 283 { 284 $query = DB::table('media_file') 285 ->join('gedcom_setting', 'gedcom_id', '=', 'm_file') 286 ->where('setting_name', '=', 'MEDIA_DIRECTORY') 287 //->where('multimedia_file_refn', 'LIKE', '%/%') 288 ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 289 ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 290 ->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%') 291 ->select(new Expression('setting_value || multimedia_file_refn AS path')) 292 ->orderBy(new Expression('setting_value || multimedia_file_refn')); 293 294 if (!$subfolders) { 295 $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%'); 296 } 297 298 return $query->pluck('path'); 299 } 300 301 /** 302 * Generate a list of all folders in either the database or the filesystem. 303 * 304 * @param FilesystemInterface $data_filesystem 305 * 306 * @return Collection 307 */ 308 public function allMediaFolders(FilesystemInterface $data_filesystem): Collection 309 { 310 $db_folders = DB::table('media_file') 311 ->join('gedcom_setting', 'gedcom_id', '=', 'm_file') 312 ->where('setting_name', '=', 'MEDIA_DIRECTORY') 313 ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 314 ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 315 ->select(new Expression('setting_value || multimedia_file_refn AS path')) 316 ->pluck('path') 317 ->map(static function (string $path): string { 318 return dirname($path) . '/'; 319 }); 320 321 $media_roots = DB::table('gedcom_setting') 322 ->where('setting_name', '=', 'MEDIA_DIRECTORY') 323 ->pluck('setting_value') 324 ->unique(); 325 326 $disk_folders = new Collection($media_roots); 327 328 foreach ($media_roots as $media_folder) { 329 $tmp = Collection::make($data_filesystem->listContents($media_folder, true)) 330 ->filter(static function (array $metadata) { 331 return $metadata['type'] === 'dir'; 332 }) 333 ->map(static function (array $metadata): string { 334 return $metadata['path'] . '/'; 335 }) 336 ->filter(static function (string $dir): bool { 337 return strpos($dir, '/thumbs/') === false && strpos($dir, 'watermarks') === false; 338 }); 339 340 $disk_folders = $disk_folders->concat($tmp); 341 } 342 343 return $disk_folders->concat($db_folders) 344 ->unique() 345 ->mapWithKeys(static function (string $folder): array { 346 return [$folder => $folder]; 347 }); 348 } 349} 350