Merge branch 'master' into disco

This commit is contained in:
n1rwana 2022-09-08 15:22:41 +03:00 committed by GitHub
commit 3e119ccbc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
254 changed files with 9305 additions and 3330 deletions

View file

@ -1,11 +1,8 @@
<?xml encoding="UTF-8"?>
<!ELEMENT latte (tags,filters,variables,functions)>
<!ATTLIST latte vendor #REQUIRED>
<!ATTLIST latte version #REQUIRED>
<!ELEMENT tags (tag)+>
<!ELEMENT tag (arguments)?>
<!ATTLIST tag name CDATA #REQUIRED>
<!ATTLIST tag type (PAIR|UNPAIRED|UNPAIRED_ATTR|ATTR_ONLY|AUTO_EMPTY) #REQUIRED>
@ -13,32 +10,24 @@
<!ATTLIST tag arguments CDATA #IMPLIED>
<!ATTLIST tag deprecatedMessage CDATA #IMPLIED>
<!ATTLIST tag multiLine (true|false) #IMPLIED>
<!ELEMENT arguments (argument)+>
<!ELEMENT argument EMPTY>
<!ATTLIST argument name #REQUIRED>
<!ATTLIST argument types CDATA #REQUIRED>
<!ATTLIST argument repeatable (true|false) #IMPLIED>
<!ATTLIST argument required (true|false) #IMPLIED>
<!ATTLIST argument validType #IMPLIED>
<!ELEMENT filters (filter)+>
<!ELEMENT filter EMPTY>
<!ATTLIST filter name #REQUIRED>
<!ATTLIST filter description CDATA #IMPLIED>
<!ATTLIST filter arguments CDATA #IMPLIED>
<!ATTLIST filter insertColons #IMPLIED>
<!ELEMENT variables (variable)+>
<!ELEMENT variable EMPTY>
<!ATTLIST variable name #REQUIRED>
<!ATTLIST variable type CDATA #REQUIRED>
<!ELEMENT functions (function)+>
<!ELEMENT function EMPTY>
<!ATTLIST function name #REQUIRED>
<!ATTLIST function arguments CDATA #REQUIRED>

View file

@ -38,6 +38,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/morphos" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View file

@ -39,6 +39,7 @@
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/wapmorgan/morphos" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">

103
CLI/FetchToncoinTransactions.php Executable file
View file

@ -0,0 +1,103 @@
<?php declare(strict_types=1);
namespace openvk\CLI;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Entities\Notifications\CoinsTransferNotification;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Nette\Utils\ImageException;
define("NANOTON", 1000000000);
class FetchToncoinTransactions extends Command
{
private $images;
protected static $defaultName = "fetch-ton";
function __construct()
{
$this->transactions = DatabaseConnection::i()->getContext()->table("cryptotransactions");
parent::__construct();
}
protected function configure(): void
{
$this->setDescription("Fetches TON transactions to top up the users' balance")
->setHelp("This command checks for new transactions on TON Wallet and then top up the balance of specified users");
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$header = $output->section();
$header->writeln([
"TONCOIN Fetcher",
"=====================",
"",
]);
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["enabled"]) {
$header->writeln("Sorry, but you handn't enabled the TON support in your config file yet.");
return Command::FAILURE;
}
$testnetSubdomain = OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["testnet"] ? "testnet." : "";
$url = "https://" . $testnetSubdomain . "toncenter.com/api/v2/getTransactions?";
$opts = [
"http" => [
"method" => "GET",
"header" => "Accept: application/json"
]
];
$selection = $this->transactions->select('hash, lt')->order("id DESC")->limit(1)->fetch();
$trHash = $selection->hash ?? NULL;
$trLt = $selection->lt ?? NULL;
$data = http_build_query([
"address" => OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["address"],
"limit" => 100,
"hash" => $trHash,
"to_lt" => $trLt
]);
$response = file_get_contents($url . $data, false, stream_context_create($opts));
$response = json_decode($response, true);
$header->writeln("Gonna up the balance of users");
foreach($response["result"] as $transfer) {
$outputArray;
preg_match('/' . OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["regex"] . '/', $transfer["in_msg"]["message"], $outputArray);
$userId = ctype_digit($outputArray[1]) ? intval($outputArray[1]) : NULL;
if(is_null($userId)) {
$header->writeln("Well, that's a donation. Thanks! XD");
} else {
$user = (new Users)->get($userId);
if(!$user) {
$header->writeln("Well, that's a donation. Thanks! XD");
} else {
$value = ($transfer["in_msg"]["value"] / NANOTON) / OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["rate"];
$user->setCoins($user->getCoins() + $value);
$user->save();
(new CoinsTransferNotification($user, (new Users)->get(OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]), (int) $value, "Via TON cryptocurrency"))->emit();
$header->writeln($value . " coins are added to " . $user->getId() . " user id");
$this->transactions->insert([
"id" => NULL,
"hash" => $transfer["transaction_id"]["hash"],
"lt" => $transfer["transaction_id"]["lt"]
]);
}
}
}
$header->writeln("Processing finished :3");
return Command::SUCCESS;
}
}

11
CODE_OF_CONFLICT.md Normal file
View file

@ -0,0 +1,11 @@
The OpenVK development effort is a very personal process compared to "traditional" ways of developing software.
Your code and ideas behind it will be carefully reviewed, often resulting in critique and criticism.
The review will almost always require improvements to the code before it can be included in the repo.
Know that this happens because everyone involved wants to see the best possible solution for the overall success of OpenVK.
If however, anyone feels personally abused, threatened, or otherwise uncomfortable due to this process, that is not acceptable.
If so, please contact the community manager @WerySkok and report the issue.
As a reviewer of code, please strive to keep things civil and focused on the technical issues involved. We are all humans,
and frustrations can be high on both sides of the process.
Try to keep in mind the immortal words of Bill and Ted, "Be excellent to each other."

277
CODE_STYLE.md Normal file
View file

@ -0,0 +1,277 @@
# Names
## Namespace Names
Namespaces should be written in PascalCase.
## File Names
Code directories should have their name written in PascalCase. Code files should contain only one class and have the name of that class.
In case of multiple class definitions in one file, it's name should be the same as the "primary" class name.
Non-code directories, non-class and non-code files should be named in lisp-case.
## Variable Names
Variable names should be written in camelCase. This also applies to function arguments, class instance names and methods.
## Constant Names
Constants are written in SCREAMING_SNAKE_CASE, but should be declared case-insensetive.
## Class Names
Classes in OpenVK should belong to `openvk\` namespace and be in the corresponding directory (according to PSR-4). Names of classes should be written in PascalCase.
## Function Names
camelCase and snake_case are allowed, but first one is the recommended way. This rule does not apply to class methods, which are written in camelCase only.
---
# Coding Rules
## File header
All OpenVK files must start with `<?php` open-tag followed by `declare(strict_types=1);` on the same line. The next line must contain namespace.
The lines after must contain use-declarations, each on it's own line (usage of {} operator is OK), if there is any. Then there must be an empty line. Example:
```php
<?php declare(strict_types=1);
namespace openvk;
use Chandler\Database\DatabaseConnection;
use Nette\Utils\{Image, FileSystem};
class ...
```
## NULL
Null should be written as constant, all-uppercase: `NULL`.
## Nullable (optional) pointer arguments
Optional pointer arguments should default to `nullptr`: `function something(&int? $myPointer = nullptr)`. `nullptr` must be written in lisp-case (lowercase).
## Comparing to NULL
In OpenVK `is_null` is the preferred way to check for equality to NULL. `??` must be used in assignments and where else possible.
In case if value can either be NULL or truthy, "boolean not" should be used to check if value is not null: `if(!$var)`.
## Arrays
Arrays must be defined with modern syntax: `[1, 2, 3]` (NOT `array(1, 2, 3)`).
Same applies to `list` construction: use `[$a, , $b] = $arr;` instead of `list($a, , $b) = $arr;`
## Casts
Typecasts must be done with modern syntax where possible: `(type) $var`. Int-to-string, string-to-int, etc conversions should also be dont with modern casting
syntax where possible, but should use `ctype_` functions where needed. `gettype`, `settype` should be used in dynamic programming only.
## Goto
```goto``` should be avoided.
## `continue n; `
It is preferable to use `continue n`, `break n` instead of guarding flags:
```php
# COOL AND GOOD
foreach($a as $b)
foreach($b as $c)
if($b == $c)
break 2;
# BRUH
foreach($a as $b) {
$shouldBreak = false;
foreach($b as $c)
if($b == $c)
$shouldBreak = true;
if($shouldBreak)
break;
}
```
## Comments
In OpenVK we use Perl-style `#` for single-line comments.
---
# Formatting
## Variables
It is preferable to declare only one variable per line in the code.
## Indentation
All things in OpenVK must be properly indented by a sequence of 4 spaces. Not tabs. \
When there are several variable declarations listed together, line up the variables:
```php
# OK
$photos = (new Photos)->where("meow", true);
$photo = $photos->fetch();
$arr = [
"a" => 10,
"bb" => true,
];
# NOT OK
$photos = (new Photos)->where("meow", true);
$photo = $photos->fetch();
$arr = [
"a" => 10,
"bb" => true,
];
```
## Tab/Space
+ **Do not use tabs**. Use spaces, as tabs are defined differently for different editors and printers.
+ Put one space after a comma and semicolons: `exp(1, 2)` `for($i = 1; $i < 100; $i++)`
+ Put one space around assignment operators: `$a = 1`
+ Always put a space around conditional operators: `$a = ($a > $b) ? $a : $b`
+ Do not put spaces between unary operators and their operands, primary operators and keywords:
```php
# OK
-$a;
$a++;
$b[1] = $a;
fun($b);
if($a) { ... }
# NOT OK
- $a;
$a ++;
$b [1] = $a;
fun ($b);
if ($a) { ... }
```
## Blank Lines
+ Use blank lines to create paragraphs in the code or comments to make the code more understandable
+ Use blank lines before `return` statement if it isn't the only statement in the block
+ Use blank lines after shorthand if/else/etc
```php
# OK
if($a)
return $x;
doSomething();
return "yay";
# NOT OK
if($a) return $x; # return must be on separate line
doSomething(); # doSomething must be separated by an extra blank line after short if/else
return "yay"; # do use blank lines before return statement
```
## Method/Function Arguments
+ When all arguments for a function do not fit on one line, try to line up the first argument in each line:
![image](https://user-images.githubusercontent.com/34442450/167248563-21fb01be-181d-48b9-ac0c-dc953c0a12cf.png)
+ If the argument lists are still too long to fit on the line, you may line up the arguments with the method name instead.
## Maximum characters per line
Lines should be no more than 80 characters long.
## Usage of curly braces
+ Curly braces should be on separate line for class, method, and function definitions.
+ In loops, if/else, try/catch, switch constructions the opening brace should be on the same line as the operator.
+ Braces must be ommited if the block contains only one statement **AND** the related blocks are also single statemented.
+ Nested single-statement+operator blocks must not be surrounded by braces.
```php
# OK
class A
{
function doSomethingFunny(): int
{
return 2;
}
}
if(true) {
doSomething();
doSomethingElse();
} else {
doSomethingFunny();
}
if($a)
return false;
else
doSomething();
foreach($b as $c => $d)
if($c == $d)
unset($b[$c]);
# NOT OK
class A {
function doSomethingFunny(): int {
return 2;
}
}
if(true) {
doSomething();
doSomethingElse();
} else
doSomethingFunny(); # why?
if($a) {
return false;
} else {
doSomething();
}
foreach($b as $c => $d) {
if($c == $d)
unset($b[$c]);
}
# lmao
if($a) { doSomething(); } else doSomethingElse();
```
## if/else, try/catch
+ Operators must not be indented with space from their operands but must have 1-space margin from braces:
```php
# OK
if($a) {
doSomething();
doSomethingElse();
} else if($b) {
try {
nukeSaintPetersburg('😈');
} finally {
return PEACE;
}
}
# NOT OK
if ($a) { # do not add space between control flow operator IF and it's operand
doSomething();
doSomethingElse();
}elseif($b){ # do add margin from braces; also ELSE and IF should be separate here
try{
nukeSaintPetersburg('😈');
}finally{
return PEACE;
}
}
```
## Switches
+ `break` must be on same indentation level as the code of le case (not the case definiton itself)
+ If there is no need to `break` a comment `# NOTICE falling through` must be places instead
```php
# OK
switch($a) {
case 1:
echo $a;
break;
case 2:
echo $a++;
# NOTICE falling through
default:
echo "c";
}
# NOT OK
switch($a) {
case 1:
echo $a;
break;
case 2:
echo $a++;
default:
echo "c";
}
```

View file

@ -0,0 +1,204 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Подтверждение изменения Email</title>
<link rel="stylesheet" href="foundation.css" />
<style>
.container {
border-top: 5px solid pink;
}
</style>
</head>
<body>
<table class="body" data-made-with-foundation="">
<tr>
<td class="float-center" align="center" valign="top">
<center>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<table class="container">
<tr>
<td>
<table class="row header">
<tr>
<th class="small-12 large-12 columns first last">
<table>
<tr>
<th>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<h4 class="text-center">Подтверждение изменения Email</h4>
</th>
</tr>
</table>
</th>
</tr>
</table>
<table class="row">
<tr>
<th class="small-12 large-12 columns first last">
<table class="row">
<tr>
<td>
<center>
<img src="pictures/lock.jpeg" align="center" class="float-center" width=128 height=128 />
</center>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<hr/>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
Здравствуйте, {$name}! Вы вероятно изменили свой адрес электронной почты в OpenVK. Чтобы изменение вступило в силу, необходимо подтвердить ваш новый Email.
</p>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<table class="button large expand success">
<tr>
<td>
<table>
<tr>
<td>
<center>
<a href="http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={rawurlencode($key)}" align="center" class="float-center">Подтвердить Email!</a>
</center>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
Если кнопка не работает, вы можете попробовать скопировать и вставить эту ссылку в адресную строку вашего веб-обозревателя:
</p>
<table class="callout">
<tr>
<th class="callout-inner primary">
<a href="http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key}" style="color: #000; text-decoration: none;">
http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key}
</a>
</th>
</tr>
</table>
<p class="text-left">
Обратите внимание на то, что эту ссылку нельзя:
</p>
<ul>
<li>Передавать другим людям (даже друзьям, питомцам, соседам, любимым девушкам)</li>
<li>Использовать, если прошло более двух дней с её генерации</li>
</ul>
<table class="callout">
<tr>
<th class="callout-inner alert">
<p>
Ещё раз <b>обратите внимание</b> на то, что данную ссылку или письмо <b>ни в коем случае нельзя</b> передавать другим людям! Даже если они представляются службой поддержки.<br/>
Это письмо предназначено исключительно для одноразового, <b>непосредственного</b> использования владельцем аккаунта.
</p>
</th>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-right">
С уважением, овк-тян.
</p>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<hr/>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
<small>
Вы получили это письмо так как кто-то или вы изменили адрес электронной почты. Это не рассылка и от неё нельзя отписаться. Если вы всё равно хотите перестать получать подобные письма, деактивируйте ваш аккаунт.
</small>
</p>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
</body>
</html>

View file

@ -12,13 +12,15 @@ To be honest, we don't know whether it even works. However, this version is main
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://github.com/openvk/archive/actions/workflows/nightly.yml)
* 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 (<https://t.me/openvkch/1609>)
* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Can I create my own OpenVK instance?
@ -32,29 +34,35 @@ 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. (edit: it does not work).
* PHP 8.1 is supported, but it was not tested carefully, be aware of that.
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 the **same database** you installed Chandler to and import all sqls from `install/sqls` to the **same database**
5. Import `install/init-event-db.sql` to a **separate database** (Yandex.Clickhouse can also be used, higly recommended)
6. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking
7. Run `composer install` in OpenVK directory
8. Run `composer install` in commitcaptcha directory
9. Move to `Web/static/js` and execute `yarn install`
10. 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):

View file

