diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..e110b27a0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,22 @@
+vendor
+openvk.yml
+chandler.yml
+update.pid
+update.pid.old
+Web/static/js/node_modules
+
+tmp/*
+!tmp/api-storage/.gitkeep
+!tmp/themepack_artifacts/.gitkeep
+themepacks/*
+!themepacks/.gitkeep
+!themepacks/openvk_modern
+!themepacks/midnight
+storage/*
+!storage/.gitkeep
+
+.idea
+.php-cs-fixer.cache
+
+tests/
+.github/
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index ddc730d65..4829afba1 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-custom: "https://openvk.su/donate"
\ No newline at end of file
+custom: "https://openvk.org/donate"
diff --git a/.github/workflows/analyse.yaml b/.github/workflows/analyse.yaml
new file mode 100644
index 000000000..d528c8eb4
--- /dev/null
+++ b/.github/workflows/analyse.yaml
@@ -0,0 +1,36 @@
+name: Static analysis
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ phpstan:
+ name: PHPStan
+ runs-on: ubuntu-latest
+
+ # '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@v7
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - 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
diff --git a/.github/workflows/build-base.yaml b/.github/workflows/build-base.yaml
index 0a98503f7..043f088b4 100644
--- a/.github/workflows/build-base.yaml
+++ b/.github/workflows/build-base.yaml
@@ -2,57 +2,70 @@ name: Build base images
on:
schedule:
- - cron: '0 0 * * *'
+ - cron: "0 0 * * *"
+ workflow_dispatch:
env:
BASE_IMAGE_NAME: php
- BASE_IMAGE_VERSION: "8.1"
+ BASE_IMAGE_VERSION: "8.2"
jobs:
build-cli:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
with:
lfs: false
-
+
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
-
+ uses: docker/setup-qemu-action@v4
+
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v4
+
+ - name: Change repository string to lowercase
+ id: repositorystring
+ uses: Entepotenz/change-string-case-action-min-dependencies@v1.2.0
+ with:
+ string: ${{ github.repository }}
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build cli image
run: |
- IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-cli
+ IMAGE_NAME=ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-cli
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-cli.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION
-
+
build-apache:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
with:
lfs: false
-
+
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
-
+ uses: docker/setup-qemu-action@v4
+
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v4
+
+ - name: Change repository string to lowercase
+ id: repositorystring
+ uses: Entepotenz/change-string-case-action-min-dependencies@v1.2.0
+ with:
+ string: ${{ github.repository }}
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
-
+
- name: Build apache image
run: |
- IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-apache
+ IMAGE_NAME=ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-apache
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-apache.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index d4645520d..8a88bcd36 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,64 +1,65 @@
name: Build images
-on:
- push:
- # Publish `master` as Docker `latest` image.
- branches:
- - master
-
- # Publish `v1.2.3` tags as releases.
- tags:
- - v*
+on: [push, pull_request]
env:
- BASE_IMAGE_NAME: openvk
- DB_IMAGE_NAME: mariadb
- EVENT_IMAGE_NAME: mariadb
- DB_VERSION: "10.9"
+ BASE_IMAGE_NAME: openvk
jobs:
- build:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- arch: ['x86_64']
+ buildbase:
+ name: Build base images
+
+ runs-on: ubuntu-latest
+
+ # 'push' runs on inner branches, 'pull_request' will run only on outer PRs
+ if: >
+ github.repository_owner == 'OpenVK'
+ && (github.event_name == 'push'
+ || (github.event_name == 'pull_request'
+ && github.event.pull_request.head.repo.full_name != github.repository))
- if: github.event_name == 'push'
- steps:
- - uses: actions/checkout@v3
- with:
- lfs: false
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
-
- - name: Set up Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@v2
+ steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v4
- - name: Log into registry
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v4
+ with:
+ platforms: linux/amd64,linux/arm64
- - name: Build base image
- run: |
- IMAGE_ID=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME
- IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
- VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
- [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
- [ "$VERSION" == "master" ] && VERSION=latest
- echo IMAGE_ID=$IMAGE_ID
- echo VERSION=$VERSION
+ - name: Change repository string to lowercase
+ id: repositorystring
+ uses: Entepotenz/change-string-case-action-min-dependencies@v1.2.0
+ with:
+ string: ${{ github.repository }}
- docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_ID:$VERSION . --push -f install/automated/docker/openvk.Dockerfile --build-arg GITREPO=${{ github.repository }}
-
- - name: Build MariaDB primary image
- run: |
- IMAGE_NAME=ghcr.io/${{ github.repository }}/$DB_IMAGE_NAME:$DB_VERSION-primary
+ - name: Base image meta
+ id: basemeta
+ uses: docker/metadata-action@v6
+ with:
+ images: |
+ ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/${{env.BASE_IMAGE_NAME}}
+ labels: |
+ org.opencontainers.image.documentation=https://github.com/OpenVK/openvk/blob/master/install/automated/docker/Readme.md
+ tags: |
+ type=sha
+ type=ref,event=branch
+ type=ref,event=pr
+ type=ref,event=tag
+ type=raw,value=latest,enable={{is_default_branch}}
- docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-primary.Dockerfile --build-arg VERSION=$DB_VERSION
-
- - name: Build MariaDB event image
- run: |
- IMAGE_NAME=ghcr.io/${{ github.repository }}/$EVENT_IMAGE_NAME:$DB_VERSION-eventdb
+ - name: Log into registry
+ if: github.event_name != 'pull_request'
+ run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-eventdb.Dockerfile --build-arg VERSION=$DB_VERSION
\ No newline at end of file
+ - name: Build base image
+ uses: docker/build-push-action@v7
+ with:
+ platforms: linux/amd64,linux/arm64
+ file: install/automated/docker/openvk.Dockerfile
+ tags: ${{ steps.basemeta.outputs.tags }}
+ labels: ${{ steps.basemeta.outputs.labels }}
+ push: ${{ github.event_name != 'pull_request' }}
+ build-args: |
+ GITREPO=${{ steps.repositorystring.outputs.lowercase }}
diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml
index 7d4049dc0..99dc469df 100644
--- a/.github/workflows/codeberg-mirror.yml
+++ b/.github/workflows/codeberg-mirror.yml
@@ -5,8 +5,9 @@ on: push
jobs:
to_codeberg:
runs-on: ubuntu-latest
+ if: github.repository_owner == 'OpenVK'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
new file mode 100644
index 000000000..f695ba568
--- /dev/null
+++ b/.github/workflows/lint.yaml
@@ -0,0 +1,35 @@
+name: Lint
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ # '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)
+
+ permissions:
+ contents: read
+ steps:
+ - name: Code Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.2"
+ extensions: gd, zip, intl, yaml, pdo_mysql, imagick
+ tools: composer:v2
+ coverage: none
+
+ - name: Install dependencies
+ run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
+
+ - name: PHP CS Fixer
+ run: vendor/bin/php-cs-fixer fix --dry-run --diff
diff --git a/.github/workflows/test-screenshots.yaml b/.github/workflows/test-screenshots.yaml
new file mode 100644
index 000000000..3b0f03ed3
--- /dev/null
+++ b/.github/workflows/test-screenshots.yaml
@@ -0,0 +1,36 @@
+name: Screenshot Tests
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ # '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:
+ - uses: actions/checkout@v7
+
+ - name: Build test images
+ run: docker compose -f tests/docker-compose.test.yml build
+
+ - name: Run full test stack
+ run: >
+ docker compose -f tests/docker-compose.test.yml up
+ --abort-on-container-exit
+ --exit-code-from playwright
+ --no-build
+
+ - name: Upload test results on failure
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: playwright-results
+ path: tests/e2e/test-results/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 78f621844..0de60a76a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ update.pid.old
Web/static/js/node_modules
tmp/*
-!tmp/api-storage
+!tmp/api-storage/.gitkeep
!tmp/themepack_artifacts/.gitkeep
themepacks/*
!themepacks/.gitkeep
@@ -15,4 +15,7 @@ themepacks/*
storage/*
!storage/.gitkeep
-.idea
\ No newline at end of file
+data/knowledgebase/*
+
+.idea
+.php-cs-fixer.cache
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 000000000..6e227eb19
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,15 @@
+in(__DIR__)
+ ->name('openvkctl')
+;
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@PER-CS2.0' => true,
+ '@PHP82Migration' => true,
+ ])
+ ->setFinder($finder)
+ ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
+;
diff --git a/CLI/CleanupPendingUploadsCommand.php b/CLI/CleanupPendingUploadsCommand.php
new file mode 100644
index 000000000..8772c015f
--- /dev/null
+++ b/CLI/CleanupPendingUploadsCommand.php
@@ -0,0 +1,100 @@
+setDescription("Cleanup pending photo uploads older than specified time")
+ ->addOption(
+ "max-age",
+ "a",
+ InputOption::VALUE_OPTIONAL,
+ "Maximum age in hours (default: 24)",
+ 24
+ )
+ ->addOption(
+ "dry-run",
+ "d",
+ InputOption::VALUE_NONE,
+ "Show what would be deleted without actually deleting"
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $maxAge = (int) $input->getOption("max-age");
+ $dryRun = $input->getOption("dry-run");
+
+ $photoFolder = __DIR__ . "/../tmp/api-storage/photos";
+
+ if (!is_dir($photoFolder)) {
+ $output->writeln("Photo upload directory not found: {$photoFolder}");
+ return Command::FAILURE;
+ }
+
+ $output->writeln("Scanning for pending uploads older than {$maxAge} hours...");
+
+ $cutoffTime = time() - ($maxAge * 3600);
+ $deletedCount = 0;
+ $totalSize = 0;
+
+ $files = glob($photoFolder . "/*_*.oct");
+
+ foreach ($files as $file) {
+ $fileTime = filemtime($file);
+
+ if ($fileTime < $cutoffTime) {
+ $fileSize = filesize($file);
+ $totalSize += $fileSize;
+
+ if ($dryRun) {
+ $age = round((time() - $fileTime) / 3600, 1);
+ $output->writeln("Would delete: " . basename($file) . " (age: {$age}h, size: " . $this->formatBytes($fileSize) . ")");
+ } else {
+ if (unlink($file)) {
+ $deletedCount++;
+ $output->writeln("Deleted: " . basename($file) . "");
+ } else {
+ $output->writeln("Failed to delete: " . basename($file) . "");
+ }
+ }
+ }
+ }
+
+ if ($dryRun) {
+ $output->writeln("Dry run completed. Would delete {$deletedCount} files (" . $this->formatBytes($totalSize) . ")");
+ } else {
+ $output->writeln("Cleanup completed. Deleted {$deletedCount} files (" . $this->formatBytes($totalSize) . ")");
+ }
+
+ return Command::SUCCESS;
+ }
+
+ private function formatBytes(int $bytes): string
+ {
+ $units = ['B', 'KB', 'MB', 'GB'];
+ $bytes = max($bytes, 0);
+ $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
+ $pow = min($pow, count($units) - 1);
+
+ $bytes /= pow(1024, $pow);
+
+ return round($bytes, 2) . ' ' . $units[$pow];
+ }
+}
diff --git a/CLI/FetchToncoinTransactions.php b/CLI/FetchToncoinTransactions.php
index b4e66dcb7..deb89eee1 100755
--- a/CLI/FetchToncoinTransactions.php
+++ b/CLI/FetchToncoinTransactions.php
@@ -1,103 +1,117 @@
-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;
- }
-}
\ No newline at end of file
+getContext();
+ if (array_any(
+ $ctx->getStructure()->getTables(),
+ fn($value) => $value["name"] === "cryptotransactions"
+ )) {
+ $this->transactions = $ctx->table("cryptotransactions");
+ }
+ }
+
+ 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 haven't enabled the TON support in your config file yet.");
+
+ return Command::FAILURE;
+ }
+
+ if (!isset($this->transactions)) {
+ $header->writeln("The 'cryptotransactions' table can't be found in the database.");
+
+ return Command::FAILURE;
+ }
+
+ $testnetSubdomain = OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["testnet"] ? "testnet." : "";
+ $url = "https://" . $testnetSubdomain . "toncenter.com/api/v3/transactions?";
+
+ $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([
+ "account" => OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["address"],
+ "limit" => 100,
+ "sort" => 'desc',
+ "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["transactions"] as $transfer) {
+ preg_match('/' . OPENVK_ROOT_CONF["openvk"]["preferences"]["ton"]["regex"] . '/', $transfer["in_msg"]["message_content"]["decoded"]["comment"], $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["hash"],
+ "lt" => $transfer["lt"],
+ ]);
+ }
+ }
+ }
+
+ $header->writeln("Processing finished :3");
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/CLI/GenerateUsersCommand.php b/CLI/GenerateUsersCommand.php
new file mode 100644
index 000000000..152be9f5d
--- /dev/null
+++ b/CLI/GenerateUsersCommand.php
@@ -0,0 +1,116 @@
+setDescription("Generate test user accounts for development")
+ ->addOption(
+ "count",
+ "c",
+ InputOption::VALUE_REQUIRED,
+ "Number of users to create",
+ 1
+ );
+ }
+
+ //Duplicate the logic from the Web/Presenters/AuthPresenter.php class
+ /**
+ * @throws RandomException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $faker = FakerFactory::create("en_US");
+ $count = (int) $input->getOption("count");
+
+ if ($count < 1) {
+ $io->error("Count must be at least 1.");
+
+ return Command::FAILURE;
+ }
+
+ $created = [];
+
+ for ($i = 1; $i <= $count; $i++) {
+ $email = $faker->unique()->safeEmail();
+ $password = $this->generatePassword();
+
+ try {
+ $user = new User();
+ $user->setFirst_Name($faker->firstName());
+ $user->setLast_Name($faker->lastName());
+ $user->setSex(0);
+ $user->setEmail($email);
+ $user->setSince(date("Y-m-d H:i:s"));
+ $user->setRegistering_Ip("127.0.0.1");
+ $user->setBirthday($faker->dateTimeBetween("-60 years", "-18 years")->getTimestamp());
+ $user->setActivated(1);
+ } catch (InvalidUserNameException $ex) {
+ $io->error("Failed to set name for user #{$i}: " . $ex->getMessage());
+
+ return Command::FAILURE;
+ }
+
+ $chUser = ChandlerUser::create($email, $password);
+ if (!$chUser) {
+ $io->error("Failed to create Chandler user for {$email}");
+
+ return Command::FAILURE;
+ }
+
+ $user->setUser($chUser->getId());
+ $user->save(false);
+
+ $created[] = [
+ "id" => $user->getId(),
+ "email" => $email,
+ "password" => $password,
+ "url" => "/id" . $user->getId(),
+ ];
+ }
+
+ $io->success("Created " . count($created) . " user(s).");
+
+ $rows = array_map(static fn(array $u): array => [
+ $u["id"],
+ $u["email"],
+ $u["password"],
+ $u["url"],
+ ], $created);
+
+ $io->table(["ID", "Email", "Password", "Profile URL"], $rows);
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * @throws RandomException
+ */
+ private function generatePassword(): string
+ {
+ do {
+ $password = "OvK" . bin2hex(random_bytes(4)) . "A1";
+ } while (!Validator::i()->passwordStrong($password));
+
+ return $password;
+ }
+
+}
diff --git a/CLI/README.md b/CLI/README.md
new file mode 100644
index 000000000..845798744
--- /dev/null
+++ b/CLI/README.md
@@ -0,0 +1,62 @@
+# OpenVK CLI Commands
+
+This directory contains command-line utilities for OpenVK management.
+
+## Available Commands
+
+### cleanup-pending-uploads
+Automatically removes pending photo uploads older than the specified time.
+
+**Usage:**
+```bash
+# Clean up uploads older than 24 hours (default)
+php openvkctl cleanup-pending-uploads
+
+# Clean up uploads older than 1 hour
+php openvkctl cleanup-pending-uploads --max-age=1
+
+# Dry run to see what would be deleted
+php openvkctl cleanup-pending-uploads --dry-run
+```
+
+**Options:**
+- `--max-age`, `-a`: Maximum age in hours (default: 24)
+- `--dry-run`, `-d`: Show what would be deleted without actually deleting
+
+**Cron Setup:**
+To automatically clean up pending uploads daily, add to your crontab:
+```bash
+# Clean up pending uploads daily at 2 AM
+0 2 * * * cd /path/to/openvk && php openvkctl cleanup-pending-uploads
+```
+
+### build-images
+Rebuilds photo thumbnails and image sizes.
+
+### fetch-toncoin-transactions
+Fetches Toncoin transactions for payment processing.
+
+### upgrade
+Performs database upgrades and migrations.
+
+## Available Commands for local development
+
+### generate-users
+Creates test user accounts for local development.
+
+**Usage:**
+```bash
+# Create one user (default)
+php openvkctl generate-users
+
+# Create 20 users
+php openvkctl generate-users --count=20
+
+# Create 20 users
+php openvkctl generate-users -c 20
+```
+
+**Options:**
+- `--count`, `-c`: Number of users to create (default: 1)
+
+The command prints a table with profile ID, email, password, and profile URL for each created user.
diff --git a/CLI/RebuildImagesCommand.php b/CLI/RebuildImagesCommand.php
index 937978b17..6340743c6 100644
--- a/CLI/RebuildImagesCommand.php
+++ b/CLI/RebuildImagesCommand.php
@@ -1,5 +1,9 @@
-images = DatabaseConnection::i()->getContext()->table("photos");
+ $ctx = DatabaseConnection::i()->getContext();
+ if (in_array("photos", $ctx->getStructure()->getTables())) {
+ $this->images = $ctx->table("photos");
+ }
parent::__construct();
}
@@ -40,8 +47,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
]);
$filter = ["deleted" => false];
- if($input->getOption("upgrade-only"))
- $filter["sizes"] = NULL;
+ if ($input->getOption("upgrade-only")) {
+ $filter["sizes"] = null;
+ }
$selection = $this->images->select("id")->where($filter);
$totalPics = $selection->count();
@@ -52,24 +60,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$errors = 0;
$count = 0;
- $avgTime = NULL;
+ $avgTime = null;
$begin = new \DateTimeImmutable("now");
- foreach($selection as $idHolder) {
+ foreach ($selection as $idHolder) {
$start = microtime(true);
try {
- $photo = (new Photos)->get($idHolder->id);
+ $photo = (new Photos())->get($idHolder->id);
$photo->getSizes(true, true);
$photo->getDimensions();
- } catch(ImageException $ex) {
+ } catch (ImageException $ex) {
$errors++;
}
$timeConsumed = microtime(true) - $start;
- if(!$avgTime)
+ if (!$avgTime) {
$avgTime = $timeConsumed;
- else
+ } else {
$avgTime = ($avgTime + $timeConsumed) / 2;
+ }
$eta = $begin->getTimestamp() + ceil($totalPics * $avgTime);
$int = (new \DateTimeImmutable("now"))->diff(new \DateTimeImmutable("@$eta"));
@@ -83,4 +92,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return Command::SUCCESS;
}
-}
\ No newline at end of file
+}
diff --git a/CLI/UpgradeCommand.php b/CLI/UpgradeCommand.php
new file mode 100644
index 000000000..92abf161f
--- /dev/null
+++ b/CLI/UpgradeCommand.php
@@ -0,0 +1,364 @@
+db = DatabaseConnection::i()->getConnection();
+ $this->eventDb = eventdb()->getConnection();
+
+ parent::__construct();
+ }
+
+ protected function configure(): void
+ {
+ $this->setDescription("Upgrade OpenVK installation")
+ ->setHelp("This command upgrades database schema after OpenVK was updated")
+ ->addOption(
+ "quick",
+ "Q",
+ InputOption::VALUE_NEGATABLE,
+ "Don't display warning before migrating database",
+ false
+ )
+ ->addOption(
+ "repair",
+ "R",
+ InputOption::VALUE_NEGATABLE,
+ "Attempt to repair database schema if tables are missing",
+ false
+ )
+ ->addOption(
+ "oneshot",
+ "O",
+ InputOption::VALUE_NONE,
+ "Only execute one operation"
+ )
+ ->addArgument(
+ "chandler",
+ InputArgument::OPTIONAL,
+ "Location of Chandler installation"
+ );
+ }
+
+ protected function checkDatabaseReadiness(bool &$chandlerOk, bool &$ovkOk, bool &$eventOk, bool &$migrationsOk): void
+ {
+ $tables = $this->db->query("SHOW TABLES")->fetchAll();
+ $tables = array_map(fn($x) => strtoupper($x->offsetGet(0)), $tables);
+
+ $missingTables = array_diff($this->chandlerTables, $tables);
+ if (sizeof($missingTables) == 0) {
+ $chandlerOk = true;
+ } elseif (sizeof($missingTables) == sizeof($this->chandlerTables)) {
+ $chandlerOk = null;
+ } else {
+ $chandlerOk = false;
+ }
+
+ if (is_null($this->eventDb)) {
+ $eventOk = false;
+ } elseif (is_null($this->eventDb->query("SHOW TABLES LIKE \"notifications\"")->fetch())) {
+ $eventOk = null;
+ } else {
+ $eventOk = true;
+ }
+
+ $ovkOk = in_array("PROFILES", $tables);
+ $migrationsOk = in_array("OVK_UPGRADE_HISTORY", $tables);
+ }
+
+ protected function executeSqlScript(
+ int $errCode,
+ string $script,
+ SymfonyStyle $io,
+ bool $transaction = false,
+ bool $eventDb = false
+ ): int {
+ $pdo = ($eventDb ? $this->eventDb : $this->db)->getPdo();
+
+ $res = false;
+ try {
+ if ($transaction) {
+ $res = $pdo->beginTransaction();
+ }
+
+ $res = $pdo->exec($script);
+
+ if ($transaction) {
+ $res = $pdo->commit();
+ }
+ } catch (\PDOException $e) {
+ }
+
+ if ($res === false) {
+ goto error;
+ }
+
+ return 0;
+
+ error:
+ $io->getErrorStyle()->error([
+ "Failed to execute SQL statement:",
+ implode("\t", $pdo->errorInfo()),
+ ]);
+
+ return $errCode;
+ }
+
+ protected function getNextLevel(bool $eventDb = false): int
+ {
+ $db = $eventDb ? $this->eventDb : $this->db;
+ $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history";
+ $record = $db->query("SELECT level FROM $tbl ORDER BY level DESC LIMIT 1");
+ if (!$record->getRowCount()) {
+ return 0;
+ }
+
+ return $record->fetchField() + 1;
+ }
+
+ protected function getMigrationFiles(bool $eventDb = false): array
+ {
+ $files = [];
+ $root = dirname(__DIR__ . "/../install/init-static-db.sql");
+ $dir = $eventDb ? "sqls/eventdb" : "sqls";
+
+ foreach (glob("$root/$dir/*.sql") as $file) {
+ $files[(int) basename($file)] = basename($file);
+ }
+
+ ksort($files);
+
+ return $files;
+ }
+
+ protected function installChandler(InputInterface $input, SymfonyStyle $io, bool $drop = false): int
+ {
+ $chandlerLocation = $input->getArgument("chandler") ?? (__DIR__ . "/../../../../");
+ $chandlerConfigLocation = "$chandlerLocation/chandler.yml";
+
+ if (!file_exists($chandlerConfigLocation)) {
+ $err = ["Could not find chandler location. Perhaps your config is too unique?"];
+ if (!$input->getOption("chandler")) {
+ $err[] = "Specify absolute path to your chandler installation using the --chandler option.";
+ }
+
+ $io->getErrorStyle()->error($err);
+
+ return 21;
+ }
+
+ if ($drop) {
+ $bar = new ProgressBar($io, sizeof($this->chandlerTables));
+ $io->writeln("Dropping chandler tables...");
+
+ foreach ($bar->iterate($this->chandlerTables) as $table) {
+ $this->db->query("DROP TABLE IF EXISTS $table;");
+ }
+
+ $io->newLine();
+ }
+
+ $installFile = file_get_contents("$chandlerLocation/install/init-db.sql");
+
+ return $this->executeSqlScript(22, $installFile, $io);
+ }
+
+ protected function initSchema(SymfonyStyle $io): int
+ {
+ $installFile = file_get_contents(__DIR__ . "/../install/init-static-db.sql");
+
+ return $this->executeSqlScript(31, $installFile, $io);
+ }
+
+ protected function initEventSchema(SymfonyStyle $io): int
+ {
+ $installFile = file_get_contents(__DIR__ . "/../install/init-event-db.sql");
+
+ return $this->executeSqlScript(31, $installFile, $io, true, true);
+ }
+
+ protected function initUpgradeLog(SymfonyStyle $io): int
+ {
+ $installFile = file_get_contents(__DIR__ . "/../install/init-migration-table.sql");
+ $rc = $this->executeSqlScript(31, $installFile, $io);
+ if ($rc) {
+ var_dump($rc);
+ return $rc;
+ }
+
+ $installFile = file_get_contents(__DIR__ . "/../install/init-migration-table-event.sql");
+
+ return $this->executeSqlScript(32, $installFile, $io, false, true);
+ }
+
+ protected function runMigrations(SymfonyStyle $io, bool $eventDb, bool $oneshot): int
+ {
+ $dir = $eventDb ? "sqls/eventdb" : "sqls";
+ $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history";
+ $db = $eventDb ? $this->eventDb : $this->db;
+ $nextLevel = $this->getNextLevel($eventDb);
+ $migrations = array_filter(
+ $this->getMigrationFiles($eventDb),
+ fn($id) => $id >= $nextLevel,
+ ARRAY_FILTER_USE_KEY
+ );
+
+ if (!sizeof($migrations)) {
+ return 24;
+ }
+
+ $uname = addslashes(`whoami`);
+ $bar = new ProgressBar($io, sizeof($migrations));
+ $bar->setFormat("very_verbose");
+
+ foreach ($bar->iterate($migrations) as $num => $migration) {
+ $script = file_get_contents(__DIR__ . "/../install/$dir/$migration");
+ $res = $this->executeSqlScript(100 + $num, $script, $io, true, $eventDb);
+ if ($res != 0) {
+ $io->getErrorStyle()->error("Error while executing migration №$num");
+
+ return $res;
+ }
+
+ $t = time();
+ $db->query("INSERT INTO $tbl VALUES ($num, $t, \"$uname\");");
+
+ if ($oneshot) {
+ return 5;
+ }
+ }
+
+ $io->newLine();
+
+ return 0;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $oneShotMode = $input->getOption("oneshot");
+ $io = new SymfonyStyle($input, $output);
+
+ if (!$input->getOption("quick")) {
+ $io->writeln("Do full backup of the database before executing this command!");
+ $io->writeln("Command will resume execution after 5 seconds.");
+ $io->writeln("You can skip this warning with --quick option.");
+ sleep(5);
+ }
+
+ $migrationsOk = false;
+ $chandlerOk = false;
+ $eventOk = false;
+ $ovkOk = false;
+
+ $this->checkDatabaseReadiness($chandlerOk, $ovkOk, $eventOk, $migrationsOk);
+
+ $res = -1;
+ if ($chandlerOk === null) {
+ $io->writeln("Chandler schema not detected, attempting to install...");
+
+ $res = $this->installChandler($input, $io);
+ } elseif ($chandlerOk === false) {
+ if ($input->getOption("repair")) {
+ $io->warning("Chandler schema detected but is broken, attempting to repair...");
+
+ $res = $this->installChandler($input, $io, true);
+ } else {
+ $io->writeln("Chandler schema detected but is broken");
+ $io->writeln("Run command with --repair to repair (PERMISSIONS WILL BE LOST)");
+
+ return 1;
+ }
+ }
+
+ if ($res > 0) {
+ return $res;
+ } elseif ($res == 0 && $oneShotMode) {
+ return 5;
+ }
+
+ if (!$ovkOk) {
+ $io->writeln("Initializing OpenVK schema...");
+ $res = $this->initSchema($io);
+ if ($res > 0) {
+ return $res;
+ } elseif ($oneShotMode) {
+ return 5;
+ }
+ }
+
+ if (!$migrationsOk) {
+ $io->writeln("Initializing upgrade log...");
+ $res = $this->initUpgradeLog($io);
+ if ($res > 0) {
+ return $res;
+ } elseif ($oneShotMode) {
+ return 5;
+ }
+ }
+
+ if ($eventOk !== false) {
+ if ($eventOk === null) {
+ $io->writeln("Initializing event database...");
+ $res = $this->initEventSchema($io);
+ if ($res > 0) {
+ return $res;
+ } elseif ($oneShotMode) {
+ return 5;
+ }
+ }
+
+ $io->writeln("Upgrading event database...");
+ $res = $this->runMigrations($io, true, $oneShotMode);
+ if ($res == 24) {
+ $output->writeln("Event database already up to date.");
+ } elseif ($res > 0) {
+ return $res;
+ }
+ }
+
+ $io->writeln("Upgrading database...");
+ $res = $this->runMigrations($io, false, $oneShotMode);
+
+ if (!$res) {
+ $io->success("Database has been upgraded!");
+
+ return 0;
+ } elseif ($res != 24) {
+ return $res;
+ }
+
+ $io->writeln("Database up to date. Nothing left to do.");
+
+ return 0;
+ }
+}
diff --git a/CODE_STYLE.md b/CODE_STYLE.md
deleted file mode 100644
index 85f5ab743..000000000
--- a/CODE_STYLE.md
+++ /dev/null
@@ -1,277 +0,0 @@
-# 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 `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:
-
-
-+ 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";
-}
-```
diff --git a/DBEntity.updated.php b/DBEntity.updated.php
deleted file mode 100644
index 4c039b54e..000000000
--- a/DBEntity.updated.php
+++ /dev/null
@@ -1,140 +0,0 @@
-getTable()->getName();
- if($_table !== $this->tableName)
- throw new ISE("Invalid data supplied for model: table $_table is not compatible with table" . $this->tableName);
-
- $this->record = $row;
- }
-
- function __call(string $fName, array $args)
- {
- if(substr($fName, 0, 3) === "set") {
- $field = mb_strtolower(substr($fName, 3));
- $this->stateChanges($field, $args[0]);
- } else {
- throw new \Error("Call to undefined method " . get_class($this) . "::$fName");
- }
- }
-
- private function getTable(): Selection
- {
- return DatabaseConnection::i()->getContext()->table($this->tableName);
- }
-
- protected function getRecord(): ?ActiveRow
- {
- return $this->record;
- }
-
- protected function stateChanges(string $column, $value): void
- {
- if(!is_null($this->record))
- $t = $this->record->{$column}; #Test if column exists
-
- $this->changes[$column] = $value;
- }
-
- function getId()
- {
- return $this->getRecord()->id;
- }
-
- function isDeleted(): bool
- {
- return (bool) $this->getRecord()->deleted;
- }
-
- function unwrap(): object
- {
- return (object) $this->getRecord()->toArray();
- }
-
- function delete(bool $softly = true): void
- {
- $user = CurrentUser::i()->getUser();
- $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId();
-
- if(is_null($this->record))
- throw new ISE("Can't delete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?");
-
- (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 2, $this->record->toArray(), $this->changes);
-
- if($softly) {
- $this->record = $this->getTable()->where("id", $this->record->id)->update(["deleted" => true]);
- } else {
- $this->record->delete();
- $this->deleted = true;
- }
- }
-
- function undelete(): void
- {
- if(is_null($this->record))
- throw new ISE("Can't undelete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?");
-
- $user = CurrentUser::i()->getUser();
- $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId();
-
- (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 3, $this->record->toArray(), ["deleted" => false]);
-
- $this->getTable()->where("id", $this->record->id)->update(["deleted" => false]);
- }
-
- function save(?bool $log = true): void
- {
- if ($log) {
- $user = CurrentUser::i();
- $user_id = is_null($user) ? (int)OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getUser()->getId();
- }
-
- if(is_null($this->record)) {
- $this->record = $this->getTable()->insert($this->changes);
-
- if ($log && $this->getTable()->getName() !== "logs") {
- (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 0, $this->record->toArray(), $this->changes);
- }
- } else {
- if ($log && $this->getTable()->getName() !== "logs") {
- (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 1, $this->record->toArray(), $this->changes);
- }
-
- if ($this->deleted) {
- $this->record = $this->getTable()->insert((array)$this->record);
- } else {
- $this->getTable()->get($this->record->id)->update($this->changes);
- $this->record = $this->getTable()->get($this->record->id);
- }
- }
-
- $this->changes = [];
- }
-
- function getTableName(): string
- {
- return $this->getTable()->getName();
- }
-
- use \Nette\SmartObject;
-}
diff --git a/Email/assets/res/pictures/lock.jpeg b/Email/assets/res/pictures/lock.jpeg
deleted file mode 100644
index 9afc6e0b1..000000000
Binary files a/Email/assets/res/pictures/lock.jpeg and /dev/null differ
diff --git a/Email/change-email.eml.latte b/Email/change-email.eml.latte
index 6cff8c11e..30f4c4a90 100644
--- a/Email/change-email.eml.latte
+++ b/Email/change-email.eml.latte
@@ -46,17 +46,14 @@
-
+
-
-
-
-
+
-
+
-
+
-
+
Здравствуйте, {$name}! Вы вероятно изменили свой адрес электронной почты в OpenVK. Чтобы изменение вступило в силу, необходимо подтвердить ваш новый Email.
-
+
-
+
-
+
-
+
Если кнопка не работает, вы можете попробовать скопировать и вставить эту ссылку в адресную строку вашего веб-обозревателя:
-
+
-
+
Обратите внимание на то, что эту ссылку нельзя:
-
+
- - Передавать другим людям (даже друзьям, питомцам, соседам, любимым девушкам)
+ - Передавать другим людям (даже друзьям, питомцам, соседям, любимым девушкам)
- Использовать, если прошло более двух дней с её генерации
-
+
-
+
-
+
С уважением, овк-тян.
-
+
-
+
-
+
-
+
Вы получили это письмо так как кто-то или вы изменили адрес электронной почты. Это не рассылка и от неё нельзя отписаться. Если вы всё равно хотите перестать получать подобные письма, деактивируйте ваш аккаунт.
@@ -201,4 +198,4 @@
|
|