diff --git a/Email/verify-email.eml.latte b/Email/verify-email.eml.latte new file mode 100755 index 00000000..40711f94 --- /dev/null +++ b/Email/verify-email.eml.latte @@ -0,0 +1,204 @@ + + + + + + Подтверждение Email + + + + + + + + +
+
+ + + + +
+   +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+   +
+

Подтверждение Email

+
+
+ + + + + +
+ + + + +
+
+ +
+ + + + + +
+   +
+ +
+ + + + + +
+   +
+ +

+ Здравствуйте, {$name}! Вы вероятно зарегистрировались на одном из инстансов OpenVK. Чтобы ваш аккаунт активировался, необходимо подтвердить Email. +

+ + + + + +
+   +
+ + + + + +
+ + + + +
+
+ Подтвердить Email! +
+
+
+ + + + + +
+   +
+ +

+ Если кнопка не работает, вы можете попробовать скопировать и вставить эту ссылку в адресную строку вашего веб-обозревателя: +

+ + + + + +
+ + http://{$_SERVER['HTTP_HOST']}/regFinish?key={$key} + +
+ +

+ Обратите внимание на то, что эту ссылку нельзя: +

+ +
    +
  • Передавать другим людям (даже друзьям, питомцам, соседам, любимым девушкам)
  • +
  • Использовать, если прошло более двух дней с её генерации
  • +
+ + + + + +
+

+ Ещё раз обратите внимание на то, что данную ссылку или письмо ни в коем случае нельзя передавать другим людям! Даже если они представляются службой поддержки.
+ Это письмо предназначено исключительно для одноразового, непосредственного использования владельцем аккаунта. +

+
+ + + + + +
+   +
+ +

+ С уважением, овк-тян. +

+ + + + + +
+   +
+ +
+ + + + + +
+   +
+ +

+ + Вы получили это письмо так как кто-то или вы зарегистрировались на инстансе OpenVK. Это не рассылка и от неё нельзя отписаться. Если вы всё равно хотите перестать получать подобные письма, деактивируйте ваш аккаунт. + +

