<?php
/**
* @file classes/services/PKPFileService.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPFileService
*
* @ingroup services
*
* @brief Helper class that encapsulates business logic for publications
*/
namespace PKP\services;
use APP\core\Application;
use Exception;
use finfo;
use Illuminate\Support\Facades\DB;
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use PKP\config\Config;
use PKP\core\PKPString;
use PKP\file\FileManager;
use PKP\plugins\Hook;
class PKPFileService
{
private const FALLBACK_MIME_TYPE = 'application/octet-stream';
/** @var Filesystem */
public $fs;
/**
* Initialize and configure flysystem
*/
public function __construct()
{
$umask = Config::getVar('files', 'umask', 0022);
$adapter = new LocalFilesystemAdapter(
Config::getVar('files', 'files_dir'),
PortableVisibilityConverter::fromArray([
'file' => [
'public' => FileManager::FILE_MODE_MASK & ~$umask,
'private' => FileManager::FILE_MODE_MASK & ~$umask,
],
'dir' => [
'public' => FileManager::DIRECTORY_MODE_MASK & ~$umask,
'private' => FileManager::DIRECTORY_MODE_MASK & ~$umask,
]
]),
LOCK_EX,
LocalFilesystemAdapter::DISALLOW_LINKS
);
Hook::call('File::adapter', [&$adapter, $this]);
$this->fs = new Filesystem($adapter);
}
/**
* Get a file by its id
*
* @param int $id
*
* @return object
*/
public function get($id)
{
$file = DB::table('files')
->where('file_id', '=', $id)
->select(['file_id as id', 'path', 'mimetype'])
->first();
return $file;
}
/**
* Add a file
*
* @param string $from absolute path to file
* @param string $to relative path in file dir
*
* @return int file id
*/
public function add($from, $to)
{
$stream = fopen($from, 'r+');
if (!$stream) {
throw new Exception("Unable to copy {$from} to {$to}.");
}
$this->fs->writeStream($to, $stream);
if (is_resource($stream)) {
fclose($stream);
}
try {
$mimetype = $this->fs->mimeType($to);
} catch (Exception $e) {
// When a very good mime-type cannot be guessed, FlySystem emits an Exception
$mimetype = (new finfo(FILEINFO_MIME_TYPE))->file($to) ?: static::FALLBACK_MIME_TYPE;
}
// Check and override ambiguous mime types based on file extension
if ($extension = pathinfo($to, PATHINFO_EXTENSION)) {
$checkAmbiguous = strtolower($extension . ':' . $mimetype);
if (array_key_exists($checkAmbiguous, $extensionsMap = PKPString::getAmbiguousExtensionsMap())) {
$mimetype = $extensionsMap[$checkAmbiguous];
}
}
return DB::table('files')->insertGetId([
'path' => $to,
'mimetype' => $mimetype,
], 'file_id');
}
/**
* Delete an uploaded file
*
* @param int $id
*/
public function delete($id)
{
$file = $this->get($id);
if (!$file) {
throw new Exception("Unable to locate file {$id}.");
}
$path = $file->path;
if ($this->fs->has($path)) {
try {
$this->fs->delete($path);
} catch (Exception $e) {
throw new Exception("Unable to delete file {$id} at {$path}.");
}
}
DB::table('files')
->where('file_id', '=', $file->id)
->delete();
}
/**
* Download a file
*
* This method sends a HTTP response and ends the request handling.
* No code will run after this method is called.
*
* @param int $fileId File ID
* @param string $filename Filename to give to the downloaded file
* @param bool $inline Whether to stream the file to the browser
*/
public function download($fileId, $filename, $inline = false)
{
$file = $this->get($fileId);
$dispatcher = Application::get()->getRequest()->getDispatcher();
if (!$file) {
$dispatcher->handle404();
}
$path = $file->path;
if (!$this->fs->has($path)) {
$dispatcher->handle404();
}
if (Hook::call('File::download', [$file, &$filename, $inline])) {
return;
}
// Stream the file to the end user.
$mimetype = $file->mimetype ?? 'application/octet-stream';
$filesize = $this->fs->fileSize($path);
$encodedFilename = urlencode($filename);
header("Content-Type: {$mimetype}");
header("Content-Length: {$filesize}");
header('Accept-Ranges: none');
header('Content-Disposition: ' . ($inline ? 'inline' : 'attachment') . ";filename=\"{$encodedFilename}\";filename*=UTF-8''{$encodedFilename}");
header('Cache-Control: private'); // Workarounds for IE weirdness
header('Pragma: public');
fpassthru($this->fs->readStream($path));
exit;
}
/**
* Convert a filename into a consistent format with the correct extension
*
* @param string $path Path to the file
* @param string $filename Source filename to sanitize
*
* @return string
*/
public function formatFilename($path, $filename)
{
$newFilename = $filename;
# pattern extended to also capture captures .tar.gz extensions
if (preg_match('/(\\.\\w{1,3})?\\.\\w+$/', $path, $extension)) {
# If $newFilename has no/not the correct extension: Append extension
if (strcasecmp(substr($newFilename, (strlen($extension[0]) * -1)), $extension[0]) != 0) {
$newFilename .= $extension[0];
}
}
Hook::call('File::formatFilename', [&$newFilename, $path, $filename]);
return $newFilename;
}
/**
* Get document type based on the mimetype
*
* @param string $mimetype
*
* @return string One of the FileManager::DOCUMENT_TYPE_ constants
*/
public function getDocumentType($mimetype)
{
switch ($mimetype) {
case 'application/pdf':
case 'application/x-pdf':
case 'text/pdf':
case 'text/x-pdf':
return FileManager::DOCUMENT_TYPE_PDF;
case 'application/msword':
case 'application/word':
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return FileManager::DOCUMENT_TYPE_WORD;
case 'application/excel':
case 'application/vnd.ms-excel':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return FileManager::DOCUMENT_TYPE_EXCEL;
case 'text/html':
return FileManager::DOCUMENT_TYPE_HTML;
case 'application/zip':
case 'application/x-zip':
case 'application/x-zip-compressed':
case 'application/x-compress':
case 'application/x-compressed':
case 'multipart/x-zip':
return FileManager::DOCUMENT_TYPE_ZIP;
case 'application/epub':
case 'application/epub+zip':
return FileManager::DOCUMENT_TYPE_EPUB;
case 'image/gif':
case 'image/jpeg':
case 'image/pjpeg':
case 'image/png':
case 'image/x-png':
case 'image/vnd.microsoft.icon':
case 'image/x-icon':
case 'image/x-ico':
case 'image/ico':
return FileManager::DOCUMENT_TYPE_IMAGE;
case 'application/x-shockwave-flash':
case 'video/x-flv':
case 'application/x-flash-video':
case 'flv-application/octet-stream':
case 'video/mpeg':
case 'video/quicktime':
case 'video/mp4':
return FileManager::DOCUMENT_TYPE_VIDEO;
case 'audio/mpeg':
case 'audio/x-aiff':
case 'audio/x-wav':
return FileManager::DOCUMENT_TYPE_AUDIO;
default:
return FileManager::DOCUMENT_TYPE_DEFAULT;
}
}
/**
* Get a pretty file size string
*
* Examples: 82B, 12KB, 2MB, 2GB
*
* @param int $size File size in bytes
*
* @return string
*/
public function getNiceFileSize($size)
{
$niceFileSizeUnits = ['B', 'KB', 'MB', 'GB'];
for ($i = 0; $i < 4 && $size > 1024; $i++) {
$size >>= 10;
}
return $size . $niceFileSizeUnits[$i];
}
}
|