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 InvalidArgumentException; 28use League\Flysystem\FilesystemInterface; 29use Psr\Http\Message\ServerRequestInterface; 30use Psr\Http\Message\UploadedFileInterface; 31use RuntimeException; 32use Symfony\Component\HttpFoundation\File\UploadedFile; 33 34use function array_combine; 35use function array_diff; 36use function array_filter; 37use function array_map; 38use function assert; 39use function intdiv; 40use function pathinfo; 41use function preg_match; 42use function sha1; 43use function sort; 44use function str_replace; 45use function strpos; 46use function strtolower; 47use function trim; 48 49use const PATHINFO_EXTENSION; 50use const UPLOAD_ERR_OK; 51 52/** 53 * Managing media files. 54 */ 55class MediaFileService 56{ 57 public const EDIT_RESTRICTIONS = [ 58 'locked', 59 ]; 60 61 public const PRIVACY_RESTRICTIONS = [ 62 'none', 63 'privacy', 64 'confidential', 65 ]; 66 67 /** 68 * What is the largest file a user may upload? 69 */ 70 public function maxUploadFilesize(): string 71 { 72 $bytes = UploadedFile::getMaxFilesize(); 73 $kb = intdiv($bytes + 1023, 1024); 74 75 return I18N::translate('%s KB', I18N::number($kb)); 76 } 77 78 /** 79 * A list of key/value options for media types. 80 * 81 * @param string $current 82 * 83 * @return array 84 */ 85 public function mediaTypes($current = ''): array 86 { 87 $media_types = GedcomTag::getFileFormTypes(); 88 89 $media_types = ['' => ''] + [$current => $current] + $media_types; 90 91 return $media_types; 92 } 93 94 /** 95 * A list of media files not already linked to a media object. 96 * 97 * @param Tree $tree 98 * @param FilesystemInterface $data_filesystem 99 * 100 * @return array 101 */ 102 public function unusedFiles(Tree $tree, FilesystemInterface $data_filesystem): array 103 { 104 $used_files = DB::table('media_file') 105 ->where('m_file', '=', $tree->id()) 106 ->where('multimedia_file_refn', 'NOT LIKE', 'http://%') 107 ->where('multimedia_file_refn', 'NOT LIKE', 'https://%') 108 ->pluck('multimedia_file_refn') 109 ->all(); 110 111 $disk_files = $tree->mediaFilesystem($data_filesystem)->listContents('', true); 112 113 $disk_files = array_filter($disk_files, static function (array $item) { 114 // Older versions of webtrees used a couple of special folders. 115 return 116 $item['type'] === 'file' && 117 strpos($item['path'], '/thumbs/') === false && 118 strpos($item['path'], '/watermarks/') === false; 119 }); 120 121 $disk_files = array_map(static function (array $item): string { 122 return $item['path']; 123 }, $disk_files); 124 125 $unused_files = array_diff($disk_files, $used_files); 126 127 sort($unused_files); 128 129 return array_combine($unused_files, $unused_files); 130 } 131 132 /** 133 * Store an uploaded file (or URL), either to be added to a media object 134 * or to create a media object. 135 * 136 * @param ServerRequestInterface $request 137 * 138 * @return string The value to be stored in the 'FILE' field of the media object. 139 */ 140 public function uploadFile(ServerRequestInterface $request): string 141 { 142 $tree = $request->getAttribute('tree'); 143 assert($tree instanceof Tree); 144 145 $data_filesystem = $request->getAttribute('filesystem.data'); 146 assert($data_filesystem instanceof FilesystemInterface); 147 148 $params = $request->getParsedBody(); 149 $file_location = $params['file_location']; 150 151 switch ($file_location) { 152 case 'url': 153 $remote = $params['remote']; 154 155 if (strpos($remote, '://') !== false) { 156 return $remote; 157 } 158 159 return ''; 160 161 case 'unused': 162 $unused = $params['unused']; 163 164 if ($tree->mediaFilesystem($data_filesystem)->has($unused)) { 165 return $unused; 166 } 167 168 return ''; 169 170 case 'upload': 171 default: 172 $folder = $params['folder']; 173 $auto = $params['auto']; 174 $new_file = $params['new_file']; 175 176 /** @var UploadedFileInterface|null $uploaded_file */ 177 $uploaded_file = $request->getUploadedFiles()['file']; 178 if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) { 179 return ''; 180 } 181 182 // The filename 183 $new_file = str_replace('\\', '/', $new_file); 184 if ($new_file !== '' && strpos($new_file, '/') === false) { 185 $file = $new_file; 186 } else { 187 $file = $uploaded_file->getClientFilename(); 188 } 189 190 // The folder 191 $folder = str_replace('\\', '/', $folder); 192 $folder = trim($folder, '/'); 193 if ($folder !== '') { 194 $folder .= '/'; 195 } 196 197 // Generate a unique name for the file? 198 if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->has($folder . $file)) { 199 $folder = ''; 200 $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION); 201 $file = sha1((string) $uploaded_file->getStream()) . '.' . $extension; 202 } 203 204 try { 205 $tree->mediaFilesystem($data_filesystem)->writeStream($folder . $file, $uploaded_file->getStream()->detach()); 206 207 return $folder . $file; 208 } catch (RuntimeException | InvalidArgumentException $ex) { 209 FlashMessages::addMessage(I18N::translate('There was an error uploading your file.')); 210 211 return ''; 212 } 213 } 214 } 215 216 /** 217 * Convert the media file attributes into GEDCOM format. 218 * 219 * @param string $file 220 * @param string $type 221 * @param string $title 222 * 223 * @return string 224 */ 225 public function createMediaFileGedcom(string $file, string $type, string $title): string 226 { 227 if (preg_match('/\.([a-z0-9]+)/i', $file, $match)) { 228 $extension = strtolower($match[1]); 229 $extension = str_replace('jpg', 'jpeg', $extension); 230 $extension = ' ' . $extension; 231 } else { 232 $extension = ''; 233 } 234 235 $gedcom = '1 FILE ' . $file; 236 if ($type !== '') { 237 $gedcom .= "\n2 FORM" . $extension . "\n3 TYPE " . $type; 238 } 239 if ($title !== '') { 240 $gedcom .= "\n2 TITL " . $title; 241 } 242 243 return $gedcom; 244 } 245} 246