+
+
+
+ + + + +
+   +
+
+
+ + diff --git a/README.md b/README.md index 56211045..54719355 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Updating the source code is done with this command: `git pull` * **[openvk.su](https://openvk.su/)** * [social.fetbuk.ru](http://social.fetbuk.ru/) -* [openvk.zavsc.pw](https://openvk.zavsc.pw/) ## Can I create my own OpenVK instance? @@ -48,7 +47,7 @@ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/ext ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ``` -4. Import `install/init-static-db.sql` to **same database** you installed Chandler to +4. Import `install/init-static-db.sql` to **same database** you installed Chandler to and import all sqls from `install/sqls` to **same database** 5. Import `install/init-event-db.sql` to **separate database** 6. Copy `openvk-example.yml` to `openvk.yml` and change options 7. Run `composer install` in OpenVK directory @@ -75,11 +74,12 @@ You may reach out to us via: * [Bug-tracker](https://github.com/openvk/openvk/projects/1) * [Ticketing system](https://openvk.su/support?act=new) -* Telegram chat: Go to [our channel](https://t.me/openvkch) and open discussion in our channel menu. +* Telegram chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. * [Reddit](https://www.reddit.com/r/openvk/) * [Discussions](https://github.com/openvk/openvk/discussions) +* Matrix chat: #openvk:matrix.org -**Attention**: bug tracker and telegram chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com** +**Attention**: bug tracker, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com** Get it on Codeberg diff --git a/README_RU.md b/README_RU.md index 2ed13a7b..369e38a6 100644 --- a/README_RU.md +++ b/README_RU.md @@ -18,7 +18,6 @@ _[English](README.md)_ * **[openvk.su](https://openvk.su/)** * [social.fetbuk.ru](http://social.fetbuk.ru/) -* [openvk.zavsc.pw](https://openvk.zavsc.pw/) ## Могу ли я создать свою собственную инстанцию OpenVK? @@ -48,7 +47,7 @@ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/ext ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ``` -4. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler +4. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler, и импортируйте все SQL файлы из папки `install/sqls` в **ту же базу данных** 5. Импортируйте `install/init-event-db.sql` в **отдельную базу данных** 6. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры 7. Запустите `composer install` в директории OpenVK @@ -78,8 +77,9 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions * Telegram-чат: Перейдите на [наш канал](https://t.me/openvkch) и откройте обсуждение в меню нашего канала. * [Reddit](https://www.reddit.com/r/openvk/) * [Обсуждения](https://github.com/openvk/openvk/discussions) +* Чат в Matrix: #ovk:matrix.org -**Внимание**: баг-трекер и телеграм-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [at] tutanota [dot] com**. +**Внимание**: баг-трекер, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [at] tutanota [dot] com**. Get it on Codeberg diff --git a/VKAPI/README.md b/VKAPI/README.md new file mode 100644 index 00000000..b3c85918 --- /dev/null +++ b/VKAPI/README.md @@ -0,0 +1,57 @@ +# VK API Compatability layer for OpenVK + +This directory contains VK api handlers, structures and relared +exceptions. It is still a work-in-progress functionality. +**Note**: requests to api are routed through +openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. + +## Implementing API methods + +VK API methods have names like this: `example.test`. To implement a +method like this you will need to create a class `Example` in the +Handlers subdirectory. This class **must** extend VKAPIHandler and be +final. +Next step is to create test method. It **must** have a type hint that is +not void. Everything else is fine, the return value of method will be +authomatically converted to JSON and sent back to client. + +### Parameters + +Method arguments are parameters. To declare a parameter just create an +argument with the same name. You should also provide correct type hints +for them. Type conversion is done automatically if possible. If not +possible error №1 will be returned. +If parameter is not passed by client then router will pass default value +to argument. If there is no default value but argument accepts NULL then +NULL will be passed. If NULL is not acceptable, default value is +undefined and parameter is not passed, API will return missing parameter +error to client. + +### Returning errors + +To return an error, call fail method like this: `$this->fail(5, +"error")` (first argument is error code and second is error message). +You can also throw the exception manually: `throw new +APIErrorException("error", 5)` (class: +openvk.VKAPI.Exceptions.APIErrorException). +If you throw any exception that does not inherit APIErrorException then +API will return error №1 (unknown error) to client. + +### Refering to user + +To get user use `getUser` method: `$this->getUser()`. Keep in mind it +will return NULL if user is undefined (no access\_token passed or it is +invalid/expired or roaming authentification failed). +If you need to check whether user is defined use `userAuthorized`. This +method returns true if user is present and false if not. +If your method can’t work without user context call `requireUser` and it +will automatically return unauthorized error. + +### Working with data + +You can use OpenVK models for that. However, **do not** return them +(either you will leak data or JSON conversion will fail). It is better +to create a response object and return it. It is also a good idea to +define a structure in Structures subdirectory. + +Have a lot of fun diff --git a/VKAPI/README.textile b/VKAPI/README.textile deleted file mode 100644 index f212658d..00000000 --- a/VKAPI/README.textile +++ /dev/null @@ -1,31 +0,0 @@ -h1. VK API Compatability layer for OpenVK - -This directory contains VK api handlers, structures and relared exceptions. It is still a work-in-progress functionality. -**Note**: requests to api are routed through openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. - -h2. Implementing API methods - -VK API methods have names like this: @example.test@. To implement a method like this you will need to create a class @Example@ in the Handlers subdirectory. This class **must** extend VKAPIHandler and be final. -Next step is to create %test% method. It **must** have a type hint that is not %void%. Everything else is fine, the return value of method will be authomatically converted to JSON and sent back to client. - -h3. Parameters - -Method arguments are parameters. To declare a parameter just create an argument with the same name. You should also provide correct type hints for them. Type conversion is done automatically if possible. If not possible error №1 will be returned. -If parameter is not passed by client then router will pass default value to argument. If there is no default value but argument accepts NULL then NULL will be passed. If NULL is not acceptable, default value is undefined and parameter is not passed, API will return missing parameter error to client. - -h3. Returning errors - -To return an error, call %fail% method like this: @$this->fail(5, "error")@ (first argument is error code and second is error message). You can also throw the exception manually: @throw new APIErrorException("error", 5)@ (class: openvk.VKAPI.Exceptions.APIErrorException). -If you throw any exception that does not inherit APIErrorException then API will return error №1 (unknown error) to client. - -h3. Refering to user - -To get user use @getUser@ method: @$this->getUser()@. Keep in mind it will return NULL if user is undefined (no access_token passed or it is invalid/expired or roaming authentification failed). -If you need to check whether user is defined use @userAuthorized@. This method returns true if user is present and false if not. -If your method can't work without user context call @requireUser@ and it will automatically return unauthorized error. - -h3. Working with data - -You can use OpenVK models for that. However, **do not** return them (either you will leak data or JSON conversion will fail). It is better to create a response object and return it. It is also a good idea to define a structure in Structures subdirectory. - -Have a lot of fun ^^ diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 2230b1a0..0c91b11e 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -40,7 +40,7 @@ class Club extends RowModel function getAvatarUrl(): string { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; + $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; $avPhoto = $this->getAvatarPhoto(); return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURL(); @@ -64,16 +64,6 @@ class Club extends RowModel else return "/club" . $this->getId(); } - /* - function getAvatarUrl(): string - { - $avAlbum = (new Albums)->getUserAvatarAlbum($this); - $avCount = $avAlbum->getPhotosCount(); - $avPhotos = $avAlbum->getPhotos($avCount, 1); - $avPhoto = iterator_to_array($avPhotos)[0] ?? NULL; - - return is_null($avPhoto) ? "/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURL(); - } */ function getName(): string { diff --git a/Web/Models/Entities/EmailVerification.php b/Web/Models/Entities/EmailVerification.php new file mode 100755 index 00000000..cfd057f9 --- /dev/null +++ b/Web/Models/Entities/EmailVerification.php @@ -0,0 +1,10 @@ +getRecord()->timestamp > (time() - 301); + return $this->getRecord()->timestamp > (time() - (5 * MINUTE)); } + /** + * Token is valid only for 3 days. + */ function isStillValid(): bool { - return $this->getRecord()->timestamp > (time() - 172801); + return $this->getRecord()->timestamp > (time() - (3 * DAY)); } function verify(string $token): bool diff --git a/Web/Models/Entities/Traits/TRichText.php b/Web/Models/Entities/Traits/TRichText.php index 07caacdc..3eaaeaff 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -64,7 +64,7 @@ trait TRichText $text = preg_replace("%@([A-Za-z0-9]++) \(([\p{L} 0-9]+)\)%Xu", "[$1|$2]", $text); $text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text); $text = preg_replace("%\[([A-Za-z0-9]++)\|([\p{L} 0-9@]+)\]%Xu", "$2", $text); - $text = preg_replace("%(#([\p{L}_-]++[0-9]*[\p{L}_-]*))%Xu", "$1", $text); + $text = preg_replace("%([\n\r\s]|^)(#([\p{L}_-]++[0-9]*[\p{L}_-]*))%Xu", "$1$2", $text); $text = $this->formatEmojis($text); } diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 81c10695..64b689e4 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -5,6 +5,7 @@ use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications}; +use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; use Chandler\Security\User as ChandlerUser; @@ -103,7 +104,7 @@ class User extends RowModel function getAvatarUrl(): string { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; + $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; if($this->getRecord()->deleted) return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; @@ -739,6 +740,27 @@ class User extends RowModel return true; } + function setFirst_Name(string $firstName): void + { + $firstName = mb_convert_case($firstName, MB_CASE_TITLE); + if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?(?:[\p{L&}\p{Lo}]\p{Mn}?){1,16}$%u', $firstName)) + throw new InvalidUserNameException; + + $this->stateChanges("first_name", $firstName); + } + + function setLast_Name(string $lastName): void + { + if(!empty($lastName)) + { + $lastName = mb_convert_case($lastName, MB_CASE_TITLE); + if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?([\p{L&}\p{Lo}]\p{Mn}?){1,16}(\-\g<1>+)?$%u', $lastName)) + throw new InvalidUserNameException; + } + + $this->stateChanges("last_name", $lastName); + } + function setNsfwTolerance(int $tolerance): void { $this->stateChanges("nsfw_tolerance", $tolerance); @@ -850,10 +872,10 @@ class User extends RowModel function isDeleted(): bool { - if ($this->getRecord()->deleted == 1) - return TRUE; - else - return FALSE; + if ($this->getRecord()->deleted == 1) + return TRUE; + else + return FALSE; } /** @@ -882,6 +904,12 @@ class User extends RowModel { return $this->getRecord()->website; } + + // ты устрица + function isActivated(): bool + { + return (bool) $this->getRecord()->activated; + } use Traits\TSubscribable; } diff --git a/Web/Models/Exceptions/InvalidUserNameException.php b/Web/Models/Exceptions/InvalidUserNameException.php new file mode 100644 index 00000000..12a595a6 --- /dev/null +++ b/Web/Models/Exceptions/InvalidUserNameException.php @@ -0,0 +1,7 @@ +users->where("CONCAT_WS(' ', first_name, last_name) LIKE ?", $query)->where("deleted", 0); + $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo) LIKE ?", $query)->where("deleted", 0); return new Util\EntityStream("User", $result); } diff --git a/Web/Models/Repositories/Verifications.php b/Web/Models/Repositories/Verifications.php new file mode 100755 index 00000000..83c13e59 --- /dev/null +++ b/Web/Models/Repositories/Verifications.php @@ -0,0 +1,33 @@ +context = DatabaseConnection::i()->getContext(); + $this->verifications = $this->context->table("email_verifications"); + } + + function toEmailVerification(?ActiveRow $ar): ?EmailVerification + { + return is_null($ar) ? NULL : new EmailVerification($ar); + } + + function getByToken(string $token): ?EmailVerification + { + return $this->toEmailVerification($this->verifications->where("key", $token)->fetch()); + } + + function getLatestByUser(User $user): ?EmailVerification + { + return $this->toEmailVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch()); + } +} diff --git a/Web/Models/VideoDrivers/YouTubeVideoDriver.php b/Web/Models/VideoDrivers/YouTubeVideoDriver.php index 1c5b3160..f517d7fd 100644 --- a/Web/Models/VideoDrivers/YouTubeVideoDriver.php +++ b/Web/Models/VideoDrivers/YouTubeVideoDriver.php @@ -5,7 +5,7 @@ final class YouTubeVideoDriver extends VideoDriver { function getThumbnailURL(): string { - return "https://img.youtube.com/vi/$this->id/mq3.jpg"; + return "https://img.youtube.com/vi/$this->id/mqdefault.jpg"; } function getURL(): string diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php index bd84efbc..e7351897 100644 --- a/Web/Presenters/AboutPresenter.php +++ b/Web/Presenters/AboutPresenter.php @@ -8,6 +8,7 @@ use Chandler\Session\Session; final class AboutPresenter extends OpenVKPresenter { protected $banTolerant = true; + protected $activationTolerant = true; function renderIndex(): void { diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index 9123f12a..a80bbc68 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -3,9 +3,11 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\IP; use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\PasswordReset; +use openvk\Web\Models\Entities\EmailVerification; use openvk\Web\Models\Repositories\IPs; use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\Restores; +use openvk\Web\Models\Repositories\Verifications; use openvk\Web\Util\Validator; use Chandler\Session\Session; use Chandler\Security\User as ChandlerUser; @@ -16,19 +18,22 @@ use lfkeitel\phptotp\{Base32, Totp}; final class AuthPresenter extends OpenVKPresenter { protected $banTolerant = true; + protected $activationTolerant = true; private $authenticator; private $db; private $users; private $restores; + private $verifications; - function __construct(Users $users, Restores $restores) + function __construct(Users $users, Restores $restores, Verifications $verifications) { $this->authenticator = Authenticator::i(); $this->db = DatabaseConnection::i()->getContext(); $this->users = $users; $this->restores = $restores; + $this->verifications = $verifications; parent::__construct(); } @@ -81,7 +86,7 @@ final class AuthPresenter extends OpenVKPresenter $this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment")); if (strtotime($this->postParam("birthday")) > time()) - $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); + $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); $chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password")); if(!$chUser) @@ -96,12 +101,25 @@ final class AuthPresenter extends OpenVKPresenter $user->setSince(date("Y-m-d H:i:s")); $user->setRegistering_Ip(CONNECTING_IP); $user->setBirthday(strtotime($this->postParam("birthday"))); + $user->setActivated((int) !OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']); $user->save(); if(!is_null($referer)) { $user->toggleSubscription($referer); $referer->toggleSubscription($user); } + + if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']) { + $verification = new EmailVerification; + $verification->setProfile($user->getId()); + $verification->save(); + + $params = [ + "key" => $verification->getKey(), + "name" => $user->getCanonicalName(), + ]; + $this->sendmail($user->getEmail(), "verify-email", $params); #Vulnerability possible + } $this->authenticator->authenticate($chUser->getId()); $this->redirect("/id" . $user->getId(), static::REDIRECT_TEMPORARY); @@ -253,4 +271,48 @@ final class AuthPresenter extends OpenVKPresenter $this->flashFail("succ", tr("information_-1"), tr("password_reset_email_sent")); } } + + function renderResendEmail(): void + { + if(!is_null($this->user) && $this->user->identity->isActivated()) + $this->redirect("/id" . $this->user->id, static::REDIRECT_TEMPORARY); + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $user = $this->user->identity; + if(!$user || $user->isDeleted() || $user->isActivated()) + $this->flashFail("err", tr("error"), tr("email_error")); + + $request = $this->verifications->getLatestByUser($user); + if(!is_null($request) && $request->isNew()) + $this->flashFail("err", tr("forbidden"), tr("email_rate_limit_error")); + + $verification = new EmailVerification; + $verification->setProfile($user->getId()); + $verification->save(); + + $params = [ + "key" => $verification->getKey(), + "name" => $user->getCanonicalName(), + ]; + $this->sendmail($user->getEmail(), "verify-email", $params); #Vulnerability possible + + $this->flashFail("succ", tr("information_-1"), tr("email_sent")); + } + } + + function renderVerifyEmail(): void + { + $request = $this->verifications->getByToken(str_replace(" ", "+", $this->queryParam("key"))); + if(!$request || !$request->isStillValid()) { + $this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment")); + $this->redirect("/"); + } else { + $user = $request->getUser(); + $user->setActivated(1); + $user->save(); + + $this->flash("success", tr("email_verify_success")); + $this->redirect("/"); + } + } } diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 51840c3d..e307c134 100755 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -13,6 +13,7 @@ use WhichBrowser; abstract class OpenVKPresenter extends SimplePresenter { protected $banTolerant = false; + protected $activationTolerant = false; protected $errorTemplate = "@error"; protected $user = NULL; @@ -212,6 +213,27 @@ abstract class OpenVKPresenter extends SimplePresenter $this->template->userTainted = $user->isTainted(); if($this->user->identity->isDeleted()) { + /* + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⠶⠶⣶⠶⠶⠶⠶⠶⠶⠶⠶⠶⢶⠶⠶⠶⠤⠤⠤⠤⣄⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠒⠒⠒⠀⠀⠀⠀⠤⢤⣤⣄⠉⠉⠛⠛⠷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⣰⠟⠀⠀⠀⠀⠀⠐⠋⢑⣤⣶⣶⣤⡢⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣄⡂⠀⠀⠶⢄⠙⢷⣤⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⣸⡿⠚⠉⡀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⢢⠀⠀⡀⣰⣿⣿⣿⣿⣦⡀⠀⠀⠡⡀⢹⡆⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⢀⣴⠏⠀⣀⣀⣀⡤⢤⣄⣠⣿⣿⣿⣿⣻⣿⣿⣷⠀⢋⣾⠈⠙⣶⠒⢿⣿⣿⣿⣿⡿⠟⠃⠀⡀⠡⠼⣧⡀⠀⠀⠀⠀⠀⠀ + ⠀⠀⢀⣴⣿⢃⡴⢊⢽⣶⣤⣀⠀⠊⠉⠉⡛⢿⣿⣿⣿⠿⠋⢀⡀⠁⠀⠀⢸⣁⣀⣉⣉⣉⡉⠀⠩⡡⠀⣩⣦⠀⠈⠻⣦⡀⠀⠀⠀⠀ + ⠀⢠⡟⢡⠇⡞⢀⠆⠀⢻⣿⣿⣷⣄⠀⢀⠈⠂⠈⢁⡤⠚⡟⠉⠀⣀⣀⠀⠈⠳⣍⠓⢆⢀⡠⢀⣨⣴⣿⣿⡏⢀⡆⠀⢸⡇⠀⠀⠀⠀ + ⠀⣾⠁⢸⠀⠀⢸⠀⠀⠀⠹⣿⣿⣿⣿⣶⣬⣦⣤⡈⠀⠀⠇⠀⠛⠉⣩⣤⣤⣤⣿⣤⣤⣴⣾⣿⣿⣿⣿⣿⣧⠞⠀⠀⢸⡇⠀⠀⠀⠀ + ⠀⢹⣆⠸⠀⠀⢸⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣟⣛⠛⠛⣛⡛⠛⠛⣛⣋⡉⠉⣡⠶⢾⣿⣿⣿⣿⣿⣿⡇⠀⠀⢀⣾⠃⠀⠀⠀⠀ + ⠀⠀⠻⣆⡀⠀⠈⢂⠀⠀⠀⠠⡈⢻⣿⣿⣿⣿⡟⠁⠈⢧⡼⠉⠙⣆⡞⠁⠈⢹⣴⠃⠀⢸⣿⣿⣿⣿⣿⣿⠃⠀⡆⣾⠃⠀⠀⠀⠀⠀ + ⠀⠀⠀⠈⢻⣇⠀⠀⠀⠀⠀⠀⢡⠀⠹⣿⣿⣿⣷⡀⠀⣸⡇⠀⠀⣿⠁⠀⠀⠘⣿⠀⠀⠘⣿⣿⣿⣿⣿⣿⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠹⣇⠀⠠⠀⠀⠀⠀⠡⠐⢬⡻⣿⣿⣿⣿⣿⣷⣶⣶⣿⣦⣤⣤⣤⣿⣦⣶⣿⣿⣿⣿⣿⣿⣿⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠹⣧⡀⠡⡀⠀⠀⠀⠑⠄⠙⢎⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⢿⡇⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠈⠳⣤⡐⡄⠀⠀⠀⠈⠂⠀⠱⣌⠻⣿⣿⣿⣿⣿⣿⣿⠿⣿⠟⢻⡏⢻⣿⣿⣿⣿⣿⣿⣿⠀⢸⡇⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢮⣦⡀⠂⠀⢀⠀⠀⠈⠳⣈⠻⣿⣿⣿⡇⠘⡄⢸⠀⠀⣇⠀⣻⣿⣿⣿⣿⣿⡏⠀⠸⡇⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢶⣤⣄⡑⠄⠀⠀⠈⠑⠢⠙⠻⢷⣶⣵⣞⣑⣒⣋⣉⣁⣻⣿⠿⠟⠱⠃⡸⠀⣧⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⣷⣄⡀⠐⠢⣄⣀⡀⠀⠉⠉⠉⠉⠛⠙⠭⠭⠄⠒⠈⠀⠐⠁⢀⣿⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠷⢦⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣒⡠⠄⣠⡾⠃⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠷⠶⣦⣤⣭⣤⣬⣭⣭⣴⠶⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀ + */ Authenticator::i()->logout(); Session::i()->set("_su", NULL); $this->flashFail("err", tr("error"), tr("profile_not_found")); @@ -227,6 +249,17 @@ abstract class OpenVKPresenter extends SimplePresenter ]); exit; } + + // ето для емейл уже надо (и по хорошему надо бы избавится от повторяющегося кода мда) + if(!$this->user->identity->isActivated() && !$this->activationTolerant) { + header("HTTP/1.1 403 Forbidden"); + $this->getTemplatingEngine()->render(__DIR__ . "/templates/@email.xml", [ + "thisUser" => $this->user->identity, + "csrfToken" => $GLOBALS["csrfToken"], + "isTimezoned" => Session::i()->get("_timezoneOffset"), + ]); + exit; + } if ($this->user->identity->onlineStatus() == 0) { $this->user->identity->setOnline(time()); diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index b05f7843..c3765d82 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -9,8 +9,9 @@ use openvk\Web\Models\Repositories\Albums; use openvk\Web\Models\Repositories\Videos; use openvk\Web\Models\Repositories\Notes; use openvk\Web\Models\Repositories\Vouchers; +use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; -use openvk\Web\Models\Entities\Notifications\CoinsTransferNotification; +use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; use Chandler\Security\Authenticator; use lfkeitel\phptotp\{Base32, Totp}; use chillerlan\QRCode\{QRCode, QROptions}; @@ -137,10 +138,18 @@ final class UserPresenter extends OpenVKPresenter $this->willExecuteWriteAction($_GET['act'] === "status"); if($_GET['act'] === "main" || $_GET['act'] == NULL) { - $user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); - $user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name")); + try { + $user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); + $user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name")); + } catch(InvalidUserNameException $ex) { + $this->flashFail("err", tr("error"), tr("invalid_real_name")); + } + $user->setPseudo(empty($this->postParam("pseudo")) ? NULL : $this->postParam("pseudo")); $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setHometown(empty($this->postParam("hometown")) ? NULL : $this->postParam("hometown")); + + if (strtotime($this->postParam("birthday")) < time()) $user->setBirthday(strtotime($this->postParam("birthday"))); @@ -179,7 +188,7 @@ final class UserPresenter extends OpenVKPresenter $user->setCity(empty($this->postParam("city")) ? NULL : $this->postParam("city")); $user->setAddress(empty($this->postParam("address")) ? NULL : $this->postParam("address")); - + $website = $this->postParam("website") ?? ""; if(empty($website)) $user->setWebsite(NULL); @@ -510,4 +519,44 @@ final class UserPresenter extends OpenVKPresenter $this->flashFail("succ", tr("information_-1"), tr("points_transfer_successful", tr("points_amount", $value), $receiver->getURL(), htmlentities($receiver->getCanonicalName()))); } + + function renderIncreaseRating(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + $this->flashFail("err", tr("error"), tr("feature_disabled")); + + $receiverAddress = $this->postParam("receiver"); + $value = (int) $this->postParam("value"); + $message = $this->postParam("message"); + + if(!$receiverAddress || !$value) + $this->flashFail("err", tr("failed_to_increase_rating"), tr("not_all_information_has_been_entered")); + + if($value < 0) + $this->flashFail("err", tr("failed_to_increase_rating"), tr("negative_rating_value")); + + if(iconv_strlen($message) > 255) + $this->flashFail("err", tr("failed_to_increase_rating"), tr("message_is_too_long")); + + $receiver = $this->users->getByAddress($receiverAddress); + if(!$receiver) + $this->flashFail("err", tr("failed_to_increase_rating"), tr("receiver_not_found")); + + if($this->user->identity->getCoins() < $value) + $this->flashFail("err", tr("failed_to_increase_rating"), tr("you_dont_have_enough_points")); + + $this->user->identity->setCoins($this->user->identity->getCoins() - $value); + $this->user->identity->save(); + + $receiver->setRating($receiver->getRating() + $value); + $receiver->save(); + + if($this->user->id !== $receiver->getId()) + (new RatingUpNotification($receiver, $this->user->identity, $value, $message))->emit(); + + $this->flashFail("succ", tr("information_-1"), tr("rating_increase_successful", $receiver->getURL(), htmlentities($receiver->getCanonicalName()), $value)); + } } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index ccb13886..98e8a4cd 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -5,6 +5,9 @@ use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotifi use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; +use Bhaktaraz\RSSGenerator\Item; +use Bhaktaraz\RSSGenerator\Feed; +use Bhaktaraz\RSSGenerator\Channel; final class WallPresenter extends OpenVKPresenter { @@ -42,7 +45,7 @@ final class WallPresenter extends OpenVKPresenter function renderWall(int $user, bool $embedded = false): void { if(false) - exit("Ошибка доступа: " . (string) random_int(0, 255)); + exit(tr("forbidden") . ": " . (string) random_int(0, 255)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); if(is_null($this->user)) { @@ -51,15 +54,15 @@ final class WallPresenter extends OpenVKPresenter if(!$owner->isBanned()) $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); else - $this->flashFail("err", tr("error"), "Ошибка доступа"); - } else if($user < 0) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; else $canPost = $owner->canPost(); } else { $canPost = false; - } + } if ($embedded == true) $this->template->_template = "components/wall.xml"; $this->template->oObj = $owner; @@ -82,6 +85,49 @@ final class WallPresenter extends OpenVKPresenter { $this->renderWall($user, true); } + + function renderRSS(int $user): void + { + if(false) + exit(tr("forbidden") . ": " . (string) random_int(0, 255)); + + $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); + if(is_null($this->user)) { + $canPost = false; + } else if($user > 0) { + if(!$owner->isBanned()) + $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); + else + $this->flashFail("err", tr("error"), tr("forbidden")); + } else if($user < 0) { + if($owner->canBeModifiedBy($this->user->identity)) + $canPost = true; + else + $canPost = $owner->canPost(); + } else { + $canPost = false; + } + + $posts = iterator_to_array($this->posts->getPostsFromUsersWall($user)); + + $feed = new Feed(); + + $channel = new Channel(); + $channel->title(OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["SERVER_NAME"])->appendTo($feed); + + foreach($posts as $post) { + $item = new Item(); + $item + ->title($post->getOwner()->getCanonicalName()) + ->description($post->getText()) + ->url(ovk_scheme(true).$_SERVER["SERVER_NAME"]."/wall{$post->getPrettyId()}") + ->pubDate($post->getPublicationTime()->timestamp()) + ->appendTo($channel); + } + + header("Content-Type: application/rss+xml"); + exit($feed); + } function renderFeed(): void { @@ -167,12 +213,12 @@ final class WallPresenter extends OpenVKPresenter $this->willExecuteWriteAction(); $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) - ?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует."); + ?? $this->flashFail("err", tr("failed_to_publish_post"), tr("error_4")); if($wall > 0) { if(!$wallOwner->isBanned()) $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); else - $this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену."); + $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); } else if($wall < 0) { if($wallOwner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -183,7 +229,7 @@ final class WallPresenter extends OpenVKPresenter } if(!$canPost) - $this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену."); + $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { @@ -217,13 +263,13 @@ final class WallPresenter extends OpenVKPresenter $video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon); } } catch(\DomainException $ex) { - $this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён."); + $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик."); + $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); } if(empty($this->postParam("text")) && !$photo && !$video) - $this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой."); + $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { $post = new Post; @@ -236,7 +282,7 @@ final class WallPresenter extends OpenVKPresenter $post->setNsfw($this->postParam("nsfw") === "on"); $post->save(); } catch (\LengthException $ex) { - $this->flashFail("err", "Не удалось опубликовать пост", "Пост слишком большой."); + $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); } if(!is_null($photo)) @@ -270,7 +316,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->wallOwner = (new Users)->get($post->getTargetWall()); $this->template->isWallOfGroup = false; if($this->template->wallOwner->isBanned()) - $this->flashFail("err", tr("error"), "Ошибка доступа"); + $this->flashFail("err", tr("error"), tr("forbidden")); } else { $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); $this->template->isWallOfGroup = true; @@ -334,7 +380,7 @@ final class WallPresenter extends OpenVKPresenter $user = $this->user->id; $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) - ?? $this->flashFail("err", "Не удалось удалить пост", "Такого пользователя не существует."); + ?? $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4")); if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); else $canBeDeletedByOtherUser = false; @@ -345,7 +391,7 @@ final class WallPresenter extends OpenVKPresenter $post->delete(); } } else { - $this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт."); + $this->flashFail("err", tr("failed_to_delete_post"), tr("login_required_error_comment")); } $this->redirect($wall < 0 ? "/club".($wall*-1) : "/id".$wall, static::REDIRECT_TEMPORARY); @@ -362,7 +408,7 @@ final class WallPresenter extends OpenVKPresenter $this->notFound(); if(!$post->canBePinnedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Вам нельзя закреплять этот пост."); + $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); if(($this->queryParam("act") ?? "pin") === "pin") { $post->pin(); @@ -371,6 +417,6 @@ final class WallPresenter extends OpenVKPresenter } // TODO localize message based on language and ?act=(un)pin - $this->flashFail("succ", "Операция успешна", "Операция успешна."); + $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); } } diff --git a/Web/Presenters/templates/@email.xml b/Web/Presenters/templates/@email.xml new file mode 100755 index 00000000..3de8b2e1 --- /dev/null +++ b/Web/Presenters/templates/@email.xml @@ -0,0 +1,19 @@ +{extends "@layout.xml"} +{block title}{_ec_header}{/block} + +{block header} +{_ec_header} +{/block} + +{block content} +
+

{_ec_title}

+

{tr("ec_1", htmlentities($thisUser->getCanonicalName()))|noescape}

+

{_ec_2}

+

+

+ +
+

+
+{/block} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f6542040..e6697c9d 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -100,7 +100,7 @@