Compare commits

...

6 commits

Author SHA1 Message Date
koke228666
e87d1673e7
Merge c82e2c9f4c into a12c77083b 2025-03-17 01:33:42 +07:00
Vladimir Barinov
a12c77083b
fix(links, away) (fixes #1253) 2025-03-16 17:57:21 +03:00
Vladimir Barinov
4815186b79
chore(statistics): change behavior of active users count (#1254) 2025-03-16 17:53:58 +03:00
Vladimir Barinov
9ef7d2d7c4
fix(age): calculation (fixes #1252) 2025-03-16 16:51:05 +03:00
mrilyew
b92bf7f41a
feat(fave) (#1240)
* add liked content

* fix order

* linter

* linter errors fix

* задушнил

---------

Co-authored-by: veselcraft <veselcraft@icloud.com>
2025-03-09 16:42:19 +03:00
def76226b7
feat(core): add phpstan for static analysis (#1223)
* feat: add phpstan for static analysis

* ci(actions): add phpstan action

* ci(actions): do analysing inside docker container

* fix(FetchToncoinTransactions): add var declaration

* fix(ServiceAPI/Wall): add var declaration

* fix(bootstrap): remove case-insensitive false vars

* fix(VKAPI/Handlers/Board): change parameters order

* fix(VKAPIRequestHandler): set fail's return type as never

* fix(VKAPI/Handlers/Groups): add array declaration

* fix(VKAPI/Handlers/Newsfeed): add return_banned declaration

* fix(VKAPI/Handlers/Notes): move $nodez declaration up

* fix(phpstan): most of the things and stupid lines of code

* fix(lint)

* fix(phpstan): less errors

* fix(lint): again. cuz i forgot about it

* fix(stan): all errors are gone now =3

---------

Co-authored-by: veselcraft <veselcraft@icloud.com>
2025-03-09 16:03:33 +03:00
62 changed files with 560 additions and 232 deletions

36
.github/workflows/analyse.yaml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Static analysis
on:
push:
pull_request:
jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-20.04
# 'push' runs on inner branches, 'pull_request' will run only on outer PRs
if: >
github.event_name == 'push'
|| (github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name != github.repository)
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and start Docker container
working-directory: install/automated/docker
run: |
docker build -t openvk ../../.. -f openvk.Dockerfile
- name: Run Docker container with PHPStan
working-directory: install/automated/docker
run: |
docker container run --rm \
-v ./chandler.example.yml:/opt/chandler/chandler.yml \
-v ./openvk.example.yml:/opt/chandler/extensions/available/openvk/openvk.yml \
openvk vendor/bin/phpstan analyse --memory-limit 1G

View file

@ -18,6 +18,7 @@ define("NANOTON", 1000000000);
class FetchToncoinTransactions extends Command
{
private $images;
private $transactions;
protected static $defaultName = "fetch-ton";

View file

@ -13,6 +13,7 @@ class Wall implements Handler
protected $user;
protected $posts;
protected $notes;
protected $videos;
public function __construct(?User $user)
{

View file

@ -248,8 +248,9 @@ final class Board extends VKAPIRequestHandler
return 1;
}
public function editComment(int $comment_id, int $group_id = 0, int $topic_id = 0, string $message, string $attachments)
public function editComment(string $message, string $attachments, int $comment_id, int $group_id = 0, int $topic_id = 0)
{
# FIXME
/*
$this->requireUser();
$this->willExecuteWriteAction();

View file

@ -45,7 +45,7 @@ final class Groups extends VKAPIRequestHandler
$clbsCount = $user->getClubCount();
}
$rClubs;
$rClubs = [];
$ic = sizeof($clbs);
if (sizeof($clbs) > $count) {

View file

@ -52,7 +52,7 @@ final class Newsfeed extends VKAPIRequestHandler
return $response;
}
public function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $rss = 0)
public function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $rss = 0, int $return_banned = 0)
{
$this->requireUser();

View file

@ -185,12 +185,14 @@ final class Notes extends VKAPIRequestHandler
$this->fail(15, "Access denied");
}
$nodez = (object) [
"count" => 0,
"notes" => [],
];
if (empty($note_ids)) {
$nodez->count = (new NotesRepo())->getUserNotesCount($user);
$notes = array_slice(iterator_to_array((new NotesRepo())->getUserNotes($user, 1, $count + $offset, $sort == 0 ? "ASC" : "DESC")), $offset);
$nodez = (object) [
"count" => (new NotesRepo())->getUserNotesCount((new UsersRepo())->get($user_id)),
"notes" => [],
];
foreach ($notes as $note) {
if ($note->isDeleted()) {
@ -210,6 +212,7 @@ final class Notes extends VKAPIRequestHandler
$note = (new NotesRepo())->getNoteById((int) $id[0], (int) $id[1]);
if ($note && !$note->isDeleted()) {
$nodez->notes[] = $note->toVkApiStruct();
$nodez->count++;
}
}
}

View file

@ -9,6 +9,7 @@ use Nette\Utils\ImageException;
use openvk\Web\Models\Entities\{Photo, Album, Comment};
use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;

View file

@ -292,14 +292,14 @@ final class Users extends VKAPIRequestHandler
break;
case 'blacklisted_by_me':
if (!$authuser) {
continue;
break;
}
$response[$i]->blacklisted_by_me = (int) $usr->isBlacklistedBy($this->getUser());
break;
case 'blacklisted':
if (!$authuser) {
continue;
break;
}
$response[$i]->blacklisted = (int) $this->getUser()->isBlacklistedBy($usr);
@ -383,7 +383,8 @@ final class Users extends VKAPIRequestHandler
string $fav_music = "",
string $fav_films = "",
string $fav_shows = "",
string $fav_books = ""
string $fav_books = "",
string $interests = ""
) {
if ($count > 100) {
$this->fail(100, "One of the parameters specified was missing or invalid: count should be less or equal to 100");

View file

@ -20,7 +20,7 @@ abstract class VKAPIRequestHandler
$this->platform = $platform;
}
protected function fail(int $code, string $message): void
protected function fail(int $code, string $message): never
{
throw new APIErrorException($message, $code);
}

View file

@ -53,7 +53,7 @@ final class Wall extends VKAPIRequestHandler
$this->fail(15, "Access denied: wall is disabled");
} // Don't search for logic here pls
$iteratorv;
$iteratorv = null;
switch ($filter) {
case "all":
@ -722,7 +722,7 @@ final class Wall extends VKAPIRequestHandler
$post->attach($attachment);
}
if ($wall > 0 && $wall !== $this->user->identity->getId()) {
if ($owner_id > 0 && $owner_id !== $this->user->identity->getId()) {
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
}
@ -734,7 +734,7 @@ final class Wall extends VKAPIRequestHandler
$this->requireUser();
$this->willExecuteWriteAction();
$postArray;
$postArray = [];
if (preg_match('/(wall|video|photo)((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) {
$this->fail(100, "One of the parameters specified was missing or invalid: object is incorrect");
}

View file

@ -351,7 +351,7 @@ class Document extends Media
return $this->getRecord()->owner;
}
public function toApiPreview(): object
public function toApiPreview(): ?object
{
$preview = $this->getPreview();
if ($preview instanceof Photo) {
@ -360,6 +360,8 @@ class Document extends Media
"sizes" => array_values($preview->getVkApiSizes()),
],
];
} else {
return null;
}
}

View file

@ -112,7 +112,6 @@ class Gift extends RowModel
public function setImage(string $file): bool
{
$imgBlob;
try {
$image = Image::fromFile($file);
$image->resize(512, 512, Image::SHRINK_ONLY);

View file

@ -33,6 +33,8 @@ class Message extends RowModel
return (new Users())->get($this->getRecord()->sender_id);
} elseif ($this->getRecord()->sender_type === 'openvk\Web\Models\Entities\Club') {
return (new Clubs())->get($this->getRecord()->sender_id);
} else {
return null;
}
}
@ -49,6 +51,8 @@ class Message extends RowModel
return (new Users())->get($this->getRecord()->recipient_id);
} elseif ($this->getRecord()->recipient_type === 'openvk\Web\Models\Entities\Club') {
return (new Clubs())->get($this->getRecord()->recipient_id);
} else {
return null;
}
}
@ -147,7 +151,7 @@ class Message extends RowModel
"id" => $author->getId(),
"link" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $author->getURL(),
"avatar" => $author->getAvatarUrl(),
"name" => $author->getFirstName() . $unreadmsg,
"name" => $author->getFirstName(),
],
"timing" => [
"sent" => (string) $this->getSendTimeHumanized(),

View file

@ -64,13 +64,15 @@ class Notification
return $this->recipient;
}
public function getModel(int $index): RowModel
public function getModel(int $index): ?RowModel
{
switch ($index) {
case 0:
return $this->originModel;
case 1:
return $this->targetModel;
default:
return null;
}
}

View file

@ -385,9 +385,9 @@ class Photo extends Media
}
}
public static function fastMake(int $owner, string $description = "", array $file, ?Album $album = null, bool $anon = false): Photo
public static function fastMake(int $owner, string $description, array $file, ?Album $album = null, bool $anon = false): Photo
{
$photo = new static();
$photo = new Photo();
$photo->setOwner($owner);
$photo->setDescription(iconv_substr($description, 0, 36) . "...");
$photo->setAnonymous($anon);

View file

@ -45,11 +45,7 @@ class Report extends RowModel
public function isDeleted(): bool
{
if ($this->getRecord()->deleted === 0) {
return false;
} elseif ($this->getRecord()->deleted === 1) {
return true;
}
return $this->getRecord()->deleted === 1;
}
public function authorId(): int

View file

@ -41,11 +41,11 @@ trait TRichText
return preg_replace_callback(
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]);
$href = rawurlencode(str_replace(";", "&#59;", $href));
$link = str_replace("#", "&num;", $matches[3]);
$href = rawurlencode($matches[1]);
$href = str_replace("%26amp%3B", "%26", $href);
$link = $matches[3];
# this string breaks ampersands
$link = str_replace(";", "&#59;", $link);
# $link = str_replace(";", "&#59;", $link);
$rel = $this->isAd() ? "sponsored" : "ugc";
/*$server_domain = str_replace(':' . $_SERVER['SERVER_PORT'], '', $_SERVER['HTTP_HOST']);

View file

@ -524,7 +524,10 @@ class User extends RowModel
public function getAge(): ?int
{
return (int) floor((time() - $this->getBirthday()->timestamp()) / YEAR);
$birthday = new \DateTime();
$birthday->setTimestamp($this->getBirthday()->timestamp());
$today = new \DateTime();
return (int) $today->diff($birthday)->y;
}
public function get2faSecret(): ?string
@ -558,6 +561,7 @@ class User extends RowModel
"poster",
"apps",
"docs",
"fave",
],
])->get($id);
}
@ -932,6 +936,7 @@ class User extends RowModel
case 1:
return tr('female');
case 2:
default:
return tr('neutral');
}
}
@ -1195,6 +1200,7 @@ class User extends RowModel
"poster",
"apps",
"docs",
"fave",
],
])->set($id, (int) $status)->toInteger();
@ -1559,14 +1565,14 @@ class User extends RowModel
break;
case "blacklisted_by_me":
if (!$user) {
continue;
break;
}
$res->blacklisted_by_me = (int) $this->isBlacklistedBy($user);
break;
case "blacklisted":
if (!$user) {
continue;
break;
}
$res->blacklisted = (int) $user->isBlacklistedBy($this);

View file

@ -9,12 +9,13 @@ use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandE
use openvk\Web\Models\VideoDrivers\VideoDriver;
use Nette\InvalidStateException as ISE;
define("VIDEOS_FRIENDLY_ERROR", "Uploads are disabled on this instance :<", false);
define("VIDEOS_FRIENDLY_ERROR", "Uploads are disabled on this instance :<");
class Video extends Media
{
public const TYPE_DIRECT = 0;
public const TYPE_EMBED = 1;
public const TYPE_DIRECT = 0;
public const TYPE_EMBED = 1;
public const TYPE_UNKNOWN = -1;
protected $tableName = "videos";
protected $fileExtension = "mp4";
@ -108,6 +109,7 @@ class Video extends Media
} elseif (!is_null($this->getRecord()->link)) {
return Video::TYPE_EMBED;
}
return Video::TYPE_UNKNOWN;
}
public function getVideoDriver(): ?VideoDriver
@ -238,7 +240,7 @@ class Video extends Media
$this->save();
}
public static function fastMake(int $owner, string $name = "Unnamed Video.ogv", string $description = "", array $file, bool $unlisted = true, bool $anon = false): Video
public static function fastMake(int $owner, string $name, string $description, array $file, bool $unlisted = true, bool $anon = false): Video
{
if (OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) {
exit(VIDEOS_FRIENDLY_ERROR);
@ -269,7 +271,7 @@ class Video extends Media
return false;
}
$streams = Shell::ffprobe("-i", $path, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
$streams = Shell::ffprobe("-i", $path, "-show_streams", "-select_streams v", "-loglevel error")->execute();
$durations = [];
preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);

View file

@ -13,6 +13,8 @@ class ChandlerGroups
{
private $context;
private $groups;
private $members;
private $perms;
public function __construct()
{

View file

@ -91,7 +91,7 @@ class Clubs
return (clone $this->clubs)->count('*');
}
public function getPopularClubs(): \Traversable
public function getPopularClubs(): ?\Traversable
{
// TODO rewrite
@ -106,6 +106,8 @@ class Clubs
"subscriptions" => $entry["subscriptions"],
];
*/
trigger_error("Clubs::getPopularClubs() is currently commented out and returns null", E_USER_WARNING);
return null;
}
public function getWriteableClubs(int $id): \Traversable

View file

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\{Messages as M, User};
use Chandler\Database\DatabaseConnection as DB;
use Nette\Database\Table\ActiveRow;
class Conversations
{
private $context;
private $convos;
public function __construct()
{
$this->context = DB::i()->getContext();
$this->convos = $this->context->table("conversations");
}
private function toConversation(?ActiveRow $ar): ?M\AbstractConversation
{
if (is_null($ar)) {
return null;
} elseif ($ar->is_pm) {
return new M\PrivateConversation($ar);
} else {
return new M\Conversation($ar);
}
}
public function get(int $id): ?M\AbstractConversation
{
return $this->toConversation($this->convos->get($id));
}
public function getConversationsByUser(User $user, int $page = 1, ?int $perPage = null): \Traversable
{
$rels = $this->context->table("conversation_members")->where([
"deleted" => false,
"user" => $user->getId(),
])->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach ($rels as $rel) {
yield $this->get($rel->conversation);
}
}
public function getPrivateConversation(User $user, int $peer): M\PrivateConversation
{
;
}
}

View file

@ -152,7 +152,7 @@ class Documents
switch ($paramName) {
case "type":
if ($paramValue < 1 || $paramValue > 8) {
continue;
break;
}
$result->where("type", $paramValue);
break;

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\User;
use Nette\Database\Table\ActiveRow;
class Faves
{
private $context;
private $likes;
public function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->likes = $this->context->table("likes");
}
private function fetchLikes(User $user, string $class = 'Post')
{
$fetch = $this->likes->where([
"model" => "openvk\\Web\\Models\\Entities\\" . $class,
"origin" => $user->getRealId(),
]);
return $fetch;
}
public function fetchLikesSection(User $user, string $class = 'Post', int $page = 1, ?int $perPage = null): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$fetch = $this->fetchLikes($user, $class)->page($page, $perPage)->order("index DESC");
foreach ($fetch as $like) {
$className = "openvk\\Web\\Models\\Repositories\\" . $class . "s";
$repo = new $className();
if (!$repo) {
continue;
}
$entity = $repo->get($like->target);
yield $entity;
}
}
public function fetchLikesSectionCount(User $user, string $class = 'Post')
{
return $this->fetchLikes($user, $class)->count();
}
}

View file

@ -11,7 +11,7 @@ use openvk\Web\Models\Entities\{User, SupportAgent};
class SupportAgents
{
private $context;
private $tickets;
private $agents;
public function __construct()
{

View file

@ -57,7 +57,7 @@ class Tickets
{
$requests = $this->tickets->where(["id" => $requestId])->fetch();
if (!is_null($requests)) {
return new Req($requests);
return new Ticket($requests);
} else {
return null;
}

View file

@ -157,7 +157,7 @@ class Users
{
return (object) [
"all" => (clone $this->users)->count('*'),
"active" => (clone $this->users)->where("online > 0")->count('*'),
"active" => (clone $this->users)->where("online >= ?", time() - MONTH)->count('*'),
"online" => (clone $this->users)->where("online >= ?", time() - 900)->count('*'),
];
}

View file

@ -255,7 +255,7 @@ final class AdminPresenter extends OpenVKPresenter
{
$this->warnIfNoCommerce();
$cat;
$cat = null;
$gen = false;
if ($id !== 0) {
$cat = $this->gifts->getCat($id);

View file

@ -20,7 +20,7 @@ final class AwayPresenter extends OpenVKPresenter
header("HTTP/1.0 302 Found");
header("X-Robots-Tag: noindex, nofollow, noarchive");
header("Location: " . $this->queryParam("to"));
header("Location: " . rawurldecode($this->queryParam("to")));
exit;
}

View file

@ -34,7 +34,8 @@ final class BlobPresenter extends OpenVKPresenter
}
if (isset($_SERVER["HTTP_IF_NONE_MATCH"])) {
exit(header("HTTP/1.1 304 Not Modified"));
header("HTTP/1.1 304 Not Modified");
exit();
}
header("Content-Type: " . mime_content_type($path));

View file

@ -7,6 +7,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios};
use Nette\InvalidStateException as ISE;
final class CommentPresenter extends OpenVKPresenter
{

View file

@ -8,17 +8,17 @@ use openvk\Web\Models\Repositories\ContentSearchRepository;
final class ContentSearchPresenter extends OpenVKPresenter
{
private $repo;
protected $repo;
public function __construct(ContentSearchRepository $repo)
public function __construct(ContentSearchRepository $repository)
{
$this->repo = $repo;
$this->repo = $repository;
}
public function renderIndex(): void
{
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->template->results = $repo->find([
$this->template->results = $this->repo->find([
"query" => $this->postParam("query"),
]);
}

View file

@ -9,6 +9,7 @@ use Nette\InvalidStateException;
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification;
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts, Documents};
use Chandler\Security\Authenticator;
use Nette\InvalidStateException as ISE;
final class GroupPresenter extends OpenVKPresenter
{
@ -288,7 +289,6 @@ final class GroupPresenter extends OpenVKPresenter
(new Albums())->getClubAvatarAlbum($club)->addPhoto($photo);
} catch (ISE $ex) {
$name = $album->getName();
$this->flashFail("err", tr("error"), tr("error_when_uploading_photo"));
}
}
@ -373,6 +373,7 @@ final class GroupPresenter extends OpenVKPresenter
public function renderDeleteAvatar(int $id)
{
$this->assertUserLoggedIn();
$this->assertNoCSRF();
$this->willExecuteWriteAction();
$club = $this->clubs->get($id);

View file

@ -93,9 +93,11 @@ final class MessengerPresenter extends OpenVKPresenter
header("Content-Type: application/json");
if ($this->queryParam("act") !== "a_check") {
exit(header("HTTP/1.1 400 Bad Request"));
header("HTTP/1.1 400 Bad Request");
exit();
} elseif (!$this->queryParam("key")) {
exit(header("HTTP/1.1 403 Forbidden"));
header("HTTP/1.1 403 Forbidden");
exit();
}
$key = $this->queryParam("key");
@ -158,7 +160,8 @@ final class MessengerPresenter extends OpenVKPresenter
$sel = $this->getCorrespondent($sel);
if ($sel->getId() !== $this->user->id && !$sel->getPrivacyPermission('messages.write', $this->user->identity)) {
exit(header("HTTP/1.1 403 Forbidden"));
header("HTTP/1.1 403 Forbidden");
exit();
}
$cor = new Correspondence($this->user->identity, $sel);

View file

@ -151,77 +151,6 @@ final class NoSpamPresenter extends OpenVKPresenter
$this->assertNoCSRF();
$this->willExecuteWriteAction();
function searchByAdditionalParams(?string $table = null, ?string $where = null, ?string $ip = null, ?string $useragent = null, ?int $ts = null, ?int $te = null, $user = null)
{
$db = DatabaseConnection::i()->getContext();
if ($table && ($ip || $useragent || $ts || $te || $user)) {
$conditions = [];
if ($ip) {
$conditions[] = "`ip` REGEXP '$ip'";
}
if ($useragent) {
$conditions[] = "`useragent` REGEXP '$useragent'";
}
if ($ts) {
$conditions[] = "`ts` < $ts";
}
if ($te) {
$conditions[] = "`ts` > $te";
}
if ($user) {
$users = new Users();
$_user = $users->getByChandlerUser((new ChandlerUsers())->getById($user))
?? $users->get((int) $user)
?? $users->getByAddress($user)
?? null;
if ($_user) {
$conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'";
}
}
$whereStart = "WHERE `object_table` = '$table'";
if ($table === "profiles") {
$whereStart .= "AND `type` = 0";
}
$conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : "";
$response = [];
if ($conditions) {
$logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`");
foreach ($logs as $log) {
$log = (new Logs())->get($log->id);
$object = $log->getObject()->unwrap();
if (!$object) {
continue;
}
if ($where) {
if (str_starts_with($where, " AND")) {
$where = substr_replace($where, "", 0, strlen(" AND"));
}
$a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll();
foreach ($a as $o) {
if ($object->id == $o["id"]) {
$response[] = $object;
}
}
} else {
$response[] = $object;
}
}
}
return $response;
}
}
try {
$response = [];
$processed = 0;
@ -290,7 +219,7 @@ final class NoSpamPresenter extends OpenVKPresenter
}
if ($ip || $useragent || $ts || $te || $user) {
$rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
$rows = $this->searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
} else {
if (!$where) {
$rows = [];
@ -408,4 +337,75 @@ final class NoSpamPresenter extends OpenVKPresenter
$this->returnJson(["success" => false, "error" => $e->getMessage()]);
}
}
private function searchByAdditionalParams(?string $table = null, ?string $where = null, ?string $ip = null, ?string $useragent = null, ?int $ts = null, ?int $te = null, $user = null)
{
$db = DatabaseConnection::i()->getContext();
if ($table && ($ip || $useragent || $ts || $te || $user)) {
$conditions = [];
if ($ip) {
$conditions[] = "`ip` REGEXP '$ip'";
}
if ($useragent) {
$conditions[] = "`useragent` REGEXP '$useragent'";
}
if ($ts) {
$conditions[] = "`ts` < $ts";
}
if ($te) {
$conditions[] = "`ts` > $te";
}
if ($user) {
$users = new Users();
$_user = $users->getByChandlerUser((new ChandlerUsers())->getById($user))
?? $users->get((int) $user)
?? $users->getByAddress($user)
?? null;
if ($_user) {
$conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'";
}
}
$whereStart = "WHERE `object_table` = '$table'";
if ($table === "profiles") {
$whereStart .= "AND `type` = 0";
}
$conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : "";
$response = [];
if ($conditions) {
$logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`");
foreach ($logs as $log) {
$log = (new Logs())->get($log->id);
$object = $log->getObject()->unwrap();
if (!$object) {
continue;
}
if ($where) {
if (str_starts_with($where, " AND")) {
$where = substr_replace($where, "", 0, strlen(" AND"));
}
$a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll();
foreach ($a as $o) {
if ($object->id == $o["id"]) {
$response[] = $object;
}
}
} else {
$response[] = $object;
}
}
}
return $response;
}
}
}

View file

@ -9,6 +9,7 @@ use Chandler\MVC\SimplePresenter;
use Chandler\Session\Session;
use Chandler\Security\Authenticator;
use Latte\Engine as TemplatingEngine;
use Nette\InvalidStateException as ISE;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser, Posts};
@ -74,7 +75,6 @@ abstract class OpenVKPresenter extends SimplePresenter
protected function logInUserWithToken(): void
{
$header = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
$token;
preg_match("%Bearer (.*)$%", $header, $matches);
$token = $matches[1] ?? "";
@ -130,7 +130,7 @@ abstract class OpenVKPresenter extends SimplePresenter
}
if ($throw) {
throw new SecurityPolicyViolationException("Permission error");
throw new ISE("Permission error");
} else {
$this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
}

View file

@ -70,7 +70,7 @@ final class SearchPresenter extends OpenVKPresenter
case 'marital_status':
case 'polit_views':
if ((int) $param_value == 0) {
continue;
break;
}
$parameters[$param_name] = $param_value;
@ -96,7 +96,7 @@ final class SearchPresenter extends OpenVKPresenter
# дай бог работал этот case
case 'from_me':
if ((int) $param_value != 1) {
continue;
break;
}
$parameters['from_me'] = $this->user->id;

View file

@ -314,17 +314,20 @@ final class SupportPresenter extends OpenVKPresenter
$comment = $this->comments->get($id);
if ($this->user->id !== $comment->getTicket()->getUser()->getId()) {
exit(header("HTTP/1.1 403 Forbidden"));
header("HTTP/1.1 403 Forbidden");
exit();
}
if ($mark !== 1 && $mark !== 2) {
exit(header("HTTP/1.1 400 Bad Request"));
header("HTTP/1.1 400 Bad Request");
exit();
}
$comment->setMark($mark);
$comment->save();
exit(header("HTTP/1.1 200 OK"));
header("HTTP/1.1 200 OK");
exit();
}
public function renderQuickBanInSupport(int $id): void

View file

@ -6,6 +6,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Topic, Club, Comment, Photo, Video};
use openvk\Web\Models\Repositories\{Topics, Clubs};
use Nette\InvalidStateException as ISE;
final class TopicsPresenter extends OpenVKPresenter
{
@ -112,9 +113,6 @@ final class TopicsPresenter extends OpenVKPresenter
$video = null;
if ($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
$album = null;
if ($wall > 0 && $wall === $this->user->id) {
$album = (new Albums())->getUserWallAlbum($wallOwner);
}
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album);
}

View file

@ -9,7 +9,7 @@ use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks;
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, Audios};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios, Faves};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator;
use Chandler\Security\Authenticator;
@ -474,6 +474,7 @@ final class UserPresenter extends OpenVKPresenter
public function renderDeleteAvatar()
{
$this->assertUserLoggedIn();
$this->assertNoCSRF();
$this->willExecuteWriteAction();
$avatar = $this->user->identity->getAvatarPhoto();
@ -666,6 +667,7 @@ final class UserPresenter extends OpenVKPresenter
"menu_standardo" => "poster",
"menu_aplikoj" => "apps",
"menu_doxc" => "docs",
"menu_feva" => "fave",
];
foreach ($settings as $checkbox => $setting) {
$user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox));
@ -942,4 +944,65 @@ final class UserPresenter extends OpenVKPresenter
$this->redirect("/settings");
}
}
public function renderFave(): void
{
$this->assertUserLoggedIn();
$page = (int) ($this->queryParam("p") ?? 1);
$section = $this->queryParam("section") ?? "posts";
$display_section = "posts";
$data = null;
$count = 0;
switch ($section) {
default:
$this->notFound();
break;
case 'wall':
case 'post':
case 'posts':
$data = (new Faves())->fetchLikesSection($this->user->identity, 'Post', $page);
$count = (new Faves())->fetchLikesSectionCount($this->user->identity, 'Post');
$display_section = "posts";
break;
case 'comment':
case 'comments':
$data = (new Faves())->fetchLikesSection($this->user->identity, 'Comment', $page);
$count = (new Faves())->fetchLikesSectionCount($this->user->identity, 'Comment');
$display_section = "comments";
break;
case 'photo':
case 'photos':
$data = (new Faves())->fetchLikesSection($this->user->identity, 'Photo', $page);
$count = (new Faves())->fetchLikesSectionCount($this->user->identity, 'Photo');
$display_section = "photos";
break;
case 'video':
case 'videos':
$data = (new Faves())->fetchLikesSection($this->user->identity, 'Video', $page);
$count = (new Faves())->fetchLikesSectionCount($this->user->identity, 'Video');
$display_section = "videos";
break;
}
$this->template->data = iterator_to_array($data);
$this->template->count = $count;
$this->template->page = $page;
$this->template->perPage = OPENVK_DEFAULT_PER_PAGE;
$this->template->section = $display_section;
$this->template->paginatorConf = (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($this->template->data),
"perPage" => $this->template->perPage,
"atBottom" => false,
"tidy" => true,
'pageCount' => ceil($count / $this->template->perPage),
];
$this->template->extendedPaginatorConf = clone $this->template->paginatorConf;
$this->template->extendedPaginatorConf->space = 11;
$this->template->paginatorConf->atTop = true;
}
}

