File indexing completed on 2024-12-22 05:33:08
0001 <?php 0002 /** @noinspection PhpUnused */ 0003 /** @noinspection PhpUndefinedFieldInspection */ 0004 0005 use Aws\Credentials\Credentials; 0006 use Aws\S3\S3Client; 0007 use Ocs\Filter\File\Filename; 0008 use Ocs\Storage\FilesystemAdapter; 0009 use Ocs\Url\UrlSigner; 0010 0011 /** 0012 * ocs-fileserver 0013 * 0014 * Copyright 2016 by pling GmbH. 0015 * 0016 * This file is part of ocs-fileserver. 0017 * 0018 * ocs-fileserver is free software: you can redistribute it and/or modify 0019 * it under the terms of the GNU Affero General Public License as published by 0020 * the Free Software Foundation, either version 3 of the License, or 0021 * (at your option) any later version. 0022 * 0023 * ocs-fileserver is distributed in the hope that it will be useful, 0024 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0025 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0026 * GNU Affero General Public License for more details. 0027 * 0028 * You should have received a copy of the GNU Affero General Public License 0029 * along with Foobar. If not, see <http://www.gnu.org/licenses/>. 0030 **/ 0031 class Files extends BaseController 0032 { 0033 0034 const MIN_TIME = 60; 0035 const MAX_REQUEST_PER_MINUTE = 10; 0036 const BLOCKING_PERIOD = 180; 0037 0038 /** 0039 * @throws Flooer_Exception 0040 */ 0041 public function getIndex() 0042 { 0043 $originId = null; 0044 $status = 'active'; 0045 $clientId = null; 0046 $ownerId = null; 0047 $collectionId = null; 0048 $collectionStatus = 'active'; 0049 $collectionCategory = null; 0050 $collectionTags = null; // Comma-separated list 0051 $collectionContentId = null; 0052 $types = null; // Comma-separated list 0053 $category = null; 0054 $tags = null; // Comma-separated list 0055 $ocsCompatibility = 'all'; 0056 $contentId = null; 0057 $search = null; // 3 or more strings 0058 $ids = null; // Comma-separated list 0059 $favoriteIds = array(); 0060 $downloadedTimeperiodBegin = null; // Datetime format 0061 $downloadedTimeperiodEnd = null; // Datetime format 0062 $sort = 'name'; 0063 $perpage = $this->appConfig->general['perpage']; 0064 $page = 1; 0065 0066 if (!empty($this->request->origin_id)) { 0067 $originId = $this->request->origin_id; 0068 } 0069 if (!empty($this->request->status)) { 0070 $status = $this->request->status; 0071 } 0072 if (!empty($this->request->client_id)) { 0073 $clientId = $this->request->client_id; 0074 } 0075 if (!empty($this->request->owner_id)) { 0076 $ownerId = $this->request->owner_id; 0077 } 0078 if (!empty($this->request->collection_id)) { 0079 $collectionId = $this->request->collection_id; 0080 } 0081 if (!empty($this->request->collection_status)) { 0082 $collectionStatus = $this->request->collection_status; 0083 } 0084 if (isset($this->request->collection_category)) { 0085 $collectionCategory = $this->request->collection_category; 0086 } 0087 if (isset($this->request->collection_tags)) { 0088 $collectionTags = $this->request->collection_tags; 0089 } 0090 if (isset($this->request->collection_content_id)) { 0091 $collectionContentId = $this->request->collection_content_id; 0092 } 0093 if (!empty($this->request->types)) { 0094 $types = $this->request->types; 0095 } 0096 if (isset($this->request->category)) { 0097 $category = $this->request->category; 0098 } 0099 if (isset($this->request->tags)) { 0100 $tags = $this->request->tags; 0101 } 0102 if (!empty($this->request->ocs_compatibility)) { 0103 $ocsCompatibility = $this->request->ocs_compatibility; 0104 } 0105 if (isset($this->request->content_id)) { 0106 $contentId = $this->request->content_id; 0107 } 0108 if (!empty($this->request->search)) { 0109 $search = $this->request->search; 0110 } 0111 if (!empty($this->request->ids)) { 0112 $ids = $this->request->ids; 0113 } 0114 if (!empty($this->request->client_id) && !empty($this->request->favoritesby)) { 0115 $favoriteIds = $this->_getFavoriteIds($this->request->client_id, $this->request->favoritesby); 0116 if (!$favoriteIds) { 0117 $this->response->setStatus(404); 0118 throw new Flooer_Exception('Not found', LOG_NOTICE); 0119 } 0120 } 0121 if (!empty($this->request->downloaded_timeperiod_begin)) { 0122 $downloadedTimeperiodBegin = $this->request->downloaded_timeperiod_begin; 0123 } 0124 if (!empty($this->request->downloaded_timeperiod_end)) { 0125 $downloadedTimeperiodEnd = $this->request->downloaded_timeperiod_end; 0126 } 0127 if (!empty($this->request->sort)) { 0128 $sort = $this->request->sort; 0129 } 0130 if (!empty($this->request->perpage) && $this->_isValidPerpageNumber($this->request->perpage)) { 0131 $perpage = $this->request->perpage; 0132 } 0133 if (!empty($this->request->page) && $this->_isValidPageNumber($this->request->page)) { 0134 $page = $this->request->page; 0135 } 0136 0137 $files = $this->models->files->getFiles($originId, $status, $clientId, $ownerId, $collectionId, $collectionStatus, $collectionCategory, $collectionTags, $collectionContentId, $types, $category, $tags, $ocsCompatibility, $contentId, $search, $ids, $favoriteIds, $downloadedTimeperiodBegin, $downloadedTimeperiodEnd, $sort, $perpage, $page); 0138 0139 if (!$files) { 0140 $this->response->setStatus(404); 0141 throw new Flooer_Exception('Not found', LOG_NOTICE); 0142 } 0143 0144 $this->_setResponseContent('success', $files); 0145 } 0146 0147 /** 0148 * @throws Flooer_Exception 0149 */ 0150 public function getFile() 0151 { 0152 $id = null; 0153 0154 if (!empty($this->request->id)) { 0155 $id = $this->request->id; 0156 } 0157 0158 $file = $this->models->files->getFile($id); 0159 0160 if (!$file) { 0161 $this->response->setStatus(404); 0162 throw new Flooer_Exception('Not found', LOG_NOTICE); 0163 } 0164 0165 $this->_setResponseContent('success', array('file' => $file)); 0166 } 0167 0168 /** 0169 * @param $element_name 0170 * @param $error_message 0171 * 0172 * @return bool 0173 */ 0174 protected function testFileUpload($element_name, &$error_message): bool 0175 { 0176 if (!isset($_FILES[$element_name])) { 0177 $error_message = "No file upload with name '$element_name' in request."; 0178 0179 return false; 0180 } 0181 $error = $_FILES[$element_name]['error']; 0182 0183 // List at: http://php.net/manual/en/features.file-upload.errors.php 0184 if ($error != UPLOAD_ERR_OK) { 0185 switch ($error) { 0186 case UPLOAD_ERR_INI_SIZE: 0187 $error_message = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'; 0188 break; 0189 0190 case UPLOAD_ERR_FORM_SIZE: 0191 $error_message = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'; 0192 break; 0193 0194 case UPLOAD_ERR_PARTIAL: 0195 $error_message = 'The uploaded file was only partially uploaded.'; 0196 break; 0197 0198 case UPLOAD_ERR_NO_FILE: 0199 $error_message = 'No file was uploaded.'; 0200 break; 0201 0202 case UPLOAD_ERR_NO_TMP_DIR: 0203 $error_message = 'Missing a temporary folder.'; 0204 break; 0205 0206 case UPLOAD_ERR_CANT_WRITE: 0207 $error_message = 'Failed to write file to disk.'; 0208 break; 0209 0210 case UPLOAD_ERR_EXTENSION: 0211 $error_message = 'A PHP extension interrupted the upload.'; 0212 break; 0213 0214 default: 0215 $error_message = 'Unknown error'; 0216 break; 0217 } 0218 0219 return false; 0220 } 0221 0222 $error_message = null; 0223 0224 return true; 0225 } 0226 0227 /** 0228 * @throws Flooer_Exception 0229 */ 0230 public function postFile() { 0231 if (!$this->_isAllowedAccess()) { 0232 $this->response->setStatus(403); 0233 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0234 } 0235 0236 $errors = array(); 0237 if (!$this->request->client_id) { 0238 $errors['client_id'] = 'Required'; 0239 } 0240 if (!$this->request->owner_id) { 0241 $errors['owner_id'] = 'Required'; 0242 } 0243 /* 0244 if (!isset($_FILES['file'])) { 0245 $errors['file'] = 'Required'; 0246 } 0247 else if (!empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 0248 $errors['file'] = $_FILES['file']['error']; 0249 } 0250 */ 0251 // for hive files importing (Deprecated) ---------- 0252 if (!isset($_FILES['file']) && !isset($this->request->local_file_path)) { 0253 $errors['file'] = 'Required'; 0254 } 0255 if (isset($_FILES['file']) && !empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 0256 $errors['file'] = $_FILES['file']['error']; 0257 } 0258 // ------------------------------------------------ 0259 0260 if ($errors) { 0261 $this->response->setStatus(400); 0262 $this->_setResponseContent('error', array( 0263 'message' => 'Validation error', 0264 'errors' => $errors, 0265 )); 0266 0267 return; 0268 } 0269 0270 $file = $this->processFileUpload(); 0271 0272 $this->_setResponseContent('success', array('file' => $file)); 0273 } 0274 0275 /** 0276 * @return Flooer_Db_Table_Row|null 0277 * @throws Flooer_Exception 0278 */ 0279 private function processFileUpload(): ?Flooer_Db_Table_Row 0280 { 0281 $id = null; // Auto generated 0282 $originId = null; // Auto generated 0283 $active = 1; 0284 $clientId = null; 0285 $ownerId = null; 0286 $collectionId = null; 0287 $name = null; // Auto generated 0288 $type = null; // Auto detect 0289 $size = null; // Auto detect 0290 $md5sum = null; // Auto detect 0291 $title = null; // Name as default 0292 $description = null; 0293 $category = null; 0294 $tags = null; // Comma-separated list 0295 $version = null; 0296 $ocsCompatible = 1; 0297 $contentId = null; 0298 $contentPage = null; 0299 0300 $downloadedCount = 0; // for hive files importing (Deprecated) 0301 0302 if (!empty($this->request->client_id)) { 0303 $clientId = $this->request->client_id; 0304 } 0305 if (!empty($this->request->owner_id)) { 0306 $ownerId = $this->request->owner_id; 0307 } 0308 if (!empty($this->request->collection_id)) { 0309 $collectionId = $this->request->collection_id; 0310 } 0311 if (isset($this->request->tags)) { 0312 $tags = strip_tags($this->request->tags); 0313 } 0314 if (isset($_FILES['file'])) { 0315 if (!empty($_FILES['file']['name'])) { 0316 //$name = mb_substr(strip_tags(basename($_FILES['file']['name'])), 0, 200); 0317 $filter = new Filename(['beautify' => true]); 0318 $name = $filter->filter(basename($_FILES['file']['name'])); 0319 } 0320 if (!empty($_FILES['file']['tmp_name'])) { 0321 $info = new finfo(FILEINFO_MIME_TYPE); 0322 $type = $info->file($_FILES['file']['tmp_name']); 0323 if (!$type) { 0324 $type = 'application/octet-stream'; 0325 } 0326 $md5sum = md5_file($_FILES['file']['tmp_name']); 0327 } 0328 if (!empty($_FILES['file']['size'])) { 0329 $size = $_FILES['file']['size']; 0330 } 0331 } 0332 else { // alternative application path when user uploads an url 0333 if (isset($this->request->local_file_path)) { 0334 if (!empty($this->request->local_file_path)) { 0335 // $name = mb_substr(strip_tags(basename($this->request->local_file_path)), 0, 200); 0336 $filter = new Filename(['beautify' => true]); 0337 $name = $filter->filter(basename($this->request->local_file_path)); 0338 $externalUri = $this->_detectLinkInTags($tags); 0339 if ($name == 'empty' && !empty($externalUri)) { 0340 $fileAttribs = $this->getRemoteFileInfo($externalUri); 0341 $size = isset($fileAttribs['fileSize']) ? (int)$fileAttribs['fileSize'] : 0; 0342 $this->logWithRequestId(__METHOD__ . " - file size detected: $size"); 0343 0344 //$data = get_headers($externalUri, true); 0345 //$size = isset($data['Content-Length']) ? (int)$data['Content-Length'] : 0; 0346 0347 $type = $this->_detectMimeTypeFromUri($externalUri); 0348 if (0 >= $size) { 0349 $size = $this->_detectFilesizeFromUri($externalUri); 0350 $this->logWithRequestId(__METHOD__ . " - file size detected: $size"); 0351 } 0352 } else { 0353 $info = new finfo(FILEINFO_MIME_TYPE); 0354 $type = $info->file($this->request->local_file_path); 0355 if (!$type) { 0356 $type = 'application/octet-stream'; 0357 } 0358 $size = filesize($this->request->local_file_path); 0359 } 0360 } 0361 if (!empty($this->request->local_file_name)) { 0362 //$name = mb_substr(strip_tags(basename($this->request->local_file_name)), 0, 200); 0363 $filter = new Filename(['beautify' => true]); 0364 $name = $filter->filter(basename($this->request->local_file_name)); 0365 } 0366 } 0367 } 0368 if (!empty($this->request->title)) { 0369 $title = mb_substr(strip_tags($this->request->title), 0, 200); 0370 } 0371 if (isset($this->request->description)) { 0372 $description = strip_tags($this->request->description); 0373 } 0374 if (isset($this->request->category)) { 0375 $category = mb_substr(strip_tags($this->request->category), 0, 64); 0376 } 0377 if (isset($this->request->version)) { 0378 $version = mb_substr(strip_tags($this->request->version), 0, 64); 0379 } 0380 if (isset($this->request->ocs_compatible)) { 0381 if ($this->request->ocs_compatible == 1) { 0382 $ocsCompatible = 1; 0383 } else { 0384 if ($this->request->ocs_compatible == 0) { 0385 $ocsCompatible = 0; 0386 } 0387 } 0388 } 0389 if (isset($this->request->content_id)) { 0390 $contentId = $this->request->content_id; 0391 } 0392 if (!empty($this->request->content_page)) { 0393 $contentPage = $this->request->content_page; 0394 } 0395 // for hive files importing (Deprecated) ---------- 0396 if (!empty($this->request->downloaded_count)) { 0397 $downloadedCount = intval($this->request->downloaded_count); 0398 } 0399 // ------------------------------------------------ 0400 0401 0402 // Get ID3 tags in the file. 'getid3' may not work on network storage 0403 $id3Tags = $this->_getId3Tags($type, $_FILES['file']['tmp_name']); 0404 0405 $fileSystemAdapter = new FilesystemAdapter($this->appConfig); 0406 // $fileSystemAdapter = new \Ocs\Storage\S3Adapter($this->appConfig); 0407 0408 // Prepare to append the file to collection 0409 $collectionName = null; 0410 $collectionData = array(); 0411 if ($collectionId) { 0412 // Get specified collection and check owner and client 0413 $collection = $this->models->collections->$collectionId; 0414 if (!$collection || $collection->client_id != $clientId || $collection->owner_id != $ownerId) { 0415 $this->response->setStatus(403); 0416 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0417 } 0418 $collectionName = $collection->name; 0419 // add file count and size 0420 $collectionData = array('files' => $collection->files + 1, 0421 'size' => $collection->size + $size,); 0422 } else { 0423 // Prepare new collection 0424 $collectionId = $this->models->collections->generateId(); 0425 $collectionActive = 1; 0426 $collectionName = $collectionId; 0427 $collectionTitle = $collectionName; 0428 $collectionDescription = null; 0429 $collectionCategory = null; 0430 $collectionTags = null; 0431 $collectionVersion = null; 0432 $collectionContentId = null; 0433 $collectionContentPage = null; 0434 0435 $collectionPath = $this->appConfig->general['filesDir'] . DIRECTORY_SEPARATOR . $collectionName; 0436 if (!$fileSystemAdapter->testAndCreate($collectionPath)) { 0437 $this->response->setStatus(500); 0438 throw new Flooer_Exception('Failed to create new collection path: ' . $collectionPath, LOG_ALERT); 0439 } 0440 $collectionData = array('active' => $collectionActive, 0441 'client_id' => $clientId, 0442 'owner_id' => $ownerId, 0443 'name' => $collectionName, 0444 'files' => 1, 0445 'size' => $size, 0446 'title' => $collectionTitle, 0447 'description' => $collectionDescription, 0448 'category' => $collectionCategory, 0449 'tags' => $collectionTags, 0450 'version' => $collectionVersion, 0451 'content_id' => $collectionContentId, 0452 'content_page' => $collectionContentPage,); 0453 } 0454 0455 $id = $this->models->files->generateId(); 0456 $originId = $id; 0457 $collectionPath = $this->appConfig->general['filesDir'] . DIRECTORY_SEPARATOR . $collectionName; 0458 $name = $fileSystemAdapter->fixFilename($name, $collectionPath); 0459 if (!$title) { 0460 $title = mb_substr(strip_tags($name), 0, 200); 0461 } 0462 0463 // Save the uploaded file 0464 $pathFile = $collectionPath . DIRECTORY_SEPARATOR . $name; 0465 $this->logWithRequestId(__METHOD__ . ' - check general storage path exists: ' . $collectionPath . ' :: ' . (is_dir($collectionPath) ? 'true' : 'false')); 0466 if (empty($this->request->local_file_path) && !$fileSystemAdapter->moveUploadedFile($_FILES['file']['tmp_name'], $pathFile)) { 0467 $this->response->setStatus(500); 0468 throw new Flooer_Exception('Failed to save the file: ' . $_FILES['file']['tmp_name'] . ' --> ' . $pathFile, LOG_ALERT); 0469 } 0470 if (!empty($this->request->local_file_path) && !$fileSystemAdapter->copyFile($this->appConfig->general['filesDir'] . '/empty', $pathFile)) { 0471 $this->response->setStatus(500); 0472 throw new Flooer_Exception('Failed to copy the empty dummy file ('.$this->appConfig->general['filesDir'] . '/empty'.') to destination: ' . $pathFile, LOG_ALERT); 0473 } 0474 // Add/Update the collection 0475 $this->models->collections->$collectionId = $collectionData; 0476 0477 // Add the file 0478 $this->models->files->$id = array('origin_id' => $originId, 0479 'active' => $active, 0480 'client_id' => $clientId, 0481 'owner_id' => $ownerId, 0482 'collection_id' => $collectionId, 0483 'name' => $name, 0484 'type' => $type, 0485 'size' => $size, 0486 'md5sum' => $md5sum, 0487 'title' => $title, 0488 'description' => $description, 0489 'category' => $category, 0490 'tags' => $tags, 0491 'version' => $version, 0492 'ocs_compatible' => $ocsCompatible, 0493 'content_id' => $contentId, 0494 'content_page' => $contentPage, 0495 'downloaded_count' => $downloadedCount); 0496 0497 // Add the media 0498 if ($id3Tags) { 0499 $this->_addMedia($id3Tags, $clientId, $ownerId, $collectionId, $id, $name); 0500 } 0501 0502 return $this->models->files->getFile($id); 0503 } 0504 0505 private function _getId3Tags($filetype, $filepath) 0506 { 0507 // NOTE: getid3 may not work for a files in a network storage. 0508 $id3Tags = null; 0509 if (strpos($filetype, 'audio/') !== false || strpos($filetype, 'video/') !== false || strpos($filetype, 'application/ogg') !== false) { 0510 require_once 'getid3/getid3.php'; 0511 $getID3 = new getID3(); 0512 $id3Tags = $getID3->analyze($filepath); 0513 getid3_lib::CopyTagsToComments($id3Tags); 0514 } 0515 0516 return $id3Tags; 0517 } 0518 0519 private function _addMedia(array $id3Tags, $clientId, $ownerId, $collectionId, $fileId, $defaultTitle) 0520 { 0521 // Get artist id or add new one 0522 $artistName = 'Unknown'; 0523 if (isset($id3Tags['comments']['artist'][0]) && $id3Tags['comments']['artist'][0] != '') { 0524 $artistName = mb_substr(strip_tags($id3Tags['comments']['artist'][0]), 0, 255); 0525 } 0526 $artistId = $this->models->media_artists->getId($clientId, $artistName); 0527 if (!$artistId) { 0528 $artistId = $this->models->media_artists->generateId(); 0529 $this->models->media_artists->$artistId = array('client_id' => $clientId, 0530 'name' => $artistName,); 0531 } 0532 0533 // Get album id or add new one 0534 $albumName = 'Unknown'; 0535 if (isset($id3Tags['comments']['album'][0]) && $id3Tags['comments']['album'][0] != '') { 0536 $albumName = mb_substr(strip_tags($id3Tags['comments']['album'][0]), 0, 255); 0537 } 0538 $albumId = $this->models->media->getAlbumId($clientId, $artistName, $albumName); 0539 if (!$albumId) { 0540 $albumId = $this->models->media_albums->generateId(); 0541 $this->models->media_albums->$albumId = array('client_id' => $clientId, 0542 'name' => $albumName,); 0543 } 0544 0545 // Add the media 0546 $mediaData = array('client_id' => $clientId, 0547 'owner_id' => $ownerId, 0548 'collection_id' => $collectionId, 0549 'file_id' => $fileId, 0550 'artist_id' => $artistId, 0551 'album_id' => $albumId, 0552 'title' => $defaultTitle, 0553 'genre' => null, 0554 'track' => null, 0555 'creationdate' => null, 0556 'bitrate' => 0, 0557 'playtime_seconds' => 0, 0558 'playtime_string' => 0,); 0559 if (isset($id3Tags['comments']['title'][0]) && $id3Tags['comments']['title'][0] != '') { 0560 $mediaData['title'] = mb_substr(strip_tags($id3Tags['comments']['title'][0]), 0, 255); 0561 } 0562 if (!empty($id3Tags['comments']['genre'][0])) { 0563 $mediaData['genre'] = mb_substr(strip_tags($id3Tags['comments']['genre'][0]), 0, 64); 0564 } 0565 if (!empty($id3Tags['comments']['track_number'][0])) { 0566 $mediaData['track'] = mb_substr(strip_tags($id3Tags['comments']['track_number'][0]), 0, 5); 0567 } 0568 if (!empty($id3Tags['comments']['creationdate'][0])) { 0569 $mediaData['creationdate'] = mb_substr(strip_tags($id3Tags['comments']['creationdate'][0]), 0, 4); 0570 } 0571 if (!empty($id3Tags['bitrate'])) { 0572 $mediaData['bitrate'] = mb_substr(strip_tags($id3Tags['bitrate']), 0, 11); 0573 } 0574 if (!empty($id3Tags['playtime_seconds'])) { 0575 $mediaData['playtime_seconds'] = mb_substr(strip_tags($id3Tags['playtime_seconds']), 0, 11); 0576 } 0577 if (!empty($id3Tags['playtime_string'])) { 0578 $mediaData['playtime_string'] = mb_substr(strip_tags($id3Tags['playtime_string']), 0, 8); 0579 } 0580 0581 $mediaId = $this->models->media->generateId(); 0582 $this->models->media->$mediaId = $mediaData; 0583 0584 // Save the album cover 0585 if (!empty($id3Tags['comments']['picture'][0]['data'])) { 0586 $image = imagecreatefromstring($id3Tags['comments']['picture'][0]['data']); 0587 if ($image !== false) { 0588 imagejpeg($image, $this->appConfig->general['thumbnailsDir'] . '/album_' . $albumId . '.jpg', 75); 0589 imagedestroy($image); 0590 } 0591 } 0592 } 0593 0594 /** 0595 * @throws Flooer_Exception 0596 */ 0597 public function putFile() { 0598 if (!$this->_isAllowedAccess()) { 0599 $this->response->setStatus(403); 0600 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0601 } 0602 0603 $errors = array(); 0604 if (!empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 0605 $errors['file'] = $_FILES['file']['error']; 0606 } 0607 if ($errors) { 0608 $this->response->setStatus(400); 0609 $this->_setResponseContent('error', array( 0610 'message' => 'File upload error', 0611 'errors' => $errors, 0612 )); 0613 0614 return; 0615 } 0616 $errors = array(); 0617 if (!$this->request->id) { 0618 $errors['id'] = 'Required'; 0619 } 0620 if (!$this->request->client_id) { 0621 $errors['client_id'] = 'Required'; 0622 } 0623 if ($errors) { 0624 $this->response->setStatus(400); 0625 $this->_setResponseContent('error', array( 0626 'message' => 'Validation error', 0627 'errors' => $errors, 0628 )); 0629 0630 return; 0631 } 0632 0633 $file = $this->processFileUpdate(); 0634 0635 $this->_setResponseContent('success', array('file' => $file)); 0636 } 0637 0638 /** 0639 * @return Flooer_Db_Table_Row|null 0640 * @throws Flooer_Exception 0641 */ 0642 protected function processFileUpdate(): ?Flooer_Db_Table_Row 0643 { 0644 $id = null; 0645 $title = null; 0646 $description = null; 0647 $category = null; 0648 $tags = null; // Comma-separated list 0649 $version = null; 0650 $ocsCompatible = null; 0651 $contentId = null; 0652 $contentPage = null; 0653 0654 if (!empty($this->request->id)) { 0655 $id = $this->request->id; 0656 } 0657 if (!empty($this->request->title)) { 0658 $title = mb_substr(strip_tags($this->request->title), 0, 200); 0659 } 0660 if (isset($this->request->description)) { 0661 $description = strip_tags($this->request->description); 0662 } 0663 if (isset($this->request->category)) { 0664 $category = mb_substr(strip_tags($this->request->category), 0, 64); 0665 } 0666 if (isset($this->request->tags)) { 0667 $tags = strip_tags($this->request->tags); 0668 } 0669 if (isset($this->request->version)) { 0670 $version = mb_substr(strip_tags($this->request->version), 0, 64); 0671 } 0672 if (isset($this->request->ocs_compatible)) { 0673 if ($this->request->ocs_compatible == 1) { 0674 $ocsCompatible = 1; 0675 } else { 0676 if ($this->request->ocs_compatible == 0) { 0677 $ocsCompatible = 0; 0678 } 0679 } 0680 } 0681 if (isset($this->request->content_id)) { 0682 $contentId = $this->request->content_id; 0683 } 0684 if (!empty($this->request->content_page)) { 0685 $contentPage = $this->request->content_page; 0686 } 0687 0688 $file = $this->models->files->$id; 0689 0690 if (!$file) { 0691 $this->response->setStatus(404); 0692 throw new Flooer_Exception('Not found', LOG_NOTICE); 0693 } else { 0694 if (!$file->active || $file->client_id != $this->request->client_id) { 0695 $this->response->setStatus(403); 0696 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0697 } 0698 } 0699 0700 // If new file has uploaded, 0701 // remove old file and replace to the new file with new file id 0702 if (isset($_FILES['file'])) { 0703 $id = null; 0704 $originId = $file->origin_id; 0705 $active = 1; 0706 $clientId = $file->client_id; 0707 $ownerId = $file->owner_id; 0708 $collectionId = $file->collection_id; 0709 $name = null; // Auto generated 0710 $type = null; // Auto detect 0711 $size = null; // Auto detect 0712 $md5sum = null; // Auto detect 0713 0714 $downloadedCount = 0; // for hive files importing (Deprecated) 0715 0716 if (!empty($_FILES['file']['name'])) { 0717 //$name = mb_substr(strip_tags(basename($_FILES['file']['name'])), 0, 200); 0718 $filter = new Filename(['beautify' => true]); 0719 $name = $filter->filter(basename($_FILES['file']['name'])); 0720 } 0721 if (!empty($_FILES['file']['tmp_name'])) { 0722 $info = new finfo(FILEINFO_MIME_TYPE); 0723 $type = $info->file($_FILES['file']['tmp_name']); 0724 $md5sum = md5_file($_FILES['file']['tmp_name']); 0725 if (!$type) { 0726 $type = 'application/octet-stream'; 0727 } 0728 } 0729 if (!empty($_FILES['file']['size'])) { 0730 $size = $_FILES['file']['size']; 0731 } 0732 0733 if ($description === null) { 0734 $description = $file->description; 0735 } 0736 if ($category === null) { 0737 $category = $file->category; 0738 } 0739 if ($tags === null) { 0740 $tags = $file->tags; 0741 } 0742 if ($version === null) { 0743 $version = $file->version; 0744 } 0745 if ($ocsCompatible === null) { 0746 $ocsCompatible = $file->ocs_compatible; 0747 } 0748 if ($contentId === null) { 0749 $contentId = $file->content_id; 0750 } 0751 if ($contentPage === null) { 0752 $contentPage = $file->content_page; 0753 } 0754 0755 $fileSystemAdapter = new FilesystemAdapter($this->appConfig); 0756 // $fileSystemAdapter = new S3Adapter($this->appConfig); 0757 0758 // Remove old file 0759 $this->_removeFile($file); 0760 0761 // Get ID3 tags in the file. 'getid3' may not work on a network storage 0762 $id3Tags = $this->_getId3Tags($type, $_FILES['file']['tmp_name']); 0763 0764 // Prepare to append the file to collection 0765 $collection = $this->models->collections->$collectionId; 0766 $collectionName = $collection->name; 0767 $collectionData = array('files' => $collection->files + 1, 0768 'size' => $collection->size + $size,); 0769 0770 $id = $this->models->files->generateId(); 0771 $collectionPath = $this->appConfig->general['filesDir'] . DIRECTORY_SEPARATOR . $collectionName; 0772 $name = $fileSystemAdapter->fixFilename($name, $collectionPath); 0773 if (!$title) { 0774 $title = mb_substr(strip_tags($name), 0, 200); 0775 } 0776 0777 // Save the uploaded file 0778 if (!$fileSystemAdapter->moveUploadedFile($_FILES['file']['tmp_name'], $collectionPath . DIRECTORY_SEPARATOR . $name)) { 0779 $this->response->setStatus(500); 0780 throw new Flooer_Exception('Failed to save the file', LOG_ALERT); 0781 } 0782 0783 // Add the file 0784 $this->models->files->$id = array('origin_id' => $originId, 0785 'active' => $active, 0786 'client_id' => $clientId, 0787 'owner_id' => $ownerId, 0788 'collection_id' => $collectionId, 0789 'name' => $name, 0790 'type' => $type, 0791 'size' => $size, 0792 'md5sum' => $md5sum, 0793 'title' => $title, 0794 'description' => $description, 0795 'category' => $category, 0796 'tags' => $tags, 0797 'version' => $version, 0798 'ocs_compatible' => $ocsCompatible, 0799 'content_id' => $contentId, 0800 'content_page' => $contentPage, 0801 'downloaded_count' => $downloadedCount 0802 // for hive files importing (Deprecated) 0803 ); 0804 0805 // Update the collection 0806 $this->models->collections->$collectionId = $collectionData; 0807 0808 // Add the media 0809 if ($id3Tags) { 0810 $this->_addMedia($id3Tags, $clientId, $ownerId, $collectionId, $id, $name); 0811 } 0812 } // Update only file information 0813 else { 0814 $updata = array(); 0815 0816 if ($title !== null) { 0817 $updata['title'] = $title; 0818 } 0819 if ($description !== null) { 0820 $updata['description'] = $description; 0821 } 0822 if ($category !== null) { 0823 $updata['category'] = $category; 0824 } 0825 if ($tags !== null) { 0826 $updata['tags'] = $tags; 0827 } 0828 if ($version !== null) { 0829 $updata['version'] = $version; 0830 } 0831 if ($ocsCompatible !== null) { 0832 $updata['ocs_compatible'] = $ocsCompatible; 0833 } 0834 if ($contentId !== null) { 0835 $updata['content_id'] = $contentId; 0836 } 0837 if ($contentPage !== null) { 0838 $updata['content_page'] = $contentPage; 0839 } 0840 0841 $this->models->files->$id = $updata; 0842 } 0843 0844 return $this->models->files->getFile($id); 0845 } 0846 0847 /** 0848 * @throws Flooer_Exception 0849 */ 0850 private function _removeFile(Flooer_Db_Table_Row &$file) { 0851 // Please be care the remove process in Collections::deleteCollection() 0852 0853 $id = $file->id; 0854 0855 $collectionId = $file->collection_id; 0856 $collection = $this->models->collections->$collectionId; 0857 $fileSystemAdapter = new FilesystemAdapter($this->appConfig); 0858 // $fileSystemAdapter = new S3Adapter($this->appConfig); 0859 0860 $trashDir = $this->appConfig->general['filesDir'] . DIRECTORY_SEPARATOR . $collection->name . '/.trash'; 0861 $this->logWithRequestId(__METHOD__ . ' - test trash dir exists: ' . $trashDir . ' :: ' . (is_dir($trashDir) ? 'true' : 'false')); 0862 if (!$fileSystemAdapter->testAndCreate($trashDir)) { 0863 $this->response->setStatus(500); 0864 throw new Flooer_Exception('Failed to create trash dir ' . $trashDir, LOG_ALERT); 0865 } 0866 $from = $this->appConfig->general['filesDir'] . DIRECTORY_SEPARATOR . $collection->name . DIRECTORY_SEPARATOR . $file->name; 0867 if (is_file($from)) { 0868 if (!$fileSystemAdapter->moveFile($from, $trashDir . '/' . $id . '-' . $file->name)) { 0869 $this->response->setStatus(500); 0870 throw new Flooer_Exception('Failed to remove the file ' . $from, LOG_ALERT); 0871 } 0872 } else { 0873 $this->logWithRequestId(__METHOD__ . ' - File could not be found. But we continue and set file inactive in the database. ' . $from,LOG_WARNING); 0874 } 0875 0876 $this->models->files->$id = array('active' => 0); 0877 //$this->models->files_downloaded->deleteByFileId($id); 0878 $this->models->favorites->deleteByFileId($id); 0879 $this->models->media->deleteByFileId($id); 0880 $this->models->media_played->deleteByFileId($id); 0881 0882 $this->models->collections->$collectionId = array( 0883 'files' => $collection->files - 1, 0884 'size' => $collection->size - $file->size, 0885 ); 0886 } 0887 0888 /** 0889 * @param $name 0890 * @param $collectionName 0891 * 0892 * @return mixed|string 0893 * @deprecated 0894 */ 0895 private function _fixFilename($name, $collectionName) 0896 { 0897 if (is_file($this->appConfig->general['filesDir'] . '/' . $collectionName . '/' . $name)) { 0898 $fix = date('YmdHis'); 0899 if (preg_match("/^([^.]+)(\..+)/", $name, $matches)) { 0900 $name = $matches[1] . '-' . $fix . $matches[2]; 0901 } else { 0902 $name = $name . '-' . $fix; 0903 } 0904 } 0905 0906 return $name; 0907 } 0908 0909 /** 0910 * @throws Flooer_Exception 0911 */ 0912 public function deleteFile() 0913 { 0914 if (!$this->_isAllowedAccess()) { 0915 $this->response->setStatus(403); 0916 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0917 } 0918 0919 $id = null; 0920 0921 if (!empty($this->request->id)) { 0922 $id = $this->request->id; 0923 } 0924 0925 $file = $this->models->files->$id; 0926 0927 if (!$file) { 0928 $this->response->setStatus(404); 0929 throw new Flooer_Exception('Not found', LOG_NOTICE); 0930 } else { 0931 if (!$file->active || $file->client_id != $this->request->client_id) { 0932 $this->response->setStatus(403); 0933 throw new Flooer_Exception('Forbidden', LOG_NOTICE); 0934 } 0935 } 0936 0937 $this->_removeFile($file); 0938 0939 $this->_setResponseContent('success'); 0940 } 0941 0942 /** 0943 * @throws Flooer_Exception 0944 * @deprecated 0945 */ 0946 public function headDownloadfile() // Deprecated 0947 { 0948 // This is alias for HEAD /files/download 0949 $this->headDownload(); 0950 } 0951 0952 /** 0953 * @throws Flooer_Exception 0954 */ 0955 public function headDownload() 0956 { 0957 $this->getDownload(true); 0958 } 0959 0960 /** 0961 * @throws Flooer_Exception 0962 */ 0963 public function getDownload($headeronly = false) 0964 { 0965 $id = null; 0966 $as = null; 0967 $userId = null; 0968 $hashGiven = null; 0969 $validUntil = null; 0970 $isFromOcsApi = false; 0971 $isFilepreview = false; 0972 0973 $linkType = null; 0974 0975 $fp = null; 0976 $ip = null; 0977 $payload = null; 0978 $payloadHash = null; 0979 0980 if (!empty($this->request->j)) { 0981 require_once '../../library/JWT.php'; 0982 $payload = JWT::decode($this->request->j, $this->appConfig->general['jwt_secret'], true); 0983 $payloadHash = sha1($this->request->j); 0984 $id = isset($payload->id) ? $payload->id : null; 0985 $hashGiven = isset($payload->s) ? $payload->s : null; 0986 $validUntil = isset($payload->t) ? $payload->t : null; 0987 $userId = isset($payload->u) ? $payload->u : null; 0988 $linkType = isset($payload->lt) ? $payload->lt : null; 0989 $isFilepreview = ($linkType === 'filepreview') ? true : false; 0990 $fp = isset($payload->stfp) ? $payload->stfp : null; 0991 $ip = isset($payload->stip) ? $payload->stip : null; //TODO: set ip from request if null 0992 $as = isset($payload->as) ? $payload->as : null; 0993 $isFromOcsApi = (isset($payload->o) and ($payload->o == 1)); 0994 } 0995 if (!empty($this->request->id)) { 0996 $id = $this->request->id; 0997 } 0998 if (!empty($this->request->as)) { 0999 $as = $this->request->as; 1000 } 1001 if (!empty($this->request->u)) { 1002 $userId = $this->request->u; 1003 } 1004 if (!empty($this->request->s)) { 1005 $hashGiven = $this->request->s; 1006 } 1007 if (!empty($this->request->t)) { 1008 $validUntil = $this->request->t; 1009 } 1010 if (!empty($this->request->o)) { 1011 $isFromOcsApi = ($this->request->o == 1); 1012 } 1013 if (!empty($this->request->lt)) { 1014 $linkType = $this->request->lt; 1015 if ($linkType === 'filepreview') { 1016 $isFilepreview = true; 1017 } 1018 } 1019 1020 // if $as = origin || latest then overwrite the file id 1021 if ($id && $as) { 1022 $id = $this->models->files->getFileId($id, $as); 1023 } 1024 1025 $file = $this->models->files->$id; 1026 if (!$file) { 1027 $this->response->setStatus(404); 1028 throw new Flooer_Exception('Not found', LOG_NOTICE); 1029 } 1030 1031 $collectionId = $file->collection_id; 1032 1033 $salt = $this->_getDownloadSecret($file->client_id); 1034 $hash = hash('sha512', $salt . $collectionId . $validUntil); 1035 $expires = $validUntil - time(); 1036 1037 $agent = null; 1038 if (isset($_SERVER)) { 1039 $agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unidentified'; 1040 } 1041 1042 // Log 1043 $this->logWithRequestId("Prepare Download (collection: $file->collection_id; file: $file->id; agent: $agent; remote ip: {$this->getIpAddress()}; request uri: {$_SERVER['REQUEST_URI']})", LOG_NOTICE); 1044 1045 //Save all(!) download requests, but not for preview downloads 1046 //remark: I really don't understand why we keep all this shit (20210125 alex) 1047 $ref = null; 1048 if (false == $isFilepreview) { 1049 $data = array('client_id' => $file->client_id, 1050 'owner_id' => $file->owner_id, 1051 'collection_id' => $file->collection_id, 1052 'file_id' => $file->id, 1053 'user_id' => $userId, 1054 'link_type' => $linkType, 1055 'source' => 'OCS-Webserver', 1056 'referer' => $ref, 1057 'user_agent' => $agent,); 1058 // if request comes from api then overwrite some details 1059 if ($isFromOcsApi) { 1060 $ref = 'OCS-API'; 1061 $data['referer'] = $ref; 1062 $data['source'] = $ref; 1063 } 1064 1065 try { 1066 //$downloadedId = $this->models->files_downloaded_all->generateId(); 1067 $downloadedId = $this->models->files_downloaded_all->generateNewId(); 1068 $this->models->files_downloaded_all->$downloadedId = $data; 1069 } catch (Exception $exc) { 1070 //echo $exc->getTraceAsString(); 1071 $this->logWithRequestId("ERROR saving Download Data to DB: {$exc->getMessage()} :: {$exc->getTraceAsString()}", LOG_ERR); 1072 } 1073 } 1074 1075 // check preconditions 1076 if (0 >= $expires) { 1077 // Log 1078 $this->logWithRequestId("Download expired (file: $file->id; time-div: $expires; )", LOG_NOTICE); 1079 $this->response->setStatus(410); 1080 $this->_setResponseContent('error', array('message' => 'link expired')); 1081 1082 return; 1083 } 1084 if ($hashGiven != $hash) { 1085 // Log 1086 $this->logWithRequestId("Download hash invalid (file: $file->id; hash: $hash; hashGiven: $hashGiven; )", LOG_NOTICE); 1087 $this->response->setStatus(400); 1088 $this->_setResponseContent('error', array('message' => 'link invalid')); 1089 1090 return; 1091 } 1092 if ($this->tooManyRequests($payloadHash, self::BLOCKING_PERIOD)) { 1093 $this->logWithRequestId("Too many requests (file: $file->id; payload hash: $payloadHash; )", LOG_NOTICE); 1094 $this->response->setStatus(429); 1095 $this->_setResponseContent('error', array('message' => 'too many requests')); 1096 1097 return; 1098 } 1099 $uniqueDownload = $this->uniqueDownload($payloadHash, $expires); 1100 if (!$uniqueDownload) { 1101 $this->logWithRequestId("Too many downloads for one token (file: $file->id; time-div: $expires; paypload hash: $payloadHash; )", LOG_NOTICE); 1102 } 1103 1104 1105 // incoming Link is ok, go on and check collection and file 1106 $collection = $this->models->collections->$collectionId; 1107 if (!$collection) { 1108 $this->logWithRequestId("Collection not found (collection id: $collectionId; file: $file->id; )", LOG_NOTICE); 1109 $this->response->setStatus(404); 1110 throw new Flooer_Exception('Not found', LOG_NOTICE); 1111 } 1112 1113 // important note: When a collection is inactive, it means it is archived. 1114 if ($collection->active) { 1115 $collectionDir = $this->appConfig->general['filesDir'] . '/' . $collection->name; 1116 // $sendFileCollection = $collection->name; 1117 } else { 1118 $collectionDir = $this->appConfig->general['filesDir'] . '/.trash/' . $collection->id . '-' . $collection->name; 1119 $this->logWithRequestId("Collection inactive take it from trash (collection: $collection->id; file: $file->id; )", LOG_NOTICE); 1120 // $sendFileCollection = '.trash/' . $collection->id . '-' . $collection->name; 1121 } 1122 1123 // important note: When a file is inactive, it means it is archived. 1124 if ($file->active) { 1125 $filePath = $collectionDir . '/' . $file->name; 1126 // $sendFilePath = 'data/files/' . $sendFileCollection . '/' . $file->name; 1127 } else { 1128 $filePath = $collectionDir . '/.trash/' . $file->id . '-' . $file->name; 1129 $this->logWithRequestId("File inactive take it from trash (file: $file->id; )", LOG_NOTICE); 1130 // $sendFilePath = 'data/files/' . $sendFileCollection . '/.trash/' . $file->id . '-' . $file->name; 1131 } 1132 1133 $fileName = $file->name; 1134 $fileType = $file->type; 1135 $fileSize = $file->size; 1136 1137 // If request URI ended with .zsync, make a response data as zsync data 1138 if (strtolower(substr($this->request->getUri(), -6)) == '.zsync') { 1139 // But don't make zsync for external URI 1140 if (!empty($this->_detectLinkInTags($file->tags))) { 1141 $this->response->setStatus(404); 1142 throw new Flooer_Exception('Not found. (request id: '.$this->getRequestId().')', LOG_NOTICE); 1143 } 1144 1145 $zsyncPath = $this->appConfig->general['zsyncDir'] . '/' . $file->id . '.zsync'; 1146 if (!is_file($zsyncPath)) { 1147 $this->_generateZsync($filePath, $zsyncPath, $fileName); 1148 } 1149 1150 $filePath = $zsyncPath; 1151 $fileName .= '.zsync'; 1152 $fileType = 'application/x-zsync'; 1153 $fileSize = filesize($zsyncPath); 1154 1155 $this->_sendFile($filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1156 } 1157 1158 if (!$isFilepreview && !$headeronly) { 1159 $this->models->files->updateDownloadedStatus($file->id); 1160 1161 try { 1162 //$downloadedId = $this->models->files_downloaded->generateId(); 1163 $downloadedId = $this->models->files_downloaded->generateNewId(); 1164 $this->models->files_downloaded->$downloadedId = array('client_id' => $file->client_id, 1165 'owner_id' => $file->owner_id, 1166 'collection_id' => $file->collection_id, 1167 'file_id' => $file->id, 1168 'user_id' => $userId, 1169 'referer' => $ref,); 1170 1171 //save unique dataset 1172 if ($uniqueDownload) { 1173 $downloadedId = $this->models->files_downloaded_unique->generateNewId(); 1174 $this->models->files_downloaded_unique->$downloadedId = array('client_id' => $file->client_id, 1175 'owner_id' => $file->owner_id, 1176 'collection_id' => $file->collection_id, 1177 'file_id' => $file->id, 1178 'user_id' => $userId, 1179 'referer' => $ref,); 1180 } 1181 1182 // save download in impression table 1183 // $this->modelOcs->ocs_downloads->save(array('file_id' => $file->id, 1184 // 'ip' => $ip, 1185 // 'fp' => $fp, 1186 // 'u' => $userId,)); 1187 } catch (Exception $exc) { 1188 //echo $exc->getTraceAsString(); 1189 $this->logWithRequestId("ERROR saving Download Data to DB: $exc->getMessage()", LOG_ERR); 1190 $this->response->setStatus(500); 1191 $this->_setResponseContent('error', array('message' => 'internal error' . " (id: {$this->getRequestId()})")); 1192 1193 return; 1194 } 1195 } 1196 1197 // If external URI has set, redirect to it 1198 $externalUri = $this->_detectLinkInTags($file->tags); 1199 if (!empty($externalUri)) { 1200 $this->response->redirect($externalUri); 1201 } 1202 1203 // if (getenv('MOD_X_ACCEL_REDIRECT_ENABLED') === 'on') { 1204 // $this->_xSendFile($sendFilePath, $filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1205 // } 1206 1207 // if (getenv('X_OCS_ACCEL_REDIRECT_ENABLED') === 'on') { 1208 // $this->_xSendFile($sendFilePath, $filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1209 // } 1210 1211 if (getenv('X_OCS_S3_DOWNLOAD_ENABLED') === 'on') { 1212 $this->logWithRequestId(__METHOD__ . ' - check storage for file: ' . $filePath . ' :: ' . (is_file($filePath) ? 'true' : 'false')); 1213 if (is_file($filePath)) { 1214 $sendFilePath = preg_replace("|^{$this->appConfig->general['basePath']}|", '', $filePath); 1215 $this->_s3SendFile($sendFilePath, $filePath, $fileName, $fileType, $fileSize, true, $headeronly, $this->appConfig->s3storage); 1216 } else 1217 // checks if the alternate storage contains the file. If it does, we continue with that file path. 1218 if ($this->appConfig->s3storage2) { 1219 $alternativeCollectionDir = rtrim($this->appConfig->s3storage2['basePath'],'/') . '/' . preg_replace("|^{$this->appConfig->general['basePath']}|", '', $this->appConfig->general['filesDir']) . '/' . $collection->name; 1220 $alternativeFilePath = $alternativeCollectionDir . DIRECTORY_SEPARATOR . $file->name; 1221 $sendFilePath = preg_replace("|^{$this->appConfig->s3storage2['basePath']}|", '', $alternativeFilePath); 1222 $this->logWithRequestId(__METHOD__ . ' - check alternative storage for file: ' . $alternativeFilePath . ' :: ' . (is_file($alternativeFilePath) ? 'true' : 'false')); 1223 if (is_file($alternativeFilePath)) { 1224 $this->_s3SendFile($sendFilePath, $alternativeFilePath, $fileName, $fileType, $fileSize, true, $headeronly, $this->appConfig->s3storage2); 1225 } 1226 } 1227 $this->logWithRequestId(__METHOD__ . ' - file not found. ' . $filePath, LOG_ERR); 1228 $this->response->setStatus(500); 1229 $this->_setResponseContent('error', array('message' => 'file not found' . " (id: {$this->getRequestId()})")); 1230 1231 return; 1232 } 1233 1234 $this->_sendFile($filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1235 } 1236 1237 /** 1238 * @param string $payloadHash 1239 * @param int $expires 1240 * 1241 * @return bool 1242 */ 1243 private function tooManyRequests(string $payloadHash, int $expires): bool { 1244 $ttl = intval($this->appConfig->redis['ttl']); 1245 if (0 < $expires) { 1246 $ttl = $expires; 1247 } 1248 $request = array( 1249 'count' => 1, 1250 'last_seen' => time(), 1251 ); 1252 if ($this->redisCache) { 1253 if ($this->redisCache->has($payloadHash)) { 1254 $request = $this->redisCache->get($payloadHash); 1255 if ($request['count'] > self::MAX_REQUEST_PER_MINUTE) { 1256 return true; 1257 } 1258 // Count (only) new requests made in last minute 1259 if ($request["last_seen"] >= time() - self::MIN_TIME) { 1260 $request['count'] += 1; 1261 } else { 1262 // restart timer 1263 $request['last_seen'] = time(); 1264 $request['count'] = 1; 1265 } 1266 } 1267 $this->redisCache->set($payloadHash, $request, $ttl); 1268 } 1269 1270 return false; 1271 } 1272 1273 /** 1274 * @param string $ip 1275 * @param int $expires 1276 * 1277 * @return bool 1278 */ 1279 private function tooManyRequestsFromIP(string $ip, int $expires): bool { 1280 $ttl = intval($this->appConfig->redis['ttl']); 1281 if (0 < $expires) { 1282 $ttl = $expires; 1283 } 1284 $request = array( 1285 'count' => 1, 1286 'last_seen' => time(), 1287 ); 1288 if ($this->redisCache) { 1289 if ($this->redisCache->has($ip)) { 1290 $request = $this->redisCache->get($ip); 1291 if ($request['count'] > self::MAX_REQUEST_PER_MINUTE) { 1292 return true; 1293 } 1294 // Count (only) new requests made in last minute 1295 if ($request["last_seen"] >= time() - self::MIN_TIME) { 1296 $request['count'] += 1; 1297 } else { 1298 // restart timer 1299 $request['last_seen'] = time(); 1300 $request['count'] = 1; 1301 } 1302 } 1303 $this->redisCache->set($ip, $request, $ttl); 1304 } 1305 1306 return false; 1307 } 1308 1309 /** 1310 * @param string $payloadHash 1311 * @param int $expires 1312 * 1313 * @return bool 1314 */ 1315 private function uniqueDownload(string $payloadHash, int $expires): bool 1316 { 1317 $ttl = (0 < $expires) ? intval($expires) : intval($this->appConfig->redis['ttl']); 1318 $keyName = __FUNCTION__ . ':' . $payloadHash; 1319 $count = 1; 1320 1321 if ($this->redisCache) { 1322 if ($this->redisCache->has($keyName)) { 1323 1324 return false; 1325 } 1326 $this->redisCache->set($keyName, $count, $ttl); 1327 } 1328 1329 return true; 1330 } 1331 1332 /** 1333 * @param string $sendFilePath 1334 * @param string $filePath 1335 * @param string $fileName 1336 * @param string $fileType 1337 * @param string $fileSize 1338 * @param bool $attachment 1339 * @param bool $headeronly 1340 */ 1341 private function _xSendFile(string $sendFilePath, 1342 string $filePath, 1343 string $fileName, 1344 string $fileType, 1345 string $fileSize, 1346 bool $attachment = false, 1347 bool $headeronly = false) 1348 { 1349 if (($headeronly) or (false == $this->appConfig->xsendfile['enabled'])) { 1350 $this->_sendFile($filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1351 } 1352 1353 $disposition = 'inline'; 1354 if ($attachment) { 1355 $disposition = 'attachment'; 1356 } 1357 1358 $this->response->setHeader('Content-Type', $fileType); 1359 $this->response->setHeader('Content-Length', $fileSize); 1360 $this->response->setHeader('Content-Disposition', $disposition . '; filename="' . $fileName . '"'); 1361 $path = $this->appConfig->xsendfile['pathPrefix'] . $sendFilePath; 1362 if (boolval($this->appConfig->awss3['enabled'])) { 1363 $signedUrl = $this->generateSignedDownloadUrl($sendFilePath, $this->appConfig->awss3); 1364 $path = $this->appConfig->awss3['pathPrefix'] . $signedUrl; 1365 } 1366 $this->logWithRequestId("Response (mod_accel_redirect: $path)", LOG_NOTICE); 1367 1368 $this->response->setHeader($this->appConfig->xsendfile['headerName'], $path); 1369 $this->response->send(); 1370 1371 if (php_sapi_name() == 'fpm-fcgi') { 1372 fastcgi_finish_request(); 1373 } 1374 1375 exit(); 1376 } 1377 1378 /** 1379 * @param string $sendFilePath 1380 * @param array|null $s3Config 1381 * @param bool $withoutHttpScheme 1382 * 1383 * @return string 1384 */ 1385 private function generateSignedDownloadUrl(string $sendFilePath, array $s3Config = null, 1386 bool $withoutHttpScheme = true): string { 1387 if (false == $s3Config['enabled']) { 1388 return $sendFilePath; 1389 } 1390 1391 // Instantiate an S3 client. 1392 if (empty($s3Config['endpoint'])) { 1393 $s3Client = new S3Client([ 1394 'credentials' => new Credentials($s3Config['key'], $s3Config['secret']), 1395 'version' => 'latest', 1396 'region' => $s3Config['region'], 1397 ]); 1398 } else { 1399 $s3Client = new S3Client([ 1400 'credentials' => new Credentials($s3Config['key'], $s3Config['secret']), 1401 'version' => 'latest', 1402 'region' => $s3Config['region'], 1403 'endpoint' => $s3Config['endpoint'], 1404 ]); 1405 } 1406 1407 // Creating a pre-signed URL and request 1408 $cmd = $s3Client->getCommand('GetObject', [ 1409 'Bucket' => $s3Config['bucket'], 1410 'Key' => $sendFilePath, 1411 'ResponseContentDisposition' => 'attachment;%20' . basename($sendFilePath), 1412 ]); 1413 $request = $s3Client->createPresignedRequest($cmd, $s3Config['signedUrlExpires']); 1414 1415 $uri = (string)$request->getUri(); 1416 1417 if (!empty($s3Config['cdn']) && !empty($s3Config['endpoint'])) { 1418 $host = preg_replace("(^https?://)", "", $s3Config['cdn']); 1419 $uri = (string)$request->getUri()->withHost($host); 1420 } 1421 1422 if ($withoutHttpScheme) { 1423 return preg_replace("(^https?://)", "", $uri); 1424 } 1425 1426 return $uri; 1427 } 1428 1429 private function _s3SendFile(string $sendFilePath, string $filePath, string $fileName, string $fileType, bool $fileSize, bool $attachment, $headeronly, array $appconfig) { 1430 if (($headeronly) or !$appconfig['enabled']) { 1431 $this->_sendFile($filePath, $fileName, $fileType, $fileSize, true, $headeronly); 1432 } 1433 $signedUrl = $this->generateSignedDownloadUrl($sendFilePath, $appconfig, false); 1434 $this->logWithRequestId("Response (redirect: $signedUrl)", LOG_NOTICE); 1435 1436 $this->response->setHeader('Location', $signedUrl); 1437 $this->response->setStatus(302); 1438 $this->response->send(); 1439 1440 if (php_sapi_name() == 'fpm-fcgi') { 1441 fastcgi_finish_request(); 1442 } 1443 1444 exit(); 1445 } 1446 1447 /** 1448 * @param bool $headeronly 1449 * 1450 * @throws Flooer_Exception 1451 * @deprecated 1452 */ 1453 public function getDownloadfile(bool $headeronly = false) // Deprecated 1454 { 1455 // This is alias for GET /files/download 1456 $this->getDownload($headeronly); 1457 } 1458 1459 public function optionsFile() 1460 { 1461 $response = $this->response; 1462 $response->setStatus(200); 1463 $this->response->send(); 1464 if (php_sapi_name() == 'fpm-fcgi') { 1465 fastcgi_finish_request(); 1466 } 1467 exit(); 1468 } 1469 1470 /** 1471 * @throws Flooer_Exception 1472 */ 1473 public function postUpload() 1474 { 1475 if (!$this->isValidSignedUrl()) { 1476 $this->response->setStatus(403); 1477 throw new Flooer_Exception("Forbidden", LOG_NOTICE); 1478 } 1479 1480 $errors = array(); 1481 if (!$this->request->client_id) { 1482 $errors['client_id'] = 'Required'; 1483 } 1484 if (!$this->request->owner_id) { 1485 $errors['owner_id'] = 'Required'; 1486 } 1487 /* 1488 if (!isset($_FILES['file'])) { 1489 $errors['file'] = 'Required'; 1490 } 1491 else if (!empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 1492 $errors['file'] = $_FILES['file']['error']; 1493 } 1494 */ 1495 // for hive files importing (Deprecated) ---------- 1496 if (!isset($_FILES['file']) && !isset($this->request->local_file_path)) { 1497 $errors['file'] = 'Required'; 1498 } 1499 if (isset($_FILES['file']) && !empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 1500 $errors['file'] = $_FILES['file']['error']; 1501 } 1502 // ------------------------------------------------ 1503 1504 if ($errors) { 1505 $this->response->setStatus(400); 1506 $this->_setResponseContent('error', array('message' => 'Validation error', 1507 'errors' => $errors,)); 1508 1509 return; 1510 } 1511 1512 $file = $this->processFileUpload(); 1513 1514 $this->_setResponseContent('success', array('file' => $file)); 1515 } 1516 1517 /** 1518 * @return bool 1519 */ 1520 private function isValidSignedUrl(): bool 1521 { 1522 $result = false; 1523 if (!empty($this->request->client_id)) { 1524 $clients = parse_ini_file('configs/clients.ini', true); 1525 $url = $this->getScheme() . '://' . $this->getHost() . $this->request->getUri(); 1526 $this->logWithRequestId(__METHOD__ . ' - ' . print_r($_SERVER, true), LOG_NOTICE); 1527 if (isset($clients[$this->request->client_id]) && (UrlSigner::verifySignedUrl($url, $clients[$this->request->client_id]['secret']))) { 1528 $result = true; 1529 } 1530 } 1531 $this->logWithRequestId(__METHOD__ . ' - verify signature for $url: ' . ($url ? $url : '(url is empty caused by missing client_id)') . ' :: ' . ($result?'true':'false'), LOG_NOTICE); 1532 1533 return $result; 1534 } 1535 1536 public function optionsUpload() 1537 { 1538 $response = $this->response; 1539 $response->setStatus(200); 1540 $this->response->send(); 1541 if (php_sapi_name() == 'fpm-fcgi') { 1542 fastcgi_finish_request(); 1543 } 1544 exit(); 1545 } 1546 1547 /** 1548 * @throws Flooer_Exception 1549 */ 1550 public function putUpload() 1551 { 1552 if (!$this->isValidSignedUrl()) { 1553 $this->response->setStatus(403); 1554 throw new Flooer_Exception("Forbidden", LOG_NOTICE); 1555 } 1556 1557 1558 $errors = array(); 1559 if (!empty($_FILES['file']['error'])) { // 0 = UPLOAD_ERR_OK 1560 $errors['file'] = $_FILES['file']['error']; 1561 } 1562 if ($errors) { 1563 $this->response->setStatus(400); 1564 $this->_setResponseContent('error', array('message' => 'File upload error', 1565 'errors' => $errors,)); 1566 1567 return; 1568 } 1569 $errors = array(); 1570 if (!$this->request->id) { 1571 $errors['id'] = 'Required'; 1572 } 1573 if (!$this->request->client_id) { 1574 $errors['client_id'] = 'Required'; 1575 } 1576 if ($errors) { 1577 $this->response->setStatus(400); 1578 $this->_setResponseContent('error', array('message' => 'Validation error', 1579 'errors' => $errors,)); 1580 1581 return; 1582 } 1583 1584 $file = $this->processFileUpdate(); 1585 1586 $this->_setResponseContent('success', array('file' => $file)); 1587 } 1588 }