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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/intellij-latte/xmlSources/Latte.dtd b/.idea/intellij-latte/xmlSources/Latte.dtd new file mode 100644 index 00000000..0cf3a95a --- /dev/null +++ b/.idea/intellij-latte/xmlSources/Latte.dtd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/intellij-latte/xmlSources/Latte.xml b/.idea/intellij-latte/xmlSources/Latte.xml new file mode 100644 index 00000000..0bfceaf8 --- /dev/null +++ b/.idea/intellij-latte/xmlSources/Latte.xml @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/NetteApplication.xml b/.idea/intellij-latte/xmlSources/NetteApplication.xml new file mode 100644 index 00000000..aa4a4685 --- /dev/null +++ b/.idea/intellij-latte/xmlSources/NetteApplication.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/NetteForms.xml b/.idea/intellij-latte/xmlSources/NetteForms.xml new file mode 100644 index 00000000..036e07f6 --- /dev/null +++ b/.idea/intellij-latte/xmlSources/NetteForms.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..639900d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..de1f1bbf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/openvk.iml b/.idea/openvk.iml new file mode 100644 index 00000000..fe58bea6 --- /dev/null +++ b/.idea/openvk.iml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 00000000..7b4f97fd --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..797acea5 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CLI/RebuildImagesCommand.php b/CLI/RebuildImagesCommand.php new file mode 100644 index 00000000..937978b1 --- /dev/null +++ b/CLI/RebuildImagesCommand.php @@ -0,0 +1,86 @@ +images = DatabaseConnection::i()->getContext()->table("photos"); + + parent::__construct(); + } + + protected function configure(): void + { + $this->setDescription("Create resized versions of images") + ->setHelp("This command allows you to resize all your images after configuration change") + ->addOption("upgrade-only", "U", InputOption::VALUE_NEGATABLE, "Only upgrade images which aren't resized?"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $header = $output->section(); + $counter = $output->section(); + + $header->writeln([ + "Image Rebuild Utility", + "=====================", + "", + ]); + + $filter = ["deleted" => false]; + if($input->getOption("upgrade-only")) + $filter["sizes"] = NULL; + + $selection = $this->images->select("id")->where($filter); + $totalPics = $selection->count(); + $header->writeln([ + "Total of $totalPics images found.", + "", + ]); + + $errors = 0; + $count = 0; + $avgTime = NULL; + $begin = new \DateTimeImmutable("now"); + foreach($selection as $idHolder) { + $start = microtime(true); + + try { + $photo = (new Photos)->get($idHolder->id); + $photo->getSizes(true, true); + $photo->getDimensions(); + } catch(ImageException $ex) { + $errors++; + } + + $timeConsumed = microtime(true) - $start; + if(!$avgTime) + $avgTime = $timeConsumed; + else + $avgTime = ($avgTime + $timeConsumed) / 2; + + $eta = $begin->getTimestamp() + ceil($totalPics * $avgTime); + $int = (new \DateTimeImmutable("now"))->diff(new \DateTimeImmutable("@$eta")); + $int = $int->d . "d" . $int->h . "h" . $int->i . "m" . $int->s . "s"; + $pct = floor(100 * ($count / $totalPics)); + + $counter->overwrite("Processed " . ++$count . " images... ($pct% $int left $errors/$count fail)"); + } + + $counter->overwrite("Processing finished :3"); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 43f8e958..e67043d7 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,27 @@ _[Русский](README_RU.md)_ VKontakte belongs to Pavel Durov and VK Group. -To be honest, we don't even know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this). +To be honest, we don't know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this). ## When's the release? -Please use the master branch, as it has the most changes. - -Updating the source code is done with this command: `git pull` +We will release OpenVK as soon as it's ready. As for now you can: +* `git clone` this repo's master branch (use `git pull` to update) +* Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip) ## Instances * **[openvk.su](https://openvk.su/)** +* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su () +* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS () * [social.fetbuk.ru](http://social.fetbuk.ru/) -* [openvk.zavsc.pw](https://openvk.zavsc.pw/) +* [vepurovk.xyz](http://vepurovk.xyz/) ## Can I create my own OpenVK instance? Yes! And you're very welcome to. -However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. this extensions are available on most of ISPManager hostings). +However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings). If you want, you can add your instance to the list above so that people can register there. @@ -32,42 +34,47 @@ If you want, you can add your instance to the list above so that people can regi 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) -* PHP 8 has **not** yet been tested, so you should not expect it to work. +* PHP 8 has **not** yet been tested, so you should not expect it to work. (edit: it does not work). -2. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this: +2. Install MySQL-compatible database. + +* We recommend using Percona Server, but any MySQL-compatible server should work +* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ recommended. +* Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs. + +3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this: ```bash git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha ``` -3. And enable them: +4. And enable them: ```bash ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/ 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 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 -8. Move to `Web/static/js` and execute `yarn install` -9. Set `openvk` as your root app in `chandler.yml` +5. Import `install/init-static-db.sql` to the **same database** you installed Chandler to and import all sqls from `install/sqls` to the **same database** +6. Import `install/init-event-db.sql` to a **separate database** (Yandex.Clickhouse can also be used, highly recommended) +7. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking +8. Run `composer install` in OpenVK directory +9. Run `composer install` in commitcaptcha directory +10. Move to `Web/static/js` and execute `yarn install` +11. Set `openvk` as your root app in `chandler.yml` Once you are done, you can login as a system administrator on the network itself (no registration required): * **Login**: `admin@localhost.localdomain6` * **Password**: `admin` - * It is recommended to change the password before using the built-in account. + * It is recommended to change the password of the built-in account or disable it. -Full example installation instruction for CentOS 8 is also available [here](https://docs.openvk.su/openvk_engine/centos8_installation/). +💡Confused? Full installation walkthrough is available [here](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)). -### If my website uses OpenVK, should I publish it's sources? +### If my website uses OpenVK, should I release it's sources? -You are encouraged to do so. We don't enforce this though. You can keep your sources to yourself (unless you distribute your OpenVK distro to other people). - -You also not required to publish source texts of your themepacks and plugins. +It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). ## Where can I get assistance? @@ -80,7 +87,7 @@ You may reach out to us via: * [Discussions](https://github.com/openvk/openvk/discussions) * Matrix chat: #openvk:matrix.org -**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** +**Attention**: bug tracker, board, 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 65b0dbd6..6bf29502 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,23 +2,25 @@ _[English](README.md)_ -**OpenVK** это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. Представленный здесь код пока не стабилен. +**OpenVK** - это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент представленный здесь исходный код проекта пока не является стабильным. ВКонтакте принадлежит Павлу Дурову и VK Group. Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK). -## Когда релиз? +## Когда выйдет релизная версия? -Пожалуйста, используйте ветку master, так как в ней больше всего изменений. - -Обновление исходного кода выполняется с помощью этой команды: `git pull`. +Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете: +* Склонировать master ветку репозитория командой `git clone` (используйте `git pull` для обновления) +* Взять готовую сборку OpenVK из [GitHub Actions](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip) ## Инстанции * **[openvk.su](https://openvk.su/)** +* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su () +* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS () * [social.fetbuk.ru](http://social.fetbuk.ru/) -* [openvk.zavsc.pw](https://openvk.zavsc.pw/) +* [vepurovk.xyz](http://vepurovk.xyz/) ## Могу ли я создать свою собственную инстанцию OpenVK? @@ -32,42 +34,47 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать. +* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). -2. Установите [commitcaptcha](https://github.com/openvk/commitcaptcha) и OpenVK в качестве расширений Chandler следующим образом: +2. Установите MySQL-совместимую базу данных. + +* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать +* Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+. +* Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно. + +3. Установите [commitcaptcha](https://github.com/openvk/commitcaptcha) и OpenVK в качестве расширений Chandler: ```bash git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha ``` -3. И включите их: +4. И включите их: ```bash ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ``` -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 -8. Перейдите в `Web/static/js` и выполните `yarn install` -9. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` +5. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler, и импортируйте все SQL файлы из папки `install/sqls` в **ту же базу данных** +6. Импортируйте `install/init-event-db.sql` в **отдельную базу данных** (Яндекс.Clickhouse также может быть использован, настоятельно рекомендуется) +7. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры под свои нужды +8. Запустите `composer install` в директории OpenVK +9. Запустите `composer install` в директории commitcaptcha +10. Перейдите в `Web/static/js` и выполните `yarn install` +11. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): * **Логин**: `admin@localhost.localdomain6` * **Пароль**: `admin` - * Перед использованием встроенной учетной записи рекомендуется сменить пароль. + * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её. -Полный пример инструкции по установке CentOS 8 также доступен [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/). +💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? -Вам рекомендуется это делать. Однако мы не следим за этим. Вы можете держать свои исходные тексты при себе (если только вы не распространяете свой дистрибутив OpenVK среди других людей). - -Вы также не обязаны публиковать исходные тексты ваших тематических пакетов и плагинов. +Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.). ## Где я могу получить помощь? @@ -80,7 +87,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions * [Обсуждения](https://github.com/openvk/openvk/discussions) * Чат в Matrix: #ovk:matrix.org -**Внимание**: баг-трекер, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [at] tutanota [dot] com**. +**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**. Get it on Codeberg diff --git a/VKAPI/Handlers/Account.php b/VKAPI/Handlers/Account.php index ee55e863..7e707cf3 100644 --- a/VKAPI/Handlers/Account.php +++ b/VKAPI/Handlers/Account.php @@ -60,7 +60,7 @@ final class Account extends VKAPIRequestHandler return 1; } - function getAppPermissions(): object + function getAppPermissions(): int { return 9355263; } diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index c9125a47..2e917f2d 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -25,7 +25,7 @@ final class Friends extends VKAPIRequestHandler $usersApi = new Users($this->getUser()); if (!is_null($fields)) { - $response = $usersApi->get(implode(',', $friends), $fields, 0, $count, true); // FIXME + $response = $usersApi->get(implode(',', $friends), $fields, 0, $count); // FIXME } return (object) [ diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 21f4c93b..eb5d3230 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -88,4 +88,100 @@ final class Groups extends VKAPIRequestHandler "items" => $rClubs ]; } + + function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array + { + $this->requireUser(); + + $clubs = new ClubsRepo; + + if ($group_ids == null && $group_id != null) + $group_ids = $group_id; + + if ($group_ids == null && $group_id == null) + $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); + + $clbs = explode(',', $group_ids); + $response; + + $ic = sizeof($clbs); + + for ($i=0; $i < $ic; $i++) { + if($i > 500) + break; + + if($clbs[$i] < 0) + $this->fail(100, "ты ошибся чутка, у айди группы убери минус"); + + $clb = $clubs->get((int) $clbs[$i]); + if(is_null($clb)) + { + $response[$i] = (object)[ + "id" => intval($clbs[$i]), + "name" => "DELETED", + "screen_name" => "club".intval($clbs[$i]), + "type" => "group", + "description" => "This group was deleted or it doesn't exist" + ]; + }else if($clbs[$i] == null){ + + }else{ + $response[$i] = (object)[ + "id" => $clb->getId(), + "name" => $clb->getName(), + "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), + "is_closed" => false, + "type" => "group", + "can_access_closed" => true, + ]; + + $flds = explode(',', $fields); + + foreach($flds as $field) { + switch ($field) { + case 'verified': + $response[$i]->verified = intval($clb->isVerified()); + break; + case 'has_photo': + $response[$i]->has_photo = is_null($clb->getAvatarPhoto()) ? 0 : 1; + break; + case 'photo_max_orig': + $response[$i]->photo_max_orig = $clb->getAvatarURL(); + break; + case 'photo_max': + $response[$i]->photo_max = $clb->getAvatarURL(); + break; + case 'members_count': + $response[$i]->members_count = $clb->getFollowersCount(); + break; + case 'site': + $response[$i]->site = $clb->getWebsite(); + break; + case 'description': + $response[$i]->desctiption = $clb->getDescription(); + break; + case 'contacts': + $contacts; + $contactTmp = $clb->getManagers(1, true); + foreach($contactTmp as $contact) { + $contacts[] = array( + 'user_id' => $contact->getUser()->getId(), + 'desc' => $contact->getComment() + ); + } + $response[$i]->contacts = $contacts; + break; + case 'can_post': + if($clb->canBeModifiedBy($this->getUser())) + $response[$i]->can_post = true; + else + $response[$i]->can_post = $clb->canPost(); + break; + } + } + } + } + + return $response; + } } diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php new file mode 100644 index 00000000..b002075b --- /dev/null +++ b/VKAPI/Handlers/Likes.php @@ -0,0 +1,74 @@ +requireUser(); + + switch ($type) { + case 'post': + $post = (new PostsRepo)->getPostById($owner_id, $item_id); + if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found'); + + $post->setLike(true, $this->getUser()); + return (object)[ + "likes" => $post->getLikesCount() + ]; + break; + default: + $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type'); + break; + } + } + + function remove(string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); + + switch ($type) { + case 'post': + $post = (new PostsRepo)->getPostById($owner_id, $item_id); + if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found'); + + $post->setLike(false, $this->getUser()); + return (object)[ + "likes" => $post->getLikesCount() + ]; + break; + default: + $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type'); + break; + } + } + + function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); + + switch ($type) { + case 'post': + $user = (new UsersRepo)->get($user_id); + if (is_null($user)) return (object)[ + "liked" => 0, + "copied" => 0, + "sex" => 0 + ]; + + $post = (new PostsRepo)->getPostById($owner_id, $item_id); + if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found'); + + return (object)[ + "liked" => (int) $post->hasLikeFrom($user), + "copied" => 0 // TODO: handle this + ]; + break; + default: + $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type'); + break; + } + } +} diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index 2d9b575e..456e31df 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -4,6 +4,7 @@ use openvk\Web\Events\NewMessageEvent; use openvk\Web\Models\Entities\{Correspondence, Message}; use openvk\Web\Models\Repositories\{Messages as MSGRepo, Users as USRRepo}; use openvk\VKAPI\Structures\{Message as APIMsg, Conversation as APIConvo}; +use openvk\VKAPI\Handlers\Users as APIUsers; use Chandler\Signaling\SignalManager; final class Messages extends VKAPIRequestHandler @@ -48,10 +49,12 @@ final class Messages extends VKAPIRequestHandler $rMsg->read_state = 1; $rMsg->out = (int) ($message->getSender()->getId() === $this->getUser()->getId()); $rMsg->body = $message->getText(false); + $rMsg->text = $message->getText(false); $rMsg->emoji = true; if($preview_length > 0) $rMsg->body = ovk_proc_strtr($rMsg->body, $preview_length); + $rMsg->text = ovk_proc_strtr($rMsg->text, $preview_length); $items[] = $rMsg; } @@ -145,12 +148,14 @@ final class Messages extends VKAPIRequestHandler return 1; } - function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0): object + function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0, string $fields = ""): object { $this->requireUser(); $convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset); $list = []; + + $users = []; foreach($convos as $convo) { $correspondents = $convo->getCorrespondents(); if($correspondents[0]->getId() === $this->getUser()->getId()) @@ -189,7 +194,13 @@ final class Messages extends VKAPIRequestHandler $lastMessagePreview->read_state = 1; $lastMessagePreview->out = (int) ($lastMessage->getSender()->getId() === $this->getUser()->getId()); $lastMessagePreview->body = $lastMessage->getText(false); + $lastMessagePreview->text = $lastMessage->getText(false); $lastMessagePreview->emoji = true; + + if($extended == 1) { + $users[] = $lastMessage->getSender()->getId(); + $users[] = $author; + } } $list[] = [ @@ -198,10 +209,20 @@ final class Messages extends VKAPIRequestHandler ]; } - return (object) [ - "count" => sizeof($list), - "items" => $list, - ]; + if($extended == 0){ + return (object) [ + "count" => sizeof($list), + "items" => $list, + ]; + } else { + $users = array_unique($users); + + return (object) [ + "count" => sizeof($list), + "items" => $list, + "profiles" => (new APIUsers)->get(implode(',', $users), $fields, $offset, $count) + ]; + } } function getHistory(int $offset = 0, int $count = 20, int $user_id = -1, int $peer_id = -1, int $start_message_id = 0, int $rev = 0, int $extended = 0): object @@ -230,6 +251,7 @@ final class Messages extends VKAPIRequestHandler $rMsg->read_state = 1; $rMsg->out = (int) ($msgU->sender_id === $this->getUser()->getId()); $rMsg->body = $message->getText(false); + $rMsg->text = $message->getText(false); $rMsg->emoji = true; $results[] = $rMsg; diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php new file mode 100644 index 00000000..dd4fd436 --- /dev/null +++ b/VKAPI/Handlers/Newsfeed.php @@ -0,0 +1,42 @@ +requireUser(); + + if($offset != 0) $start_from = $offset; + + $id = $this->getUser()->getId(); + $subs = DatabaseConnection::i() + ->getContext() + ->table("subscriptions") + ->where("follower", $id); + $ids = array_map(function($rel) { + return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1); + }, iterator_to_array($subs)); + $ids[] = $this->getUser()->getId(); + + $posts = DatabaseConnection::i() + ->getContext() + ->table("posts") + ->select("id") + ->where("wall IN (?)", $ids) + ->where("deleted", 0) + ->order("created DESC"); + + $rposts = []; + foreach($posts->page((int) ($offset + 1), $count) as $post) + $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); + + return (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); + } +} 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/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 705ff3fe..3664903f 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -5,13 +5,15 @@ use openvk\Web\Models\Repositories\Users as UsersRepo; final class Users extends VKAPIRequestHandler { - function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100): array + function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100, User $authuser = null /* костыль(( */): array { - $this->requireUser(); + // $this->requireUser(); + + if($authuser == null) $authuser = $this->getUser(); $users = new UsersRepo; if($user_ids == "0") - $user_ids = (string) $this->getUser()->getId(); + $user_ids = (string) $authuser->getId(); $usrs = explode(',', $user_ids); $response; @@ -51,7 +53,7 @@ final class Users extends VKAPIRequestHandler $response[$i]->verified = intval($usr->isVerified()); break; case 'sex': - $response[$i]->sex = $this->getUser()->isFemale() ? 1 : 2; + $response[$i]->sex = $usr->isFemale() ? 1 : 2; break; case 'has_photo': $response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; @@ -60,8 +62,26 @@ final class Users extends VKAPIRequestHandler $response[$i]->photo_max_orig = $usr->getAvatarURL(); break; case 'photo_max': - $response[$i]->photo_max = $usr->getAvatarURL(); + $response[$i]->photo_max = $usr->getAvatarURL("original"); break; + case 'photo_50': + $response[$i]->photo_50 = $usr->getAvatarURL(); + break; + case 'photo_100': + $response[$i]->photo_50 = $usr->getAvatarURL("tiny"); + break; + case 'photo_200': + $response[$i]->photo_50 = $usr->getAvatarURL("normal"); + break; + case 'photo_200_orig': // вообще не ебу к чему эта строка ну пусть будет кек + $response[$i]->photo_50 = $usr->getAvatarURL("normal"); + break; + case 'photo_400_orig': + $response[$i]->photo_50 = $usr->getAvatarURL("normal"); + break; + + // Она хочет быть выебанной видя матан + // Покайфу когда ты Виет а вокруг лишь дискриминант case 'status': if($usr->getStatus() != null) $response[$i]->status = $usr->getStatus(); @@ -71,10 +91,10 @@ final class Users extends VKAPIRequestHandler $response[$i]->screen_name = $usr->getShortCode(); break; case 'friend_status': - switch($usr->getSubscriptionStatus($this->getUser())) { + switch($usr->getSubscriptionStatus($authuser)) { case 3: case 0: - $response[$i]->friend_status = $usr->getSubscriptionStatus($this->getUser()); + $response[$i]->friend_status = $usr->getSubscriptionStatus($authuser); break; case 1: $response[$i]->friend_status = 2; @@ -158,13 +178,14 @@ final class Users extends VKAPIRequestHandler $users = new UsersRepo; $array = []; + $find = $users->find($q); - foreach ($users->find($q) as $user) { + foreach ($find as $user) { $array[] = $user->getId(); } return (object)[ - "count" => $users->getFoundCount($q), + "count" => $find->size(), "items" => $this->get(implode(',', $array), $fields, $offset, $count) ]; } diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 530c8aa4..6acc9c89 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -22,6 +22,27 @@ final class Wall extends VKAPIRequestHandler foreach ($posts->getPostsFromUsersWall((int)$owner_id, 1, $count, $offset) as $post) { $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); + + $attachments; + foreach($post->getChildren() as $attachment) + { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) + { + $attachments[] = [ + "type" => "photo", + "photo" => [ + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : null, + "date" => $attachment->getPublicationTime()->timestamp(), + "id" => $attachment->getVirtualId(), + "owner_id" => $attachment->getOwner()->getId(), + "sizes" => array_values($attachment->getVkApiSizes()), + "text" => "", + "has_tags" => false + ] + ]; + } + } + $items[] = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, @@ -35,6 +56,7 @@ final class Wall extends VKAPIRequestHandler "can_archive" => false, // TODO MAYBE "is_archived" => false, "is_pinned" => $post->isPinned(), + "attachments" => $attachments, "post_source" => (object)["type" => "vk"], "comments" => (object)[ "count" => $post->getCommentsCount(), @@ -56,6 +78,8 @@ final class Wall extends VKAPIRequestHandler $profiles[] = $from_id; else $groups[] = $from_id * -1; + + $attachments = null; // free attachments so it will not clone everythingg } if($extended == 1) @@ -110,6 +134,169 @@ final class Wall extends VKAPIRequestHandler ]; } + function getById(string $posts, int $extended = 0, string $fields = "", User $user = null) + { + if($user == null) $user = $this->getUser(); // костыли костыли крылышки + + $items = []; + $profiles = []; + $groups = []; + # $count = $posts->getPostCountOnUserWall((int) $owner_id); + + $psts = explode(",", $posts); + + foreach($psts as $pst) + { + $id = explode("_", $pst); + $post = (new PostsRepo)->getPostById(intval($id[0]), intval($id[1])); + if($post) { + $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); + $attachments; + foreach($post->getChildren() as $attachment) + { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) + { + $attachments[] = [ + "type" => "photo", + "photo" => [ + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : null, + "date" => $attachment->getPublicationTime()->timestamp(), + "id" => $attachment->getVirtualId(), + "owner_id" => $attachment->getOwner()->getId(), + "sizes" => array( + [ + "height" => 2560, + "url" => $attachment->getURLBySizeId("normal"), + "type" => "m", + "width" => 2560, + ], + [ + "height" => 130, + "url" => $attachment->getURLBySizeId("tiny"), + "type" => "o", + "width" => 130, + ], + [ + "height" => 604, + "url" => $attachment->getURLBySizeId("normal"), + "type" => "p", + "width" => 604, + ], + [ + "height" => 807, + "url" => $attachment->getURLBySizeId("large"), + "type" => "q", + "width" => 807, + ], + [ + "height" => 1280, + "url" => $attachment->getURLBySizeId("larger"), + "type" => "r", + "width" => 1280, + ], + [ + "height" => 75, // Для временного компросима оставляю статическое число. Если каждый раз обращаться к файлу за количеством пикселов, то наступает пuпuська полная с производительностью, так что пока так + "url" => $attachment->getURLBySizeId("miniscule"), + "type" => "s", + "width" => 75, + ]), + "text" => "", + "has_tags" => false + ] + ]; + } + } + + $items[] = (object)[ + "id" => $post->getVirtualId(), + "from_id" => $from_id, + "owner_id" => $post->getTargetWall(), + "date" => $post->getPublicationTime()->timestamp(), + "post_type" => "post", + "text" => $post->getText(), + "can_edit" => 0, // TODO + "can_delete" => $post->canBeDeletedBy($user), + "can_pin" => $post->canBePinnedBy($user), + "can_archive" => false, // TODO MAYBE + "is_archived" => false, + "is_pinned" => $post->isPinned(), + "post_source" => (object)["type" => "vk"], + "attachments" => $attachments, + "comments" => (object)[ + "count" => $post->getCommentsCount(), + "can_post" => 1 + ], + "likes" => (object)[ + "count" => $post->getLikesCount(), + "user_likes" => (int) $post->hasLikeFrom($user), + "can_like" => 1, + "can_publish" => 1, + ], + "reposts" => (object)[ + "count" => $post->getRepostCount(), + "user_reposted" => 0 + ] + ]; + + if ($from_id > 0) + $profiles[] = $from_id; + else + $groups[] = $from_id * -1; + + $attachments = null; // free attachments so it will not clone everythingg + } + } + + if($extended == 1) + { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach ($profiles as $prof) { + $user = (new UsersRepo)->get($prof); + $profilesFormatted[] = (object)[ + "first_name" => $user->getFirstName(), + "id" => $user->getId(), + "last_name" => $user->getLastName(), + "can_access_closed" => false, + "is_closed" => false, + "sex" => $user->isFemale() ? 1 : 2, + "screen_name" => $user->getShortCode(), + "photo_50" => $user->getAvatarUrl(), + "photo_100" => $user->getAvatarUrl(), + "online" => $user->isOnline() + ]; + } + + foreach($groups as $g) { + $group = (new ClubsRepo)->get($g); + $groupsFormatted[] = (object)[ + "id" => $group->getId(), + "name" => $group->getName(), + "screen_name" => $group->getShortCode(), + "is_closed" => 0, + "type" => "group", + "photo_50" => $group->getAvatarUrl(), + "photo_100" => $group->getAvatarUrl(), + "photo_200" => $group->getAvatarUrl(), + ]; + } + + return (object)[ + "items" => (array)$items, + "profiles" => (array)$profilesFormatted, + "groups" => (array)$groupsFormatted + ]; + } + else + return (object)[ + "items" => (array)$items + ]; + } + function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object { $this->requireUser(); diff --git a/VKAPI/Structures/Message.php b/VKAPI/Structures/Message.php index b728925b..ba022f3f 100644 --- a/VKAPI/Structures/Message.php +++ b/VKAPI/Structures/Message.php @@ -11,10 +11,11 @@ final class Message public $out; public $title = ""; public $body; + public $text; public $attachments = []; public $fwd_messages = []; public $emoji; - public $important = 1; + public $important = true; public $deleted = 0; public $random_id = NULL; } diff --git a/Vagrantfile b/Vagrantfile index bcbf34d9..f8b3e250 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,16 +1,22 @@ # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| - config.vm.box = "freebsd/FreeBSD-12.1-STABLE" + config.vm.box = "freebsd/FreeBSD-13.1-RC2" + config.vm.box_version = "2022.04.07" config.vm.network "forwarded_port", guest: 80, host: 4000 - config.vm.synced_folder ".", "/.ovk_release" - config.vm.provider "virtualbox" do |vb| vb.gui = true - vb.memory = "1024" + vb.cpus = 4 + vb.memory = "1568" + end + + config.vm.provider "vmware_workstation" do |vwx| + vwx.gui = true + vwx.vmx["memsize"] = "1568" + vwx.vmx["numvcpus"] = "4" end - config.vm.provision "shell", inline: "/bin/tcsh /.ovk_release/install/automated/freebsd-12/install" + config.vm.provision "shell", inline: "/bin/tcsh /.ovk_release/install/automated/freebsd-13/install" end diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 0c91b11e..f306a8e6 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -38,12 +38,12 @@ class Club extends RowModel return iterator_to_array($avPhotos)[0] ?? NULL; } - function getAvatarUrl(): string + function getAvatarUrl(string $size = "miniscule"): string { $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(); + return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size); } function getAvatarLink(): string @@ -346,6 +346,11 @@ class Club extends RowModel { return $this->getRecord()->website; } + + function getAlert(): ?string + { + return $this->getRecord()->alert; + } use Traits\TSubscribable; } diff --git a/Web/Models/Entities/Media.php b/Web/Models/Entities/Media.php index d86e6441..9377f3e8 100644 --- a/Web/Models/Entities/Media.php +++ b/Web/Models/Entities/Media.php @@ -6,7 +6,9 @@ abstract class Media extends Postable { protected $fileExtension = "oct"; #octet stream xddd protected $upperNodeReferenceColumnName = "owner"; - + protected $processingPlaceholder = NULL; + protected $processingTime = 30; + function __destruct() { #Remove data, if model wasn't presisted @@ -22,6 +24,11 @@ abstract class Media extends Postable else return OPENVK_ROOT . "/storage/"; } + + protected function checkIfFileIsProcessed(): bool + { + throw new \LogicException("checkIfFileIsProcessed is not implemented"); + } abstract protected function saveFile(string $filename, string $hash): bool; @@ -41,6 +48,10 @@ abstract class Media extends Postable function getURL(): string { + if(!is_null($this->processingPlaceholder)) + if(!$this->isProcessed()) + return "/assets/packages/static/openvk/$this->processingPlaceholder.$this->fileExtension"; + $hash = $this->getRecord()->hash; switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { @@ -55,7 +66,7 @@ abstract class Media extends Postable case "server": $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; return ( - $settings->protocol . + $settings->protocol ?? ovk_scheme() . "://" . $settings->host . $settings->path . substr($hash, 0, 2) . "/$hash.$this->fileExtension" @@ -68,6 +79,26 @@ abstract class Media extends Postable { return $this->getRecord()->description; } + + protected function isProcessed(): bool + { + if(is_null($this->processingPlaceholder)) + return true; + + if($this->getRecord()->processed) + return true; + + $timeDiff = time() - $this->getRecord()->last_checked; + if($timeDiff < $this->processingTime) + return false; + + $res = $this->checkIfFileIsProcessed(); + $this->stateChanges("last_checked", time()); + $this->stateChanges("processed", $res); + $this->save(); + + return $res; + } function isDeleted(): bool { @@ -89,7 +120,17 @@ abstract class Media extends Postable $this->stateChanges("hash", $hash); } - + + function save(): void + { + if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { + $this->stateChanges("processed", 0); + $this->stateChanges("last_checked", time()); + } + + parent::save(); + } + function delete(bool $softly = true): void { $deleteQuirk = ovkGetQuirk("blobs.erase-upon-deletion"); diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php index b33f1238..762ebef8 100644 --- a/Web/Models/Entities/Note.php +++ b/Web/Models/Entities/Note.php @@ -48,6 +48,7 @@ class Note extends Postable "acronym", "blockquote", "cite", + "span", ]); $config->set("HTML.AllowedAttributes", [ "table.summary", @@ -59,6 +60,8 @@ class Note extends Postable "img.style", "div.style", "div.title", + "span.class", + "p.class", ]); $config->set("CSS.AllowedProperties", [ "float", @@ -68,6 +71,9 @@ class Note extends Postable "max-width", "font-weight", ]); + $config->set("Attr.AllowedClasses", [ + "underline", + ]); $purifier = new HTMLPurifier($config); return $purifier->purify($this->getRecord()->source); diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index ecda93f6..71d5efcb 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -1,5 +1,8 @@ getWidth() / $image->getHeight()) > ($px / $py)) { + # For some weird reason using resize with EXACT flag causes system to consume an unholy amount of RAM + $image->crop(0, 0, "100%", (int) ceil(($px * $image->getWidth()) / $py)); + $res[0] = true; + } + } + + if(isset($size["maxSize"])) { + $maxSize = (int) $size["maxSize"]; + $image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); + } else if(isset($size["maxResolution"])) { + $resolution = explode("x", (string) $size["maxResolution"]); + $image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT); + } else { + throw new \RuntimeException("Malformed size description: " . (string) $size["id"]); + } + + $res[1] = $image->getWidth(); + $res[2] = $image->getHeight(); + if($res[1] <= 300 || $res[2] <= 300) + $image->save("$outputDir/" . (string) $size["id"] . ".gif"); + else + $image->save("$outputDir/" . (string) $size["id"] . ".jpeg"); + + imagedestroy($image->getImageResource()); + unset($image); + + return $res; + } + + private function saveImageResizedCopies(string $filename, string $hash): void + { + $dir = dirname($this->pathFromHash($hash)); + $dir = "$dir/$hash" . "_cropped"; + if(!is_dir($dir)) { + @unlink($dir); # Added to transparently bypass issues with dead pesudofolders summoned by buggy SWIFT impls (selectel) + mkdir($dir); + } + + $sizes = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); + if(!$sizes) + throw new \RuntimeException("Could not load photosizes.xml!"); + + $sizesMeta = []; + foreach($sizes->Size as $size) + $sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size); + + $sizesMeta = MessagePack::pack($sizesMeta); + $this->stateChanges("sizes", $sizesMeta); + } + protected function saveFile(string $filename, string $hash): bool { $image = Image::fromFile($filename); if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER))) throw new ISE("Invalid layout: image is too wide/short"); - + + $image->resize(8192, 4320, Image::SHRINK_ONLY | Image::FIT); $image->save($this->pathFromHash($hash), 92, Image::JPEG); + $this->saveImageResizedCopies($filename, $hash); 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"]; @@ -33,7 +98,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 @@ -43,7 +108,131 @@ class Photo extends Media DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete(); } - + + function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array + { + $sizes = $this->getRecord()->sizes; + if(!$sizes || $forceUpdate) { + if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { + $hash = $this->getRecord()->hash; + $this->saveImageResizedCopies($this->pathFromHash($hash), $hash); + $this->save(); + + return $this->getSizes(); + } + + return NULL; + } + + $res = []; + $sizes = MessagePack::unpack($sizes); + foreach($sizes as $id => $meta) { + $url = $this->getURL(); + $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url); + $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg"; + + $res[$id] = (object) [ + "url" => $url, + "width" => $meta[1], + "height" => $meta[2], + "crop" => $meta[0] + ]; + } + + [$x, $y] = $this->getDimensions(); + $res["UPLOADED_MAXRES"] = (object) [ + "url" => $this->getURL(), + "width" => $x, + "height" => $y, + "crop" => false + ]; + + return $res; + } + + function getVkApiSizes(): ?array + { + $res = []; + $sizes = $this->getSizes(); + if(!$sizes) + return NULL; + + $manifest = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); + if(!$manifest) + return NULL; + + $mappings = []; + foreach($manifest->Size as $size) + $mappings[(string) $size["id"]] = (string) $size["vkId"]; + + foreach($sizes as $id => $meta) { + $type = $mappings[$id] ?? $id; + $meta->type = $type; + $res[$type] = $meta; + } + + return $res; + } + + function getURLBySizeId(string $size): string + { + $sizes = $this->getSizes(); + if(!$sizes) + return $this->getURL(); + + $size = $sizes[$size]; + if(!$size) + return $this->getURL(); + + return $size->url; + } + + function getDimensions(): array + { + $x = $this->getRecord()->width; + $y = $this->getRecord()->height; + if(!$x) { # no sizes in database + $hash = $this->getRecord()->hash; + $image = Image::fromFile($this->pathFromHash($hash)); + + $x = $image->getWidth(); + $y = $image->getHeight(); + $this->stateChanges("width", $x); + $this->stateChanges("height", $y); + $this->save(); + } + + return [$x, $y]; + } + + 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->sizes = $this->getVkApiSizes(); + $res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule"); + $res->src = $res->photo_130 = $this->getURLBySizeId("tiny"); + $res->src_big = $res->photo_604 = $this->getURLBySizeId("normal"); + $res->src_xbig = $res->photo_807 = $this->getURLBySizeId("large"); + $res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger"); + $res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original"); + $res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES"); + + return $res; + } + static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo { $photo = new static; @@ -53,10 +242,10 @@ class Photo extends Media $photo->setCreated(time()); $photo->setFile($file); $photo->save(); - + if(!is_null($album)) $album->addPhoto($photo); - + return $photo; } } diff --git a/Web/Models/Entities/Traits/TRichText.php b/Web/Models/Entities/Traits/TRichText.php index 3eaaeaff..c8a85e70 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -55,16 +55,21 @@ trait TRichText { $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; - $text = htmlentities($this->getRecord()->{$contentColumn}, ENT_DISALLOWED | ENT_XHTML); + $text = htmlspecialchars($this->getRecord()->{$contentColumn}, ENT_DISALLOWED | ENT_XHTML); $proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"]; if($html) { if($proc) { $rel = $this->isAd() ? "sponsored" : "ugc"; $text = $this->formatLinks($text); - $text = preg_replace("%@([A-Za-z0-9]++) \(([\p{L} 0-9]+)\)%Xu", "[$1|$2]", $text); + $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%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("%([\n\r\s]|^)(#([\p{L}_-]++[0-9]*[\p{L}_-]*))%Xu", "$1$2", $text); + $text = preg_replace("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", "$2", $text); + $text = preg_replace_callback("%([\n\r\s]|^)(\#([\p{L}_0-9][\p{L}_0-9\(\)\-\']+[\p{L}_0-9\(\)]|[\p{L}_0-9]{1,2}))%Xu", function($m) { + $slug = rawurlencode($m[3]); + + return "$m[1]$m[2]"; + }, $text); + $text = $this->formatEmojis($text); } diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 64b689e4..68ad6a77 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1,5 +1,6 @@ getId(); $query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); - $query .= "\n LIMIT 6 OFFSET " . ( ($page - 1) * 6 ); + $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit ); $rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id); foreach($rels as $rel) { @@ -102,7 +104,7 @@ class User extends RowModel return "/id" . $this->getId(); } - function getAvatarUrl(): string + function getAvatarUrl(string $size = "miniscule"): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; @@ -115,7 +117,7 @@ class User extends RowModel if(is_null($avPhoto)) return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; else - return $avPhoto->getURL(); + return $avPhoto->getURLBySizeId($size); } function getAvatarLink(): string @@ -166,153 +168,169 @@ class User extends RowModel return $this->getFirstName() . $pseudo . $this->getLastName(); } + + function getMorphedName(string $case = "genitive", bool $fullName = true): string + { + $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); + if(!preg_match("%^[А-яё\-]+$%", $name)) + return $name; # name is probably not russian + + $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); + + return $inflected ?: $name; + } function getCanonicalName(): string { - if($this->getRecord()->deleted) + if($this->getRecord()->deleted) return "DELETED"; - else - return $this->getFirstName() . ' ' . $this->getLastName(); + else + return $this->getFirstName() . " " . $this->getLastName(); } - + function getPhone(): ?string { return $this->getRecord()->phone; } - + function getEmail(): ?string { return $this->getRecord()->email; } - + function getOnline(): DateTime { return new DateTime($this->getRecord()->online); } - + function getDescription(): ?string { return $this->getRecord()->about; } - + function getStatus(): ?string { return $this->getRecord()->status; } - + function getShortCode(): ?string { return $this->getRecord()->shortcode; } - + function getAlert(): ?string { return $this->getRecord()->alert; } - + function getBanReason(): ?string { return $this->getRecord()->block_reason; } - + + function getBanInSupportReason(): ?string + { + return $this->getRecord()->block_in_support_reason; + } + function getType(): int { return $this->getRecord()->type; } - + function getCoins(): float { if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) return 0.0; - + return $this->getRecord()->coins; } - + function getRating(): int { return OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"] ? $this->getRecord()->rating : 0; } - + function getReputation(): int { return $this->getRecord()->reputation; } - + function getRegistrationTime(): DateTime { return new DateTime($this->getRecord()->since->getTimestamp()); } - + function getRegistrationIP(): string { return $this->getRecord()->registering_ip; } - + function getHometown(): ?string { return $this->getRecord()->hometown; } - + function getPoliticalViews(): int { return $this->getRecord()->polit_views; } - + function getMaritalStatus(): int { return $this->getRecord()->marital_status; } - + function getContactEmail(): ?string { return $this->getRecord()->email_contact; } - + function getTelegram(): ?string { return $this->getRecord()->telegram; } - + function getInterests(): ?string { return $this->getRecord()->interests; } - + function getFavoriteMusic(): ?string { return $this->getRecord()->fav_music; } - + function getFavoriteFilms(): ?string { return $this->getRecord()->fav_films; } - + function getFavoriteShows(): ?string { return $this->getRecord()->fav_shows; } - + function getFavoriteBooks(): ?string { return $this->getRecord()->fav_books; } - + function getFavoriteQuote(): ?string { return $this->getRecord()->fav_quote; } - + function getCity(): ?string { return $this->getRecord()->city; } - + function getPhysicalAddress(): ?string { return $this->getRecord()->address; } - + function getNotificationOffset(): int { return $this->getRecord()->notification_offset; @@ -327,7 +345,7 @@ class User extends RowModel { return (int)floor((time() - $this->getBirthday()->timestamp()) / YEAR); } - + function get2faSecret(): ?string { return $this->getRecord()["2fa_secret"]; @@ -342,7 +360,7 @@ class User extends RowModel { $this->stateChanges("notification_offset", time()); } - + function getLeftMenuItemStatus(string $id): bool { return (bool) bmask($this->getRecord()->left_menu, [ @@ -359,7 +377,7 @@ class User extends RowModel ], ])->get($id); } - + function getPrivacySetting(string $id): int { return (int) bmask($this->getRecord()->privacy, [ @@ -378,7 +396,7 @@ class User extends RowModel ], ])->get($id); } - + function getPrivacyPermission(string $permission, ?User $user = NULL): bool { $permStatus = $this->getPrivacySetting($permission); @@ -386,7 +404,7 @@ class User extends RowModel return $permStatus === User::PRIVACY_EVERYONE; else if($user->getId() === $this->getId()) return true; - + switch($permStatus) { case User::PRIVACY_ONLY_FRIENDS: return $this->getSubscriptionStatus($user) === User::SUBSCRIPTION_MUTUAL; @@ -397,12 +415,12 @@ class User extends RowModel return false; } } - + function getProfileCompletenessReport(): object { $incompleteness = 0; $unfilled = []; - + if(!$this->getRecord()->status) { $unfilled[] = "status"; $incompleteness += 15; @@ -423,46 +441,46 @@ class User extends RowModel $unfilled[] = "interests"; $incompleteness += 20; } - + $total = max(100 - $incompleteness + $this->getRating(), 0); if(ovkGetQuirk("profile.rating-bar-behaviour") === 0) if ($total >= 100) $percent = round(($total / 10**strlen(strval($total))) * 100, 0); else $percent = min($total, 100); - + return (object) [ "total" => $total, "percent" => $percent, "unfilled" => $unfilled, ]; } - - function getFriends(int $page = 1): \Traversable + + function getFriends(int $page = 1, int $limit = 6): \Traversable { - return $this->_abstractRelationGenerator("get-friends", $page); + return $this->_abstractRelationGenerator("get-friends", $page, $limit); } - + function getFriendsCount(): int { return $this->_abstractRelationCount("get-friends"); } - - function getFollowers(int $page = 1): \Traversable + + function getFollowers(int $page = 1, int $limit = 6): \Traversable { - return $this->_abstractRelationGenerator("get-followers", $page); + return $this->_abstractRelationGenerator("get-followers", $page, $limit); } - + function getFollowersCount(): int { return $this->_abstractRelationCount("get-followers"); } - - function getSubscriptions(int $page = 1): \Traversable + + function getSubscriptions(int $page = 1, int $limit = 6): \Traversable { - return $this->_abstractRelationGenerator("get-subscriptions-user", $page); + return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit); } - + function getSubscriptionsCount(): int { return $this->_abstractRelationCount("get-subscriptions-user"); @@ -472,7 +490,7 @@ class User extends RowModel { return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); } - + function getClubs(int $page = 1, bool $admin = false): \Traversable { if($admin) { @@ -497,7 +515,7 @@ class User extends RowModel } } } - + function getClubCount(bool $admin = false): int { if($admin) { @@ -512,7 +530,7 @@ class User extends RowModel return sizeof($sel); } } - + function getPinnedClubs(): \Traversable { foreach($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) { @@ -553,16 +571,16 @@ class User extends RowModel foreach($sel as $target) { $target = (new Clubs)->get($target->event); if(!$target) continue; - + yield $target; } } - + function getMeetingCount(): int { return sizeof($this->getRecord()->related("event_turnouts.user")); } - + function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable { $gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); @@ -576,7 +594,7 @@ class User extends RowModel ]; } } - + function getGiftCount(): int { return sizeof($this->getRecord()->related("gift_user_relations.receiver")); @@ -611,9 +629,9 @@ class User extends RowModel function use2faBackupCode(int $code): bool { - return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete(); + return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete(); } - + function getSubscriptionStatus(User $user): int { $subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([ @@ -624,71 +642,76 @@ class User extends RowModel "model" => static::class, "follower" => $user->getId(), ])->fetch()); - + if($subbed && $followed) return User::SUBSCRIPTION_MUTUAL; if($subbed) return User::SUBSCRIPTION_INCOMING; if($followed) return User::SUBSCRIPTION_OUTGOING; - + return User::SUBSCRIPTION_ABSENT; } - + function getNotificationsCount(bool $archived = false): int { return (new Notifications)->getNotificationCountByUser($this, $this->getNotificationOffset(), $archived); } - + function getNotifications(int $page, bool $archived = false): \Traversable { return (new Notifications)->getNotificationsByUser($this, $this->getNotificationOffset(), $archived, $page); } - + function getPendingPhoneVerification(): ?ActiveRow { return $this->getRecord()->ref("number_verification", "id"); } - + function getRefLinkId(): string { $hash = hash_hmac("Snefru", (string) $this->getId(), CHANDLER_ROOT_CONF["security"]["secret"], true); - + return dechex($this->getId()) . " " . base64_encode($hash); } - + function getNsfwTolerance(): int { return $this->getRecord()->nsfw_tolerance; } - + function isFemale(): bool { return (bool) $this->getRecord()->sex; } - + function isVerified(): bool { return (bool) $this->getRecord()->verified; } - + function isBanned(): bool { return !is_null($this->getBanReason()); } - + + function isBannedInSupport(): bool + { + return !is_null($this->getBanInSupportReason()); + } + function isOnline(): bool { return time() - $this->getRecord()->online <= 300; } - + function prefersNotToSeeRating(): bool { return !((bool) $this->getRecord()->show_rating); } - + function hasPendingNumberChange(): bool { return !is_null($this->getPendingPhoneVerification()); } - + function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void { DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([ @@ -700,7 +723,7 @@ class User extends RowModel "sent" => time(), ]); } - + function ban(string $reason, bool $deleteSubscriptions = true): void { if($deleteSubscriptions) { @@ -713,42 +736,42 @@ class User extends RowModel ); $subs->delete(); } - + $this->setBlock_Reason($reason); $this->save(); } - + function verifyNumber(string $code): bool { $ver = $this->getPendingPhoneVerification(); if(!$ver) return false; - + try { if(sodium_memcmp((string) $ver->code, $code) === -1) return false; } catch(\SodiumException $ex) { return false; } - + $this->setPhone($ver->number); $this->save(); - + DatabaseConnection::i()->getContext() ->table("number_verification") ->where("user", $this->getId()) ->delete(); - + 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)) @@ -757,15 +780,15 @@ class User extends RowModel 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); } - + function setPrivacySetting(string $id, int $status): void { $this->stateChanges("privacy", bmask($this->changes["privacy"] ?? $this->getRecord()->privacy, [ @@ -784,7 +807,7 @@ class User extends RowModel ], ])->set($id, $status)->toInteger()); } - + function setLeftMenuItemStatus(string $id, bool $status): void { $mask = bmask($this->changes["left_menu"] ?? $this->getRecord()->left_menu, [ @@ -800,10 +823,10 @@ class User extends RowModel "poster", ], ])->set($id, (int) $status)->toInteger(); - + $this->stateChanges("left_menu", $mask); } - + function setShortCode(?string $code = NULL, bool $force = false): ?bool { if(!is_null($code)) { @@ -815,20 +838,20 @@ class User extends RowModel return false; if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") return false; - + $pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch(); if(!is_null($pClub)) return false; } - + $this->stateChanges("shortcode", $code); return true; } - + function setPhoneWithVerification(string $phone): string { $code = unpack("S", openssl_random_pseudo_bytes(2))[1]; - + if($this->hasPendingNumberChange()) { DatabaseConnection::i()->getContext() ->table("number_verification") @@ -839,10 +862,10 @@ class User extends RowModel ->table("number_verification") ->insert(["user" => $this->getId(), "number" => $phone, "code" => $code]); } - + return (string) $code; } - + # KABOBSQL temporary fix # Tuesday, the 7th of January 2020 @ 22:43 : implementing quick fix to this problem and monitoring # NOTICE: this is an ongoing conversation, add your comments just above this line. Thanks! @@ -850,10 +873,10 @@ class User extends RowModel { $this->stateChanges("shortcode", $this->getRecord()->shortcode); #fix KABOBSQL $this->stateChanges("online", $time); - + return true; } - + function adminNotify(string $message): bool { $admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; @@ -861,12 +884,12 @@ class User extends RowModel return false; else if(is_null($admin = (new Users)->get($admId))) return false; - + $cor = new Correspondence($admin, $this); $msg = new Message; $msg->setContent($message); $cor->sendMessage($msg, true); - + return true; } @@ -893,7 +916,7 @@ class User extends RowModel case 2: return 2; break; - + default: return 0; break; diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index 17500e8d..7bf1b010 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -14,6 +14,8 @@ class Video extends Media protected $tableName = "videos"; protected $fileExtension = "ogv"; + + protected $processingPlaceholder = "video/rendering"; protected function saveFile(string $filename, string $hash): bool { @@ -37,11 +39,13 @@ class Video extends Media throw new \DomainException("$filename does not contain any meaningful video streams"); try { - if(!is_dir($dirId = $this->pathFromHash($hash))) + if(!is_dir($dirId = dirname($this->pathFromHash($hash)))) mkdir($dirId); $dir = $this->getBaseDir(); - Shell::bash(__DIR__ . "/../shell/processVideo.sh", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD + $ext = Shell::isPowershell() ? "ps1" : "sh"; + $cmd = Shell::isPowershell() ? "powershell" : "bash"; + Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD } catch(ShellUnavailableException $suex) { exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "Shell is unavailable" : VIDEOS_FRIENDLY_ERROR); } catch(UnknownCommandException $ucex) { @@ -51,7 +55,23 @@ class Video extends Media usleep(200100); return true; } - + + protected function checkIfFileIsProcessed(): bool + { + if($this->getType() != Video::TYPE_DIRECT) + return true; + + if(!file_exists($this->getFileName())) { + if((time() - $this->getRecord()->last_checked) > 3600) { + // TODO notify that video processor is probably dead + } + + return false; + } + + return true; + } + function getName(): string { return $this->getRecord()->name; @@ -81,6 +101,9 @@ class Video extends Media function getThumbnailURL(): string { if($this->getType() === Video::TYPE_DIRECT) { + if(!$this->isProcessed()) + return "/assets/packages/static/openvk/video/rendering.apng"; + return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL()); } else { return $this->getVideoDriver()->getThumbnailURL(); diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php index e1bcc671..99c6c732 100644 --- a/Web/Models/Repositories/Albums.php +++ b/Web/Models/Repositories/Albums.php @@ -1,6 +1,7 @@ context->table("album_relations")->where(["media" => $photo->getId()])->fetch(); + + return $dbalbum->collection ? $this->get($dbalbum->collection) : null; + } } diff --git a/Web/Models/Repositories/Clubs.php b/Web/Models/Repositories/Clubs.php index bea73cef..b7b59251 100644 --- a/Web/Models/Repositories/Clubs.php +++ b/Web/Models/Repositories/Clubs.php @@ -45,7 +45,7 @@ class Clubs function getPopularClubs(): \Traversable { - $query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 10;"; + $query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;"; $entries = DatabaseConnection::i()->getConnection()->query($query); foreach($entries as $entry) diff --git a/Web/Models/Repositories/Notes.php b/Web/Models/Repositories/Notes.php index f1b19eaf..a41c6914 100644 --- a/Web/Models/Repositories/Notes.php +++ b/Web/Models/Repositories/Notes.php @@ -29,7 +29,7 @@ class Notes function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL): \Traversable { $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->page($page, $perPage) as $album) + foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created DESC")->page($page, $perPage) as $album) yield new Note($album); } diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php index ba590baf..94ce6482 100644 --- a/Web/Models/Repositories/Posts.php +++ b/Web/Models/Repositories/Posts.php @@ -94,7 +94,6 @@ class Posts { $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); if(!is_null($post)) - return new Post($post); else return null; diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index 66f9f5ae..9cd7d001 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -39,7 +39,7 @@ class Users function find(string $query): Util\EntityStream { $query = "%$query%"; - $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo) LIKE ?", $query)->where("deleted", 0); + $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0); return new Util\EntityStream("User", $result); } diff --git a/Web/Models/shell/processVideo.ps1 b/Web/Models/shell/processVideo.ps1 new file mode 100644 index 00000000..254f619d --- /dev/null +++ b/Web/Models/shell/processVideo.ps1 @@ -0,0 +1,20 @@ +$ovkRoot = $args[0] +$file = $args[1] +$dir = $args[2] +$hash = $args[3] +$hashT = $hash.substring(0, 2) +$temp = [System.IO.Path]::GetTempFileName() +$temp2 = [System.IO.Path]::GetTempFileName() + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) + +Move-Item $file $temp + +# video stub logic was implicitly deprecated, so we start processing at once +ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif" +ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2 + +Move-Item $temp2 "$dir$hashT/$hash.ogv" +Remove-Item $temp +Remove-Item $temp2 diff --git a/Web/Models/shell/processVideo.sh b/Web/Models/shell/processVideo.sh index 43441b9e..f5542c63 100644 --- a/Web/Models/shell/processVideo.sh +++ b/Web/Models/shell/processVideo.sh @@ -5,7 +5,7 @@ cp ../files/video/rendering.apng $3${4:0:2}/$4.gif cp ../files/video/rendering.ogv $3/${4:0:2}/$4.ogv nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif -nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf scale=640x360,setsar=1:1 -y "/tmp/ffmOi$tmpfile.ogv" +nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.ogv" rm -rf $3${4:0:2}/$4.ogv mv "/tmp/ffmOi$tmpfile.ogv" $3${4:0:2}/$4.ogv diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php index e7351897..cf4572b8 100644 --- a/Web/Presenters/AboutPresenter.php +++ b/Web/Presenters/AboutPresenter.php @@ -126,4 +126,11 @@ final class AboutPresenter extends OpenVKPresenter header("Location: https://github.com/openvk/openvk#readme"); exit; } + + function renderDev(): void + { + header("HTTP/1.1 302 Found"); + header("Location: https://docs.openvk.su/"); + exit; + } } diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 9c356d55..65c93598 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -23,7 +23,7 @@ final class AdminPresenter extends OpenVKPresenter private function warnIfNoCommerce(): void { if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) - $this->flash("warn", "Коммерция отключена системным администратором", "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния."); + $this->flash("warn", tr("admin_commerce_disabled"), tr("admin_commerce_disabled_desc")); } private function searchResults(object $repo, &$count) @@ -346,7 +346,20 @@ final class AdminPresenter extends OpenVKPresenter exit(json_encode([ "error" => "User does not exist" ])); $user->ban($this->queryParam("reason")); - exit(json_encode([ "reason" => $this->queryParam("reason") ])); + exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); + } + + function renderQuickUnban(int $id): void + { + $this->assertNoCSRF(); + + $user = $this->users->get($id); + if(!$user) + exit(json_encode([ "error" => "User does not exist" ])); + + $user->setBlock_Reason(null); + $user->save(); + exit(json_encode([ "success" => true ])); } function renderQuickWarn(int $id): void diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index a80bbc68..8d15d3e2 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -4,6 +4,7 @@ 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\Exceptions\InvalidUserNameException; use openvk\Web\Models\Repositories\IPs; use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\Restores; @@ -88,20 +89,25 @@ final class AuthPresenter extends OpenVKPresenter if (strtotime($this->postParam("birthday")) > time()) $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); + try { + $user = new User; + $user->setFirst_Name($this->postParam("first_name")); + $user->setLast_Name($this->postParam("last_name")); + $user->setSex((int)($this->postParam("sex") === "female")); + $user->setEmail($this->postParam("email")); + $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']); + } catch(InvalidUserNameException $ex) { + $this->flashFail("err", tr("error"), tr("invalid_real_name")); + } + $chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password")); if(!$chUser) $this->flashFail("err", tr("failed_to_register"), tr("user_already_exists")); - - $user = new User; + $user->setUser($chUser->getId()); - $user->setFirst_Name($this->postParam("first_name")); - $user->setLast_Name($this->postParam("last_name")); - $user->setSex((int) ($this->postParam("sex") === "female")); - $user->setEmail($this->postParam("email")); - $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)) { diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 117eebca..21bba2da 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -17,20 +17,23 @@ final class BlobPresenter extends OpenVKPresenter function renderFile(/*string*/ $dir, string $name, string $format) { $dir = $this->getDirName($dir); - $name = preg_replace("%[^a-zA-Z0-9_\-]++%", "", $name); - $path = OPENVK_ROOT . "/storage/$dir/$name.$format"; - if(!file_exists($path)) { + $base = realpath(OPENVK_ROOT . "/storage/$dir"); + $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); + if(!$path) # Will also check if file exists since realpath fails on ENOENT $this->notFound(); - } else { - if(isset($_SERVER["HTTP_IF_NONE_MATCH"])) + else if(strpos($path, $path) !== 0) # Prevent directory traversal and storage container escape + $this->notFound(); + + if(isset($_SERVER["HTTP_IF_NONE_MATCH"])) exit(header("HTTP/1.1 304 Not Modified")); header("Content-Type: " . mime_content_type($path)); header("Content-Size: " . filesize($path)); + header("Cache-Control: public, max-age=1210000"); + header("X-Accel-Expires: 1210000"); header("ETag: W/\"" . hash_file("snefru", $path) . "\""); readfile($path); exit; - } } } diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index e307c134..410f4d5d 100755 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -204,6 +204,8 @@ abstract class OpenVKPresenter extends SimplePresenter $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isTimezoned = Session::i()->get("_timezoneOffset"); + $userValidated = 0; + $cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0; if(!is_null($user)) { $this->user = (object) []; $this->user->raw = $user; @@ -261,6 +263,8 @@ abstract class OpenVKPresenter extends SimplePresenter exit; } + $userValidated = 1; + $cacheTime = 0; # Force no cache if ($this->user->identity->onlineStatus() == 0) { $this->user->identity->setOnline(time()); $this->user->identity->save(); @@ -271,6 +275,8 @@ abstract class OpenVKPresenter extends SimplePresenter $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); } + header("X-OpenVK-User-Validated: $userValidated"); + header("X-Accel-Expires: $cacheTime"); setlocale(LC_TIME, ...(explode(";", tr("__locale")))); parent::onStartup(); diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index b79b7547..aed5fbfd 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -72,6 +72,8 @@ final class PhotosPresenter extends OpenVKPresenter if($_SERVER["REQUEST_METHOD"] === "POST") { if(empty($this->postParam("name"))) $this->flashFail("err", tr("error"), tr("error_segmentation")); + else if(strlen($this->postParam("name")) > 36) + $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); $album = new Album; $album->setOwner(isset($club) ? $club->getId() * -1 : $this->user->id); @@ -100,6 +102,9 @@ final class PhotosPresenter extends OpenVKPresenter $this->template->album = $album; if($_SERVER["REQUEST_METHOD"] === "POST") { + if(strlen($this->postParam("name")) > 36) + $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); + $album->setName(empty($this->postParam("name")) ? $album->getName() : $this->postParam("name")); $album->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $album->setEdited(time()); @@ -276,6 +281,8 @@ final class PhotosPresenter extends OpenVKPresenter $photo->isolate(); $photo->delete(); - exit("Фотография успешно удалена!"); + + $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); + $this->redirect("/id0", static::REDIRECT_TEMPORARY); } } diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index 6af05712..0fc4611f 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -25,6 +25,10 @@ final class SearchPresenter extends OpenVKPresenter $type = $this->queryParam("type") ?? "users"; $page = (int) ($this->queryParam("p") ?? 1); + $this->willExecuteWriteAction(); + if($query != "") + $this->assertUserLoggedIn(); + // https://youtu.be/pSAWM5YuXx8 $repos = [ "groups" => "clubs", "users" => "users" ]; diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index a96ac465..9c2bcc8e 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -1,7 +1,7 @@ template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); } + if($this->template->mode === "new") + $this->template->banReason = $this->user->identity->getBanInSupportReason(); + if($_SERVER["REQUEST_METHOD"] === "POST") { + if($this->user->identity->isBannedInSupport()) + $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); + if(!empty($this->postParam("name")) && !empty($this->postParam("text"))) { $this->willExecuteWriteAction(); @@ -268,4 +274,32 @@ final class SupportPresenter extends OpenVKPresenter exit(header("HTTP/1.1 200 OK")); } -} \ No newline at end of file + + function renderQuickBanInSupport(int $id): void + { + $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); + $this->assertNoCSRF(); + + $user = (new Users)->get($id); + if(!$user) + exit(json_encode([ "error" => "User does not exist" ])); + + $user->setBlock_In_Support_Reason($this->queryParam("reason")); + $user->save(); + $this->returnJson([ "success" => true, "reason" => $this->queryParam("reason") ]); + } + + function renderQuickUnbanInSupport(int $id): void + { + $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); + $this->assertNoCSRF(); + + $user = (new Users)->get($id); + if(!$user) + exit(json_encode([ "error" => "User does not exist" ])); + + $user->setBlock_In_Support_Reason(null); + $user->save(); + $this->returnJson([ "success" => true ]); + } +} diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index c3765d82..b9e8b714 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -31,7 +31,7 @@ final class UserPresenter extends OpenVKPresenter { $user = $this->users->get($id); if(!$user || $user->isDeleted()) - $this->notFound(); + $this->template->_template = "User/deleted.xml"; else { if($user->getShortCode()) if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode()) @@ -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")); } @@ -482,6 +481,22 @@ final class UserPresenter extends OpenVKPresenter $this->flashFail("succ", tr("information_-1"), tr("two_factor_authentication_disabled_message")); } + function renderResetThemepack(): void + { + $this->assertNoCSRF(); + + $this->setSessionTheme(Themepacks::DEFAULT_THEME_ID); + + if($this->user) { + $this->willExecuteWriteAction(); + + $this->user->identity->setStyle(Themepacks::DEFAULT_THEME_ID); + $this->user->identity->save(); + } + + $this->redirect("/", static::REDIRECT_TEMPORARY_PRESISTENT); + } + function renderCoinsTransfer(): void { $this->assertUserLoggedIn(); 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/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 3fb4ba63..7fd59994 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -45,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)) { @@ -54,7 +54,7 @@ final class WallPresenter extends OpenVKPresenter if(!$owner->isBanned()) $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); else - $this->flashFail("err", tr("error"), "Ошибка доступа"); + $this->flashFail("err", tr("error"), tr("forbidden")); } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -89,7 +89,7 @@ final class WallPresenter extends OpenVKPresenter function renderRSS(int $user): 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)) { @@ -98,7 +98,7 @@ final class WallPresenter extends OpenVKPresenter if(!$owner->isBanned()) $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); else - $this->flashFail("err", tr("error"), "Ошибка доступа"); + $this->flashFail("err", tr("error"), tr("forbidden")); } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -213,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; @@ -229,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) { @@ -265,10 +265,18 @@ final class WallPresenter extends OpenVKPresenter } catch(ISE $ex) { $this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик."); } + + if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { + $video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon); + } + } catch(\DomainException $ex) { + $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); + } catch(ISE $ex) { + $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); } - - if(empty($this->postParam("text")) && empty($photos)) - $this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой."); + + if(empty($this->postParam("text")) && !$photo && !$video) + $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { $post = new Post; @@ -281,7 +289,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")); } foreach($photos as $photo) { @@ -300,8 +308,6 @@ final class WallPresenter extends OpenVKPresenter function renderPost(int $wall, int $post_id): void { - $this->assertUserLoggedIn(); - $post = $this->posts->getPostById($wall, $post_id); if(!$post || $post->isDeleted()) $this->notFound(); @@ -313,7 +319,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; @@ -377,7 +383,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; @@ -388,7 +394,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); @@ -405,7 +411,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(); @@ -414,6 +420,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/@CanonicalListView.xml b/Web/Presenters/templates/@CanonicalListView.xml index 9ad98739..a0c8f7d7 100644 --- a/Web/Presenters/templates/@CanonicalListView.xml +++ b/Web/Presenters/templates/@CanonicalListView.xml @@ -4,7 +4,7 @@
- {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} {if sizeof($data) > 0}
diff --git a/Web/Presenters/templates/@MilkshakeListView.xml b/Web/Presenters/templates/@MilkshakeListView.xml index 4473e056..31e366fa 100644 --- a/Web/Presenters/templates/@MilkshakeListView.xml +++ b/Web/Presenters/templates/@MilkshakeListView.xml @@ -3,7 +3,7 @@ {block wrap}
- {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} {if sizeof($data) > 0} diff --git a/Web/Presenters/templates/@error.xml b/Web/Presenters/templates/@error.xml index c1e99bc1..64359f7e 100644 --- a/Web/Presenters/templates/@error.xml +++ b/Web/Presenters/templates/@error.xml @@ -1,5 +1,4 @@ -{var instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} - +{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index e6697c9d..1869c038 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -1,7 +1,7 @@ -{var instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} - +{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} +{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'} - + {ifset title}{include title} - {/ifset}{$instance_name} @@ -98,12 +98,12 @@ <div class="layout"> <div id="xhead" class="dm"></div> - <div class="page_header {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}page_custom_header{/if}"> - <a href="/" class="home_button {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}home_button_custom{/if}" title="{$instance_name}">{$instance_name}</a> + <div class="page_header{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME} page_custom_header{/if}"> + <a href="/" class="home_button{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME} home_button_custom{/if}" title="{$instance_name}">{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if}</a> <div n:if="isset($thisUser) ? (!$thisUser->isBanned() XOR !$thisUser->isActivated()) : true" class="header_navigation"> {ifset $thisUser} <div class="link"> - <a href="/">{_header_home}</a> + <a href="/" title="[Alt+Shift+,]" accesskey=",">{_header_home}</a> </div> <div class="link"> <a href="/search?type=groups">{_header_groups}</a> @@ -122,14 +122,14 @@ </div> <div class="link"> <form action="/search" method="get"> - <input type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" /> + <input type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" /> </form> </div> {else} <div class="link"> <a href="/login">{_header_login}</a> </div> - <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" class="link"> + <div class="link"> <a href="/reg">{_header_registration}</a> </div> <div class="link"> @@ -144,7 +144,7 @@ {ifset $thisUser} {if !$thisUser->isBanned() XOR !$thisUser->isActivated()} <a href="/edit" class="link edit-button">{_edit_button}</a> - <a href="{$thisUser->getURL()}" class="link">{_my_page}</a> + <a href="{$thisUser->getURL()}" class="link" title="{_my_page} [Alt+Shift+.]" accesskey=".">{_my_page}</a> <a href="/friends{$thisUser->getId()}" class="link">{_my_friends} <object type="internal/link" n:if="$thisUser->getFollowersCount() > 0"> <a href="/friends{$thisUser->getId()}?act=incoming"> @@ -161,19 +161,19 @@ </a> <a n:if="$thisUser->getLeftMenuItemStatus('notes')" href="/notes{$thisUser->getId()}" class="link">{_my_notes}</a> <a n:if="$thisUser->getLeftMenuItemStatus('groups')" href="/groups{$thisUser->getId()}" class="link">{_my_groups}</a> - <a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link">{_my_feed}</a> - <a href="/notifications" class="link">{_my_feedback} + <a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link" title="{_my_feed} [Alt+Shift+W]" accesskey="w">{_my_feed}</a> + <a href="/notifications" class="link" title="{_my_feedback} [Alt+Shift+N]" accesskey="n">{_my_feedback} {if $thisUser->getNotificationsCount() > 0} (<b>{$thisUser->getNotificationsCount()}</b>) {/if} </a> <a href="/settings" class="link">{_my_settings}</a> - {var canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} - {var canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} - {var menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} + {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} + {var $canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} + {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} <div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div> - <a href="/admin" class="link" n:if="$canAccessAdminPanel">Админ-панель</a> + <a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a> <a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">Helpdesk {if $helpdeskTicketNotAnsweredCount > 0} (<b>{$helpdeskTicketNotAnsweredCount}</b>) @@ -186,6 +186,14 @@ <div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div> <a n:foreach="$thisUser->getPinnedClubs() as $club" href="{$club->getURL()}" class="link group_link">{$club->getName()}</a> </div> + + <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $thisUser->getCoins() != 0" id="votesBalance"> + {tr("you_still_have_x_points", $thisUser->getCoins())|noescape} + <br /><br /> + + <a href="/settings?act=finance">{_top_up_your_account} »</a> + </div> + <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['enable'] && $thisUser->getLeftMenuItemStatus('poster')" href="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['link']}" > <img src="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['src']}" alt="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['caption']}" class="psa-poster" style="max-width: 100%; margin-top: 50px;" /> </a> @@ -208,7 +216,7 @@ <input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" /> <input type="hidden" name="hash" value="{$csrfToken}" /> <input type="submit" value="{_log_in}" class="button" style="display: inline-block;" /> - <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br> + <a href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br> <a href="/restore">{_forgot_password}</a> </form> {/ifset} @@ -251,7 +259,7 @@ </div> <div class="page_footer"> - {var dbVersion = \Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)} + {var $dbVersion = \Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)} <div class="navigation_footer"> <a href="/about" class="link">{_footer_about_instance}</a> @@ -288,12 +296,44 @@ {/if} <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script> + + <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']['enable']"> + {var $piwik = (object) OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']} + + //<![CDATA[ + (function(window,document,dataLayerName,id){ + window[dataLayerName]=window[dataLayerName]||[],window[dataLayerName].push({ start:(new Date).getTime(),event:"stg.start" });var scripts=document.getElementsByTagName('script')[0],tags=document.createElement('script'); + function stgCreateCookie(a,b,c){ var d="";if(c){ var e=new Date;e.setTime(e.getTime()+24*c*60*60*1e3),d=";expires="+e.toUTCString() }document.cookie=a+"="+b+d+";path=/" } + var isStgDebug=(window.location.href.match("stg_debug")||document.cookie.match("stg_debug"))&&!window.location.href.match("stg_disable_debug");stgCreateCookie("stg_debug",isStgDebug?1:"",isStgDebug?14:-1); + var qP=[];dataLayerName!=="dataLayer"&&qP.push("data_layer_name="+dataLayerName),isStgDebug&&qP.push("stg_debug");var qPString=qP.length>0?("?"+qP.join("&")):""; + tags.async=!0,tags.src={$piwik->container . "/"}+id+".js"+qPString,scripts.parentNode.insertBefore(tags,scripts); + !function(a,n,i){ a[n]=a[n]||{ };for(var c=0;c<i.length;c++)!function(i){ a[n][i]=a[n][i]||{ },a[n][i].api=a[n][i].api||function(){ var a=[].slice.call(arguments,0);"string"==typeof a[0]&&window[dataLayerName].push({ event:n+"."+i+":"+a[0],parameters:[].slice.call(arguments,1) }) } }(i[c]) }(window,"ppms",["tm","cm"]); + })(window,document,{$piwik->layer}, {$piwik->site}); + //]]> + </script> + + <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['matomo']['enable']"> + {var $matomo = (object) OPENVK_ROOT_CONF['openvk']['telemetry']['matomo']} + //<![CDATA[ + var _paq = window._paq = window._paq || []; + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + (function() { + var u="//" + {$matomo->container} + "/"; + _paq.push(['setTrackerUrl', u+'matomo.php']); + _paq.push(['setSiteId', {$matomo->site}]); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); + })(); + //]]> + </script> {ifset bodyScripts} {include bodyScripts} {/ifset} </body> </html> +{/if} {if isset($parentModule) && substr($parentModule, 0, 21) !== 'libchandler:absolute.'} <!-- INCLUDING TEMPLATE FROM PARENTMODULE: {$parentModule} --> diff --git a/Web/Presenters/templates/@listView.xml b/Web/Presenters/templates/@listView.xml index e6d02350..7d69bf00 100644 --- a/Web/Presenters/templates/@listView.xml +++ b/Web/Presenters/templates/@listView.xml @@ -1,70 +1,74 @@ {extends "@layout.xml"} {block wrap} - <div class="page_wrap"> - <div n:ifset="tabs" n:ifcontent class="tabs"> - {include tabs} - </div> +<div class="wrap2"> + <div class="wrap1"> + <div class="page_wrap padding_top"> + <div n:ifset="tabs" class="tabs"> + {include tabs} + </div> - {ifset size} - {include size, x => $dat} - {/ifset} - - {ifset specpage} - {include specpage, x => $dat} - {else} - <div class="container_gray"> - {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {ifset size} + {include size, x => $dat} + {/ifset} - {if sizeof($data) > 0} - <div class="content" n:foreach="$data as $dat"> - <table> - <tbody> - <tr> - <td valign="top"> - <a href="{include link, x => $dat}"> - {include preview, x => $dat} - </a> - </td> - <td valign="top" style="width: 100%"> - {ifset infoTable} - {include infoTable, x => $dat} - {else} + {ifset specpage} + {include specpage, x => $dat} + {else} + <div class="container_gray"> + {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + + {if sizeof($data) > 0} + <div class="content" n:foreach="$data as $dat"> + <table> + <tbody> + <tr> + <td valign="top"> + <a href="{include link, x => $dat}"> + {include preview, x => $dat} + </a> + </td> + <td valign="top" style="width: 100%"> + {ifset infotable} + {include infotable, x => $dat} + {else} <a href="{include link, x => $dat}"> <b> {include name, x => $dat} </b> </a> <br/> - {include description, x => $dat} - {/ifset} - </td> - <td n:ifset="actions" valign="top" class="action_links" style="width: 150px; text-transform: lowercase;"> - {include actions, x => $dat} - </td> - </tr> - </tbody> - </table> - </div> - {include "components/paginator.xml", conf => (object) [ - "page" => $page, - "count" => $count, - "amount" => sizeof($data), - "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, - "atBottom" => true, - ]} - {else} - {ifset customErrorMessage} - {include customErrorMessage} + {include description, x => $dat} + {/ifset} + </td> + <td n:ifset="actions" valign="top" class="action_links" style="width: 150px; text-transform: lowercase;"> + {include actions, x => $dat} + </td> + </tr> + </tbody> + </table> + </div> + {include "components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($data), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "atBottom" => true, + ]} {else} - {include "components/nothing.xml"} - {/ifset} - {/if} - </div> - {/ifset} + {ifset customErrorMessage} + {include customErrorMessage} + {else} + {include "components/nothing.xml"} + {/ifset} + {/if} + </div> + {/ifset} - {ifset bottom} - {include bottom} - {/ifset} + {ifset bottom} + {include bottom} + {/ifset} + </div> </div> -{/block} \ No newline at end of file +</div> +{/block} diff --git a/Web/Presenters/templates/About/AboutInstance.xml b/Web/Presenters/templates/About/AboutInstance.xml index af63d9ee..00cddfcc 100644 --- a/Web/Presenters/templates/About/AboutInstance.xml +++ b/Web/Presenters/templates/About/AboutInstance.xml @@ -9,7 +9,7 @@ <table width="100%" cellspacing="0" cellpadding="0"> <tbody> <tr valign="top"> - <td width="250" {if sizeof($admins) > 0}style="padding-right: 10px;"{/if}> + <td width="250"{if sizeof($admins) > 0} style="padding-right: 10px;"{/if}> <h4>{_statistics}</h4> <div style="margin-top: 5px;"> {_on_this_instance_are} @@ -21,6 +21,15 @@ <li><span>{tr("about_wall_posts", $postsCount)|noescape}</span></li> </ul> </div> + {if OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']} + <h4>{_about_links}</h4> + <div style="margin-top: 5px;"> + {_instance_links} + <ul> + <li n:foreach="OPENVK_ROOT_CONF['openvk']['preferences']['about']['links'] as $aboutLink"><a href="{$aboutLink['url']}" target="_blank" class="link">{$aboutLink["name"]}</a></li> + </ul> + </div> + {/if} </td> <td n:if="sizeof($admins) > 0"> <h4>{_administrators}</h4> @@ -44,14 +53,23 @@ {if sizeof($popularClubs) !== 0} <h4>{_most_popular_groups}</h4> - <ol> - <li n:foreach="$popularClubs as $entry" style="margin-top: 5px;"> - <a href="{$entry->club->getURL()}">{$entry->club->getName()}</a> - <div> - {tr("participants", $entry->subscriptions)} - </div> - </li> - </ol> + {var $entries = array_chunk($popularClubs, 10, true)} + <table width="100%" cellspacing="0" cellpadding="0"> + <tbody> + <tr valign="top"> + <td n:foreach="$entries as $chunk"> + <ol> + <li value="{$num+1}" style="margin-top: 5px;" n:foreach="$chunk as $num => $club"> + <a href="{$club->club->getURL()}">{$club->club->getName()}</a> + <div> + {tr("participants", $club->subscriptions)} + </div> + </li> + </ol> + </td> + </tr> + </tbody> + </table> {/if} <h4>{_rules}</h4> diff --git a/Web/Presenters/templates/About/Index.xml b/Web/Presenters/templates/About/Index.xml index 71feceb7..9df911e7 100644 --- a/Web/Presenters/templates/About/Index.xml +++ b/Web/Presenters/templates/About/Index.xml @@ -9,7 +9,7 @@ {presenter "openvk!Support->knowledgeBaseArticle", "about"} <center> <a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_"log_in"}</a> - <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" class="button" style="cursor: pointer;" href="/reg">{_"registration"}</a> + <a class="button" style="cursor: pointer;" href="/reg">{_"registration"}</a> </center> {* TO-DO: Add statistics about this instance as on mastodon.social *} {/block} diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml index ec8a213c..92de0cc0 100644 --- a/Web/Presenters/templates/Admin/@layout.xml +++ b/Web/Presenters/templates/Admin/@layout.xml @@ -1,12 +1,13 @@ +{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> <style> - {var css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")} + {var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")} {str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape} </style> - <title>{include title} - Админ-панель {=OPENVK_ROOT_CONF['openvk']['appearance']['name']} + {include title} - {_admin} {$instance_name}
@@ -16,23 +17,15 @@
    - - + +
@@ -46,83 +39,64 @@
{ifset $flashMessage} - {var type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]} + {var $type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]}