View file

@ -273,7 +273,7 @@ final class VKAPIPresenter extends OpenVKPresenter
}
}
define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false);
define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100");
try {
$res = $handler->{$method}(...$params);

View file

@ -265,8 +265,11 @@ final class WallPresenter extends OpenVKPresenter
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$wallOwner = ($wall > 0 ? (new Users())->get($wall) : (new Clubs())->get($wall * -1))
?? $this->flashFail("err", tr("failed_to_publish_post"), tr("error_4"));
$wallOwner = ($wall > 0 ? (new Users())->get($wall) : (new Clubs())->get($wall * -1));
if ($wallOwner === null) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("error_4"));
}
if ($wallOwner->isBanned()) {
$this->flashFail("err", tr("error"), tr("forbidden"));
@ -568,8 +571,11 @@ final class WallPresenter extends OpenVKPresenter
}
$user = $this->user->id;
$wallOwner = ($wall > 0 ? (new Users())->get($wall) : (new Clubs())->get($wall * -1))
?? $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4"));
$wallOwner = ($wall > 0 ? (new Users())->get($wall) : (new Clubs())->get($wall * -1));
if ($wallOwner === null) {
$this->flashFail("err", tr("failed_to_delete_post"), tr("error_4"));
}
if ($wallOwner->isBanned()) {
$this->flashFail("err", tr("error"), tr("forbidden"));

View file

@ -198,10 +198,11 @@
</a>
<a href="/settings" class="link">{_my_settings}</a>
{if $thisUser->getLeftMenuItemStatus('docs') || $thisUser->getLeftMenuItemStatus('apps')}
{if $thisUser->getLeftMenuItemStatus('docs') || $thisUser->getLeftMenuItemStatus('apps') || $thisUser->getLeftMenuItemStatus('fave')}
<div class="menu_divider"></div>
<a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_apps}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('docs')" href="/docs" class="link">{_my_documents}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('fave')" href="/fave" class="link">{_bookmarks_tab}</a>
{/if}
{var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}

