From 1a2a0805d4f661f582d037e23d56c0a896d02a21 Mon Sep 17 00:00:00 2001 From: Celestora Date: Tue, 29 Mar 2022 20:43:34 +0300 Subject: [PATCH] Add photos.save, photos.saveWallPhoto, photos.saveOwnerPhoto, photos.getUploadServer Didn't test, but shouldn't be really tough to fix afterwards if it'll break. --- .gitignore | 2 +- VKAPI/Handlers/Photos.php | 230 ++++++++++++++++++++++++++ Web/Models/Entities/Photo.php | 64 ++++--- Web/Presenters/UserPresenter.php | 1 - Web/Presenters/VKAPIPresenter.php | 86 ++++++++++ Web/routes.yml | 2 + openvk-example.yml | 3 + tmp/{ => api-storage/audios}/.gitkeep | 0 tmp/api-storage/photos/.gitkeep | 0 tmp/api-storage/videos/.gitkeep | 0 10 files changed, 367 insertions(+), 21 deletions(-) create mode 100644 VKAPI/Handlers/Photos.php rename tmp/{ => api-storage/audios}/.gitkeep (100%) create mode 100644 tmp/api-storage/photos/.gitkeep create mode 100644 tmp/api-storage/videos/.gitkeep diff --git a/.gitignore b/.gitignore index 28edaa42..b3bb2167 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ update.pid.old Web/static/js/node_modules tmp/* -!tmp/.gitkeep +!tmp/api-storage !tmp/themepack_artifacts/.gitkeep themepacks/* !themepacks/.gitkeep diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php new file mode 100644 index 00000000..9a02dd8c --- /dev/null +++ b/VKAPI/Handlers/Photos.php @@ -0,0 +1,230 @@ +getUser()->getId(), + $group, + 0, # this is unused but stays here base64 reasons (X2 doesn't work, so there's dummy value for short) + ]; + $uploadInfo = pack("vZ10v2P3S", ...$uploadInfo); + $uploadInfo = base64_encode($uploadInfo); + $uploadHash = hash_hmac("sha3-224", $uploadInfo, $secret); + $uploadInfo = rawurlencode($uploadInfo); + + return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/upload/photo/$uploadHash?$uploadInfo"; + } + + private function getImagePath(string $photo, string $hash, ?string& $up = NULL, ?string& $group = NULL): string + { + $secret = CHANDLER_ROOT_CONF["security"]["secret"]; + if(!hash_equals(hash_hmac("sha3-224", $photo, $secret), $hash)) + $this->fail(121, "Incorrect hash"); + + [$up, $image, $group] = explode("|", $photo); + + $imagePath = __DIR__ . "/../../tmp/api-storage/photos/$up" . "_$image.oct"; + if(!file_exists($imagePath)) + $this->fail(10, "Invalid image"); + + return $imagePath; + } + + function getOwnerPhotoUploadServer(int $owner_id = 0): object + { + $this->requireUser(); + + if($owner_id < 0) { + $club = (new Clubs)->get(abs($owner_id)); + if(!$club) + $this->fail(0404, "Club not found"); + else if(!$club->canBeModifiedBy($this->getUser())) + $this->fail(200, "Access: Club can't be 'written' by user"); + } + + return (object) [ + "upload_url" => $this->getPhotoUploadUrl("photo", isset($club) ? 0 : $club->getId()), + ]; + } + + function saveOwnerPhoto(string $photo, string $hash): object + { + $imagePath = $this->getImagePath($photo, $hash, $uploader, $group); + if($group == 0) { + $user = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader); + $album = (new Albums)->getUserAvatarAlbum($user); + } else { + $club = (new Clubs)->get((int) $group); + $album = (new Albums)->getClubAvatarAlbum($club); + } + + try { + $avatar = new Photo; + $avatar->setOwner((int) $uploader); + $avatar->setDescription("Profile photo"); + $avatar->setCreated(time()); + $avatar->setFile([ + "tmp_name" => $imagePath, + "error" => 0, + ]); + $avatar->save(); + $album->addPhoto($avatar); + unlink($imagePath); + } catch(ImageException | InvalidStateException $e) { + unlink($imagePath); + $this->fail(129, "Invalid image file"); + } + + return (object) [ + "photo_hash" => NULL, + "photo_src" => $avatar->getURL(), + ]; + } + + function getWallUploadServer(?int $group_id = NULL): object + { + $this->requireUser(); + + $album = NULL; + if(!is_null($group_id)) { + $club = (new Clubs)->get(abs($group_id)); + if(!$club) + $this->fail(0404, "Club not found"); + else if(!$club->canBeModifiedBy($this->getUser())) + $this->fail(200, "Access: Club can't be 'written' by user"); + } else { + $album = (new Albums)->getUserWallAlbum($this->getUser()); + } + + return (object) [ + "upload_url" => $this->getPhotoUploadUrl("photo", $group_id ?? 0), + "album_id" => $album, + "user_id" => $this->getUser()->getId(), + ]; + } + + function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = NULL): array + { + $imagePath = $this->getImagePath($photo, $hash, $uploader, $group); + if($group_id != $group) + $this->fail(8, "group_id doesn't match"); + + $album = NULL; + if($group_id != 0) { + $uploader = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader); + $album = (new Albums)->getUserWallAlbum($uploader); + } + + try { + $photo = new Photo; + $photo->setOwner((int) $uploader); + $photo->setCreated(time()); + $photo->setFile([ + "tmp_name" => $imagePath, + "error" => 0, + ]); + + if (!is_null($caption)) + $photo->setDescription($caption); + + $photo->save(); + unlink($imagePath); + } catch(ImageException | InvalidStateException $e) { + unlink($imagePath); + $this->fail(129, "Invalid image file"); + } + + if(!is_null($album)) + $album->addPhoto($photo); + + return [ + $photo->toVkApiStruct(), + ]; + } + + function getUploadServer(?int $album_id = NULL): object + { + $this->requireUser(); + + # Not checking rights to album because save() method will do so anyways + return (object) [ + "upload_url" => $this->getPhotoUploadUrl("photo", 0, true), + "album_id" => $album_id, + "user_id" => $this->getUser()->getId(), + ]; + } + + function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = NULL): object + { + $this->requireUser(); + + $secret = CHANDLER_ROOT_CONF["security"]["secret"]; + if(!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash)) + $this->fail(121, "Incorrect hash"); + + $album = NULL; + if($album_id != 0) { + $album_ = (new Albums)->get($album_id); + if(!$album_) + $this->fail(0404, "Invalid album"); + else if(!$album_->canBeModifiedBy($this->getUser())) + $this->fail(15, "Access: Album can't be 'written' by user"); + + $album = $album_; + } + + $pList = json_decode($photos_list); + $imagePaths = []; + foreach($pList as $pDesc) + $imagePaths[] = __DIR__ . "/../../tmp/api-storage/photos/$pDesc->keyholder" . "_$pDesc->resource.oct"; + + $images = []; + try { + foreach($imagePaths as $imagePath) { + $photo = new Photo; + $photo->setOwner($this->getUser()->getId()); + $photo->setCreated(time()); + $photo->setFile([ + "tmp_name" => $imagePath, + "error" => 0, + ]); + + if (!is_null($caption)) + $photo->setDescription($caption); + + $photo->save(); + unlink($imagePath); + + if(!is_null($album)) + $album->addPhoto($photo); + + $images[] = $photo->toVkApiStruct(); + } + } catch(ImageException | InvalidStateException $e) { + foreach($imagePaths as $imagePath) + unlink($imagePath); + + $this->fail(129, "Invalid image file"); + } + + return (object) [ + "count" => sizeof($images), + "items" => $images, + ]; + } +} \ No newline at end of file diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index 0c0e8941..75f06805 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -12,7 +12,7 @@ class Photo extends Media protected $fileExtension = "jpeg"; const ALLOWED_SIDE_MULTIPLIER = 7; - + protected function saveFile(string $filename, string $hash): bool { $image = Image::fromFile($filename); @@ -24,7 +24,7 @@ class Photo extends Media return true; } - function crop(real $left, real $top, real $width, real $height): bool + function crop(real $left, real $top, real $width, real $height): void { if(isset($this->changes["hash"])) $hash = $this->changes["hash"]; @@ -35,7 +35,7 @@ class Photo extends Media $image = Image::fromFile($this->pathFromHash($hash)); $image->crop($left, $top, $width, $height); - return $image->save($this->pathFromHash($hash)); + $image->save($this->pathFromHash($hash)); } function isolate(): void @@ -45,8 +45,46 @@ class Photo extends Media DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete(); } - - static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo + + function getDimensions(): array + { + $hash = $this->getRecord()->hash; + + return array_slice(getimagesize($this->pathFromHash($hash)), 0, 2); + } + + function getDimentions(): array + { + trigger_error("getDimentions is deprecated, use Photo::getDimensions instead."); + + return $this->getDimensions(); + } + + function getAlbum(): ?Album + { + return (new Albums)->getAlbumByPhotoId($this); + } + + function toVkApiStruct(): object + { + $res = (object) []; + + $res->id = $res->pid = $this->getId(); + $res->owner_id = $res->user_id = $this->getOwner()->getId()->getId(); + $res->aid = $res->album_id = NULL; + $res->width = $this->getDimensions()[0]; + $res->height = $this->getDimensions()[1]; + $res->date = $res->created = $this->getPublicationTime()->timestamp(); + + $res->src = + $res->src_small = $res->src_big = $res->src_xbig = $res->src_xxbig = + $res->src_xxxbig = $res->photo_75 = $res->photo_130 = $res->photo_604 = + $res->photo_807 = $res->photo_1280 = $res->photo_2560 = $this->getURL(); + + return $res; + } + + static function fastMake(int $owner, array $file, string $description = "", ?Album $album = NULL, bool $anon = false): Photo { $photo = new static; $photo->setOwner($owner); @@ -55,22 +93,10 @@ class Photo extends Media $photo->setCreated(time()); $photo->setFile($file); $photo->save(); - + if(!is_null($album)) $album->addPhoto($photo); - + return $photo; } - - function getDimentions() - { - $hash = $this->getRecord()->hash; - - return getimagesize($this->pathFromHash($hash)); - } - - function getAlbum(): ?Album - { - return (new Albums)->getAlbumByPhotoId($this); - } } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 13766acb..1eca5f57 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -285,7 +285,6 @@ final class UserPresenter extends OpenVKPresenter $photo->setCreated(time()); $photo->save(); } catch(ISE $ex) { - $name = $album->getName(); $this->flashFail("err", tr("error"), tr("error_upload_failed")); } diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 2add61ea..5e1959e5 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -77,6 +77,92 @@ final class VKAPIPresenter extends OpenVKPresenter exit; # Terminate request processing as this is definitely a CORS preflight request. } } + + function renderPhotoUpload(string $signature): void + { + $secret = CHANDLER_ROOT_CONF["security"]["secret"]; + $computedSignature = hash_hmac("sha3-224", $_SERVER["QUERY_STRING"], $secret); + if(!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) { + header("HTTP/1.1 422 Unprocessable Entity"); + exit("Try harder <3"); + } + + $data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($_SERVER["QUERY_STRING"])); + if((time() - $data["TIME"]) > 600) { + header("HTTP/1.1 422 Unprocessable Entity"); + exit("Expired"); + } + + $folder = __DIR__ . "../../tmp/api-storage/photos"; + $maxSize = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFileSize"]; + $maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"]; + $usrFiles = sizeof(glob("$folder/$data[USER]_*.oct")); + if($usrFiles >= $maxFiles) { + header("HTTP/1.1 507 Insufficient Storage"); + exit("There are $maxFiles pending already. Please save them before uploading more :3"); + } + + # Not multifile + if($data["MF"] === 0) { + $file = $_FILES[$data["FIELD"]]; + if(!$file) { + header("HTTP/1.0 400"); + exit("No file"); + } else if($file["error"] != UPLOAD_ERR_OK) { + header("HTTP/1.0 500"); + exit("File could not be consumed"); + } else if($file["size"] > $maxSize) { + header("HTTP/1.0 507 Insufficient Storage"); + exit("File is too big"); + } + + move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_" . ($usrFiles + 1) . ".oct"); + header("HTTP/1.0 202 Accepted"); + + $photo = $data["USER"] . "|" . ($usrFiles + 1) . "|" . $data["GROUP"]; + exit(json_encode([ + "server" => "ephemeral", + "photo" => $photo, + "hash" => hash_hmac("sha3-224", $photo, $secret), + ])); + } + + $files = []; + for($i = 1; $i <= 5; $i++) { + $file = $_FILES[$data["FIELD"] . $i] ?? NULL; + if (!$file || $file["error"] != UPLOAD_ERR_OK || $file["size"] > $maxSize) { + continue; + } else if((sizeof($files) + $usrFiles) > $maxFiles) { + # Clear uploaded files since they can't be saved anyway + foreach($files as $f) + unlink($f); + + header("HTTP/1.1 507 Insufficient Storage"); + exit("There are $maxFiles pending already. Please save them before uploading more :3"); + } + + $files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct"); + } + + if(sizeof($files) === 0) { + header("HTTP/1.0 400"); + exit("No file"); + } + + $filesManifest = []; + foreach($files as $id => $file) + $filesManifest[] = ["keyholder" => $data["USER"], "resource" => $id, "club" => $data["GROUP"]]; + + $filesManifest = json_encode($filesManifest); + $manifestHash = hash_hmac("sha3-224", $filesManifest, $secret); + header("HTTP/1.0 202 Accepted"); + exit(json_encode([ + "server" => "ephemeral", + "photos_list" => $filesManifest, + "album_id" => "undefined", + "hash" => $manifestHash, + ])); + } function renderRoute(string $object, string $method): void { diff --git a/Web/routes.yml b/Web/routes.yml index a31bffad..cacb8cff 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -279,6 +279,8 @@ routes: handler: "Support->quickBanInSupport" - url: "/admin/support/unban/{num}" handler: "Support->quickUnbanInSupport" + - url: "/upload/photo/{text}" + handler: "VKAPI->photoUpload" - url: "/method/{text}.{text}" handler: "VKAPI->route" - url: "/token" diff --git a/openvk-example.yml b/openvk-example.yml index 5342160c..50f519d1 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -9,6 +9,9 @@ openvk: uploads: disableLargeUploads: false mode: "basic" + api: + maxFilesPerDomain: 10 + maxFileSize: 25000000 shortcodes: minLength: 3 # won't affect existing short urls or the ones set via admin panel forbiddenNames: diff --git a/tmp/.gitkeep b/tmp/api-storage/audios/.gitkeep similarity index 100% rename from tmp/.gitkeep rename to tmp/api-storage/audios/.gitkeep diff --git a/tmp/api-storage/photos/.gitkeep b/tmp/api-storage/photos/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/api-storage/videos/.gitkeep b/tmp/api-storage/videos/.gitkeep new file mode 100644 index 00000000..e69de29b