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'; } 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/'; } 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(); } } public function jsonSerialize() { 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; } 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); } }