View file

@ -0,0 +1,80 @@
{extends "../@layout.xml"}
{block title}{_bookmarks_tab}{/block}
{block header}
{_bookmarks_tab}
{/block}
{block wrap}
<div class="wrap2">
<div class="wrap1">
<div class="page_wrap">
<div class='summaryBar summaryBarFlex padding'>
<div class='summary'>
<b>{tr("faves", $count)} {*tr("showing_x_y", $page, $count)*}</b>
</div>
{include "../components/paginator.xml", conf => $paginatorConf}
</div>
<div class='page_wrap_content' id='search_page'>
<div n:class='page_wrap_content_main, scroll_container, ($section == "photos" && $count > 0) ? album-flex'>
<style>
.scroll_node:first-of-type .comment {
border-top: unset;
padding: 0px;
}
.scroll_node:last-of-type .post {
border-bottom: unset;
}
.content_page_error {
min-height: 400px;
}
</style>
{if $count > 0}
{foreach $data as $dat}
{if $section == "posts"}
<div class="scroll_node">
{include "../components/post.xml", post => $dat, commentSection => true}
</div>
{elseif $section == "comments"}
<div class="scroll_node">
{include "../components/comment.xml", comment => $dat, correctLink => true, no_reply_button => true}
</div>
{elseif $section == "photos"}
<div class="album-photo scroll_node" onclick="OpenMiniature(event, {$dat->getURLBySizeId('larger')}, null, {$dat->getPrettyId()}, null)">
<a href="/photo{$dat->getPrettyId()}">
<img class="album-photo--image" src="{$dat->getURLBySizeId('tinier')}" alt="{$dat->getDescription()}" loading="lazy" />
</a>
</div>
{elseif $section == "videos"}
<div class="scroll_node">
{include "../components/video.xml", video => $dat}
</div>
{/if}
{/foreach}
{else}
{include "../components/content_error.xml", description => tr("faves_".$section."_empty_tip")}
{/if}
</div>
<div class='page_wrap_content_options verticalGrayTabsWrapper'>
<div class="page_wrap_content_options_list verticalGrayTabs with_padding">
<a n:attr="id => $section === 'posts' ? 'used'" href="/fave?section=posts">{_s_posts}</a>
<a n:attr="id => $section === 'comments' ? 'used'" href="/fave?section=comments">{_s_comments}</a>
<a n:attr="id => $section === 'photos' ? 'used'" href="/fave?section=photos">{_s_photos}</a>
<a n:attr="id => $section === 'videos' ? 'used'" href="/fave?section=videos">{_s_videos}</a>
</div>
</div>
</div>
<div n:if='$paginatorConf->pageCount > 1' class='page_content_paginator_bottom'>
{include "../components/paginator.xml", conf => $extendedPaginatorConf}
</div>
</div>
</div>
</div>
</div>
{/block}