{$flashMessage->title} @@ -139,11 +113,11 @@

{$flashMessage->msg|noescape}

{/ifset} - + {ifset preHeader} {include preHeader} {/ifset} - +
@@ -167,11 +141,11 @@
- + {script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"} - + {ifset scripts} {include scripts} {/ifset} diff --git a/Web/Presenters/templates/Admin/Club.xml b/Web/Presenters/templates/Admin/Club.xml index c79a3cf7..a8f3ad50 100644 --- a/Web/Presenters/templates/Admin/Club.xml +++ b/Web/Presenters/templates/Admin/Club.xml @@ -1,191 +1,157 @@ {extends "@layout.xml"} {block title} - Редактировать {$club->getCanonicalName()} + {_edit} {$club->getCanonicalName()} {/block} {block heading} {$club->getCanonicalName()} {/block} - {block content} -{var isMain = $mode === 'main'} -{var isBan = $mode === 'ban'} -{var isFollowers = $mode === 'followers'} + {var $isMain = $mode === 'main'} + {var $isBan = $mode === 'ban'} + {var $isFollowers = $mode === 'followers'} -{if $isMain} - - - -
- -
-
- - - - - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- isVerified()} checked {/if} /> - -
-
- isHideFromGlobalFeedEnabled()} checked {/if} /> - -
-
-
-
- - -
-
- -
-{/if} - -{if $isBan} - - - -
- -
-
- - -
-
-
-
- - -
-
- -
-{/if} - -{if $isFollowers} - - - -{var followers = iterator_to_array($followers)} - -
- -
- - - - - - - - - - -
{$follower->getId()} - + {if $isMain} +
+ +
+
+ + - {$follower->getCanonicalName()} + - - {$follower->getCanonicalName()} - - - заблокирован - -
{$follower->isFemale() ? "Женский" : "Мужской"}{$follower->getShortCode() ?? "(отсутствует)"}{$follower->getRegistrationTime()} - - Редактировать - -
-
- {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} - - - ⭁ туда - - - ⭇ сюда - -
-
-{/if} -{/block} \ No newline at end of file +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ isVerified()} checked {/if} /> + +
+
+ isHideFromGlobalFeedEnabled()} checked {/if} /> + +
+
+
+
+ + +
+
+ +
+ {/if} + + {if $isBan} +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ {/if} + + {if $isFollowers} + {var $followers = iterator_to_array($followers)} + +
+ + + + + + + + + + + + +
{$follower->getId()} + + + {$follower->getCanonicalName()} + + + + {$follower->getCanonicalName()} + + {_admin_banned} + {$follower->isFemale() ? tr("female") : tr("male")}{$follower->getShortCode() ?? "(" . tr("none") . ")"}{$follower->getRegistrationTime()} + + {_edit} + +
+
+ {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + + « + » +
+
+ {/if} +{/block} diff --git a/Web/Presenters/templates/Admin/Clubs.xml b/Web/Presenters/templates/Admin/Clubs.xml index 4e9de1cb..d240768b 100644 --- a/Web/Presenters/templates/Admin/Clubs.xml +++ b/Web/Presenters/templates/Admin/Clubs.xml @@ -1,29 +1,31 @@ {extends "@layout.xml"} -{var search = true} +{var $search = true} {block title} - Группы + {_admin_club_search} {/block} {block heading} - Бутылки + {_groups} {/block} -{block searchTitle}Поиск бутылок{/block} +{block searchTitle} + {include title} +{/block} {block content} - {var clubs = iterator_to_array($clubs)} - {var amount = sizeof($clubs)} + {var $clubs = iterator_to_array($clubs)} + {var $amount = sizeof($clubs)} - - - - - - + + + + + + @@ -32,28 +34,28 @@ - + @@ -61,13 +63,9 @@
#ИмяАвторОписаниеКороткий адресДействияID{_admin_title}{_admin_author}{_admin_description}{_admin_shortcode}{_admin_actions}
- {$club->getCanonicalName()} + {$club->getCanonicalName()} {$club->getCanonicalName()} - {var user = $club->getOwner()} + {var $user = $club->getOwner()} - {$user->getCanonicalName()} + {$user->getCanonicalName()} {$user->getCanonicalName()} {$club->getDescription() ?? "(не указано)"}{$club->getDescription() ?? "(" . tr("none") . ")"} {$club->getShortCode()} - Редактировать + {_edit}

- {var isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + {var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} - - ⭁ туда - - - ⭇ сюда - + « + »
{/block} diff --git a/Web/Presenters/templates/Admin/Gift.xml b/Web/Presenters/templates/Admin/Gift.xml index fd05037c..8a20c538 100644 --- a/Web/Presenters/templates/Admin/Gift.xml +++ b/Web/Presenters/templates/Admin/Gift.xml @@ -2,9 +2,9 @@ {block title} {if $form->id === 0} - Новый подарок + {_admin_newgift} {else} - Подарок "{$form->name}" + {_gift} "{$form->name}" {/if} {/block} @@ -16,7 +16,7 @@
{if $form->id === 0} @@ -29,43 +29,39 @@ {/if}
- +
- +
- - + +
@@ -75,13 +71,13 @@
- - + +
- + - +
@@ -94,12 +90,12 @@ {block scripts} diff --git a/Web/Presenters/templates/Admin/GiftCategories.xml b/Web/Presenters/templates/Admin/GiftCategories.xml index 2dd41ad9..d0fe0f30 100644 --- a/Web/Presenters/templates/Admin/GiftCategories.xml +++ b/Web/Presenters/templates/Admin/GiftCategories.xml @@ -1,7 +1,7 @@ {extends "@layout.xml"} {block title} - Наборы подарков + {_admin_giftsets} {/block} {block headingWrap} @@ -9,7 +9,7 @@ {_create} -

Наборы подарков

+

{_admin_giftsets}

{/block} {block content} @@ -27,12 +27,11 @@ - Редактировать + {_edit} - Открыть - Открыть + {_admin_open} @@ -40,17 +39,13 @@ {else}
-

Наборов подарков нету. Чтобы создать подарок, создайте набор.

+

{_admin_giftsets_none}

{/if}
- {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count} - - ⭁ туда - - - ⭇ сюда - + {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count} + « + »
{/block} diff --git a/Web/Presenters/templates/Admin/GiftCategory.xml b/Web/Presenters/templates/Admin/GiftCategory.xml index 936a823f..15494b41 100644 --- a/Web/Presenters/templates/Admin/GiftCategory.xml +++ b/Web/Presenters/templates/Admin/GiftCategory.xml @@ -2,7 +2,7 @@ {block title} {if $form->id === 0} - Создать набор подарков + {_admin_giftsets_create} {else} {$form->languages["master"]->name} {/if} @@ -14,7 +14,7 @@ {block content} -

Общие настройки

+

{_admin_commonsettings}

-
Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.
+
{_admin_giftsets_title}
-
Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.
+
{_admin_giftsets_description}
-

Языко-зависимые настройки

+

{_admin_langsettings}

{foreach $form->languages as $locale => $data} {continueIf $locale === "master"}
diff --git a/Web/Presenters/templates/Admin/Gifts.xml b/Web/Presenters/templates/Admin/Gifts.xml index c068e067..64cde8f7 100644 --- a/Web/Presenters/templates/Admin/Gifts.xml +++ b/Web/Presenters/templates/Admin/Gifts.xml @@ -9,7 +9,7 @@ {_create} -

Набор "{$cat->getName()}"

+

{_admin_giftset} "{$cat->getName()}"

{/block} {block content} @@ -32,11 +32,11 @@ {$gift->getName()} - бесплатный + {_admin_price_free} - {$gift->getPrice()} голосов + {tr("points_amount", $gift->getPrice())} {$gift->getUsages()} раз @@ -71,12 +71,9 @@ {/if}
- {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count} - - ⭁ туда - - - ⭇ сюда - + {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count} + + « + »
{/block} diff --git a/Web/Presenters/templates/Admin/Index.xml b/Web/Presenters/templates/Admin/Index.xml index f860f86f..62019de9 100644 --- a/Web/Presenters/templates/Admin/Index.xml +++ b/Web/Presenters/templates/Admin/Index.xml @@ -1,13 +1,13 @@ {extends "@layout.xml"} {block title} - Сводка + {_admin_overview_summary} {/block} {block heading} - Сводка + {_admin_overview_summary} {/block} {block content} - Да! + ┬─┬︵/(.□.)╯ {/block} diff --git a/Web/Presenters/templates/Admin/User.xml b/Web/Presenters/templates/Admin/User.xml index a43a96ec..e0f4d905 100644 --- a/Web/Presenters/templates/Admin/User.xml +++ b/Web/Presenters/templates/Admin/User.xml @@ -1,7 +1,7 @@ {extends "@layout.xml"} {block title} - Редактировать {$user->getCanonicalName()} + {_edit} {$user->getCanonicalName()} {/block} {block heading} @@ -10,89 +10,70 @@ {block content}
- -
- - - - + +
+ + + + + - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - isVerified()} checked {/if} /> -
-
- - -
-
-
- -
-
- - +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + isVerified()} checked {/if} /> +
+
+ + +
+
+
+ + +
+
+
{/block} diff --git a/Web/Presenters/templates/Admin/Users.xml b/Web/Presenters/templates/Admin/Users.xml index d0e1ece3..f538d7ad 100644 --- a/Web/Presenters/templates/Admin/Users.xml +++ b/Web/Presenters/templates/Admin/Users.xml @@ -1,29 +1,31 @@ {extends "@layout.xml"} -{var search = true} +{var $search = true} {block title} - Пользователи + {_admin_user_search} {/block} {block heading} - Пиздюки + {_users} {/block} -{block searchTitle}Поиск пиздюков{/block} +{block searchTitle} + {include title} +{/block} {block content} - {var users = iterator_to_array($users)} - {var amount = sizeof($users)} + {var $users = iterator_to_array($users)} + {var $amount = sizeof($users)} - - - - - - + + + + + + @@ -32,26 +34,24 @@ - - + + @@ -60,13 +60,9 @@
#ИмяПолКороткий адресДата регистрацииДействияID{_admin_name}{_gender}{_admin_shortcode}{_registration_date}{_admin_actions}
- {$user->getCanonicalName()} + {$user->getCanonicalName()} - + {$user->getCanonicalName()} - - - заблокирован - + + {_admin_banned} {$user->isFemale() ? "Женский" : "Мужской"}{$user->getShortCode() ?? "(отсутствует)"}{$user->isFemale() ? tr("female") : tr("male")}{$user->getShortCode() ?? "(" . tr("none") . ")"} {$user->getRegistrationTime()} - Редактировать + {_edit} {if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)} - + {/if}