@ -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).
## Когда релиз?
## Когда выйдет релизная версия?
Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете:
* Сделать `git clone` master ветки этой репозитории (используйте `git pull` для обновления)
* Взять готовую сборку OpenVK из [GitHub Actions](https://github.com/openvk/archive/actions/workflows/nightly.yml)
* Склонировать 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 (<https://t.me/openvkch/1609>)
* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Могу ли я создать свою собственную инстанцию OpenVK?
@ -32,35 +34,41 @@ _[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` в **отдельную базу данных** (Яндекс.Clickhouse также может быть использован, настоятельно рекомендуется)
6. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры
7. Запустите `composer install` в директории OpenVK
8. Запустите `composer install` в директории commitcaptcha
9. Перейдите в `Web/static/js` и выполните `yarn install`
10. Установите `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`
* Перед использованием встроенной учетной записи рекомендуется сменить пароль.
* Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её.
💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)).
@ -74,7 +82,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Баг-трекер](https://github.com/openvk/openvk/projects/1)
* [Помощь в OVK](https://openvk.su/support?act=new)
* Telegram-чат: Перейдите на [наш канал](https://t.me/openvkch) и откройте обсуждение в меню нашего канала.
* Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала.
* [Reddit](https://www.reddit.com/r/openvk/)
* [Обсуждения](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org
@ -82,5 +90,5 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
<a href="https://codeberg.org/OpenVK/openvk">
<img alt="Получить на Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">
</a>

87
ServiceAPI/Apps.php Normal file
View file

@ -0,0 +1,87 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Applications;
class Apps implements Handler
{
private $user;
private $apps;
public function __construct(?User $user)
{
$this->user = $user;
$this->apps = new Applications;
}
function getUserInfo(callable $resolve, callable $reject): void
{
$hexId = dechex($this->user->getId());
$sign = hash_hmac("sha512/224", $hexId, CHANDLER_ROOT_CONF["security"]["secret"], true);
$marketingId = $hexId . "_" . base64_encode($sign);
$resolve([
"id" => $this->user->getId(),
"marketing_id" => $marketingId,
"name" => [
"first" => $this->user->getFirstName(),
"last" => $this->user->getLastName(),
"full" => $this->user->getFullName(),
],
"ava" => $this->user->getAvatarUrl(),
]);
}
function updatePermission(int $app, string $perm, string $state, callable $resolve, callable $reject): void
{
$app = $this->apps->get($app);
if(!$app || !$app->isEnabled()) {
$reject("No application with this id found");
return;
}
if(!$app->setPermission($this->user, $perm, $state == "yes"))
$reject("Invalid permission $perm");
$resolve(1);
}
function pay(int $appId, float $amount, callable $resolve, callable $reject): void
{
$app = $this->apps->get($appId);
if(!$app || !$app->isEnabled()) {
$reject("No application with this id found");
return;
}
$coinsLeft = $this->user->getCoins() - $amount;
if($coinsLeft < 0) {
$reject(41, "Not enough money");
return;
}
$this->user->setCoins($coinsLeft);
$this->user->save();
$app->addCoins($amount);
$t = time();
$resolve($t . "," . hash_hmac("whirlpool", "$appId:$amount:$t", CHANDLER_ROOT_CONF["security"]["secret"]));
}
function withdrawFunds(int $appId, callable $resolve, callable $reject): void
{
$app = $this->apps->get($appId);
if(!$app) {
$reject("No application with this id found");
return;
} else if($app->getOwner()->getId() != $this->user->getId()) {
$reject("You don't have rights to edit this app");
return;
}
$coins = $app->getBalance();
$app->withdrawCoins();
$resolve($coins);
}
}

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Posts;
@ -55,4 +56,19 @@ class Wall implements Handler
$resolve((array) $res);
}
function newStatus(string $text, callable $resolve, callable $reject): void
{
$post = new Post;
$post->setOwner($this->user->getId());
$post->setWall($this->user->getId());
$post->setCreated(time());
$post->setContent($text);
$post->setAnonymous(false);
$post->setFlags(0);
$post->setNsfw(false);
$post->save();
$resolve($post->getId());
}
}

View file

@ -13,9 +13,9 @@ final class Account extends VKAPIRequestHandler
"last_name" => $this->getUser()->getLastName(),
"home_town" => $this->getUser()->getHometown(),
"status" => $this->getUser()->getStatus(),
"bdate" => "1.1.1970", // TODO
"bdate_visibility" => 0, // TODO
"phone" => "+420 ** *** 228", // TODO
"bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(),
"sex" => $this->getUser()->isFemale() ? 1 : 2
];
@ -25,12 +25,10 @@ final class Account extends VKAPIRequestHandler
{
$this->requireUser();
// Цiй метод є заглушка
return (object) [
"2fa_required" => 0,
"country" => "CZ", // TODO
"eu_user" => false, // TODO
"2fa_required" => $this->getUser()->is2faEnabled() ? 1 : 0,
"country" => "CZ", # TODO
"eu_user" => false, # TODO
"https_required" => 1,
"intro" => 0,
"community_comments" => false,
@ -47,6 +45,7 @@ final class Account extends VKAPIRequestHandler
$this->requireUser();
$this->getUser()->setOnline(time());
$this->getUser()->save();
return 1;
}
@ -55,7 +54,7 @@ final class Account extends VKAPIRequestHandler
{
$this->requireUser();
// Цiй метод є заглушка
# Цiй метод є заглушка
return 1;
}
@ -73,6 +72,6 @@ final class Account extends VKAPIRequestHandler
"messages" => $this->getUser()->getUnreadMessagesCount()
];
// TODO: Filter
# TODO: Filter
}
}

View file

@ -316,6 +316,20 @@ final class Audio extends VKAPIRequestHandler
"count" => sizeof($items),
"items" => $items,
];
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
return (object) [
"count" => 1,
"items" => [(object) [
"id" => 1,
"owner_id" => 1,
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ",
"title" => "ЖДИТЕ :)))",
"duration" => 22,
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3"
]]
];
}
function getLyrics(int $lyrics_id): object

View file

@ -1,6 +1,5 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Friends extends VKAPIRequestHandler
@ -15,7 +14,7 @@ final class Friends extends VKAPIRequestHandler
$this->requireUser();
foreach ($users->get($user_id)->getFriends($offset, $count) as $friend) {
foreach($users->get($user_id)->getFriends($offset, $count) as $friend) {
$friends[$i] = $friend->getId();
$i++;
}
@ -24,9 +23,8 @@ final class Friends extends VKAPIRequestHandler
$usersApi = new Users($this->getUser());
if (!is_null($fields)) {
$response = $usersApi->get(implode(',', $friends), $fields, 0, $count); // FIXME
}
if(!is_null($fields))
$response = $usersApi->get(implode(',', $friends), $fields, 0, $count); # FIXME
return (object) [
"count" => $users->get($user_id)->getFriendsCount(),
@ -70,33 +68,28 @@ final class Friends extends VKAPIRequestHandler
$this->requireUser();
$users = new UsersRepo;
$user = $users->get(intval($user_id));
if(is_null($user)){
if(is_null($user)) {
$this->fail(177, "Cannot add this user to friends as user not found");
} else if($user->getId() == $this->getUser()->getId()) {
$this->fail(174, "Cannot add user himself as friend");
}
switch ($user->getSubscriptionStatus($this->getUser())) {
switch($user->getSubscriptionStatus($this->getUser())) {
case 0:
$user->toggleSubscription($this->getUser());
return 1;
break;
case 1:
$user->toggleSubscription($this->getUser());
return 2;
break;
case 3:
return 2;
break;
default:
return 1;
break;
}
}
@ -108,15 +101,13 @@ final class Friends extends VKAPIRequestHandler
$user = $users->get(intval($user_id));
switch ($user->getSubscriptionStatus($this->getUser())) {
switch($user->getSubscriptionStatus($this->getUser())) {
case 3:
$user->toggleSubscription($this->getUser());
return 1;
break;
default:
fail(15, "Access denied: No friend or friend request found.");
break;
}
}
@ -130,25 +121,9 @@ final class Friends extends VKAPIRequestHandler
$response = [];
for ($i=0; $i < sizeof($friends); $i++) {
for($i=0; $i < sizeof($friends); $i++) {
$friend = $users->get(intval($friends[$i]));
$status = 0;
switch ($friend->getSubscriptionStatus($this->getUser())) {
case 3:
case 0:
$status = $friend->getSubscriptionStatus($this->getUser());
break;
case 1:
$status = 2;
break;
case 2:
$status = 1;
break;
}
$response[] = (object)[
"friend_status" => $friend->getSubscriptionStatus($this->getUser()),
"user_id" => $friend->getId()
@ -157,4 +132,32 @@ final class Friends extends VKAPIRequestHandler
return $response;
}
function getRequests(string $fields = "", int $offset = 0, int $count = 100): object
{
$this->requireUser();
$i = 0;
$offset++;
$followers = [];
foreach($this->getUser()->getFollowers() as $follower) {
$followers[$i] = $follower->getId();
$i++;
}
$response = $followers;
$usersApi = new Users($this->getUser());
if(!is_null($fields))
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count); # FIXME
foreach($response as $user)
$user->user_id = $user->id;
return (object) [
"count" => $this->getUser()->getFollowersCount(),
"items" => $response
];
}
}

View file

@ -1,12 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\Clubs;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\Postable;
use openvk\Web\Models\Repositories\Posts as PostsRepo;
final class Groups extends VKAPIRequestHandler
{
@ -14,44 +9,44 @@ final class Groups extends VKAPIRequestHandler
{
$this->requireUser();
if ($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club) {
if($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club)
$clbs[] = $club;
}
$clbsCount = $this->getUser()->getClubCount();
} else {
$users = new UsersRepo;
$user = $users->get($user_id);
if (is_null($user)) {
if(is_null($user))
$this->fail(15, "Access denied");
}
foreach($user->getClubs($offset+1) as $club) {
foreach($user->getClubs($offset+1) as $club)
$clbs[] = $club;
}
$clbsCount = $user->getClubCount();
}
$rClubs;
$ic = sizeof($clbs);
if(sizeof($clbs) > $count)
$ic = $count;
if(sizeof($clbs) > $count) $ic = $count;
if(!empty($clbs)) {
$clbs = array_slice($clbs, $offset * $count);
for ($i=0; $i < $ic; $i++) {
for($i=0; $i < $ic; $i++) {
$usr = $clbs[$i];
if(is_null($usr))
{
if(is_null($usr)) {
$rClubs[$i] = (object)[
"id" => $clbs[$i],
"name" => "DELETED",
"deactivated" => "deleted"
];
}else if($clbs[$i] == null){
} else if($clbs[$i] == NULL) {
}else{
$rClubs[$i] = (object)[
} else {
$rClubs[$i] = (object) [
"id" => $usr->getId(),
"name" => $usr->getName(),
"screen_name" => $usr->getShortCode(),
@ -62,26 +57,44 @@ final class Groups extends VKAPIRequestHandler
$flds = explode(',', $fields);
foreach($flds as $field) {
switch ($field) {
case 'verified':
switch($field) {
case "verified":
$rClubs[$i]->verified = intval($usr->isVerified());
break;
case 'has_photo':
case "has_photo":
$rClubs[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1;
break;
case 'photo_max_orig':
case "photo_max_orig":
$rClubs[$i]->photo_max_orig = $usr->getAvatarURL();
break;
case 'photo_max':
$rClubs[$i]->photo_max = $usr->getAvatarURL();
case "photo_max":
$rClubs[$i]->photo_max = $usr->getAvatarURL("original"); // ORIGINAL ANDREI CHINITEL 🥵🥵🥵🥵
break;
case 'members_count':
case "photo_50":
$rClubs[$i]->photo_50 = $usr->getAvatarURL();
break;
case "photo_100":
$rClubs[$i]->photo_100 = $usr->getAvatarURL("tiny");
break;
case "photo_200":
$rClubs[$i]->photo_200 = $usr->getAvatarURL("normal");
break;
case "photo_200_orig":
$rClubs[$i]->photo_200_orig = $usr->getAvatarURL("normal");
break;
case "photo_400_orig":
$rClubs[$i]->photo_400_orig = $usr->getAvatarURL("normal");
break;
case "members_count":
$rClubs[$i]->members_count = $usr->getFollowersCount();
break;
}
}
}
}
} else {
$rClubs = [];
}
return (object) [
"count" => $clbsCount,
@ -91,14 +104,12 @@ final class Groups extends VKAPIRequestHandler
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array
{
$this->requireUser();
$clubs = new ClubsRepo;
if ($group_ids == null && $group_id != null)
if($group_ids == NULL && $group_id != NULL)
$group_ids = $group_id;
if ($group_ids == null && $group_id == null)
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);
@ -106,7 +117,7 @@ final class Groups extends VKAPIRequestHandler
$ic = sizeof($clbs);
for ($i=0; $i < $ic; $i++) {
for($i=0; $i < $ic; $i++) {
if($i > 500)
break;
@ -114,8 +125,7 @@ final class Groups extends VKAPIRequestHandler
$this->fail(100, "ты ошибся чутка, у айди группы убери минус");
$clb = $clubs->get((int) $clbs[$i]);
if(is_null($clb))
{
if(is_null($clb)) {
$response[$i] = (object)[
"id" => intval($clbs[$i]),
"name" => "DELETED",
@ -123,9 +133,9 @@ final class Groups extends VKAPIRequestHandler
"type" => "group",
"description" => "This group was deleted or it doesn't exist"
];
}else if($clbs[$i] == null){
} else if($clbs[$i] == NULL) {
}else{
} else {
$response[$i] = (object)[
"id" => $clb->getId(),
"name" => $clb->getName(),
@ -138,45 +148,66 @@ final class Groups extends VKAPIRequestHandler
$flds = explode(',', $fields);
foreach($flds as $field) {
switch ($field) {
case 'verified':
switch($field) {
case "verified":
$response[$i]->verified = intval($clb->isVerified());
break;
case 'has_photo':
case "has_photo":
$response[$i]->has_photo = is_null($clb->getAvatarPhoto()) ? 0 : 1;
break;
case 'photo_max_orig':
case "photo_max_orig":
$response[$i]->photo_max_orig = $clb->getAvatarURL();
break;
case 'photo_max':
case "photo_max":
$response[$i]->photo_max = $clb->getAvatarURL();
break;
case 'members_count':
case "photo_50":
$response[$i]->photo_50 = $clb->getAvatarURL();
break;
case "photo_100":
$response[$i]->photo_100 = $clb->getAvatarURL("tiny");
break;
case "photo_200":
$response[$i]->photo_200 = $clb->getAvatarURL("normal");
break;
case "photo_200_orig":
$response[$i]->photo_200_orig = $clb->getAvatarURL("normal");
break;
case "photo_400_orig":
$response[$i]->photo_400_orig = $clb->getAvatarURL("normal");
break;
case "members_count":
$response[$i]->members_count = $clb->getFollowersCount();
break;
case 'site':
case "site":
$response[$i]->site = $clb->getWebsite();
break;
case 'description':
case "description":
$response[$i]->desctiption = $clb->getDescription();
break;
case 'contacts':
case "contacts":
$contacts;
$contactTmp = $clb->getManagers(1, true);
foreach($contactTmp as $contact) {
foreach($contactTmp as $contact)
$contacts[] = array(
'user_id' => $contact->getUser()->getId(),
'desc' => $contact->getComment()
"user_id" => $contact->getUser()->getId(),
"desc" => $contact->getComment()
);
}
$response[$i]->contacts = $contacts;
break;
case 'can_post':
case "can_post":
if(!is_null($this->getUser()))
if($clb->canBeModifiedBy($this->getUser()))
$response[$i]->can_post = true;
else
$response[$i]->can_post = $clb->canPost();
break;
case "is_member":
if(!is_null($this->getUser()))
$response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser());
break;
}
}
}

View file

@ -9,39 +9,38 @@ final class Likes extends VKAPIRequestHandler
{
$this->requireUser();
switch ($type) {
case 'post':
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');
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)[
return (object) [
"likes" => $post->getLikesCount()
];
break;
default:
$this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
break;
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
}
function remove(string $type, int $owner_id, int $item_id): object
function delete(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
switch ($type) {
case 'post':
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');
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)[
return (object) [
"likes" => $post->getLikesCount()
];
break;
default:
$this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
break;
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
}
@ -49,26 +48,26 @@ final class Likes extends VKAPIRequestHandler
{
$this->requireUser();
switch ($type) {
case 'post':
switch($type) {
case "post":
$user = (new UsersRepo)->get($user_id);
if (is_null($user)) return (object)[
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');
if (is_null($post))
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
return (object)[
return (object) [
"liked" => (int) $post->hasLikeFrom($user),
"copied" => 0 // TODO: handle this
"copied" => 0 # TODO: handle this
];
break;
default:
$this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
break;
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
}
}
}

View file

@ -99,7 +99,7 @@ final class Messages extends VKAPIRequestHandler
if(!$peer)
$this->fail(936, "There is no peer with this id");
if($this->getUser()->getId() !== $peer->getId() && $peer->getSubscriptionStatus($this->getUser()) !== 3)
if($this->getUser()->getId() !== $peer->getId() && !$peer->getPrivacyPermission('messages.write', $this->getUser()))
$this->fail(945, "This chat is disabled because of privacy settings");
# Finally we get to send a message!
@ -123,9 +123,8 @@ final class Messages extends VKAPIRequestHandler
$items = [];
foreach($ids as $id) {
$message = $msgs->get((int) $id);
if(!$message || $message->getSender()->getId() !== $this->getUser()->getId() && $message->getRecipient()->getId() !== $this->getUser()->getId()) {
if(!$message || $message->getSender()->getId() !== $this->getUser()->getId() && $message->getRecipient()->getId() !== $this->getUser()->getId())
$items[$id] = 0;
}
$message->delete();
$items[$id] = 1;
@ -153,6 +152,7 @@ final class Messages extends VKAPIRequestHandler
$this->requireUser();
$convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset);
$convosCount = (new MSGRepo)->getCorrespondenciesCount($this->getUser());
$list = [];
$users = [];
@ -198,7 +198,6 @@ final class Messages extends VKAPIRequestHandler
$lastMessagePreview->emoji = true;
if($extended == 1) {
$users[] = $lastMessage->getSender()->getId();
$users[] = $author;
}
}
@ -211,21 +210,81 @@ final class Messages extends VKAPIRequestHandler
if($extended == 0){
return (object) [
"count" => sizeof($list),
"count" => $convosCount,
"items" => $list,
];
} else {
$users[] = $this->getUser()->getId();
$users = array_unique($users);
return (object) [
"count" => sizeof($list),
"count" => $convosCount,
"items" => $list,
"profiles" => (new APIUsers)->get(implode(',', $users), $fields, $offset, $count)
"profiles" => (!empty($users) ? (new APIUsers)->get(implode(',', $users), $fields, 0, $count+1) : [])
];
}
}
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
function getConversationsById(string $peer_ids, int $extended = 0, string $fields = "")
{
$this->requireUser();
$peers = explode(',', $peer_ids);
$output = [
"count" => 0,
"items" => []
];
$userslist = [];
foreach($peers as $peer) {
if(key($peers) > 100)
continue;
if(is_null($user_id = $this->resolvePeer((int) $peer)))
$this->fail(-151, "Chats are not implemented");
$user = (new USRRepo)->get((int) $peer);
$dialogue = new Correspondence($this->getUser(), $user);
$iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false);
$msg = $iterator[0]->unwrap(); // шоб удобнее было
$output['items'][] = [
"peer" => [
"id" => $user->getId(),
"type" => "user",
"local_id" => $user->getId()
],
"last_message_id" => $msg->id,
"in_read" => $msg->id,
"out_read" => $msg->id,
"sort_id" => [
"major_id" => 0,
"minor_id" => $msg->id, // КОНЕЧНО ЖЕ
],
"last_conversation_message_id" => $user->getId(),
"in_read_cmid" => $user->getId(),
"out_read_cmid" => $user->getId(),
"is_marked_unread" => $iterator[0]->isUnread(),
"important" => false, // целестора когда релиз
"can_write" => [
"allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true)
]
];
$userslist[] = $user->getId();
}
if($extended == 1) {
$userslist = array_unique($userslist);
$output['profiles'] = (!empty($userslist) ? (new APIUsers)->get(implode(',', $userslist), $fields) : []);
}
$output['count'] = sizeof($output['items']);
return (object) $output;
}
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, string $fields = ""): object
{
$this->requireUser();
@ -257,10 +316,18 @@ final class Messages extends VKAPIRequestHandler
$results[] = $rMsg;
}
return (object) [
$output = [
"count" => sizeof($results),
"items" => $results,
];
if ($extended == 1) {
$users[] = $this->getUser()->getId();
$users[] = $user_id;
$output["profiles"] = (!empty($users) ? (new APIUsers($this->getUser()))->get(implode(',', $users), $fields) : []);
}
return (object) $output;
}
function getLongPollHistory(int $ts = -1, int $preview_length = 0, int $events_limit = 1000, int $msgs_limit = 1000): object

View file

@ -1,8 +1,5 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\Postable;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\VKAPI\Handlers\Wall;
@ -13,8 +10,6 @@ final class Newsfeed extends VKAPIRequestHandler
{
$this->requireUser();
if($offset != 0) $start_from = $offset;
$id = $this->getUser()->getId();
$subs = DatabaseConnection::i()
->getContext()
@ -31,12 +26,15 @@ final class Newsfeed extends VKAPIRequestHandler
->select("id")
->where("wall IN (?)", $ids)
->where("deleted", 0)
->where("id < (?)", empty($start_from) ? time()+1 : $start_from)
->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());
$response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser());
$response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно)
return $response;
}
}

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\{Users as UsersRepo, Clubs as ClubsRepo, Posts as PostsRepo};
final class Ovk extends VKAPIRequestHandler
{
@ -21,4 +22,54 @@ final class Ovk extends VKAPIRequestHandler
{
return "крылышки";
}
function aboutInstance(string $fields = "statistics,administrators,popular_groups,links", string $admin_fields = "", string $group_fields = ""): object
{
$fields = explode(',', $fields);
$response = (object) [];
if(in_array("statistics", $fields)) {
$usersStats = (new UsersRepo)->getStatistics();
$clubsCount = (new ClubsRepo)->getCount();
$postsCount = (new PostsRepo)->getCount();
$response->statistics = (object) [
"users_count" => $usersStats->all,
"online_users_count" => $usersStats->online,
"active_users_count" => $usersStats->active,
"groups_count" => $clubsCount,
"wall_posts_count" => $postsCount
];
}
if(in_array("administrators", $fields)) {
$admins = iterator_to_array((new UsersRepo)->getInstanceAdmins());
$adminsResponse = (new Users($this->getUser()))->get(implode(',', array_map(function($admin) {
return $admin->getId();
}, $admins)), $admin_fields, 0, sizeof($admins));
$response->administrators = (object) [
"count" => sizeof($admins),
"items" => $adminsResponse
];
}
if(in_array("popular_groups", $fields)) {
$popularClubs = iterator_to_array((new ClubsRepo)->getPopularClubs());
$clubsResponse = (new Groups($this->getUser()))->getById(implode(',', array_map(function($entry) {
return $entry->club->getId();
}, $popularClubs)), "", "members_count, " . $group_fields);
$response->popular_groups = (object) [
"count" => sizeof($popularClubs),
"items" => $clubsResponse
];
}
if(in_array("links", $fields))
$response->links = (object) [
"count" => sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']),
"items" => is_null(OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']) ? [] : OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']
];
return $response;
}
}

42
VKAPI/Handlers/Pay.php Normal file
View file

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Applications;
final class Pay extends VKAPIRequestHandler
{
function getIdByMarketingId(string $marketing_id): int
{
[$hexId, $signature] = explode("_", $marketing_id);
try {
$key = CHANDLER_ROOT_CONF["security"]["secret"];
if(sodium_memcmp(base64_decode($signature), hash_hmac("sha512/224", $hexId, $key, true)) == -1)
$this->fail(4, "Invalid marketing id");
} catch (\SodiumException $e) {
$this->fail(4, "Invalid marketing id");
}
return hexdec($hexId);
}
function verifyOrder(int $app_id, float $amount, string $signature): bool
{
$this->requireUser();
$app = (new Applications())->get($app_id);
if(!$app)
$this->fail(26, "No app found with this id");
else if($app->getOwner()->getId() != $this->getUser()->getId())
$this->fail(15, "Access error");
[$time, $signature] = explode(",", $signature);
try {
$key = CHANDLER_ROOT_CONF["security"]["secret"];
if(sodium_memcmp($signature, hash_hmac("whirlpool", "$app_id:$amount:$time", $key)) == -1)
$this->fail(4, "Invalid order");
} catch (\SodiumException $e) {
$this->fail(4, "Invalid order");
}
return true;
}
}

View file

@ -7,36 +7,35 @@ final class Users extends VKAPIRequestHandler
{
function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100, User $authuser = null /* костыль(( */): array
{
// $this->requireUser();
if($authuser == null) $authuser = $this->getUser();
if($authuser == NULL) $authuser = $this->getUser();
$users = new UsersRepo;
if($user_ids == "0")
$user_ids = (string) $authuser->getId();
$usrs = explode(',', $user_ids);
$response;
$response = array();
$ic = sizeof($usrs);
if(sizeof($usrs) > $count) $ic = $count;
if(sizeof($usrs) > $count)
$ic = $count;
$usrs = array_slice($usrs, $offset * $count);
for ($i=0; $i < $ic; $i++) {
for($i=0; $i < $ic; $i++) {
if($usrs[$i] != 0) {
$usr = $users->get((int) $usrs[$i]);
if(is_null($usr))
{
if(is_null($usr) || $usr->isDeleted()) {
$response[$i] = (object)[
"id" => $usrs[$i],
"id" => (int) $usrs[$i],
"first_name" => "DELETED",
"last_name" => "",
"deactivated" => "deleted"
];
}else if($usrs[$i] == null){
} else if($usrs[$i] == NULL) {
}else{
} else {
$response[$i] = (object)[
"id" => $usr->getId(),
"first_name" => $usr->getFirstName(),
@ -48,33 +47,55 @@ final class Users extends VKAPIRequestHandler
$flds = explode(',', $fields);
foreach($flds as $field) {
switch ($field) {
case 'verified':
switch($field) {
case "verified":
$response[$i]->verified = intval($usr->isVerified());
break;
case 'sex':
case "sex":
$response[$i]->sex = $usr->isFemale() ? 1 : 2;
break;
case 'has_photo':
case "has_photo":
$response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1;
break;
case 'photo_max_orig':
case "photo_max_orig":
$response[$i]->photo_max_orig = $usr->getAvatarURL();
break;
case 'photo_max':
$response[$i]->photo_max = $usr->getAvatarURL();
case "photo_max":
$response[$i]->photo_max = $usr->getAvatarURL("original");
break;
case 'status':
if($usr->getStatus() != null)
case "photo_50":
$response[$i]->photo_50 = $usr->getAvatarURL();
break;
case "photo_100":
$response[$i]->photo_100 = $usr->getAvatarURL("tiny");
break;
case "photo_200":
$response[$i]->photo_200 = $usr->getAvatarURL("normal");
break;
case "photo_200_orig": # вообще не ебу к чему эта строка ну пусть будет кек
$response[$i]->photo_200_orig = $usr->getAvatarURL("normal");
break;
case "photo_400_orig":
$response[$i]->photo_400_orig = $usr->getAvatarURL("normal");
break;
# Она хочет быть выебанной видя матан
# Покайфу когда ты Виет а вокруг лишь дискриминант
# ору а когда я это успел написать
# вова кстати не матерись в коде мамка же спалит азщазаззазщазазаззазазазх
case "status":
if($usr->getStatus() != NULL)
$response[$i]->status = $usr->getStatus();
break;
case 'screen_name':
if($usr->getShortCode() != null)
case "screen_name":
if($usr->getShortCode() != NULL)
$response[$i]->screen_name = $usr->getShortCode();
break;
case 'friend_status':
case "friend_status":
switch($usr->getSubscriptionStatus($authuser)) {
case 3:
# NOTICE falling through
case 0:
$response[$i]->friend_status = $usr->getSubscriptionStatus($authuser);
break;
@ -86,50 +107,44 @@ final class Users extends VKAPIRequestHandler
break;
}
break;
case 'last_seen':
if ($usr->onlineStatus() == 0) {
case "last_seen":
if ($usr->onlineStatus() == 0)
$response[$i]->last_seen = (object) [
"platform" => 1,
"time" => $usr->getOnline()->timestamp()
];
}
case 'music':
case "music":
$response[$i]->music = $usr->getFavoriteMusic();
break;
case 'movies':
case "movies":
$response[$i]->movies = $usr->getFavoriteFilms();
break;
case 'tv':
case "tv":
$response[$i]->tv = $usr->getFavoriteShows();
break;
case 'books':
case "books":
$response[$i]->books = $usr->getFavoriteBooks();
break;
case 'city':
case "city":
$response[$i]->city = $usr->getCity();
break;
case 'interests':
case "interests":
$response[$i]->interests = $usr->getInterests();
break;
}
}
if($usr->getOnline()->timestamp() + 300 > time()) {
if($usr->getOnline()->timestamp() + 300 > time())
$response[$i]->online = 1;
}else{
else
$response[$i]->online = 0;
}
}
}
return $response;
}
/* private function getUsersById(string $user_ids, string $fields = "", int $offset = 0, int $count = PHP_INT_MAX){
} */
function getFollowers(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object
{
$offset++;
@ -139,15 +154,13 @@ final class Users extends VKAPIRequestHandler
$this->requireUser();
foreach ($users->get($user_id)->getFollowers($offset, $count) as $follower) {
foreach($users->get($user_id)->getFollowers($offset, $count) as $follower)
$followers[] = $follower->getId();
}
$response = $followers;
if (!is_null($fields)) {
if(!is_null($fields))
$response = $this->get(implode(',', $followers), $fields, 0, $count);
}
return (object) [
"count" => $users->get($user_id)->getFollowersCount(),
@ -155,18 +168,17 @@ final class Users extends VKAPIRequestHandler
];
}
function search(string $q, string $fields = '', int $offset = 0, int $count = 100)
function search(string $q, string $fields = "", int $offset = 0, int $count = 100)
{
$users = new UsersRepo;
$array = [];
$find = $users->find($q);
foreach ($find as $user) {
foreach ($find as $user)
$array[] = $user->getId();
}
return (object)[
return (object) [
"count" => $find->size(),
"items" => $this->get(implode(',', $array), $fields, $offset, $count)
];

View file

@ -1,49 +1,66 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\Notifications\{WallPostNotification};
use openvk\Web\Models\Entities\Notifications\{WallPostNotification, RepostNotification, CommentNotification};
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\Postable;
use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Entities\Comment;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
final class Wall extends VKAPIRequestHandler
{
function get(string $owner_id, string $domain = "", int $offset = 0, int $count = 30, int $extended = 0): object
function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 30, int $extended = 0): object
{
$posts = new PostsRepo;
$items = [];
$profiles = [];
$groups = [];
$count = $posts->getPostCountOnUserWall((int) $owner_id);
$cnt = $posts->getPostCountOnUserWall($owner_id);
foreach ($posts->getPostsFromUsersWall((int)$owner_id, 1, $count, $offset) as $post) {
$wallOnwer = (new UsersRepo)->get($owner_id);
if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted())
$this->fail(18, "User was deleted or banned");
foreach($posts->getPostsFromUsersWall($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(),
$attachments = [];
$repost = [];
foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted())
continue;
$attachments[] = $this->getApiPhoto($attachment);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted())
continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment);
/* Рекурсии, сука! Заказывали? */
}
}
$repost[] = [
"id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(),
"sizes" => array([
"height" => 500, // Для временного компросима оставляю статическое число. Если каждый раз обращаться к файлу за количеством пикселов, то наступает пuпuська полная с производительностью, так что пока так
"url" => $attachment->getURL(),
"type" => "m",
"width" => 500,
]),
"text" => "",
"has_tags" => false
]
"owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
"from_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
"date" => $attachment->getPublicationTime()->timestamp(),
"post_type" => "post",
"text" => $attachment->getText(false),
"attachments" => $repostAttachments,
"post_source" => [
"type" => "vk"
],
];
}
}
@ -54,11 +71,12 @@ final class Wall extends VKAPIRequestHandler
"owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post",
"text" => $post->getText(),
"can_edit" => 0, // TODO
"text" => $post->getText(false),
"copy_history" => $repost,
"can_edit" => 0, # TODO
"can_delete" => $post->canBeDeletedBy($this->getUser()),
"can_pin" => $post->canBePinnedBy($this->getUser()),
"can_archive" => false, // TODO MAYBE
"can_archive" => false, # TODO MAYBE
"is_archived" => false,
"is_pinned" => $post->isPinned(),
"attachments" => $attachments,
@ -84,18 +102,17 @@ final class Wall extends VKAPIRequestHandler
else
$groups[] = $from_id * -1;
$attachments = null; // free attachments so it will not clone everythingg
$attachments = NULL; # free attachments so it will not clone everythingg
}
if($extended == 1)
{
if($extended == 1) {
$profiles = array_unique($profiles);
$groups = array_unique($groups);
$profilesFormatted = [];
$groupsFormatted = [];
foreach ($profiles as $prof) {
foreach($profiles as $prof) {
$user = (new UsersRepo)->get($prof);
$profilesFormatted[] = (object)[
"first_name" => $user->getFirstName(),
@ -125,58 +142,66 @@ final class Wall extends VKAPIRequestHandler
];
}
return (object)[
"count" => $count,
"items" => (array)$items,
"profiles" => (array)$profilesFormatted,
"groups" => (array)$groupsFormatted
return (object) [
"count" => $cnt,
"items" => $items,
"profiles" => $profilesFormatted,
"groups" => $groupsFormatted
];
}
else
return (object)[
"count" => $count,
"items" => (array)$items
} else
return (object) [
"count" => $cnt,
"items" => $items
];
}
function getById(string $posts, int $extended = 0, string $fields = "", User $user = null)
function getById(string $posts, int $extended = 0, string $fields = "", User $user = NULL)
{
if($user == null) $user = $this->getUser(); // костыли костыли крылышки
if($user == NULL) {
$this->requireUser();
$user = $this->getUser(); # костыли костыли крылышки
}
$items = [];
$profiles = [];
$groups = [];
# $count = $posts->getPostCountOnUserWall((int) $owner_id);
$psts = explode(",", $posts);
$psts = explode(',', $posts);
foreach($psts as $pst)
{
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(),
$attachments = [];
$repost = []; // чел высрал семь сигарет 😳 помянем 🕯
foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted())
continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment);
/* Рекурсии, сука! Заказывали? */
}
}
$repost[] = [
"id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(),
"sizes" => array([
"height" => 500, // я ещё я заебался вставлять одинаковый код в два разных места
"url" => $attachment->getURL(),
"type" => "m",
"width" => 500,
]),
"text" => "",
"has_tags" => false
]
"owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
"from_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
"date" => $attachment->getPublicationTime()->timestamp(),
"post_type" => "post",
"text" => $attachment->getText(false),
"attachments" => $repostAttachments,
"post_source" => [
"type" => "vk"
],
];
}
}
@ -187,11 +212,12 @@ final class Wall extends VKAPIRequestHandler
"owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post",
"text" => $post->getText(),
"can_edit" => 0, // TODO
"text" => $post->getText(false),
"copy_history" => $repost,
"can_edit" => 0, # TODO
"can_delete" => $post->canBeDeletedBy($user),
"can_pin" => $post->canBePinnedBy($user),
"can_archive" => false, // TODO MAYBE
"can_archive" => false, # TODO MAYBE
"is_archived" => false,
"is_pinned" => $post->isPinned(),
"post_source" => (object)["type" => "vk"],
@ -217,19 +243,19 @@ final class Wall extends VKAPIRequestHandler
else
$groups[] = $from_id * -1;
$attachments = null; // free attachments so it will not clone everythingg
$attachments = NULL; # free attachments so it will not clone everything
$repost = NULL; # same
}
}
if($extended == 1)
{
if($extended == 1) {
$profiles = array_unique($profiles);
$groups = array_unique($groups);
$profilesFormatted = [];
$groupsFormatted = [];
foreach ($profiles as $prof) {
foreach($profiles as $prof) {
$user = (new UsersRepo)->get($prof);
$profilesFormatted[] = (object)[
"first_name" => $user->getFirstName(),
@ -259,14 +285,13 @@ final class Wall extends VKAPIRequestHandler
];
}
return (object)[
return (object) [
"items" => (array)$items,
"profiles" => (array)$profilesFormatted,
"groups" => (array)$groupsFormatted
];
}
else
return (object)[
} else
return (object) [
"items" => (array)$items
];
}
@ -308,12 +333,12 @@ final class Wall extends VKAPIRequestHandler
if($signed == 1)
$flags |= 0b01000000;
// TODO: Compatible implementation of this
# TODO: Compatible implementation of this
try {
$photo = null;
$video = null;
$photo = NULL;
$video = NULL;
if($_FILES["photo"]["error"] === UPLOAD_ERR_OK) {
$album = null;
$album = NULL;
if(!$anon && $owner_id > 0 && $owner_id === $this->getUser()->getId())
$album = (new AlbumsRepo)->getUserWallAlbum($wallOwner);
@ -354,4 +379,201 @@ final class Wall extends VKAPIRequestHandler
return (object)["post_id" => $post->getVirtualId()];
}
function repost(string $object, string $message = "") {
$this->requireUser();
$postArray;
if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0)
$this->fail(100, "One of the parameters specified was missing or invalid: object is incorrect");
$post = (new PostsRepo)->getPostById((int) $postArray[1], (int) $postArray[2]);
if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid");
$nPost = new Post;
$nPost->setOwner($this->user->getId());
$nPost->setWall($this->user->getId());
$nPost->setContent($message);
$nPost->save();
$nPost->attach($post);
if($post->getOwner(false)->getId() !== $this->user->getId() && !($post->getOwner() instanceof Club))
(new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit();
return (object) [
"success" => 1, // 👍
"post_id" => $nPost->getVirtualId(),
"reposts_count" => $post->getRepostCount(),
"likes_count" => $post->getLikesCount()
];
}
function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $offset = 0, int $count = 10, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online", string $sort = "asc", bool $extended = false) {
$this->requireUser();
$post = (new PostsRepo)->getPostById($owner_id, $post_id);
if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid");
$comments = (new CommentsRepo)->getCommentsByTarget($post, $offset+1, $count, $sort == "desc" ? "DESC" : "ASC");
$items = [];
$profiles = [];
foreach($comments as $comment) {
$item = [
"id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(),
"date" => $comment->getPublicationTime()->timestamp(),
"text" => $comment->getText(false),
"post_id" => $post->getVirtualId(),
"owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(),
"parents_stack" => [],
"thread" => [
"count" => 0,
"items" => [],
"can_post" => false,
"show_reply_button" => true,
"groups_can_post" => false,
]
];
if($need_likes == true)
$item['likes'] = [
"can_like" => 1,
"count" => $comment->getLikesCount(),
"user_likes" => (int) $comment->hasLikeFrom($this->getUser()),
"can_publish" => 1
];
$items[] = $item;
if($extended == true)
$profiles[] = $comment->getOwner()->getId();
}
$response = [
"count" => (new CommentsRepo)->getCommentsCountByTarget($post),
"items" => $items,
"current_level_count" => (new CommentsRepo)->getCommentsCountByTarget($post),
"can_post" => true,
"show_reply_button" => true,
"groups_can_post" => false
];
if($extended == true) {
$profiles = array_unique($profiles);
$response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []);
}
return (object) $response;
}
function getComment(int $owner_id, int $comment_id, bool $extended = false, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online") {
$this->requireUser();
$comment = (new CommentsRepo)->get($comment_id); // один хуй айди всех комментов общий
$profiles = [];
$item = [
"id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(),
"date" => $comment->getPublicationTime()->timestamp(),
"text" => $comment->getText(false),
"post_id" => $comment->getTarget()->getVirtualId(),
"owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(),
"parents_stack" => [],
"likes" => [
"can_like" => 1,
"count" => $comment->getLikesCount(),
"user_likes" => (int) $comment->hasLikeFrom($this->getUser()),
"can_publish" => 1
],
"thread" => [
"count" => 0,
"items" => [],
"can_post" => false,
"show_reply_button" => true,
"groups_can_post" => false,
]
];
if($extended == true)
$profiles[] = $comment->getOwner()->getId();
$response = [
"items" => [$item],
"can_post" => true,
"show_reply_button" => true,
"groups_can_post" => false
];
if($extended == true) {
$profiles = array_unique($profiles);
$response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []);
}
return $response;
}
function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0) {
$post = (new PostsRepo)->getPostById($owner_id, $post_id);
if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid");
if($post->getTargetWall() < 0)
$club = (new ClubsRepo)->get(abs($post->getTargetWall()));
$flags = 0;
if($from_group != 0 && !is_null($club) && $club->canBeModifiedBy($this->user))
$flags |= 0b10000000;
try {
$comment = new Comment;
$comment->setOwner($this->user->getId());
$comment->setModel(get_class($post));
$comment->setTarget($post->getId());
$comment->setContent($message);
$comment->setCreated(time());
$comment->setFlags($flags);
$comment->save();
} catch (\LengthException $ex) {
$this->fail(1, "ошибка про то что коммент большой слишком");
}
if($post->getOwner()->getId() !== $this->user->getId())
if(($owner = $post->getOwner()) instanceof User)
(new CommentNotification($owner, $comment, $post, $this->user))->emit();
return (object) [
"comment_id" => $comment->getId(),
"parents_stack" => []
];
}
function deleteComment(int $comment_id) {
$this->requireUser();
$comment = (new CommentsRepo)->get($comment_id);
if(!$comment) $this->fail(100, "One of the parameters specified was missing or invalid");;
if(!$comment->canBeDeletedBy($this->user))
$this->fail(7, "Access denied");
$comment->delete();
return 1;
}
private function getApiPhoto($attachment) {
return [
"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
]
];
}
}

16
Vagrantfile vendored
View file

@ -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.provision "shell", inline: "/bin/tcsh /.ovk_release/install/automated/freebsd-12/install"
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-13/install"
end

View file

@ -0,0 +1,316 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use Nette\Utils\Image;
use Nette\Utils\UnknownImageFileException;
use openvk\Web\Models\Repositories\Notes;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
class Application extends RowModel
{
protected $tableName = "apps";
const PERMS = [
"notify",
"friends",
"photos",
"audio",
"video",
"stories",
"pages",
"status",
"notes",
"messages",
"wall",
"ads",
"docs",
"groups",
"notifications",
"stats",
"email",
"market",
];
private function getAvatarsDir(): string
{
$uploadSettings = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"];
if($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn")
return $uploadSettings["server"]["directory"];
else
return OPENVK_ROOT . "/storage/";
}
function getId(): int
{
return $this->getRecord()->id;
}
function getOwner(): User
{
return (new Users)->get($this->getRecord()->owner);
}
function getName(): string
{
return $this->getRecord()->name;
}
function getDescription(): string
{
return $this->getRecord()->description;
}
function getAvatarUrl(): string
{
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
if(is_null($this->getRecord()->avatar_hash))
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
$hash = $this->getRecord()->avatar_hash;
switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) {
default:
case "default":
case "basic":
return "$serverUrl/blob_" . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png";
case "accelerated":
return "$serverUrl/openvk-datastore/$hash" . "_app_avatar.png";
case "server":
$settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"];
return (
$settings->protocol ?? ovk_scheme() .
"://" . $settings->host .
$settings->path .
substr($hash, 0, 2) . "/$hash" . "_app_avatar.png"
);
}
}
function getNote(): ?Note
{
if(!$this->getRecord()->news)
return NULL;
return (new Notes)->get($this->getRecord()->news);
}
function getNoteLink(): string
{
$note = $this->getNote();
if(!$note)
return "";
return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/note" . $note->getPrettyId();
}
function getBalance(): float
{
return $this->getRecord()->coins;
}
function getURL(): string
{
return $this->getRecord()->address;
}
function getOrigin(): string
{
$parsed = parse_url($this->getURL());
return (
($parsed["scheme"] ?? "https") . "://"
. ($parsed["host"] ?? "127.0.0.1") . ":"
. ($parsed["port"] ?? "443")
);
}
function getUsersCount(): int
{
$cx = DatabaseConnection::i()->getContext();
return sizeof($cx->table("app_users")->where("app", $this->getId()));
}
function getInstallationEntry(User $user): ?array
{
$cx = DatabaseConnection::i()->getContext();
$entry = $cx->table("app_users")->where([
"app" => $this->getId(),
"user" => $user->getId(),
])->fetch();
if(!$entry)
return NULL;
return $entry->toArray();
}
function getPermissions(User $user): array
{
$permMask = 0;
$installInfo = $this->getInstallationEntry($user);
if(!$installInfo)
$this->install($user);
else
$permMask = $installInfo["access"];
$res = [];
for($i = 0; $i < sizeof(self::PERMS); $i++) {
$checkVal = 1 << $i;
if(($permMask & $checkVal) > 0)
$res[] = self::PERMS[$i];
}
return $res;
}
function isInstalledBy(User $user): bool
{
return !is_null($this->getInstallationEntry($user));
}
function setNoteLink(?string $link): bool
{
if(!$link) {
$this->stateChanges("news", NULL);
return true;
}
preg_match("%note([0-9]+)_([0-9]+)$%", $link, $matches);
if(sizeof($matches) != 3)
return false;
$owner = is_null($this->getRecord()) ? $this->changes["owner"] : $this->getRecord()->owner;
[, $ownerId, $vid] = $matches;
if($ownerId != $owner)
return false;
$note = (new Notes)->getNoteById((int) $ownerId, (int) $vid);
if(!$note)
return false;
$this->stateChanges("news", $note->getId());
return true;
}
function setAvatar(array $file): int
{
if($file["error"] !== UPLOAD_ERR_OK)
return -1;
try {
$image = Image::fromFile($file["tmp_name"]);
} catch (UnknownImageFileException $e) {
return -2;
}
$hash = hash_file("adler32", $file["tmp_name"]);
if(!is_dir($this->getAvatarsDir() . substr($hash, 0, 2)))
if(!mkdir($this->getAvatarsDir() . substr($hash, 0, 2)))
return -3;
$image->resize(140, 140, Image::STRETCH);
$image->save($this->getAvatarsDir() . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png");
$this->stateChanges("avatar_hash", $hash);
return 0;
}
function setPermission(User $user, string $perm, bool $enabled): bool
{
$permMask = 0;
$installInfo = $this->getInstallationEntry($user);
if(!$installInfo)
$this->install($user);
else
$permMask = $installInfo["access"];
$index = array_search($perm, self::PERMS);
if($index === false)
return false;
$permVal = 1 << $index;
$permMask = $enabled ? ($permMask | $permVal) : ($permMask ^ $permVal);
$cx = DatabaseConnection::i()->getContext();
$cx->table("app_users")->where([
"app" => $this->getId(),
"user" => $user->getId(),
])->update([
"access" => $permMask,
]);
return true;
}
function isEnabled(): bool
{
return (bool) $this->getRecord()->enabled;
}
function enable(): void
{
$this->stateChanges("enabled", 1);
$this->save();
}
function disable(): void
{
$this->stateChanges("enabled", 0);
$this->save();
}
function install(User $user): void
{
if(!$this->getInstallationEntry($user)) {
$cx = DatabaseConnection::i()->getContext();
$cx->table("app_users")->insert([
"app" => $this->getId(),
"user" => $user->getId(),
]);
}
}
function uninstall(User $user): void
{
$cx = DatabaseConnection::i()->getContext();
$cx->table("app_users")->where([
"app" => $this->getId(),
"user" => $user->getId(),
])->delete();
}
function addCoins(float $coins): float
{
$res = $this->getBalance() + $coins;
$this->stateChanges("coins", $res);
$this->save();
return $res;
}
function withdrawCoins(): void
{
$balance = $this->getBalance();
$tax = ($balance / 100) * OPENVK_ROOT_CONF["openvk"]["preferences"]["apps"]["withdrawTax"];
$owner = $this->getOwner();
$owner->setCoins($owner->getCoins() + ($balance - $tax));
$this->setCoins(0.0);
$this->save();
$owner->save();
}
function delete(bool $softly = true): void
{
if($softly)
throw new \UnexpectedValueException("Can't delete apps softly.");
$cx = DatabaseConnection::i()->getContext();
$cx->table("app_users")->where("app", $this->getId())->delete();
parent::delete(false);
}
}

View file

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User};
use openvk\Web\Models\Repositories\{Users};
use Nette\Database\Table\ActiveRow;
class BannedLink extends RowModel
{
protected $tableName = "links_banned";
private $overrideContentColumn = "reason";
function getId(): int
{
return $this->getRecord()->id;
}
function getDomain(): string
{
return $this->getRecord()->domain;
}
function getReason(): string
{
return $this->getRecord()->reason ?? tr("url_is_banned_default_reason");
}
function getInitiator(): ?User
{
return (new Users)->get($this->getRecord()->initiator);
}
function getComment(): string
{
return OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["showReason"]
? tr("url_is_banned_comment_r", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"], $this->getReason())
: tr("url_is_banned_comment", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"]);
}
function getRegexpRule(): string
{
return addslashes("/" . $this->getDomain() . $this->getRawRegexp() . "/");
}
function getRawRegexp(): string
{
return $this->getRecord()->regexp_rule;
}
}

View file

@ -67,7 +67,7 @@ class Club extends RowModel
function getName(): string
{
return ovk_proc_strtr($this->getRecord()->name, 32);
return $this->getRecord()->name;
}
function getCanonicalName(): string
@ -100,6 +100,14 @@ class Club extends RowModel
return $this->getRecord()->about;
}
function getDescriptionHtml(): ?string
{
if(!is_null($this->getDescription()))
return nl2br(htmlspecialchars($this->getDescription(), ENT_DISALLOWED | ENT_XHTML));
else
return NULL;
}
function getShortCode(): ?string
{
return $this->getRecord()->shortcode;
@ -302,8 +310,8 @@ class Club extends RowModel
{
$manager = (new Managers)->getByUserAndClub($user->getId(), $this->getId());
if ($ignoreHidden && $manager !== null && $manager->isHidden())
return null;
if ($ignoreHidden && $manager !== NULL && $manager->isHidden())
return NULL;
return $manager;
}
@ -347,5 +355,10 @@ class Club extends RowModel
return $this->getRecord()->website;
}
function getAlert(): ?string
{
return $this->getRecord()->alert;
}
use Traits\TSubscribable;
}

View file

@ -131,7 +131,7 @@ class Correspondence
*/
function getPreviewMessage(): ?Message
{
$messages = $this->getMessages(1, null, 1);
$messages = $this->getMessages(1, NULL, 1);
return $messages[0] ?? NULL;
}

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
use openvk\Web\Util\DateTime;
class EmailChangeVerification extends PasswordReset
{
protected $tableName = "email_change_verifications";
function getNewEmail(): string
{
return $this->getRecord()->new_email;
}
}

View file

@ -6,6 +6,8 @@ abstract class Media extends Postable
{
protected $fileExtension = "oct"; #octet stream xddd
protected $upperNodeReferenceColumnName = "owner";
protected $processingPlaceholder = NULL;
protected $processingTime = 30;
function __destruct()
{
@ -23,6 +25,11 @@ abstract class Media extends Postable
return OPENVK_ROOT . "/storage/";
}
protected function checkIfFileIsProcessed(): bool
{
throw new \LogicException("checkIfFileIsProcessed is not implemented");
}
abstract protected function saveFile(string $filename, string $hash): bool;
protected function pathFromHash(string $hash): string
@ -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"
@ -69,6 +80,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
{
return (bool) $this->getRecord()->deleted;
@ -90,6 +121,16 @@ 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");

View file

@ -61,6 +61,17 @@ class Message extends RowModel
return new DateTime($this->getRecord()->created);
}
function getSendTimeHumanized(): string
{
$dateTime = new DateTime($this->getRecord()->created);
if($dateTime->format("%d.%m.%y") == ovk_strftime_safe("%d.%m.%y", time())) {
return $dateTime->format("%T %p");
} else {
return $dateTime->format("%d.%m.%y");
}
}
/**
* Get date of last edit, if any edits were made, otherwise null.
*
@ -125,8 +136,8 @@ class Message extends RowModel
"name" => $author->getFirstName().$unreadmsg,
],
"timing" => [
"sent" => (string) $this->getSendTime()->format("%e %B %G" . tr("time_at_sp") . "%X"),
"edited" => is_null($this->getEditTime()) ? null : (string) $this->getEditTime(),
"sent" => (string) $this->getSendTimeHumanized(),
"edited" => is_null($this->getEditTime()) ? NULL : (string) $this->getEditTime(),
],
"text" => $this->getText(),
"read" => !$this->isUnread(),

View file

@ -75,8 +75,18 @@ class Note extends Postable
"underline",
]);
$source = NULL;
if(is_null($this->getRecord())) {
if(isset($this->changes["source"]))
$source = $this->changes["source"];
else
throw new \LogicException("Can't render note without content set.");
} else {
$source = $this->getRecord()->source;
}
$purifier = new HTMLPurifier($config);
return $purifier->purify($this->getRecord()->source);
return $purifier->purify($source);
}
function getName(): string
@ -91,6 +101,9 @@ class Note extends Postable
function getText(): string
{
if(is_null($this->getRecord()))
return $this->renderHTML();
$cached = $this->getRecord()->cached_content;
if(!$cached) {
$cached = $this->renderHTML();

View file

@ -165,8 +165,11 @@ class Photo extends Media
foreach($manifest->Size as $size)
$mappings[(string) $size["id"]] = (string) $size["vkId"];
foreach($sizes as $id => $meta)
$res[$mappings[$id] ?? $id] = $meta;
foreach($sizes as $id => $meta) {
$type = $mappings[$id] ?? $id;
$meta->type = $type;
$res[$type] = $meta;
}
return $res;
}
@ -240,8 +243,11 @@ class Photo extends Media
$photo->setFile($file);
$photo->save();
if(!is_null($album))
if(!is_null($album)) {
$album->addPhoto($photo);
$album->setEdited(time());
$album->save();
}
return $photo;
}

View file

@ -85,6 +85,11 @@ class Post extends Postable
return ($this->getRecord()->flags & 0b01000000) > 0;
}
function isDeactivationMessage(): bool
{
return ($this->getRecord()->flags & 0b00100000) > 0;
}
function isExplicit(): bool
{
return (bool) $this->getRecord()->nsfw;

View file

@ -87,7 +87,7 @@ abstract class Postable extends Attachable
]));
}
// TODO add pagination
# TODO add pagination
function getLikers(): \Traversable
{
$sel = DB::i()->getContext()->table("likes")->where([

View file

@ -35,12 +35,12 @@ trait TRichText
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]);
$href = str_replace(";", "&#59;", $matches[1]);
$href = rawurlencode(str_replace(";", "&#59;", $matches[1]));
$link = str_replace("#", "&num;", $matches[3]);
$link = str_replace(";", "&#59;", $matches[3]);
$rel = $this->isAd() ? "sponsored" : "ugc";
return "<a href='$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
}),
$text
);
@ -55,21 +55,28 @@ 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", "<a href='/$1'>$2</a>", $text);
$text = preg_replace("%([\n\r\s]|^)(#([\p{L}_-]++[0-9]*[\p{L}_-]*))%Xu", "$1<a href='/feed/hashtag/$3'>$2</a>", $text);
$text = preg_replace("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", "<a href='/$1'>$2</a>", $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]<a href='/feed/hashtag/$slug'>$m[2]</a>";
}, $text);
$text = $this->formatEmojis($text);
}
$text = $this->removeZalgo($text);
$text = nl2br($text);
} else {
$text = str_replace("\r\n","\n", $text);
}
if(OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["christian"])

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use morphos\Gender;
use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
@ -9,6 +10,7 @@ use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser;
use function morphos\Russian\inflectName;
class User extends RowModel
{
@ -84,6 +86,11 @@ class User extends RowModel
return (bool) $this->getRecord()->microblog;
}
function getMainPage(): int
{
return $this->getRecord()->main_page;
}
function getChandlerGUID(): string
{
return $this->getRecord()->user;
@ -138,24 +145,32 @@ class User extends RowModel
return iterator_to_array($avPhotos)[0] ?? NULL;
}
function getFirstName(): string
function getFirstName(bool $pristine = false): string
{
return $this->getRecord()->deleted ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE);
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine)
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE);
else
return $name;
}
function getLastName(): string
function getLastName(bool $pristine = false): string
{
return $this->getRecord()->deleted ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE);
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine)
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE);
else
return $name;
}
function getPseudo(): ?string
{
return $this->getRecord()->deleted ? "DELETED" : $this->getRecord()->pseudo;
return ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : $this->getRecord()->pseudo);
}
function getFullName(): string
{
if($this->getRecord()->deleted)
if($this->isDeleted() && !$this->isDeactivated())
return "DELETED";
$pseudo = $this->getPseudo();
@ -167,12 +182,23 @@ 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->isDeleted() && !$this->isDeactivated())
return "DELETED";
else
return $this->getFirstName() . ' ' . $this->getLastName();
return $this->getFirstName() . " " . $this->getLastName();
}
function getPhone(): ?string
@ -268,6 +294,19 @@ class User extends RowModel
return $this->getRecord()->marital_status;
}
function getLocalizedMaritalStatus(): string
{
$status = $this->getMaritalStatus();
$string = "relationship_$status";
if($this->isFemale()) {
$res = tr($string . "_fem");
if($res != ("@" . $string . "_fem"))
return $res; # If fem version exists, return
}
return tr($string);
}
function getContactEmail(): ?string
{
return $this->getRecord()->email_contact;
@ -325,9 +364,17 @@ class User extends RowModel
function getBirthday(): ?DateTime
{
if(is_null($this->getRecord()->birthday))
return NULL;
else
return new DateTime($this->getRecord()->birthday);
}
function getBirthdayPrivacy(): int
{
return $this->getRecord()->birthday_privacy;
}
function getAge(): ?int
{
return (int)floor((time() - $this->getBirthday()->timestamp()) / YEAR);
@ -453,6 +500,16 @@ class User extends RowModel
return $this->_abstractRelationCount("get-friends");
}
function getFriendsOnline(int $page = 1, int $limit = 6): \Traversable
{
return $this->_abstractRelationGenerator("get-online-friends", $page, $limit);
}
function getFriendsOnlineCount(): int
{
return $this->_abstractRelationCount("get-online-friends");
}
function getFollowers(int $page = 1, int $limit = 6): \Traversable
{
return $this->_abstractRelationGenerator("get-followers", $page, $limit);
@ -711,7 +768,7 @@ class User extends RowModel
]);
}
function ban(string $reason, bool $deleteSubscriptions = true): void
function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void
{
if($deleteSubscriptions) {
$subs = DatabaseConnection::i()->getContext()->table("subscriptions");
@ -725,9 +782,31 @@ class User extends RowModel
}
$this->setBlock_Reason($reason);
$this->setUnblock_time($unban_time);
$this->save();
}
function deactivate(?string $reason): void
{
$this->setDeleted(1);
$this->setDeact_Date(time() + (MONTH * 7));
$this->setDeact_Reason($reason);
$this->save();
}
function reactivate(): void
{
$this->setDeleted(0);
$this->setDeact_Date(0);
$this->setDeact_Reason("");
$this->save();
}
function getDeactivationDate(): DateTime
{
return new DateTime($this->getRecord()->deact_date);
}
function verifyNumber(string $code): bool
{
$ver = $this->getPendingPhoneVerification();
@ -864,6 +943,17 @@ class User extends RowModel
return true;
}
function changeEmail(string $email): void
{
DatabaseConnection::i()->getContext()->table("ChandlerUsers")
->where("id", $this->getChandlerUser()->getId())->update([
"login" => $email
]);
$this->stateChanges("email", $email);
$this->save();
}
function adminNotify(string $message): bool
{
$admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"];
@ -882,7 +972,15 @@ class User extends RowModel
function isDeleted(): bool
{
if ($this->getRecord()->deleted == 1)
if($this->getRecord()->deleted == 1)
return TRUE;
else
return FALSE;
}
function isDeactivated(): bool
{
if($this->getDeactivationDate()->timestamp() > time())
return TRUE;
else
return FALSE;
@ -915,11 +1013,27 @@ class User extends RowModel
return $this->getRecord()->website;
}
// ты устрица
# ты устрица
function isActivated(): bool
{
return (bool) $this->getRecord()->activated;
}
function getUnbanTime(): ?string
{
return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL;
}
function canUnbanThemself(): bool
{
if (!$this->isBanned())
return false;
if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0)
return false;
return true;
}
use Traits\TSubscribable;
}

View file

@ -15,6 +15,8 @@ class Video extends Media
protected $tableName = "videos";
protected $fileExtension = "ogv";
protected $processingPlaceholder = "video/rendering";
protected function saveFile(string $filename, string $hash): bool
{
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
@ -37,7 +39,7 @@ 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();
@ -54,6 +56,22 @@ class Video extends Media
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;
@ -83,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();

View file

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Entities\Application;
use openvk\Web\Models\Entities\User;
class Applications
{
private $context;
private $apps;
private $appRels;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->apps = $this->context->table("apps");
$this->appRels = $this->context->table("app_users");
}
private function toApp(?ActiveRow $ar): ?Application
{
return is_null($ar) ? NULL : new Application($ar);
}
function get(int $id): ?Application
{
return $this->toApp($this->apps->get($id));
}
function getList(int $page = 1, ?int $perPage = NULL): \Traversable
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$apps = $this->apps->where("enabled", 1)->page($page, $perPage);
foreach($apps as $app)
yield new Application($app);
}
function getListCount(): int
{
return sizeof($this->apps->where("enabled", 1));
}
function getByOwner(User $owner, int $page = 1, ?int $perPage = NULL): \Traversable
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$apps = $this->apps->where("owner", $owner->getId())->page($page, $perPage);
foreach($apps as $app)
yield new Application($app);
}
function getOwnCount(User $owner): int
{
return sizeof($this->apps->where("owner", $owner->getId()));
}
function getInstalled(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$apps = $this->appRels->where("user", $user->getId())->page($page, $perPage);
foreach($apps as $appRel)
yield $this->get($appRel->app);
}
function getInstalledCount(User $user): int
{
return sizeof($this->appRels->where("user", $user->getId()));
}
}

View file

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection as DB;
use Nette\Database\Table\{ActiveRow, Selection};
use openvk\Web\Models\Entities\BannedLink;
class BannedLinks
{
private $context;
private $bannedLinks;
function __construct()
{
$this->context = DB::i()->getContext();
$this->bannedLinks = $this->context->table("links_banned");
}
function toBannedLink(?ActiveRow $ar): ?BannedLink
{
return is_null($ar) ? NULL : new BannedLink($ar);
}
function get(int $id): ?BannedLink
{
return $this->toBannedLink($this->bannedLinks->get($id));
}
function getList(?int $page = 1): \Traversable
{
foreach($this->bannedLinks->order("id DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $link)
yield new BannedLink($link);
}
function getCount(int $page = 1): int
{
return sizeof($this->bannedLinks->fetch());
}
function getByDomain(string $domain): ?Selection
{
return $this->bannedLinks->where("domain", $domain);
}
function isDomainBanned(string $domain): bool
{
return sizeof($this->bannedLinks->where(["link" => $domain, "regexp_rule" => ""])) > 0;
}
function genLinks($rules): \Traversable
{
foreach ($rules as $rule)
yield $this->get($rule->id);
}
function genEntries($links, $uri): \Traversable
{
foreach($links as $link)
if (preg_match($link->getRegexpRule(), $uri))
yield $link->getId();
}
function check(string $url): ?array
{
$uri = strstr(str_replace(["https://", "http://"], "", $url), "/", true);
$domain = str_replace("www.", "", $uri);
$rules = $this->getByDomain($domain);
if (is_null($rules))
return NULL;
return iterator_to_array($this->genEntries($this->genLinks($rules), $uri));
}
}

View file

@ -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)

View file

@ -26,13 +26,13 @@ class Comments
return $this->toComment($this->comments->get($id));
}
function getCommentsByTarget(Postable $target, int $page, ?int $perPage = NULL): \Traversable
function getCommentsByTarget(Postable $target, int $page, ?int $perPage = NULL, ?string $sort = "ASC"): \Traversable
{
$comments = $this->comments->where([
"model" => get_class($target),
"target" => $target->getId(),
"deleted" => false,
])->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
])->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("created ".$sort);;
foreach($comments as $comment)
yield $this->toComment($comment);

View file

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\EmailChangeVerification;
use openvk\Web\Models\Entities\User;
use Nette\Database\Table\ActiveRow;
class EmailChangeVerifications
{
private $context;
private $verifications;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->verifications = $this->context->table("email_change_verifications");
}
function toEmailChangeVerification(?ActiveRow $ar): ?EmailChangeVerification
{
return is_null($ar) ? NULL : new EmailChangeVerification($ar);
}
function getByToken(string $token): ?EmailChangeVerification
{
return $this->toEmailChangeVerification($this->verifications->where("key", $token)->fetch());
}
function getLatestByUser(User $user): ?EmailChangeVerification
{
return $this->toEmailChangeVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch());
}
}

View file

@ -44,4 +44,15 @@ class Messages
yield new Correspondence($correspondent, $anotherCorrespondent);
}
}
function getCorrespondenciesCount(RowModel $correspondent): ?int
{
$id = $correspondent->getId();
$class = get_class($correspondent);
$query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql");
DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql"));
$count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt;
bdump($count);
return $count;
}
}

View file

@ -39,7 +39,7 @@ class Notes
if(!is_null($note))
return new Note($note);
else
return null;
return NULL;
}
function getUserNotesCount(User $user): int

View file

@ -71,7 +71,7 @@ class Posts
{
$hashtag = "#$hashtag";
$sel = $this->posts
->where("content LIKE ?", "%$hashtag%")
->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag")
->where("deleted", 0)
->order("created DESC")
->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
@ -96,7 +96,7 @@ class Posts
if(!is_null($post))
return new Post($post);
else
return null;
return NULL;
}

View file

@ -1,8 +1,5 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
// use openvk\Web\Models\Entities\Ticket;
// use openvk\Web\Models\Entities\User;
// use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Entities\TicketComment;
use Chandler\Database\DatabaseConnection;
@ -22,32 +19,6 @@ class TicketComments
foreach($this->comments->where(['ticket_id' => $ticket_id, 'deleted' => 0]) as $comment) yield new TicketComment($comment);
}
// private function toTicket(?ActiveRow $ar): ?Ticket
// {
// return is_null($ar) ? NULL : new Ticket($ar);
// }
// function getTicketsByuId(int $user_id): \Traversable
// {
// foreach($this->tickets->where(['user_id' => $user_id, 'deleted' => 0]) as $ticket) yield new Ticket($ticket);
// }
// function getRequestById(int $req_id): ?Ticket
// {
// $requests = $this->tickets->where(['id' => $req_id])->fetch();
// if(!is_null($requests))
// return new Req($requests);
// else
// return null;
// }
// function get(int $id): ?Ticket
// {
// return $this->toTicket($this->tickets->get($id));
// }
function get(int $id): ?TicketComment
{
$comment = $this->comments->get($id);;

View file

@ -50,7 +50,7 @@ class Tickets
if(!is_null($requests))
return new Req($requests);
else
return null;
return NULL;
}

View file

@ -35,7 +35,7 @@ class Topics
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
// Get pinned topics first
# Get pinned topics first
$query = "SELECT `id` FROM `topics` WHERE `pinned` = 1 AND `group` = ? AND `deleted` = 0 UNION SELECT `id` FROM `topics` WHERE `pinned` = 0 AND `group` = ? AND `deleted` = 0";
$query .= " LIMIT " . $perPage . " OFFSET " . ($page - 1) * $perPage;

View file

@ -13,7 +13,7 @@ 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=640x360,setsar=1:1 -y $temp2
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

View file

@ -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

View file

@ -0,0 +1,20 @@
SELECT COUNT(id) AS cnt FROM
(
(
SELECT
recipient_id AS id
FROM messages
WHERE
sender_id = ?
AND
sender_type = ?
) UNION (
SELECT
sender_id AS id
FROM messages
WHERE
recipient_id = ?
AND
recipient_type = ?
)
) dt

View file

@ -0,0 +1,6 @@
(SELECT follower AS __id FROM
(SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0
INNER JOIN
(SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1
ON u0.follower = u1.target) u2
INNER JOIN profiles ON profiles.id = u2.__id WHERE online > (UNIX_TIMESTAMP() - 300)

View file

@ -9,19 +9,19 @@ final class AboutPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
protected $activationTolerant = true;
protected $deactivationTolerant = true;
function renderIndex(): void
{
if(!is_null($this->user)) {
header("HTTP/1.1 302 Found");
header("Location: /id" . $this->user->id);
exit;
if($this->user->identity->getMainPage())
$this->redirect("/feed");
else
$this->redirect($this->user->identity->getURL());
}
if($_SERVER['REQUEST_URI'] == "/id0") {
header("HTTP/1.1 302 Found");
header("Location: /");
exit;
$this->redirect("/");
}
$this->template->stats = (new Users)->getStatistics();
@ -85,7 +85,7 @@ final class AboutPresenter extends OpenVKPresenter
if(is_null($lg))
$this->throwError(404, "Not found", "Language is not found");
header("Content-Type: application/javascript");
echo "window.lang = " . json_encode($localizer->export($lang)) . ";"; // привет хардкод :DDD
echo "window.lang = " . json_encode($localizer->export($lang)) . ";"; # привет хардкод :DDD
exit;
}
@ -102,6 +102,15 @@ final class AboutPresenter extends OpenVKPresenter
. "# covered from unauthorized persons (for example, due to\n"
. "# lack of rights to access the admin panel)\n\n"
. "User-Agent: *\n"
. "Disallow: /albums/create\n"
. "Disallow: /videos/upload\n"
. "Disallow: /invite\n"
. "Disallow: /groups_create\n"
. "Disallow: /notifications\n"
. "Disallow: /settings\n"
. "Disallow: /edit\n"
. "Disallow: /gifts\n"
. "Disallow: /support\n"
. "Disallow: /rpc\n"
. "Disallow: /language\n"
. "Disallow: /badbrowser.php\n"
@ -120,10 +129,12 @@ final class AboutPresenter extends OpenVKPresenter
function renderHumansTxt(): void
{
// :D
# :D
$this->redirect("https://github.com/openvk/openvk#readme");
}
header("HTTP/1.1 302 Found");
header("Location: https://github.com/openvk/openvk#readme");
exit;
function renderDev(): void
{
$this->redirect("https://docs.openvk.su/");
}
}

View file

@ -1,7 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts};
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter
{
@ -9,13 +10,15 @@ final class AdminPresenter extends OpenVKPresenter
private $clubs;
private $vouchers;
private $gifts;
private $bannedLinks;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts)
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks)
{
$this->users = $users;
$this->clubs = $clubs;
$this->vouchers = $vouchers;
$this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
parent::__construct();
}
@ -23,7 +26,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)
@ -70,14 +73,14 @@ final class AdminPresenter extends OpenVKPresenter
$user->setLast_Name($this->postParam("last_name"));
$user->setPseudo($this->postParam("nickname"));
$user->setStatus($this->postParam("status"));
$user->setVerified(empty($this->postParam("verify") ? 0 : 1));
if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online")));
if(!$user->setShortCode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode")))
$this->flash("err", tr("error"), tr("error_shorturl_incorrect"));
$user->changeEmail($this->postParam("email"));
if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online")));
$user->setVerified(empty($this->postParam("verify") ? 0 : 1));
$user->save();
break;
}
}
@ -170,8 +173,7 @@ final class AdminPresenter extends OpenVKPresenter
$voucher->save();
$this->redirect("/admin/vouchers/id" . $voucher->getId(), static::REDIRECT_TEMPORARY);
exit;
$this->redirect("/admin/vouchers/id" . $voucher->getId());
}
function renderGiftCategories(): void
@ -193,7 +195,7 @@ final class AdminPresenter extends OpenVKPresenter
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $slug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta", static::REDIRECT_TEMPORARY);
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta");
} else {
$gen = true;
$cat = new GiftCategory;
@ -234,7 +236,7 @@ final class AdminPresenter extends OpenVKPresenter
$cat->setDescription($code, $this->postParam("description_$code"));
}
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta", static::REDIRECT_TEMPORARY);
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta");
}
function renderGifts(string $catSlug, int $catId): void
@ -245,7 +247,7 @@ final class AdminPresenter extends OpenVKPresenter
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $catSlug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/", static::REDIRECT_TEMPORARY);
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/");
$this->template->cat = $cat;
$this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
@ -284,7 +286,7 @@ final class AdminPresenter extends OpenVKPresenter
$name = $catTo->getName();
$this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>.");
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/", static::REDIRECT_TEMPORARY);
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/");
break;
default:
case "edit":
@ -328,7 +330,7 @@ final class AdminPresenter extends OpenVKPresenter
$cat->addGift($gift);
}
$this->redirect("/admin/gifts/id" . $gift->getId(), static::REDIRECT_TEMPORARY);
$this->redirect("/admin/gifts/id" . $gift->getId());
}
}
@ -341,11 +343,13 @@ final class AdminPresenter extends OpenVKPresenter
{
$this->assertNoCSRF();
$unban_time = strtotime($this->queryParam("date")) ?: NULL;
$user = $this->users->get($id);
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->ban($this->queryParam("reason"));
$user->ban($this->queryParam("reason"), true, $unban_time);
exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ]));
}
@ -357,7 +361,8 @@ final class AdminPresenter extends OpenVKPresenter
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->setBlock_Reason(null);
$user->setBlock_Reason(NULL);
$user->setUnblock_time(NULL);
$user->save();
exit(json_encode([ "success" => true ]));
}
@ -373,4 +378,73 @@ final class AdminPresenter extends OpenVKPresenter
$user->adminNotify("⚠️ " . $this->queryParam("message"));
exit(json_encode([ "message" => $this->queryParam("message") ]));
}
function renderBannedLinks(): void
{
$this->template->links = $this->bannedLinks->getList((int) $this->queryParam("p") ?: 1);
$this->template->users = new Users;
}
function renderBannedLink(int $id): void
{
$this->template->form = (object) [];
if($id === 0) {
$this->template->form->id = 0;
$this->template->form->link = NULL;
$this->template->form->reason = NULL;
} else {
$link = (new BannedLinks)->get($id);
if(!$link)
$this->notFound();
$this->template->form->id = $link->getId();
$this->template->form->link = $link->getDomain();
$this->template->form->reason = $link->getReason();
$this->template->form->regexp = $link->getRawRegexp();
}
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$link = (new BannedLinks)->get($id);
$new_domain = parse_url($this->postParam("link"))["host"];
$new_reason = $this->postParam("reason") ?: NULL;
$lid = $id;
if ($link) {
$link->setDomain($new_domain ?? $this->postParam("link"));
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->save();
} else {
if (!$new_domain)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_specified"));
$link = new BannedLink;
$link->setDomain($new_domain);
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->setInitiator($this->user->identity->getId());
$link->save();
$lid = $link->getId();
}
$this->redirect("/admin/bannedLink/id" . $lid);
}
function renderUnbanLink(int $id): void
{
$link = (new BannedLinks)->get($id);
if (!$link)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_found"));
$link->delete(false);
$this->redirect("/admin/bannedLinks");
}
}

View file

@ -0,0 +1,138 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Application;
use openvk\Web\Models\Repositories\Applications;
final class AppsPresenter extends OpenVKPresenter
{
private $apps;
function __construct(Applications $apps)
{
$this->apps = $apps;
parent::__construct();
}
function renderPlay(int $app): void
{
$this->assertUserLoggedIn();
$app = $this->apps->get($app);
if(!$app || !$app->isEnabled())
$this->notFound();
$this->template->id = $app->getId();
$this->template->name = $app->getName();
$this->template->desc = $app->getDescription();
$this->template->origin = $app->getOrigin();
$this->template->url = $app->getURL();
$this->template->owner = $app->getOwner();
$this->template->news = $app->getNote();
$this->template->perms = $app->getPermissions($this->user->identity);
}
function renderUnInstall(): void
{
$this->assertUserLoggedIn();
$this->assertNoCSRF();
$app = $this->apps->get((int) $this->queryParam("app"));
if(!$app)
$this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc"));
$app->uninstall($this->user->identity);
$this->flashFail("succ", tr("app_uninstalled"), tr("app_uninstalled_desc"));
}
function renderEdit(): void
{
$this->assertUserLoggedIn();
$app = NULL;
if($this->queryParam("act") !== "create") {
if(empty($this->queryParam("app")))
$this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc"));
$app = $this->apps->get((int) $this->queryParam("app"));
if(!$app)
$this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc"));
if($app->getOwner()->getId() != $this->user->identity->getId())
$this->flashFail("err", tr("forbidden"), tr("app_err_forbidden_desc"));
}
if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!$app) {
$app = new Application;
$app->setOwner($this->user->id);
}
if(!filter_var($this->postParam("url"), FILTER_VALIDATE_URL))
$this->flashFail("err", tr("app_err_url"), tr("app_err_url_desc"));
if(isset($_FILES["ava"]) && $_FILES["ava"]["size"] > 0) {
if(($res = $app->setAvatar($_FILES["ava"])) !== 0)
$this->flashFail("err", tr("app_err_ava"), tr("app_err_ava_desc", $res));
}
if(empty($this->postParam("note"))) {
$app->setNoteLink(NULL);
} else {
if(!$app->setNoteLink($this->postParam("note")))
$this->flashFail("err", tr("app_err_note"), tr("app_err_note_desc"));
}
$app->setName($this->postParam("name"));
$app->setDescription($this->postParam("desc"));
$app->setAddress($this->postParam("url"));
if($this->postParam("enable") === "on")
$app->enable();
else
$app->disable(); # no need to save since enable/disable will call save() internally
$this->redirect("/editapp?act=edit&app=" . $app->getId()); # will exit here
}
if(!is_null($app)) {
$this->template->create = false;
$this->template->id = $app->getId();
$this->template->name = $app->getName();
$this->template->desc = $app->getDescription();
$this->template->coins = $app->getBalance();
$this->template->origin = $app->getOrigin();
$this->template->url = $app->getURL();
$this->template->note = $app->getNoteLink();
$this->template->users = $app->getUsersCount();
$this->template->on = $app->isEnabled();
} else {
$this->template->create = true;
}
}
function renderList(): void
{
$this->assertUserLoggedIn();
$act = $this->queryParam("act");
if(!in_array($act, ["list", "installed", "dev"]))
$act = "installed";
$page = (int) ($this->queryParam("p") ?? 1);
if($act == "list") {
$apps = $this->apps->getList($page);
$count = $this->apps->getListCount();
} else if($act == "installed") {
$apps = $this->apps->getInstalled($this->user->identity, $page);
$count = $this->apps->getInstalledCount($this->user->identity);
} else if($act == "dev") {
$apps = $this->apps->getByOwner($this->user->identity, $page);
$count = $this->apps->getOwnCount($this->user->identity);
}
$this->template->act = $act;
$this->template->iterator = $apps;
$this->template->count = $count;
$this->template->page = $page;
}
}

View file

@ -1,14 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\PasswordReset;
use openvk\Web\Models\Entities\EmailVerification;
use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification};
use openvk\Web\Models\Repositories\{IPs, Users, Restores, Verifications};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Models\Repositories\IPs;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Restores;
use openvk\Web\Models\Repositories\Verifications;
use openvk\Web\Util\Validator;
use Chandler\Session\Session;
use Chandler\Security\User as ChandlerUser;
@ -20,6 +14,7 @@ final class AuthPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
protected $activationTolerant = true;
protected $deactivationTolerant = true;
private $authenticator;
private $db;
@ -50,7 +45,7 @@ final class AuthPresenter extends OpenVKPresenter
function renderRegister(): void
{
if(!is_null($this->user))
$this->redirect("/id" . $this->user->id, static::REDIRECT_TEMPORARY);
$this->redirect($this->user->identity->getURL());
if(!$this->hasPermission("user", "register", -1)) exit("Вас забанили");
@ -89,6 +84,9 @@ final class AuthPresenter extends OpenVKPresenter
if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment"));
if (!$this->postParam("confirmation"))
$this->flashFail("err", tr("error"), tr("checkbox_in_registration_unchecked"));
try {
$user = new User;
$user->setFirst_Name($this->postParam("first_name"));
@ -97,7 +95,7 @@ final class AuthPresenter extends OpenVKPresenter
$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->setBirthday(empty($this->postParam("birthday")) ? NULL : 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"));
@ -128,7 +126,7 @@ final class AuthPresenter extends OpenVKPresenter
}
$this->authenticator->authenticate($chUser->getId());
$this->redirect("/id" . $user->getId(), static::REDIRECT_TEMPORARY);
$this->redirect("/id" . $user->getId());
}
}
@ -137,12 +135,11 @@ final class AuthPresenter extends OpenVKPresenter
$redirUrl = $this->requestParam("jReturnTo");
if(!is_null($this->user))
$this->redirect($redirUrl ?? "/id" . $this->user->id, static::REDIRECT_TEMPORARY);
$this->redirect($redirUrl ?? $this->user->identity->getURL());
if(!$this->hasPermission("user", "login", -1)) exit("Вас забанили");
if($_SERVER["REQUEST_METHOD"] === "POST") {
$user = $this->db->table("ChandlerUsers")->where("login", $this->postParam("login"))->fetch();
if(!$user)
$this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password"));
@ -151,7 +148,7 @@ final class AuthPresenter extends OpenVKPresenter
$this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password"));
$ovkUser = new User($user->related("profiles.user")->fetch());
if($ovkUser->isDeleted())
if($ovkUser->isDeleted() && !$ovkUser->isDeactivated())
$this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password"));
$secret = $user->related("profiles.user")->fetch()["2fa_secret"];
@ -171,8 +168,7 @@ final class AuthPresenter extends OpenVKPresenter
}
$this->authenticator->authenticate($user->id);
$this->redirect($redirUrl ?? "/id" . $user->related("profiles.user")->fetch()->id, static::REDIRECT_TEMPORARY);
exit;
$this->redirect($redirUrl ?? $ovkUser->getURL());
}
}
@ -183,7 +179,7 @@ final class AuthPresenter extends OpenVKPresenter
if($uuid === "unset") {
Session::i()->set("_su", NULL);
$this->redirect("/", static::REDIRECT_TEMPORARY);
$this->redirect("/");
}
if(!$this->db->table("ChandlerUsers")->where("id", $uuid))
@ -192,8 +188,7 @@ final class AuthPresenter extends OpenVKPresenter
$this->assertPermission('openvk\Web\Models\Entities\User', 'substitute', 0);
Session::i()->set("_su", $uuid);
$this->flash("succ", tr("profile_changed"), tr("profile_changed_comment"));
$this->redirect("/", static::REDIRECT_TEMPORARY);
exit;
$this->redirect("/");
}
function renderLogout(): void
@ -203,7 +198,7 @@ final class AuthPresenter extends OpenVKPresenter
$this->authenticator->logout();
Session::i()->set("_su", NULL);
$this->redirect("/", static::REDIRECT_TEMPORARY_PRESISTENT);
$this->redirect("/");
}
function renderFinishRestoringPassword(): void
@ -243,7 +238,7 @@ final class AuthPresenter extends OpenVKPresenter
function renderRestore(): void
{
if(!is_null($this->user))
$this->redirect("/id" . $this->user->id, static::REDIRECT_TEMPORARY);
$this->redirect($this->user->identity->getURL());
if(($this->queryParam("act") ?? "default") === "finish")
$this->pass("openvk!Auth->finishRestoringPassword");
@ -273,7 +268,6 @@ final class AuthPresenter extends OpenVKPresenter
];
$this->sendmail($uRow->login, "password-reset", $params); #Vulnerability possible
$this->flashFail("succ", tr("information_-1"), tr("password_reset_email_sent"));
}
}
@ -281,7 +275,7 @@ final class AuthPresenter extends OpenVKPresenter
function renderResendEmail(): void
{
if(!is_null($this->user) && $this->user->identity->isActivated())
$this->redirect("/id" . $this->user->id, static::REDIRECT_TEMPORARY);
$this->redirect($this->user->identity->getURL());
if($_SERVER["REQUEST_METHOD"] === "POST") {
$user = $this->user->identity;
@ -321,4 +315,49 @@ final class AuthPresenter extends OpenVKPresenter
$this->redirect("/");
}
}
function renderReactivatePage(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->user->identity->reactivate();
$this->redirect("/");
}
function renderUnbanThemself(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if(!$this->user->identity->canUnbanThemself())
$this->flashFail("err", tr("error"), tr("forbidden"));
$user = $this->users->get($this->user->id);
$user->setBlock_Reason(NULL);
$user->setUnblock_Time(NULL);
$user->save();
$this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description"));
}
/*
* This function will revoke all tokens, including API and Web tokens and except active one
*
* OF COURSE it requires CSRF
*/
function renderRevokeAllTokens(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
// API tokens
$this->db->table("api_tokens")->where("user", $this->user->identity->getId())->delete();
// Web tokens
$this->db->table("ChandlerTokens")->where("user", $this->user->identity->getChandlerGUID())->where("token != ?", Session::i()->get("tok"))->delete();
$this->flashFail("succ", tr("information_-1"), tr("end_all_sessions_done"));
}
}

View file

@ -1,13 +1,29 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\BannedLinks;
use openvk\Web\Models\Entities\BannedLink;
final class AwayPresenter extends OpenVKPresenter
{
function renderAway(): void
{
$checkBanEntries = (new BannedLinks)->check($this->queryParam("to") . "/");
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["warnings"])
if (sizeof($checkBanEntries) > 0)
$this->pass("openvk!Away->view", $checkBanEntries[0]);
header("HTTP/1.0 302 Found");
header("X-Robots-Tag: noindex, nofollow, noarchive");
header("Location: " . $this->queryParam("to"));
exit;
}
function renderView(int $lid) {
$this->template->link = (new BannedLinks)->get($lid);
if (!$this->template->link)
$this->notFound();
$this->template->to = $this->queryParam("to");
}
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\BannedLink;
use openvk\Web\Models\Repositories\BannedLinks;
final class BannedLinkPresenter extends OpenVKPresenter
{
function renderView(int $lid) {
$this->template->link = (new BannedLinks)->get($lid);
$this->template->to = $this->queryParam("to");
}
}

View file

@ -19,20 +19,23 @@ final class BlobPresenter extends OpenVKPresenter
header("Access-Control-Allow-Origin: *");
$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 {
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;
}
}
}

View file

@ -24,7 +24,7 @@ final class CommentPresenter extends OpenVKPresenter
if(!is_null($this->user)) $comment->toggleLike($this->user->identity);
$this->redirect($_SERVER["HTTP_REFERER"], static::REDIRECT_TEMPORARY);
$this->redirect($_SERVER["HTTP_REFERER"]);
}
function renderMakeComment(string $repo, int $eId): void
@ -60,7 +60,7 @@ final class CommentPresenter extends OpenVKPresenter
}
}
// TODO move to trait
# TODO move to trait
try {
$photo = NULL;
$video = NULL;

View file

@ -91,7 +91,7 @@ final class GiftsPresenter extends OpenVKPresenter
$gift->used();
$this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $gift->getPrice() . " голосов.");
$this->redirect($user->getURL(), static::REDIRECT_TEMPORARY);
$this->redirect($user->getURL());
}
function renderStub(): void

View file

@ -22,15 +22,12 @@ final class GroupPresenter extends OpenVKPresenter
if(!$club) {
$this->notFound();
} else {
if($club->getShortCode())
if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $club->getShortCode())
$this->redirect("/" . $club->getShortCode(), static::REDIRECT_TEMPORARY_PRESISTENT);
$this->template->club = $club;
$this->template->albums = (new Albums)->getClubAlbums($club, 1, 3);
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
$this->template->topics = (new Topics)->getLastTopics($club, 3);
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
$this->template->club = $club;
}
}
@ -57,8 +54,7 @@ final class GroupPresenter extends OpenVKPresenter
}
$club->toggleSubscription($this->user->identity);
header("HTTP/1.1 302 Found");
header("Location: /club" . $club->getId());
$this->redirect("/club" . $club->getId());
}else{
$this->flashFail("err", "Ошибка", "Вы не ввели название группы.");
}
@ -77,9 +73,7 @@ final class GroupPresenter extends OpenVKPresenter
$club->toggleSubscription($this->user->identity);
header("HTTP/1.1 302 Found");
header("Location: /club" . $club->getId());
exit;
$this->redirect($club->getURL());
}
function renderFollowers(int $id): void
@ -89,7 +83,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->club = $this->clubs->get($id);
$this->template->onlyShowManagers = $this->queryParam("onlyAdmins") == "1";
if($this->template->onlyShowManagers) {
$this->template->followers = null;
$this->template->followers = NULL;
$this->template->managers = $this->template->club->getManagers((int) ($this->queryParam("p") ?? 1), !$this->template->club->canBeModifiedBy($this->user->identity));
if($this->template->club->canBeModifiedBy($this->user->identity) || !$this->template->club->isOwnerHidden()) {
@ -99,7 +93,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->count = $this->template->club->getManagersCount();
} else {
$this->template->followers = $this->template->club->getFollowers((int) ($this->queryParam("p") ?? 1));
$this->template->managers = null;
$this->template->managers = NULL;
$this->template->count = $this->template->club->getFollowersCount();
}
@ -116,7 +110,7 @@ final class GroupPresenter extends OpenVKPresenter
$user = is_null($this->queryParam("user")) ? $this->postParam("user") : $this->queryParam("user");
$comment = $this->postParam("comment");
$removeComment = $this->postParam("removeComment") === "1";
$hidden = ["0" => false, "1" => true][$this->queryParam("hidden")] ?? null;
$hidden = ["0" => false, "1" => true][$this->queryParam("hidden")] ?? NULL;
//$index = $this->queryParam("index");
if(!$user)
$this->badRequest();
@ -202,9 +196,11 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->club = $club;
if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!$club->setShortcode( empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode") ))
$this->flashFail("err", tr("error"), tr("error_shorturl_incorrect"));
$club->setName(empty($this->postParam("name")) ? $club->getName() : $this->postParam("name"));
$club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about"));
$club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"));
$club->setWall(empty($this->postParam("wall")) ? 0 : 1);
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
$club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1);

View file

@ -29,9 +29,10 @@ final class InternalAPIPresenter extends OpenVKPresenter
function renderRoute(): void
{
if($_SERVER["REQUEST_METHOD"] !== "POST")
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("ты дебил это точка апи");
}
try {
$input = (object) MessagePack::unpack(file_get_contents("php://input"));
} catch (\Exception $ex) {
@ -71,20 +72,21 @@ final class InternalAPIPresenter extends OpenVKPresenter
}
function renderTimezone() {
if($_SERVER["REQUEST_METHOD"] !== "POST")
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("ты дебил это метод апи");
}
$sessionOffset = Session::i()->get("_timezoneOffset");
if(is_numeric($this->postParam("timezone", false))) {
$postTZ = intval($this->postParam("timezone", false));
if ($postTZ != $sessionOffset || $sessionOffset == null) {
Session::i()->set("_timezoneOffset", $postTZ ? $postTZ : 3 * MINUTE );
$this->returnJson([
"success" => 1 // If it's new value
"success" => 1 # If it's new value
]);
} else {
$this->returnJson([
"success" => 2 // If it's the same value (if for some reason server will call this func)
"success" => 2 # If it's the same value (if for some reason server will call this func)
]);
}
} else {

View file

@ -34,10 +34,18 @@ final class MessengerPresenter extends OpenVKPresenter
if(isset($_GET["sel"]))
$this->pass("openvk!Messenger->app", $_GET["sel"]);
$page = $_GET["p"] ?? 1;
$page = (int) ($_GET["p"] ?? 1);
$correspondences = iterator_to_array($this->messages->getCorrespondencies($this->user->identity, $page));
// #КакаоПрокакалось
$this->template->corresps = $correspondences;
$this->template->paginatorConf = (object) [
"count" => $this->messages->getCorrespondenciesCount($this->user->identity),
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($this->template->corresps),
"perPage" => OPENVK_DEFAULT_PER_PAGE,
];
}
function renderApp(int $sel): void
@ -106,7 +114,7 @@ final class MessengerPresenter extends OpenVKPresenter
$messages = [];
$correspondence = new Correspondence($this->user->identity, $correspondent);
foreach($correspondence->getMessages(1, $lastMsg === 0 ? null : $lastMsg) as $message)
foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message)
$messages[] = $message->simplify();
header("Content-Type: application/json");

View file

@ -1,7 +1,6 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Notes;
use openvk\Web\Models\Repositories\{Users, Notes};
use openvk\Web\Models\Entities\Note;
final class NotesPresenter extends OpenVKPresenter
@ -47,6 +46,29 @@ final class NotesPresenter extends OpenVKPresenter
$this->template->note = $note;
}
function renderPreView(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 400 Bad Request");
exit;
}
if(empty($this->postParam("html")) || empty($this->postParam("title"))) {
header("HTTP/1.1 400 Bad Request");
exit(tr("note_preview_empty_err"));
}
$note = new Note;
$note->setSource($this->postParam("html"));
$this->flash("info", tr("note_preview_warn"), tr("note_preview_warn_details"));
$this->template->title = $this->postParam("title");
$this->template->html = $note->getText();
}
function renderCreate(): void
{
$this->assertUserLoggedIn();

View file

@ -8,10 +8,19 @@ final class NotificationPresenter extends OpenVKPresenter
$this->assertUserLoggedIn();
$archive = $this->queryParam("act") === "archived";
$this->template->mode = $archive ? "archived" : "new";
$count = $this->user->identity->getNotificationsCount($archive);
if($count == 0 && $this->queryParam("act") == NULL) {
$mode = "archived";
$archive = true;
} else {
$mode = $archive ? "archived" : "new";
}
$this->template->mode = $mode;
$this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->iterator = iterator_to_array($this->user->identity->getNotifications($this->template->page, $archive));
$this->template->count = $this->user->identity->getNotificationsCount($archive);
$this->template->count = $count;
$this->user->identity->updateNotificationOffset();
$this->user->identity->save();

View file

@ -14,6 +14,7 @@ abstract class OpenVKPresenter extends SimplePresenter
{
protected $banTolerant = false;
protected $activationTolerant = false;
protected $deactivationTolerant = false;
protected $errorTemplate = "@error";
protected $user = NULL;
@ -60,9 +61,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->flash($type, $title, $message, $code);
$referer = $_SERVER["HTTP_REFERER"] ?? "/";
header("HTTP/1.1 302 Found");
header("Location: $referer");
exit;
$this->redirect($referer);
}
}
@ -98,9 +97,8 @@ abstract class OpenVKPresenter extends SimplePresenter
}
$this->flash("err", tr("login_required_error"), tr("login_required_error_comment"));
header("HTTP/1.1 302 Found");
header("Location: $loginUrl");
exit;
$this->redirect($loginUrl);
}
}
@ -110,15 +108,13 @@ abstract class OpenVKPresenter extends SimplePresenter
if($model !== "user") {
$this->flash("info", tr("login_required_error"), tr("login_required_error_comment"));
header("HTTP/1.1 302 Found");
header("Location: /login");
exit;
$this->redirect("/login");
}
return ($action === "register" || $action === "login");
}
return (bool) $this->user->raw->can($action)->model($model)->whichBelongsTo($context === -1 ? null : $context);
return (bool) $this->user->raw->can($action)->model($model)->whichBelongsTo($context === -1 ? NULL : $context);
}
protected function assertPermission(string $model, string $action, int $context, bool $throw = false): void
@ -214,32 +210,21 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->thisUser = $this->user->identity;
$this->template->userTainted = $user->isTainted();
if($this->user->identity->isDeleted()) {
/*
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⠶⠶⣶⠶⠶⠶⠶⠶⠶⠶⠶⠶⢶⠶⠶⠶⠤⠤⠤⠤⣄⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠒⠒⠒⠀⠀⠀⠀⠤⢤⣤⣄⠉⠉⠛⠛⠷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣰⠟⠀⠀⠀⠀⠀⠐⠋⢑⣤⣶⣶⣤⡢⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣄⡂⠀⠀⠶⢄⠙⢷⣤⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⣸⡿⠚⠉⡀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⢢⠀⠀⡀⣰⣿⣿⣿⣿⣦⡀⠀⠀⠡⡀⢹⡆⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣴⠏⠀⣀⣀⣀⡤⢤⣄⣠⣿⣿⣿⣿⣻⣿⣿⣷⠀⢋⣾⠈⠙⣶⠒⢿⣿⣿⣿⣿⡿⠟⠃⠀⡀⠡⠼⣧⡀⠀⠀⠀⠀⠀⠀
⠀⠀⢀⣴⣿⢃⡴⢊⢽⣶⣤⣀⠀⠊⠉⠉⡛⢿⣿⣿⣿⠿⠋⢀⡀⠁⠀⠀⢸⣁⣀⣉⣉⣉⡉⠀⠩⡡⠀⣩⣦⠀⠈⠻⣦⡀⠀⠀⠀⠀
⠀⢠⡟⢡⠇⡞⢀⠆⠀⢻⣿⣿⣷⣄⠀⢀⠈⠂⠈⢁⡤⠚⡟⠉⠀⣀⣀⠀⠈⠳⣍⠓⢆⢀⡠⢀⣨⣴⣿⣿⡏⢀⡆⠀⢸⡇⠀⠀⠀⠀
⠀⣾⠁⢸⠀⠀⢸⠀⠀⠀⠹⣿⣿⣿⣿⣶⣬⣦⣤⡈⠀⠀⠇⠀⠛⠉⣩⣤⣤⣤⣿⣤⣤⣴⣾⣿⣿⣿⣿⣿⣧⠞⠀⠀⢸⡇⠀⠀⠀⠀
⠀⢹⣆⠸⠀⠀⢸⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣟⣛⠛⠛⣛⡛⠛⠛⣛⣋⡉⠉⣡⠶⢾⣿⣿⣿⣿⣿⣿⡇⠀⠀⢀⣾⠃⠀⠀⠀⠀
⠀⠀⠻⣆⡀⠀⠈⢂⠀⠀⠀⠠⡈⢻⣿⣿⣿⣿⡟⠁⠈⢧⡼⠉⠙⣆⡞⠁⠈⢹⣴⠃⠀⢸⣿⣿⣿⣿⣿⣿⠃⠀⡆⣾⠃⠀⠀⠀⠀⠀
⠀⠀⠀⠈⢻⣇⠀⠀⠀⠀⠀⠀⢡⠀⠹⣿⣿⣿⣷⡀⠀⣸⡇⠀⠀⣿⠁⠀⠀⠘⣿⠀⠀⠘⣿⣿⣿⣿⣿⣿⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠹⣇⠀⠠⠀⠀⠀⠀⠡⠐⢬⡻⣿⣿⣿⣿⣿⣷⣶⣶⣿⣦⣤⣤⣤⣿⣦⣶⣿⣿⣿⣿⣿⣿⣿⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠹⣧⡀⠡⡀⠀⠀⠀⠑⠄⠙⢎⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⢿⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠳⣤⡐⡄⠀⠀⠀⠈⠂⠀⠱⣌⠻⣿⣿⣿⣿⣿⣿⣿⠿⣿⠟⢻⡏⢻⣿⣿⣿⣿⣿⣿⣿⠀⢸⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢮⣦⡀⠂⠀⢀⠀⠀⠈⠳⣈⠻⣿⣿⣿⡇⠘⡄⢸⠀⠀⣇⠀⣻⣿⣿⣿⣿⣿⡏⠀⠸⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢶⣤⣄⡑⠄⠀⠀⠈⠑⠢⠙⠻⢷⣶⣵⣞⣑⣒⣋⣉⣁⣻⣿⠿⠟⠱⠃⡸⠀⣧⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⣷⣄⡀⠐⠢⣄⣀⡀⠀⠉⠉⠉⠉⠛⠙⠭⠭⠄⠒⠈⠀⠐⠁⢀⣿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠷⢦⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣒⡠⠄⣠⡾⠃⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠷⠶⣦⣤⣭⣤⣬⣭⣭⣴⠶⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀
*/
if($this->user->identity->isDeleted() && !$this->deactivationTolerant) {
if($this->user->identity->isDeactivated()) {
header("HTTP/1.1 403 Forbidden");
$this->getTemplatingEngine()->render(__DIR__ . "/templates/@deactivated.xml", [
"thisUser" => $this->user->identity,
"csrfToken" => $GLOBALS["csrfToken"],
"isTimezoned" => Session::i()->get("_timezoneOffset"),
]);
} else {
Authenticator::i()->logout();
Session::i()->set("_su", NULL);
$this->flashFail("err", tr("error"), tr("profile_not_found"));
$this->redirect("/", static::REDIRECT_TEMPORARY);
$this->redirect("/");
}
exit;
}
if($this->user->identity->isBanned() && !$this->banTolerant) {
@ -252,7 +237,7 @@ abstract class OpenVKPresenter extends SimplePresenter
exit;
}
// ето для емейл уже надо (и по хорошему надо бы избавится от повторяющегося кода мда)
# ето для емейл уже надо (и по хорошему надо бы избавится от повторяющегося кода мда)
if(!$this->user->identity->isActivated() && !$this->activationTolerant) {
header("HTTP/1.1 403 Forbidden");
$this->getTemplatingEngine()->render(__DIR__ . "/templates/@email.xml", [
@ -265,7 +250,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$userValidated = 1;
$cacheTime = 0; # Force no cache
if ($this->user->identity->onlineStatus() == 0) {
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time());
$this->user->identity->save();
}
@ -288,7 +273,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$whichbrowser = new WhichBrowser\Parser(getallheaders());
$mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"];
if($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == null)
if($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
$this->setSessionTheme($mobiletheme);
$theme = NULL;
@ -299,7 +284,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$theme = Themepacks::i()[Session::i()->get("_sessionTheme", "ovk")];
} else if($this->requestParam("themePreview")) {
$theme = Themepacks::i()[$this->requestParam("themePreview")];
} else if($this->user->identity !== null && $this->user->identity->getTheme()) {
} else if($this->user->identity !== NULL && $this->user->identity->getTheme()) {
$theme = $this->user->identity->getTheme();
}

View file

@ -1,12 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Entities\Album;
use openvk\Web\Models\Repositories\Photos;
use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Entities\{Club, Photo, Album};
use openvk\Web\Models\Repositories\{Photos, Albums, Users, Clubs};
use Nette\InvalidStateException as ISE;
final class PhotosPresenter extends OpenVKPresenter
@ -72,6 +67,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);
@ -81,9 +78,9 @@ final class PhotosPresenter extends OpenVKPresenter
$album->save();
if(isset($club))
$this->redirect("/album-" . $album->getOwner()->getId() . "_" . $album->getId(), static::REDIRECT_TEMPORARY);
$this->redirect("/album-" . $album->getOwner()->getId() . "_" . $album->getId());
else
$this->redirect("/album" . $album->getOwner()->getId() . "_" . $album->getId(), static::REDIRECT_TEMPORARY);
$this->redirect("/album" . $album->getOwner()->getId() . "_" . $album->getId());
}
}
@ -100,6 +97,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());
@ -124,6 +124,7 @@ final class PhotosPresenter extends OpenVKPresenter
$name = $album->getName();
$owner = $album->getOwner();
$album->delete();
$this->flash("succ", "Альбом удалён", "Альбом $name был успешно удалён.");
$this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId());
}
@ -198,7 +199,7 @@ final class PhotosPresenter extends OpenVKPresenter
$photo->save();
$this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с фоткой.");
$this->redirect("/photo" . $photo->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/photo" . $photo->getPrettyId());
}
$this->template->photo = $photo;
@ -236,7 +237,10 @@ final class PhotosPresenter extends OpenVKPresenter
}
$album->addPhoto($photo);
$this->redirect("/photo" . $photo->getPrettyId(), static::REDIRECT_TEMPORARY);
$album->setEdited(time());
$album->save();
$this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId());
} else {
$this->template->album = $album;
}
@ -257,9 +261,11 @@ final class PhotosPresenter extends OpenVKPresenter
if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
$album->removePhoto($photo);
$album->setEdited(time());
$album->save();
$this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
$this->redirect("/album" . $album->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/album" . $album->getPrettyId());
}
}
@ -278,6 +284,6 @@ final class PhotosPresenter extends OpenVKPresenter
$photo->delete();
$this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
$this->redirect("/id0", static::REDIRECT_TEMPORARY);
$this->redirect("/id0");
}
}

View file

@ -1,9 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs};
use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter
@ -29,7 +27,7 @@ final class SearchPresenter extends OpenVKPresenter
if($query != "")
$this->assertUserLoggedIn();
// https://youtu.be/pSAWM5YuXx8
# https://youtu.be/pSAWM5YuXx8
$repos = [ "groups" => "clubs", "users" => "users" ];
$repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type.");

View file

@ -1,16 +1,16 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Ticket;
use openvk\Web\Models\Repositories\{Tickets, Users};
use openvk\Web\Models\Entities\TicketComment;
use openvk\Web\Models\Repositories\TicketComments;
use openvk\Web\Models\Entities\{Ticket, TicketComment};
use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments};
use openvk\Web\Util\Telegram;
use Chandler\Session\Session;
use Chandler\Database\DatabaseConnection;
use Parsedown;
final class SupportPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
protected $deactivationTolerant = true;
private $tickets;
private $comments;
@ -28,6 +28,41 @@ final class SupportPresenter extends OpenVKPresenter
$this->assertUserLoggedIn();
$this->template->mode = in_array($this->queryParam("act"), ["faq", "new", "list"]) ? $this->queryParam("act") : "faq";
if($this->template->mode === "faq") {
$lang = Session::i()->get("lang", "ru");
$base = OPENVK_ROOT . "/data/knowledgebase/faq";
if(file_exists("$base.$lang.md"))
$file = "$base.$lang.md";
else if(file_exists("$base.md"))
$file = "$base.md";
else
$file = NULL;
if(is_null($file)) {
$this->template->faq = [];
} else {
$lines = file($file);
$faq = [];
$index = 0;
foreach($lines as $line) {
if(strpos($line, "# ") === 0)
++$index;
$faq[$index][] = $line;
}
$this->template->faq = array_map(function($section) {
$title = substr($section[0], 2);
array_shift($section);
return [
$title,
(new Parsedown())->text(implode("\n", $section))
];
}, $faq);
}
}
$this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id);
if($this->template->mode === "list") {
$this->template->page = (int) ($this->queryParam("p") ?? 1);
@ -63,8 +98,7 @@ final class SupportPresenter extends OpenVKPresenter
Telegram::send($helpdeskChat, $telegramText);
}
header("HTTP/1.1 302 Found");
header("Location: /support/view/" . $ticket->getId());
$this->redirect("/support/view/" . $ticket->getId());
} else {
$this->flashFail("err", tr("error"), tr("you_have_not_entered_name_or_text"));
}
@ -79,6 +113,7 @@ final class SupportPresenter extends OpenVKPresenter
$act = $this->queryParam("act") ?? "open";
switch($act) {
default:
# NOTICE falling through
case "open":
$state = 0;
break;
@ -154,8 +189,7 @@ final class SupportPresenter extends OpenVKPresenter
$comment->setCreated(time());
$comment->save();
header("HTTP/1.1 302 Found");
header("Location: /support/view/" . $id);
$this->redirect("/support/view/" . $id);
} else {
$this->flashFail("err", tr("error"), tr("you_have_not_entered_text"));
}
@ -286,6 +320,10 @@ final class SupportPresenter extends OpenVKPresenter
$user->setBlock_In_Support_Reason($this->queryParam("reason"));
$user->save();
if($this->queryParam("close_tickets"))
DatabaseConnection::i()->getConnection()->query("UPDATE tickets SET type = 2 WHERE user_id = ".$id);
$this->returnJson([ "success" => true, "reason" => $this->queryParam("reason") ]);
}

View file

@ -91,7 +91,7 @@ final class TopicsPresenter extends OpenVKPresenter
$topic->setFlags($flags);
$topic->save();
// TODO move to trait
# TODO move to trait
try {
$photo = NULL;
$video = NULL;
@ -108,7 +108,7 @@ final class TopicsPresenter extends OpenVKPresenter
}
} catch(ISE $ex) {
$this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/topic" . $topic->getPrettyId());
}
if(!empty($this->postParam("text")) || $photo || $video) {
@ -123,7 +123,7 @@ final class TopicsPresenter extends OpenVKPresenter
$comment->save();
} catch (\LengthException $ex) {
$this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой.");
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/topic" . $topic->getPrettyId());
}
if(!is_null($photo))
@ -133,7 +133,7 @@ final class TopicsPresenter extends OpenVKPresenter
$comment->attach($video);
}
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/topic" . $topic->getPrettyId());
}
$this->template->club = $club;
@ -167,7 +167,7 @@ final class TopicsPresenter extends OpenVKPresenter
$topic->save();
$this->flash("succ", tr("changes_saved"), tr("topic_changes_saved_comment"));
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/topic" . $topic->getPrettyId());
}
$this->template->topic = $topic;
@ -189,6 +189,6 @@ final class TopicsPresenter extends OpenVKPresenter
$this->willExecuteWriteAction();
$topic->deleteTopic();
$this->redirect("/board" . $topic->getClub()->getId(), static::REDIRECT_TEMPORARY);
$this->redirect("/board" . $topic->getClub()->getId());
}
}

View file

@ -1,7 +1,6 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\{Users, Clubs};
final class UnknownTextRouteStrategyPresenter extends OpenVKPresenter
{

View file

@ -2,24 +2,21 @@
namespace openvk\Web\Presenters;
use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Videos;
use openvk\Web\Models\Repositories\Notes;
use openvk\Web\Models\Repositories\Vouchers;
use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification};
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator;
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use Chandler\Security\Authenticator;
use lfkeitel\phptotp\{Base32, Totp};
use chillerlan\QRCode\{QRCode, QROptions};
use Nette\Database\UniqueConstraintViolationException;
final class UserPresenter extends OpenVKPresenter
{
private $users;
public $deactivationTolerant = false;
function __construct(Users $users)
{
$this->users = $users;
@ -30,13 +27,15 @@ final class UserPresenter extends OpenVKPresenter
function renderView(int $id): void
{
$user = $this->users->get($id);
if(!$user || $user->isDeleted())
$this->template->_template = "User/deleted.xml";
else {
if($user->getShortCode())
if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode())
$this->redirect("/" . $user->getShortCode(), static::REDIRECT_TEMPORARY_PRESISTENT);
if(!$user || $user->isDeleted()) {
if($user->isDeactivated()) {
$this->template->_template = "User/deactivated.xml";
$this->template->user = $user;
} else {
$this->template->_template = "User/deleted.xml";
}
} else {
$this->template->albums = (new Albums)->getUserAlbums($user);
$this->template->albumsCount = (new Albums)->getUserAlbumsCount($user);
$this->template->videos = (new Videos)->getByUser($user, 1, 2);
@ -72,7 +71,7 @@ final class UserPresenter extends OpenVKPresenter
$name = $user->getFullName();
$this->flash("err", "Ошибка доступа", "Вы не можете просматривать полный список подписок $name.");
$this->redirect("/id$id", static::REDIRECT_TEMPORARY_PRESISTENT);
$this->redirect($user->getURL());
}
}
}
@ -87,6 +86,9 @@ final class UserPresenter extends OpenVKPresenter
elseif (!$user->getPrivacyPermission('groups.read', $this->user->identity ?? NULL))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
else {
if($this->queryParam("act") === "managed" && $this->user->id !== $user->getId())
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$this->template->user = $user;
$this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->admin = $this->queryParam("act") == "managed";
@ -151,7 +153,10 @@ final class UserPresenter extends OpenVKPresenter
if (strtotime($this->postParam("birthday")) < time())
$user->setBirthday(strtotime($this->postParam("birthday")));
$user->setBirthday(empty($this->postParam("birthday")) ? NULL : strtotime($this->postParam("birthday")));
if ($this->postParam("birthday_privacy") <= 1 && $this->postParam("birthday_privacy") >= 0)
$user->setBirthday_Privacy($this->postParam("birthday_privacy"));
if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0)
$user->setMarital_Status($this->postParam("marialstatus"));
@ -267,9 +272,7 @@ final class UserPresenter extends OpenVKPresenter
$user->toggleSubscription($this->user->identity);
header("HTTP/1.1 302 Found");
header("Location: /id" . $user->getId());
exit;
$this->redirect($user->getURL());
}
function renderSetAvatar(): void
@ -288,7 +291,11 @@ final class UserPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("error_upload_failed"));
}
(new Albums)->getUserAvatarAlbum($this->user->identity)->addPhoto($photo);
$album = (new Albums)->getUserAvatarAlbum($this->user->identity);
$album->addPhoto($photo);
$album->setEdited(time());
$album->save();
$this->flashFail("succ", tr("photo_saved"), tr("photo_saved_comment"));
}
@ -312,7 +319,7 @@ final class UserPresenter extends OpenVKPresenter
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
if($this->user->identity->is2faEnabled()) {
$code = $this->postParam("code");
$code = $this->postParam("password_change_code");
if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $code)))
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
}
@ -324,6 +331,46 @@ final class UserPresenter extends OpenVKPresenter
}
}
if($this->postParam("new_email")) {
if(!Validator::i()->emailValid($this->postParam("new_email")))
$this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment"));
if(!Authenticator::verifyHash($this->postParam("email_change_pass"), $user->getChandlerUser()->getRaw()->passwordHash))
$this->flashFail("err", tr("error"), tr("incorrect_password"));
if($user->is2faEnabled()) {
$code = $this->postParam("email_change_code");
if(!($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code)))
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
}
if($this->postParam("new_email") !== $user->getEmail()) {
if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']) {
$request = (new EmailChangeVerifications)->getLatestByUser($user);
if(!is_null($request) && $request->isNew())
$this->flashFail("err", tr("forbidden"), tr("email_rate_limit_error"));
$verification = new EmailChangeVerification;
$verification->setProfile($user->getId());
$verification->setNew_Email($this->postParam("new_email"));
$verification->save();
$params = [
"key" => $verification->getKey(),
"name" => $user->getCanonicalName(),
];
$this->sendmail($this->postParam("new_email"), "change-email", $params); #Vulnerability possible
$this->flashFail("succ", tr("information_-1"), tr("email_change_confirm_message"));
}
try {
$user->changeEmail($this->postParam("new_email"));
} catch(UniqueConstraintViolationException $ex) {
$this->flashFail("err", tr("error"), tr("user_already_exists"));
}
}
}
if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc")))
$this->flashFail("err", tr("error"), tr("error_shorturl_incorrect"));
} else if($_GET['act'] === "privacy") {
@ -376,6 +423,9 @@ final class UserPresenter extends OpenVKPresenter
if(in_array($this->postParam("nsfw"), [0, 1, 2]))
$user->setNsfwTolerance((int) $this->postParam("nsfw"));
if(in_array($this->postParam("main_page"), [0, 1]))
$user->setMain_Page((int) $this->postParam("main_page"));
} else if($_GET['act'] === "lMenu") {
$settings = [
"menu_bildoj" => "photos",
@ -400,20 +450,54 @@ final class UserPresenter extends OpenVKPresenter
throw $ex;
}
$this->flash(
"succ",
"Изменения сохранены",
"Новые данные появятся на вашей странице."
);
$this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
}
$this->template->mode = in_array($this->queryParam("act"), [
"main", "privacy", "finance", "finance.top-up", "interface"
"main", "security", "privacy", "finance", "finance.top-up", "interface"
]) ? $this->queryParam("act")
: "main";
if($this->template->mode == "finance") {
$address = OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["address"];
$text = str_replace("$1", (string) $this->user->identity->getId(), OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["hint"]);
$qrCode = explode("base64,", (new QRCode(new QROptions([
"imageTransparent" => false
])))->render("ton://transfer/$address?text=$text"));
$this->template->qrCodeType = substr($qrCode[0], 5);
$this->template->qrCodeData = $qrCode[1];
}
$this->template->user = $user;
$this->template->themes = Themepacks::i()->getThemeList();
}
function renderDeactivate(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$flags = 0;
$reason = $this->postParam("deactivate_reason");
$share = $this->postParam("deactivate_share");
if($share) {
$flags |= 0b00100000;
$post = new Post;
$post->setOwner($this->user->id);
$post->setWall($this->user->id);
$post->setCreated(time());
$post->setContent($reason);
$post->setFlags($flags);
$post->save();
}
$this->user->identity->deactivate($reason);
$this->redirect("/");
}
function renderTwoFactorAuthSettings(): void
{
$this->assertUserLoggedIn();
@ -456,7 +540,7 @@ final class UserPresenter extends OpenVKPresenter
$this->template->secret = $secret;
}
// Why are these crutch? For some reason, the QR code is not displayed if you just pass the render output to the view
# Why are these crutch? For some reason, the QR code is not displayed if you just pass the render output to the view
$issuer = OPENVK_ROOT_CONF["openvk"]["appearance"]["name"];
$email = $this->user->identity->getEmail();
@ -481,11 +565,30 @@ 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("/");
}
function renderCoinsTransfer(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flashFail("err", tr("error"), tr("feature_disabled"));
$receiverAddress = $this->postParam("receiver");
$value = (int) $this->postParam("value");
$message = $this->postParam("message");
@ -558,4 +661,24 @@ final class UserPresenter extends OpenVKPresenter
$this->flashFail("succ", tr("information_-1"), tr("rating_increase_successful", $receiver->getURL(), htmlentities($receiver->getCanonicalName()), $value));
}
function renderEmailChangeFinish(): void
{
$request = (new EmailChangeVerifications)->getByToken(str_replace(" ", "+", $this->queryParam("key")));
if(!$request || !$request->isStillValid()) {
$this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment"));
$this->redirect("/settings");
} else {
$request->delete(false);
try {
$request->getUser()->changeEmail($request->getNewEmail());
} catch(UniqueConstraintViolationException $ex) {
$this->flashFail("err", tr("error"), tr("user_already_exists"));
}
$this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
$this->redirect("/settings");
}
}
}

View file

@ -43,6 +43,24 @@ final class VKAPIPresenter extends OpenVKPresenter
exit(json_encode($payload));
}
private function twofaFail(int $userId): void
{
header("HTTP/1.1 401 Unauthorized");
header("Content-Type: application/json");
$payload = [
"error" => "need_validation",
"error_description" => "use app code",
"validation_type" => "2fa_app",
"validation_sid" => "2fa_".$userId."_2839041_randommessdontread",
"phone_mask" => "+374 ** *** 420",
"redirect_url" => "https://http.cat/418", // Not implemented yet :( So there is a photo of cat :3
"validation_resend" => "nowhere"
];
exit(json_encode($payload));
}
private function badMethod(string $object, string $method): void
{
$this->fail(3, "Unknown method passed.", $object, $method);
@ -249,8 +267,12 @@ final class VKAPIPresenter extends OpenVKPresenter
$user = (new Users)->get($uId);
$code = $this->requestParam("code");
if($user->is2faEnabled() && !($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code)))
if($user->is2faEnabled() && !($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code))) {
if($this->requestParam("2fa_supported") == "1")
$this->twofaFail($user->getId());
else
$this->fail(28, "Invalid 2FA code", "internal", "acquireToken");
}
$token = new APIToken;
$token->setUser($user);

View file

@ -1,8 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Video;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Videos;
use openvk\Web\Models\Repositories\{Users, Videos};
use Nette\InvalidStateException as ISE;
final class VideosPresenter extends OpenVKPresenter
@ -80,7 +79,7 @@ final class VideosPresenter extends OpenVKPresenter
$video->save();
$this->redirect("/video" . $video->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/video" . $video->getPrettyId());
} else {
$this->flashFail("err", "Произошла ошибка", "Видео не может быть опубликовано без названия.");
}
@ -104,7 +103,7 @@ final class VideosPresenter extends OpenVKPresenter
$video->save();
$this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с видосиком.");
$this->redirect("/video" . $video->getPrettyId(), static::REDIRECT_TEMPORARY);
$this->redirect("/video" . $video->getPrettyId());
}
$this->template->video = $video;
@ -128,7 +127,6 @@ final class VideosPresenter extends OpenVKPresenter
$this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт.");
}
$this->redirect("/videos".$owner, static::REDIRECT_TEMPORARY);
exit;
$this->redirect("/videos" . $owner);
}
}

View file

@ -113,14 +113,14 @@ final class WallPresenter extends OpenVKPresenter
$feed = new Feed();
$channel = new Channel();
$channel->title(OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["SERVER_NAME"])->appendTo($feed);
$channel->title($owner->getCanonicalName() . "" . OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"])->appendTo($feed);
foreach($posts as $post) {
$item = new Item();
$item
->title($post->getOwner()->getCanonicalName())
->description($post->getText())
->url(ovk_scheme(true).$_SERVER["SERVER_NAME"]."/wall{$post->getPrettyId()}")
->url(ovk_scheme(true).$_SERVER["HTTP_HOST"]."/wall{$post->getPrettyId()}")
->pubDate($post->getPublicationTime()->timestamp())
->appendTo($channel);
}
@ -294,17 +294,11 @@ final class WallPresenter extends OpenVKPresenter
if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
if($wall > 0)
$this->redirect("/id$wall", 2); #Will exit
$wall = $wall * -1;
$this->redirect("/club$wall", 2);
$this->redirect($wallOwner->getURL());
}
function renderPost(int $wall, int $post_id): void
{
$this->assertUserLoggedIn();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post || $post->isDeleted())
$this->notFound();
@ -339,10 +333,7 @@ final class WallPresenter extends OpenVKPresenter
$post->toggleLike($this->user->identity);
}
$this->redirect(
"$_SERVER[HTTP_REFERER]#postGarter=" . $post->getId(),
static::REDIRECT_TEMPORARY
);
$this->redirect("$_SERVER[HTTP_REFERER]#postGarter=" . $post->getId());
}
function renderShare(int $wall, int $post_id): void
@ -394,8 +385,7 @@ final class WallPresenter extends OpenVKPresenter
$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);
exit;
$this->redirect($wall < 0 ? "/club" . ($wall*-1) : "/id" . $wall);
}
function renderPin(int $wall, int $post_id): void
@ -416,7 +406,7 @@ final class WallPresenter extends OpenVKPresenter
$post->unpin();
}
// TODO localize message based on language and ?act=(un)pin
# TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment"));
}
}

View file

@ -4,7 +4,7 @@
</div>
<div class="container_gray">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0}
<div class="content" n:foreach="$data as $dat">

View file

@ -3,7 +3,7 @@
{block wrap}
<div class="ovk-lw-container">
<div class="ovk-lw--list">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0}
<table n:foreach="$data as $dat" border="0" style="font-size:11px;" class="post">
@ -52,9 +52,9 @@
{include actions}
<hr/>
<div n:if="$sorting ?? true" class="tile">
<a href="?C=I;O=R" class="profile_link">{_"sort_randomly"}</a>
<a href="?C=M;O=D" class="profile_link">{_"sort_up"}</a>
<a href="?C=M;O=A" class="profile_link">{_"sort_down"}</a>
<a href="?C=I;O=R" class="profile_link">{_sort_randomly}</a>
<a href="?C=M;O=D" class="profile_link">{_sort_up}</a>
<a href="?C=M;O=A" class="profile_link">{_sort_down}</a>
</div>
</div>
</div>

View file

@ -1,17 +1,27 @@
{extends "@layout.xml"}
{block title}{_"banned_title"}{/block}
{block title}{_banned_title}{/block}
{block header}
{_"banned_header"}
{_banned_header}
{/block}
{block content}
<center>
<img src="/assets/packages/static/openvk/img/oof.apng" alt="{_'banned_alt'}" style="width: 20%;" />
<img src="/assets/packages/static/openvk/img/oof.apng" alt="{_banned_alt}" style="width: 20%;" />
</center>
<p>
{tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}<br/>
{tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape}
{if !$thisUser->getUnbanTime()}
{_banned_perm}
{else}
{tr("banned_until_time", $thisUser->getUnbanTime())|noescape}
{/if}
</p>
<p n:if="$thisUser->canUnbanThemself()">
<hr/>
<center><a class="button" href="/unban.php">{_banned_unban_myself}</a></center>
</p>
<hr/>
<p>

View file

@ -0,0 +1,34 @@
{extends "@layout.xml"}
{block title}{$thisUser->getCanonicalName()}{/block}
{block header}
{$thisUser->getCanonicalName()}
{/block}
{block content}
<div class="container_gray bottom" style="margin: -10px -10px 10px;">
{tr("profile_deactivated_msg", $thisUser->getDeactivationDate()->format("%e %B %G" . tr("time_at_sp") . "%R"))|noescape}
</div>
<div class="left_small_block">
<div>
<img src="{$thisUser->getAvatarUrl('normal')}"
alt="{$thisUser->getCanonicalName()}"
style="width: 100%; image-rendering: -webkit-optimize-contrast;" />
</div>
</div>
<div class="right_big_block">
<div class="page_info">
<div class="accountInfo clearFix">
<div class="profileName">
<h2>{$thisUser->getFullName()}</h2>
<div class="page_status" style="color: #AAA;">{_profile_deactivated_status}</div>
</div>
</div>
<center style="color: #AAA; margin: 40px 0; font-size: 13px;">
{_profile_deactivated_info|noescape}
</center>
</div>
</div>
{/block}

View file

@ -1,5 +1,4 @@
{var instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']}
{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']}
<!DOCTYPE html>
<html>
<head>

View file

@ -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.'}
<!DOCTYPE html>
<html n:if="!isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'">
<html>
<head>
<title>
{ifset title}{include title} - {/ifset}{$instance_name}
@ -17,7 +17,7 @@
{script "js/l10n.js"}
{script "js/openvk.cls.js"}
{if $isTimezoned == null}
{if $isTimezoned == NULL}
{script "js/timezone.js"}
{/if}
@ -26,7 +26,7 @@
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== null}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
@ -102,8 +102,13 @@
<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}
{if $thisUser->isDeactivated()}
<div class="link">
<a href="/" title="[Alt+Shift+,]" accesskey=",">{_header_home}</a>
<a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a>
</div>
{else}
<div class="link">
<a href="/">{_header_home}</a>
</div>
<div class="link">
<a href="/search?type=groups">{_header_groups}</a>
@ -125,6 +130,7 @@
<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>
{/if}
{else}
<div class="link">
<a href="/login">{_header_login}</a>
@ -142,7 +148,7 @@
<div class="sidebar">
<div class="navigation">
{ifset $thisUser}
{if !$thisUser->isBanned() XOR !$thisUser->isActivated()}
{if !$thisUser->isBanned() XOR !$thisUser->isActivated() XOR $thisUser->isDeactivated()}
<a href="/edit" class="link edit-button">{_edit_button}</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}
@ -161,19 +167,20 @@
</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" title="{_my_feed} [Alt+Shift+W]" accesskey="w">{_my_feed}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link" title="{_my_feed} [Alt+Shift+,]" accesskey=",">{_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="/apps?act=installed" class="link">{_my_apps}</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" title="Админ-панель [Alt+Shift+A]" accesskey="a">Админ-панель</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,9 +193,50 @@
<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} &#xbb;</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;" />
<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: 10px;" />
</a>
<div class="floating_sidebar">
<a class="minilink" href="/friends{$thisUser->getId()}">
<object type="internal/link" n:if="$thisUser->getFollowersCount() > 0">
<div class="counter">
+{$thisUser->getFollowersCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/friends.svg">
</a>
<a class="minilink" href="/albums{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/photos.svg">
</a>
<a class="minilink" href="/im">
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
<div class="counter">
+{$thisUser->getUnreadMessagesCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/messages.svg">
</a>
<a class="minilink" href="/groups{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/groups.svg">
</a>
<a class="minilink" href="/notifications">
<object type="internal/link" n:if="$thisUser->getNotificationsCount() > 0">
<div class="counter">
+{$thisUser->getNotificationsCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/feedback.svg">
</a>
</div>
{elseif !$thisUser->isActivated()}
<a href="/logout?hash={urlencode($csrfToken)}" class="link">{_menu_logout}</a>
{else}
@ -251,7 +299,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>
@ -267,6 +315,8 @@
</p>
</div>
{include "components/cookies.xml"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/node_modules/soundjs/lib/soundjs.min.js"}
{script "js/node_modules/ky/umd.js"}
@ -325,6 +375,7 @@
{/ifset}
</body>
</html>
{/if}
{if isset($parentModule) && substr($parentModule, 0, 21) !== 'libchandler:absolute.'}
<!-- INCLUDING TEMPLATE FROM PARENTMODULE: {$parentModule} -->

View file

@ -4,7 +4,7 @@
<div class="wrap2">
<div class="wrap1">
<div class="page_wrap padding_top">
<div n:ifset="tabs" class="tabs">
<div n:ifset="tabs" n:ifcontent class="tabs stupid-fix">
{include tabs}
</div>
@ -16,7 +16,7 @@
{include specpage, x => $dat}
{else}
<div class="container_gray">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0}
<div class="content" n:foreach="$data as $dat">
@ -41,7 +41,7 @@
{include description, x => $dat}
{/ifset}
</td>
<td n:ifset="actions" valign="top" class="action_links" style="width: 150px; text-transform: lowercase;">
<td n:ifset="actions" valign="top" class="action_links" style="width: 150px;">
{include actions, x => $dat}
</td>
</tr>

View file

@ -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>
{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 n:foreach="$popularClubs as $entry" style="margin-top: 5px;">
<a href="{$entry->club->getURL()}">{$entry->club->getName()}</a>
<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", $entry->subscriptions)}
{tr("participants", $club->subscriptions)}
</div>
</li>
</ol>
</td>
</tr>
</tbody>
</table>
{/if}
<h4>{_rules}</h4>

View file

@ -1,15 +1,15 @@
{extends "../@layout.xml"}
{block title}{_"welcome"}{/block}
{block title}{_welcome}{/block}
{block header}
{_"welcome"}
{_welcome}
{/block}
{block content}
{presenter "openvk!Support->knowledgeBaseArticle", "about"}
<center>
<a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_"log_in"}</a>
<a class="button" style="cursor: pointer;" href="/reg">{_"registration"}</a>
<a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_log_in}</a>
<a class="button" style="cursor: pointer;" href="/reg">{_registration}</a>
</center>
{* TO-DO: Add statistics about this instance as on mastodon.social *}
{/block}

View file

@ -6,9 +6,9 @@
{/block}
{block content}
{_"you_can_invite"}<br><br>
{_you_can_invite}<br><br>
<center>
<input type="text" readonly value="https://{$_SERVER["HTTP_HOST"]}/reg?ref={rawurlencode($thisUser->getRefLinkId())}" size="50" />
</center>
<p>{_"you_can_invite_2"}</p>
<p>{_you_can_invite_2}</p>
{/block}

View file

@ -1,8 +1,8 @@
{extends "../@layout.xml"}
{block title}{_"select_language"}{/block}
{block title}{_select_language}{/block}
{block header}
{_"select_language"}
{_select_language}
{/block}
{block content}

View file

@ -396,8 +396,8 @@
<tr>
<td class="e">
Vladimir Barinov (veselcraft), Celestora, Konstantin Kichulkin (kosfurler),
Nikita Volkov (sup_ban), Daniel Myslivets, Alexander Kotov (l-lacker),
Alexey Assemblerov (BiosNod), Ilya Prokopenko (dsrev) and Maxim Leshchenko (maksales / maksalees)
Nikita Volkov (sup_ban), Daniel Myslivets, Maxim Leshchenko (maksales / maksalees)
and n1rwana
</td>
</tr>
</tbody>
@ -474,7 +474,7 @@
</tr>
<tr>
<td class="e">Best barmaid</td>
<td class="v">Jill</td> {* I can agree ~~ dsrev *}
<td class="v">Jill</td>
</tr>
<tr>
<td class="e">Initial Helpdesk implementation</td>

View file

@ -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']}</title>
<title>{include title} - {_admin} {$instance_name}</title>
</head>
<body>
<div id="page">
@ -15,23 +16,15 @@
<div class="aui-header-inner">
<div class="aui-header-primary">
<h1 id="logo" class="aui-header-logo aui-header-logo-textonly">
<a href="/admin">
<span class="aui-header-logo-device">{=OPENVK_ROOT_CONF['openvk']['appearance']['name']}</span>
<a href="/">
<span class="aui-header-logo-device">{$instance_name}</span>
</a>
</h1>
</div>
<div n:if="$search ?? false" class="aui-header-secondary">
<ul class="aui-nav">
<form class="aui-quicksearch dont-default-focus ajs-dirty-warning-exempt">
<input
id="quickSearchInput"
autocomplete="off"
class="search"
type="text"
placeholder="{include searchTitle}"
value="{$_GET['q'] ?? ''}"
name="q"
accesskey="Q" />
<input id="quickSearchInput" autocomplete="off" class="search" type="text" placeholder="{include searchTitle}" value="{$_GET['q'] ?? ''}" name="q" accesskey="Q" />
<input type="hidden" value=1 name=p />
</form>
</ul>
@ -46,83 +39,67 @@
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<div class="aui-nav-heading">
<strong>Обзор</strong>
<strong>{_admin_overview}</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin">
Сводка
</a>
<a href="/admin">{_admin_overview_summary}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Пользовательский контент</strong>
<strong>{_admin_content}</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin/users">
Пользователи
</a>
<a href="/admin/users">{_users}</a>
</li>
<li>
<a href="/admin/clubs">
Группы
</a>
<a href="/admin/clubs">{_groups}</a>
</li>
<li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Платные услуги</strong>
<strong>{_admin_services}</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin/vouchers">
{_vouchers}
</a>
<a href="/admin/vouchers">{_vouchers}</a>
</li>
<li>
<a href="/admin/gifts">
Подарки
</a>
<a href="/admin/gifts">{_gifts}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Настройки</strong>
<strong>{_admin_settings}</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin/settings/tuning">
Общие
</a>
<a href="/admin/settings/tuning">{_admin_settings_tuning}</a>
</li>
<li>
<a href="/admin/settings/appearance">
Внешний вид
</a>
<a href="/admin/settings/appearance">{_admin_settings_appearance}</a>
</li>
<li>
<a href="/admin/settings/security">
Безопасность
</a>
<a href="/admin/settings/security">{_admin_settings_security}</a>
</li>
<li>
<a href="/admin/settings/integrations">
Интеграции
</a>
<a href="/admin/settings/integrations">{_admin_settings_integrations}</a>
</li>
<li>
<a href="/admin/settings/system">
Система
</a>
<a href="/admin/settings/system">{_admin_settings_system}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Об OpenVK</strong>
<strong>{_admin_about}</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/about:openvk">
Версия
</a>
<a href="/about:openvk">{_admin_about_version}</a>
</li>
<li>
<a href="/about">{_admin_about_instance}</a>
</li>
</ul>
</div>
@ -131,7 +108,7 @@
</div>
<section class="aui-page-panel-content">
{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]}
<div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;">
<p class="title">
<strong>{$flashMessage->title}</strong>

View file

@ -0,0 +1,48 @@
{extends "@layout.xml"}
{block title}
{_edit}
{/block}
{block heading}
{_edit} #{$form->id ?? "undefined"}
{/block}
{block content}
<div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs">
<ul class="tabs-menu">
<li class="menu-item active-tab">
<a href="#info">{_admin_banned_link}</a>
</li>
</ul>
<div class="tabs-pane active-pane" id="info">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div>
<div class="field-group">
<label for="token">{_admin_banned_domain}</label>
<input class="text long-field" type="text" id="link" name="link" value="{$form->link}" />
<div class="description">{_admin_banned_link_description}</div>
</div>
<div class="field-group">
<label for="token">{_admin_banned_link_regexp}</label>
<input class="text long-field" type="text" id="regexp" name="regexp" value="{$form->regexp}" />
<div class="description">{_admin_banned_link_regexp_description}</div>
</div>
<div class="field-group">
<label for="coins">{_admin_banned_link_reason}</label>
<input class="text long-field" type="text" id="reason" name="reason" value="{$form->reason}" />
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
<a class="aui-button aui-button-secondary" href="/admin/bannedLink/id{$form->id}/unban">{_delete}</a>
</div>
</div>
</form>
</div>
</div>
{/block}

View file

@ -0,0 +1,46 @@
{extends "@layout.xml"}
{block title}
{_admin_banned_links}
{/block}
{block heading}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/bannedLink/id0">
{_create}
</a>
{_admin_banned_links}
{/block}
{block content}
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_banned_domain}</th>
<th>REGEXP</th>
<th>{_admin_banned_link_reason}</th>
<th>{_admin_banned_link_initiator}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$links as $link">
<td>{$link->getId()}</td>
<td>{$link->getDomain()}</td>
<td>{$link->getRegexpRule()}</td>
<td>{$link->getReason() ?? "-"}</td>
<td>{$link->getInitiator()->getCanonicalName()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/bannedLink/id{$link->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
</tbody>
</table>
<div align="right">
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">«</a>
<a class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">»</a>
</div>
{/block}

View file

@ -1,41 +1,35 @@
{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}
<!-- This main block -->
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
{if $isMain}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li class="aui-nav-selected"><a href="?act=main">Главное</a></li>
<li><a href="?act=ban">Бан</a></li>
<li><a href="?act=followers">Участники</a></li>
<li class="aui-nav-selected"><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=ban">{_admin_tab_ban}</a></li>
<li><a href="?act=followers">{_admin_tab_followers}</a></li>
</ul>
</div>
</div>
</nav>
</nav>
<form class="aui" method="POST">
<div class="field-group">
<label for="avatar">
Аватарка
</label>
<label for="avatar">{_avatar}</label>
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img src="{$club->getAvatarUrl('tiny')}" style="object-fit: cover;"></img>
@ -43,149 +37,121 @@
</span>
</div>
<div class="field-group">
<label for="id">
ID
</label>
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$club->getId()}" />
</div>
<div class="field-group">
<label for="id_owner">
ID владельца
</label>
<label for="id_owner">{_admin_ownerid}</label>
<input class="text medium-field" type="text" id="id_owner" name="id_owner" value="{$club->getOwner()->getId()}" />
</div>
<div class="field-group">
<label for="name">
Название
</label>
<label for="name">{_admin_title}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$club->getName()}" />
</div>
<div class="field-group">
<label for="about">
Описание
</label>
<label for="about">{_admin_description}</label>
<input class="text medium-field" type="text" id="about" name="about" value="{$club->getDescription()}" />
</div>
<div class="field-group">
<label for="shortcode">
Адрес
</label>
<label for="shortcode">{_admin_shortcode}</label>
<input class="text medium-field" type="text" id="shortcode" name="shortcode" value="{$club->getShortCode()}" />
</div>
<br/>
<div class="group">
<input class="toggle-large" type="checkbox" id="verify" name="verify" value="1" {if $club->isVerified()} checked {/if} />
<label for="verify">
Верификация
</label>
<label for="verify">{_admin_verification}</label>
</div>
<div class="group">
<input class="toggle-large" type="checkbox" id="hide_from_global_feed" name="hide_from_global_feed" value="1" {if $club->isHideFromGlobalFeedEnabled()} checked {/if} />
<label for="hide_from_global_feed">
Не отображать записи в глобальной ленте
</label>
<label for="hide_from_global_feed">{_admin_club_excludeglobalfeed}</label>
</div>
<hr/>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="Сохранить">
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{/if}
</div>
{/if}
{if $isBan}
<!-- This ban block -->
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
{if $isBan}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">Главное</a></li>
<li class="aui-nav-selected"><a href="?act=ban">Бан</a></li>
<li><a href="?act=followers">Участники</a></li>
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li class="aui-nav-selected"><a href="?act=ban">{_admin_tab_ban}</a></li>
<li><a href="?act=followers">{_admin_tab_followers}</a></li>
</ul>
</div>
</div>
</nav>
<form class="aui" method="POST">
<div class="field-group">
<label for="ban_reason">
Причина бана
</label>
</nav>
<form class="aui" method="POST">
<div class="field-group">
<label for="ban_reason">{_admin_banreason}</label>
<input class="text" type="text" id="text-input" name="ban_reason" value="{$club->getBanReason()}" />
</div>
<hr/>
<div class="buttons-container">
</div>
<hr/>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="Сохранить">
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{/if}
</div>
</form>
</div>
{/if}
{if $isFollowers}
{if $isFollowers}
{var $followers = iterator_to_array($followers)}
<!-- This followers block -->
{var followers = iterator_to_array($followers)}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">Главное</a></li>
<li><a href="?act=ban">Бан</a></li>
<li class="aui-nav-selected"><a href="?act=followers">Участники</a></li>
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=ban">{_admin_tab_ban}</a></li>
<li class="aui-nav-selected"><a href="?act=followers">{_admin_tab_followers}</a></li>
</ul>
</div>
</div>
</nav>
<table rules="none" class="aui aui-table-list">
</nav>
<table rules="none" class="aui aui-table-list">
<tbody>
<tr n:foreach="$followers as $follower">
<td>{$follower->getId()}</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$follower->getAvatarUrl('miniscule')}" alt="{$follower->getCanonicalName()}" role="presentation" />
<img src="{$follower->getAvatarUrl()}" alt="{$follower->getCanonicalName()}" role="presentation" />
</span>
</span>
<a href="{$follower->getURL()}">{$follower->getCanonicalName()}</a>
<span n:if="$follower->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">
заблокирован
</span>
<span n:if="$follower->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$follower->isFemale() ? "Женский" : "Мужской"}</td>
<td>{$follower->getShortCode() ?? "(отсутствует)"}</td>
<td>{$follower->isFemale() ? tr("female") : tr("male")}</td>
<td>{$follower->getShortCode() ?? "(" . tr("none") . ")"}</td>
<td>{$follower->getRegistrationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/users/id{$follower->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
</tbody>
</table>
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
{var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">«</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">»</a>
</div>
</div>
{/if}
</div>
{/if}
{/block}

View file

@ -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)}
<table class="aui aui-table-list">
<thead>
<tr>
<th>#</th>
<th>Имя</th>
<th>Автор</th>
<th>Описание</th>
<th>Короткий адрес</th>
<th>Действия</th>
<th>ID</th>
<th>{_admin_title}</th>
<th>{_admin_author}</th>
<th>{_admin_description}</th>
<th>{_admin_shortcode}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
@ -39,7 +41,7 @@
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
</td>
<td>
{var user = $club->getOwner()}
{var $user = $club->getOwner()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
@ -49,11 +51,11 @@
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
</td>
<td>{$club->getDescription() ?? "(не указано)"}</td>
<td>{$club->getDescription() ?? "(" . tr("none") . ")"}</td>
<td>{$club->getShortCode()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/clubs/id{$club->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
@ -61,13 +63,9 @@
</table>
<br/>
<div align="right">
{var isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
{var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -2,9 +2,9 @@
{block title}
{if $form->id === 0}
Новый подарок
{_admin_newgift}
{else}
Подарок "{$form->name}"
{_gift} "{$form->name}"
{/if}
{/block}
@ -16,7 +16,7 @@
<form class="aui" method="POST" enctype="multipart/form-data">
<div class="field-group">
<label for="avatar">
Изображение
{_admin_image}
<span n:if="$form->id === 0" class="aui-icon icon-required"></span>
</label>
{if $form->id === 0}
@ -29,43 +29,39 @@
</span>
<input style="display: none;" id="picInput" type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" />
<div class="description">
<a id="picChange" href="javascript:false">Заменить изображение?</a>
<a id="picChange" href="javascript:false">{_admin_image_replace}</a>
</div>
{/if}
</div>
<div class="field-group">
<label for="id">
ID
</label>
<label for="id">ID</label>
<input class="text long-field" type="number" id="id" disabled="disabled" value="{$form->id}" />
</div>
<div class="field-group">
<label for="putin">
Использований
</label>
<input class="text long-field" type="number" id="putin" disabled="disabled" value="{$form->usages}" />
<label for="usages">{_admin_uses}</label>
<input class="text long-field" type="number" id="usages" disabled="disabled" value="{$form->usages}" />
<div n:if="$form->usages > 0" class="description">
<a href="javascript:$('#putin').value(0);">Обнулить?</a>
<a href="javascript:$('#usages').value(0);">{_admin_uses_reset}</a>
</div>
</div>
<div class="field-group">
<label for="name">
Внутренее имя
{_admin_name}
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name" name="name" value="{$form->name}" />
</div>
<div class="field-group">
<label for="price">
Цена
{_admin_price}
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" id="price" name="price" min="0" value="{$form->price}" />
</div>
<div class="field-group">
<label for="limit">
Ограничение
{_admin_limits}
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" min="-1" id="limit" name="limit" value="{$form->limit}" />
@ -76,7 +72,7 @@
<input n:attr="disabled => $form->id === 0, checked => $form->id === 0" class="checkbox" type="checkbox" name="reset_limit" id="reset_limit" />
<span class="aui-form-glyph"></span>
<label for="reset_limit">Сбросить счётчик ограничений</label>
<label for="reset_limit">{_admin_limits_reset}</label>
</div>
</fieldset>

View file

@ -1,7 +1,7 @@
{extends "@layout.xml"}
{block title}
Наборы подарков
{_admin_giftsets}
{/block}
{block headingWrap}
@ -9,7 +9,7 @@
{_create}
</a>
<h1>Наборы подарков</h1>
<h1>{_admin_giftsets}</h1>
{/block}
{block content}
@ -27,12 +27,11 @@
</td>
<td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}.meta">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
<a class="aui-button" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}/">
<span class="aui-icon aui-icon-small aui-iconfont-gallery">Открыть</span>
Открыть
<span class="aui-icon aui-icon-small aui-iconfont-gallery">{_admin_open}</span>
</a>
</td>
</tr>
@ -40,17 +39,13 @@
</table>
{else}
<center>
<p>Наборов подарков нету. Чтобы создать подарок, создайте набор.</p>
<p>{_admin_giftsets_none}</p>
</center>
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
{var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -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}
<form class="aui" method="POST">
<h3>Общие настройки</h3>
<h3>{_admin_commonsettings}</h3>
<fieldset>
<div class="field-group">
<label for="id">
@ -24,37 +24,37 @@
</div>
<div class="field-group">
<label for="name_master">
Наименование
{_admin_name}
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name_master" name="name_master" value="{$form->languages['master']->name}" />
<div class="description">Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
<div class="description">{_admin_giftsets_title}</div>
</div>
<div class="field-group">
<label for="description_master">
Описание
{_admin_description}
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="description_master" name="description_master" value="{$form->languages['master']->description}" />
<div class="description">Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
<div class="description">{_admin_giftsets_description}</div>
</div>
</fieldset>
<h3>Языко-зависимые настройки</h3>
<h3>{_admin_langsettings}</h3>
<fieldset>
{foreach $form->languages as $locale => $data}
{continueIf $locale === "master"}
<div class="field-group">
<label for="name_{$locale}">
Наименование
{_admin_name}
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="name_{$locale}" name="name_{$locale}" value="{$data->name}" />
</div>
<div class="field-group">
<label for="description_{$locale}">
Описание
{_admin_description}
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="description_{$locale}" name="description_{$locale}" value="{$data->description}" />

View file

@ -9,7 +9,7 @@
{_create}
</a>
<h1>Набор "{$cat->getName()}"</h1>
<h1>{_admin_giftset} "{$cat->getName()}"</h1>
{/block}
{block content}
@ -32,11 +32,11 @@
<td style="vertical-align: middle;">
{$gift->getName()}
<span n:if="$gift->isFree()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">
бесплатный
{_admin_price_free}
</span>
</td>
<td style="vertical-align: middle;">
{$gift->getPrice()} голосов
{tr("points_amount", $gift->getPrice())}
</td>
<td style="vertical-align: middle;">
{$gift->getUsages()} раз
@ -71,12 +71,9 @@
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
{var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -1,13 +1,13 @@
{extends "@layout.xml"}
{block title}
Сводка
{_admin_overview_summary}
{/block}
{block heading}
Сводка
{_admin_overview_summary}
{/block}
{block content}
Да!
┬─┬︵/(.□.)╯
{/block}

View file

@ -1,7 +1,7 @@
{extends "@layout.xml"}
{block title}
Редактировать {$user->getCanonicalName()}
{_edit} {$user->getCanonicalName()}
{/block}
{block heading}
@ -12,9 +12,7 @@
<div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST">
<div class="field-group">
<label for="avatar">
Аватарка
</label>
<label for="avatar">{_avatar}</label>
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl('tiny')}" style="object-fit: cover;"></img>
@ -22,83 +20,60 @@
</span>
</div>
<div class="field-group">
<label for="id">
ID
</label>
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$user->getId()}" />
</div>
<div class="field-group">
<label for="guid">
GUID
</label>
<label for="guid">GUID</label>
<input class="text medium-field" id="guid" disabled value="{$user->getChandlerUser()->getId()}" />
</div>
<div class="field-group">
<label for="registration_ip">
Первый IP
</label>
<input class="text medium-field" id="registration_ip" disabled value="{$user->getRegistrationIP()}" />
<label for="registration_ip">{_admin_first_known_ip}</label>
<input class="text medium-field" id="guid" disabled value="{$user->getRegistrationIP()}" />
</div>
<div class="field-group">
<label for="first_name">
Имя
</label>
<label for="first_name">{_name}</label>
<input class="text medium-field" type="text" id="first_name" name="first_name" value="{$user->getFirstName()}" />
</div>
<div class="field-group">
<label for="last_name">
Фамилия
</label>
<label for="last_name">{_surname}</label>
<input class="text medium-field" type="text" id="last_name" name="last_name" value="{$user->getLastName()}" />
</div>
<div class="field-group">
<label for="nickname">
Никнейм
</label>
<label for="nickname">{_nickname}</label>
<input class="text medium-field" type="text" id="nickname" name="nickname" value="{$user->getPseudo()}" />
</div>
<div class="field-group">
<label for="status">
Статус
</label>
<label for="status">{_status}</label>
<input class="text medium-field" type="text" id="status" name="status" value="{$user->getStatus()}" />
</div>
<div class="field-group">
<label for="email">
E-Mail
</label>
<label for="email">E-Mail</label>
<input class="text medium-field" type="email" id="email" name="email" value="{$user->getEmail()}" />
</div>
<div class="field-group">
<label for="shortcode">
Адрес страницы
</label>
<label for="shortcode">{_admin_shortcode}</label>
<input class="text medium-field" type="text" id="shortcode" name="shortcode" value="{$user->getShortCode()}" />
</div>
<hr>
<div class="field-group">
<label for="city">
Верификация
</label>
<label for="city">{_admin_verification}</label>
<input class="toggle-large" type="checkbox" id="verify" name="verify" value="1" {if $user->isVerified()} checked {/if} />
</div>
<div class="field-group">
<label for="city">
Онлайн статус
</label>
<label for="city">{_admin_user_online}</label>
<select name="online" class="select">
<option value="0" {if $user->onlineStatus() > 2}selected{/if}>По-умолчанию</option>
<option value="1" {if $user->onlineStatus() == 1}selected{/if}>Инкогнито</option>
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>Юзер умер</option>
<option value="0" {if $user->onlineStatus() > 2}selected{/if}>{_admin_user_online_default}</option>
<option value="1" {if $user->onlineStatus() == 1}selected{/if}>{_admin_user_online_incognite}</option>
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option>
</select>
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="Сохранить">
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{/block}

View file

@ -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)}
<table class="aui aui-table-list">
<thead>
<tr>
<th>#</th>
<th>Имя</th>
<th>Пол</th>
<th>Короткий адрес</th>
<th>Дата регистрации</th>
<th>Действия</th>
<th>ID</th>
<th>{_admin_name}</th>
<th>{_gender}</th>
<th>{_admin_shortcode}</th>
<th>{_registration_date}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
@ -38,20 +40,18 @@
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">
заблокирован
</span>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$user->isFemale() ? "Женский" : "Мужской"}</td>
<td>{$user->getShortCode() ?? "(отсутствует)"}</td>
<td>{$user->isFemale() ? tr("female") : tr("male")}</td>
<td>{$user->getShortCode() ?? "(" . tr("none") . ")"}</td>
<td>{$user->getRegistrationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/users/id{$user->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
{if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)}
<a class="aui-button" href="/setSID/{$user->getChandlerUser()->getId()}?hash={rawurlencode($csrfToken)}">
<span class="aui-icon aui-icon-small aui-iconfont-sign-in">Войти как</span>
<span class="aui-icon aui-icon-small aui-iconfont-sign-in">{_admin_loginas}</span>
</a>
{/if}
</td>
@ -60,13 +60,9 @@
</table>
<br/>
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
{var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

Some files were not shown because too many files have changed in this diff Show more