View file

@ -696,6 +696,17 @@
<span class="nobold">{_my_documents}</span>
</td>
</tr>
<tr>
<td width="120" valign="top" align="right" align="right">
<input
n:attr="checked => $user->getLeftMenuItemStatus('fave')"
type="checkbox"
name="menu_feva" />
</td>
<td>
<span class="nobold">{_bookmarks_tab}</span>
</td>
</tr>
<tr n:if="sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0">
<td width="120" valign="top" align="right" align="right">
<input

View file

@ -60,8 +60,9 @@
<a href="javascript:reportComment({$comment->getId()})">{_report}</a>
{/if}
<div style="float: right; font-size: .7rem;">
{var $isLiked = $comment->hasLikeFrom($thisUser)}
<a class="post-like-button" href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}" data-likes='{$likesCount}' data-id="1_{$comment->getPrettyId()}" data-type='comment'>
<div class="heart" style="{if $comment->hasLikeFrom($thisUser)}opacity: 1;{else}opacity: 0.4;{/if}"></div>
<div class="heart" n:attr="id => $isLiked ? liked" style="{if $isLiked}opacity: 1;{else}opacity: 0.4;{/if}"></div>
<span class="likeCnt">{if $likesCount > 0}{$likesCount}{/if}</span>
</a>
</div>

