2020-05-08 22:53:21 +00:00
|
|
|
<?php
|
|
|
|
namespace EEPROM;
|
|
|
|
|
|
|
|
use Exception;
|
2020-05-30 23:49:59 +00:00
|
|
|
use Imagick;
|
2020-05-08 22:53:21 +00:00
|
|
|
use JsonSerializable;
|
|
|
|
|
|
|
|
class UploadNotFoundException extends Exception {};
|
|
|
|
class UploadCreationFailedException extends Exception {};
|
|
|
|
|
|
|
|
final class Upload implements JsonSerializable {
|
|
|
|
private const ID_CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789-_';
|
|
|
|
|
|
|
|
public function getId(): string {
|
|
|
|
return $this->upload_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPath(): string {
|
|
|
|
return PRM_UPLOADS . '/' . $this->getId();
|
|
|
|
}
|
|
|
|
public function getThumbPath(): string {
|
|
|
|
return PRM_THUMBS . '/' . $this->getId();
|
|
|
|
}
|
|
|
|
public function getRemotePath(): string {
|
|
|
|
return '/uploads/' . $this->getId();
|
|
|
|
}
|
|
|
|
public function getPublicUrl(bool $forceReal = false): string {
|
|
|
|
if(!$forceReal && Config::has('Uploads', 'short_domain'))
|
|
|
|
return '//' . Config::get('Uploads', 'short_domain') . '/' . $this->getId();
|
|
|
|
return '//' . $_SERVER['HTTP_HOST'] . $this->getRemotePath();
|
|
|
|
}
|
|
|
|
public function getPublicThumbUrl(bool $forceReal = false): string {
|
|
|
|
return $this->getPublicUrl($forceReal) . '.t';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getUserId(): int {
|
|
|
|
return $this->user_id ?? 0;
|
|
|
|
}
|
|
|
|
public function setUserId(int $userId): void {
|
|
|
|
$this->user_id = $userId;
|
|
|
|
}
|
|
|
|
public function getUser(): User {
|
|
|
|
return User::byId($this->getUserId());
|
|
|
|
}
|
|
|
|
public function setUser(User $user): void {
|
|
|
|
$this->setUserId($user->getId());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getApplicationId(): int {
|
|
|
|
return $this->app_id ?? 0;
|
|
|
|
}
|
|
|
|
public function setApplicationId(int $appId): void {
|
|
|
|
$this->app_id = $appId;
|
|
|
|
|
|
|
|
$updateAppl = DB::prepare('
|
|
|
|
UPDATE `prm_uploads`
|
|
|
|
SET `app_id` = :app
|
|
|
|
WHERE `upload_id` = :upload
|
|
|
|
');
|
|
|
|
$updateAppl->bindValue('app', $appId);
|
|
|
|
$updateAppl->bindValue('upload', $this->getId());
|
|
|
|
$updateAppl->execute();
|
|
|
|
}
|
|
|
|
public function getApplication(): User {
|
|
|
|
return Application::byId($this->getApplicationId());
|
|
|
|
}
|
|
|
|
public function setApplication(Application $app): void {
|
|
|
|
$this->setApplicationId($app->getId());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getType(): string {
|
|
|
|
return $this->upload_type ?? 'text/plain';
|
|
|
|
}
|
2020-05-30 23:49:59 +00:00
|
|
|
public function isImage(): bool {
|
|
|
|
return substr($this->getType(), 0, 6) === 'image/';
|
|
|
|
}
|
|
|
|
public function isVideo(): bool {
|
|
|
|
return substr($this->getType(), 0, 6) === 'video/';
|
|
|
|
}
|
|
|
|
public function isAudio(): bool {
|
|
|
|
return substr($this->getType(), 0, 6) === 'audio/';
|
|
|
|
}
|
2020-05-08 22:53:21 +00:00
|
|
|
|
|
|
|
public function getName(): string {
|
|
|
|
return $this->upload_name ?? '';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSize(): int {
|
|
|
|
return $this->upload_size ?? 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getHash(): string {
|
|
|
|
return $this->upload_hash ?? str_pad('', 64, '0');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCreated(): int {
|
|
|
|
return $this->upload_created;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getLastAccessed(): int {
|
|
|
|
return $this->upload_accessed ?? 0;
|
|
|
|
}
|
|
|
|
public function bumpAccess(): void {
|
|
|
|
if(empty($this->getId()))
|
|
|
|
return;
|
|
|
|
$this->upload_accessed = time();
|
|
|
|
$bumpAccess = DB::prepare('
|
|
|
|
UPDATE `prm_uploads`
|
|
|
|
SET `upload_accessed` = NOW()
|
|
|
|
WHERE `upload_id` = :upload
|
|
|
|
');
|
|
|
|
$bumpAccess->bindValue('upload', $this->getId());
|
|
|
|
$bumpAccess->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getExpires(): int {
|
|
|
|
return $this->upload_expires ?? 0;
|
|
|
|
}
|
|
|
|
public function hasExpired(): bool {
|
|
|
|
return $this->getExpires() > 1 && $this->getExpires() <= time();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getDeleted(): int {
|
|
|
|
return $this->upload_deleted ?? 0;
|
|
|
|
}
|
|
|
|
public function isDeleted(): bool {
|
|
|
|
return $this->getDeleted() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getDMCA(): int {
|
|
|
|
return $this->upload_dmca ?? 0;
|
|
|
|
}
|
|
|
|
public function isDMCA(): bool {
|
|
|
|
return $this->getDMCA() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getExpiryBump(): int {
|
|
|
|
return $this->upload_bump ?? 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function bumpExpiry(): void {
|
|
|
|
if(empty($this->getId()) || $this->getExpires() < 1)
|
|
|
|
return;
|
|
|
|
$bumpSeconds = $this->getExpiryBump();
|
|
|
|
|
|
|
|
if($bumpSeconds < 1)
|
|
|
|
return;
|
|
|
|
$this->upload_expires = time() + $bumpSeconds;
|
|
|
|
|
|
|
|
$bumpExpiry = DB::prepare('
|
|
|
|
UPDATE `prm_uploads`
|
|
|
|
SET `upload_expires` = NOW() + INTERVAL :seconds SECOND
|
|
|
|
WHERE `upload_id` = :upload
|
|
|
|
');
|
|
|
|
$bumpExpiry->bindValue('seconds', $bumpSeconds);
|
|
|
|
$bumpExpiry->bindValue('upload', $this->getId());
|
|
|
|
$bumpExpiry->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function restore(): void {
|
|
|
|
$this->upload_deleted = null;
|
|
|
|
$restore = DB::prepare('
|
|
|
|
UPDATE `prm_uploads`
|
|
|
|
SET `upload_deleted` = NULL
|
|
|
|
WHERE `upload_id` = :id
|
|
|
|
');
|
|
|
|
$restore->bindValue('id', $this->getId());
|
|
|
|
$restore->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function delete(bool $hard): void {
|
|
|
|
$this->upload_deleted = time();
|
|
|
|
|
|
|
|
if($hard) {
|
|
|
|
if(is_file($this->getPath()))
|
|
|
|
unlink($this->getPath());
|
|
|
|
if(is_file($this->getThumbPath()))
|
|
|
|
unlink($this->getThumbPath());
|
|
|
|
|
|
|
|
if($this->getDMCA() < 1) {
|
|
|
|
$delete = DB::prepare('
|
|
|
|
DELETE FROM `prm_uploads`
|
|
|
|
WHERE `upload_id` = :id
|
|
|
|
');
|
|
|
|
$delete->bindValue('id', $this->getId());
|
|
|
|
$delete->execute();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$delete = DB::prepare('
|
|
|
|
UPDATE `prm_uploads`
|
|
|
|
SET `upload_deleted` = NOW()
|
|
|
|
WHERE `upload_id` = :id
|
|
|
|
');
|
|
|
|
$delete->bindValue('id', $this->getId());
|
|
|
|
$delete->execute();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-05 14:02:49 +00:00
|
|
|
public function jsonSerialize(): mixed {
|
2020-05-08 22:53:21 +00:00
|
|
|
return [
|
|
|
|
'id' => $this->getId(),
|
|
|
|
'url' => $this->getPublicUrl(),
|
|
|
|
'urlf' => $this->getPublicUrl(true),
|
|
|
|
'thumb' => $this->getPublicThumbUrl(),
|
|
|
|
'name' => $this->getName(),
|
|
|
|
'type' => $this->getType(),
|
|
|
|
'size' => $this->getSize(),
|
|
|
|
'user' => $this->getUserId(),
|
|
|
|
'appl' => $this->getApplicationId(),
|
|
|
|
'hash' => $this->getHash(),
|
|
|
|
'created' => date('c', $this->getCreated()),
|
|
|
|
'accessed' => $this->getLastAccessed() < 1 ? null : date('c', $this->getLastAccessed()),
|
|
|
|
'expires' => $this->getExpires() < 1 ? null : date('c', $this->getExpires()),
|
|
|
|
'deleted' => $this->getDeleted() < 1 ? null : date('c', $this->getDeleted()),
|
|
|
|
'dmca' => $this->getDMCA() < 1 ? null : date('c', $this->getDMCA()),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function generateId(int $length = 32): string {
|
|
|
|
$token = random_bytes($length);
|
|
|
|
$chars = strlen(self::ID_CHARS);
|
|
|
|
|
|
|
|
for($i = 0; $i < $length; $i++)
|
|
|
|
$token[$i] = self::ID_CHARS[ord($token[$i]) % $chars];
|
|
|
|
|
|
|
|
return $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function create(
|
|
|
|
Application $app, User $user,
|
|
|
|
string $fileName, string $fileType,
|
|
|
|
string $fileSize, string $fileHash,
|
|
|
|
int $fileExpiry, bool $bumpExpiry
|
|
|
|
): self {
|
|
|
|
$appId = $app->getId();
|
|
|
|
$userId = $user->getId();
|
|
|
|
|
|
|
|
if(strpos($fileType, '/') === false || $fileSize < 1 || strlen($fileHash) !== 64 || $fileExpiry < 0)
|
|
|
|
throw new UploadCreationFailedException('Bad args.');
|
|
|
|
|
|
|
|
$id = self::generateId();
|
|
|
|
$create = DB::prepare('
|
|
|
|
INSERT INTO `prm_uploads` (
|
|
|
|
`upload_id`, `app_id`, `user_id`, `upload_name`,
|
|
|
|
`upload_type`, `upload_size`, `upload_hash`, `upload_ip`,
|
|
|
|
`upload_expires`, `upload_bump`
|
|
|
|
) VALUES (
|
|
|
|
:id, :app, :user, :name, :type, :size,
|
|
|
|
UNHEX(:hash), INET6_ATON(:ip),
|
|
|
|
FROM_UNIXTIME(:expire), :bump
|
|
|
|
)
|
|
|
|
');
|
|
|
|
$create->bindValue('id', $id);
|
|
|
|
$create->bindValue('app', $appId < 1 ? null : $appId);
|
|
|
|
$create->bindValue('user', $userId < 1 ? null : $userId);
|
|
|
|
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
|
|
|
$create->bindValue('name', $fileName);
|
|
|
|
$create->bindValue('type', $fileType);
|
|
|
|
$create->bindValue('hash', $fileHash);
|
|
|
|
$create->bindValue('size', $fileSize);
|
|
|
|
$create->bindValue('expire', $fileExpiry > 0 ? (time() + $fileExpiry) : 0);
|
|
|
|
$create->bindValue('bump', $bumpExpiry ? $fileExpiry : 0);
|
|
|
|
$create->execute();
|
|
|
|
|
|
|
|
try {
|
|
|
|
return self::byId($id);
|
|
|
|
} catch(UploadNotFoundException $ex) {
|
|
|
|
throw new UploadCreationFailedException;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function byId(string $id): self {
|
|
|
|
$getUpload = DB::prepare('
|
|
|
|
SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,
|
|
|
|
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
|
|
|
UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,
|
|
|
|
UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,
|
|
|
|
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
|
|
|
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
|
|
|
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
|
|
|
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
|
|
|
FROM `prm_uploads`
|
|
|
|
WHERE `upload_id` = :id
|
|
|
|
AND `upload_deleted` IS NULL
|
|
|
|
');
|
|
|
|
$getUpload->bindValue('id', $id);
|
|
|
|
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
|
|
|
|
|
|
|
|
if(!$upload)
|
|
|
|
throw new UploadNotFoundException;
|
|
|
|
|
|
|
|
return $upload;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function byHash(string $hash): ?self {
|
|
|
|
$getUpload = DB::prepare('
|
|
|
|
SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,
|
|
|
|
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
|
|
|
UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,
|
|
|
|
UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,
|
|
|
|
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
|
|
|
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
|
|
|
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
|
|
|
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
|
|
|
FROM `prm_uploads`
|
|
|
|
WHERE `upload_hash` = UNHEX(:hash)
|
|
|
|
');
|
|
|
|
$getUpload->bindValue('hash', $hash);
|
|
|
|
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
|
|
|
|
return $upload ? $upload : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function deleted(): array {
|
|
|
|
$getDeleted = DB::prepare('
|
|
|
|
SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,
|
|
|
|
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
|
|
|
UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,
|
|
|
|
UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,
|
|
|
|
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
|
|
|
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
|
|
|
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
|
|
|
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
|
|
|
FROM `prm_uploads`
|
|
|
|
WHERE `upload_deleted` IS NOT NULL
|
|
|
|
OR `upload_dmca` IS NOT NULL
|
|
|
|
OR `user_id` IS NULL
|
|
|
|
OR `app_id` IS NULL
|
|
|
|
');
|
|
|
|
if(!$getDeleted->execute())
|
|
|
|
return [];
|
|
|
|
|
|
|
|
$deleted = [];
|
|
|
|
|
|
|
|
while($upload = $getDeleted->fetchObject(self::class))
|
|
|
|
$deleted[] = $upload;
|
|
|
|
|
|
|
|
return $deleted;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function expired(): array {
|
|
|
|
$getExpired = DB::prepare('
|
|
|
|
SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,
|
|
|
|
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
|
|
|
UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,
|
|
|
|
UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,
|
|
|
|
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
|
|
|
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
|
|
|
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
|
|
|
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
|
|
|
FROM `prm_uploads`
|
|
|
|
WHERE `upload_expires` IS NOT NULL
|
|
|
|
AND `upload_expires` <= NOW()
|
|
|
|
AND `upload_dmca` IS NULL
|
|
|
|
');
|
|
|
|
if(!$getExpired->execute())
|
|
|
|
return [];
|
|
|
|
|
|
|
|
$deleted = [];
|
|
|
|
|
|
|
|
while($upload = $getExpired->fetchObject(self::class))
|
|
|
|
$deleted[] = $upload;
|
|
|
|
|
|
|
|
return $deleted;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function all(int $limit = 0, string $after = ''): array {
|
|
|
|
$query = '
|
|
|
|
SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,
|
|
|
|
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
|
|
|
UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,
|
|
|
|
UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,
|
|
|
|
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
|
|
|
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
|
|
|
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
|
|
|
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
|
|
|
FROM `prm_uploads`
|
|
|
|
';
|
|
|
|
|
|
|
|
if(!empty($after))
|
|
|
|
$query .= ' WHERE `upload_id` > :after';
|
|
|
|
|
|
|
|
$query .= ' ORDER BY `upload_id`';
|
|
|
|
|
|
|
|
if($limit > 0)
|
|
|
|
$query .= sprintf(' LIMIT %d', $limit);
|
|
|
|
|
|
|
|
$getUploads = DB::prepare($query);
|
|
|
|
|
|
|
|
if(!empty($after))
|
|
|
|
$getUploads->bindValue('after', $after);
|
|
|
|
|
|
|
|
$getUploads->execute();
|
|
|
|
$out = [];
|
|
|
|
|
|
|
|
while($upload = $getUploads->fetchObject(self::class))
|
|
|
|
$out[] = $upload;
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
}
|
2020-05-30 23:49:59 +00:00
|
|
|
|
|
|
|
public function supportsThumbnail(): bool {
|
|
|
|
return $this->isImage() || $this->isAudio() || $this->isVideo();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function createThumbnail(): void {
|
|
|
|
$tmpFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'eeprom' . bin2hex(random_bytes(10)) . '.jpg';
|
|
|
|
|
|
|
|
try {
|
|
|
|
if($this->isImage())
|
|
|
|
$imagick = new Imagick($this->getPath());
|
|
|
|
elseif($this->isAudio())
|
|
|
|
$imagick = $this->getCoverFromAudio($tmpFile);
|
|
|
|
elseif($this->isVideo())
|
|
|
|
$imagick = $this->getFrameFromVideo($tmpFile);
|
|
|
|
|
|
|
|
if(!isset($imagick))
|
|
|
|
return;
|
|
|
|
|
|
|
|
$imagick->setImageFormat('jpg');
|
|
|
|
$imagick->setImageCompressionQuality(40);
|
|
|
|
|
|
|
|
$thumbRes = 100;
|
|
|
|
$width = $imagick->getImageWidth();
|
|
|
|
$height = $imagick->getImageHeight();
|
|
|
|
|
|
|
|
if ($width > $height) {
|
|
|
|
$resizeWidth = $width * $thumbRes / $height;
|
|
|
|
$resizeHeight = $thumbRes;
|
|
|
|
} else {
|
|
|
|
$resizeWidth = $thumbRes;
|
|
|
|
$resizeHeight = $height * $thumbRes / $width;
|
|
|
|
}
|
|
|
|
|
|
|
|
$imagick->resizeImage(
|
|
|
|
$resizeWidth, $resizeHeight,
|
|
|
|
Imagick::FILTER_GAUSSIAN, 0.7
|
|
|
|
);
|
|
|
|
|
|
|
|
$imagick->cropImage(
|
|
|
|
$thumbRes,
|
|
|
|
$thumbRes,
|
|
|
|
($resizeWidth - $thumbRes) / 2,
|
|
|
|
($resizeHeight - $thumbRes) / 2
|
|
|
|
);
|
|
|
|
|
|
|
|
$imagick->writeImage($this->getThumbPath());
|
|
|
|
} catch(Exception $ex) {}
|
|
|
|
|
|
|
|
if(is_file($tmpFile))
|
|
|
|
unlink($tmpFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getFrameFromVideo(string $path): Imagick {
|
|
|
|
shell_exec(sprintf('ffmpeg -i %s -ss 00:00:01.000 -vframes 1 %s', $this->getPath(), $path));
|
|
|
|
return new Imagick($path);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getCoverFromAudio(string $path): Imagick {
|
|
|
|
shell_exec(sprintf('ffmpeg -i %s -an -vcodec copy %s', $this->getPath(), $path));
|
|
|
|
return new Imagick($path);
|
|
|
|
}
|
2020-05-08 22:53:21 +00:00
|
|
|
}
|