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 }