View file

@ -128,6 +128,6 @@ class Themepack
throw new Exceptions\IncompatibleThemeException("Theme is built for newer OVK (themeEngine" . $manifest->openvk_version . ")");
}
return new static($manifest->id, $manifest->version, (bool) ($manifest->inherit_master ?? true), (bool) ($manifest->override_templates ?? false), (bool) ($manifest->enabled ?? true), (object) $manifest->metadata);
return new Themepack($manifest->id, $manifest->version, (bool) ($manifest->inherit_master ?? true), (bool) ($manifest->override_templates ?? false), (bool) ($manifest->enabled ?? true), (object) $manifest->metadata);
}
}

View file

@ -88,25 +88,6 @@ class Themepacks implements \ArrayAccess
/* /ArrayAccess */
public function install(string $archivePath): bool
{
if (!file_exists($archivePath)) {
return false;
}
$tmpDir = mkdir(tempnam(OPENVK_ROOT . "/tmp/themepack_artifacts/", "themex_"));
try {
$archive = new \CabArchive($archivePath);
$archive->extract($tmpDir);
return $this->installUnpacked($tmpDir);
} catch (\Exception $e) {
return false;
} finally {
rmdir($tmpDir);
}
}
public function uninstall(string $id): bool
{
if (!isset($loadedThemepacks[$id])) {

View file

@ -78,7 +78,7 @@ class Bitmask
} elseif (gettype($key) === "int") {
$this->setByOffset($key, $data);
} else {
throw new TypeError("Key must be either offset (int) or a string index");
throw new \TypeError("Key must be either offset (int) or a string index");
}
return $this;
@ -89,7 +89,7 @@ class Bitmask
if (gettype($key) === "string") {
$key = $this->getOffsetByKey($key);
} elseif (gettype($key) !== "int") {
throw new TypeError("Key must be either offset (int) or a string index");
throw new \TypeError("Key must be either offset (int) or a string index");
}
return $this->length === 1 ? $this->getBoolByOffset($key) : $this->getNumberByOffset($key);

View file

@ -66,6 +66,7 @@ class DateTime
case static::RELATIVE_FORMAT_LOWER:
return $this->zmdate();
case static::RELATIVE_FORMAT_SHORT:
default:
return "";
}
}