- {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} - - - ⭁ туда - - - ⭇ сюда - + {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + + « + »
{/block} diff --git a/Web/Presenters/templates/Admin/Voucher.xml b/Web/Presenters/templates/Admin/Voucher.xml index 163f6111..712fb7e9 100644 --- a/Web/Presenters/templates/Admin/Voucher.xml +++ b/Web/Presenters/templates/Admin/Voucher.xml @@ -5,45 +5,36 @@ {/block} {block heading} - {_edit} №{$form->token ?? "undefined"} + {_edit} #{$form->token ?? "undefined"} {/block} {block content}
-
- +
- + -
Номер состоит из 24 символов, если формат неправильный или поле не заполнено, будет назначен автоматически.
+
{_admin_voucher_serial_desc}
- +
- +
@@ -55,9 +46,9 @@ {/if} -
Количество аккаунтов, которые могут использовать ваучер. Если написать -1, будет Infinity.
+
{_admin_voucher_usages_desc}
- +
@@ -66,7 +57,6 @@
-
@@ -74,15 +64,13 @@ diff --git a/Web/Presenters/templates/Admin/Vouchers.xml b/Web/Presenters/templates/Admin/Vouchers.xml index 2f28fe21..331e0429 100644 --- a/Web/Presenters/templates/Admin/Vouchers.xml +++ b/Web/Presenters/templates/Admin/Vouchers.xml @@ -8,7 +8,7 @@ {_create} - +

{_vouchers}

{/block} @@ -16,13 +16,13 @@
- {$user->getCanonicalName()} + {$user->getCanonicalName()} - + {$user->getCanonicalName()} - - - заблокирован - + + {_admin_banned}
- - - - - - - + + + + + + + @@ -34,28 +34,25 @@
#Серийный номерГолосаРейгтингОсталось использованийСостояниеДействияID{_admin_voucher_serial}{_coins}{_admin_voucher_rating}{_usages_left}{_admin_voucher_status}{_admin_actions}
{$voucher->getRemainingUsages() === INF ? "∞" : $voucher->getRemainingUsages()} {if $voucher->isExpired()} - закончился + {_admin_voucher_status_closed} {else} - активен + {_admin_voucher_status_opened} {/if} - Редактировать + {_edit}

- +
- {var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count} - - ⭁ туда - - - ⭇ сюда - + {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count} + + « + »
{/block} diff --git a/Web/Presenters/templates/Auth/LoginSecondFactor.xml b/Web/Presenters/templates/Auth/LoginSecondFactor.xml index 48462595..05c5d986 100644 --- a/Web/Presenters/templates/Auth/LoginSecondFactor.xml +++ b/Web/Presenters/templates/Auth/LoginSecondFactor.xml @@ -18,7 +18,7 @@ {_code}: - + diff --git a/Web/Presenters/templates/Auth/Register.xml b/Web/Presenters/templates/Auth/Register.xml index bf6ba594..ca47e712 100644 --- a/Web/Presenters/templates/Auth/Register.xml +++ b/Web/Presenters/templates/Auth/Register.xml @@ -47,7 +47,7 @@ {_"gender"}: - {var femalePreferred = OPENVK_ROOT_CONF["openvk"]["preferences"]["femaleGenderPriority"]} + {var $femalePreferred = OPENVK_ROOT_CONF["openvk"]["preferences"]["femaleGenderPriority"]} {_group_display_only_creator}
{_group_display_all_administrators}
{_group_dont_display_administrators_list}
diff --git a/Web/Presenters/templates/Group/Followers.xml b/Web/Presenters/templates/Group/Followers.xml index f8980f7f..90db51c4 100644 --- a/Web/Presenters/templates/Group/Followers.xml +++ b/Web/Presenters/templates/Group/Followers.xml @@ -1,9 +1,9 @@ {extends "../@listView.xml"} {var $Manager = openvk\Web\Models\Entities\Manager::class} -{var iterator = $onlyShowManagers ? $managers : $followers} -{var count = $paginatorConf->count} -{var page = $paginatorConf->page} -{var perPage = 6} +{var $iterator = $onlyShowManagers ? $managers : $followers} +{var $count = $paginatorConf->count} +{var $page = $paginatorConf->page} +{var $perPage = 6} {block title}{_followers} {$club->getCanonicalName()}{/block} @@ -41,7 +41,7 @@ {/block} {block preview} - {$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()} + {$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()} {/block} {block name} @@ -49,8 +49,8 @@ {/block} {block description} - {var user = $x instanceof $Manager ? $x->getUser() : $x} - {var manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} + {var $user = $x instanceof $Manager ? $x->getUser() : $x} + {var $manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} @@ -106,8 +106,8 @@ {/block} {block actions} - {var user = $x instanceof $Manager ? $x->getUser() : $x} - {var manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} + {var $user = $x instanceof $Manager ? $x->getUser() : $x} + {var $manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} {if $club->canBeModifiedBy($thisUser ?? NULL)} {if $manager} @@ -140,4 +140,4 @@ {/if} {/if} -{/block} \ No newline at end of file +{/block} diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index a37a5a1b..5e9acaac 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -14,6 +14,8 @@ {block content}
+
{strpos($alert, "@") === 0 ? tr(substr($alert, 1)) : $alert}
+
{_"information"}
@@ -41,7 +43,7 @@
- {var followersCount = $club->getFollowersCount()} + {var $followersCount = $club->getFollowersCount()}
{_participants} @@ -57,7 +59,7 @@
- {var avatarPhoto = $club->getAvatarPhoto()} - {var avatarLink = ((is_null($avatarPhoto) ? FALSE : $avatarPhoto->isAnonymous()) ? "/photo" . ("s/" . base_convert((string) $avatarPhoto->getId(), 10, 32)) : $club->getAvatarLink())} + {var $avatarPhoto = $club->getAvatarPhoto()} + {var $avatarLink = ((is_null($avatarPhoto) ? FALSE : $avatarPhoto->isAnonymous()) ? "/photo" . ("s/" . base_convert((string) $avatarPhoto->getId(), 10, 32)) : $club->getAvatarLink())} - +
- {var author = $club->getOwner()} + {var $author = $club->getOwner()}
- {var managersCount = $club->getManagersCount(true)} + {var $managersCount = $club->getManagersCount(true)}
{_"administrators"} @@ -163,7 +165,7 @@
- {var user = $manager->getUser()} + {var $user = $manager->getUser()}
@@ -203,7 +205,7 @@
- {var cover = $album->getCoverPhoto()} + {var $cover = $album->getCoverPhoto()} {_my_messages} » {$correspondent->getCanonicalName()}
- {var diff = date_diff(date_create(), date_create('@' . $online))} + {var $diff = date_diff(date_create(), date_create('@' . $online))} {if 5 >= $diff->i} {_online} {else} @@ -44,7 +44,7 @@
{if $correspondent->getId() === $thisUser->getId() || $correspondent->getPrivacyPermission('messages.write', $thisUser)} - {$thisUser->getCanonicalName()} + {$thisUser->getCanonicalName()}
- {$correspondent->getCanonicalName()} + {$correspondent->getCanonicalName()} {else}
{/if} diff --git a/Web/Presenters/templates/Messenger/Index.xml b/Web/Presenters/templates/Messenger/Index.xml index 63461d92..5397fde4 100644 --- a/Web/Presenters/templates/Messenger/Index.xml +++ b/Web/Presenters/templates/Messenger/Index.xml @@ -21,11 +21,11 @@
- {var recipient = $coresp->getCorrespondents()[1]} - {var lastMsg = $coresp->getPreviewMessage()} + {var $recipient = $coresp->getCorrespondents()[1]} + {var $lastMsg = $coresp->getPreviewMessage()}
- Фотография пользователя
- {var _author = $lastMsg->getSender()} + {var $_author = $lastMsg->getSender()}
- Фотография пользователя
diff --git a/Web/Presenters/templates/Notes/Edit.xml b/Web/Presenters/templates/Notes/Edit.xml index b3b37ee2..b010c6bc 100644 --- a/Web/Presenters/templates/Notes/Edit.xml +++ b/Web/Presenters/templates/Notes/Edit.xml @@ -3,7 +3,7 @@ {block title}{_edit_note}{/block} {block header} - {var author = $note->getOwner()} + {var $author = $note->getOwner()} {$author->getCanonicalName()} » {_notes} diff --git a/Web/Presenters/templates/Notes/List.xml b/Web/Presenters/templates/Notes/List.xml index 2fb31c0c..36c48fd3 100644 --- a/Web/Presenters/templates/Notes/List.xml +++ b/Web/Presenters/templates/Notes/List.xml @@ -1,6 +1,6 @@ {extends "../@listView.xml"} -{var iterator = iterator_to_array($notes)} -{var page = $paginatorConf->page} +{var $iterator = iterator_to_array($notes)} +{var $page = $paginatorConf->page} {block title}{_notes}{/block} @@ -34,15 +34,41 @@ {* BEGIN ELEMENTS DESCRIPTION *} {block specpage} + +
- {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} {if sizeof($data) > 0}
diff --git a/Web/Presenters/templates/Notes/View.xml b/Web/Presenters/templates/Notes/View.xml index 6c57d1c0..e168c237 100644 --- a/Web/Presenters/templates/Notes/View.xml +++ b/Web/Presenters/templates/Notes/View.xml @@ -3,7 +3,7 @@ {block title}{$note->getName()}{/block} {block header} - {var author = $note->getOwner()} + {var $author = $note->getOwner()} {$author->getCanonicalName()} » {_notes} @@ -12,7 +12,7 @@ {/block} {block content} - {var author = $note->getOwner()} + {var $author = $note->getOwner()}
diff --git a/Web/Presenters/templates/Notification/Feed.xml b/Web/Presenters/templates/Notification/Feed.xml index cb2474da..34de4ecf 100644 --- a/Web/Presenters/templates/Notification/Feed.xml +++ b/Web/Presenters/templates/Notification/Feed.xml @@ -1,5 +1,5 @@ {extends "../@listView.xml"} -{var sorting = false} +{var $sorting = false} {block title} {_feedback} @@ -26,7 +26,7 @@ {/block} {block preview} - + {/block} {block name} diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml index 3655f6fa..47b1f983 100644 --- a/Web/Presenters/templates/Photos/Album.xml +++ b/Web/Presenters/templates/Photos/Album.xml @@ -3,7 +3,7 @@ {block title}Альбом {$album->getName()}{/block} {block header} - {var isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)} + {var $isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)} {$album->getOwner()->getCanonicalName()} diff --git a/Web/Presenters/templates/Photos/AlbumList.xml b/Web/Presenters/templates/Photos/AlbumList.xml index 7db097d0..1c9bc790 100644 --- a/Web/Presenters/templates/Photos/AlbumList.xml +++ b/Web/Presenters/templates/Photos/AlbumList.xml @@ -1,16 +1,35 @@ {extends "../@listView.xml"} -{var iterator = iterator_to_array($albums)} -{var page = $paginatorConf->page} +{var $iterator = iterator_to_array($albums)} +{var $page = $paginatorConf->page} {block title}{_"albums"} {$owner->getCanonicalName()}{/block} {block header} - {$owner->getCanonicalName()} - » {_"albums"} - -
- {var isClub = ($owner instanceof openvk\Web\Models\Entities\Club)} - {_"create_album"} + {if isset($thisUser) && $thisUser->getId() == $owner->getId()} + {_my_photos} + {else} + + {$owner->getCanonicalName()} + » + {_albums} + {/if} +{/block} + +{block size} +
+
+ {if !is_null($thisUser) && $owner->getId() === $thisUser->getId()} + {tr("albums_list", $count)} + {else} + {tr("albums", $count)} + {/if} + + +  |  + {var $isClub = ($owner instanceof \openvk\Web\Models\Entities\Club)} + {_create_album} + +
{/block} @@ -25,8 +44,8 @@ {/block} {block preview} - {var cover = $x->getCoverPhoto()} - {var preview = is_null($cover) ? "/assets/packages/static/openvk/img/camera_200.png" : $cover->getURL()} + {var $cover = $x->getCoverPhoto()} + {var $preview = is_null($cover) ? "/assets/packages/static/openvk/img/camera_200.png" : $cover->getURLBySizeId("normal")} {$x->getName()} diff --git a/Web/Presenters/templates/Photos/Photo.xml b/Web/Presenters/templates/Photos/Photo.xml index dbd89013..a28f0f90 100644 --- a/Web/Presenters/templates/Photos/Photo.xml +++ b/Web/Presenters/templates/Photos/Photo.xml @@ -21,7 +21,7 @@ {block content}
- +