View file

@ -188,9 +188,9 @@ class Makima
$tries = [];
$firstLine;
$secondLine;
$thirdLine;
$firstLine = null;
$secondLine = null;
$thirdLine = null;
# Try one line:
$tries[$firstLine = $count] = [$this->calculateMultiThumbsHeight($ratiosCropped, $maxWidth, $marginWidth)];
@ -234,7 +234,7 @@ class Makima
}
}
if (!$optimalConfiguration || $confDigff < $optimalDifference) {
if (!$optimalConfiguration || $confDiff < $optimalDifference) {
$optimalConfiguration = $config;
$optimalDifference = $confDiff;
}

View file

@ -54,5 +54,6 @@ services:
- openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Models\Repositories\ChandlerGroups
- openvk\Web\Models\Repositories\Documents
- openvk\Web\Models\Repositories\Faves
- openvk\Web\Presenters\MaintenancePresenter
- openvk\Web\Presenters\NoSpamPresenter

View file

@ -413,6 +413,8 @@ routes:
handler: "InternalAPI->getPostTemplate"
- url: "/tour"
handler: "About->tour"
- url: "/fave"
handler: "User->fave"
- url: "/{?shortCode}"
handler: "UnknownTextRouteStrategy->delegate"
placeholders:

View file

@ -478,8 +478,8 @@ return (function () {
define('YEAR', 365 * DAY);
define("nullptr", null);
define("OPENVK_DEFAULT_INSTANCE_NAME", "OpenVK", false);
define("OPENVK_VERSION", "Altair Preview ($ver)", false);
define("OPENVK_DEFAULT_PER_PAGE", 10, false);
define("__OPENVK_ERROR_CLOCK_IN_FUTURE", "Server clock error: FK1200-DTF", false);
define("OPENVK_DEFAULT_INSTANCE_NAME", "OpenVK");
define("OPENVK_VERSION", "Altair Preview ($ver)");
define("OPENVK_DEFAULT_PER_PAGE", 10);
define("__OPENVK_ERROR_CLOCK_IN_FUTURE", "Server clock error: FK1200-DTF");
});

10
chandler_loader.php Normal file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace openvk;
$_SERVER["HTTP_ACCEPT_LANGUAGE"] = false;
$bootstrap = require(__DIR__ . "/../../../chandler/Bootstrap.php");
$bootstrap->ignite(true);

View file

@ -1,7 +1,8 @@
{
"scripts": {
"fix": "php-cs-fixer fix",
"lint": "php-cs-fixer fix --dry-run --diff --verbose"
"lint": "php-cs-fixer fix --dry-run --diff --verbose",
"analyse": "phpstan analyse --memory-limit 1G"
},
"require": {
"php": "~7.3||~8.1",
@ -28,6 +29,7 @@
},
"minimum-stability": "beta",
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.68"
"friendsofphp/php-cs-fixer": "^3.68",
"phpstan/phpstan": "^2.1"
}
}

60
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b92d2ddd207f394a31c429c65d1785a7",
"content-hash": "fe88a04383a75cc5c6591abac3128201",
"packages": [
{
"name": "al/emoji-detector",
@ -3092,6 +3092,64 @@
],
"time": "2025-01-30T17:00:50+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "7d08f569e582ade182a375c366cbd896eccadd3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/7d08f569e582ade182a375c366cbd896eccadd3a",
"reference": "7d08f569e582ade182a375c366cbd896eccadd3a",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2025-01-21T14:54:06+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",

View file

@ -641,6 +641,8 @@
"my_feed" = "My Feed";
"my_feedback" = "My Feedback";
"my_settings" = "My Settings";
"bookmarks" = "Bookmarks";
"bookmarks_tab" = "Saved";
"bug_tracker" = "Bug Tracker";
"menu_settings" = "Settings";
@ -2113,6 +2115,7 @@
"s_apps" = "Applications";
"s_posts" = "Posts";
"s_comments" = "Comments";
"s_photos" = "Photos";
"s_videos" = "Videos";
"s_audios" = "Music";
"s_audios_playlists" = "Playlists";
@ -2385,3 +2388,17 @@
"select_doc" = "Attach document";
"no_documents" = "No documents found";
"go_to_my_documents" = "Go to own documents";
/* Fave */
"faves" = "Bookmarks";
"faves_empty_tip" = "There will be your liked content.";
"faves_posts_empty_tip" = "There will be posts liked by you.";
"faves_comments_empty_tip" = "There will be comments liked by you.";
"faves_photos_empty_tip" = "There will be photos liked by you.";
"faves_videos_empty_tip" = "There will be videos liked by you.";
"faves_zero" = "No bookmarks";
"faves_one" = "One bookmark";
"faves_few" = "$1 bookmarks";
"faves_many" = "$1 bookmarks";
"faves_other" = "$1 bookmarks";

View file

@ -624,6 +624,8 @@
"my_feed" = "Мои Новости";
"my_feedback" = "Мои Ответы";
"my_settings" = "Мои Настройки";
"bookmarks" = "Закладки";
"bookmarks_tab" = "Избранное";
"bug_tracker" = "Баг-трекер";
"menu_settings" = "Настройки";
@ -2008,6 +2010,7 @@
"s_apps" = "Приложения";
"s_posts" = "Записи";
"s_comments" = "Комментарии";
"s_photos" = "Фотографии";
"s_videos" = "Видео";
"s_audios" = "Аудио";
"s_audios_playlists" = "Плейлисты";
@ -2280,3 +2283,17 @@
"select_doc" = "Выбор документа";
"no_documents" = "Документов нет";
"go_to_my_documents" = "Перейти к своим документам";
/* Fave */
"faves" = "Закладки";
"faves_empty_tip" = "Здесь будет отображаться понравившийся Вам контент...";
"faves_posts_empty_tip" = "Здесь будут отображаться понравившиеся Вам записи.";
"faves_comments_empty_tip" = "Здесь будут отображаться понравившиеся Вам комментарии.";
"faves_photos_empty_tip" = "Здесь будут отображаться понравившиеся Вам фотографии.";
"faves_videos_empty_tip" = "Здесь будут отображаться понравившиеся Вам видео.";
"faves_zero" = "Ни одной закладки"; /* на украинском можно как ни одной вподобайки */
"faves_one" = "Одна закладка";
"faves_few" = "$1 закладки";
"faves_many" = "$1 закладок";
"faves_other" = "$1 закладок";

View file

@ -7,9 +7,7 @@ namespace openvk;
use Symfony\Component\Console\Application;
$_SERVER["HTTP_ACCEPT_LANGUAGE"] = false;
$bootstrap = require(__DIR__ . "/../../../chandler/Bootstrap.php");
$bootstrap->ignite(true);
require(__DIR__ . "/chandler_loader.php");
$application = new Application();
$application->add(new CLI\RebuildImagesCommand());

14
phpstan.neon Normal file
View file

@ -0,0 +1,14 @@
parameters:
level: 0
paths:
- CLI
- ServiceAPI
- VKAPI
- Web
- bootstrap.php
- openvkctl
- chandler_loader.php
bootstrapFiles:
- chandler_loader.php
- bootstrap.php