diff --git a/Web/Presenters/templates/Search/Index.xml b/Web/Presenters/templates/Search/Index.xml index 0a346f71..1814f6b7 100644 --- a/Web/Presenters/templates/Search/Index.xml +++ b/Web/Presenters/templates/Search/Index.xml @@ -50,7 +50,7 @@ {/block} {block preview} - {_ + {_ {/block} {block name} diff --git a/Web/Presenters/templates/Support/AnswerTicket.xml b/Web/Presenters/templates/Support/AnswerTicket.xml index a35ad6a6..f2fbfed7 100644 --- a/Web/Presenters/templates/Support/AnswerTicket.xml +++ b/Web/Presenters/templates/Support/AnswerTicket.xml @@ -47,7 +47,7 @@ {if $comment->getUType() === 0} - + {else} @@ -70,7 +70,7 @@ {if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
- {var lastName = $comment->getUser()->getLastName()} + {var $lastName = $comment->getUser()->getLastName()} {if empty(trim($lastName))} ({$comment->getUser()->getFirstName()}) {else} diff --git a/Web/Presenters/templates/Support/Index.xml b/Web/Presenters/templates/Support/Index.xml index 8f0f8737..3524da40 100644 --- a/Web/Presenters/templates/Support/Index.xml +++ b/Web/Presenters/templates/Support/Index.xml @@ -6,9 +6,9 @@ {/block} {block content} - {var isMain = $mode === 'faq'} - {var isNew = $mode === 'new'} - {var isList = $mode === 'list'} + {var $isMain = $mode === 'faq'} + {var $isNew = $mode === 'new'} + {var $isList = $mode === 'list'} {if $thisUser}
@@ -26,16 +26,26 @@
{if $isNew} -
-
-
-

-

- -

-
-
-
+ {if !is_null($banReason)} +
+ {_'banned_alt'} +
+

+ {tr("banned_in_support_1", htmlentities($thisUser->getCanonicalName()))|noescape}
+ {tr("banned_in_support_2", htmlentities($banReason))|noescape} +

+ {else} +
+
+
+

+

+ +

+
+
+
+ {/if} {/if} {/if} diff --git a/Web/Presenters/templates/Support/List.xml b/Web/Presenters/templates/Support/List.xml index 06146230..cd8c9981 100644 --- a/Web/Presenters/templates/Support/List.xml +++ b/Web/Presenters/templates/Support/List.xml @@ -37,7 +37,7 @@ {/block} {block description} - {var author = $x->getUser()} + {var $author = $x->getUser()} {ovk_proc_strtr($x->getContext(), 50)}
{_author}:
{$author->getCanonicalName()} diff --git a/Web/Presenters/templates/Support/View.xml b/Web/Presenters/templates/Support/View.xml index 4ef1ce2b..b239c351 100644 --- a/Web/Presenters/templates/Support/View.xml +++ b/Web/Presenters/templates/Support/View.xml @@ -60,7 +60,7 @@ {if $comment->getUType() === 0} - + {else} @@ -109,7 +109,7 @@ {if $comment->getUType() === 1}
- {var isLikedByUser = $comment->isLikedByUser()} + {var $isLikedByUser = $comment->isLikedByUser()} {if !is_null($isLikedByUser)} {if $comment->isLikedByUser()} diff --git a/Web/Presenters/templates/Topics/Board.xml b/Web/Presenters/templates/Topics/Board.xml index 1afeabb8..eb6c4396 100644 --- a/Web/Presenters/templates/Topics/Board.xml +++ b/Web/Presenters/templates/Topics/Board.xml @@ -1,6 +1,6 @@ {extends "../@listView.xml"} -{var iterator = iterator_to_array($topics)} -{var page = $paginatorConf->page} +{var $iterator = iterator_to_array($topics)} +{var $page = $paginatorConf->page} {block title}{_discussions} {$club->getCanonicalName()}{/block} @@ -46,7 +46,7 @@
{tr("messages", $x->getCommentsCount())}
- {var lastComment = $x->getLastComment()} + {var $lastComment = $x->getLastComment()}
diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml index 96d12f75..3f19d38d 100644 --- a/Web/Presenters/templates/User/Edit.xml +++ b/Web/Presenters/templates/User/Edit.xml @@ -7,10 +7,10 @@ {block content} -{var isMain = $mode === 'main'} -{var isContacts = $mode === 'contacts'} -{var isInterests = $mode === 'interests'} -{var isAvatar = $mode === 'avatar'} +{var $isMain = $mode === 'main'} +{var $isContacts = $mode === 'contacts'} +{var $isInterests = $mode === 'interests'} +{var $isAvatar = $mode === 'avatar'}
Подтверждение номера телефона
Введите код для подтверждения смены номера:
ввести код. diff --git a/Web/Presenters/templates/User/Friends.xml b/Web/Presenters/templates/User/Friends.xml index 94a5f524..4bbedfe4 100644 --- a/Web/Presenters/templates/User/Friends.xml +++ b/Web/Presenters/templates/User/Friends.xml @@ -1,17 +1,17 @@ {extends "../@listView.xml"} -{var perPage = 6} {* Why 6? Check User::_abstractRelationGenerator *} +{var $perPage = 6} {* Why 6? Check User::_abstractRelationGenerator *} -{var act = $_GET["act"] ?? "friends"} +{var $act = $_GET["act"] ?? "friends"} {if $act == "incoming"} - {var iterator = iterator_to_array($user->getFollowers($page))} - {var count = $user->getFollowersCount()} + {var $iterator = iterator_to_array($user->getFollowers($page))} + {var $count = $user->getFollowersCount()} {elseif $act == "outcoming"} - {var iterator = iterator_to_array($user->getSubscriptions($page))} - {var count = $user->getSubscriptionsCount()} + {var $iterator = iterator_to_array($user->getSubscriptions($page))} + {var $count = $user->getSubscriptionsCount()} {else} - {var iterator = iterator_to_array($user->getFriends($page))} - {var count = $user->getFriendsCount()} + {var $iterator = iterator_to_array($user->getFriends($page))} + {var $count = $user->getFriendsCount()} {/if} {block title} @@ -25,13 +25,17 @@ {/block} {block header} - {$user->getCanonicalName()} » - {if $act == "incoming"} - {_"incoming_req"} - {elseif $act == "outcoming"} - {_"outcoming_req"} + {if isset($thisUser) && $thisUser->getId() == $user->getId()} + {_my_friends} {else} - {_"friends"} + {$user->getCanonicalName()} » + {if $act == "incoming"} + {_"incoming_req"} + {elseif $act == "outcoming"} + {_"outcoming_req"} + {else} + {_"friends"} + {/if} {/if} {/block} @@ -39,11 +43,38 @@ -
- {_incoming_req} +
+ {_req}
-
- {_outcoming_req} +{/block} + +{block size} +
+
+ +
+ +
+
+
+ {if !is_null($thisUser) && $user->getId() === $thisUser->getId()} + {if $act == "incoming"} + {tr("req", $count)} + {elseif $act == "outcoming"} + {tr("req", $count)} + {else} + {tr("friends_list", $count)} + {/if} + {else} + {tr("friends", $count)} + {/if} +
{/block} @@ -54,7 +85,7 @@ {/block} {block preview} - Фотография группы + Фотография пользователя {/block} {block name} @@ -82,7 +113,7 @@ {block actions} {if $x->getId() !== $thisUser->getId()} - {var subStatus = $x->getSubscriptionStatus($thisUser)} + {var $subStatus = $x->getSubscriptionStatus($thisUser)} {if $subStatus === 0}