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: -![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"; -} -``` 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 @@ - + diff --git a/Email/password-reset.eml.latte b/Email/password-reset.eml.latte index aa6911c64..a8253e231 100644 --- a/Email/password-reset.eml.latte +++ b/Email/password-reset.eml.latte @@ -46,17 +46,14 @@
-
- -
- +
@@ -64,9 +61,9 @@
- +
- +
@@ -74,11 +71,11 @@
- +

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

- +
@@ -86,7 +83,7 @@
- + @@ -102,7 +99,7 @@
@@ -94,7 +91,7 @@
- Подтвердить Email! + Подтвердить Email!
- +
@@ -110,30 +107,30 @@
- +

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

- +
- - http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key} + + https://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key}
- +

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

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

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

- +
@@ -164,9 +161,9 @@
- +
- +
@@ -174,7 +171,7 @@
- +

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

- + \ No newline at end of file diff --git a/Email/hello.eml.latte b/Email/hello.eml.latte index 48fd562ee..1bcd1e032 100644 --- a/Email/hello.eml.latte +++ b/Email/hello.eml.latte @@ -12,7 +12,7 @@
- Добро пожаловать в OpenVK! Приятного времяприпровождения, надеюсь вам понравится.

Если появились вопросы, касаемые нашего сайта, пишите сюда + Добро пожаловать в OpenVK! Приятного времяприпровождения, надеюсь вам понравится.

Если появились вопросы, касаемые нашего сайта, пишите сюда
- +
-
- -
- +
@@ -64,9 +61,9 @@
- +
- +
@@ -74,11 +71,11 @@
- +

Здравствуйте, {$name}! Вы вероятно забыли пароль от аккаунта OpenVK? Мы идём к Вам на помощь!

- +
@@ -86,7 +83,7 @@
- + @@ -102,7 +99,7 @@
@@ -94,7 +91,7 @@
- Сбросить пароль! + Сбросить пароль!
- +
@@ -110,30 +107,30 @@
- +

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

- +
- - http://{$_SERVER['HTTP_HOST']}/restore?act=finish&key={$key} + + https://{$_SERVER['HTTP_HOST']}/restore?act=finish&key={$key}
- +

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

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

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

- +
@@ -164,9 +161,9 @@
- +
- +
@@ -174,7 +171,7 @@
- +

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

- + \ No newline at end of file diff --git a/Email/verify-email.eml.latte b/Email/verify-email.eml.latte index 40711f945..7f1f36836 100755 --- a/Email/verify-email.eml.latte +++ b/Email/verify-email.eml.latte @@ -46,17 +46,14 @@
- +
-
- -
- +
@@ -64,9 +61,9 @@
- +
- +
@@ -74,11 +71,11 @@
- +

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

- +
@@ -86,7 +83,7 @@
- + @@ -102,7 +99,7 @@
@@ -94,7 +91,7 @@
- Подтвердить Email! + Подтвердить Email!
- +
@@ -110,30 +107,30 @@
- +

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

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

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

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

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

- +
@@ -164,9 +161,9 @@
- +
- +
@@ -174,7 +171,7 @@
- +

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

- + \ No newline at end of file diff --git a/README.md b/README.md index 934ff0b7b..2d290b780 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# openvkOpenVK +# openvkOpenVK _[Русский](README_RU.md)_ -**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VKontakte. Code provided here is not stable yet. +**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VKontakte. Code provided here is not stable yet. -VKontakte belongs to Pavel Durov and VK Group. +> [!WARNING] +> **OpenVK it is fan project, not affiliated in any way with VKontakte and it's company VK LLC. Below is the same message in Russian.** \ +> **OpenVK является любительской разработкой и никак не связан с ВКонтакте и компанией ООО "ВК".** -To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OpenVK account for this). +To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.org/support?act=new) (you will need an OpenVK account for this). ## When's the release? @@ -26,15 +28,21 @@ However, OVK makes use of Chandler Application Server. This software requires ex If you want, you can add your instance to the list above so that people can register there. -### Installation procedure +### System requirements + +Here is our minimum hardware recommendation: -1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) +* **CPU:** Any dual-core 1GHz+ CPU or more powerful +* **RAM:** At least 2GB RAM (we recommend 6GB or 8GB for OpenVK with Redis) +* **Minimum database space:** 10GB -* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. +### Installation procedure + +1. Install PHP 8.2 or later, web-server, Composer, Node.js, NPM and [Chandler](https://github.com/openvk/chandler) 2. Install MySQL-compatible database. -* We recommend using Percona Server, but any MySQL-compatible server should work too. +* We recommend using MariaDB or Percona Server, but any MySQL-compatible server should work too. * Server should be compatible with at least MySQL 5.6, MySQL 8.0+ is recommended. * Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs. @@ -52,13 +60,13 @@ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/ext ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ``` -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` +5. You need to set up 2 databases: one for main data (it is be configured in `chandler.yml`), and another one for events (it is configured in `openvk.yml`) +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 `npm install` +10. Set `openvk` as your root app in `chandler.yml` +11. Run database migrations by executing `./openvkctl upgrade` Once you are done, you can login as a system administrator on the network itself (no registration required): @@ -66,28 +74,62 @@ Once you are done, you can login as a system administrator on the network itself * **Password**: `admin` * It is recommended to change the password of the built-in account or disable it. -💡Confused? Full installation walkthrough is available [here](https://docs.openvk.uk/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)). +💡 Confused? Full installation walkthrough is available [here](https://docs.openvk.org/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)). + +### Auto-install script + +You can also use auto-install script for FreeBSD 15: + +```shell +pkg install wget +wget https://github.com/OpenVK/openvk/raw/refs/heads/master/install/automated/freebsd-15/install +chmod +x install +./install +``` + +### Real-time notifs + +You can install Redis to take advantage of real-time notifications (if you enabled Event DB in config). + +1. Install Redis from your beloved package manager in your OS +2. Set `notificationsBroker` under `credentials` to `true` + +It should work out of box. If not, tweak Redis and OpenVK config settings + +> [!WARNING] +> Kafka in OpenVK was been deprecated since [this commit](https://github.com/OpenVK/openvk/commit/e99cdd1b08002dbfbd1aaef2cbc52ccbe34026c6) and no longer used in OpenVK codebase. If you see any mention of Kafka in source code, config or documentation, you should know that this will not work at all. ### Looking for Docker or Kubernetes deployment? See `install/automated/docker/README.md` and `install/automated/kubernetes/README.md` for Docker and Kubernetes deployment instructions. -### If my website uses OpenVK, should I release it's sources? +### If my website uses OpenVK, should I release its sources? It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you are planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). +## Localization + +Want to translate our project to your native language? You can try either: + +* [Weblate](https://hosted.weblate.org/engage/openvk/) (simple way) +* Send Pull Request to us (hard way) + +Localization is located in "locales" repository. List of languages is maintained in list.yml file, and the languages itself are in iOS String format. + ## Where can I get assistance? You may reach out to us via: -* [Bug Tracker](https://github.com/openvk/openvk/projects/1) -* [Ticketing System](https://openvk.su/support?act=new) -* Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. -* [Reddit](https://www.reddit.com/r/openvk/) +* [Bug Tracker](https://github.com/OpenVK/openvk/issues) * [GitHub Discussions](https://github.com/openvk/openvk/discussions) +* [Ticketing System](https://openvk.org/support?act=new) +* [Discord Server](https://discord.gg/8TDpTeRw5k) +* Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. * Matrix Chat: #openvk:matrix.org -**Attention**: bug tracker, board, Telegram and Matrix chat are public places, ticketing system is being served by volunteers. If you need to report something that should not be immediately disclosed to general public (for instance, a vulnerability), please contact us directly via this email: **openvk [at] tutanota [dot] com** +**Attention**: bug tracker, board, Telegram, Discord and Matrix chat are public places, ticketing system is being served by volunteers. If you need to report something that should not be immediately disclosed to general public (for instance, a vulnerability), please contact us directly via this email: **contact [at] openvk [dot] org** Get it on Codeberg + +[![Translation status](https://hosted.weblate.org/widget/openvk/openvk/svg-badge.svg)](https://hosted.weblate.org/engage/openvk/) \ No newline at end of file diff --git a/README_RU.md b/README_RU.md index 7de91c398..8d81efbeb 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,12 +1,13 @@ -# openvkOpenVK +# openvkOpenVK _[English](README.md)_ **OpenVK** — это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным. -ВКонтакте принадлежит Павлу Дурову и VK Group. +> [!WARNING] +> **OpenVK является любительской разработкой и никак не связан с ВКонтакте и компанией ООО "ВК"** -Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OpenVK). +Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.org/support?act=new) (для этого вам понадобится учетная запись OpenVK). ## Когда выйдет релизная версия? @@ -28,13 +29,11 @@ _[English](README.md)_ ### Процедура установки -1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) - -* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. +1. Установите PHP 8.2, веб-сервер, Composer, Node.js, NPM и [Chandler](https://github.com/openvk/chandler) 2. Установите MySQL-совместимую базу данных. -* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать. +* Мы рекомендуем использовать MariaDB или Percona Server, но любая MySQL-совместимая база данных должна работать. * Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+. * Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно. @@ -52,13 +51,13 @@ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/ext ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ``` -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` +5. Вам необходимо иметь 2 базы данных: одна для основных данных (указывается в `chandler.yml`), другая для событий (указывается в `openvk.yml`) +6. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры под свои нужды +7. Запустите `composer install` в директории OpenVK +8. Запустите `composer install` в директории commitcaptcha +9. Перейдите в `Web/static/js` и выполните `npm install` +10. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` +11. Запустите миграции базы данных, выполнив `./openvkctl upgrade` После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): @@ -66,27 +65,48 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions * **Пароль**: `admin` * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её. -💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.uk/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). +💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.org/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). + +### Уведомления в реальном времени + +Вы можете установить Redis для уведомлений в реальном времени (если вы, конечно, включили Event DB в конфиге). + +1. Установите Redis в вашу операционную систему +2. Поставьте `notificationsBroker` внутри `credentials` на `true` + +Оно должно заработать сразу же из коробки. Если нет, попробуйте отредактировать настройки Redis и OpenVK. -# Установка в Docker/Kubernetes +> [!WARNING] +> Kafka в OpenVK устарела начиная с [этого коммита](https://github.com/OpenVK/openvk/commit/e99cdd1b08002dbfbd1aaef2cbc52ccbe34026c6) и больше не используется в кодовой базе OpenVK. Если вы наткнулись на любое упоминание Kafka в исходном коде, в конфиге или в документации, мы должны вас оповестить о том, что оно не будет работать и информация о ней устарела. Совсем. + +### Установка в Docker/Kubernetes Подробные иструкции можно найти в `install/automated/docker/README.md` и `install/automated/kubernetes/README.md` соответственно. ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.). +## Локализация + +Хотите перевести наш проект на свой родной или национальный язык? Есть два способа это сделать: + +* Через [Weblate](https://hosted.weblate.org/engage/openvk/) (простой путь) +* Отправить нам Pull Request (сложный путь) + +Локаль лежит в папке "locales". Список языков хранится в файл list.yml, сами языки в формате iOS String. + ## Где я могу получить помощь? Вы можете связаться с нами через: -* [Баг-трекер](https://github.com/openvk/openvk/projects/1) -* [Помощь в OVK](https://openvk.su/support?act=new) -* Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала. -* [Reddit](https://www.reddit.com/r/openvk/) +* [Баг-трекер](https://github.com/OpenVK/openvk/issues) * [GitHub Discussions](https://github.com/openvk/openvk/discussions) +* [Помощь в OVK](https://openvk.org/support?act=new) +* [Discord-сервер](https://discord.gg/8TDpTeRw5k) +* Telegram-чат: Перейдите на [наш канал](https://t.me/openvkenglish) и откройте обсуждение в меню нашего канала. (_внимание: помощь доступна только на английском языке_) * Чат в Matrix: #ovk:matrix.org -**Внимание**: баг-трекер, форум, Telegram- и Matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собачка] tutanota [точка] com**. +**Внимание**: баг-трекер, форум, Telegram-, Discord- и Matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **contact [собачка] openvk [точка] org**. Get it on Codeberg diff --git a/ServiceAPI/Apps.php b/ServiceAPI/Apps.php index 6504c23e9..16f596360 100644 --- a/ServiceAPI/Apps.php +++ b/ServiceAPI/Apps.php @@ -1,26 +1,32 @@ -user = $user; - $this->apps = new Applications; + $this->apps = new Applications(); } - - function getUserInfo(callable $resolve, callable $reject): void + + public 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, @@ -32,61 +38,84 @@ function getUserInfo(callable $resolve, callable $reject): void "ava" => $this->user->getAvatarUrl(), ]); } - - function updatePermission(int $app, string $perm, string $state, callable $resolve, callable $reject): void + + public function updatePermission(int $app, string $perm, string $state, callable $resolve, callable $reject): void { $app = $this->apps->get($app); - if(!$app || !$app->isEnabled()) { + if (!$app || !$app->isEnabled()) { $reject("No application with this id found"); return; } - - if(!$app->setPermission($this->user, $perm, $state == "yes")) + + 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 + + public function pay(int $appId, float $amount, callable $resolve, callable $reject): void { $app = $this->apps->get($appId); - if(!$app || !$app->isEnabled()) { + if (!$app || !$app->isEnabled()) { $reject("No application with this id found"); return; } - if($amount < 0) { + if ($amount < 0) { $reject(552, "Payment amount is invalid"); return; } - + $coinsLeft = $this->user->getCoins() - $amount; - if($coinsLeft < 0) { + 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 + + public function withdrawFunds(int $appId, callable $resolve, callable $reject): void { $app = $this->apps->get($appId); - if(!$app) { + if (!$app) { $reject("No application with this id found"); return; - } else if($app->getOwner()->getId() != $this->user->getId()) { + } elseif ($app->getOwner()->getId() != $this->user->getId()) { $reject("You don't have rights to edit this app"); return; } - + $coins = $app->getBalance(); $app->withdrawCoins(); $resolve($coins); } -} \ No newline at end of file + + public function getRegularToken(string $clientName, bool $acceptsStale, callable $resolve, callable $reject): void + { + $token = null; + $stale = true; + if ($acceptsStale) { + $token = (new APITokens())->getStaleByUser($this->user->getId(), $clientName); + } + + if (is_null($token)) { + $stale = false; + $token = new APIToken(); + $token->setUser($this->user); + $token->setPlatform($clientName ?? (new WhichBrowser\Parser(getallheaders()))->toString()); + $token->save(); + } + + $resolve([ + 'is_stale' => $stale, + 'token' => $token->getFormattedToken(), + ]); + } +} diff --git a/ServiceAPI/Groups.php b/ServiceAPI/Groups.php index 9eed0e8d1..b9662e607 100644 --- a/ServiceAPI/Groups.php +++ b/ServiceAPI/Groups.php @@ -1,5 +1,9 @@ -user = $user; - $this->groups = new Clubs; + $this->groups = new Clubs(); } - - function getWriteableClubs(callable $resolve, callable $reject) + + public function getWriteableClubs(callable $resolve, callable $reject) { $clubs = []; $wclubs = $this->groups->getWriteableClubs($this->user->getId()); $count = $this->groups->getWriteableClubsCount($this->user->getId()); - if(!$count) { + if (!$count) { $reject("You don't have any groups with write access"); return; } - foreach($wclubs as $club) { + foreach ($wclubs as $club) { $clubs[] = [ "name" => $club->getName(), "id" => $club->getId(), - "avatar" => $club->getAvatarUrl() # если в овк когда-нибудь появится крутой список с аватарками, то можно использовать это поле + "avatar" => $club->getAvatarUrl(), # если в овк когда-нибудь появится крутой список с аватарками, то можно использовать это поле ]; } diff --git a/ServiceAPI/Handler.php b/ServiceAPI/Handler.php index 7b19d6b6b..a17112988 100644 --- a/ServiceAPI/Handler.php +++ b/ServiceAPI/Handler.php @@ -1,8 +1,12 @@ -user = $user; } - function resolve(int $id, callable $resolve, callable $reject): void + public function resolve(int $id, callable $resolve, callable $reject): void { - if($id > 0) { - $user = (new Users)->get($id); - if(!$user) { + if ($id > 0) { + $user = (new Users())->get($id); + if (!$user) { $reject("Not found"); return; } @@ -32,8 +36,8 @@ function resolve(int $id, callable $resolve, callable $reject): void return; } - $club = (new Clubs)->get(abs($id)); - if(!$club) { + $club = (new Clubs())->get(abs($id)); + if (!$club) { $reject("Not found"); return; } diff --git a/ServiceAPI/Notes.php b/ServiceAPI/Notes.php index 456cfaee5..67e78dfdd 100644 --- a/ServiceAPI/Notes.php +++ b/ServiceAPI/Notes.php @@ -1,6 +1,7 @@ user = $user; - $this->notes = new NoteRepo; + $this->notes = new NoteRepo(); } - - function getNote(int $noteId, callable $resolve, callable $reject): void + + public function getNote(int $noteId, callable $resolve, callable $reject): void { $note = $this->notes->get($noteId); - if(!$note || $note->isDeleted()) + if (!$note || $note->isDeleted()) { $reject(83, "Note is gone"); - + } + $noteOwner = $note->getOwner(); assert($noteOwner instanceof User); - if(!$noteOwner->getPrivacyPermission("notes.read", $this->user)) + if (!$noteOwner->getPrivacyPermission("notes.read", $this->user)) { $reject(160, "You don't have permission to access this note"); - + } + + if (!$note->canBeViewedBy($this->user)) { + $reject(15, "Access to note denied"); + } + $resolve([ "title" => $note->getName(), "link" => "/note" . $note->getPrettyId(), diff --git a/ServiceAPI/Notifications.php b/ServiceAPI/Notifications.php index 12f0ed8ee..5a11a2d06 100644 --- a/ServiceAPI/Notifications.php +++ b/ServiceAPI/Notifications.php @@ -1,83 +1,87 @@ -user = $user; - $this->notifs = new N; - } - - function ack(callable $resolve, callable $reject): void - { - $this->user->updateNotificationOffset(); - $this->user->save(); - $resolve("OK"); + $this->notifs = new N(); } - - function fetch(callable $resolve, callable $reject): void + + public function fetch(callable $resolve, callable $reject): void { - $kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"]; - if(!$kafkaConf["enable"]) { + $notifConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"]; + if (!($notifConf["enable"] ?? false)) { $reject(1999, "Disabled"); return; } - - $kafkaConf = $kafkaConf["kafka"]; - $conf = new RDKConf(); - $conf->set("metadata.broker.list", $kafkaConf["addr"] . ":" . $kafkaConf["port"]); - $conf->set("group.id", "UserFetch-" . $this->user->getId()); # Чтобы уведы приходили только на разные устройства одного чебупелика - $conf->set("auto.offset.reset", "latest"); - - set_time_limit(30); - $consumer = new KafkaConsumer($conf); - $consumer->subscribe([ $kafkaConf["topic"] ]); - - while(true) { - $message = $consumer->consume(30*1000); - switch ($message->err) { - case RD_KAFKA_RESP_ERR_NO_ERROR: - $descriptor = $message->payload; - [,$user,] = explode(",", $descriptor); - if(((int) $user) === $this->user->getId()) { - $data = (object) []; - $notification = $this->notifs->fromDescriptor($descriptor, $data); - if(!$notification) { - $reject(1982, "Server Error"); - return; - } - - $tplDir = __DIR__ . "/../Web/Presenters/templates/components/notifications/"; - $tplId = "$tplDir$data->actionCode/_$data->originModelType" . "_" . $data->targetModelType . "_.xml"; - $latte = new TemplatingEngine; - $latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates"); - $latte->addFilter("translate", fn($trId) => tr($trId)); - $resolve([ - "title" => tr("notif_" . $data->actionCode . "_" . $data->originModelType . "_" . $data->targetModelType), - "body" => trim(preg_replace('%(\s){2,}%', "$1", $latte->renderToString($tplId, ["notification" => $notification]))), - "ava" => $notification->getModel(1)->getAvatarUrl(), - "priority" => 1, - ]); - return; - } - - break; - case RD_KAFKA_RESP_ERR__TIMED_OUT: - case RD_KAFKA_RESP_ERR__PARTITION_EOF: - $reject(1983, "Nothing to report"); - break 2; - default: - $reject(1981, "Kafka Error: " . $message->errstr()); - break 2; + + try { + $broker = NotificationBroker::i(); + if (!$broker->isConnected()) { + $reject(1998, "Redis connection error"); + return; + }; + + $userId = $this->user->getId(); + + $session = Session::i(); + $lastId = $session->get("notifs_cursor"); + + if (!$lastId) { + $lastId = '0'; } + + $events = $broker->getNew($userId, $lastId); + + if (empty($events)) { + $reject(1983, "Nothing to report"); + return; + } + + $event = end($events); + $newCursor = $event['id']; + $payload = (object) $event['data']['data']; + + $notification = $this->notifs->fromArray((array) $payload); + + if (!$notification) { + $reject(1982, "Server Error"); + return; + } + + $tplDir = __DIR__ . "/../Web/Presenters/templates/components/notifications/"; + $tplId = "$tplDir$payload->actionCode/_$payload->originModelType" . "_" . $payload->targetModelType . "_.latte"; + $latte = new TemplatingEngine(); + $latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates"); + $latte->addExtension(new \Latte\Essential\TranslatorExtension(tr(...))); + + $session->set("notifs_cursor", $newCursor); + + $userModel = $notification->getModel(1); + + $resolve([ + "title" => tr("notif_" . $payload->actionCode . "_" . $payload->originModelType . "_" . $payload->targetModelType), + "body" => trim(preg_replace('%(\s){2,}%', "$1", $latte->renderToString($tplId, ["notification" => $notification]))), + "ava" => $userModel->getAvatarUrl(), + "priority" => 1, + ]); + + } catch (\Exception $e) { + $reject(1981, "Redis Error: " . $e->getMessage()); } } } diff --git a/ServiceAPI/Photos.php b/ServiceAPI/Photos.php deleted file mode 100644 index 16d602f22..000000000 --- a/ServiceAPI/Photos.php +++ /dev/null @@ -1,92 +0,0 @@ -user = $user; - $this->photos = new PhotosRepo; - } - - function getPhotos(int $page = 1, int $album = 0, callable $resolve, callable $reject) - { - if($album == 0) { - $photos = $this->photos->getEveryUserPhoto($this->user, $page, 24); - $count = $this->photos->getUserPhotosCount($this->user); - } else { - $album = (new Albums)->get($album); - - if(!$album || $album->isDeleted()) - $reject(55, "Invalid ."); - - if($album->getOwner() instanceof User) { - if($album->getOwner()->getId() != $this->user->getId()) - $reject(555, "Access to album denied"); - } else { - if(!$album->getOwner()->canBeModifiedBy($this->user)) - $reject(555, "Access to album denied"); - } - - $photos = $album->getPhotos($page, 24); - $count = $album->size(); - } - - $arr = [ - "count" => $count, - "items" => [], - ]; - - foreach($photos as $photo) { - $res = json_decode(json_encode($photo->toVkApiStruct()), true); - - $arr["items"][] = $res; - } - - $resolve($arr); - } - - function getAlbums(int $club, callable $resolve, callable $reject) - { - $albumsRepo = (new Albums); - - $count = $albumsRepo->getUserAlbumsCount($this->user); - $albums = $albumsRepo->getUserAlbums($this->user, 1, $count); - - $arr = [ - "count" => $count, - "items" => [], - ]; - - foreach($albums as $album) { - $res = ["id" => $album->getId(), "name" => $album->getName()]; - - $arr["items"][] = $res; - } - - if($club > 0) { - $cluber = (new Clubs)->get($club); - - if(!$cluber || !$cluber->canBeModifiedBy($this->user)) - $reject(1337, "Invalid (club), or you can't modify him"); - - $clubCount = (new Albums)->getClubAlbumsCount($cluber); - $clubAlbums = (new Albums)->getClubAlbums($cluber, 1, $clubCount); - - foreach($clubAlbums as $albumr) { - $res = ["id" => $albumr->getId(), "name" => $albumr->getName()]; - - $arr["items"][] = $res; - } - - $arr["count"] = $arr["count"] + $clubCount; - } - - $resolve($arr); - } -} diff --git a/ServiceAPI/Polls.php b/ServiceAPI/Polls.php index 9d3e2e7f0..885cc9e1a 100644 --- a/ServiceAPI/Polls.php +++ b/ServiceAPI/Polls.php @@ -1,5 +1,9 @@ -user = $user; - $this->polls = new PollRepo; + $this->polls = new PollRepo(); } - + private function getPollHtml(int $poll): string { return Router::i()->execute("/poll$poll", "SAPI"); } - - function vote(int $pollId, string $options, callable $resolve, callable $reject): void + + public function vote(int $pollId, string $options, callable $resolve, callable $reject): void { $poll = $this->polls->get($pollId); - if(!$poll) { + if (!$poll) { $reject("Poll not found"); return; } - + try { $options = explode(",", $options); $poll->vote($this->user, $options); - } catch(AlreadyVotedException $ex) { + } catch (AlreadyVotedException $ex) { $reject("Poll state changed: user has already voted."); return; - } catch(PollLockedException $ex) { + } catch (PollLockedException $ex) { $reject("Poll state changed: poll has ended."); return; - } catch(InvalidOptionException $ex) { + } catch (InvalidOptionException $ex) { $reject("Foreign options passed."); return; - } catch(UnexpectedValueException $ex) { + } catch (UnexpectedValueException $ex) { $reject("Too much options passed."); return; } - + $resolve(["html" => $this->getPollHtml($pollId)]); } - - function unvote(int $pollId, callable $resolve, callable $reject): void + + public function unvote(int $pollId, callable $resolve, callable $reject): void { $poll = $this->polls->get($pollId); - if(!$poll) { + if (!$poll) { $reject("Poll not found"); return; } - + try { $poll->revokeVote($this->user); - } catch(PollLockedException $ex) { + } catch (PollLockedException $ex) { $reject("Votes can't be revoked from this poll."); return; } - + $resolve(["html" => $this->getPollHtml($pollId)]); } -} \ No newline at end of file +} diff --git a/ServiceAPI/Search.php b/ServiceAPI/Search.php deleted file mode 100644 index 46def4f4d..000000000 --- a/ServiceAPI/Search.php +++ /dev/null @@ -1,76 +0,0 @@ -user = $user; - $this->users = new Users; - $this->clubs = new Clubs; - $this->videos = new Videos; - } - - function fastSearch(string $query, string $type = "users", callable $resolve, callable $reject) - { - if($query == "" || strlen($query) < 3) - $reject(12, "No input or input < 3"); - - $repo; - $sort; - - switch($type) { - default: - case "users": - $repo = (new Users); - $sort = "rating DESC"; - - break; - case "groups": - $repo = (new Clubs); - $sort = "id ASC"; - - break; - case "videos": - $repo = (new Videos); - $sort = "created ASC"; - - break; - } - - $res = $repo->find($query, ["doNotSearchMe" => $this->user->getId()], $sort); - - $results = array_slice(iterator_to_array($res), 0, 5); - - $count = sizeof($results); - - $arr = [ - "count" => $count, - "items" => [] - ]; - - if(sizeof($results) < 1) { - $reject(2, "No results"); - } - - foreach($results as $res) { - $arr["items"][] = [ - "id" => $res->getId(), - "name" => $type == "users" ? $res->getCanonicalName() : $res->getName(), - "avatar" => $type != "videos" ? $res->getAvatarUrl() : $res->getThumbnailURL(), - "url" => $type != "videos" ? $res->getUrl() : "/video".$res->getPrettyId(), - "description" => ovk_proc_strtr($res->getDescription() ?? "...", 40) - ]; - } - - $resolve($arr); - } -} diff --git a/ServiceAPI/Service.php b/ServiceAPI/Service.php index 0c024925b..990b958d1 100644 --- a/ServiceAPI/Service.php +++ b/ServiceAPI/Service.php @@ -1,23 +1,27 @@ -user = $user; } - - function getTime(callable $resolve, callable $reject): void + + public function getTime(callable $resolve, callable $reject): void { - $resolve(trim((new DateTime)->format("%e %B %G" . tr("time_at_sp") . "%X"))); + $resolve(trim((new DateTime())->format("%e %B %G" . tr("time_at_sp") . "%X"))); } - - function getServerVersion(callable $resolve, callable $reject): void + + public function getServerVersion(callable $resolve, callable $reject): void { $resolve("OVK " . OPENVK_VERSION); } diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php index 787a998ef..db6c32b6e 100644 --- a/ServiceAPI/Wall.php +++ b/ServiceAPI/Wall.php @@ -1,5 +1,9 @@ -user = $user; - $this->posts = new Posts; - $this->notes = new Notes; - $this->videos = new Videos; + $this->posts = new Posts(); + $this->notes = new Notes(); + $this->videos = new Videos(); } - - function getPost(int $id, callable $resolve, callable $reject): void + + public function getPost(int $id, callable $resolve, callable $reject): void { $post = $this->posts->get($id); - if(!$post || $post->isDeleted()) - $reject("No post with id=$id"); - + if (!$post || $post->isDeleted()) { + $reject(53, "No post with id=$id"); + } + + if ($post->getSuggestionType() != 0) { + $reject(25, "Can't get suggested post"); + } + + if (!$post->canBeViewedBy($this->user)) { + $reject(12, "Access denied"); + } + $res = (object) []; $res->id = $post->getId(); $res->wall = $post->getTargetWall(); $res->author = (($owner = $post->getOwner())) instanceof User ? ($owner->getId()) : ($owner->getId() * -1); - - if($post->isSigned()) + + if ($post->isSigned()) { $res->signedOffBy = $post->getOwnerPost(); - + } + $res->pinned = $post->isPinned(); $res->sponsored = $post->isAd(); $res->nsfw = $post->isExplicit(); $res->text = $post->getText(); - + $res->likes = [ "count" => $post->getLikesCount(), "hasLike" => $post->hasLikeFrom($this->user), "likedBy" => [], ]; - foreach($post->getLikers() as $liker) { + foreach ($post->getLikers() as $liker) { $res->likes["likedBy"][] = [ "id" => $liker->getId(), "url" => $liker->getURL(), @@ -52,17 +67,17 @@ function getPost(int $id, callable $resolve, callable $reject): void "avatar" => $liker->getAvatarURL(), ]; } - + $res->created = (string) $post->getPublicationTime(); $res->canPin = $post->canBePinnedBy($this->user); $res->canEdit = $res->canDelete = $post->canBeDeletedBy($this->user); - + $resolve((array) $res); } - - function newStatus(string $text, callable $resolve, callable $reject): void + + public function newStatus(string $text, callable $resolve, callable $reject): void { - $post = new Post; + $post = new Post(); $post->setOwner($this->user->getId()); $post->setWall($this->user->getId()); $post->setCreated(time()); @@ -71,70 +86,7 @@ function newStatus(string $text, callable $resolve, callable $reject): void $post->setFlags(0); $post->setNsfw(false); $post->save(); - - $resolve($post->getId()); - } - function getMyNotes(callable $resolve, callable $reject) - { - $count = $this->notes->getUserNotesCount($this->user); - $myNotes = $this->notes->getUserNotes($this->user, 1, $count); - - $arr = [ - "count" => $count, - "closed" => $this->user->getPrivacySetting("notes.read"), - "items" => [], - ]; - - foreach($myNotes as $note) { - $arr["items"][] = [ - "id" => $note->getId(), - "name" => ovk_proc_strtr($note->getName(), 30), - #"preview" => $note->getPreview() - ]; - } - - $resolve($arr); - } - - function getVideos(int $page = 1, callable $resolve, callable $reject) - { - $videos = $this->videos->getByUser($this->user, $page, 8); - $count = $this->videos->getUserVideosCount($this->user); - - $arr = [ - "count" => $count, - "items" => [], - ]; - - foreach($videos as $video) { - $res = json_decode(json_encode($video->toVkApiStruct()), true); - $res["video"]["author_name"] = $video->getOwner()->getCanonicalName(); - - $arr["items"][] = $res; - } - - $resolve($arr); - } - - function searchVideos(int $page = 1, string $query, callable $resolve, callable $reject) - { - $dbc = $this->videos->find($query); - $videos = $dbc->page($page, 8); - $count = $dbc->size(); - - $arr = [ - "count" => $count, - "items" => [], - ]; - - foreach($videos as $video) { - $res = json_decode(json_encode($video->toVkApiStruct()), true); - $res["video"]["author_name"] = $video->getOwner()->getCanonicalName(); - - $arr["items"][] = $res; - } - - $resolve($arr); + $resolve($post->getId()); } } diff --git a/VKAPI/Exceptions/APIErrorException.php b/VKAPI/Exceptions/APIErrorException.php index c570e0e94..f5e44f631 100644 --- a/VKAPI/Exceptions/APIErrorException.php +++ b/VKAPI/Exceptions/APIErrorException.php @@ -1,5 +1,7 @@ -requireUser(); - - return (object) [ - "first_name" => $this->getUser()->getFirstName(), - "id" => $this->getUser()->getId(), - "last_name" => $this->getUser()->getLastName(), - "home_town" => $this->getUser()->getHometown(), - "status" => $this->getUser()->getStatus(), - "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'), - "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), + $user = $this->getUser(); + $return_object = (object) [ + "first_name" => $user->getFirstName(), + "photo_200" => $user->getAvatarURL("normal"), + "nickname" => $user->getPseudo(), + "is_service_account" => false, + "id" => $user->getId(), + "is_verified" => $user->isVerified(), + "verification_status" => $user->isVerified() ? 'verified' : 'unverified', + "last_name" => $user->getLastName(), + "home_town" => $user->getHometown(), + "status" => $user->getStatus(), + "bdate" => is_null($user->getBirthday()) ? '01.01.1970' : $user->getBirthday()->format('%e.%m.%Y'), + "bdate_visibility" => $user->getBirthdayPrivacy(), "phone" => "+420 ** *** 228", # TODO - "relation" => $this->getUser()->getMaritalStatus(), - "sex" => $this->getUser()->isFemale() ? 1 : 2 + "relation" => $user->getMaritalStatus(), + "screen_name" => $user->getShortCode(), + "sex" => $user->isFemale() ? 1 : 2, + #"email" => $user->getEmail(), ]; + + $audio_status = $user->getCurrentAudioStatus(); + if (!is_null($audio_status)) { + $return_object->audio_status = $audio_status->toVkApiStruct($user); + } + + return $return_object; } - function getInfo(): object + public function getInfo(): object { $this->requireUser(); @@ -37,58 +56,71 @@ function getInfo(): object "is_new_live_streaming_enabled" => false, "lang" => 1, "no_wall_replies" => 0, - "own_posts_default" => 0 + "own_posts_default" => 0, ]; } - function setOnline(): int + public function setOnline(): int { $this->requireUser(); $this->getUser()->updOnline($this->getPlatform()); - + return 1; } - function setOffline(): int + public function setOffline(): int { $this->requireUser(); # Цiй метод є заглушка - + return 1; } - function getAppPermissions(): int + public function getAppPermissions(): int { return 9355263; } - function getCounters(string $filter = ""): object + public function getCounters(string $filter = ""): object { $this->requireUser(); - - return (object) [ + + $all_counters = [ "friends" => $this->getUser()->getFollowersCount(), "notifications" => $this->getUser()->getNotificationsCount(), - "messages" => $this->getUser()->getUnreadMessagesCount() + "messages" => $this->getUser()->getUnreadMessagesCount(), + "requests" => $this->getUser()->getRequestsCount(), ]; - # TODO: Filter + if (!empty($filter)) { + $response = []; + $fields = explode(',', $filter); + + foreach ($fields as $field) { + if (isset($all_counters[$field])) { + $response[$field] = $all_counters[$field]; + } + } + return (object) $response; + } + + return (object) $all_counters; } - function saveProfileInfo(string $first_name = "", string $last_name = "", string $screen_name = "", int $sex = -1, int $relation = -1, string $bdate = "", int $bdate_visibility = -1, string $home_town = "", string $status = ""): object + public function saveProfileInfo(string $first_name = "", string $last_name = "", string $screen_name = "", int $sex = -1, int $relation = -1, string $bdate = "", int $bdate_visibility = -1, string $home_town = "", string $status = "", string $telegram = null): object { $this->requireUser(); $this->willExecuteWriteAction(); - + $user = $this->getUser(); $output = [ "changed" => 0, ]; - if(!empty($first_name) || !empty($last_name)) { + if (!empty($first_name) || !empty($last_name)) { $output["name_request"] = [ "id" => random_int(1, 2048), # For compatibility with original VK API "status" => "success", @@ -97,37 +129,44 @@ function saveProfileInfo(string $first_name = "", string $last_name = "", string ]; try { - if(!empty($first_name)) + if (!empty($first_name)) { $user->setFirst_name($first_name); - if(!empty($last_name)) + } + if (!empty($last_name)) { $user->setLast_Name($last_name); + } } catch (InvalidUserNameException $e) { $output["name_request"]["status"] = "declined"; return (object) $output; } } - if(!empty($screen_name)) - if (!$user->setShortCode($screen_name)) + if (!empty($screen_name)) { + if (!$user->setShortCode($screen_name)) { $this->fail(1260, "Invalid screen name"); + } + } # For compatibility with original VK API - if($sex > 0) + if ($sex > 0) { $user->setSex($sex == 1 ? 1 : 0); - - if($relation > -1) + } + + if ($relation > -1 && $relation <= 8) { $user->setMarital_Status($relation); + } - if(!empty($bdate)) { + if (!empty($bdate)) { $birthday = strtotime($bdate); - if (!is_int($birthday)) + if (!is_int($birthday) || $birthday > time()) { $this->fail(100, "invalid value of bdate."); + } $user->setBirthday($birthday); } # For compatibility with original VK API - switch($bdate_visibility) { + switch ($bdate_visibility) { case 0: $this->fail(946, "Hiding date of birth is not implemented."); break; @@ -137,18 +176,232 @@ function saveProfileInfo(string $first_name = "", string $last_name = "", string case 2: $user->setBirthday_privacy(1); } - - if(!empty($home_town)) + + if (!empty($home_town)) { $user->setHometown($home_town); + } - if(!empty($status)) + if (!empty($status)) { $user->setStatus($status); - - if($sex > 0 || $relation > -1 || $bdate_visibility > 1 || !empty("$first_name$last_name$screen_name$bdate$home_town$status")) { + } + + if (!is_null($telegram)) { + if (empty($telegram)) { + $user->setTelegram(null); + } elseif (Validator::i()->telegramValid($telegram)) { + if (strpos($telegram, "t.me/") === 0) { + $user->setTelegram($telegram); + } else { + $user->setTelegram(ltrim($telegram, "@")); + } + } + } + + if ($sex > 0 || $relation > -1 || $bdate_visibility > 1 || !is_null($telegram) || !empty("$first_name$last_name$screen_name$bdate$home_town$status")) { $output["changed"] = 1; - $user->save(); + + try { + $user->save(); + } catch (\TypeError $e) { + $output["changed"] = 0; + } } return (object) $output; } + + public function getBalance(): object + { + $this->requireUser(); + if (!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) { + $this->fail(-105, "Commerce is disabled on this instance"); + } + + return (object) ['votes' => $this->getUser()->getCoins()]; + } + + public function getOvkSettings(): object + { + $this->requireUser(); + $user = $this->getUser(); + + $settings_list = (object) [ + 'avatar_style' => $user->getStyleAvatar(), + 'style' => $user->getStyle(), + 'show_rating' => !$user->prefersNotToSeeRating(), + 'nsfw_tolerance' => $user->getNsfwTolerance(), + 'post_view' => $user->hasMicroblogEnabled() ? 'microblog' : 'old', + 'main_page' => $user->getMainPage() == 0 ? 'my_page' : 'news', + ]; + + return $settings_list; + } + + public function sendVotes(int $receiver, int $value, string $message = ""): object + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { + $this->fail(-105, "Commerce is disabled on this instance"); + } + + if ($receiver < 0) { + $this->fail(-248, "Invalid receiver id"); + } + + if ($value < 1) { + $this->fail(-248, "Invalid value"); + } + + if (iconv_strlen($message) > 255) { + $this->fail(-249, "Message is too long"); + } + + if ($this->getUser()->getCoins() < $value) { + $this->fail(-252, "Not enough votes"); + } + + $receiver_entity = (new \openvk\Web\Models\Repositories\Users())->get($receiver); + if (!$receiver_entity || $receiver_entity->isDeleted() || !$receiver_entity->canBeViewedBy($this->getUser())) { + $this->fail(-250, "Invalid receiver"); + } + + if ($receiver_entity->getId() === $this->getUser()->getId()) { + $this->fail(-251, "Can't transfer votes to yourself"); + } + + $this->getUser()->setCoins($this->getUser()->getCoins() - $value); + $this->getUser()->save(); + + $receiver_entity->setCoins($receiver_entity->getCoins() + $value); + $receiver_entity->save(); + + (new \openvk\Web\Models\Entities\Notifications\CoinsTransferNotification($receiver_entity, $this->getUser(), $value, $message))->emit(); + + return (object) ['votes' => $this->getUser()->getCoins()]; + } + + public function ban(int $owner_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if ($owner_id < 0) { + return 1; + } + + if ($owner_id == $this->getUser()->getId()) { + $this->fail(15, "Access denied: cannot blacklist yourself"); + } + + $config_limit = OPENVK_ROOT_CONF['openvk']['preferences']['blacklists']['limit'] ?? 100; + $user_blocks = $this->getUser()->getBlacklistSize(); + if (($user_blocks + 1) > $config_limit) { + $this->fail(-7856, "Blacklist limit exceeded"); + } + + $entity = get_entity_by_id($owner_id); + if (!$entity || $entity->isDeleted()) { + return 0; + } + + if ($entity->isBlacklistedBy($this->getUser())) { + return 1; + } + + $this->getUser()->addToBlacklist($entity); + + return 1; + } + + public function unban(int $owner_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if ($owner_id < 0) { + return 1; + } + + if ($owner_id == $this->getUser()->getId()) { + return 1; + } + + $entity = get_entity_by_id($owner_id); + if (!$entity) { + return 0; + } + + if (!$entity->isBlacklistedBy($this->getUser())) { + return 1; + } + + $this->getUser()->removeFromBlacklist($entity); + + return 1; + } + + public function getBanned(int $offset = 0, int $count = 100, string $fields = ""): object + { + $this->requireUser(); + + $result = (object) [ + 'count' => $this->getUser()->getBlacklistSize(), + 'items' => [], + ]; + $banned = $this->getUser()->getBlacklist($offset, $count); + foreach ($banned as $ban) { + if (!$ban) { + continue; + } + $result->items[] = $ban->toVkApiStruct($this->getUser(), $fields); + } + + return $result; + } + + public function saveInterestsInfo( + string $interests = null, + string $fav_music = null, + string $fav_films = null, + string $fav_shows = null, + string $fav_books = null, + string $fav_quote = null, + string $fav_games = null, + string $about = null, + ) { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $user = $this->getUser(); + $changes = 0; + $changes_array = [ + "interests" => $interests, + "fav_music" => $fav_music, + "fav_films" => $fav_films, + "fav_books" => $fav_books, + "fav_shows" => $fav_shows, + "fav_quote" => $fav_quote, + "fav_games" => $fav_games, + "about" => $about, + ]; + + foreach ($changes_array as $change_name => $change_value) { + $set_name = "set" . ucfirst($change_name); + $get_name = "get" . str_replace("Fav", "Favorite", str_replace("_", "", ucfirst($change_name))); + if (!is_null($change_value) && $change_value !== $user->$get_name()) { + $user->$set_name(ovk_proc_strtr($change_value, 1000)); + $changes += 1; + } + } + + if ($changes > 0) { + $user->save(); + } + + return (object) [ + "changed" => (int) ($changes > 0), + ]; + } } diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 3fa68e722..20eafa061 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -1,22 +1,910 @@ - 1, - "items" => [(object) [ - "id" => 1, - "owner_id" => 1, - "artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", - "title" => "ЖДИТЕ :)))", - "duration" => 22, - "url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" - ]] - ]; - } + private function toSafeAudioStruct(?AEntity $audio, ?string $hash = null, bool $need_user = false): object + { + if (!$audio) { + $this->fail(0o404, "Audio not found"); + } elseif (!$audio->canBeViewedBy($this->getUser())) { + $this->fail(201, "Access denied to audio(" . $audio->getId() . ")"); + } + + $audioObj = $audio->toVkApiStruct($this->getUser()); + + if ($need_user) { + $user = (new \openvk\Web\Models\Repositories\Users())->get($audio->getOwner()->getId()); + $audioObj->user = (object) [ + "id" => $user->getId(), + "photo" => $user->getAvatarUrl(), + "name" => $user->getCanonicalName(), + "name_gen" => $user->getCanonicalName(), + ]; + } + + return $audioObj; + } + + private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = null): object + { + $items = []; + foreach ($es->offsetLimit($offset, $count) as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + private function validateGenre(?string& $genre_str, ?int $genre_id): void + { + if (!is_null($genre_str)) { + if (!in_array($genre_str, AEntity::genres)) { + $this->fail(8, "Invalid genre_str"); + } + } elseif (!is_null($genre_id)) { + $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? null; + if (!$genre_str) { + $this->fail(8, "Invalid genre ID $genre_id"); + } + } + } + + private function audioFromAnyId(string $id): ?AEntity + { + $descriptor = explode("_", $id); + if (sizeof($descriptor) === 1) { + if (ctype_digit($descriptor[0])) { + $audio = (new Audios())->get((int) $descriptor[0]); + } else { + $aid = base64_decode($descriptor[0], true); + if (!$aid) { + $this->fail(8, "Invalid audio $id"); + } + + $audio = (new Audios())->get((int) $aid); + } + } elseif (sizeof($descriptor) === 2) { + $audio = (new Audios())->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); + } else { + $this->fail(8, "Invalid audio $id"); + } + + return $audio; + } + + public function getById(string $audios, ?string $hash = null, int $need_user = 0): object + { + $this->requireUser(); + + $audioIds = array_unique(explode(",", $audios)); + if (sizeof($audioIds) === 1) { + $audio = $this->audioFromAnyId($audioIds[0]); + + return (object) [ + "count" => 1, + "items" => [ + $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), + ], + ]; + } elseif (sizeof($audioIds) > 6000) { + $this->fail(1980, "Can't get more than 6000 audios at once"); + } + + $audios = []; + foreach ($audioIds as $id) { + $audios[] = $this->getById($id, $hash)->items[0]; + } + + return (object) [ + "count" => sizeof($audios), + "items" => $audios, + ]; + } + + public function isLagtrain(string $audio_id): int + { + $this->requireUser(); + + $audio = $this->audioFromAnyId($audio_id); + if (!$audio) { + $this->fail(0o404, "Audio not found"); + } + + # Possible information disclosure risks are acceptable :D + return (int) (strpos($audio->getName(), "Lagtrain") !== false); + } + + // TODO stub + public function getRecommendations(): object + { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + + public function getPopular(?int $genre_id = null, ?string $genre_str = null, int $offset = 0, int $count = 100, ?string $hash = null): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios())->getGlobal(Audios::ORDER_POPULAR, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + public function getFeed(?int $genre_id = null, ?string $genre_str = null, int $offset = 0, int $count = 100, ?string $hash = null): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios())->getGlobal(Audios::ORDER_NEW, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + public function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = null): object + { + $this->requireUser(); + + if (($auto_complete + $search_own) != 0) { + $this->fail(10, "auto_complete and search_own are not supported"); + } elseif ($count > 300 || $count < 1) { + $this->fail(8, "count is invalid: $count"); + } + + $results = (new Audios())->search($q, $sort, (bool) $performer_only, (bool) $lyrics); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + public function getCount(int $owner_id, int $uploaded_only = 0): int + { + $this->requireUser(); + + if ($owner_id < 0) { + $owner_id *= -1; + $group = (new Clubs())->get($owner_id); + if (!$group) { + $this->fail(0o404, "Group not found"); + } + + return (new Audios())->getClubCollectionSize($group); + } + + $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); + if (!$user) { + $this->fail(0o404, "User not found"); + } + + if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { + $this->fail(15, "Access denied"); + } + + if ($uploaded_only && $owner_id == $this->getUser()->getRealId()) { + return DatabaseConnection::i()->getContext()->table("audios") + ->where([ + "deleted" => false, + "owner" => $owner_id, + ])->count('*'); + } + + return (new Audios())->getUserCollectionSize($user); + } + + public function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = null, int $shuffle = 0, ?string $hash = null): object + { + $this->requireUser(); + + if ($owner_id == 0) { + $owner_id = $this->getUser()->getRealId(); + } + + $shuffleSeed = null; + $shuffleSeedStr = null; + if ($shuffle == 1) { + if (!$shuffle_seed) { + if ($need_seed == 1) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeedStr = base64_encode($shuffleSeed); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + } else { + $hOffset = ((int) date("i") * 60) + (int) date("s"); + $thisHour = time() - $hOffset; + $shuffleSeed = $thisHour + $this->getUser()->getId(); + $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); + } + } else { + $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); + $shuffleSeedStr = $shuffle_seed; + } + } + + if ($album_id != 0) { + $album = (new Audios())->getPlaylist($album_id); + if (!$album) { + $this->fail(0o404, "album_id invalid"); + } elseif (!$album->canBeViewedBy($this->getUser())) { + $this->fail(600, "Can't open this album for reading"); + } + + $songs = []; + $list = $album->getAudios($offset, $count, $shuffleSeed); + + foreach ($list as $song) { + $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); + } + + $response = (object) [ + "count" => sizeof($songs), + "items" => $songs, + ]; + if (!is_null($shuffleSeed)) { + $response->shuffle_seed = $shuffleSeedStr; + } + + return $response; + } + + if (!empty($audio_ids)) { + $audio_ids = explode(",", $audio_ids); + if (!$audio_ids) { + $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); + } elseif (sizeof($audio_ids) < 1) { + $this->fail(8, "Invalid audio_ids syntax"); + } + + if (!is_null($shuffleSeed)) { + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + } + + $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); + if (!is_null($shuffleSeed)) { + $obj->shuffle_seed = $shuffleSeedStr; + } + + return $obj; + } + + $dbCtx = DatabaseConnection::i()->getContext(); + if ($uploaded_only == 1 && $owner_id == $this->getUser()->getRealId()) { + if ($owner_id <= 0) { + $this->fail(8, "uploaded_only can only be used with owner_id > 0"); + } + + $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); + + if (!$user) { + $this->fail(0o602, "Invalid user"); + } + + if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + + if (!is_null($shuffleSeed)) { + $audio_ids = []; + $query = $dbCtx->table("audios")->select("virtual_id")->where([ + "owner" => $owner_id, + "deleted" => 0, + ]); + + foreach ($query as $res) { + $audio_ids[] = $res->virtual_id; + } + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; # audio.getById query + foreach ($audio_ids as $aid) { + $audio_q .= ",$owner_id" . "_$aid"; + } + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $res = (new Audios())->getByUploader((new \openvk\Web\Models\Repositories\Users())->get($owner_id)); + + return $this->streamToResponse($res, $offset, $count, $hash, $need_user); + } + + $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); + if (!is_null($shuffleSeed)) { + $audio_ids = []; + foreach ($query as $aid) { + $audio_ids[] = $aid->audio; + } + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; + foreach ($audio_ids as $aid) { + $audio_q .= ",$aid"; + } + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $items = []; + + if ($owner_id > 0) { + $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); + + if (!$user) { + $this->fail(50, "Invalid user"); + } + + if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + } + + $audios = (new Audios())->getByEntityID($owner_id, $offset, $count); + foreach ($audios as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + public function getLyrics(int $lyrics_id): object + { + $this->requireUser(); + + $audio = (new Audios())->get($lyrics_id); + if (!$audio || !$audio->getLyrics()) { + $this->fail(0o404, "Not found"); + } + + if (!$audio->canBeViewedBy($this->getUser())) { + $this->fail(201, "Access denied to lyrics"); + } + + return (object) [ + "lyrics_id" => $lyrics_id, + "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), + ]; + } + + public function beacon(int $aid, ?int $gid = null): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios())->get($aid); + if (!$audio) { + $this->fail(0o404, "Not Found"); + } elseif (!$audio->canBeViewedBy($this->getUser())) { + $this->fail(201, "Insufficient permissions to listen this audio"); + } + + $group = null; + if (!is_null($gid)) { + $group = (new Clubs())->get($gid); + if (!$group) { + $this->fail(0o404, "Not Found"); + } elseif (!$group->canBeModifiedBy($this->getUser())) { + $this->fail(203, "Insufficient rights to this group"); + } + } + + return (int) $audio->listen($group ?? $this->getUser()); + } + + public function setBroadcast(string $audio, string $target_ids): array + { + $this->requireUser(); + + [$owner, $aid] = explode("_", $audio); + $song = (new Audios())->getByOwnerAndVID((int) $owner, (int) $aid); + $ids = []; + foreach (explode(",", $target_ids) as $id) { + $id = (int) $id; + if ($id > 0) { + if ($id != $this->getUser()->getId()) { + $this->fail(600, "Can't listen on behalf of $id"); + } else { + $ids[] = $id; + $this->beacon($song->getId()); + continue; + } + } + + $group = (new Clubs())->get($id * -1); + if (!$group) { + $this->fail(0o404, "Not Found"); + } elseif (!$group->canBeModifiedBy($this->getUser())) { + $this->fail(203, "Insufficient rights to this group"); + } + + $ids[] = $id; + $this->beacon($song ? $song->getId() : 0, $id * -1); + } + + return $ids; + } + + public function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = null): object + { + $this->requireUser(); + + if (!in_array($filter, ["all", "friends", "groups"])) { + $this->fail(8, "Invalid filter $filter"); + } + + $broadcastList = $this->getUser()->getBroadcastList($filter); + $items = []; + foreach ($broadcastList as $res) { + $struct = $res->toVkApiStruct(); + $status = $res->getCurrentAudioStatus(); + + $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : null; + $items[] = $struct; + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + public function edit(int $owner_id, int $audio_id, ?string $artist = null, ?string $title = null, ?string $text = null, ?int $genre_id = null, ?string $genre_str = null, int $no_search = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); + if (!$audio) { + $this->fail(0o404, "Not Found"); + } elseif (!$audio->canBeModifiedBy($this->getUser())) { + $this->fail(201, "Insufficient permissions to edit this audio"); + } + + if (!is_null($genre_id)) { + $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? null; + if (!$genre) { + $this->fail(8, "Invalid genre ID $genre_id"); + } + + $audio->setGenre($genre); + } elseif (!is_null($genre_str)) { + if (!in_array($genre_str, AEntity::genres)) { + $this->fail(8, "Invalid genre ID $genre_str"); + } + + $audio->setGenre($genre_str); + } + + $lyrics = 0; + if (!is_null($text)) { + $audio->setLyrics($text); + $lyrics = $audio->getId(); + } + + if (!is_null($artist)) { + $audio->setPerformer($artist); + } + + if (!is_null($title)) { + $audio->setName($title); + } + + $audio->setSearchability(!((bool) $no_search)); + $audio->setEdited(time()); + $audio->save(); + + return $lyrics; + } + + public function add(int $audio_id, int $owner_id, ?int $group_id = null, ?int $album_id = null): string + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if (!is_null($album_id)) { + $this->fail(10, "album_id not implemented"); + } + + // TODO get rid of dups + $to = $this->getUser(); + if (!is_null($group_id)) { + $group = (new Clubs())->get($group_id); + if (!$group) { + $this->fail(0o404, "Invalid group_id"); + } elseif (!$group->canBeModifiedBy($this->getUser())) { + $this->fail(203, "Insufficient rights to this group"); + } + + $to = $group; + } + + $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); + if (!$audio) { + $this->fail(0o404, "Not found"); + } elseif (!$audio->canBeViewedBy($this->getUser())) { + $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); + } + + try { + $audio->add($to); + } catch (\OverflowException $ex) { + $this->fail(300, "Album is full"); + } + + return $audio->getPrettyId(); + } + + public function delete(int $audio_id, int $owner_id, ?int $group_id = null): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $from = $this->getUser(); + if (!is_null($group_id)) { + $group = (new Clubs())->get($group_id); + if (!$group) { + $this->fail(0o404, "Invalid group_id"); + } elseif (!$group->canBeModifiedBy($this->getUser())) { + $this->fail(203, "Insufficient rights to this group"); + } + + $from = $group; + } + + $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); + if (!$audio) { + $this->fail(0o404, "Not found"); + } + + $audio->remove($from); + + return 1; + } + + public function restore(int $audio_id, int $owner_id, ?int $group_id = null, ?string $hash = null): object + { + $this->requireUser(); + + $vid = $this->add($audio_id, $owner_id, $group_id); + + return $this->getById($vid, $hash)->items[0]; + } + + public function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + $this->requireUser(); + + $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; + $playlists = []; + + if ($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); + + if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { + $this->fail(50, "Access to playlists denied"); + } + } + + foreach ((new Audios())->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { + if (!$playlist->canBeViewedBy($this->getUser())) { + if ($drop_private == 1) { + continue; + } + + $playlists[] = null; + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + public function searchAlbums(string $query = '', int $offset = 0, int $limit = 25, int $drop_private = 0, int $order = 0, int $from_me = 0): object + { + $this->requireUser(); + + $playlists = []; + $params = []; + $order_str = (['id', 'length', 'listens'][$order] ?? 'id'); + if ($from_me === 1) { + $params['from_me'] = $this->getUser()->getId(); + } + + $search = (new Audios())->findPlaylists($query, $params, ['type' => $order_str, 'invert' => false]); + foreach ($search->offsetLimit($offset, $limit) as $playlist) { + if (!$playlist->canBeViewedBy($this->getUser())) { + if ($drop_private == 0) { + $playlists[] = null; + } + + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => $search->size(), + "items" => $playlists, + ]; + } + + public function addAlbum(string $title, ?string $description = null, int $group_id = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $group = null; + if ($group_id != 0) { + $group = (new Clubs())->get($group_id); + if (!$group) { + $this->fail(0o404, "Invalid group_id"); + } elseif (!$group->canBeModifiedBy($this->getUser())) { + $this->fail(600, "Insufficient rights to this group"); + } + } + + $album = new Playlist(); + $album->setName($title); + if (!is_null($group)) { + $album->setOwner($group_id * -1); + } else { + $album->setOwner($this->getUser()->getId()); + } + + if (!is_null($description)) { + $album->setDescription($description); + } + + $album->save(); + if (!is_null($group)) { + $album->bookmark($group); + } else { + $album->bookmark($this->getUser()); + } + + return $album->getId(); + } + + public function editAlbum(int $album_id, ?string $title = null, ?string $description = null): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios())->getPlaylist($album_id); + if (!$album) { + $this->fail(0o404, "Album not found"); + } elseif (!$album->canBeModifiedBy($this->getUser())) { + $this->fail(600, "Insufficient rights to this album"); + } + + if (!is_null($title)) { + $album->setName($title); + } + + if (!is_null($description)) { + $album->setDescription($description); + } + + $album->setEdited(time()); + $album->save(); + + return (int) !(!$title && !$description); + } + + public function deleteAlbum(int $album_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios())->getPlaylist($album_id); + if (!$album) { + $this->fail(0o404, "Album not found"); + } elseif (!$album->canBeModifiedBy($this->getUser())) { + $this->fail(600, "Insufficient rights to this album"); + } + + $album->delete(); + + return 1; + } + + public function moveToAlbum(int $album_id, string $audio_ids, ?bool $do_link = false): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = null; + if ($album_id > 0) { + $album = (new Audios())->getPlaylist($album_id); + if (!$album) { + $this->fail(0o404, "Album not found"); + } elseif (!$album->canBeModifiedBy($this->getUser())) { + $this->fail(600, "Insufficient rights to this album"); + } + } elseif (!$do_link) { + return 0; + } + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if (sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) { + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + } + + foreach ($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if (!$audio) { + continue; + } elseif (!$audio->canBeViewedBy($this->getUser())) { + continue; + } + + $audios[] = $audio; + } + + if (sizeof($audios) < 1) { + return 0; + } + + $res = 1; + try { + foreach ($audios as $audio) { + if ($do_link) { + if ($audio->canBeModifiedBy($this->getUser())) { + if ($album) { + $audio->setAlbum($album); + } else { + $audio->setAlbumId(0); + } + $audio->save(); + } + } else { + if ($album) { + $res = min($res, (int) $album->add($audio)); + } + } + } + } catch (\OutOfBoundsException $ex) { + return 0; + } + + return $res; + } + + public function removeFromAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios())->getPlaylist($album_id); + if (!$album) { + $this->fail(0o404, "Album not found"); + } elseif (!$album->canBeModifiedBy($this->getUser())) { + $this->fail(600, "Insufficient rights to this album"); + } + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if (sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) { + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + } + + foreach ($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if (!$audio) { + continue; + } elseif ($audio->canBeViewedBy($this->getUser())) { + continue; + } + + $audios[] = $audio; + } + + if (sizeof($audios) < 1) { + return 0; + } + + foreach ($audios as $audio) { + $album->remove($audio); + } + + return 1; + } + + public function copyToAlbum(int $album_id, string $audio_ids): int + { + return $this->moveToAlbum($album_id, $audio_ids); + } + + public function bookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios())->getPlaylist($id); + if (!$album) { + $this->fail(0o404, "Not found"); + } + + if (!$album->canBeViewedBy($this->getUser())) { + $this->fail(600, "Access error"); + } + + return (int) $album->bookmark($this->getUser()); + } + + public function unBookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios())->getPlaylist($id); + if (!$album) { + $this->fail(0o404, "Not found"); + } + + if (!$album->canBeViewedBy($this->getUser())) { + $this->fail(600, "Access error"); + } + + return (int) $album->unbookmark($this->getUser()); + } + + public function getPlaylists(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + // alias of getPlaylists + return $this->getAlbums($owner_id, $offset, $count, $drop_private); + } + + public function getPlaylistById(int $owner_id = 0, int $playlist_id = 0): object + { + $playlist = (new Audios())->getPlaylistByOwnerAndVID($owner_id, $playlist_id); + + if (!$playlist || $playlist->isDeleted()) { + $this->fail(15, "Access error"); + } + + return (object) [ + "id" => $playlist->getId(), + "owner_id" => $playlist->getOwnerId(), + "title" => $playlist->getName(), + "cover_url" => $playlist->getCoverURL(), + ]; + } + + public function subscribeToQueue(): object + { + # dummy function + + return (object) ["url" => ""]; + } } diff --git a/VKAPI/Handlers/Board.php b/VKAPI/Handlers/Board.php index 5a4b87efe..e2e9b86b5 100644 --- a/VKAPI/Handlers/Board.php +++ b/VKAPI/Handlers/Board.php @@ -1,5 +1,9 @@ -requireUser(); $this->willExecuteWriteAction(); - $club = (new ClubsRepo)->get($group_id); + $club = (new ClubsRepo())->get($group_id); - if(!$club) { - $this->fail(403, "Invalid club"); + if (!$club) { + $this->fail(15, "Access denied"); } - if(!$club->canBeModifiedBy($this->getUser()) && !$club->isEveryoneCanCreateTopics()) { - $this->fail(403, "Access to club denied"); + if (!$club->canBeModifiedBy($this->getUser()) && !$club->isEveryoneCanCreateTopics()) { + $this->fail(15, "Access denied"); } $flags = 0; - if($from_group == true && $club->canBeModifiedBy($this->getUser())) + if ($from_group == true && $club->canBeModifiedBy($this->getUser())) { $flags |= 0b10000000; - - $topic = new Topic; + } + + $topic = new Topic(); $topic->setGroup($club->getId()); $topic->setOwner($this->getUser()->getId()); $topic->setTitle(ovk_proc_strtr($title, 127)); $topic->setCreated(time()); $topic->setFlags($flags); - $topic->save(); - - if(!empty($text)) { - $comment = new Comment; - $comment->setOwner($this->getUser()->getId()); - $comment->setModel(get_class($topic)); - $comment->setTarget($topic->getId()); - $comment->setContent($text); - $comment->setCreated(time()); - $comment->setFlags($flags); - $comment->save(); - - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - # блин а мне это везде копировать типа - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $comment->attach($attacc); - } - } + $topic->save(); + try { + if (!empty($text)) { + $comment = new Comment(); + $comment->setOwner($this->getUser()->getId()); + $comment->setModel(get_class($topic)); + $comment->setTarget($topic->getId()); + $comment->setContent($text); + $comment->setCreated(time()); + $comment->setFlags($flags); + + $comment->save(); } - + } catch (\Throwable $e) { + return $topic->getId(); } return $topic->getId(); } - function closeTopic(int $group_id, int $topic_id) + public function closeTopic(int $group_id, int $topic_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || !$topic->getClub()->canBeModifiedBy($this->getUser())) { return 0; } - if(!$topic->isClosed()) { + if (!$topic->isClosed()) { $topic->setClosed(1); $topic->save(); } @@ -119,108 +82,48 @@ function closeTopic(int $group_id, int $topic_id) return 1; } - function createComment(int $group_id, int $topic_id, string $message = "", string $attachments = "", bool $from_group = true) + public function createComment(int $group_id, int $topic_id, string $message = "", bool $from_group = true) { $this->requireUser(); $this->willExecuteWriteAction(); - if(empty($message) && empty($attachments)) { + if (empty($message)) { $this->fail(100, "Required parameter 'message' missing."); } - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || $topic->isDeleted() || $topic->isClosed()) { - $this->fail(100, "Topic is deleted, closed or invalid."); + if (!$topic || $topic->isDeleted() || $topic->isClosed()) { + $this->fail(15, "Access denied"); } $flags = 0; - - if($from_group != 0 && !is_null($topic->getClub()) && $topic->getClub()->canBeModifiedBy($this->user)) + if ($from_group != 0 && ($topic->getClub()->canBeModifiedBy($this->user))) { $flags |= 0b10000000; - - if(strlen($message) > 300) { - $this->fail(20, "Comment is too long."); } - $comment = new Comment; + $comment = new Comment(); + $comment->setOwner($this->getUser()->getId()); $comment->setModel(get_class($topic)); $comment->setTarget($topic->getId()); $comment->setContent($message); $comment->setCreated(time()); $comment->setFlags($flags); - $comment->save(); - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $comment->attach($attacc); - } - } - } + $comment->save(); return $comment->getId(); } - function deleteComment(int $comment_id, int $group_id = 0, int $topic_id = 0) - { - $this->requireUser(); - $this->willExecuteWriteAction(); - - $comment = (new CommentsRepo)->get($comment_id); - - if($comment->isDeleted() || !$comment || !$comment->canBeDeletedBy($this->getUser())) - $this->fail(403, "Access to comment denied"); - - $comment->delete(); - - return 1; - } - - function deleteTopic(int $group_id, int $topic_id) + public function deleteTopic(int $group_id, int $topic_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { return 0; } @@ -229,32 +132,14 @@ function deleteTopic(int $group_id, int $topic_id) return 1; } - function editComment(int $comment_id, int $group_id = 0, int $topic_id = 0, string $message, string $attachments) - { - /* - $this->requireUser(); - $this->willExecuteWriteAction(); - - $comment = (new CommentsRepo)->get($comment_id); - - if($comment->getOwner() != $this->getUser()->getId()) - $this->fail(15, "Access to comment denied"); - - $comment->setContent($message); - $comment->setEdited(time()); - $comment->save(); - */ - return 1; - } - - function editTopic(int $group_id, int $topic_id, string $title) + public function editTopic(int $group_id, int $topic_id, string $title) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || $topic->isDeleted() || !$topic->canBeModifiedBy($this->getUser())) { return 0; } @@ -265,14 +150,14 @@ function editTopic(int $group_id, int $topic_id, string $title) return 1; } - function fixTopic(int $group_id, int $topic_id) + public function fixTopic(int $group_id, int $topic_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || !$topic->getClub()->canBeModifiedBy($this->getUser())) { return 0; } @@ -283,89 +168,106 @@ function fixTopic(int $group_id, int $topic_id) return 1; } - function getComments(int $group_id, int $topic_id, bool $need_likes = false, int $start_comment_id = 0, int $offset = 0, int $count = 40, bool $extended = false, string $sort = "asc") + public function getComments(int $group_id, int $topic_id, bool $need_likes = false, int $offset = 0, int $count = 10, bool $extended = false) { - # start_comment_id ne robit $this->requireUser(); - $this->willExecuteWriteAction(); - - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || $topic->isDeleted()) { - $this->fail(5, "Invalid topic"); + if ($count < 1 || $count > 100) { + $this->fail(4, "Invalid count"); + } + + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); + + if (!$topic || $topic->isDeleted()) { + $this->fail(5, "Not found"); } - $arr = [ - "items" => [] + $obj = (object) [ + "items" => [], ]; - $comms = array_slice(iterator_to_array($topic->getComments(1, $count + $offset)), $offset); - foreach($comms as $comm) { - $arr["items"][] = $this->getApiBoardComment($comm, $need_likes); - - if($extended) { - if($comm->getOwner() instanceof \openvk\Web\Models\Entities\User) { - $arr["profiles"][] = $comm->getOwner()->toVkApiStruct(); + if ($extended) { + $obj->profiles = []; + $obj->groups = []; + } + + $comments = array_slice(iterator_to_array($topic->getComments(1, $count + $offset)), $offset); + + foreach ($comments as $comment) { + $obj->items[] = $comment->toVkApiStruct($this->getUser(), $need_likes); + + if ($extended) { + $owner = $comment->getOwner(); + + if ($owner instanceof \openvk\Web\Models\Entities\User) { + $obj->profiles[] = $owner->toVkApiStruct(); } - if($comm->getOwner() instanceof \openvk\Web\Models\Entities\Club) { - $arr["groups"][] = $comm->getOwner()->toVkApiStruct(); + if ($owner instanceof \openvk\Web\Models\Entities\Club) { + $obj->groups[] = $owner->toVkApiStruct(); } } } - return $arr; + return $obj; } - function getTopics(int $group_id, string $topic_ids = "", int $order = 1, int $offset = 0, int $count = 40, bool $extended = false, int $preview = 0, int $preview_length = 90) + public function getTopics(int $group_id, string $topic_ids = "", int $offset = 0, int $count = 10, bool $extended = false, int $preview = 0, int $preview_length = 90) { - # order и extended ничё не делают + # TODO: $extended + $this->requireUser(); - $this->willExecuteWriteAction(); - $arr = []; - $club = (new ClubsRepo)->get($group_id); + if ($count < 1 || $count > 100) { + $this->fail(4, "Invalid count"); + } - $topics = array_slice(iterator_to_array((new TopicsRepo)->getClubTopics($club, 1, $count + $offset)), $offset); - $arr["count"] = (new TopicsRepo)->getClubTopicsCount($club); - $arr["items"] = []; - $arr["default_order"] = $order; - $arr["can_add_topics"] = $club->canBeModifiedBy($this->getUser()) ? true : $club->isEveryoneCanCreateTopics() ? true : false; - $arr["profiles"] = []; + $obj = (object) []; - if(empty($topic_ids)) { - foreach($topics as $topic) { - if($topic->isDeleted()) continue; - $arr["items"][] = $topic->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); + $club = (new ClubsRepo())->get($group_id); + + if (!$club || !$club->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + $topics = array_slice(iterator_to_array((new TopicsRepo())->getClubTopics($club, 1, $count + $offset)), $offset); + + $obj->count = (new TopicsRepo())->getClubTopicsCount($club); + $obj->items = []; + $obj->profiles = []; + $obj->can_add_topics = $club->canBeModifiedBy($this->getUser()) ? true : ($club->isEveryoneCanCreateTopics() ? true : false); + + if (empty($topic_ids)) { + foreach ($topics as $topic) { + $obj->items[] = $topic->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); } } else { $topics = explode(',', $topic_ids); - foreach($topics as $topic) { - $id = explode("_", $topic); - $topicy = (new TopicsRepo)->getTopicById((int)$id[0], (int)$id[1]); + foreach ($topics as $topic_id) { + $topic = (new TopicsRepo())->getTopicById($group_id, (int) $topic_id); - if($topicy && !$topicy->isDeleted()) { - $arr["items"][] = $topicy->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); + if ($topic && !$topic->isDeleted()) { + $obj->items[] = $topic->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); } } } - return $arr; + return $obj; } - function openTopic(int $group_id, int $topic_id) + public function openTopic(int $group_id, int $topic_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || !$topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || !$topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { return 0; } - if($topic->isClosed()) { + if ($topic->isClosed()) { $topic->setClosed(0); $topic->save(); } @@ -373,23 +275,18 @@ function openTopic(int $group_id, int $topic_id) return 1; } - function restoreComment(int $group_id, int $topic_id, int $comment_id) - { - $this->fail(501, "Not implemented"); - } - - function unfixTopic(int $group_id, int $topic_id) + public function unfixTopic(int $group_id, int $topic_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); + $topic = (new TopicsRepo())->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if (!$topic || !$topic->getClub()->canBeModifiedBy($this->getUser())) { return 0; } - if($topic->isPinned()) { + if ($topic->isPinned()) { $topic->setClosed(0); $topic->save(); } @@ -400,32 +297,4 @@ function unfixTopic(int $group_id, int $topic_id) return 1; } - - private function getApiBoardComment(?Comment $comment, bool $need_likes = false) - { - $res = (object) []; - - $res->id = $comment->getId(); - $res->from_id = $comment->getOwner()->getId(); - $res->date = $comment->getPublicationTime()->timestamp(); - $res->text = $comment->getText(false); - $res->attachments = []; - $res->likes = []; - if($need_likes) { - $res->likes = [ - "count" => $comment->getLikesCount(), - "user_likes" => (int) $comment->hasLikeFrom($this->getUser()), - "can_like" => 1 # а чё типо не может ахахаххахах - ]; - } - - foreach($comment->getChildren() as $attachment) { - if($attachment->isDeleted()) - continue; - - $res->attachments[] = $attachment->toVkApiStruct(); - } - - return $res; - } -} \ No newline at end of file +} diff --git a/VKAPI/Handlers/Docs.php b/VKAPI/Handlers/Docs.php new file mode 100644 index 000000000..c7766802b --- /dev/null +++ b/VKAPI/Handlers/Docs.php @@ -0,0 +1,239 @@ +requireUser(); + $this->willExecuteWriteAction(); + + $doc = (new Documents())->getDocumentById($owner_id, $doc_id, $access_key); + if (!$doc || $doc->isDeleted()) { + $this->fail(1150, "Invalid document id"); + } + + if (!$doc->checkAccessKey($access_key)) { + $this->fail(15, "Access denied"); + } + + if ($doc->isCopiedBy($this->getUser())) { + $this->fail(100, "One of the parameters specified was missing or invalid: this document already added"); + } + + $new_doc = $doc->copy($this->getUser()); + + return $new_doc->getPrettyId(); + } + + public function delete(int $owner_id, int $doc_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + $doc = (new Documents())->getDocumentByIdUnsafe($owner_id, $doc_id); + if (!$doc || $doc->isDeleted()) { + $this->fail(1150, "Invalid document id"); + } + + if (!$doc->canBeModifiedBy($this->getUser())) { + $this->fail(1153, "Access to document is denied"); + } + + $doc->delete(); + + return 1; + } + + public function restore(int $owner_id, int $doc_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return $this->add($owner_id, $doc_id, ""); + } + + public function edit(int $owner_id, int $doc_id, ?string $title = "", ?string $tags = "", ?int $folder_id = 0, int $owner_hidden = -1): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $doc = (new Documents())->getDocumentByIdUnsafe($owner_id, $doc_id); + if (!$doc || $doc->isDeleted()) { + $this->fail(1150, "Invalid document id"); + } + if (!$doc->canBeModifiedBy($this->getUser())) { + $this->fail(1153, "Access to document is denied"); + } + if (iconv_strlen($title ?? "") > 128 || iconv_strlen($title ?? "") < 0) { + $this->fail(1152, "Invalid document title"); + } + if (iconv_strlen($tags ?? "") > 256) { + $this->fail(1154, "Invalid tags"); + } + + if ($title) { + $doc->setName($title); + } + + $doc->setTags($tags); + if (in_array($folder_id, [0, 3])) { + $doc->setFolder_id($folder_id); + } + if (in_array($owner_hidden, [0, 1])) { + $doc->setOwner_hidden($owner_hidden); + } + + try { + $doc->setEdited(time()); + $doc->save(); + } catch (\Throwable $e) { + return 0; + } + + return 1; + } + + public function get(int $count = 30, int $offset = 0, int $type = -1, int $owner_id = null, int $return_tags = 0, int $order = 0): object + { + $this->requireUser(); + if (!$owner_id) { + $owner_id = $this->getUser()->getId(); + } + + if ($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $this->fail(15, "Access denied"); + } + + $documents = (new Documents())->getDocumentsByOwner($owner_id, $order, $type); + $items = []; + + foreach ($documents->offsetLimit($offset, $count) as $doc) { + $items[] = $doc->toVkApiStruct($this->getUser(), $return_tags == 1); + } + + return $this->generateItems($documents->size(), $items); + } + + public function getById(string $docs, int $return_tags = 0): array + { + $this->requireUser(); + + $item_ids = explode(",", $docs); + $response = []; + if (sizeof($item_ids) < 1) { + $this->fail(100, "One of the parameters specified was missing or invalid: docs is undefined"); + } + + foreach ($item_ids as $id) { + $splitted_id = explode("_", $id); + $doc = (new Documents())->getDocumentById((int) $splitted_id[0], (int) $splitted_id[1], $splitted_id[2]); + if (!$doc || $doc->isDeleted()) { + continue; + } + + $response[] = $doc->toVkApiStruct($this->getUser(), $return_tags === 1); + } + + return $response; + } + + public function getTypes(?int $owner_id) + { + $this->requireUser(); + if (!$owner_id) { + $owner_id = $this->getUser()->getId(); + } + + if ($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $this->fail(15, "Access denied"); + } + + $types = (new Documents())->getTypes($owner_id); + return [ + "count" => sizeof($types), + "items" => $types, + ]; + } + + public function getTags(?int $owner_id, ?int $type = 0) + { + $this->requireUser(); + if (!$owner_id) { + $owner_id = $this->getUser()->getId(); + } + + if ($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $this->fail(15, "Access denied"); + } + + $tags = (new Documents())->getTags($owner_id, $type); + return $tags; + } + + public function search(string $q = "", int $search_own = -1, int $order = -1, int $count = 30, int $offset = 0, int $return_tags = 0, int $type = 0, ?string $tags = null): object + { + $this->requireUser(); + + $params = []; + $o_order = ["type" => "id", "invert" => false]; + + if (iconv_strlen($q) > 512) { + $this->fail(100, "One of the parameters specified was missing or invalid: q should be not more 512 letters length"); + } + + if (in_array($type, [1,2,3,4,5,6,7,8])) { + $params["type"] = $type; + } + + if (iconv_strlen($tags ?? "") < 512) { + $params["tags"] = $tags; + } + + if ($search_own === 1) { + $params["from_me"] = $this->getUser()->getId(); + } + + $documents = (new Documents())->find($q, $params, $o_order); + $res = (object) [ + "count" => $documents->size(), + "items" => [], + ]; + + foreach ($documents->offsetLimit($offset, $count) as $doc) { + $res->items[] = $doc->toVkApiStruct($this->getUser(), $return_tags == 1); + } + + return $res; + } + + public function getUploadServer(?int $group_id = null) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + public function getWallUploadServer(?int $group_id = null) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + public function save(string $file, string $title, string $tags, ?int $return_tags = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } +} diff --git a/VKAPI/Handlers/Execute.php b/VKAPI/Handlers/Execute.php new file mode 100644 index 000000000..3d92eb3ec --- /dev/null +++ b/VKAPI/Handlers/Execute.php @@ -0,0 +1,130 @@ +requireUser(); + $this->willExecuteWriteAction(); + + $profile = $this->getUser()->toVkApiStruct(null, $fields); + $info = (object) [ + "country" => "AM", + "https_required" => 0, + "intro" => 0, + "lang" => 0, + "support_url" => ovk_scheme(true) . $_SERVER["HTTP_HOST"] . '/support', + "money_p2p_params" => (object) [ + "min_amount" => 100, + "max_amount" => 75000, + "currency" => "SPAMTON", + ], + "audio_ads" => (object) [ + "day_limit" => 10000, + "track_limit" => 10000, + "types_allowed" => [], + "sections" => ["my","user_playlists","group_playlists","my_playlists","recent","audio_feed","recs", + "recs_audio","recs_album","search","global_search","group_list","user_list", + "user_wall","group_wall","feed","other"], + ], + "profiler_settings" => (object) [ + "api_requests" => true, + "download_patterns" => [], + "raise_to_record_enabled" => true, + "music_intro" => false, + "settings" => [ + (object) [ + "name" => "audio_ads", + "available" => false, + ], + (object) [ + "name" => "audio_background_limit", + "available" => false, + "value" => "1440", + ], + (object) [ + "name" => "gif_autoplay", + "available" => true, + ], + (object) [ + "name" => "audio_restrictions", + "available" => false, + ], + (object) [ + "name" => "stories", + "available" => false, + ], + (object) [ + "name" => "masks", + "available" => false, + ], + (object) [ + "name" => "video_autoplay", + "available" => true, + ], + ], + "community_comments" => false, + ], + ]; + $counters = (object) [ + "friends" => $this->getUser()->getFollowersCount(), + "messages" => $this->getUser()->getUnreadMessagesCount(), + "photos" => 0, + "videos" => 0, + "groups" => 0, + "notifications" => $this->getUser()->getNotificationsCount(), + "sdk" => 0, + "app_requests" => 0, + ]; + $newsdata = (object) [ + "lists" => [], + "sections" => [], + "feed_type" => "top", + "refresh_timeout_recent" => 600000, + "refresh_timeout_top" => 600000, + "refresh_timeout_recommended" => 600000, + "items" => [], + "profiles" => [], + "groups" => [], + ]; + + return (object) [ + 'profile' => $profile, + 'info' => $info, + 'counters' => $counters, + 'newsfeed' => $newsdata, + 'time' => time(), + 'allow_buy_votes' => 1, + 'ads_stoplist' => [], + 'show_html_games' => 0, + 'defaultAudioPlayer' => 'standard', + ]; + } + + public function getNewsfeedWithPromo(string $fields = "", string $start_from = "", int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 1) + { + // alias of newsfeed.get + $newsfeed = $this->createHandler(Newsfeed::class); + return $newsfeed->get($fields, $start_from, $start_time, $end_time, $offset, $count, $extended, 0); + } + + public function getNewsfeedSmart(string $fields = "", string $start_from = "", int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 1) + { + // alias of newsfeed.get + $newsfeed = $this->createHandler(Newsfeed::class); + return $newsfeed->get($fields, $start_from, $start_time, $end_time, $offset, $count, $extended, 0); + } +} diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index 56de32949..4c109e130 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -1,171 +1,199 @@ -requireUser(); - $this->requireUser(); + if ($user_id == 0) { + $user_id = $this->getUser()->getId(); + } - if (is_null($users->get($user_id))) { - $this->fail(100, "One of the parameters specified was missing or invalid"); - } - - foreach($users->get($user_id)->getFriends($offset, $count) as $friend) { - $friends[$i] = $friend->getId(); - $i++; - } + $user = $users->get($user_id); - $response = $friends; + if (!$user || $user->isDeleted()) { + $this->fail(100, "Invalid user"); + } - $usersApi = new Users($this->getUser()); + if (!$user->getPrivacyPermission("friends.read", $this->getUser())) { + $this->fail(15, "Access denied: this user chose to hide his friends."); + } - if(!is_null($fields)) - $response = $usersApi->get(implode(',', $friends), $fields, 0, $count); # FIXME + foreach ($user->getFriends($offset, $count) as $friend) { + $friends[$i] = $friend->getId(); + $i++; + } - return (object) [ - "count" => $users->get($user_id)->getFriendsCount(), - "items" => $response - ]; - } + $response = $friends; - function getLists(): object - { - $this->requireUser(); + $usersApi = new Users($this->getUser()); - return (object) [ - "count" => 0, - "items" => (array)[] - ]; - } + if (!is_null($fields)) { + $response = $usersApi->get(implode(',', $friends), $fields, 0, $count); + } # FIXME - function deleteList(): int - { - $this->requireUser(); + return (object) [ + "count" => $users->get($user_id)->getFriendsCount(), + "items" => $response, + ]; + } - return 1; - } + public function getLists(): object + { + $this->requireUser(); - function edit(): int - { - $this->requireUser(); + return (object) [ + "count" => 0, + "items" => (array) [], + ]; + } - return 1; - } + public function deleteList(): int + { + $this->requireUser(); - function editList(): int - { - $this->requireUser(); + return 1; + } - return 1; - } + public function edit(): int + { + $this->requireUser(); - function add(string $user_id): int - { - $this->requireUser(); + return 1; + } + + public function editList(): int + { + $this->requireUser(); + + return 1; + } + + public function add(string $user_id): int + { + $this->requireUser(); $this->willExecuteWriteAction(); - $users = new UsersRepo; - $user = $users->get(intval($user_id)); - - 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())) { - case 0: - $user->toggleSubscription($this->getUser()); - return 1; - - case 1: - $user->toggleSubscription($this->getUser()); - return 2; - - case 3: - return 2; - - default: - return 1; - } - } - - function delete(string $user_id): int - { - $this->requireUser(); + $users = new UsersRepo(); + $user = $users->get(intval($user_id)); + + if (is_null($user)) { + $this->fail(177, "Cannot add this user to friends as user not found"); + } elseif ($user->getId() == $this->getUser()->getId()) { + $this->fail(174, "Cannot add user himself as friend"); + } + + switch ($user->getSubscriptionStatus($this->getUser())) { + case 0: + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "friends.outgoing_sub")) { + $this->failTooOften(); + } + + $user->toggleSubscription($this->getUser()); + return 1; + + case 1: + $user->toggleSubscription($this->getUser()); + return 2; + + case 3: + return 2; + + default: + return 1; + } + } + + public function delete(string $user_id): int + { + $this->requireUser(); $this->willExecuteWriteAction(); - $users = new UsersRepo; + $users = new UsersRepo(); + + $user = $users->get(intval($user_id)); - $user = $users->get(intval($user_id)); + switch ($user->getSubscriptionStatus($this->getUser())) { + case 3: + $user->toggleSubscription($this->getUser()); + return 1; - switch($user->getSubscriptionStatus($this->getUser())) { - case 3: - $user->toggleSubscription($this->getUser()); - return 1; - - default: - $this->fail(15, "Access denied: No friend or friend request found."); - } - } + default: + $this->fail(15, "Access denied: No friend or friend request found."); + } + } - function areFriends(string $user_ids): array - { - $this->requireUser(); + public function areFriends(string $user_ids): array + { + $this->requireUser(); - $users = new UsersRepo; + $users = new UsersRepo(); - $friends = explode(',', $user_ids); + $friends = explode(',', $user_ids); - $response = []; + $response = []; - for($i=0; $i < sizeof($friends); $i++) { - $friend = $users->get(intval($friends[$i])); + for ($i = 0; $i < sizeof($friends); $i++) { + $friend = $users->get(intval($friends[$i])); - $response[] = (object)[ - "friend_status" => $friend->getSubscriptionStatus($this->getUser()), - "user_id" => $friend->getId() - ]; - } + $response[] = (object) [ + "friend_status" => $friend->getSubscriptionStatus($this->getUser()), + "user_id" => $friend->getId(), + ]; + } - return $response; - } + return $response; + } - function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $extended = 0): object - { - if ($count >= 1000) - $this->fail(100, "One of the required parameters was not passed or is invalid."); + public function getRequests(string $fields = "", int $out = 0, int $offset = 0, int $count = 100, int $extended = 0): object + { + if ($count >= 1000) { + $this->fail(100, "One of the required parameters was not passed or is invalid."); + } - $this->requireUser(); + $this->requireUser(); - $i = 0; - $offset++; - $followers = []; + $i = 0; + $offset++; + $followers = []; - foreach($this->getUser()->getFollowers($offset, $count) as $follower) { - $followers[$i] = $follower->getId(); - $i++; - } + if ($out != 0) { + foreach ($this->getUser()->getFollowers($offset, $count) as $follower) { + $followers[$i] = $follower->getId(); + $i++; + } + } else { + foreach ($this->getUser()->getRequests($offset, $count) as $follower) { + $followers[$i] = $follower->getId(); + $i++; + } + } - $response = $followers; - $usersApi = new Users($this->getUser()); + $response = $followers; + $usersApi = new Users($this->getUser()); - $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); + $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); - foreach($response as $user) - $user->user_id = $user->id; + foreach ($response as $user) { + $user->user_id = $user->id; + } - return (object) [ - "count" => $this->getUser()->getFollowersCount(), - "items" => $response - ]; - } + return (object) [ + "count" => $this->getUser()->getFollowersCount(), + "items" => $response, + ]; + } } diff --git a/VKAPI/Handlers/Gifts.php b/VKAPI/Handlers/Gifts.php index 2702924d5..e98f2bfd8 100644 --- a/VKAPI/Handlers/Gifts.php +++ b/VKAPI/Handlers/Gifts.php @@ -1,90 +1,106 @@ -requireUser(); - $i = 0; + $server_url = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - $i += $offset; + if ($user_id < 1) { + $user_id = $this->getUser()->getId(); + } - $user = (new UsersRepo)->get($user_id); + $user = (new UsersRepo())->get($user_id); - if(!$user || $user->isDeleted()) - $this->fail(177, "Invalid user"); + if (!$user || $user->isDeleted()) { + $this->fail(15, "Access denied"); + } - $gift_item = []; + if (!$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - $userGifts = array_slice(iterator_to_array($user->getGifts(1, $count, false)), $offset); - - if(sizeof($userGifts) < 0) { - return NULL; - } - - foreach($userGifts as $gift) { - if($i < $count) { - $gift_item[] = [ - "id" => $i, - "from_id" => $gift->anon == true ? 0 : $gift->sender->getId(), - "message" => $gift->caption == NULL ? "" : $gift->caption, - "date" => $gift->sent->timestamp(), - "gift" => [ - "id" => $gift->gift->getId(), - "thumb_256" => $gift->gift->getImage(2), - "thumb_96" => $gift->gift->getImage(2), - "thumb_48" => $gift->gift->getImage(2) - ], - "privacy" => 0 - ]; - } - $i+=1; + $gift_item = []; + $user_gifts = array_slice(iterator_to_array($user->getGifts(1, $count)), $offset, $count); + + foreach ($user_gifts as $gift) { + $gift_item[] = [ + "id" => $gift->id, + "from_id" => $gift->anon == true ? 0 : $gift->sender->getId(), + "message" => $gift->caption == null ? "" : $gift->caption, + "date" => $gift->sent->timestamp(), + "privacy" => $gift->anon == true ? 1 : 0, + "gift" => [ + "id" => $gift->gift->getId(), + "thumb_256" => $server_url . $gift->gift->getImage(2), + "thumb_96" => $server_url . $gift->gift->getImage(2), + "thumb_48" => $server_url . $gift->gift->getImage(2), + ], + ]; } - return $gift_item; + return $this->generateItems($user->getGiftCount(), $gift_item); } - function send(int $user_ids, int $gift_id, string $message = "", int $privacy = 0) + public function send(int $user_ids, int $gift_id, string $message = "", int $privacy = 0) { $this->requireUser(); $this->willExecuteWriteAction(); - $user = (new UsersRepo)->get((int) $user_ids); + if (!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) { + $this->fail(-105, "Commerce is disabled on this instance"); + } + + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "gifts.send", false)) { + $this->failTooOften(); + } + + $user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids) - if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) - $this->fail(105, "Commerce is disabled on this instance"); - - if(!$user || $user->isDeleted()) - $this->fail(177, "Invalid user"); + if (!$user || $user->isDeleted()) { + $this->fail(15, "Access denied"); + } - $gift = (new GiftsRepo)->get($gift_id); + if (!$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + $gift = (new GiftsRepo())->get($gift_id); + + if (!$gift) { + $this->fail(15, "Invalid gift"); + } - if(!$gift) - $this->fail(165, "Invalid gift"); - $price = $gift->getPrice(); $coinsLeft = $this->getUser()->getCoins() - $price; - if(!$gift->canUse($this->getUser())) + if (!$gift->canUse($this->getUser())) { return (object) [ "success" => 0, "user_ids" => $user_ids, - "error" => "You don't have any more of these gifts." + "error" => "You don't have any more of these gifts.", ]; + } - if($coinsLeft < 0) + if ($coinsLeft < 0) { return (object) [ "success" => 0, "user_ids" => $user_ids, - "error" => "You don't have enough voices." + "error" => "You don't have enough voices.", ]; + } $user->gift($this->getUser(), $gift, $message); $gift->used(); @@ -99,39 +115,34 @@ function send(int $user_ids, int $gift_id, string $message = "", int $privacy = [ "success" => 1, "user_ids" => $user_ids, - "withdraw_votes" => $price + "withdraw_votes" => $price, ]; } - function delete() + public function getCategories(bool $extended = false, int $page = 1) { $this->requireUser(); - $this->willExecuteWriteAction(); - - $this->fail(501, "Not implemented"); - } - # этих методов не было в ВК, но я их добавил чтобы можно было отобразить список подарков - function getCategories(bool $extended = false, int $page = 1) - { - $cats = (new GiftsRepo)->getCategories($page); + $cats = (new GiftsRepo())->getCategories($page); $categ = []; $i = 0; + $server_url = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) - $this->fail(105, "Commerce is disabled on this instance"); + if (!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) { + $this->fail(-105, "Commerce is disabled on this instance"); + } - foreach($cats as $cat) { + foreach ($cats as $cat) { $categ[$i] = [ "name" => $cat->getName(), "description" => $cat->getDescription(), "id" => $cat->getId(), - "thumbnail" => $cat->getThumbnailURL(), - ]; - - if($extended == true) { + "thumbnail" => $server_url . $cat->getThumbnailURL(), + ]; + + if ($extended == true) { $categ[$i]["localizations"] = []; - foreach(getLanguages() as $lang) { + foreach (getLanguages() as $lang) { $code = $lang["code"]; $categ[$i]["localizations"][$code] = [ @@ -142,30 +153,34 @@ function getCategories(bool $extended = false, int $page = 1) } $i++; } - + return $categ; } - function getGiftsInCategory(int $id, int $page = 1) + public function getGiftsInCategory(int $id, int $page = 1) { $this->requireUser(); - if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) - $this->fail(105, "Commerce is disabled on this instance"); + if (!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) { + $this->fail(-105, "Commerce is disabled on this instance"); + } + + $gift_category = (new GiftsRepo())->getCat($id); - if(!(new GiftsRepo)->getCat($id)) - $this->fail(177, "Category not found"); + if (!$gift_category) { + $this->fail(15, "Category not found"); + } - $giftz = ((new GiftsRepo)->getCat($id))->getGifts($page); + $gifts_list = $gift_category->getGifts($page); $gifts = []; - foreach($giftz as $gift) { + foreach ($gifts_list as $gift) { $gifts[] = [ "name" => $gift->getName(), "image" => $gift->getImage(2), - "usages_left" => (int)$gift->getUsagesLeft($this->getUser()), - "price" => $gift->getPrice(), # голосов - "is_free" => $gift->isFree() + "usages_left" => (int) $gift->getUsagesLeft($this->getUser()), + "price" => $gift->getPrice(), + "is_free" => $gift->isFree(), ]; } diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 3123a43f4..4edda1680 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -1,88 +1,62 @@ -requireUser(); - if($user_id == 0) { - foreach($this->getUser()->getClubs($offset, false, $count, true) as $club) - $clbs[] = $club; - $clbsCount = $this->getUser()->getClubCount(); + # InfoApp fix + if ($filter == "admin" && ($user_id != 0 && $user_id != $this->getUser()->getId())) { + $this->fail(15, 'Access denied: filter admin is available only for current user'); + } + + $clbs = []; + if ($user_id == 0) { + foreach ($this->getUser()->getClubs($offset, $filter == "admin", $count, true) as $club) { + $clbs[] = $club; + } + $clbsCount = $this->getUser()->getClubCount(); } else { - $users = new UsersRepo; - $user = $users->get($user_id); + $users = new UsersRepo(); + $user = $users->get($user_id); - if(is_null($user)) - $this->fail(15, "Access denied"); + if (is_null($user) || $user->isDeleted()) { + $this->fail(15, "Access denied"); + } - foreach($user->getClubs($offset, false, $count, true) as $club) - $clbs[] = $club; + if (!$user->getPrivacyPermission('groups.read', $this->getUser())) { + $this->fail(260, "Access to the groups list is denied due to the user's privacy settings"); + } + + foreach ($user->getClubs($offset, $filter == "admin", $count, true) as $club) { + $clbs[] = $club; + } - $clbsCount = $user->getClubCount(); + $clbsCount = $user->getClubCount(); } - - $rClubs; + + $rClubs = []; $ic = sizeof($clbs); - if(sizeof($clbs) > $count) + if (sizeof($clbs) > $count) { $ic = $count; + } - if(!empty($clbs)) { - for($i=0; $i < $ic; $i++) { - $usr = $clbs[$i]; - if(is_null($usr)) { - - } else { - $rClubs[$i] = (object) [ - "id" => $usr->getId(), - "name" => $usr->getName(), - "screen_name" => $usr->getShortCode(), - "is_closed" => false, - "can_access_closed" => true, - ]; - - $flds = explode(',', $fields); - - foreach($flds as $field) { - switch($field) { - case "verified": - $rClubs[$i]->verified = intval($usr->isVerified()); - break; - case "has_photo": - $rClubs[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; - break; - case "photo_max_orig": - $rClubs[$i]->photo_max_orig = $usr->getAvatarURL(); - break; - case "photo_max": - $rClubs[$i]->photo_max = $usr->getAvatarURL("original"); // ORIGINAL ANDREI CHINITEL 🥵🥵🥵🥵 - break; - 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; - } - } + if (!empty($clbs)) { + for ($i = 0; $i < $ic; $i++) { + $clb = $clbs[$i]; + if (!is_null($clb)) { + $rClubs[$i] = $clb->toVkApiStruct($this->user, $fields . ",photo_50,photo_100,photo_200"); } } } else { @@ -90,446 +64,263 @@ function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count } return (object) [ - "count" => $clbsCount, - "items" => $rClubs + "count" => $clbsCount, + "items" => $rClubs, ]; } - function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array + public function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array { - /* Both offset and count SHOULD be used only in OpenVK code, + /* Both offset and count SHOULD be used only in OpenVK code, not in your app or script, since it's not oficially documented by VK */ - $clubs = new ClubsRepo; - - if(empty($group_ids) && !empty($group_id)) + $clubs = new ClubsRepo(); + + if (empty($group_ids) && !empty($group_id)) { $group_ids = $group_id; - - if(empty($group_ids) && empty($group_id)) + } + + if (empty($group_ids) && empty($group_id)) { $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); - + } + $clbs = explode(',', $group_ids); - $response = array(); + $response = []; $ic = sizeof($clbs); - if(sizeof($clbs) > $count) - $ic = $count; + if (sizeof($clbs) > $count) { + $ic = $count; + } $clbs = array_slice($clbs, $offset * $count); - for($i=0; $i < $ic; $i++) { - if($i > 500 || $clbs[$i] == 0) + for ($i = 0; $i < $ic; $i++) { + if ($i > 500 || $clbs[$i] == 0) { break; + } - if($clbs[$i] < 0) + if ($clbs[$i] < 0) { $this->fail(100, "ты ошибся чутка, у айди группы убери минус"); + } $clb = $clubs->get((int) $clbs[$i]); - if(is_null($clb)) { - $response[$i] = (object)[ + if (is_null($clb)) { + $response[$i] = (object) [ "id" => intval($clbs[$i]), "name" => "DELETED", - "screen_name" => "club".intval($clbs[$i]), + "screen_name" => "club" . intval($clbs[$i]), "type" => "group", - "description" => "This group was deleted or it doesn't exist" - ]; - } else if($clbs[$i] == NULL) { - - } else { - $response[$i] = (object)[ - "id" => $clb->getId(), - "name" => $clb->getName(), - "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), - "is_closed" => false, - "type" => "group", - "is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0, - "can_access_closed" => true, + "description" => "This group was deleted or it doesn't exist", ]; + } elseif ($clbs[$i] == null) { - $flds = explode(',', $fields); - - foreach($flds as $field) { - switch($field) { - case "verified": - $response[$i]->verified = intval($clb->isVerified()); - break; - case "has_photo": - $response[$i]->has_photo = is_null($clb->getAvatarPhoto()) ? 0 : 1; - break; - case "photo_max_orig": - $response[$i]->photo_max_orig = $clb->getAvatarURL(); - break; - case "photo_max": - $response[$i]->photo_max = $clb->getAvatarURL(); - break; - case "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": - $response[$i]->site = $clb->getWebsite(); - break; - case "description": - $response[$i]->description = $clb->getDescription(); - break; - case "contacts": - $contacts; - $contactTmp = $clb->getManagers(1, true); - - foreach($contactTmp as $contact) - $contacts[] = array( - "user_id" => $contact->getUser()->getId(), - "desc" => $contact->getComment() - ); - - $response[$i]->contacts = $contacts; - break; - case "can_post": - if(!is_null($this->getUser())) - if($clb->canBeModifiedBy($this->getUser())) - $response[$i]->can_post = true; - else - $response[$i]->can_post = $clb->canPost(); - break; - } - } + } else { + $response[$i] = $clb->toVkApiStruct($this->user, $fields . ",photo_50,photo_100,photo_200"); } } return $response; } - function search(string $q, int $offset = 0, int $count = 100) + public function search(string $q, int $offset = 0, int $count = 100, string $fields = "screen_name,is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200") { - $clubs = new ClubsRepo; - + if ($count > 100) { + $this->fail(100, "One of the parameters specified was missing or invalid: count should be less or equal to 100"); + } + + $clubs = new ClubsRepo(); + $array = []; - $find = $clubs->find($q); + $find = $clubs->find($q); - foreach ($find as $group) + foreach ($find->offsetLimit($offset, $count) as $group) { $array[] = $group->getId(); + } - return (object) [ - "count" => $find->size(), - "items" => $this->getById(implode(',', $array), "", "is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200", $offset, $count) - /* - * As there is no thing as "fields" by the original documentation - * i'll just bake this param by the example shown here: https://dev.vk.com/method/groups.search - */ - ]; + if (!$array || sizeof($array) < 1) { + return $this->generateItems(0, []); + } + + return $this->generateItems($find->size(), $this->getById(implode(',', $array), "", $fields)); } - function join(int $group_id) + public function join(int $group_id) { $this->requireUser(); $this->willExecuteWriteAction(); - - $club = (new ClubsRepo)->get($group_id); - + + $club = (new ClubsRepo())->get($group_id); + $isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0; - if($isMember == 0) + if ($isMember == 0) { + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "groups.sub")) { + $this->failTooOften(); + } + $club->toggleSubscription($this->getUser()); + } return 1; } - function leave(int $group_id) + public function leave(int $group_id) { $this->requireUser(); $this->willExecuteWriteAction(); - - $club = (new ClubsRepo)->get($group_id); - + + $club = (new ClubsRepo())->get($group_id); + $isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0; - if($isMember == 1) + if ($isMember == 1) { $club->toggleSubscription($this->getUser()); + } return 1; } - function create(string $title, string $description = "", string $type = "group", int $public_category = 1, int $public_subcategory = 1, int $subtype = 1) - { + public function edit( + int $group_id, + string $title = null, + string $description = null, + string $screen_name = null, + string $website = null, + int $wall = -1, + int $topics = null, + int $adminlist = null, + int $topicsAboveWall = null, + int $hideFromGlobalFeed = null, + int $audio = null + ) { $this->requireUser(); $this->willExecuteWriteAction(); - $club = new Club; + $club = (new ClubsRepo())->get($group_id); - $club->setName($title); - $club->setAbout($description); - $club->setOwner($this->getUser()->getId()); - $club->save(); + if (!$club) { + $this->fail(15, "Access denied"); + } - $club->toggleSubscription($this->getUser()); + if (!$club || !$club->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - return $this->getById((string)$club->getId()); - } + if (!empty($screen_name) && !$club->setShortcode($screen_name)) { + $this->fail(103, "Invalid screen_name"); + } - function edit( - int $group_id, - string $title = NULL, - string $description = NULL, - string $screen_name = NULL, - string $website = NULL, - int $wall = NULL, - int $topics = NULL, - int $adminlist = NULL, - int $topicsAboveWall = NULL, - int $hideFromGlobalFeed = NULL) - { - $this->requireUser(); - $this->willExecuteWriteAction(); + !empty($title) ? $club->setName($title) : null; + !empty($description) ? $club->setAbout($description) : null; + !empty($screen_name) ? $club->setShortcode($screen_name) : null; + !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : null; - $club = (new ClubsRepo)->get($group_id); + try { + $wall != -1 ? $club->setWall($wall) : null; + } catch (\Exception $e) { + $this->fail(50, "Invalid wall value"); + } - if(!$club) $this->fail(203, "Club not found"); - if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); - if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode."); + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : null; + !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : null; + !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : null; - !is_null($title) ? $club->setName($title) : NULL; - !is_null($description) ? $club->setAbout($description) : NULL; - !is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; - !is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !is_null($wall) ? $club->setWall($wall) : NULL; - !is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; - !is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; - !is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - !is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + if (!$club->isHidingFromGlobalFeedEnforced()) { + !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : null; + } - $club->save(); + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : null; + + try { + $club->save(); + } catch (\TypeError $e) { + return 1; + } catch (\Exception $e) { + return 0; + } return 1; } - function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, int $count = 100, string $fields = "", string $filter = "any") + public function getMembers(int $group_id, int $offset = 0, int $count = 10, string $fields = "") { - # bdate,can_post,can_see_all_posts,can_see_audio,can_write_private_message,city,common_count,connections,contacts,country,domain,education,has_mobile,last_seen,lists,online,online_mobile,photo_100,photo_200,photo_200_orig,photo_400_orig,photo_50,photo_max,photo_max_orig,relation,relatives,schools,sex,site,status,universities - $club = (new ClubsRepo)->get((int) $group_id); - if(!$club) - $this->fail(125, "Invalid group id"); - - $sorter = "follower ASC"; - - switch($sort) { - default: - case "time_asc": - case "id_asc": - $sorter = "follower ASC"; - break; - case "time_desc": - case "id_desc": - $sorter = "follower DESC"; - break; + $this->requireUser(); + + $club = (new ClubsRepo())->get($group_id); + + if (!$club || !$club->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); } - $members = array_slice(iterator_to_array($club->getFollowers(1, $count, $sorter)), $offset); - $arr = (object) [ - "count" => count($members), - "items" => array()]; - - $filds = explode(",", $fields); - - $i = 0; - foreach($members as $member) { - if($i > $count) { - break; - } + $sort_string = "follower ASC"; + $members = array_slice(iterator_to_array($club->getFollowers(1, $count, $sort_string)), $offset, $count); - $arr->items[] = (object) [ - "id" => $member->getId(), - "first_name" => $member->getFirstName(), - "last_name" => $member->getLastName(), - ]; + $obj = (object) [ + "count" => sizeof($members), + "items" => [], + ]; - foreach($filds as $fild) { - switch($fild) { - case "bdate": - $arr->items[$i]->bdate = $member->getBirthday()->format('%e.%m.%Y'); - break; - case "can_post": - $arr->items[$i]->can_post = $club->canBeModifiedBy($member); - break; - case "can_see_all_posts": - $arr->items[$i]->can_see_all_posts = 1; - break; - case "can_see_audio": - $arr->items[$i]->can_see_audio = 0; - break; - case "can_write_private_message": - $arr->items[$i]->can_write_private_message = 0; - break; - case "common_count": - $arr->items[$i]->common_count = 420; - break; - case "connections": - $arr->items[$i]->connections = 1; - break; - case "contacts": - $arr->items[$i]->contacts = $member->getContactEmail(); - break; - case "country": - $arr->items[$i]->country = 1; - break; - case "domain": - $arr->items[$i]->domain = ""; - break; - case "education": - $arr->items[$i]->education = ""; - break; - case "has_mobile": - $arr->items[$i]->has_mobile = false; - break; - case "last_seen": - $arr->items[$i]->last_seen = $member->getOnline()->timestamp(); - break; - case "lists": - $arr->items[$i]->lists = ""; - break; - case "online": - $arr->items[$i]->online = $member->isOnline(); - break; - case "online_mobile": - $arr->items[$i]->online_mobile = $member->getOnlinePlatform() == "android" || $member->getOnlinePlatform() == "iphone" || $member->getOnlinePlatform() == "mobile"; - break; - case "photo_100": - $arr->items[$i]->photo_100 = $member->getAvatarURL("tiny"); - break; - case "photo_200": - $arr->items[$i]->photo_200 = $member->getAvatarURL("normal"); - break; - case "photo_200_orig": - $arr->items[$i]->photo_200_orig = $member->getAvatarURL("normal"); - break; - case "photo_400_orig": - $arr->items[$i]->photo_400_orig = $member->getAvatarURL("normal"); - break; - case "photo_max": - $arr->items[$i]->photo_max = $member->getAvatarURL("original"); - break; - case "photo_max_orig": - $arr->items[$i]->photo_max_orig = $member->getAvatarURL(); - break; - case "relation": - $arr->items[$i]->relation = $member->getMaritalStatus(); - break; - case "relatives": - $arr->items[$i]->relatives = 0; - break; - case "schools": - $arr->items[$i]->schools = 0; - break; - case "sex": - $arr->items[$i]->sex = $member->isFemale() ? 1 : 2; - break; - case "site": - $arr->items[$i]->site = $member->getWebsite(); - break; - case "status": - $arr->items[$i]->status = $member->getStatus(); - break; - case "universities": - $arr->items[$i]->universities = 0; - break; - } - } - $i++; + foreach ($members as $member) { + $obj->items[] = $member->toVkApiStruct($this->getUser(), $fields); } - return $arr; + + return $obj; } - function getSettings(string $group_id) + public function getSettings(string $group_id) { $this->requireUser(); - $club = (new ClubsRepo)->get((int)$group_id); - if(!$club || !$club->canBeModifiedBy($this->getUser())) - $this->fail(15, "You can't get settings of this group."); + $club = (new ClubsRepo())->get((int) $group_id); + + if (!$club || !$club->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } $arr = (object) [ "title" => $club->getName(), - "description" => $club->getDescription() != NULL ? $club->getDescription() : "", + "description" => $club->getDescription(), "address" => $club->getShortcode(), - "wall" => $club->canPost() == true ? 1 : 0, + "wall" => $club->getWallType(), # is different from vk values "photos" => 1, "video" => 0, - "audio" => 0, - "docs" => 0, + "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, + "docs" => 1, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, - "wiki" => 0, - "messages" => 0, - "obscene_filter" => 0, - "obscene_stopwords" => 0, - "obscene_words" => "", - "access" => 1, - "subject" => 1, - "subject_list" => [ - 0 => "в", - 1 => "опенвк", - 2 => "нет", - 3 => "категорий", - 4 => "групп", - ], - "rss" => "/club".$club->getId()."/rss", "website" => $club->getWebsite(), - "age_limits" => 0, - "market" => [], ]; return $arr; } - function isMember(string $group_id, int $user_id, string $user_ids = "", bool $extended = false) + public function isMember(string $group_id, int $user_id, int $extended = 0) { $this->requireUser(); - $id = $user_id != NULL ? $user_id : explode(",", $user_ids); - - if($group_id < 0) - $this->fail(228, "Remove the minus from group_id"); - $club = (new ClubsRepo)->get((int)$group_id); - $usver = (new UsersRepo)->get((int)$id); + $input_club = (new ClubsRepo())->get(abs((int) $group_id)); + $input_user = (new UsersRepo())->get(abs((int) $user_id)); - if(!$club || $group_id == 0) - $this->fail(203, "Invalid club"); + if (!$input_club || !$input_club->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - if(!$usver || $usver->isDeleted() || $user_id == 0) - $this->fail(30, "Invalid user"); + if (!$input_user || $input_user->isDeleted()) { + $this->fail(15, "Not found"); + } - if($extended == false) { - return $club->getSubscriptionStatus($usver) ? 1 : 0; + if ($extended == 0) { + return $input_club->getSubscriptionStatus($input_user) ? 1 : 0; } else { return (object) [ - "member" => $club->getSubscriptionStatus($usver) ? 1 : 0, + "member" => $input_club->getSubscriptionStatus($input_user) ? 1 : 0, "request" => 0, "invitation" => 0, "can_invite" => 0, - "can_recall" => 0 + "can_recall" => 0, ]; } } - - function remove(int $group_id, int $user_id) - { - $this->requireUser(); - - $this->fail(501, "Not implemented"); - } } diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php index 9501b4335..02b880b4a 100644 --- a/VKAPI/Handlers/Likes.php +++ b/VKAPI/Handlers/Likes.php @@ -1,71 +1,224 @@ -requireUser(); - $this->willExecuteWriteAction(); - - switch($type) { - case "post": - $post = (new PostsRepo)->getPostById($owner_id, $item_id); - if(is_null($post)) - $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); - - $post->setLike(true, $this->getUser()); - - return (object) [ - "likes" => $post->getLikesCount() - ]; - default: - $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); - } - } - - function delete(string $type, int $owner_id, int $item_id): object - { - $this->requireUser(); - $this->willExecuteWriteAction(); - - switch($type) { - case "post": - $post = (new PostsRepo)->getPostById($owner_id, $item_id); - if (is_null($post)) - $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); - - $post->setLike(false, $this->getUser()); - return (object) [ - "likes" => $post->getLikesCount() - ]; - default: - $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); - } - } - - function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object - { - $this->requireUser(); - - switch($type) { - case "post": - $user = (new UsersRepo)->get($user_id); - if (is_null($user)) - $this->fail(100, "One of the parameters specified was missing or invalid: user not found"); - - $post = (new PostsRepo)->getPostById($owner_id, $item_id); - if (is_null($post)) - $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); - - return (object) [ - "liked" => (int) $post->hasLikeFrom($user), - "copied" => 0 # TODO: handle this - ]; - default: - $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); - } - } -} +requireUser(); + $this->willExecuteWriteAction(); + + $postable = null; + switch ($type) { + case "post": + $post = (new PostsRepo())->getPostById($owner_id, $item_id); + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo())->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo())->getNoteById($owner_id, $item_id); + $postable = $note; + break; + default: + $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); + } + + if (is_null($postable) || $postable->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + } + + if (!$postable->canBeViewedBy($this->getUser() ?? null)) { + $this->fail(2, "Access to postable denied"); + } + + $postable->setLike(true, $this->getUser()); + + return (object) [ + "likes" => $postable->getLikesCount(), + ]; + } + + public function delete(string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $postable = null; + switch ($type) { + case "post": + $post = (new PostsRepo())->getPostById($owner_id, $item_id); + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo())->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo())->getNoteById($owner_id, $item_id); + $postable = $note; + break; + default: + $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); + } + + if (is_null($postable) || $postable->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + } + + if (!$postable->canBeViewedBy($this->getUser() ?? null)) { + $this->fail(2, "Access to postable denied"); + } + + if (!is_null($postable)) { + $postable->setLike(false, $this->getUser()); + + return (object) [ + "likes" => $postable->getLikesCount(), + ]; + } + } + + public function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); + + $user = (new UsersRepo())->get($user_id); + + if (is_null($user) || $user->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: user not found"); + } + + if (!$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + if ($user->isPrivateLikes()) { + return (object) [ + "liked" => 1, + "copied" => 1, + ]; + } + + $postable = null; + switch ($type) { + case "post": + $post = (new PostsRepo())->getPostById($owner_id, $item_id); + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo())->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo())->getNoteById($owner_id, $item_id); + $postable = $note; + break; + default: + $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); + } + + if (is_null($postable) || $postable->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + } + + if (!$postable->canBeViewedBy($this->getUser())) { + $this->fail(665, "Access to postable denied"); + } + + return (object) [ + "liked" => (int) $postable->hasLikeFrom($user), + "copied" => 0, + ]; + } + + public function getList(string $type, int $owner_id, int $item_id, bool $extended = false, int $offset = 0, int $count = 10, bool $skip_own = false) + { + $this->requireUser(); + + $object = null; + + switch ($type) { + case "post": + $object = (new PostsRepo())->getPostById($owner_id, $item_id); + break; + case "comment": + $object = (new CommentsRepo())->get($item_id); + break; + case "photo": + $object = (new PhotosRepo())->getByOwnerAndVID($owner_id, $item_id); + break; + case "video": + $object = (new VideosRepo())->getByOwnerAndVID($owner_id, $item_id); + break; + default: + $this->fail(58, "Invalid type"); + break; + } + + if (!$object || $object->isDeleted()) { + $this->fail(56, "Invalid postable"); + } + + if (!$object->canBeViewedBy($this->getUser())) { + $this->fail(665, "Access to postable denied"); + } + + $res = (object) [ + "count" => $object->getLikesCount(), + "items" => [], + ]; + + $likers = array_slice(iterator_to_array($object->getLikers(1, $offset + $count)), $offset); + + foreach ($likers as $liker) { + if ($skip_own && $liker->getId() == $this->getUser()->getId()) { + continue; + } + + if (!$extended) { + $res->items[] = $liker->getId(); + } else { + $res->items[] = $liker->toVkApiStruct(null, 'photo_50'); + } + } + + return $res; + } +} diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index bc9035f53..51de9b4b9 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -1,6 +1,10 @@ - 0) - return NULL; - + if ($user_id === -1) { + if ($peer_id === -1) { + return null; + } elseif ($peer_id < 0) { + return null; + } elseif (($peer_id - 2000000000) > 0) { + return null; + } + return $peer_id; } - + return $user_id; } - - function getById(string $message_ids, int $preview_length = 0, int $extended = 0): object + + public function getById(string $message_ids, int $preview_length = 0, int $extended = 0): object { $this->requireUser(); - - $msgs = new MSGRepo; + + $msgs = new MSGRepo(); $ids = preg_split("%, ?%", $message_ids); $items = []; - foreach($ids as $id) { + foreach ($ids as $id) { $message = $msgs->get((int) $id); - if(!$message) + if (!$message) { continue; - else if($message->getSender()->getId() !== $this->getUser()->getId() && $message->getRecipient()->getId() !== $this->getUser()->getId()) + } elseif ($message->getSender()->getId() !== $this->getUser()->getId() && $message->getRecipient()->getId() !== $this->getUser()->getId()) { continue; - + } + $author = $message->getSender()->getId() === $this->getUser()->getId() ? $message->getRecipient()->getId() : $message->getSender()->getId(); - $rMsg = new APIMsg; - + $rMsg = new APIMsg(); + $rMsg->id = $message->getId(); $rMsg->user_id = $author; $rMsg->from_id = $message->getSender()->getId(); @@ -51,172 +57,225 @@ function getById(string $message_ids, int $preview_length = 0, int $extended = 0 $rMsg->body = $message->getText(false); $rMsg->text = $message->getText(false); $rMsg->emoji = true; - - if($preview_length > 0) + + if ($preview_length > 0) { $rMsg->body = ovk_proc_strtr($rMsg->body, $preview_length); - $rMsg->text = ovk_proc_strtr($rMsg->text, $preview_length); - + } + $rMsg->text = ovk_proc_strtr($rMsg->text, $preview_length); + $items[] = $rMsg; } - + return (object) [ "count" => sizeof($items), "items" => $items, ]; } - - function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0) - { + + public function send( + int $user_id = -1, + int $peer_id = -1, + string $domain = "", + int $chat_id = -1, + string $user_ids = "", + string $message = "", + int $sticker_id = -1, + int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0, + string $attachment = "" + ) { # интересно почему не attachments $this->requireUser(); $this->willExecuteWriteAction(); - if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0) - { + if ($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0) { $this->getUser()->updOnline($this->getPlatform()); } - - if($chat_id !== -1) + + if ($chat_id !== -1) { $this->fail(946, "Chats are not implemented"); - else if($sticker_id !== -1) + } elseif ($sticker_id !== -1) { $this->fail(-151, "Stickers are not implemented"); - else if(empty($message)) + } + + if (empty($message) && empty($attachment)) { $this->fail(100, "Message text is empty or invalid"); - + } + # lol recursion - if(!empty($user_ids)) { + if (!empty($user_ids)) { $rIds = []; $ids = preg_split("%, ?%", $user_ids); - if(sizeof($ids) > 100) + if (sizeof($ids) > 100) { $this->fail(913, "Too many recipients"); - - foreach($ids as $id) + } + + foreach ($ids as $id) { $rIds[] = $this->send(-1, $id, "", -1, "", $message); - + } + return $rIds; } - - if(!empty($domain)) { - $peer = (new USRRepo)->getByShortCode($domain); + + if (!empty($domain)) { + $peer = (new USRRepo())->getByShortCode($domain); } else { $peer = $this->resolvePeer($user_id, $peer_id); - $peer = (new USRRepo)->get($peer); + $peer = (new USRRepo())->get($peer); } - - if(!$peer) + + if (!$peer) { $this->fail(936, "There is no peer with this id"); - - if($this->getUser()->getId() !== $peer->getId() && !$peer->getPrivacyPermission('messages.write', $this->getUser())) + } + + 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! $chat = new Correspondence($this->getUser(), $peer); - $msg = new Message; + $msg = new Message(); $msg->setContent($message); - + $msg = $chat->sendMessage($msg, true); - if(!$msg) + if (!$msg) { $this->fail(950, "Internal error"); - else - return $msg->getId(); + } elseif (!empty($attachment)) { + $attachs = parseAttachments($attachment); + + # Работают только фотки, остальное просто не будет отображаться. + if (sizeof($attachs) >= 10) { + $this->fail(15, "Too many attachments"); + } + + foreach ($attachs as $attach) { + if ($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId()) { + $msg->attach($attach); + } else { + $this->fail(52, "One of the attachments is invalid"); + } + } + } + + return $msg->getId(); } - - function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object + + public function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object { $this->requireUser(); $this->willExecuteWriteAction(); - - $msgs = new MSGRepo; + + $msgs = new MSGRepo(); $ids = preg_split("%, ?%", $message_ids); $items = []; - foreach($ids as $id) { + 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; } - + return (object) $items; } - - function restore(int $message_id): int + + public function restore(int $message_id): int { $this->requireUser(); $this->willExecuteWriteAction(); - - $msg = (new MSGRepo)->get($message_id); - if(!$msg) + + $msg = (new MSGRepo())->get($message_id); + if (!$msg) { return 0; - else if($msg->getSender()->getId() !== $this->getUser()->getId()) + } elseif ($msg->getSender()->getId() !== $this->getUser()->getId()) { return 0; - + } + $msg->undelete(); return 1; } - - function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0, string $fields = ""): object + + public function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 1, string $fields = ""): object { $this->requireUser(); - - $convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset); - $convosCount = (new MSGRepo)->getCorrespondenciesCount($this->getUser()); + + $convos = (new MSGRepo())->getCorrespondencies($this->getUser(), -1, $count, $offset); + $convosCount = (new MSGRepo())->getCorrespondenciesCount($this->getUser()); $list = []; $users = []; - foreach($convos as $convo) { + foreach ($convos as $convo) { $correspondents = $convo->getCorrespondents(); - if($correspondents[0]->getId() === $this->getUser()->getId()) + if ($correspondents[0]->getId() == $this->getUser()->getId()) { $peer = $correspondents[1]; - else + } else { $peer = $correspondents[0]; - + } + $lastMessage = $convo->getPreviewMessage(); - - $listConvo = new APIConvo; + + $listConvo = new APIConvo(); $listConvo->peer = [ "id" => $peer->getId(), "type" => "user", "local_id" => $peer->getId(), ]; - - $canWrite = $peer->getSubscriptionStatus($this->getUser()) === 3; - $listConvo->can_write = [ - "allowed" => $canWrite, + + if ($peer->getPrivacyPermission('messages.write', $this->getUser())) { + $listConvo->can_write = [ + "allowed" => true, + ]; + } else { + $listConvo->can_write = [ + "allowed" => false, + "reason" => 901, + ]; + } + $listConvo->chat_settings = (object) [ + "title" => "", + "active_ids" => [], ]; - - $lastMessagePreview = NULL; - if(!is_null($lastMessage)) { + + $lastMessagePreview = null; + if (!is_null($lastMessage)) { $listConvo->last_message_id = $lastMessage->getId(); - - if($lastMessage->getSender()->getId() === $this->getUser()->getId()) - $author = $lastMessage->getRecipient()->getId(); - else - $author = $lastMessage->getSender()->getId(); - - $lastMessagePreview = new APIMsg; + if ($lastMessage->isUnread()) { + $listConvo->unread_count = 1; + } + + $listConvo->in_read = $convo->getLastReadedMessage($peer->getId())?->getId() ?? 0; + $listConvo->out_read = $convo->getLastReadedMessage($this->getUser()->getId())?->getId() ?? 0; + + + $author = $lastMessage->getSender()->getId(); + + $lastMessagePreview = new APIMsg(); $lastMessagePreview->id = $lastMessage->getId(); - $lastMessagePreview->user_id = $author; - $lastMessagePreview->from_id = $lastMessage->getSender()->getId(); + $lastMessagePreview->from_id = $author == $this->getUser()->getId() ? $this->getUser()->getId() : $peer->getId(); + if (VKAPI_DECL_VER_MAJOR >= 5 && VKAPI_DECL_VER_MINOR >= 80) { + $lastMessagePreview->peer_id = $peer->getId(); + } else { + $lastMessagePreview->user_id = $peer->getId(); + $lastMessagePreview->read_state = (int) !$lastMessage->isUnread(); + } $lastMessagePreview->date = $lastMessage->getSendTime()->timestamp(); - $lastMessagePreview->read_state = 1; - $lastMessagePreview->out = (int) ($lastMessage->getSender()->getId() === $this->getUser()->getId()); + $lastMessagePreview->out = (int) ($author == $this->getUser()->getId()); $lastMessagePreview->body = $lastMessage->getText(false); $lastMessagePreview->text = $lastMessage->getText(false); $lastMessagePreview->emoji = true; - - if($extended == 1) { - $users[] = $author; + + if ($extended == 1) { + $users[] = $peer->getId(); } } - + $list[] = [ "conversation" => $listConvo, "last_message" => $lastMessagePreview, ]; } - - if($extended == 0){ + + if ($extended == 0) { return (object) [ "count" => $convosCount, "items" => $list, @@ -228,12 +287,13 @@ function getConversations(int $offset = 0, int $count = 20, string $filter = "al return (object) [ "count" => $convosCount, "items" => $list, - "profiles" => (!empty($users) ? (new APIUsers)->get(implode(',', $users), $fields, 0, $count+1) : []) + "profiles" => (!empty($users) ? (new APIUsers())->get(implode(',', $users), $fields . ',photo_50,photo_100,photo_200', 0, $count + 1) : []), + "groups" => [], ]; } } - function getConversationsById(string $peer_ids, int $extended = 0, string $fields = "") + public function getConversationsById(string $peer_ids, int $extended = 0, string $fields = "") { $this->requireUser(); @@ -241,21 +301,23 @@ function getConversationsById(string $peer_ids, int $extended = 0, string $field $output = [ "count" => 0, - "items" => [] + "items" => [], ]; $userslist = []; - foreach($peers as $peer) { - if(key($peers) > 100) + foreach ($peers as $peer) { + if (key($peers) > 100) { continue; + } - if(is_null($user_id = $this->resolvePeer((int) $peer))) + if (is_null($user_id = $this->resolvePeer((int) $peer))) { $this->fail(-151, "Chats are not implemented"); + } - $user = (new USRRepo)->get((int) $peer); + $user = (new USRRepo())->get((int) $peer); - if($user) { + if ($user) { $dialogue = new Correspondence($this->getUser(), $user); $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false); $msg = $iterator[0]->unwrap(); // шоб удобнее было @@ -263,7 +325,7 @@ function getConversationsById(string $peer_ids, int $extended = 0, string $field "peer" => [ "id" => $user->getId(), "type" => "user", - "local_id" => $user->getId() + "local_id" => $user->getId(), ], "last_message_id" => $msg->id, "in_read" => $msg->id, @@ -275,46 +337,52 @@ function getConversationsById(string $peer_ids, int $extended = 0, string $field "last_conversation_message_id" => $user->getId(), "in_read_cmid" => $user->getId(), "out_read_cmid" => $user->getId(), - "is_marked_unread" => $iterator[0]->isUnread(), + "is_marked_unread" => 0, "important" => false, // целестора когда релиз "can_write" => [ - "allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true) - ] + "allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true), + ], ]; $userslist[] = $user->getId(); } } - if($extended == 1) { + if ($extended == 1) { $userslist = array_unique($userslist); - $output['profiles'] = (!empty($userslist) ? (new APIUsers)->get(implode(',', $userslist), $fields) : []); + $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 + + public 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(); - - if(is_null($user_id = $this->resolvePeer($user_id, $peer_id))) + + if (is_null($user_id = $this->resolvePeer($user_id, $peer_id))) { $this->fail(-151, "Chats are not implemented"); - - $peer = (new USRRepo)->get($user_id); - if(!$peer) + } + + $peer = (new USRRepo())->get($user_id); + if (!$peer) { $this->fail(1, "ошибка про то что пира нет"); - + } + $results = []; $dialogue = new Correspondence($this->getUser(), $peer); $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, $start_message_id, $count, abs($offset), $rev === 1); - foreach($iterator as $message) { + foreach ($iterator as $message) { $msgU = $message->unwrap(); # Why? As of OpenVK 2 Public Preview Two database layer doesn't work correctly and refuses to cache entities. - # UPDATE: the issue seems to be caused by debug mode and json_encode (bruh_encode). ~~Dorothy - - $rMsg = new APIMsg; + # UPDATE: the issue seems to be caused by debug mode and json_encode (bruh_encode). ~~Dorothy + + $rMsg = new APIMsg(); $rMsg->id = $msgU->id; - $rMsg->user_id = $msgU->sender_id === $this->getUser()->getId() ? $msgU->recipient_id : $msgU->sender_id; + if (VKAPI_DECL_VER_MAJOR >= 5 && VKAPI_DECL_VER_MINOR >= 38) { + $rMsg->peer_id = $msgU->sender_id == $this->getUser()->getId() ? $msgU->recipient_id : $msgU->sender_id; + } else { + $rMsg->user_id = $msgU->sender_id == $this->getUser()->getId() ? $msgU->recipient_id : $msgU->sender_id; + } $rMsg->from_id = $msgU->sender_id; $rMsg->date = $msgU->created; $rMsg->read_state = 1; @@ -322,15 +390,29 @@ function getHistory(int $offset = 0, int $count = 20, int $user_id = -1, int $pe $rMsg->body = $message->getText(false); $rMsg->text = $message->getText(false); $rMsg->emoji = true; - + $results[] = $rMsg; } - + $output = [ "count" => sizeof($results), "items" => $results, + "conversations" => [ + (object) [ + "peer" => (object) [ + "id" => $user_id, + "local_id" => $user_id, + "type" => "user", + ], + "last_message_id" => $dialogue->getPreviewMessage()->getId(), + ], + ], ]; + + $output['conversations'][0]->in_read = $dialogue->getLastReadedMessage($peer->getId())?->getId() ?? 0; // TODO: check if it's read + $output['conversations'][0]->out_read = $dialogue->getLastReadedMessage($this->getUser()->getId())?->getId() ?? 0; + if ($extended == 1) { $users[] = $this->getUser()->getId(); $users[] = $user_id; @@ -339,58 +421,145 @@ function getHistory(int $offset = 0, int $count = 20, int $user_id = -1, int $pe return (object) $output; } - - function getLongPollHistory(int $ts = -1, int $preview_length = 0, int $events_limit = 1000, int $msgs_limit = 1000): object + + public function getLongPollHistory(int $ts = -1, int $preview_length = 0, int $events_limit = 1000, int $msgs_limit = 1000): object { $this->requireUser(); - + $res = [ "history" => [], "messages" => [], "profiles" => [], "new_pts" => 0, ]; - + $manager = SignalManager::i(); - $events = $manager->getHistoryFor($this->getUser()->getId(), $ts === -1 ? NULL : $ts, min($events_limit, $msgs_limit)); - foreach($events as $event) { - if(!($event instanceof NewMessageEvent)) + $events = $manager->getHistoryFor($this->getUser()->getId(), $ts === -1 ? null : $ts, min($events_limit, $msgs_limit)); + foreach ($events as $event) { + if (!($event instanceof NewMessageEvent)) { continue; - + } + $message = $this->getById((string) $event->getLongPoolSummary()->message["uuid"], $preview_length, 1)->items[0]; - if(!$message) + if (!$message) { continue; - + } + $res["messages"][] = $message; $res["history"][] = $event->getVKAPISummary($this->getUser()->getId()); } - + $res["messages"] = [ "count" => sizeof($res["messages"]), "items" => $res["messages"], ]; return (object) $res; } - - function getLongPollServer(int $need_pts = 1, int $lp_version = 3, ?int $group_id = NULL): array + + public function getLongPollServer(int $need_pts = 1, int $lp_version = 3, ?int $group_id = null): array { $this->requireUser(); - - if($group_id > 0) + + if ($group_id > 0) { $this->fail(-151, "Not implemented"); - - $url = "http" . (ovk_is_ssl() ? "s" : "") . "://$_SERVER[HTTP_HOST]/nim" . $this->getUser()->getId(); + } + + $url = "$_SERVER[HTTP_HOST]/nim" . $this->getUser()->getId(); + + if (VKAPI_DECL_VER_MINOR == 9999) { + $url = ovk_scheme(true) . $url; + } + $key = openssl_random_pseudo_bytes(8); - $key = bin2hex($key) . bin2hex($key ^ ( ~CHANDLER_ROOT_CONF["security"]["secret"] | ((string) $this->getUser()->getId()) )); + $key = bin2hex($key) . bin2hex($key ^ (~CHANDLER_ROOT_CONF["security"]["secret"] | ((string) $this->getUser()->getId()))); $res = [ "key" => $key, "server" => $url, "ts" => time(), ]; - - if($need_pts === 1) + + if ($need_pts === 1) { $res["pts"] = -1; - + } + return $res; } + + public function edit(int $message_id, string $message = "", string $attachment = "", int $peer_id = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $msg = (new MSGRepo())->get($message_id); + + if (empty($message) && empty($attachment)) { + $this->fail(100, "Required parameter 'message' missing."); + } + + if (!$msg || $msg->isDeleted()) { + $this->fail(102, "Invalid message"); + } + + if ($msg->getSender()->getId() != $this->getUser()->getId()) { + $this->fail(15, "Access to message denied"); + } + + if (!empty($message)) { + $msg->setContent($message); + } + + $msg->setEdited(time()); + $msg->save(true); + + if (!empty($attachment)) { + $attachs = parseAttachments($attachment); + $newAttachmentsCount = sizeof($attachs); + + $postsAttachments = iterator_to_array($msg->getChildren()); + + if (sizeof($postsAttachments) >= 10) { + $this->fail(15, "Message have too many attachments"); + } + + if (($newAttachmentsCount + sizeof($postsAttachments)) > 10) { + $this->fail(158, "Message will have too many attachments"); + } + + foreach ($attachs as $attach) { + if ($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId()) { + $msg->attach($attach); + } else { + $this->fail(52, "One of the attachments is invalid"); + } + } + } + + return 1; + } + + public function setActivity(int $user_id = 0, string $type = "typing", int $peer_id = 0) + { + if (empty($user_id) && empty($peer_id)) { + $this->fail(100, "One of the parameters specified was missing or invalid: user_id or peer_id"); + } elseif (empty($peer_id)) { + $peer_id = $user_id; + } + + $peer = $this->resolvePeer($peer_id, $peer_id); + $peer = (new USRRepo())->get($peer); + + if (!$peer) { + $this->fail(936, "There is no peer with this id"); + } + + $chat = new Correspondence($this->getUser(), $peer); + + switch ($type) { + case "typing": + return (int) $chat->sendTypingEvent(); + break; + default: + $this->fail(1, "Not implemented"); + } + } } diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index d99924304..8f0d327b5 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -1,76 +1,327 @@ -requireUser(); - - if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0) - { - $this->getUser()->updOnline($this->getPlatform()); - } - - $id = $this->getUser()->getId(); - $subs = DatabaseConnection::i() - ->getContext() - ->table("subscriptions") - ->where("follower", $id); - $ids = array_map(function($rel) { - return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1); - }, iterator_to_array($subs)); - $ids[] = $this->getUser()->getId(); - - $posts = DatabaseConnection::i() - ->getContext() - ->table("posts") - ->select("id") - ->where("wall IN (?)", $ids) - ->where("deleted", 0) - ->where("id < (?)", empty($start_from) ? PHP_INT_MAX : $start_from) - ->where("? <= created", empty($start_time) ? 0 : $start_time) - ->where("? >= created", empty($end_time) ? PHP_INT_MAX : $end_time) - ->order("created DESC"); - - $rposts = []; - foreach($posts->page((int) ($offset + 1), $count) as $post) - $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); - - $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); - $response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно) - - return $response; - } - - 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) - { - $this->requireUser(); - - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0"; - - if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) - $queryBase .= " AND `nsfw` = 0"; - - $start_from = empty($start_from) ? PHP_INT_MAX : $start_from; - $start_time = empty($start_time) ? 0 : $start_time; - $end_time = empty($end_time) ? PHP_INT_MAX : $end_time; - $posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " AND `posts`.`id` <= " . $start_from . " AND " . $start_time . " <= `posts`.`created` AND `posts`.`created` <= " . $end_time . " ORDER BY `created` DESC LIMIT " . $count . " OFFSET " . $offset); - - $rposts = []; - $ids = []; - foreach($posts as $post) { - $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); - $ids[] = $post->id; - } - - $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); - $response->next_from = end($ids); - - return $response; - } -} +requireUser(); + + if ($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0) { + $this->getUser()->updOnline($this->getPlatform()); + } + + [$cursorTime, $cursorId] = $this->parseCursor($start_from); + + $id = $this->getUser()->getId(); + $subs = DatabaseConnection::i() + ->getContext() + ->table("subscriptions") + ->where("follower", $id); + $ids = array_map(function ($rel) { + return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1); + }, iterator_to_array($subs)); + $ids[] = $this->getUser()->getId(); + + $posts = DatabaseConnection::i() + ->getContext() + ->table("posts") + ->select("id, created") + ->where("wall IN (?)", $ids) + ->where("deleted", 0) + ->where("suggested", 0) + ->where("created <= ?", $cursorTime) + ->where("created < ? OR id < ?", $cursorTime, $cursorId) + ->where("? <= created", empty($start_time) ? 0 : $start_time) + ->where("? >= created", empty($end_time) ? PHP_INT_MAX : $end_time) + ->order("created DESC, id DESC"); + + if ($with_alien_wall_posts == 0) { + $posts->where("(`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`)"); + } + + $rposts = []; + $lastPost = null; + foreach ($posts->page((int) ($offset + 1), $count) as $post) { + $rposts[] = (new PostsRepo())->get($post->id)->getPrettyId(); + $lastPost = $post; + } + + $response = (new Wall())->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); + + if ($lastPost) { + $response->next_from = "{$lastPost->created}_{$lastPost->id}"; + } + + foreach ($response->items as $post) { + $post->type = "post"; + $post->source_id = $post->owner_id; + } + + return $response; + } + + public function getGlobal(string $fields = "", string $start_from = "", int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 1, int $rss = 0, int $return_banned = 0, int $with_alien_wall_posts = 0) + { + $this->requireUser(); + + [$cursorTime, $cursorId] = $this->parseCursor($start_from); + + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; + $queryBase .= " WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; + + if ($with_alien_wall_posts == 0) { + $queryBase .= " AND ((`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`))"; + } + + if ($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) { + $queryBase .= " AND `nsfw` = 0"; + } + + if ($return_banned == 0) { + $ignored_sources_ids = $this->getUser()->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); + + if (sizeof($ignored_sources_ids) > 0) { + $imploded_ids = implode("', '", $ignored_sources_ids); + $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; + } + } + + $start_time = empty($start_time) ? 0 : $start_time; + $end_time = empty($end_time) ? PHP_INT_MAX : $end_time; + + $cursorFilter = " AND (`posts`.`created` < {$cursorTime} OR (`posts`.`created` = {$cursorTime} AND `posts`.`id` < {$cursorId}))"; + + $posts = DatabaseConnection::i()->getConnection()->query( + "SELECT `posts`.`id`, `posts`.`created` " . $queryBase . + $cursorFilter . + " AND " . $start_time . " <= `posts`.`created` AND `posts`.`created` <= " . $end_time . + " ORDER BY `created` DESC, `id` DESC LIMIT " . $count . " OFFSET " . $offset + ); + + $rposts = []; + $lastPost = null; + if ($rss == 1) { + $channel = new \Bhaktaraz\RSSGenerator\Channel(); + $channel->title("Global Feed — " . OPENVK_ROOT_CONF['openvk']['appearance']['name']) + ->description('OVK Global feed') + ->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/feed/all"); + + foreach ($posts as $item) { + $post = (new PostsRepo())->get($item->id); + if (!$post || $post->isDeleted()) { + continue; + } + + $output = $post->toRss(); + $output->appendTo($channel); + } + + return $channel; + } + + foreach ($posts as $post) { + $rposts[] = (new PostsRepo())->get($post->id)->getPrettyId(); + $lastPost = $post; + } + + $response = (new Wall())->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); + + if ($lastPost) { + $response->next_from = "{$lastPost->created}_{$lastPost->id}"; + } + + foreach ($response->items as $post) { + $post->type = "post"; + $post->source_id = $post->owner_id; + } + + return $response; + } + + public function getRecommended(string $fields = "", string $start_from = "", int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 1, int $rss = 0, int $return_banned = 0) + { + // getGlobal alias + return $this->getGlobal($fields, $start_from, $start_time, $end_time, $offset, $count, $extended, $rss, $return_banned); + } + + public function getByType(string $feed_type = 'top', string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $return_banned = 0) + { + $this->requireUser(); + + switch ($feed_type) { + case 'top': + return $this->getGlobal($fields, $start_from, $start_time, $end_time, $offset, $count, $extended, $return_banned); + break; + default: + return $this->get($fields, $start_from, $start_time, $end_time, $offset, $count, $extended); + break; + } + } + + public function getBanned(int $extended = 0, string $fields = "", string $name_case = "nom", int $merge = 0): object + { + $this->requireUser(); + + $offset = 0; + $count = OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50; + $banned = $this->getUser()->getIgnoredSources($offset, $count, ($extended != 1)); + $return_object = (object) [ + 'groups' => [], + 'members' => [], + ]; + + if ($extended == 0) { + foreach ($banned as $ban) { + if ($ban > 0) { + $return_object->members[] = $ban; + } else { + $return_object->groups[] = $ban; + } + } + } else { + if ($merge == 1) { + $return_object = (object) [ + 'count' => sizeof($banned), + 'items' => [], + ]; + + foreach ($banned as $ban) { + $return_object->items[] = $ban->toVkApiStruct($this->getUser(), $fields); + } + } else { + $return_object = (object) [ + 'groups' => [], + 'profiles' => [], + ]; + + foreach ($banned as $ban) { + if ($ban->getRealId() > 0) { + $return_object->profiles[] = $ban->toVkApiStruct($this->getUser(), $fields); + } else { + $return_object->groups[] = $ban->toVkApiStruct($this->getUser(), $fields); + } + } + } + } + + return $return_object; + } + + public function addBan(string $user_ids = "", string $group_ids = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + # Formatting input ids + if (!empty($user_ids)) { + $user_ids = array_map(function ($el) { + return (int) $el; + }, explode(',', $user_ids)); + $user_ids = array_unique($user_ids); + } else { + $user_ids = []; + } + + if (!empty($group_ids)) { + $group_ids = array_map(function ($el) { + return abs((int) $el) * -1; + }, explode(',', $group_ids)); + $group_ids = array_unique($group_ids); + } else { + $group_ids = []; + } + + $ids = array_merge($user_ids, $group_ids); + if (sizeof($ids) < 1) { + return 0; + } + + if (sizeof($ids) > 10) { + $this->fail(-10, "Limit of 'ids' is 10"); + } + + $config_limit = OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50; + $user_ignores = $this->getUser()->getIgnoredSourcesCount(); + if (($user_ignores + sizeof($ids)) > $config_limit) { + $this->fail(-50, "Ignoring limit exceeded"); + } + + $entities = get_entities($ids); + $successes = 0; + foreach ($entities as $entity) { + if (!$entity || $entity->getRealId() === $this->getUser()->getRealId() || $entity->isHideFromGlobalFeedEnabled() || $entity->isIgnoredBy($this->getUser())) { + continue; + } + + $entity->addIgnore($this->getUser()); + $successes += 1; + } + + return 1; + } + + public function deleteBan(string $user_ids = "", string $group_ids = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if (!empty($user_ids)) { + $user_ids = array_map(function ($el) { + return (int) $el; + }, explode(',', $user_ids)); + $user_ids = array_unique($user_ids); + } else { + $user_ids = []; + } + + if (!empty($group_ids)) { + $group_ids = array_map(function ($el) { + return abs((int) $el) * -1; + }, explode(',', $group_ids)); + $group_ids = array_unique($group_ids); + } else { + $group_ids = []; + } + + $ids = array_merge($user_ids, $group_ids); + if (sizeof($ids) < 1) { + return 0; + } + + if (sizeof($ids) > 10) { + $this->fail(-10, "Limit of ids is 10"); + } + + $entities = get_entities($ids); + $successes = 0; + foreach ($entities as $entity) { + if (!$entity || $entity->getRealId() === $this->getUser()->getRealId() || !$entity->isIgnoredBy($this->getUser())) { + continue; + } + + $entity->removeIgnore($this->getUser()); + $successes += 1; + } + + return 1; + } +} diff --git a/VKAPI/Handlers/Notes.php b/VKAPI/Handlers/Notes.php index 7c9c9fec4..57ac0f752 100644 --- a/VKAPI/Handlers/Notes.php +++ b/VKAPI/Handlers/Notes.php @@ -1,5 +1,9 @@ -requireUser(); $this->willExecuteWriteAction(); - $note = new Note; + if (empty($title)) { + $this->fail(100, "Required parameter 'title' missing."); + } + + $note = new Note(); + $note->setOwner($this->getUser()->getId()); $note->setCreated(time()); $note->setName($title); $note->setSource($text); $note->setEdited(time()); + $note->save(); return $note->getVirtualId(); } - function createComment(string $note_id, int $owner_id, string $message, int $reply_to = 0, string $attachments = "") + public function createComment(int $note_id, int $owner_id, string $message, string $attachments = "") { $this->requireUser(); $this->willExecuteWriteAction(); - $note = (new NotesRepo)->getNoteById((int)$owner_id, (int)$note_id); - if(!$note) - $this->fail(180, "Note not found"); - - if($note->isDeleted()) - $this->fail(189, "Note is deleted"); - - if($note->getOwner()->isDeleted()) - $this->fail(403, "Owner is deleted"); + if (empty($message)) { + $this->fail(100, "Required parameter 'message' missing."); + } - if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) - $this->fail(43, "No access"); + $note = (new NotesRepo())->getNoteById($owner_id, $note_id); - if(empty($message) && empty($attachments)) - $this->fail(100, "Required parameter 'message' missing."); + if (!$note) { + $this->fail(15, "Access denied"); + } - $comment = new Comment; + if ($note->isDeleted()) { + $this->fail(15, "Access denied"); + } + + if ($note->getOwner()->isDeleted()) { + $this->fail(15, "Access denied"); + } + + if (!$note->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + if (!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) { + $this->fail(15, "Access denied"); + } + + $comment = new Comment(); $comment->setOwner($this->getUser()->getId()); $comment->setModel(get_class($note)); $comment->setTarget($note->getId()); @@ -54,230 +74,176 @@ function createComment(string $note_id, int $owner_id, string $message, int $rep $comment->setCreated(time()); $comment->save(); - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $comment->attach($attacc); - } - } - } - return $comment->getId(); } - function delete(string $note_id) + public function delete(int $note_id) { $this->requireUser(); $this->willExecuteWriteAction(); - $note = (new NotesRepo)->get((int)$note_id); - - if(!$note) - $this->fail(180, "Note not found"); - - if(!$note->canBeModifiedBy($this->getUser())) - $this->fail(15, "Access to note denied"); - - $note->delete(); - - return 1; - } + $note = (new NotesRepo())->getNoteById($this->getUser()->getId(), $note_id); - function deleteComment(int $comment_id, int $owner_id = 0) - { - $this->requireUser(); - $this->willExecuteWriteAction(); + if (!$note) { + $this->fail(15, "Access denied"); + } - $comment = (new CommentsRepo)->get($comment_id); + if ($note->isDeleted()) { + $this->fail(15, "Access denied"); + } - if(!$comment || !$comment->canBeDeletedBy($this->getUser())) - $this->fail(403, "Access to comment denied"); + if (!$note->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - $comment->delete(); + $note->delete(); return 1; } - function edit(string $note_id, string $title = "", string $text = "", int $privacy = 0, int $comment_privacy = 0, string $privacy_view = "", string $privacy_comment = "") + public function edit(string $note_id, string $title = "", string $text = "", int $privacy = 0, int $comment_privacy = 0, string $privacy_view = "", string $privacy_comment = "") { $this->requireUser(); $this->willExecuteWriteAction(); - $note = (new NotesRepo)->getNoteById($this->getUser()->getId(), (int)$note_id); + $note = (new NotesRepo())->getNoteById($this->getUser()->getId(), (int) $note_id); + + if (!$note) { + $this->fail(15, "Access denied"); + } - if(!$note) - $this->fail(180, "Note not found"); - - if($note->isDeleted()) - $this->fail(189, "Note is deleted"); + if ($note->isDeleted()) { + $this->fail(15, "Access denied"); + } - if(!$note->canBeModifiedBy($this->getUser())) - $this->fail(403, "No access"); + if (!$note->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - !empty($title) ? $note->setName($title) : NULL; - !empty($text) ? $note->setSource($text) : NULL; + !empty($title) ? $note->setName($title) : null; + !empty($text) ? $note->setSource($text) : null; - $note->setCached_Content(NULL); + $note->setCached_Content(null); $note->setEdited(time()); $note->save(); return 1; } - function editComment(int $comment_id, string $message, int $owner_id = NULL) + public function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = 10, int $sort = 0) { - /* $this->requireUser(); - $this->willExecuteWriteAction(); - $comment = (new CommentsRepo)->get($comment_id); + $user = (new UsersRepo())->get($user_id); - if($comment->getOwner() != $this->getUser()->getId()) - $this->fail(15, "Access to comment denied"); - - $comment->setContent($message); - $comment->setEdited(time()); - $comment->save(); - */ - - return 1; - } + if (!$user || $user->isDeleted()) { + $this->fail(15, "Access denied"); + } - function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = 10, int $sort = 0) - { - $this->requireUser(); - $user = (new UsersRepo)->get($user_id); - - if(!$user || $user->isDeleted()) - $this->fail(15, "Invalid user"); - - if(!$user->getPrivacyPermission('notes.read', $this->getUser())) - $this->fail(43, "Access denied: this user chose to hide his notes"); - - if(empty($note_ids)) { - $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()) continue; - - $nodez->notes[] = $note->toVkApiStruct(); + if (!$user->getPrivacyPermission('notes.read', $this->getUser())) { + $this->fail(15, "Access denied"); + } + + if (!$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + $notes_return_object = (object) [ + "count" => 0, + "items" => [], + ]; + + if (empty($note_ids)) { + $notes_return_object->count = (new NotesRepo())->getUserNotesCount($user); + + $notes = array_slice(iterator_to_array((new NotesRepo())->getUserNotes($user, 1, $count + $offset, $sort == 0 ? "ASC" : "DESC")), $offset); + + foreach ($notes as $note) { + if ($note->isDeleted()) { + continue; + } + + $notes_return_object->items[] = $note->toVkApiStruct(); } } else { - $notes = explode(',', $note_ids); - - foreach($notes as $note) - { - $id = explode("_", $note); - - $items = []; - - $note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]); - if($note && !$note->isDeleted()) { - $nodez->notes[] = $note->toVkApiStruct(); + $notes_splitted = explode(',', $note_ids); + + foreach ($notes_splitted as $note_id) { + $note = (new NotesRepo())->getNoteById($user_id, $note_id); + + if ($note && !$note->isDeleted()) { + $notes_return_object->items[] = $note->toVkApiStruct(); } } } - return $nodez; + return $notes_return_object; } - function getById(int $note_id, int $owner_id, bool $need_wiki = false) + public function getById(int $note_id, int $owner_id, bool $need_wiki = false) { $this->requireUser(); - $note = (new NotesRepo)->getNoteById($owner_id, $note_id); + $note = (new NotesRepo())->getNoteById($owner_id, $note_id); + + if (!$note) { + $this->fail(15, "Access denied"); + } + + if ($note->isDeleted()) { + $this->fail(15, "Access denied"); + } - if(!$note) - $this->fail(180, "Note not found"); - - if($note->isDeleted()) - $this->fail(189, "Note is deleted"); + if (!$note->getOwner() || $note->getOwner()->isDeleted()) { + $this->fail(15, "Access denied"); + } - if(!$note->getOwner() || $note->getOwner()->isDeleted()) - $this->fail(177, "Owner does not exists"); + if (!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) { + $this->fail(15, "Access denied"); + } - if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) - $this->fail(40, "Access denied: this user chose to hide his notes"); + if (!$note->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } return $note->toVkApiStruct(); } - function getComments(int $note_id, int $owner_id, int $sort = 1, int $offset = 0, int $count = 100) + public function getComments(int $note_id, int $owner_id, int $sort = 1, int $offset = 0, int $count = 100) { $this->requireUser(); - $note = (new NotesRepo)->getNoteById($owner_id, $note_id); + $note = (new NotesRepo())->getNoteById($owner_id, $note_id); + + if (!$note) { + $this->fail(15, "Access denied"); + } + + if ($note->isDeleted()) { + $this->fail(15, "Access denied"); + } + + if (!$note->getOwner()) { + $this->fail(15, "Access denied"); + } + + if (!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) { + $this->fail(15, "Access denied"); + } - if(!$note) - $this->fail(180, "Note not found"); - - if($note->isDeleted()) - $this->fail(189, "Note is deleted"); - - if(!$note->getOwner()) - $this->fail(177, "Owner does not exists"); + if (!$note->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) - $this->fail(14, "No access"); - $arr = (object) [ - "count" => $note->getCommentsCount(), - "comments" => []]; + "count" => $note->getCommentsCount(), + "items" => []]; $comments = array_slice(iterator_to_array($note->getComments(1, $count + $offset)), $offset); - - foreach($comments as $comment) { - $arr->comments[] = $comment->toVkApiStruct($this->getUser(), false, false, $note); + + foreach ($comments as $comment) { + $arr->items[] = $comment->toVkApiStruct($this->getUser(), false, false, $note); } return $arr; } - - function getFriendsNotes(int $offset = 0, int $count = 0) - { - $this->fail(501, "Not implemented"); - } - - function restoreComment(int $comment_id = 0, int $owner_id = 0) - { - $this->fail(501, "Not implemented"); - } } diff --git a/VKAPI/Handlers/Notifications.php b/VKAPI/Handlers/Notifications.php new file mode 100644 index 000000000..cb3217ccd --- /dev/null +++ b/VKAPI/Handlers/Notifications.php @@ -0,0 +1,164 @@ +requireUser(); + + $res = (object) [ + "items" => [], + "profiles" => [], + "groups" => [], + "last_viewed" => $this->getUser()->getNotificationOffset(), + ]; + + if ($count > 100) { + $this->fail(125, "Count is too big"); + } + + if (!eventdb()) { + $this->fail(1289, "EventDB is disabled on this instance"); + } + + $notifs = array_slice(iterator_to_array((new Notifs())->getNotificationsByUser($this->getUser(), $this->getUser()->getNotificationOffset(), (bool) $archived, 1, $offset + $count)), $offset); + $tmpProfiles = []; + foreach ($notifs as $notif) { + $sxModel = $notif->getModel(1); + + if (!method_exists($sxModel, "getAvatarUrl")) { + $sxModel = $notif->getModel(0); + } + + $tmpProfiles[] = $sxModel instanceof Club ? $sxModel->getId() * -1 : $sxModel->getId(); + $res->items[] = $notif->toVkApiStruct(); + } + + foreach (array_unique($tmpProfiles) as $id) { + if ($id > 0) { + $sxModel = (new Users())->get($id); + $result = (object) [ + "id" => $sxModel->getId(), + "uid" => $sxModel->getId(), + "first_name" => $sxModel->getFirstName(), + "last_name" => $sxModel->getLastName(), + "photo" => $sxModel->getAvatarUrl(), + "photo_medium_rec" => $sxModel->getAvatarUrl("tiny"), + "photo_50" => $sxModel->getAvatarUrl("tiny"), + "photo_100" => $sxModel->getAvatarUrl("normal"), + "screen_name" => $sxModel->getURL(true), + ]; + + $res->profiles[] = $result; + } else { + $sxModel = (new Clubs())->get(abs($id)); + $result = $sxModel->toVkApiStruct($this->getUser()); + + $res->groups[] = $result; + } + } + + return $res; + } + + public function markAsViewed() + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + try { + $this->getUser()->updateNotificationOffset(); + $this->getUser()->save(); + } catch (\Throwable $e) { + return 0; + } + + return 1; + } + + public function fetch(string $last_id = "0") + { + $this->requireUser(); + $userId = $this->getUser()->getId(); + + $res = (object) [ + "items" => [], + "profiles" => [], + "groups" => [], + "next_last_id" => $last_id, + ]; + + try { + $broker = NotificationBroker::i(); + $events = $broker->getNew($userId, $last_id); + + if (empty($events)) { + return $res; + } + + $tmpProfiles = []; + $tmpGroups = []; + + foreach ($events as $event) { + $currentId = $event['id']; + $rawPayload = $event['data']; + + $notification = (new Notifs())->fromArray($rawPayload); + + if (!$notification) { + continue; + } + + $res->items[] = $notification->toVkApiStruct(); + $res->new_lastId = $currentId; + + $sxModel = $notification->getModel(1); + if (!method_exists($sxModel, "getAvatarUrl")) { + $sxModel = $notification->getModel(0); + } + + if ($sxModel instanceof Club) { + $tmpGroups[] = $sxModel; + } elseif ($sxModel instanceof Users) { + $tmpProfiles[] = $sxModel; + } + } + + foreach (array_unique($tmpProfiles, SORT_REGULAR) as $user) { + $res->profiles[] = (object) [ + "uid" => $user->getId(), + "first_name" => $user->getFirstName(), + "last_name" => $user->getLastName(), + "photo" => $user->getAvatarUrl(), + "photo_medium_rec" => $user->getAvatarUrl("tiny"), + "screen_name" => $user->getShortCode(), + ]; + } + + foreach (array_unique($tmpGroups, SORT_REGULAR) as $club) { + $res->groups[] = $club->toVkApiStruct($this->getUser()); + } + + return $res; + + } catch (\Exception $e) { + $this->fail(1981, "Internal error during event processing"); + } + } +} diff --git a/VKAPI/Handlers/Ovk.php b/VKAPI/Handlers/Ovk.php index a260f1c41..b92733f31 100644 --- a/VKAPI/Handlers/Ovk.php +++ b/VKAPI/Handlers/Ovk.php @@ -1,15 +1,19 @@ - $this->userAuthorized(), @@ -17,58 +21,67 @@ function test(): object "version" => VKAPI_DECL_VER, ]; } - - function chickenWings(): string + + public function chickenWings(): string { return "крылышки"; } - function aboutInstance(string $fields = "statistics,administrators,popular_groups,links", string $admin_fields = "", string $group_fields = ""): object + public 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(); + 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 + "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) { + 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 + "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); + if (in_array("popular_groups", $fields)) { + $popularClubsRaw = (new ClubsRepo())->getPopularClubs(); + $popularClubs = $popularClubsRaw !== null ? iterator_to_array($popularClubsRaw) : []; + + $clubsResponse = []; + + if (!empty($popularClubs)) { + $ids = implode(',', array_map(function ($entry) { + return $entry->club->getId(); + }, $popularClubs)); + + $clubsResponse = (new Groups($this->getUser()))->getById($ids, "", "members_count, " . $group_fields); + } $response->popular_groups = (object) [ "count" => sizeof($popularClubs), - "items" => $clubsResponse + "items" => $clubsResponse, ]; } - if(in_array("links", $fields)) + 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'] + "items" => is_null(OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']) ? [] : OPENVK_ROOT_CONF['openvk']['preferences']['about']['links'], ]; + } return $response; } diff --git a/VKAPI/Handlers/Pay.php b/VKAPI/Handlers/Pay.php index e5fb93a73..8e6ba24eb 100644 --- a/VKAPI/Handlers/Pay.php +++ b/VKAPI/Handlers/Pay.php @@ -1,42 +1,49 @@ -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 + + public function verifyOrder(int $app_id, float $amount, string $signature): bool { $this->requireUser(); - + $app = (new Applications())->get($app_id); - if(!$app) + if (!$app) { $this->fail(26, "No app found with this id"); - else if($app->getOwner()->getId() != $this->getUser()->getId()) + } elseif ($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) + 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; } -} \ No newline at end of file +} diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php index bb1a22f06..bc77297d8 100644 --- a/VKAPI/Handlers/Photos.php +++ b/VKAPI/Handlers/Photos.php @@ -1,4 +1,7 @@ -fail(121, "Incorrect hash"); + } [$up, $image, $group] = explode("|", $photo); $imagePath = __DIR__ . "/../../tmp/api-storage/photos/$up" . "_$image.oct"; - if(!file_exists($imagePath)) + if (!file_exists($imagePath)) { $this->fail(10, "Invalid image"); + } return $imagePath; } - function getOwnerPhotoUploadServer(int $owner_id = 0): object + public function getOwnerPhotoUploadServer(int $owner_id = 0): object { $this->requireUser(); - if($owner_id < 0) { - $club = (new Clubs)->get(abs($owner_id)); - if(!$club) - $this->fail(0404, "Club not found"); - else if(!$club->canBeModifiedBy($this->getUser())) + if ($owner_id < 0) { + $club = (new Clubs())->get(abs($owner_id)); + if (!$club) { + $this->fail(0o404, "Club not found"); + } elseif (!$club->canBeModifiedBy($this->getUser())) { $this->fail(200, "Access: Club can't be 'written' by user"); + } } return (object) [ @@ -65,20 +72,20 @@ function getOwnerPhotoUploadServer(int $owner_id = 0): object ]; } - function saveOwnerPhoto(string $photo, string $hash): object + public function saveOwnerPhoto(string $photo, string $hash): object { + $this->requireUser(); $imagePath = $this->getImagePath($photo, $hash, $uploader, $group); - if($group == 0) { - $user = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader); - $album = (new Albums)->getUserAvatarAlbum($user); + if ($group == 0) { + $album = (new Albums())->getUserAvatarAlbum($this->getUser()); } else { - $club = (new Clubs)->get((int) $group); - $album = (new Albums)->getClubAvatarAlbum($club); + $club = (new Clubs())->get((int) $group); + $album = (new Albums())->getClubAvatarAlbum($club); } try { - $avatar = new Photo; - $avatar->setOwner((int) $uploader); + $avatar = new Photo(); + $avatar->setOwner($this->getUser()->getId()); $avatar->setDescription("Profile photo"); $avatar->setCreated(time()); $avatar->setFile([ @@ -88,79 +95,85 @@ function saveOwnerPhoto(string $photo, string $hash): object $avatar->save(); $album->addPhoto($avatar); unlink($imagePath); - } catch(ImageException | InvalidStateException $e) { + } catch (ImageException | InvalidStateException $e) { unlink($imagePath); $this->fail(129, "Invalid image file"); } return (object) [ - "photo_hash" => NULL, + "photo_hash" => null, "photo_src" => $avatar->getURL(), ]; } - function getWallUploadServer(?int $group_id = NULL): object + public function getWallUploadServer(?int $group_id = null): object { $this->requireUser(); - $album = NULL; - if(!is_null($group_id)) { - $club = (new Clubs)->get(abs($group_id)); - if(!$club) - $this->fail(0404, "Club not found"); - else if(!$club->canBeModifiedBy($this->getUser())) + $album = null; + if (!is_null($group_id)) { + $club = (new Clubs())->get(abs($group_id)); + if (!$club) { + $this->fail(0o404, "Club not found"); + } elseif (!$club->canBeModifiedBy($this->getUser())) { $this->fail(200, "Access: Club can't be 'written' by user"); + } } else { - $album = (new Albums)->getUserWallAlbum($this->getUser()); + $album = (new Albums())->getUserWallAlbum($this->getUser()); } + $albumId = $album ? $album->getId() : null; + return (object) [ "upload_url" => $this->getPhotoUploadUrl("photo", $group_id ?? 0), - "album_id" => $album, + "album_id" => $albumId, "user_id" => $this->getUser()->getId(), ]; } - function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = NULL): array + public function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = null): array { + $this->requireUser(); $imagePath = $this->getImagePath($photo, $hash, $uploader, $group); - if($group_id != $group) + if ($group_id != $group) { $this->fail(8, "group_id doesn't match"); + } - $album = NULL; - if($group_id != 0) { - $uploader = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader); - $album = (new Albums)->getUserWallAlbum($uploader); + $album = null; + if ($group_id != 0) { + $album = (new Albums())->getUserWallAlbum($this->getUser()); } try { - $photo = new Photo; - $photo->setOwner((int) $uploader); + $photo = new Photo(); + $photo->setOwner($this->getUser()->getId()); $photo->setCreated(time()); $photo->setFile([ "tmp_name" => $imagePath, "error" => 0, ]); - if (!is_null($caption)) + if (!is_null($caption)) { $photo->setDescription($caption); + } $photo->save(); unlink($imagePath); - } catch(ImageException | InvalidStateException $e) { + } catch (ImageException | InvalidStateException $e) { unlink($imagePath); $this->fail(129, "Invalid image file"); } - if(!is_null($album)) + if (!is_null($album)) { $album->addPhoto($photo); + } return [ $photo->toVkApiStruct(), ]; } - function getUploadServer(?int $album_id = NULL): object + public function getUploadServer(?int $album_id = null): object { $this->requireUser(); @@ -172,34 +185,37 @@ function getUploadServer(?int $album_id = NULL): object ]; } - function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = NULL): object + public function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = null): object { $this->requireUser(); $secret = CHANDLER_ROOT_CONF["security"]["secret"]; - if(!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash)) + if (!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash)) { $this->fail(121, "Incorrect hash"); + } - $album = NULL; - if($album_id != 0) { - $album_ = (new Albums)->get($album_id); - if(!$album_) - $this->fail(0404, "Invalid album"); - else if(!$album_->canBeModifiedBy($this->getUser())) + $album = null; + if ($album_id != 0) { + $album_ = (new Albums())->get($album_id); + if (!$album_) { + $this->fail(0o404, "Invalid album"); + } elseif (!$album_->canBeModifiedBy($this->getUser())) { $this->fail(15, "Access: Album can't be 'written' by user"); + } $album = $album_; } $pList = json_decode($photos_list); $imagePaths = []; - foreach($pList as $pDesc) + foreach ($pList as $pDesc) { $imagePaths[] = __DIR__ . "/../../tmp/api-storage/photos/$pDesc->keyholder" . "_$pDesc->resource.oct"; + } $images = []; try { - foreach($imagePaths as $imagePath) { - $photo = new Photo; + foreach ($imagePaths as $imagePath) { + $photo = new Photo(); $photo->setOwner($this->getUser()->getId()); $photo->setCreated(time()); $photo->setFile([ @@ -207,20 +223,23 @@ function save(string $photos_list, string $hash, int $album_id = 0, ?string $cap "error" => 0, ]); - if (!is_null($caption)) + if (!is_null($caption)) { $photo->setDescription($caption); + } $photo->save(); unlink($imagePath); - if(!is_null($album)) + if (!is_null($album)) { $album->addPhoto($photo); + } $images[] = $photo->toVkApiStruct(); } - } catch(ImageException | InvalidStateException $e) { - foreach($imagePaths as $imagePath) + } catch (ImageException | InvalidStateException $e) { + foreach ($imagePaths as $imagePath) { unlink($imagePath); + } $this->fail(129, "Invalid image file"); } @@ -231,20 +250,20 @@ function save(string $photos_list, string $hash, int $album_id = 0, ?string $cap ]; } - function createAlbum(string $title, int $group_id = 0, string $description = "", int $privacy = 0) + public function createAlbum(string $title, int $group_id = 0, string $description = "") { $this->requireUser(); $this->willExecuteWriteAction(); - if($group_id != 0) { - $club = (new Clubs)->get((int) $group_id); + if ($group_id != 0) { + $club = (new Clubs())->get((int) $group_id); - if(!$club || !$club->canBeModifiedBy($this->getUser())) { - $this->fail(20, "Invalid club"); + if (!$club || !$club->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); } } - $album = new Album; + $album = new Album(); $album->setOwner(isset($club) ? $club->getId() * -1 : $this->getUser()->getId()); $album->setName($title); $album->setDescription($description); @@ -254,166 +273,133 @@ function createAlbum(string $title, int $group_id = 0, string $description = "", return $album->toVkApiStruct($this->getUser()); } - function editAlbum(int $album_id, int $owner_id, string $title, string $description = "", int $privacy = 0) + public function editAlbum(int $album_id, int $owner_id, string $title = null, string $description = null, int $privacy = 0) { $this->requireUser(); $this->willExecuteWriteAction(); - $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); + $album = (new Albums())->getAlbumByOwnerAndId($owner_id, $album_id); - if(!$album || $album->isDeleted()) { - $this->fail(2, "Invalid album"); + if (!$album || $album->isDeleted() || $album->isCreatedBySystem()) { + $this->fail(114, "Invalid album id"); } - - if(empty($title)) { - $this->fail(25, "Title is empty"); + if (!$album->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); } - if($album->isCreatedBySystem()) { - $this->fail(40, "You can't change system album"); + if (!is_null($title) && !empty($title) && !ctype_space($title)) { + $album->setName($title); } - - if(!$album->canBeModifiedBy($this->getUser())) { - $this->fail(2, "Access to album denied"); + if (!is_null($description)) { + $album->setDescription($description); } - $album->setName($title); - $album->setDescription($description); - - $album->save(); + try { + $album->save(); + } catch (\Throwable $e) { + return 1; + } - return $album->toVkApiStruct($this->getUser()); + return 1; } - function getAlbums(int $owner_id, string $album_ids = "", int $offset = 0, int $count = 100, bool $need_system = true, bool $need_covers = true, bool $photo_sizes = false) + public function getAlbums(int $owner_id = null, string $album_ids = "", int $offset = 0, int $count = 100, bool $need_system = true, bool $need_covers = true, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); - - $res = []; - - if(empty($album_ids)) { - if($owner_id > 0) { - $user = (new UsersRepo)->get($owner_id); - - $res = [ - "count" => (new Albums)->getUserAlbumsCount($user), - "items" => [] - ]; - - if(!$user || $user->isDeleted()) - $this->fail(2, "Invalid user"); - - if(!$user->getPrivacyPermission('photos.read', $this->getUser())) - $this->fail(21, "This user chose to hide his albums."); - - $albums = array_slice(iterator_to_array((new Albums)->getUserAlbums($user, 1, $count + $offset)), $offset); + $res = [ + "count" => 0, + "items" => [], + ]; + $albums_list = []; + if ($owner_id == null && empty($album_ids)) { + $owner_id = $this->getUser()->getId(); + } - foreach($albums as $album) { - if(!$need_system && $album->isCreatedBySystem()) continue; - $res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes); - } + if (empty($album_ids)) { + $owner = get_entity_by_id($owner_id); + if (!$owner || !$owner->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); } - - else { - $club = (new Clubs)->get($owner_id * -1); - - $res = [ - "count" => (new Albums)->getClubAlbumsCount($club), - "items" => [] - ]; - - if(!$club) - $this->fail(2, "Invalid club"); - - $albums = array_slice(iterator_to_array((new Albums)->getClubAlbums($club, 1, $count + $offset)), $offset); - - foreach($albums as $album) { - if(!$need_system && $album->isCreatedBySystem()) continue; - $res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes); - } + if ($owner_id > 0 && !$owner->getPrivacyPermission('photos.read', $this->getUser())) { + $this->fail(15, "Access denied"); } + $albums_list = null; + if ($owner_id > 0) { + # TODO rewrite to offset + $albums_list = array_slice(iterator_to_array((new Albums())->getUserAlbums($owner, 1, $count + $offset)), $offset); + $res["count"] = (new Albums())->getUserAlbumsCount($owner); + } else { + $albums_list = array_slice(iterator_to_array((new Albums())->getClubAlbums($owner, 1, $count + $offset)), $offset); + $res["count"] = (new Albums())->getClubAlbumsCount($owner); + } } else { - $albums = explode(',', $album_ids); + $album_ids = explode(',', $album_ids); + foreach ($album_ids as $album_id) { + $album = (new Albums())->getAlbumByOwnerAndId((int) $owner_id, (int) $album_id); + if (!$album || $album->isDeleted() || !$album->canBeViewedBy($this->getUser())) { + continue; + } - $res = [ - "count" => sizeof($albums), - "items" => [] - ]; + $albums_list[] = $album; + } + } - foreach($albums as $album) - { - $id = explode("_", $album); - - $album = (new Albums)->getAlbumByOwnerAndId((int)$id[0], (int)$id[1]); - if($album && !$album->isDeleted()) { - if(!$need_system && $album->isCreatedBySystem()) continue; - $res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes); - } + foreach ($albums_list as $album) { + if (!$need_system && $album->isCreatedBySystem()) { # TODO use queries + continue; } + + $res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes); } return $res; } - function getAlbumsCount(int $user_id = 0, int $group_id = 0) + public function getAlbumsCount(int $user_id = null, int $group_id = null) { $this->requireUser(); - $this->willExecuteWriteAction(); - if($user_id == 0 && $group_id == 0 || $user_id > 0 && $group_id > 0) { - $this->fail(21, "Select user_id or group_id"); + if (is_null($user_id) && is_null($group_id)) { + $user_id = $this->getUser()->getId(); } - if($user_id > 0) { - - $us = (new UsersRepo)->get($user_id); - if(!$us || $us->isDeleted()) { - $this->fail(21, "Invalid user"); - } - - if(!$us->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); + if (!is_null($user_id)) { + $__user = (new UsersRepo())->get($user_id); + if (!$__user || $__user->isDeleted() || !$__user->getPrivacyPermission('photos.read', $this->getUser())) { + $this->fail(15, "Access denied"); } - return (new Albums)->getUserAlbumsCount($us); + return (new Albums())->getUserAlbumsCount($__user); } - - if($group_id > 0) - { - $cl = (new Clubs)->get($group_id); - if(!$cl) { - $this->fail(21, "Invalid club"); + if (!is_null($group_id)) { + $__club = (new Clubs())->get($group_id); + if (!$__club || !$__club->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); } - return (new Albums)->getClubAlbumsCount($cl); + return (new Albums())->getClubAlbumsCount($__club); } + + return 0; } - function getById(string $photos, bool $extended = false, bool $photo_sizes = false) + public function getById(string $photos, bool $extended = false, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); - $phts = explode(",", $photos); + $photos_splitted_list = explode(",", $photos); $res = []; + if (sizeof($photos_splitted_list) > 78) { + $this->fail(-78, "Photos count must not exceed limit"); + } - foreach($phts as $phota) { - $ph = explode("_", $phota); - $photo = (new PhotosRepo)->getByOwnerAndVID((int)$ph[0], (int)$ph[1]); - - if(!$photo || $photo->isDeleted()) { - $this->fail(21, "Invalid photo"); - } - - if($photo->getOwner()->isDeleted()) { - $this->fail(21, "Owner of this photo is deleted"); - } - - if(!$photo->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his photos."); + foreach ($photos_splitted_list as $photo_id) { + $photo_s_id = explode("_", $photo_id); + $photo = (new PhotosRepo())->getByOwnerAndVID((int) $photo_s_id[0], (int) $photo_s_id[1]); + if (!$photo || $photo->isDeleted() || !$photo->canBeViewedBy($this->getUser())) { + continue; } $res[] = $photo->toVkApiStruct($photo_sizes, $extended); @@ -422,64 +408,70 @@ function getById(string $photos, bool $extended = false, bool $photo_sizes = fal return $res; } - function get(int $owner_id, int $album_id, string $photo_ids = "", bool $extended = false, bool $photo_sizes = false, int $offset = 0, int $count = 10) + public function get(int $owner_id, string $album_id, string $photo_ids = "", bool $extended = false, bool $photo_sizes = true, int $offset = 0, int $count = 10) { $this->requireUser(); - $this->willExecuteWriteAction(); $res = []; - if(empty($photo_ids)) { - $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); + if (empty($photo_ids)) { - if(!$album || $album->isDeleted()) - $this->fail(21, "Invalid album"); + if ($album_id == "profile") { + $album = (new Albums())->getUserAvatarAlbum((new UsersRepo())->get($owner_id)); + } else { + $album = (new Albums())->getAlbumByOwnerAndId($owner_id, intval($album_id)); + } + + if (!$album || $album->isDeleted() || !$album->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) - $this->fail(21, "This user chose to hide his albums."); - $photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset); - $res["count"] = sizeof($photos); + $res["count"] = $album->size(); + + foreach ($photos as $photo) { + if (!$photo || $photo->isDeleted()) { + continue; + } - foreach($photos as $photo) { - if(!$photo || $photo->isDeleted()) continue; $res["items"][] = $photo->toVkApiStruct($photo_sizes, $extended); } } else { - $photos = explode(',', $photo_ids); + $photos = array_unique(explode(',', $photo_ids)); + if (sizeof($photos) > 78) { + $this->fail(-78, "Photos count must not exceed limit"); + } $res = [ "count" => sizeof($photos), - "items" => [] + "items" => [], ]; - foreach($photos as $photo) { + foreach ($photos as $photo) { $id = explode("_", $photo); - - $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); - if($phot && !$phot->isDeleted()) { - $res["items"][] = $phot->toVkApiStruct($photo_sizes, $extended); + + $photo_entity = (new PhotosRepo())->getByOwnerAndVID((int) $id[0], (int) $id[1]); + if (!$photo_entity || $photo_entity->isDeleted() || !$photo_entity->canBeViewedBy($this->getUser())) { + continue; } + + $res["items"][] = $photo_entity->toVkApiStruct($photo_sizes, $extended); } } return $res; } - function deleteAlbum(int $album_id, int $group_id = 0) + public function deleteAlbum(int $album_id, int $group_id = 0) { $this->requireUser(); $this->willExecuteWriteAction(); - $album = (new Albums)->get($album_id); + $album = (new Albums())->get($album_id); - if(!$album || $album->canBeModifiedBy($this->getUser())) { - $this->fail(21, "Invalid album"); - } - - if($album->isDeleted()) { - $this->fail(22, "Album already deleted"); + if (!$album || $album->isDeleted() || $album->isCreatedBySystem() || !$album->canBeModifiedBy($this->getUser())) { + $this->fail(15, "Access denied"); } $album->delete(); @@ -487,22 +479,18 @@ function deleteAlbum(int $album_id, int $group_id = 0) return 1; } - function edit(int $owner_id, int $photo_id, string $caption = "") + public function edit(int $owner_id, int $photo_id, string $caption = "") { $this->requireUser(); $this->willExecuteWriteAction(); - $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $photo_id); - if(!$photo) { - $this->fail(21, "Invalid photo"); + if (!$photo || $photo->isDeleted() || !$photo->canBeModifiedBy($this->getUser())) { + $this->fail(21, "Access denied"); } - if($photo->isDeleted()) { - $this->fail(21, "Photo is deleted"); - } - - if(!empty($caption)) { + if (!empty($caption)) { $photo->setDescription($caption); $photo->save(); } @@ -510,104 +498,82 @@ function edit(int $owner_id, int $photo_id, string $caption = "") return 1; } - function delete(int $owner_id, int $photo_id, string $photos = "") + public function delete(int $owner_id = null, int $photo_id = null, string $photos = null) { $this->requireUser(); $this->willExecuteWriteAction(); - if(empty($photos)) { - $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); - - if($this->getUser()->getId() !== $photo->getOwner()->getId()) { - $this->fail(21, "You can't delete another's photo"); - } + if (!$owner_id) { + $owner_id = $this->getUser()->getId(); + } - if(!$photo) { - $this->fail(21, "Invalid photo"); + if (is_null($photos)) { + if (is_null($photo_id)) { + return 0; } - if($photo->isDeleted()) { - $this->fail(21, "Photo already deleted"); + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $photo_id); + if (!$photo || $photo->isDeleted() || !$photo->canBeModifiedBy($this->getUser())) { + return 1; } $photo->delete(); } else { - $photozs = explode(',', $photos); - - foreach($photozs as $photo) - { - $id = explode("_", $photo); - - $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); - - if($this->getUser()->getId() !== $phot->getOwner()->getId()) { - $this->fail(21, "You can't delete another's photo"); - } + $photos_list = array_unique(explode(',', $photos)); + if (sizeof($photos_list) > 10) { + $this->fail(-78, "Photos count must not exceed limit"); + } - if(!$phot) { - $this->fail(21, "Invalid photo"); - } - - if($phot->isDeleted()) { - $this->fail(21, "Photo already deleted"); + foreach ($photos_list as $photo_id) { + $id = explode("_", $photo_id); + $photo = (new PhotosRepo())->getByOwnerAndVID((int) $id[0], (int) $id[1]); + if (!$photo || $photo->isDeleted() || !$photo->canBeModifiedBy($this->getUser())) { + continue; } - $phot->delete(); + $photo->delete(); } } return 1; } - function getAllComments(int $owner_id, int $album_id, bool $need_likes = false, int $offset = 0, int $count = 100) - { - $this->fail(501, "Not implemented"); - } - - function deleteComment(int $comment_id, int $owner_id = 0) + # Поскольку комментарии едины, можно использовать метод "wall.deleteComment". + /*public function deleteComment(int $comment_id, int $owner_id = 0) { $this->requireUser(); $this->willExecuteWriteAction(); - $comment = (new CommentsRepo)->get($comment_id); - if(!$comment) { + $comment = (new CommentsRepo())->get($comment_id); + if (!$comment) { $this->fail(21, "Invalid comment"); } - if(!$comment->canBeModifiedBy($this->getUser())) { - $this->fail(21, "Forbidden"); - } - - if($comment->isDeleted()) { - $this->fail(4, "Comment already deleted"); + if (!$comment->canBeModifiedBy($this->getUser())) { + $this->fail(21, "Access denied"); } $comment->delete(); return 1; - } + }*/ - function createComment(int $owner_id, int $photo_id, string $message = "", string $attachments = "", bool $from_group = false) + public function createComment(int $owner_id, int $photo_id, string $message = "", bool $from_group = false) { $this->requireUser(); $this->willExecuteWriteAction(); - if(empty($message) && empty($attachments)) { + if (empty($message) && empty($attachments)) { $this->fail(100, "Required parameter 'message' missing."); } - $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $photo_id); - if(!$photo->getAlbum()->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); + if (!$photo || $photo->isDeleted() || !$photo->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); } - if(!$photo) - $this->fail(180, "Photo not found"); - if($photo->isDeleted()) - $this->fail(189, "Photo is deleted"); - - $comment = new Comment; + $comment = new Comment(); $comment->setOwner($this->getUser()->getId()); $comment->setModel(get_class($photo)); $comment->setTarget($photo->getId()); @@ -615,111 +581,58 @@ function createComment(int $owner_id, int $photo_id, string $message = "", strin $comment->setCreated(time()); $comment->save(); - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $comment->attach($attacc); - } - } - } - return $comment->getId(); } - function getAll(int $owner_id, bool $extended = false, int $offset = 0, int $count = 100, bool $photo_sizes = false) + public function getAll(int $owner_id, bool $extended = false, int $offset = 0, int $count = 100, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); - if($owner_id < 0) { - $this->fail(4, "This method doesn't works with clubs"); + if ($owner_id < 0) { + $this->fail(-413, "Clubs are not supported"); } - $user = (new UsersRepo)->get($owner_id); - - if(!$user) { - $this->fail(4, "Invalid user"); - } - - if(!$user->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); + $user = (new UsersRepo())->get($owner_id); + if (!$user || !$user->getPrivacyPermission('photos.read', $this->getUser())) { + $this->fail(15, "Access denied"); } - $photos = array_slice(iterator_to_array((new PhotosRepo)->getEveryUserPhoto($user, 1, $count + $offset)), $offset); - $res = []; + $photos = (new PhotosRepo())->getEveryUserPhoto($user, $offset, $count); + $res = [ + "count" => (new PhotosRepo())->getUserPhotosCount($user), + "items" => [], + ]; - foreach($photos as $photo) { - if(!$photo || $photo->isDeleted()) continue; + foreach ($photos as $photo) { + if (!$photo || $photo->isDeleted()) { + continue; + } $res["items"][] = $photo->toVkApiStruct($photo_sizes, $extended); } return $res; } - function getComments(int $owner_id, int $photo_id, bool $need_likes = false, int $offset = 0, int $count = 100, bool $extended = false, string $fields = "") + public function getComments(int $owner_id, int $photo_id, bool $need_likes = false, int $offset = 0, int $count = 100, bool $extended = false, string $fields = "") { $this->requireUser(); - $this->willExecuteWriteAction(); - $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); + $photo = (new PhotosRepo())->getByOwnerAndVID($owner_id, $photo_id); $comms = array_slice(iterator_to_array($photo->getComments(1, $offset + $count)), $offset); - if(!$photo) { - $this->fail(4, "Invalid photo"); - } - - if(!$photo->getAlbum()->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his photos."); - } - - if($photo->isDeleted()) { - $this->fail(4, "Photo is deleted"); + if (!$photo || $photo->isDeleted() || !$photo->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); } $res = [ "count" => sizeof($comms), - "items" => [] + "items" => [], ]; - foreach($comms as $comment) { + foreach ($comms as $comment) { $res["items"][] = $comment->toVkApiStruct($this->getUser(), $need_likes, $extended); - if($extended) { - if($comment->getOwner() instanceof \openvk\Web\Models\Entities\User) { + if ($extended) { + if ($comment->getOwner() instanceof \openvk\Web\Models\Entities\User) { $res["profiles"][] = $comment->getOwner()->toVkApiStruct(); } } @@ -727,4 +640,4 @@ function getComments(int $owner_id, int $photo_id, bool $need_likes = false, int return $res; } -} \ No newline at end of file +} diff --git a/VKAPI/Handlers/Polls.php b/VKAPI/Handlers/Polls.php index be947a442..2db285d3f 100755 --- a/VKAPI/Handlers/Polls.php +++ b/VKAPI/Handlers/Polls.php @@ -1,107 +1,184 @@ -get($poll_id); - - if (!$poll) - $this->fail(100, "One of the parameters specified was missing or invalid: poll_id is incorrect"); - - $users = array(); - $answers = array(); - foreach($poll->getResults()->options as $answer) { - $answers[] = (object)[ - "id" => $answer->id, - "rate" => $answer->pct, - "text" => $answer->name, - "votes" => $answer->votes - ]; - } - - $userVote = array(); - foreach($poll->getUserVote($this->getUser()) as $vote) - $userVote[] = $vote[0]; - - $response = [ - "multiple" => $poll->isMultipleChoice(), - "end_date" => $poll->endsAt() == NULL ? 0 : $poll->endsAt()->timestamp(), - "closed" => $poll->hasEnded(), - "is_board" => false, - "can_edit" => false, - "can_vote" => $poll->canVote($this->getUser()), - "can_report" => false, - "can_share" => true, - "created" => 0, - "id" => $poll->getId(), - "owner_id" => $poll->getOwner()->getId(), - "question" => $poll->getTitle(), - "votes" => $poll->getVoterCount(), - "disable_unvote" => $poll->isRevotable(), - "anonymous" => $poll->isAnonymous(), - "answer_ids" => $userVote, - "answers" => $answers, - "author_id" => $poll->getOwner()->getId(), - ]; - - if ($extended) { - $response["profiles"] = (new Users)->get(strval($poll->getOwner()->getId()), $fields, 0, 1); - /* Currently there is only one person that can be shown trough "Extended" param. - * As "friends" param will be implemented, "profiles" will show more users - */ - } - - return (object) $response; - } - - function addVote(int $poll_id, string $answers_ids) - { - $this->requireUser(); - $this->willExecuteWriteAction(); - - $poll = (new PollsRepo)->get($poll_id); - - if(!$poll) - $this->fail(251, "Invalid poll id"); - - try { - $poll->vote($this->getUser(), explode(",", $answers_ids)); - return 1; - } catch(AlreadyVotedException $ex) { - return 0; - } catch(PollLockedException $ex) { - return 0; - } catch(InvalidOptionException $ex) { - $this->fail(8, "бдсм вибратор купить в киеве"); - } - } - - function deleteVote(int $poll_id) - { - $this->requireUser(); - $this->willExecuteWriteAction(); - - $poll = (new PollsRepo)->get($poll_id); - - if(!$poll) - $this->fail(251, "Invalid poll id"); - - try { - $poll->revokeVote($this->getUser()); - return 1; - } catch(PollLockedException $ex) { - $this->fail(15, "Access denied: Poll is locked or isn't revotable"); - } catch(InvalidOptionException $ex) { - $this->fail(8, "how.to. ook.bacon.in.microwova."); - } - } -} +get($poll_id); + + if (!$poll) { + $this->fail(100, "One of the parameters specified was missing or invalid: poll_id is incorrect"); + } + + $users = []; + $answers = []; + foreach ($poll->getResults()->options as $answer) { + $answers[] = (object) [ + "id" => $answer->id, + "rate" => $answer->pct, + "text" => $answer->name, + "votes" => $answer->votes, + ]; + } + + $userVote = []; + foreach ($poll->getUserVote($this->getUser()) as $vote) { + $userVote[] = $vote[0]; + } + + $response = [ + "multiple" => $poll->isMultipleChoice(), + "end_date" => $poll->endsAt() == null ? 0 : $poll->endsAt()->timestamp(), + "closed" => $poll->hasEnded(), + "is_board" => 0, + "can_edit" => 0, + "can_vote" => (int) $poll->canVote($this->getUser()), + "can_report" => 0, + "can_share" => 1, + "created" => 0, + "id" => $poll->getId(), + "owner_id" => $poll->getOwner()->getId(), + "question" => $poll->getTitle(), + "votes" => $poll->getVoterCount(), + "disable_unvote" => !$poll->isRevotable(), + "anonymous" => (int) $poll->isAnonymous(), + "answer_ids" => $userVote, + "answers" => $answers, + "author_id" => $poll->getOwner()->getId(), + ]; + + if ($extended) { + $response["profiles"] = (new Users())->get(strval($poll->getOwner()->getId()), $fields, 0, 1); + /* Currently there is only one person that can be shown trough "Extended" param. + * As "friends" param will be implemented, "profiles" will show more users + */ + } + + return (object) $response; + } + + public function addVote(int $poll_id, string $answer_ids = "", string $answer_id = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if (empty($answer_ids) && empty($answer_id)) { + $this->fail(100, "Required parameter 'answer_ids' or 'answer_id' is missing."); + } elseif (empty($answer_ids)) { + $answer_ids = $answer_id; + } + + $poll = (new PollsRepo())->get($poll_id); + + if (!$poll) { + $this->fail(251, "Invalid poll id"); + } + + try { + $poll->vote($this->getUser(), explode(",", $answer_ids)); + return 1; + } catch (AlreadyVotedException $ex) { + return 0; + } catch (PollLockedException $ex) { + return 0; + } catch (InvalidOptionException $ex) { + $this->fail(8, "бдсм вибратор купить в киеве"); + } + } + + public function deleteVote(int $poll_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $poll = (new PollsRepo())->get($poll_id); + + if (!$poll) { + $this->fail(251, "Invalid poll id"); + } + + try { + $poll->revokeVote($this->getUser()); + return 1; + } catch (PollLockedException $ex) { + $this->fail(15, "Access denied: Poll is locked or isn't revotable"); + } catch (InvalidOptionException $ex) { + $this->fail(8, "how.to. ook.bacon.in.microwova."); + } + } + + public function getVoters(int $poll_id, int $answer_ids, int $offset = 0, int $count = 6) + { + $this->requireUser(); + + $poll = (new PollsRepo())->get($poll_id); + + if (!$poll) { + $this->fail(15, "Access denied"); + } + + if ($poll->isAnonymous()) { + $this->fail(15, "Access denied"); + } + + $voters = array_slice($poll->getVoters($answer_ids, 1, $offset + $count), $offset); + $res = (object) [ + "answer_id" => $answer_ids, + "users" => (object) ['items' => []], + ]; + + foreach ($voters as $voter) { + $res->users->items[] = $voter->toVkApiStruct(null, 'photo_50,photo_100,photo_200'); + } + + return (array) [$res]; + } + + public function create(string $question, string $add_answers, bool $disable_unvote = false, bool $is_anonymous = false, bool $is_multiple = false, int $end_date = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $options = json_decode($add_answers); + + if (!$options || empty($options)) { + $this->fail(62, "Invalid options"); + } + + if (sizeof($options) > ovkGetQuirk("polls.max-opts")) { + $this->fail(51, "Too many options"); + } + + $poll = new Poll(); + $poll->setOwner($this->getUser()); + $poll->setTitle($question); + $poll->setMultipleChoice($is_multiple); + $poll->setAnonymity($is_anonymous); + $poll->setRevotability(!$disable_unvote); + $poll->setOptions($options); + + if ($end_date > time()) { + if ($end_date > time() + (DAY * 365)) { + $this->fail(89, "End date is too big"); + } + + $poll->setEndDate($end_date); + } + + $poll->save(); + + return $this->getById($poll->getId()); + } +} diff --git a/VKAPI/Handlers/Reports.php b/VKAPI/Handlers/Reports.php new file mode 100644 index 000000000..13198dcc2 --- /dev/null +++ b/VKAPI/Handlers/Reports.php @@ -0,0 +1,57 @@ +requireUser(); + $this->willExecuteWriteAction(); + + $allowed_types = ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]; + if ($type == "" || !in_array($type, $allowed_types)) { + $this->fail(100, "One of the parameters specified was missing or invalid: type should be " . implode(", ", $allowed_types)); + } + + if ($owner_id <= 0) { + $this->fail(100, "One of the parameters specified was missing or invalid: Bad input"); + } + + if (mb_strlen($comment) === 0) { + $this->fail(100, "One of the parameters specified was missing or invalid: Comment can't be empty"); + } + + if ($type == "user" && $owner_id == $this->getUser()->getId()) { + return 1; + } + + if ($this->getUser()->isBannedInSupport()) { + return 0; + } + + if (sizeof(iterator_to_array((new ReportsRepo())->getDuplicates($type, $owner_id, null, $this->getUser()->getId()))) > 0) { + return 1; + } + + try { + $report = new Report(); + $report->setUser_id($this->getUser()->getId()); + $report->setTarget_id($owner_id); + $report->setType($type); + $report->setReason($comment); + $report->setCreated(time()); + + $report->save(); + } catch (\Throwable $e) { + $this->fail(-1, "Unknown error failed"); + } + + return 1; + } +} diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php index 843f42bdc..75240f7bc 100644 --- a/VKAPI/Handlers/Status.php +++ b/VKAPI/Handlers/Status.php @@ -1,34 +1,55 @@ -requireUser(); - if($user_id == 0 && $group_id == 0) { - return $this->getUser()->getStatus(); + + if ($user_id == 0 && $group_id == 0) { + $user_id = $this->getUser()->getId(); + } + + if ($group_id > 0) { + $this->fail(501, "Group statuses are not implemented"); } else { - if($group_id > 0) - $this->fail(501, "Group statuses are not implemented"); - else - return (new UsersRepo)->get($user_id)->getStatus(); + $user = (new UsersRepo())->get($user_id); + + if (!$user || $user->isDeleted() || !$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Invalid user"); + } + + $audioStatus = $user->getCurrentAudioStatus(); + $res = [ + "text" => $user->getStatus(), + ]; + + if ($audioStatus) { + $res["audio"] = $audioStatus->toVkApiStruct(); + } + + return $res; } } - function set(string $text, int $group_id = 0) + public function set(string $text, int $group_id = 0) { $this->requireUser(); $this->willExecuteWriteAction(); - if($group_id > 0) { + if ($group_id > 0) { $this->fail(501, "Group statuses are not implemented"); } else { $this->getUser()->setStatus($text); $this->getUser()->save(); - + return 1; } } diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index a42987879..96efd6f05 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -1,292 +1,552 @@ -getUser(); + if ($authuser == null) { + $authuser = $this->getUser(); + } - $users = new UsersRepo; - if($user_ids == "0") - $user_ids = (string) $authuser->getId(); - - $usrs = explode(',', $user_ids); - $response = array(); + $users = new UsersRepo(); + if ($user_ids == "0") { + if (!$authuser) { + return []; + } + + $user_ids = (string) $authuser->getId(); + } + + if (!empty($user_ids)) { + $usrs = explode(',', $user_ids); + } else { + $usrs = []; + } + $response = []; $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++) { - if($usrs[$i] != 0) { - $usr = $users->get((int) $usrs[$i]); - if(is_null($usr) || $usr->isDeleted()) { - $response[$i] = (object)[ - "id" => (int) $usrs[$i], - "first_name" => "DELETED", - "last_name" => "", - "deactivated" => "deleted" - ]; - } else if($usr->isBanned()) { - $response[$i] = (object)[ - "id" => $usr->getId(), - "first_name" => $usr->getFirstName(), - "last_name" => $usr->getLastName(), - "deactivated" => "banned", - "ban_reason" => $usr->getBanReason() - ]; - } else if($usrs[$i] == NULL) { - - } else { - $response[$i] = (object)[ - "id" => $usr->getId(), - "first_name" => $usr->getFirstName(), - "last_name" => $usr->getLastName(), - "is_closed" => false, - "can_access_closed" => true, - ]; - - $flds = explode(',', $fields); - - foreach($flds as $field) { - switch($field) { - case "verified": - $response[$i]->verified = intval($usr->isVerified()); - break; - case "sex": - $response[$i]->sex = $usr->isFemale() ? 1 : 2; - break; - case "has_photo": - $response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; - break; - case "photo_max_orig": - $response[$i]->photo_max_orig = $usr->getAvatarURL(); - break; - case "photo_max": - $response[$i]->photo_max = $usr->getAvatarURL("original"); - break; - 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) - $response[$i]->screen_name = $usr->getShortCode(); - break; - case "friend_status": - switch($usr->getSubscriptionStatus($authuser)) { - case 3: - # NOTICE falling through - case 0: - $response[$i]->friend_status = $usr->getSubscriptionStatus($authuser); - break; - case 1: - $response[$i]->friend_status = 2; - break; - case 2: - $response[$i]->friend_status = 1; - break; - } - break; - case "last_seen": - if ($usr->onlineStatus() == 0) { - $platform = $usr->getOnlinePlatform(true); - switch ($platform) { - case 'iphone': - $platform = 2; - break; - - case 'android': - $platform = 4; - break; - - case NULL: - $platform = 7; - break; - - default: - $platform = 1; - break; - } - - $response[$i]->last_seen = (object) [ - "platform" => $platform, - "time" => $usr->getOnline()->timestamp() - ]; - } - case "music": - $response[$i]->music = $usr->getFavoriteMusic(); - break; - case "movies": - $response[$i]->movies = $usr->getFavoriteFilms(); - break; - case "tv": - $response[$i]->tv = $usr->getFavoriteShows(); - break; - case "books": - $response[$i]->books = $usr->getFavoriteBooks(); - break; - case "city": - $response[$i]->city = $usr->getCity(); - break; - case "interests": - $response[$i]->interests = $usr->getInterests(); - break; - case "rating": - $response[$i]->rating = $usr->getRating(); - break; - } - } - - if($usr->getOnline()->timestamp() + 300 > time()) - $response[$i]->online = 1; - else - $response[$i]->online = 0; - } - } + for ($i = 0; $i < $ic; $i++) { + if ((int) $usrs[$i] != 0) { + $usr = $users->get((int) $usrs[$i]); + if (is_null($usr) || $usr->isDeleted()) { + $response[$i] = (object) [ + "id" => (int) $usrs[$i], + "first_name" => "DELETED", + "last_name" => "", + "deactivated" => "deleted", + ]; + } elseif ($usr->isBanned()) { + $response[$i] = (object) [ + "id" => $usr->getId(), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), + "deactivated" => "banned", + "ban_reason" => $usr->getBanReason(), + ]; + } elseif ($usrs[$i] == null) { + + } else { + $canView = $usr->canBeViewedBy($this->getUser()); + $response[$i] = (object) [ + "id" => $usr->getId(), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), + "is_closed" => (int) $usr->isClosed(), + "can_access_closed" => (int) $canView, + ]; + + $flds = explode(',', $fields); + foreach ($flds as $field) { + switch ($field) { + case "first_name_gen": + $response[$i]->first_name_gen = $usr->getMorphedName("genitive", false, false); + break; + case "last_name_gen": + $response[$i]->last_name_gen = $usr->getMorphedName("genitive", false, true); + break; + case "verified": + $response[$i]->verified = (int) $usr->isVerified(); + break; + case "sex": + $response[$i]->sex = $usr->isFemale() ? 1 : ($usr->isNeutral() ? 0 : 2); + break; + case "has_photo": + $response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; + break; + case "photo_max_orig": + $response[$i]->photo_max_orig = $usr->getAvatarURL(); + break; + case "photo_max": + $response[$i]->photo_max = $usr->getAvatarURL("original"); + break; + 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(); + } + + $audioStatus = $usr->getCurrentAudioStatus(); + + if ($audioStatus) { + $response[$i]->status_audio = $audioStatus->toVkApiStruct(); + } + + break; + case "nickname": + if ($usr->getShortCode() != null) { + $response[$i]->nickname = $usr->getPseudo(); + } + break; + case "screen_name": + if ($usr->getShortCode() != null) { + $response[$i]->screen_name = $usr->getShortCode(); + } + break; + case "friend_status": + $friendStatus = $authuser ? $usr->getSubscriptionStatus($authuser) : 0; + + switch ($friendStatus) { + case 3: + # NOTICE falling through + case 0: + $response[$i]->friend_status = $friendStatus; + break; + case 1: + $response[$i]->friend_status = 2; + break; + case 2: + $response[$i]->friend_status = 1; + break; + } + break; + case "last_seen": + if ($usr->onlineStatus() == 0) { + $platform = $usr->getOnlinePlatform(true); + switch ($platform) { + case 'iphone': + $platform = 2; + break; + + case 'android': + $platform = 4; + break; + + case 'web': + case null: + $platform = 7; + break; + + default: + $platform = 1; + break; + } + + $response[$i]->last_seen = (object) [ + "platform" => $platform, + "time" => $usr->getOnline()->timestamp(), + ]; + } + // no break + case "online": + if ($usr->onlineStatus() == 0) { + $response[$i]->online = 1; + + $platform = $usr->getOnlinePlatform(false); + if ($platform !== null) { + $response[$i]->online_mobile = 1; + } + } + break; + case "music": + if (!$canView) { + break; + } + + $response[$i]->music = $usr->getFavoriteMusic(); + break; + case "movies": + if (!$canView) { + break; + } + + $response[$i]->movies = $usr->getFavoriteFilms(); + break; + case "tv": + if (!$canView) { + break; + } + + $response[$i]->tv = $usr->getFavoriteShows(); + break; + case "books": + if (!$canView) { + break; + } + + $response[$i]->books = $usr->getFavoriteBooks(); + break; + case "city": + if (!$canView) { + break; + } + + $response[$i]->city = (object) [ + 'id' => 0, + 'title' => $usr->getCity(), + ]; + break; + case "interests": + if (!$canView) { + break; + } + + $response[$i]->interests = $usr->getInterests(); + break; + case "quotes": + if (!$canView) { + break; + } + + $response[$i]->quotes = $usr->getFavoriteQuote(); + break; + case "games": + if (!$canView) { + break; + } + + $response[$i]->games = $usr->getFavoriteGames(); + break; + case "email": + if (!$canView) { + break; + } + + $response[$i]->email = $usr->getContactEmail(); + break; + case "telegram": + if (!$canView) { + break; + } + + $response[$i]->telegram = $usr->getTelegram(); + break; + case "about": + if (!$canView) { + break; + } + + $response[$i]->about = $usr->getDescription(); + break; + case "rating": + if (!$canView) { + break; + } + + $response[$i]->rating = $usr->getRating(); + break; + case "counters": + case "correct_counters": + $response[$i]->counters = (object) [ + "friends" => $usr->getFriendsCount(), + "photos" => (new Photos())->getUserPhotosCount($usr), + "videos" => (new Videos())->getUserVideosCount($usr), + "audios" => (new Audios())->getUserCollectionSize($usr), + "notes" => (new Notes())->getUserNotesCount($usr), + "groups" => $usr->getClubCount(), + "online_friends" => $usr->getFriendsOnlineCount(), + "mutual_friends" => 0, // FIXME: not implemented + "user_photos" => 0, // FIXME: not implemented + "albums" => (new Albums())->getUserAlbumsCount($usr), + "followers" => $usr->getFollowersCount(), + "gifts" => $usr->getGiftCount(), + ]; + break; + case "guid": + $response[$i]->guid = $usr->getChandlerGUID(); + break; + case 'background': + $backgrounds = $usr->getBackDropPictureURLs(); + $response[$i]->background = $backgrounds; + break; + case 'reg_date': + if (!$canView) { + break; + } + + $response[$i]->reg_date = $usr->getRegistrationTime()->timestamp(); + break; + case 'is_dead': + $response[$i]->is_dead = $usr->isDead(); + break; + case 'nickname': + $response[$i]->nickname = $usr->getPseudo(); + break; + case 'blacklisted_by_me': + if (!$authuser) { + break; + } + + $response[$i]->blacklisted_by_me = (int) $usr->isBlacklistedBy($this->getUser()); + break; + case 'blacklisted': + if (!$authuser) { + break; + } + + $response[$i]->blacklisted = (int) $this->getUser()->isBlacklistedBy($usr); + break; + case "custom_fields": + if (sizeof($usrs) > 1) { + break; + } + + $c_fields = \openvk\Web\Models\Entities\UserInfoEntities\AdditionalField::getByOwner($usr->getId()); + $append_array = []; + foreach ($c_fields as $c_field) { + $append_array[] = $c_field->toVkApiStruct(); + } + + $response[$i]->custom_fields = $append_array; + break; + case "bdate": + if (!$canView) { + $response[$i]->bdate = "01.01.1970"; + break; + } + $visibility = $usr->getBirthdayPrivacy(); + $response[$i]->bdate_visibility = $visibility; + + $birthday = $usr->getBirthday(); + if ($birthday) { + switch ($visibility) { + case 1: + $response[$i]->bdate = $birthday->format('%d.%m'); + break; + case 2: + $response[$i]->bdate = $birthday->format('%d.%m.%Y'); + break; + case 0: + default: + $response[$i]->bdate = null; + break; + } + } else { + $response[$i]->bdate = null; + } + break; + } + } + + if ($usr->getOnline()->timestamp() + 300 > time()) { + $response[$i]->online = 1; + } else { + $response[$i]->online = 0; + } + } + } } return $response; } - function getFollowers(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object + public function getFollowers(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object { $offset++; $followers = []; - $users = new UsersRepo; + $users = new UsersRepo(); $this->requireUser(); - - foreach($users->get($user_id)->getFollowers($offset, $count) as $follower) + + $user = $users->get($user_id); + + if (!$user || $user->isDeleted()) { + $this->fail(14, "Invalid user"); + } + + if (!$user->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + foreach ($users->get($user_id)->getFollowers($offset, $count) as $follower) { $followers[] = $follower->getId(); + } $response = $followers; - if(!is_null($fields)) - $response = $this->get(implode(',', $followers), $fields, 0, $count); + if (!is_null($fields)) { + $response = $this->get(implode(',', $followers), $fields, 0, $count); + } return (object) [ "count" => $users->get($user_id)->getFollowersCount(), - "items" => $response + "items" => $response, ]; } - function search(string $q, - string $fields = "", - int $offset = 0, - int $count = 100, - string $city = "", - string $hometown = "", - int $sex = 2, - int $status = 0, # это про marital status - bool $online = false, - # дальше идут параметры которых нету в vkapi но есть на сайте - string $profileStatus = "", # а это уже нормальный статус - int $sort = 0, - int $before = 0, - int $politViews = 0, - int $after = 0, - string $interests = "", - string $fav_music = "", - string $fav_films = "", - string $fav_shows = "", - string $fav_books = "", - string $fav_quotes = "" - ) - { - $users = new UsersRepo; - - $sortg = "id ASC"; + public function search( + string $q, + string $fields = "", + int $offset = 0, + int $count = 100, + string $city = "", + string $hometown = "", + int $sex = 3, + int $status = 0, # marital_status + bool $online = false, + # non standart params: + int $sort = 0, + int $polit_views = 0, + string $fav_music = "", + string $fav_films = "", + string $fav_shows = "", + 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"); + } - $nfilds = $fields; + $users = new UsersRepo(); + $output_sort = ['type' => 'id', 'invert' => false]; + $output_params = [ + "ignore_private" => true, + ]; - switch($sort) { + switch ($sort) { + default: case 0: - $sortg = "id DESC"; + $output_sort = ['type' => 'id', 'invert' => false]; break; case 1: - $sortg = "id ASC"; - break; - case 2: - $sortg = "first_name DESC"; - break; - case 3: - $sortg = "first_name ASC"; + $output_sort = ['type' => 'id', 'invert' => true]; break; case 4: - $sortg = "rating DESC"; + $output_sort = ['type' => 'rating', 'invert' => false]; + break; + } - if(!str_contains($nfilds, "rating")) { - $nfilds .= "rating"; - } + if (!empty($city)) { + $output_params['city'] = $city; + } - break; - case 5: - $sortg = "rating DESC"; + if (!empty($hometown)) { + $output_params['hometown'] = $hometown; + } - if(!str_contains($nfilds, "rating")) { - $nfilds .= "rating"; - } + if ($sex != 3) { + $output_params['gender'] = $sex; + } - break; + if ($status != 0) { + $output_params['marital_status'] = $status; } - $array = []; + if ($polit_views != 0) { + $output_params['polit_views'] = $polit_views; + } - $parameters = [ - "city" => !empty($city) ? $city : NULL, - "hometown" => !empty($hometown) ? $hometown : NULL, - "gender" => $sex < 2 ? $sex : NULL, - "maritalstatus" => (bool)$status ? $status : NULL, - "politViews" => (bool)$politViews ? $politViews : NULL, - "is_online" => $online ? 1 : NULL, - "status" => !empty($profileStatus) ? $profileStatus : NULL, - "before" => $before != 0 ? $before : NULL, - "after" => $after != 0 ? $after : NULL, - "interests" => !empty($interests) ? $interests : NULL, - "fav_music" => !empty($fav_music) ? $fav_music : NULL, - "fav_films" => !empty($fav_films) ? $fav_films : NULL, - "fav_shows" => !empty($fav_shows) ? $fav_shows : NULL, - "fav_books" => !empty($fav_books) ? $fav_books : NULL, - "fav_quotes" => !empty($fav_quotes) ? $fav_quotes : NULL, - ]; + if (!empty($interests)) { + $output_params['interests'] = $interests; + } + + if (!empty($fav_music)) { + $output_params['fav_music'] = $fav_music; + } + + if (!empty($fav_films)) { + $output_params['fav_films'] = $fav_films; + } + + if (!empty($fav_shows)) { + $output_params['fav_shows'] = $fav_shows; + } + + if (!empty($fav_books)) { + $output_params['fav_books'] = $fav_books; + } + + if ($online) { + $output_params['is_online'] = 1; + } - $find = $users->find($q, $parameters, $sortg); + $array = []; + $find = $users->find($q, $output_params, $output_sort); - foreach ($find as $user) + foreach ($find->offsetLimit($offset, $count) as $user) { $array[] = $user->getId(); + } + + if (!$array || sizeof($array) < 1) { + return (object) [ + "count" => 0, + "items" => [], + ]; + } return (object) [ - "count" => $find->size(), - "items" => $this->get(implode(',', $array), $nfilds, $offset, $count) + "count" => $find->size(), + "items" => $this->get(implode(',', $array), $fields), ]; } + + public function report(int $user_id, string $type = "spam", string $comment = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if ($user_id == $this->getUser()->getId()) { + $this->fail(12, "Can't report yourself."); + } + + if (sizeof(iterator_to_array((new Reports())->getDuplicates("user", $user_id, null, $this->getUser()->getId()))) > 0) { + return 1; + } + + $report = new Report(); + $report->setUser_id($this->getUser()->getId()); + $report->setTarget_id($user_id); + $report->setType("user"); + $report->setReason($comment); + $report->setCreated(time()); + $report->save(); + + return 1; + } } diff --git a/VKAPI/Handlers/Utils.php b/VKAPI/Handlers/Utils.php index 5350a64f6..000ebe5ef 100644 --- a/VKAPI/Handlers/Utils.php +++ b/VKAPI/Handlers/Utils.php @@ -1,46 +1,62 @@ -getMatchingRoute("/$screen_name")[0]->presenter !== "UnknownTextRouteStrategy") { - if(substr($screen_name, 0, strlen("id")) === "id") { - return (object) [ - "object_id" => (int) substr($screen_name, strlen("id")), - "type" => "user" - ]; - } else if(substr($screen_name, 0, strlen("club")) === "club") { - return (object) [ - "object_id" => (int) substr($screen_name, strlen("club")), - "type" => "group" - ]; - } - } else { - $user = (new Users)->getByShortURL($screen_name); - if($user) { - return (object) [ - "object_id" => $user->getId(), - "type" => "user" - ]; - } - - $club = (new Clubs)->getByShortURL($screen_name); - if($club) { - return (object) [ - "object_id" => $club->getId(), - "type" => "group" - ]; - } - - return (object) []; - } - } -} +getMatchingRoute("/$screen_name")[0]->presenter !== "UnknownTextRouteStrategy") { + if (substr($screen_name, 0, strlen("id")) === "id") { + return (object) [ + "object_id" => (int) substr($screen_name, strlen("id")), + "type" => "user", + ]; + } elseif (substr($screen_name, 0, strlen("club")) === "club") { + return (object) [ + "object_id" => (int) substr($screen_name, strlen("club")), + "type" => "group", + ]; + } else { + $this->fail(104, "Not found"); + } + } else { + $user = (new Users())->getByShortURL($screen_name); + if ($user) { + return (object) [ + "object_id" => $user->getId(), + "type" => "user", + ]; + } + + $club = (new Clubs())->getByShortURL($screen_name); + if ($club) { + return (object) [ + "object_id" => $club->getId(), + "type" => "group", + ]; + } + + $this->fail(104, "Not found"); + } + } + + public function resolveGuid(string $guid): object + { + $user = (new Users())->getByChandlerUserId($guid); + if (is_null($user)) { + $this->fail(104, "Not found"); + } + + return $user->toVkApiStruct($this->getUser()); + } +} diff --git a/VKAPI/Handlers/VKAPIRequestHandler.php b/VKAPI/Handlers/VKAPIRequestHandler.php index d2fcfc74c..b2e92a053 100644 --- a/VKAPI/Handlers/VKAPIRequestHandler.php +++ b/VKAPI/Handlers/VKAPIRequestHandler.php @@ -1,5 +1,9 @@ -user = $user; $this->platform = $platform; } - - protected function fail(int $code, string $message): void + + protected function fail(int $code, string $message): never { throw new APIErrorException($message, $code); } - + + protected function failTooOften(): never + { + $this->fail(9, "Rate limited"); + } + protected function getUser(): ?User { return $this->user; } - + protected function getPlatform(): ?string { - return $this->platform; + return $this->platform ?? ""; } - + protected function userAuthorized(): bool { return !is_null($this->getUser()); } - + protected function requireUser(): void { - if(!$this->userAuthorized()) + if (!$this->userAuthorized()) { $this->fail(5, "User authorization failed: no access_token passed."); + } } - + protected function willExecuteWriteAction(): void { - $ip = (new IPs)->get(CONNECTING_IP); + $ip = (new IPs())->get(CONNECTING_IP); $res = $ip->rateLimit(); - - if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) { - if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) { + + if (!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) { + if ($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) { $this->user->ban("User account has been suspended for breaking API terms of service", false); $this->fail(18, "User account has been suspended due to repeated violation of API rate limits."); } - + $this->fail(29, "You have been rate limited."); } } + + protected function createHandler(string $handlerClass): VKAPIRequestHandler + { + if (!class_exists($handlerClass)) { + throw new \Exception(`Class $handlerClass not found`); + } + + return new $handlerClass($this->getUser(), $this->getPlatform()); + } + + public function generateItems(int $count, array $items) + { + if (VKAPI_DECL_VER_MAJOR >= 5) { + return (object) [ + 'count' => $count, + 'items' => $items, + ]; + } else { + array_unshift($items, $count); + return $items; + } + } } diff --git a/VKAPI/Handlers/Video.php b/VKAPI/Handlers/Video.php index 740ccd548..43d7a4687 100755 --- a/VKAPI/Handlers/Video.php +++ b/VKAPI/Handlers/Video.php @@ -1,5 +1,9 @@ -requireUser(); - if ($videos) { - $vids = explode(',', $videos); - - foreach($vids as $vid) - { + if (!empty($videos)) { + $vids = array_unique(explode(',', $videos)); + + if (sizeof($vids) > 100) { + $this->fail(15, "Too many ids given"); + } + + $profiles = []; + $groups = []; + $items = []; + + foreach ($vids as $vid) { $id = explode("_", $vid); - - $items = []; - - $video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1])); - if($video) { - $items[] = $video->getApiStructure(); + + $video = (new VideosRepo())->getByOwnerAndVID(intval($id[0]), intval($id[1])); + if ($video && !$video->isDeleted()) { + $out_video = $video->getApiStructure($this->getUser())->video; + $items[] = $out_video; + if ($out_video['owner_id']) { + if ($out_video['owner_id'] > 0) { + $profiles[] = $out_video['owner_id']; + } else { + $groups[] = abs($out_video['owner_id']); + } + } + } + } + + if ($extended == 1) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach ($profiles as $prof) { + $profile = (new UsersRepo())->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); } + + foreach ($groups as $gr) { + $group = (new ClubsRepo())->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, + ]; } - + return (object) [ "count" => count($items), - "items" => $items + "items" => $items, ]; } else { - if ($owner_id > 0) - $user = (new UsersRepo)->get($owner_id); - else - $this->fail(1, "Not implemented"); - - $videos = (new VideosRepo)->getByUser($user, $offset + 1, $count); - $videosCount = (new VideosRepo)->getUserVideosCount($user); - + if ($owner_id > 0) { + $user = (new UsersRepo())->get($owner_id); + } else { + $this->fail(1, "Not implemented"); + } + + if (!$user || $user->isDeleted()) { + $this->fail(14, "Invalid user"); + } + + if (!$user->getPrivacyPermission('videos.read', $this->getUser())) { + $this->fail(21, "This user chose to hide his videos."); + } + + $videos = (new VideosRepo())->getByUserLimit($user, $offset, $count); + $videosCount = (new VideosRepo())->getUserVideosCount($user); + $items = []; + $profiles = []; + $groups = []; foreach ($videos as $video) { - $items[] = $video->getApiStructure(); + $video = $video->getApiStructure($this->getUser())->video; + $items[] = $video; + if ($video['owner_id']) { + if ($video['owner_id'] > 0) { + $profiles[] = $video['owner_id']; + } else { + $groups[] = abs($video['owner_id']); + } + } + } + + if ($extended == 1) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach ($profiles as $prof) { + $profile = (new UsersRepo())->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); + } + + foreach ($groups as $gr) { + $group = (new ClubsRepo())->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => $videosCount, + "items" => $items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, + ]; } - + return (object) [ "count" => $videosCount, - "items" => $items + "items" => $items, + ]; + } + } + + public function search(string $q = '', int $sort = 0, int $offset = 0, int $count = 10, bool $extended = false, string $fields = ''): object + { + $this->requireUser(); + + $params = []; + $db_sort = ['type' => 'id', 'invert' => false]; + $videos = (new VideosRepo())->find($q, $params, $db_sort); + $items = iterator_to_array($videos->offsetLimit($offset, $count)); + $count = $videos->size(); + + $return_items = []; + $profiles = []; + $groups = []; + foreach ($items as $item) { + $return_item = $item->getApiStructure($this->getUser()); + $return_item = $return_item->video; + $return_items[] = $return_item; + + if ($return_item['owner_id']) { + if ($return_item['owner_id'] > 0) { + $profiles[] = $return_item['owner_id']; + } else { + $groups[] = abs($return_item['owner_id']); + } + } + } + + if ($extended) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach ($profiles as $prof) { + $profile = (new UsersRepo())->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); + } + + foreach ($groups as $gr) { + $group = (new ClubsRepo())->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => $count, + "items" => $return_items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, ]; } + + return (object) [ + "count" => $count, + "items" => $return_items, + ]; } } diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 6b78a0b0e..804e51a8c 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -1,7 +1,11 @@ -requireUser(); - $posts = new PostsRepo; + $posts = new PostsRepo(); $items = []; $profiles = []; $groups = []; - $cnt = $posts->getPostCountOnUserWall($owner_id); + $cnt = 0; - if ($owner_id > 0) - $wallOnwer = (new UsersRepo)->get($owner_id); - else - $wallOnwer = (new ClubsRepo)->get($owner_id * -1); + if ($owner_id > 0) { + $wallOnwer = (new UsersRepo())->get($owner_id); + } else { + $wallOnwer = (new ClubsRepo())->get($owner_id * -1); + } - if ($owner_id > 0) - if(!$wallOnwer || $wallOnwer->isDeleted()) + if ($owner_id > 0) { + if (!$wallOnwer || $wallOnwer->isDeleted()) { $this->fail(18, "User was deleted or banned"); - else - if(!$wallOnwer) - $this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls + } + } - foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) { + if (!$wallOnwer->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } elseif (!$wallOnwer) { + $this->fail(15, "Access denied: wall is disabled"); + } // Don't search for logic here pls + + $iteratorv = null; + + switch ($filter) { + case "all": + $iteratorv = $posts->getPostsFromUsersWall($owner_id, 1, $count, $offset); + $cnt = $posts->getPostCountOnUserWall($owner_id); + break; + case "owner": + $iteratorv = $posts->getOwnersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOwnersCountOnUserWall($owner_id); + break; + case "others": + $iteratorv = $posts->getOthersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOthersCountOnUserWall($owner_id); + break; + case "postponed": + $this->fail(42, "Postponed posts are not implemented."); + break; + case "suggests": + if ($owner_id < 0) { + if ($wallOnwer->getWallType() != 2) { + $this->fail(125, "Group's wall type is open or closed"); + } + + if ($wallOnwer->canBeModifiedBy($this->getUser())) { + $iteratorv = $posts->getSuggestedPosts($owner_id * -1, 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCount($owner_id * -1); + } else { + $iteratorv = $posts->getSuggestedPostsByUser($owner_id * -1, $this->getUser()->getId(), 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCountByUser($owner_id * -1, $this->getUser()->getId()); + } + } else { + $this->fail(528, "Suggested posts avaiable only at groups"); + } + + break; + default: + $this->fail(254, "Invalid filter"); + break; + } + + $iteratorv = iterator_to_array($iteratorv); + + foreach ($iteratorv as $post) { $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; $repost = []; - foreach($post->getChildren() as $attachment) { - if($attachment instanceof \openvk\Web\Models\Entities\Photo) { - if($attachment->isDeleted()) + 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\Poll) { + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $this->getUser()); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { - $attachments[] = $attachment->getApiStructure(); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { - $attachments[] = $attachment->toVkApiStruct(); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure($this->getUser()); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Note) { + if (VKAPI_DECL_VER === '4.100') { + $attachments[] = $attachment->toVkApiStruct(); + } else { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct(), + ]; + } + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; - foreach($attachment->getChildren() as $repostAttachment) { - if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { - if($repostAttachment->isDeleted()) + foreach ($attachment->getChildren() as $repostAttachment) { + if ($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { + if ($repostAttachment->isDeleted()) { continue; + } $repostAttachments[] = $this->getApiPhoto($repostAttachment); /* Рекурсии, сука! Заказывали? */ } } - if ($attachment->isPostedOnBehalfOfGroup()) + if ($attachment->isPostedOnBehalfOfGroup()) { $groups[] = $attachment->getOwner()->getId(); - else - $profiles[] = $attachment->getOwner()->getId(); - - $post_source = []; - - if($attachment->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; } else { - $post_source = (object)[ - "type" => "api", - "platform" => $attachment->getPlatform(true) - ]; + $profiles[] = $attachment->getOwner()->getId(); } $repost[] = [ @@ -92,93 +156,144 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 "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", + "post_type" => $attachment->getVkApiType(), "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => $post_source, + "post_source" => $attachment->getPostSourceInfo(), ]; + + if ($attachment->getTargetWall() > 0) { + $profiles[] = $attachment->getTargetWall(); + } else { + $groups[] = abs($attachment->getTargetWall()); + } + if ($post->isSigned()) { + $profiles[] = $attachment->getOwner()->getId(); + } } } - $post_source = []; - - if($post->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $post->getPlatform(true) - ]; + $signerId = null; + if ($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); } - $items[] = (object)[ + # TODO "can_pin", "copy_history" и прочее не должны возвращаться, если равны null или false + # Ну и ещё всё надо перенести в toVkApiStruct, а то слишком много дублированного кода + + $post_temp_obj = (object) [ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $post->getVkApiType(), "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 - "is_archived" => false, - "is_pinned" => $post->isPinned(), - "is_explicit" => $post->isExplicit(), + "can_edit" => (int) $post->canBeEditedBy($this->getUser()), + "can_delete" => (int) $post->canBeDeletedBy($this->getUser()), + "can_pin" => (int) $post->canBePinnedBy($this->getUser()), + "can_archive" => 0, # TODO MAYBE + "is_archived" => 0, + "is_pinned" => (int) $post->isPinned(), + "is_explicit" => (int) $post->isExplicit(), "attachments" => $attachments, - "post_source" => $post_source, - "comments" => (object)[ + "post_source" => $post->getPostSourceInfo(), + "comments" => (object) [ "count" => $post->getCommentsCount(), - "can_post" => 1 + "can_post" => 1, ], - "likes" => (object)[ + "likes" => (object) [ "count" => $post->getLikesCount(), "user_likes" => (int) $post->hasLikeFrom($this->getUser()), "can_like" => 1, "can_publish" => 1, ], - "reposts" => (object)[ + "reposts" => (object) [ "count" => $post->getRepostCount(), - "user_reposted" => 0 - ] + "user_reposted" => 0, + ], ]; - if ($from_id > 0) + if ($post->hasSource()) { + $post_temp_obj->copyright = $post->getVkApiCopyright(); + } + + if ($signerId) { + $post_temp_obj->signer_id = $signerId; + } + + if ($post->isDeactivationMessage()) { + $post_temp_obj->final_post = 1; + } + + if ($post->getGeo()) { + $post_temp_obj->geo = $post->getVkApiGeo(); + } + + $items[] = $post_temp_obj; + + if ($from_id > 0) { $profiles[] = $from_id; - else + } else { $groups[] = $from_id * -1; + } + + $owner_id = $post->getTargetWall(); + if ($owner_id > 0) { + $profiles[] = $owner_id; + } else { + $groups[] = $owner_id * -1; + } - $attachments = NULL; # free attachments so it will not clone everythingg + if ($post->isSigned()) { + $profiles[] = $post->getOwner(false)->getId(); + } + + $attachments = null; # free attachments so it will not clone everythingg } - if($extended == 1) { + if ($rss == 1) { + $channel = new \Bhaktaraz\RSSGenerator\Channel(); + $channel->title($wallOnwer->getCanonicalName() . " — " . OPENVK_ROOT_CONF['openvk']['appearance']['name']) + ->description('Wall of ' . $wallOnwer->getCanonicalName()) + ->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/wall" . $wallOnwer->getRealId()); + + foreach ($iteratorv as $item) { + $output = $item->toRss(); + $output->appendTo($channel); + } + + return $channel; + } + + if ($extended == 1) { $profiles = array_unique($profiles); $groups = array_unique($groups); $profilesFormatted = []; $groupsFormatted = []; - foreach($profiles as $prof) { - $user = (new UsersRepo)->get($prof); - $profilesFormatted[] = (object)[ + foreach ($profiles as $prof) { + $user = (new UsersRepo())->get($prof); + $profilesFormatted[] = (object) [ "first_name" => $user->getFirstName(), "id" => $user->getId(), "last_name" => $user->getLastName(), - "can_access_closed" => false, - "is_closed" => false, - "sex" => $user->isFemale() ? 1 : 2, + "can_access_closed" => (int) $user->canBeViewedBy($this->getUser()), + "is_closed" => (int) $user->isClosed(), + "sex" => $user->isFemale() ? 1 : ($user->isNeutral() ? 0 : 2), "screen_name" => $user->getShortCode(), "photo_50" => $user->getAvatarUrl(), "photo_100" => $user->getAvatarUrl(), "online" => $user->isOnline(), - "verified" => $user->isVerified() + "verified" => $user->isVerified(), ]; } - foreach($groups as $g) { - $group = (new ClubsRepo)->get($g); - $groupsFormatted[] = (object)[ + foreach ($groups as $g) { + $group = (new ClubsRepo())->get($g); + $groupsFormatted[] = (object) [ "id" => $group->getId(), "name" => $group->getName(), "screen_name" => $group->getShortCode(), @@ -187,7 +302,7 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 "photo_50" => $group->getAvatarUrl(), "photo_100" => $group->getAvatarUrl(), "photo_200" => $group->getAvatarUrl(), - "verified" => $group->isVerified() + "verified" => $group->isVerified(), ]; } @@ -195,18 +310,19 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 "count" => $cnt, "items" => $items, "profiles" => $profilesFormatted, - "groups" => $groupsFormatted + "groups" => $groupsFormatted, ]; - } else + } else { return (object) [ "count" => $cnt, - "items" => $items + "items" => $items, ]; + } } - function getById(string $posts, int $extended = 0, string $fields = "", User $user = NULL) + public function getById(string $posts, int $extended = 0, string $fields = "", User $user = null) { - if($user == NULL) { + if ($user == null) { $this->requireUser(); $user = $this->getUser(); # костыли костыли крылышки } @@ -217,49 +333,62 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us $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 && !$post->isDeleted()) { + $post = (new PostsRepo())->getPostById(intval($id[0]), intval($id[1]), true); + + if ($post && !$post->isDeleted()) { + if (!$post->canBeViewedBy($user)) { + continue; + } + + if ($post->getSuggestionType() != 0 && !$post->canBeEditedBy($this->getUser())) { + continue; + } + $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; $repost = []; // чел высрал семь сигарет 😳 помянем 🕯 - foreach($post->getChildren() as $attachment) { - if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + 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\Poll) { + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $user); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { - $attachments[] = $attachment->getApiStructure(); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { - $attachments[] = $attachment->toVkApiStruct(); - } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure($this->getUser()); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Note) { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct(), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; - foreach($attachment->getChildren() as $repostAttachment) { - if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { - if($attachment->isDeleted()) + foreach ($attachment->getChildren() as $repostAttachment) { + if ($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { + if ($attachment->isDeleted()) { continue; + } $repostAttachments[] = $this->getApiPhoto($repostAttachment); /* Рекурсии, сука! Заказывали? */ } } - if ($attachment->isPostedOnBehalfOfGroup()) + if ($attachment->isPostedOnBehalfOfGroup()) { $groups[] = $attachment->getOwner()->getId(); - else - $profiles[] = $attachment->getOwner()->getId(); - - $post_source = []; - - if($attachment->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; } else { - $post_source = (object)[ - "type" => "api", - "platform" => $attachment->getPlatform(true) - ]; + $profiles[] = $attachment->getOwner()->getId(); } $repost[] = [ @@ -270,92 +399,136 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us "post_type" => "post", "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => $post_source, + "post_source" => $attachment->getPostSourceInfo(), ]; + + if ($attachment->getTargetWall() > 0) { + $profiles[] = $attachment->getTargetWall(); + } else { + $groups[] = abs($attachment->getTargetWall()); + } + if ($post->isSigned()) { + $profiles[] = $attachment->getOwner()->getId(); + } } } - $post_source = []; - - if($post->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $post->getPlatform(true) - ]; + $signerId = null; + if ($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); } - $items[] = (object)[ + $post_temp_obj = (object) [ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), + "post_id" => $post->getVirtualId(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $post->getVkApiType(), "text" => $post->getText(false), "copy_history" => $repost, - "can_edit" => 0, # TODO + "can_edit" => $post->canBeEditedBy($this->getUser()), "can_delete" => $post->canBeDeletedBy($user), "can_pin" => $post->canBePinnedBy($user), "can_archive" => false, # TODO MAYBE "is_archived" => false, "is_pinned" => $post->isPinned(), "is_explicit" => $post->isExplicit(), - "post_source" => $post_source, + "post_source" => $post->getPostSourceInfo(), "attachments" => $attachments, - "comments" => (object)[ + "comments" => (object) [ "count" => $post->getCommentsCount(), - "can_post" => 1 + "can_post" => 1, ], - "likes" => (object)[ + "likes" => (object) [ "count" => $post->getLikesCount(), "user_likes" => (int) $post->hasLikeFrom($user), "can_like" => 1, "can_publish" => 1, ], - "reposts" => (object)[ + "reposts" => (object) [ "count" => $post->getRepostCount(), - "user_reposted" => 0 - ] + "user_reposted" => 0, + ], ]; - if ($from_id > 0) + if ($post->hasSource()) { + $post_temp_obj->copyright = $post->getVkApiCopyright(); + } + + if ($signerId) { + $post_temp_obj->signer_id = $signerId; + } + + if ($post->isDeactivationMessage()) { + $post_temp_obj->final_post = 1; + } + + if ($post->getGeo()) { + $post_temp_obj->geo = $post->getVkApiGeo(); + } + + $items[] = $post_temp_obj; + + if ($from_id > 0) { $profiles[] = $from_id; - else + } else { $groups[] = $from_id * -1; + } - $attachments = NULL; # free attachments so it will not clone everything - $repost = NULL; # same + $owner_id = $post->getTargetWall(); + if ($owner_id > 0) { + $profiles[] = $owner_id; + } else { + $groups[] = $owner_id * -1; + } + + if ($post->isSigned()) { + $profiles[] = $post->getOwner(false)->getId(); + } + + $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) { - $user = (new UsersRepo)->get($prof); - $profilesFormatted[] = (object)[ - "first_name" => $user->getFirstName(), - "id" => $user->getId(), - "last_name" => $user->getLastName(), - "can_access_closed" => false, - "is_closed" => false, - "sex" => $user->isFemale() ? 1 : 2, - "screen_name" => $user->getShortCode(), - "photo_50" => $user->getAvatarUrl(), - "photo_100" => $user->getAvatarUrl(), - "online" => $user->isOnline(), - "verified" => $user->isVerified() - ]; + foreach ($profiles as $prof) { + $user = (new UsersRepo())->get($prof); + if ($user) { + $profilesFormatted[] = (object) [ + "first_name" => $user->getFirstName(), + "id" => $user->getId(), + "last_name" => $user->getLastName(), + "can_access_closed" => (int) $user->canBeViewedBy($this->getUser()), + "is_closed" => $user->isClosed(), + "sex" => $user->isFemale() ? 1 : 2, + "screen_name" => $user->getShortCode(), + "photo_50" => $user->getAvatarUrl(), + "photo_100" => $user->getAvatarUrl(), + "online" => $user->isOnline(), + "verified" => $user->isVerified(), + ]; + } else { + $profilesFormatted[] = (object) [ + "id" => (int) $prof, + "first_name" => "DELETED", + "last_name" => "", + "deactivated" => "deleted", + ]; + } } - foreach($groups as $g) { - $group = (new ClubsRepo)->get($g); - $groupsFormatted[] = (object)[ + foreach ($groups as $g) { + $group = (new ClubsRepo())->get($g); + $groupsFormatted[] = (object) [ "id" => $group->getId(), "name" => $group->getName(), "screen_name" => $group->getShortCode(), @@ -364,204 +537,360 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us "photo_50" => $group->getAvatarUrl(), "photo_100" => $group->getAvatarUrl(), "photo_200" => $group->getAvatarUrl(), - "verified" => $group->isVerified() + "verified" => $group->isVerified(), ]; } return (object) [ - "items" => (array)$items, - "profiles" => (array)$profilesFormatted, - "groups" => (array)$groupsFormatted + "items" => (array) $items, + "profiles" => (array) $profilesFormatted, + "groups" => (array) $groupsFormatted, ]; - } else + } else { return (object) [ - "items" => (array)$items + "items" => (array) $items, ]; + } } - function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = ""): object - { + public function post( + string $owner_id, + string $message = "", + string $copyright = "", + int $from_group = 0, + int $signed = 0, + string $attachments = "", + int $post_id = 0, + int $explicit = 0, + float $lat = null, + float $long = null, + string $place_name = '' + ): object { $this->requireUser(); $this->willExecuteWriteAction(); $owner_id = intval($owner_id); - $wallOwner = ($owner_id > 0 ? (new UsersRepo)->get($owner_id) : (new ClubsRepo)->get($owner_id * -1)) + $wallOwner = ($owner_id > 0 ? (new UsersRepo())->get($owner_id) : (new ClubsRepo())->get($owner_id * -1)) ?? $this->fail(18, "User was deleted or banned"); - if($owner_id > 0) - $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->getUser()); - else if($owner_id < 0) - if($wallOwner->canBeModifiedBy($this->getUser())) + if ($owner_id > 0) { + $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->getUser()) && $wallOwner->canBeViewedBy($this->getUser()); + } elseif ($owner_id < 0) { + if ($wallOwner->canBeModifiedBy($this->getUser())) { $canPost = true; - else + } else { $canPost = $wallOwner->canPost(); - else + } + } else { $canPost = false; + } + + if ($canPost == false) { + $this->fail(15, "Access denied"); + } + + if ($post_id > 0) { + if ($owner_id > 0) { + $this->fail(62, "Suggested posts available only at groups"); + } + + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + + if (!$post || $post->isDeleted()) { + $this->fail(32, "Invald post"); + } + + if ($post->getSuggestionType() == 0) { + $this->fail(20, "Post is not suggested"); + } + + if ($post->getSuggestionType() == 2) { + $this->fail(16, "Post is declined"); + } + + if (!$post->canBePinnedBy($this->getUser())) { + $this->fail(51, "Access denied"); + } + + $author = $post->getOwner(); + $flags = 0; + $flags |= 0b10000000; + + if ($signed == 1) { + $flags |= 0b01000000; + } + + $post->setSuggested(0); + $post->setCreated(time()); + $post->setFlags($flags); + + if (!empty($message) && iconv_strlen($message) > 0) { + $post->setContent($message); + } + + $post->save(); + + if ($author->getId() != $this->getUser()->getId()) { + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + } - if($canPost == false) $this->fail(15, "Access denied"); + return (object) ["post_id" => $post->getVirtualId()]; + } $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; - if($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { + if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { $manager = $wallOwner->getManager($this->getUser()); - if($manager) + if ($manager) { $anon = $manager->isHidden(); - elseif($this->getUser()->getId() === $wallOwner->getOwner()->getId()) + } elseif ($this->getUser()->getId() === $wallOwner->getOwner()->getId()) { $anon = $wallOwner->isOwnerHidden(); + } } else { $anon = false; } $flags = 0; - if($from_group == 1 && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->getUser())) + if ($from_group == 1 && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->getUser())) { $flags |= 0b10000000; - if($signed == 1) + } + if ($signed == 1) { $flags |= 0b01000000; + } - if(empty($message) && empty($attachments)) + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio', 'doc']); + $final_attachments = []; + $should_be_suggested = $owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2; + foreach ($parsed_attachments as $attachment) { + if ($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if ((empty($message) && (empty($attachments) || sizeof($final_attachments) < 1))) { $this->fail(100, "Required parameter 'message' missing."); + } try { - $post = new Post; + $post = new Post(); $post->setOwner($this->getUser()->getId()); $post->setWall($owner_id); $post->setCreated(time()); $post->setContent($message); $post->setFlags($flags); $post->setApi_Source_Name($this->getPlatform()); + + if ($explicit === 1) { + $post->setNsfw($explicit == 1); + } + + if (!is_null($copyright) && !empty($copyright)) { + try { + $post->setSource($copyright); + } catch (\Throwable) { + } + } + + /*$info = file_get_contents("https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=jsonv2", false, stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => implode("\r\n", [ + 'User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2', + "Referer: https://$_SERVER[SERVER_NAME]/" + ]) + ] + ])); + + if ($info) { + $info = json_decode($info, true, JSON_UNESCAPED_UNICODE); + if (key_exists("place_id", $info)) { + $geo["name"] = $info["name"] ?? $info["display_name"]; + } + }*/ + if ($lat && $long) { + if (($lat > 90 || $lat < -90) || ($long > 180 || $long < -180)) { + $this->fail(-785, 'Invalid geo info'); + } + + $latitude = number_format((float) $lat, 8, ".", ''); + $longitude = number_format((float) $long, 8, ".", ''); + + $res = [ + 'lat' => $latitude, + 'lng' => $longitude, + ]; + if ($place_name && mb_strlen($place_name) > 0) { + $res['name'] = $place_name; + } else { + $res['name'] = 'Geopoint'; + } + + $post->setGeo($res); + $post->setGeo_Lat($latitude); + $post->setGeo_Lon($longitude); + } + + if ($should_be_suggested) { + $post->setSuggested(1); + } + + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post")) { + $this->failTooOften(); + } + $post->save(); - } catch(\LogicException $ex) { + } catch (\LogicException $ex) { $this->fail(100, "One of the parameters specified was missing or invalid"); } - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - # Аттачи такого вида: [тип][id владельца]_[id вложения] - # Пример: photo1_1 - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - elseif(str_contains($attac, "note")) - $attachmentType = "note"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Invalid photo"); - if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) - $this->fail(43, "Access to photo denied"); - - $post->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) - $this->fail(43, "Access to video denied"); - - $post->attach($attacc); - } elseif($attachmentType == "note") { - $attacc = (new NotesRepo)->getNoteById($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Note does not exist"); - if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) - $this->fail(11, "Access to note denied"); - - $post->attach($attacc); - } - } + foreach ($final_attachments as $attachment) { + $post->attach($attachment); } - if($wall > 0 && $wall !== $this->user->identity->getId()) - (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); + if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) { + (new WallPostNotification($wallOwner, $post, $this->getUser()))->emit(); + } - return (object)["post_id" => $post->getVirtualId()]; + return (object) ["post_id" => $post->getVirtualId()]; } - function repost(string $object, string $message = "", int $group_id = 0) { + public function repost(string $object, string $message = "", string $attachments = "", int $group_id = 0, int $as_group = 0, int $signed = 0) + { $this->requireUser(); $this->willExecuteWriteAction(); - $postArray; - if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) + $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"); - - $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; + } + + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); + $final_attachments = []; + foreach ($parsed_attachments as $attachment) { + if ($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + $repost_entity = null; + $repost_type = $postArray[1]; + switch ($repost_type) { + default: + case 'wall': + $repost_entity = (new PostsRepo())->getPostById((int) $postArray[2], (int) $postArray[3]); + break; + case 'photo': + $repost_entity = (new PhotosRepo())->getByOwnerAndVID((int) $postArray[2], (int) $postArray[3]); + break; + case 'video': + $repost_entity = (new VideosRepo())->getByOwnerAndVID((int) $postArray[2], (int) $postArray[3]); + break; + } + + if (!$repost_entity || $repost_entity->isDeleted() || !$repost_entity->canBeViewedBy($this->getUser())) { + $this->fail(100, "One of the parameters specified was missing or invalid"); + } + + $nPost = new Post(); $nPost->setOwner($this->user->getId()); - - if($group_id > 0) { - $club = (new ClubsRepo)->get($group_id); - if(!$club) + + if ($group_id > 0) { + $club = (new ClubsRepo())->get($group_id); + if (!$club) { $this->fail(42, "Invalid group"); - - if(!$club->canBeModifiedBy($this->user)) + } + + if (!$club->canBeModifiedBy($this->user)) { $this->fail(16, "Access to group denied"); - - $nPost->setWall($group_id * -1); + } + + $nPost->setWall($club->getRealId()); + $flags = 0; + if ($as_group === 1 || $signed === 1) { + $flags |= 0b10000000; + } + + if ($signed === 1) { + $flags |= 0b01000000; + } + + $nPost->setFlags($flags); } else { $nPost->setWall($this->user->getId()); } - + $nPost->setContent($message); $nPost->setApi_Source_Name($this->getPlatform()); $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))->emit(); + + $nPost->attach($repost_entity); + + foreach ($final_attachments as $attachment) { + $nPost->attach($attachment); + } + + if ($repost_type == 'wall' && $repost_entity->getOwner(false)->getId() !== $this->user->getId() && !($repost_entity->getOwner() instanceof Club)) { + (new RepostNotification($repost_entity->getOwner(false), $repost_entity, $this->user))->emit(); + } + + $repost_count = 1; + if ($repost_type == 'wall') { + $repost_count = $repost_entity->getRepostCount(); + } return (object) [ "success" => 1, // 👍 "post_id" => $nPost->getVirtualId(), - "reposts_count" => $post->getRepostCount(), - "likes_count" => $post->getLikesCount() + "pretty_id" => $nPost->getPrettyId(), + "reposts_count" => $repost_count, + "likes_count" => $repost_entity->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) { + public function getComments(int $owner_id, int $post_id, int $need_likes = 1, 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"); + $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->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } - $comments = (new CommentsRepo)->getCommentsByTarget($post, $offset+1, $count, $sort == "desc" ? "DESC" : "ASC"); + $comments = (new CommentsRepo())->getCommentsByTarget($post, $offset + 1, $count, $sort == "desc" ? "DESC" : "ASC"); $items = []; $profiles = []; - foreach($comments as $comment) { + foreach ($comments as $comment) { $owner = $comment->getOwner(); $oid = $owner->getId(); - if($owner instanceof Club) + if ($owner instanceof Club) { $oid *= -1; + } $attachments = []; - foreach($comment->getChildren() as $attachment) { - if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + foreach ($comment->getChildren() as $attachment) { + if ($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); - } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -569,9 +898,11 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ "id" => $comment->getId(), "from_id" => $oid, "date" => $comment->getPublicationTime()->timestamp(), + "can_edit" => $post->canBeEditedBy($this->getUser()), + "can_delete" => $post->canBeDeletedBy($this->getUser()), "text" => $comment->getText(false), "post_id" => $post->getVirtualId(), - "owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), + "owner_id" => method_exists($post, 'isPostedOnBehalfOfGroup') && $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), "parents_stack" => [], "attachments" => $attachments, "thread" => [ @@ -580,54 +911,86 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ "can_post" => false, "show_reply_button" => true, "groups_can_post" => false, - ] + ], ]; - if($need_likes == true) + if ($comment->isFromPostAuthor($post)) { + $item['is_from_post_author'] = true; + } + + if ($need_likes == 1) { $item['likes'] = [ "can_like" => 1, "count" => $comment->getLikesCount(), "user_likes" => (int) $comment->hasLikeFrom($this->getUser()), - "can_publish" => 1 + "can_publish" => 1, ]; + } $items[] = $item; - if($extended == true) + if ($extended == true) { $profiles[] = $comment->getOwner()->getId(); + } $attachments = null; // Reset $attachments to not duplicate prikols } $response = [ - "count" => (new CommentsRepo)->getCommentsCountByTarget($post), + "count" => (new CommentsRepo())->getCommentsCountByTarget($post), "items" => $items, - "current_level_count" => (new CommentsRepo)->getCommentsCountByTarget($post), + "current_level_count" => (new CommentsRepo())->getCommentsCountByTarget($post), "can_post" => true, "show_reply_button" => true, - "groups_can_post" => false + "groups_can_post" => false, ]; - if($extended == true) { + if ($extended == true) { $profiles = array_unique($profiles); - $response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []); + $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") { + public 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); # один хуй айди всех комментов общий - + $comment = (new CommentsRepo())->get($comment_id); # один хуй айди всех комментов общий + + if (!$comment || $comment->isDeleted()) { + $this->fail(100, "Invalid comment"); + } + + if (!$comment->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + $profiles = []; $attachments = []; - foreach($comment->getChildren() as $attachment) { - if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + foreach ($comment->getChildren() as $attachment) { + if ($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure(); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Note) { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct(), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -637,14 +1000,14 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri "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(), + "owner_id" => method_exists($comment->getTarget(), 'isPostedOnBehalfOfGroup') && $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(), "parents_stack" => [], "attachments" => $attachments, "likes" => [ "can_like" => 1, "count" => $comment->getLikesCount(), "user_likes" => (int) $comment->hasLikeFrom($this->getUser()), - "can_publish" => 1 + "can_publish" => 1, ], "thread" => [ "count" => 0, @@ -652,49 +1015,70 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri "can_post" => false, "show_reply_button" => true, "groups_can_post" => false, - ] + ], ]; - if($extended == true) + if ($comment->isFromPostAuthor()) { + $item['is_from_post_author'] = true; + } + + if ($extended == true) { $profiles[] = $comment->getOwner()->getId(); + } $response = [ "items" => [$item], "can_post" => true, "show_reply_button" => true, - "groups_can_post" => false + "groups_can_post" => false, ]; - if($extended == true) { + if ($extended == true) { $profiles = array_unique($profiles); - $response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []); + $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, string $attachments = "") { + public function createComment(int $owner_id, int $post_id, string $message = "", int $from_group = 0, string $attachments = "") + { $this->requireUser(); $this->willExecuteWriteAction(); - $post = (new PostsRepo)->getPostById($owner_id, $post_id); - if(!$post || $post->isDeleted()) $this->fail(100, "Invalid post"); + $post = (new PostsRepo())->getPostById($owner_id, $post_id); + if (!$post || $post->isDeleted()) { + $this->fail(100, "Invalid post"); + } + + if (!$post->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + if ($post->getTargetWall() < 0) { + $club = (new ClubsRepo())->get(abs($post->getTargetWall())); + } - if($post->getTargetWall() < 0) - $club = (new ClubsRepo)->get(abs($post->getTargetWall())); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); + $final_attachments = []; + foreach ($parsed_attachments as $attachment) { + if ($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } - if(empty($message) && empty($attachments)) { + if ((empty($message) && (empty($attachments) || sizeof($final_attachments) < 1))) { $this->fail(100, "Required parameter 'message' missing."); } $flags = 0; - if($from_group != 0 && !is_null($club) && $club->canBeModifiedBy($this->user)) + if ($from_group != 0 && !is_null($club) && $club->canBeModifiedBy($this->user)) { $flags |= 0b10000000; + } try { - $comment = new Comment; + $comment = new Comment(); $comment->setOwner($this->user->getId()); $comment->setModel(get_class($post)); $comment->setTarget($post->getId()); @@ -706,108 +1090,342 @@ function createComment(int $owner_id, int $post_id, string $message = "", int $f $this->fail(1, "ошибка про то что коммент большой слишком"); } - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) - $this->fail(11, "Access to photo denied"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) - $this->fail(11, "Access to video denied"); - - $comment->attach($attacc); - } - } + foreach ($final_attachments as $attachment) { + $comment->attach($attachment); } - if($post->getOwner()->getId() !== $this->user->getId()) - if(($owner = $post->getOwner()) instanceof User) + 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" => [] + "parents_stack" => [], ]; } - function deleteComment(int $comment_id) { + public function deleteComment(int $comment_id) + { $this->requireUser(); $this->willExecuteWriteAction(); - $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)) + $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) { + public function delete(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + if (!$post || $post->isDeleted()) { + $this->fail(15, "Not found"); + } + + $wallOwner = $post->getWallOwner(); + + # trying to solve the condition below. + # $post->getTargetWall() < 0 - if post on wall of club + # !$post->getWallOwner()->canBeModifiedBy($this->getUser()) - group is cannot be modifiet by %user% + # $post->getWallOwner()->getWallType() != 1 - wall is not open + # $post->getSuggestionType() == 0 - post is not suggested + if ($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->getUser()) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) { + $this->fail(15, "Access denied"); + } + + if ($post->getOwnerPost() == $this->getUser()->getId() || $post->getTargetWall() == $this->getUser()->getId() || $owner_id < 0 && $wallOwner->canBeModifiedBy($this->getUser())) { + $post->unwire(); + $post->delete(); + + return 1; + } else { + $this->fail(15, "Access denied"); + } + } + + public function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "", string $copyright = null, int $explicit = -1, int $from_group = 0, int $signed = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll', 'doc']); + $final_attachments = []; + foreach ($parsed_attachments as $attachment) { + if ($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if (empty($message) && sizeof($final_attachments) < 1) { + $this->fail(-66, "Post will be empty, don't saving."); + } + + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + + if (!$post || $post->isDeleted()) { + $this->fail(102, "Invalid post"); + } + + if (!$post->canBeEditedBy($this->getUser())) { + $this->fail(7, "Access to editing denied"); + } + + if (!empty($message) || (empty($message) && sizeof($final_attachments) > 0)) { + $post->setContent($message); + } + + $post->setEdited(time()); + if (!is_null($copyright) && !empty($copyright)) { + if ($copyright == 'remove') { + $post->resetSource(); + } else { + try { + $post->setSource($copyright); + } catch (\Throwable) { + } + } + } + + if ($explicit != -1) { + $post->setNsfw($explicit == 1); + } + + $wallOwner = ($owner_id > 0 ? (new UsersRepo())->get($owner_id) : (new ClubsRepo())->get($owner_id * -1)); + $flags = 0; + if ($from_group == 1 && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->getUser())) { + $flags |= 0b10000000; + } + if ($post->isSigned() && $from_group == 1) { + $flags |= 0b01000000; + } + + $post->setFlags($flags); + $post->save(true); + + if ($attachments == 'remove' || sizeof($final_attachments) > 0) { + foreach ($post->getChildren() as $att) { + if (!($att instanceof Post)) { + $post->detach($att); + } + } + + foreach ($final_attachments as $attachment) { + $post->attach($attachment); + } + } + + return ["post_id" => $post->getVirtualId()]; + } + + public function editComment(int $comment_id, int $owner_id = 0, string $message = "", string $attachments = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $comment = (new CommentsRepo())->get($comment_id); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); + $final_attachments = []; + foreach ($parsed_attachments as $attachment) { + if ($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if (empty($message) && sizeof($final_attachments) < 1) { + $this->fail(100, "Required parameter 'message' missing."); + } + + if (!$comment || $comment->isDeleted()) { + $this->fail(102, "Invalid comment"); + } + + if (!$comment->canBeEditedBy($this->getUser())) { + $this->fail(15, "Access to editing comment denied"); + } + + if (!empty($message) || (empty($message) && sizeof($final_attachments) > 0)) { + $comment->setContent($message); + } + + $comment->setEdited(time()); + $comment->save(true); + + if (sizeof($final_attachments) > 0) { + $comment->unwire(); + foreach ($final_attachments as $attachment) { + $comment->attach($attachment); + } + } + + return 1; + } + + public function checkCopyrightLink(string $link): int + { + $this->requireUser(); + + try { + $result = check_copyright_link($link); + } catch (\InvalidArgumentException $e) { + $this->fail(3102, "Specified link is incorrect (can't find source)"); + } catch (\LengthException $e) { + $this->fail(3103, "Specified link is incorrect (too long)"); + } catch (\LogicException $e) { + $this->fail(3104, "Link is suspicious"); + } catch (\Throwable $e) { + $this->fail(3102, "Specified link is incorrect"); + } + + return 1; + } + + public function pin(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo())->getPostById($owner_id, $post_id); + if (!$post || $post->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: post_id is undefined"); + } + + if (!$post->canBePinnedBy($this->getUser())) { + return 0; + } + + if ($post->isPinned()) { + return 1; + } + + $post->pin(); + return 1; + } + + public function unpin(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo())->getPostById($owner_id, $post_id); + if (!$post || $post->isDeleted()) { + $this->fail(100, "One of the parameters specified was missing or invalid: post_id is undefined"); + } + + if (!$post->canBePinnedBy($this->getUser())) { + return 0; + } + + if (!$post->isPinned()) { + return 1; + } + + $post->unpin(); + return 1; + } + + public function getNearby(int $owner_id, int $post_id) + { + $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: post_id is undefined"); + } + + if (!$post->canBeViewedBy($this->getUser())) { + $this->fail(15, "Access denied"); + } + + $lat = $post->getLat(); + $lon = $post->getLon(); + + if (!$lat || !$lon) { + $this->fail(-97, "Post doesn't contains geo"); + } + + $query = file_get_contents(__DIR__ . "/../../Web/Models/sql/get-nearest-posts.tsql"); + $_posts = \Chandler\Database\DatabaseConnection::i()->getContext()->query($query, $lat, $lon, $post->getId())->fetchAll(); + $posts = []; + + foreach ($_posts as $post) { + $distance = $post["distance"]; + $post = (new PostsRepo())->get($post["id"]); + if (!$post || $post->isDeleted() || !$post->canBeViewedBy($this->getUser())) { + continue; + } + + $owner = $post->getOwner(); + $preview = mb_substr($post->getText(), 0, 50) . (strlen($post->getText()) > 50 ? "..." : ""); + $posts[] = [ + "message" => strlen($preview) > 0 ? $preview : "(нет текста)", + "url" => "/wall" . $post->getPrettyId(), + "created" => $post->getPublicationTime()->html(), + "owner" => [ + "domain" => $owner->getURL(), + "photo_50" => $owner->getAvatarURL(), + "name" => $owner->getCanonicalName(), + "verified" => $owner->isVerified(), + ], + "geo" => $post->getGeo(), + "distance" => $distance, + ]; + } + + return $posts; + } + + private function getApiPhoto($attachment) + { return [ "type" => "photo", "photo" => [ - "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL, + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0, "date" => $attachment->getPublicationTime()->timestamp(), "id" => $attachment->getVirtualId(), "owner_id" => $attachment->getOwner()->getId(), - "sizes" => !is_null($attachment->getVkApiSizes()) ? array_values($attachment->getVkApiSizes()) : NULL, + "sizes" => !is_null($attachment->getVkApiSizes()) ? array_values($attachment->getVkApiSizes()) : null, "text" => "", - "has_tags" => false - ] + "has_tags" => false, + ], ]; } - private function getApiPoll($attachment, $user) { - $answers = array(); - foreach($attachment->getResults()->options as $answer) { - $answers[] = (object)[ + private function getApiPoll($attachment, $user) + { + $answers = []; + foreach ($attachment->getResults()->options as $answer) { + $answers[] = (object) [ "id" => $answer->id, "rate" => $answer->pct, "text" => $answer->name, - "votes" => $answer->votes + "votes" => $answer->votes, ]; } - $userVote = array(); - foreach($attachment->getUserVote($user) as $vote) + $userVote = []; + foreach ($attachment->getUserVote($user) as $vote) { $userVote[] = $vote[0]; + } return [ "type" => "poll", "poll" => [ "multiple" => $attachment->isMultipleChoice(), - "end_date" => $attachment->endsAt() == NULL ? 0 : $attachment->endsAt()->timestamp(), + "end_date" => $attachment->endsAt() == null ? 0 : $attachment->endsAt()->timestamp(), "closed" => $attachment->hasEnded(), "is_board" => false, "can_edit" => false, @@ -824,7 +1442,7 @@ private function getApiPoll($attachment, $user) { "answer_ids" => $userVote, "answers" => $answers, "author_id" => $attachment->getOwner()->getId(), - ] + ], ]; } } diff --git a/VKAPI/README.md b/VKAPI/README.md index 75918afcb..8cd5581bd 100644 --- a/VKAPI/README.md +++ b/VKAPI/README.md @@ -5,7 +5,7 @@ exceptions. It is still a work-in-progress functionality. **Note**: requests to API are routed through openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. -[Documentation for API clients](https://docs.openvk.uk/openvk_engine/api/description/) +[Documentation for API clients](https://openvk.org/dev) ## Implementing API methods diff --git a/VKAPI/Structures/Conversation.php b/VKAPI/Structures/Conversation.php index ad8951f37..b22471ea8 100644 --- a/VKAPI/Structures/Conversation.php +++ b/VKAPI/Structures/Conversation.php @@ -1,13 +1,18 @@ -value = $value; + } +} + +/** + * Tree-walking interpreter for VKScript, used by the `execute` API method. + * + * Evaluates the AST produced by {@see Parser}. API calls (`API.object.method({...})`) are + * delegated to a callback supplied by the caller; a maximum of 25 are allowed per run. + * Failed API calls are collected into {@see getExecuteErrors()} (the script keeps running, + * the call evaluating to false), mirroring VK's `execute_errors` field. Runtime problems + * are reported as APIErrorException with code 13. + */ +class Interpreter +{ + private const MAX_API_CALLS = 25; + private const MAX_OPERATIONS = 5000000; + + private const MUTATING_METHODS = ["push", "pop", "shift", "unshift", "splice"]; + + /** @var callable fn(string $object, string $method, array $params): mixed */ + private $apiCallback; + + /** @var array */ + private array $vars = []; + + /** @var array */ + private array $executeErrors = []; + + private int $apiCalls = 0; + private int $operations = 0; + + public function __construct(callable $apiCallback, array $args = []) + { + $this->apiCallback = $apiCallback; + $this->vars["Args"] = $args; + } + + /** @return array the collected execute_errors (empty when none occurred) */ + public function getExecuteErrors(): array + { + return $this->executeErrors; + } + + /** + * @param array $ast statement list from {@see Parser::parse()} + * @return mixed value of the script's `return`, or null + */ + public function run(array $ast) + { + try { + $this->execBlock($ast); + } catch (ReturnSignal $ret) { + return $this->export($ret->value); + } catch (BreakSignal | ContinueSignal) { + throw new APIErrorException("Runtime error: 'break'/'continue' outside of a loop", 13); + } + + return null; + } + + /* ---------- statements ---------- */ + + /** @param array $statements */ + private function execBlock(array $statements): void + { + foreach ($statements as $stmt) { + $this->exec($stmt); + } + } + + private function exec(array $node): void + { + $this->tick(); + + switch ($node["kind"]) { + case "var": + foreach ($node["decls"] as $decl) { + $this->vars[$decl["name"]] = $decl["init"] === null ? null : $this->eval($decl["init"]); + } + return; + + case "expr": + $this->eval($node["expr"]); + return; + + case "block": + $this->execBlock($node["body"]); + return; + + case "empty": + return; + + case "if": + if ($this->truthy($this->eval($node["cond"]))) { + $this->exec($node["then"]); + } elseif ($node["else"] !== null) { + $this->exec($node["else"]); + } + return; + + case "while": + while ($this->truthy($this->eval($node["cond"]))) { + try { + $this->exec($node["body"]); + } catch (BreakSignal) { + break; + } catch (ContinueSignal) { + continue; + } + } + return; + + case "dowhile": + do { + try { + $this->exec($node["body"]); + } catch (BreakSignal) { + break; + } catch (ContinueSignal) { + continue; + } + } while ($this->truthy($this->eval($node["cond"]))); + return; + + case "for": + if ($node["init"] !== null) { + $this->exec($node["init"]); + } + while ($node["cond"] === null || $this->truthy($this->eval($node["cond"]))) { + try { + $this->exec($node["body"]); + } catch (BreakSignal) { + break; + } catch (ContinueSignal) { + // fall through to update + } + if ($node["update"] !== null) { + $this->eval($node["update"]); + } + } + return; + + case "break": + throw new BreakSignal(); + + case "continue": + throw new ContinueSignal(); + + case "return": + throw new ReturnSignal($node["value"] === null ? null : $this->eval($node["value"])); + } + + throw new APIErrorException("Runtime error: unknown statement", 13); + } + + /* ---------- expressions ---------- */ + + private function eval(array $node) + { + $this->tick(); + + switch ($node["kind"]) { + case "num": + case "str": + case "bool": + return $node["value"]; + case "null": + return null; + + case "name": + if (!array_key_exists($node["name"], $this->vars)) { + throw new APIErrorException("Runtime error: unknown variable '" . $node["name"] . "'", 13); + } + return $this->vars[$node["name"]]; + + case "array": + $out = []; + foreach ($node["elements"] as $el) { + $out[] = $this->eval($el); + } + return $out; + + case "object": + $out = []; + foreach ($node["props"] as $prop) { + $out[$prop["key"]] = $this->eval($prop["value"]); + } + return $out; + + case "assign": + $value = $this->eval($node["value"]); + $ref = &$this->evalRef($node["target"]); + $ref = $value; + return $value; + + case "unary": + return $this->evalUnary($node); + + case "logical": + $left = $this->eval($node["left"]); + if ($node["op"] === "&&") { + return $this->truthy($left) ? $this->eval($node["right"]) : $left; + } + return $this->truthy($left) ? $left : $this->eval($node["right"]); + + case "binary": + return $this->evalBinary($node["op"], $this->eval($node["left"]), $this->eval($node["right"])); + + case "member": + return $this->getMember($this->eval($node["object"]), $node["name"]); + + case "index": + return $this->getIndex($this->eval($node["object"]), $this->eval($node["index"])); + + case "filter": + return $this->evalFilter($node); + + case "call": + return $this->evalCall($node); + } + + throw new APIErrorException("Runtime error: unknown expression", 13); + } + + private function evalUnary(array $node) + { + $value = $this->eval($node["operand"]); + if ($node["op"] === "!") { + return !$this->truthy($value); + } + + // "-" + return -$this->toNumber($value); + } + + private function evalBinary(string $op, $left, $right) + { + switch ($op) { + case "+": + if (is_string($left) || is_string($right)) { + return $this->toString($left) . $this->toString($right); + } + if (is_array($left) || is_object($left) || is_array($right) || is_object($right)) { + // list + list => concatenation; object + object => shallow merge (VK semantics). + if (is_array($left) && array_is_list($left) && is_array($right) && array_is_list($right)) { + return array_merge($left, $right); + } + $out = $this->toAssoc($left); + foreach ($this->toAssoc($right) as $k => $v) { + $out[$k] = $v; + } + return $out; + } + return $this->toNumber($left) + $this->toNumber($right); + case "-": + return $this->toNumber($left) - $this->toNumber($right); + case "*": + return $this->toNumber($left) * $this->toNumber($right); + case "/": + $d = $this->toNumber($right); + if ($d == 0) { + throw new APIErrorException("Runtime error: division by zero", 13); + } + return $this->toNumber($left) / $d; + case "%": + $d = (int) $this->toNumber($right); + if ($d === 0) { + throw new APIErrorException("Runtime error: modulo by zero", 13); + } + return (int) $this->toNumber($left) % $d; + case "==": + return $this->looseEquals($left, $right); + case "!=": + return !$this->looseEquals($left, $right); + case "<": + return $this->compare($left, $right) < 0; + case ">": + return $this->compare($left, $right) > 0; + case "<=": + return $this->compare($left, $right) <= 0; + case ">=": + return $this->compare($left, $right) >= 0; + } + + throw new APIErrorException("Runtime error: unknown operator '$op'", 13); + } + + private function evalFilter(array $node) + { + $value = $this->eval($node["object"]); + $elements = $this->toList($value); + $out = []; + + foreach ($elements as $el) { + if ($node["mode"] === "member") { + $out[] = $this->getMember($el, $node["name"]); + } else { + $out[] = $this->getIndex($el, $this->eval($node["index"])); + } + } + + return $out; + } + + private function evalCall(array $node) + { + $callee = $node["callee"]; + + // API.object.method({ ... }) + $api = $this->matchApiCallee($callee); + if ($api !== null) { + return $this->callApi($api[0], $api[1], $node["args"]); + } + + // receiver.method(...) — built-in string/array methods + if ($callee["kind"] === "member") { + return $this->callMethod($callee, $node["args"]); + } + + // bare function — global built-ins + if ($callee["kind"] === "name") { + return $this->callGlobal($callee["name"], $this->evalArgs($node["args"])); + } + + throw new APIErrorException("Runtime error: expression is not callable", 13); + } + + /** @return array{0: string, 1: string}|null [section, method] ; section is "" for legacy unprefixed methods */ + private function matchApiCallee(array $callee): ?array + { + if ($callee["kind"] !== "member") { + return null; + } + + $object = $callee["object"]; + + // API.method(...) — legacy method with no section + if ($object["kind"] === "name" && $object["name"] === "API") { + return ["", $callee["name"]]; + } + + // API.section.method(...) + if ( + $object["kind"] === "member" + && $object["object"]["kind"] === "name" + && $object["object"]["name"] === "API" + ) { + return [$object["name"], $callee["name"]]; + } + + return null; + } + + private function callApi(string $object, string $method, array $argNodes) + { + if (++$this->apiCalls > self::MAX_API_CALLS) { + throw new APIErrorException("Runtime error: too many API calls in execute (max " . self::MAX_API_CALLS . ")", 13); + } + + $params = []; + if (count($argNodes) > 0) { + $arg = $this->eval($argNodes[0]); + if (is_array($arg)) { + $params = $arg; + } elseif (is_object($arg)) { + $params = (array) $arg; + } + } + + // Flatten params to scalar request values the handlers expect. + $request = []; + foreach ($params as $key => $value) { + $request[$key] = $this->toRequestValue($value); + } + + $label = $object === "" ? $method : "$object.$method"; + + try { + return ($this->apiCallback)($object, $method, $request); + } catch (APIErrorException $ex) { + $this->executeErrors[] = [ + "method" => $label, + "error_code" => $ex->getCode(), + "error_msg" => $ex->getMessage(), + ]; + return false; + } + } + + private function callMethod(array $callee, array $argNodes) + { + $method = $callee["name"]; + $args = $this->evalArgs($argNodes); + $mutating = in_array($method, self::MUTATING_METHODS, true); + $assignable = in_array($callee["object"]["kind"], ["name", "member", "index"], true); + + if ($mutating && $assignable) { + $receiver = &$this->evalRef($callee["object"]); + } else { + $receiver = $this->eval($callee["object"]); + } + + if (is_array($receiver)) { + return $this->arrayMethod($receiver, $method, $args); + } + if (is_string($receiver)) { + return $this->stringMethod($receiver, $method, $args); + } + + throw new APIErrorException("Runtime error: unknown method '$method'", 13); + } + + private function callGlobal(string $name, array $args) + { + switch ($name) { + case "parseInt": + $radix = isset($args[1]) ? (int) $args[1] : 10; + return intval($this->toString($args[0] ?? ""), $radix ?: 10); + case "parseDouble": + case "parseFloat": + return (float) $this->toNumber($args[0] ?? 0); + } + + throw new APIErrorException("Runtime error: unknown function '$name'", 13); + } + + /** @param mixed $receiver passed by reference so mutating methods persist */ + private function arrayMethod(&$receiver, string $method, array $args) + { + switch ($method) { + case "push": + foreach ($args as $a) { + $receiver[] = $a; + } + return count($receiver); + case "pop": + return array_pop($receiver); + case "shift": + return array_shift($receiver); + case "unshift": + array_unshift($receiver, ...$args); + return count($receiver); + case "slice": + $start = (int) ($args[0] ?? 0); + $length = isset($args[1]) ? (int) $args[1] - $start : null; + if ($length !== null && $length < 0) { + $length = 0; + } + return array_values(array_slice($receiver, $start, $length)); + case "splice": + $start = (int) ($args[0] ?? 0); + $delete = isset($args[1]) ? (int) $args[1] : count($receiver) - $start; + $items = array_slice($args, 2); + $removed = array_splice($receiver, $start, $delete, $items); + return array_values($removed); + case "indexOf": + $idx = array_search($args[0] ?? null, $receiver, true); + return $idx === false ? -1 : $idx; + } + + throw new APIErrorException("Runtime error: unknown array method '$method'", 13); + } + + private function stringMethod(string $receiver, string $method, array $args) + { + switch ($method) { + case "substr": + $start = (int) ($args[0] ?? 0); + $length = isset($args[1]) ? (int) $args[1] : null; + return $length === null ? substr($receiver, $start) : substr($receiver, $start, $length); + case "split": + $sep = $this->toString($args[0] ?? ""); + return $sep === "" ? ($receiver === "" ? [] : str_split($receiver)) : explode($sep, $receiver); + case "indexOf": + $pos = strpos($receiver, $this->toString($args[0] ?? "")); + return $pos === false ? -1 : $pos; + } + + throw new APIErrorException("Runtime error: unknown string method '$method'", 13); + } + + /* ---------- member / index access ---------- */ + + private function getMember($obj, string $name) + { + if ($name === "length") { + if (is_string($obj)) { + return strlen($obj); + } + if (is_array($obj)) { + return count($obj); + } + } + + if (is_array($obj)) { + // Distribute member access over a plain list (covers `@`-style chains). + if (array_is_list($obj) && $name !== "length") { + $out = []; + foreach ($obj as $el) { + $out[] = $this->getMember($el, $name); + } + return $out; + } + return $obj[$name] ?? null; + } + + if (is_object($obj)) { + return $obj->{$name} ?? null; + } + + return null; + } + + private function getIndex($obj, $index) + { + if (is_array($obj)) { + return $obj[$this->toKey($index)] ?? null; + } + if (is_object($obj)) { + return $obj->{(string) $index} ?? null; + } + if (is_string($obj)) { + $i = (int) $index; + return $obj[$i] ?? null; + } + + return null; + } + + /** Returns a reference to the storage slot named by an assignable node (auto-vivifies). */ + private function &evalRef(array $node) + { + if ($node["kind"] === "name") { + if (!array_key_exists($node["name"], $this->vars)) { + $this->vars[$node["name"]] = null; + } + return $this->vars[$node["name"]]; + } + + if ($node["kind"] === "member" || $node["kind"] === "index") { + $key = $node["kind"] === "member" ? $node["name"] : $this->toKey($this->eval($node["index"])); + $parent = &$this->evalRef($node["object"]); + + if (is_object($parent)) { + if (!isset($parent->{$key})) { + $parent->{$key} = null; + } + $ref = &$parent->{$key}; + return $ref; + } + + if (!is_array($parent)) { + $parent = []; + } + if (!array_key_exists($key, $parent)) { + $parent[$key] = null; + } + $ref = &$parent[$key]; + return $ref; + } + + throw new APIErrorException("Runtime error: invalid assignment target", 13); + } + + /* ---------- helpers ---------- */ + + private function tick(): void + { + if (++$this->operations > self::MAX_OPERATIONS) { + throw new APIErrorException("Runtime error: script exceeded the operation limit", 13); + } + } + + /** @return array */ + private function evalArgs(array $argNodes): array + { + $out = []; + foreach ($argNodes as $node) { + $out[] = $this->eval($node); + } + return $out; + } + + /** @return array object-like value coerced to an associative array */ + private function toAssoc($value): array + { + if (is_object($value)) { + return (array) $value; + } + if (is_array($value)) { + return $value; + } + return []; + } + + /** @return array */ + private function toList($value): array + { + if (is_array($value)) { + return array_is_list($value) ? $value : [$value]; + } + if ($value === null) { + return []; + } + return [$value]; + } + + private function truthy($v): bool + { + if (is_array($v)) { + return count($v) > 0; + } + if (is_string($v)) { + return $v !== "" && $v !== "0"; + } + if (is_int($v) || is_float($v)) { + return $v != 0; + } + return (bool) $v; + } + + private function toNumber($v) + { + if (is_int($v) || is_float($v)) { + return $v; + } + if (is_bool($v)) { + return $v ? 1 : 0; + } + if (is_string($v)) { + return is_numeric($v) ? $v + 0 : 0; + } + return 0; + } + + private function toString($v): string + { + if (is_string($v)) { + return $v; + } + if (is_bool($v)) { + return $v ? "true" : "false"; + } + if ($v === null) { + return ""; + } + if (is_array($v) || is_object($v)) { + return json_encode($v); + } + return (string) $v; + } + + private function toKey($index) + { + if (is_int($index)) { + return $index; + } + if (is_float($index) || is_bool($index)) { + return (int) $index; + } + if (is_string($index) && preg_match('/^-?\d+$/', $index)) { + return (int) $index; + } + return (string) $index; + } + + /** Coerce a VKScript value into something an API handler accepts as a request param. */ + private function toRequestValue($value) + { + if (is_bool($value)) { + return $value ? "1" : "0"; + } + if (is_array($value)) { + // VK serialises array params as comma-separated lists (e.g. user_ids). + if (array_is_list($value)) { + return implode(",", array_map([$this, "toRequestScalar"], $value)); + } + return json_encode($value); + } + if (is_object($value)) { + return json_encode($value); + } + if ($value === null) { + return ""; + } + return $value; + } + + private function toRequestScalar($value): string + { + if (is_bool($value)) { + return $value ? "1" : "0"; + } + if ($value === null) { + return ""; + } + if (is_array($value) || is_object($value)) { + return json_encode($value); + } + return (string) $value; + } + + private function looseEquals($a, $b): bool + { + if (is_string($a) && is_string($b)) { + return $a === $b; + } + if ((is_int($a) || is_float($a) || is_bool($a)) && (is_int($b) || is_float($b) || is_bool($b))) { + return $this->toNumber($a) == $this->toNumber($b); + } + if ($a === null || $b === null) { + return $a === $b; + } + return $a == $b; + } + + private function compare($a, $b): int + { + if (is_string($a) && is_string($b)) { + return strcmp($a, $b); + } + return $this->toNumber($a) <=> $this->toNumber($b); + } + + /** Normalise associative arrays produced by object literals into stdClass for JSON output. */ + private function export($value) + { + if (is_array($value)) { + if (array_is_list($value)) { + return array_map([$this, "export"], $value); + } + $obj = new \stdClass(); + foreach ($value as $k => $v) { + $obj->{$k} = $this->export($v); + } + return $obj; + } + + return $value; + } +} diff --git a/VKAPI/VKScript/Lexer.php b/VKAPI/VKScript/Lexer.php new file mode 100644 index 000000000..defc3ee3d --- /dev/null +++ b/VKAPI/VKScript/Lexer.php @@ -0,0 +1,203 @@ +=", "&&", "||", + "+", "-", "*", "/", "%", "<", ">", "!", "=", + ]; + + private string $code; + private int $pos = 0; + private int $len; + + public function __construct(string $code) + { + $this->code = $code; + $this->len = strlen($code); + } + + /** + * @return array + */ + public function tokenize(): array + { + $tokens = []; + + while ($this->pos < $this->len) { + $this->skipTrivia(); + if ($this->pos >= $this->len) { + break; + } + + $start = $this->pos; + $ch = $this->code[$this->pos]; + + if (ctype_digit($ch) || ($ch === "." && $this->pos + 1 < $this->len && ctype_digit($this->code[$this->pos + 1]))) { + $tokens[] = $this->readNumber(); + } elseif ($ch === "\"" || $ch === "'") { + $tokens[] = $this->readString($ch); + } elseif (ctype_alpha($ch) || $ch === "_" || $ch === "$") { + $tokens[] = $this->readName(); + } elseif (strpos(".,;:()[]{}@", $ch) !== false) { + $this->pos++; + $tokens[] = ["type" => "punc", "value" => $ch, "pos" => $start]; + } else { + $tokens[] = $this->readOperator(); + } + } + + $tokens[] = ["type" => "eof", "value" => null, "pos" => $this->pos]; + + return $tokens; + } + + private function skipTrivia(): void + { + while ($this->pos < $this->len) { + $ch = $this->code[$this->pos]; + + if (ctype_space($ch)) { + $this->pos++; + continue; + } + + if ($ch === "/" && $this->pos + 1 < $this->len) { + $next = $this->code[$this->pos + 1]; + if ($next === "/") { + $this->pos += 2; + while ($this->pos < $this->len && $this->code[$this->pos] !== "\n") { + $this->pos++; + } + continue; + } + + if ($next === "*") { + $this->pos += 2; + while ($this->pos < $this->len && !($this->code[$this->pos] === "*" && ($this->code[$this->pos + 1] ?? "") === "/")) { + $this->pos++; + } + if ($this->pos >= $this->len) { + throw new APIErrorException("Unterminated comment in script", 12); + } + $this->pos += 2; + continue; + } + } + + break; + } + } + + private function readNumber(): array + { + $start = $this->pos; + $hasDot = false; + + while ($this->pos < $this->len) { + $ch = $this->code[$this->pos]; + if (ctype_digit($ch)) { + $this->pos++; + } elseif ($ch === "." && !$hasDot) { + $hasDot = true; + $this->pos++; + } else { + break; + } + } + + $raw = substr($this->code, $start, $this->pos - $start); + + return [ + "type" => "num", + "value" => $hasDot ? (float) $raw : (int) $raw, + "pos" => $start, + ]; + } + + private function readString(string $quote): array + { + $start = $this->pos; + $this->pos++; // opening quote + $buf = ""; + + while ($this->pos < $this->len) { + $ch = $this->code[$this->pos]; + + if ($ch === "\\") { + $next = $this->code[$this->pos + 1] ?? ""; + $buf .= match ($next) { + "n" => "\n", + "t" => "\t", + "r" => "\r", + "\\" => "\\", + "\"" => "\"", + "'" => "'", + "0" => "\0", + default => $next, + }; + $this->pos += 2; + continue; + } + + if ($ch === $quote) { + $this->pos++; + return ["type" => "str", "value" => $buf, "pos" => $start]; + } + + $buf .= $ch; + $this->pos++; + } + + throw new APIErrorException("Unterminated string literal in script", 12); + } + + private function readName(): array + { + $start = $this->pos; + while ($this->pos < $this->len) { + $ch = $this->code[$this->pos]; + if (ctype_alnum($ch) || $ch === "_" || $ch === "$") { + $this->pos++; + } else { + break; + } + } + + $value = substr($this->code, $start, $this->pos - $start); + $type = in_array($value, self::KEYWORDS, true) ? "keyword" : "name"; + + return ["type" => $type, "value" => $value, "pos" => $start]; + } + + private function readOperator(): array + { + $start = $this->pos; + foreach (self::OPERATORS as $op) { + if (substr($this->code, $this->pos, strlen($op)) === $op) { + $this->pos += strlen($op); + return ["type" => "op", "value" => $op, "pos" => $start]; + } + } + + throw new APIErrorException("Unexpected character '" . $this->code[$this->pos] . "' in script", 12); + } +} diff --git a/VKAPI/VKScript/Parser.php b/VKAPI/VKScript/Parser.php new file mode 100644 index 000000000..5460b1a50 --- /dev/null +++ b/VKAPI/VKScript/Parser.php @@ -0,0 +1,514 @@ + */ + private array $tokens; + private int $pos = 0; + + public function __construct(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * @return array list of statement nodes + */ + public function parse(): array + { + $statements = []; + while (!$this->isEof()) { + $statements[] = $this->parseStatement(); + } + + return $statements; + } + + /* ---------- token helpers ---------- */ + + private function peek(): array + { + return $this->tokens[$this->pos]; + } + + private function isEof(): bool + { + return $this->peek()["type"] === "eof"; + } + + private function advance(): array + { + return $this->tokens[$this->pos++]; + } + + private function check(string $type, $value = null): bool + { + $tok = $this->peek(); + if ($tok["type"] !== $type) { + return false; + } + + return $value === null || $tok["value"] === $value; + } + + private function accept(string $type, $value = null): bool + { + if ($this->check($type, $value)) { + $this->pos++; + return true; + } + + return false; + } + + private function expect(string $type, $value = null): array + { + if (!$this->check($type, $value)) { + $got = $this->peek(); + $want = $value !== null ? "'$value'" : $type; + $desc = $got["type"] === "eof" ? "end of script" : "'" . $got["value"] . "'"; + throw new APIErrorException("Syntax error: expected $want but got $desc", 12); + } + + return $this->advance(); + } + + private function acceptSemicolons(): void + { + while ($this->accept("punc", ";")) { + // VKScript treats stray semicolons as empty statements. + } + } + + /* ---------- statements ---------- */ + + private function parseStatement(): array + { + $tok = $this->peek(); + + if ($this->check("punc", ";")) { + $this->advance(); + return ["kind" => "empty"]; + } + + if ($tok["type"] === "keyword") { + switch ($tok["value"]) { + case "var": + return $this->parseVar(); + case "if": + return $this->parseIf(); + case "while": + return $this->parseWhile(); + case "do": + return $this->parseDoWhile(); + case "for": + return $this->parseFor(); + case "break": + $this->advance(); + $this->acceptSemicolons(); + return ["kind" => "break"]; + case "continue": + $this->advance(); + $this->acceptSemicolons(); + return ["kind" => "continue"]; + case "return": + return $this->parseReturn(); + } + } + + if ($this->check("punc", "{")) { + return $this->parseBlock(); + } + + $expr = $this->parseExpression(); + $this->acceptSemicolons(); + return ["kind" => "expr", "expr" => $expr]; + } + + private function parseBlock(): array + { + $this->expect("punc", "{"); + $body = []; + while (!$this->check("punc", "}") && !$this->isEof()) { + $body[] = $this->parseStatement(); + } + $this->expect("punc", "}"); + + return ["kind" => "block", "body" => $body]; + } + + private function parseVar(): array + { + $node = $this->parseVarDeclList(); + $this->acceptSemicolons(); + return $node; + } + + /** Parses `var a = .., b = ..` WITHOUT consuming a trailing semicolon (used by `for` init). */ + private function parseVarDeclList(): array + { + $this->expect("keyword", "var"); + $decls = []; + + do { + $name = $this->expect("name")["value"]; + $init = null; + if ($this->accept("op", "=")) { + $init = $this->parseAssignment(); + } + $decls[] = ["name" => $name, "init" => $init]; + } while ($this->accept("punc", ",")); + + return ["kind" => "var", "decls" => $decls]; + } + + private function parseIf(): array + { + $this->expect("keyword", "if"); + $this->expect("punc", "("); + $cond = $this->parseExpression(); + $this->expect("punc", ")"); + $then = $this->parseStatement(); + $else = null; + if ($this->accept("keyword", "else")) { + $else = $this->parseStatement(); + } + + return ["kind" => "if", "cond" => $cond, "then" => $then, "else" => $else]; + } + + private function parseWhile(): array + { + $this->expect("keyword", "while"); + $this->expect("punc", "("); + $cond = $this->parseExpression(); + $this->expect("punc", ")"); + $body = $this->parseStatement(); + + return ["kind" => "while", "cond" => $cond, "body" => $body]; + } + + private function parseDoWhile(): array + { + $this->expect("keyword", "do"); + $body = $this->parseStatement(); + $this->expect("keyword", "while"); + $this->expect("punc", "("); + $cond = $this->parseExpression(); + $this->expect("punc", ")"); + $this->acceptSemicolons(); + + return ["kind" => "dowhile", "cond" => $cond, "body" => $body]; + } + + private function parseFor(): array + { + $this->expect("keyword", "for"); + $this->expect("punc", "("); + + $init = null; + if (!$this->check("punc", ";")) { + if ($this->check("keyword", "var")) { + $init = $this->parseVarDeclList(); + } else { + $init = ["kind" => "expr", "expr" => $this->parseExpression()]; + } + } + $this->expect("punc", ";"); + + $cond = $this->check("punc", ";") ? null : $this->parseExpression(); + $this->expect("punc", ";"); + + $update = $this->check("punc", ")") ? null : $this->parseExpression(); + $this->expect("punc", ")"); + + $body = $this->parseStatement(); + + return ["kind" => "for", "init" => $init, "cond" => $cond, "update" => $update, "body" => $body]; + } + + private function parseReturn(): array + { + $this->expect("keyword", "return"); + $value = null; + if (!$this->check("punc", ";") && !$this->check("punc", "}") && !$this->isEof()) { + $value = $this->parseExpression(); + } + $this->acceptSemicolons(); + + return ["kind" => "return", "value" => $value]; + } + + /* ---------- expressions ---------- */ + + private function parseExpression(): array + { + return $this->parseAssignment(); + } + + private function parseAssignment(): array + { + $left = $this->parseLogicalOr(); + + if ($this->accept("op", "=")) { + if (!in_array($left["kind"], ["name", "member", "index"], true)) { + throw new APIErrorException("Syntax error: invalid assignment target", 12); + } + $value = $this->parseAssignment(); + return ["kind" => "assign", "target" => $left, "value" => $value]; + } + + return $left; + } + + private function parseLogicalOr(): array + { + $left = $this->parseLogicalAnd(); + while ($this->check("op", "||")) { + $this->advance(); + $right = $this->parseLogicalAnd(); + $left = ["kind" => "logical", "op" => "||", "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseLogicalAnd(): array + { + $left = $this->parseEquality(); + while ($this->check("op", "&&")) { + $this->advance(); + $right = $this->parseEquality(); + $left = ["kind" => "logical", "op" => "&&", "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseEquality(): array + { + $left = $this->parseRelational(); + while ($this->check("op", "==") || $this->check("op", "!=")) { + $op = $this->advance()["value"]; + $right = $this->parseRelational(); + $left = ["kind" => "binary", "op" => $op, "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseRelational(): array + { + $left = $this->parseAdditive(); + while ($this->check("op", "<") || $this->check("op", ">") || $this->check("op", "<=") || $this->check("op", ">=")) { + $op = $this->advance()["value"]; + $right = $this->parseAdditive(); + $left = ["kind" => "binary", "op" => $op, "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseAdditive(): array + { + $left = $this->parseMultiplicative(); + while ($this->check("op", "+") || $this->check("op", "-")) { + $op = $this->advance()["value"]; + $right = $this->parseMultiplicative(); + $left = ["kind" => "binary", "op" => $op, "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseMultiplicative(): array + { + $left = $this->parseUnary(); + while ($this->check("op", "*") || $this->check("op", "/") || $this->check("op", "%")) { + $op = $this->advance()["value"]; + $right = $this->parseUnary(); + $left = ["kind" => "binary", "op" => $op, "left" => $left, "right" => $right]; + } + + return $left; + } + + private function parseUnary(): array + { + if ($this->check("op", "-") || $this->check("op", "!")) { + $op = $this->advance()["value"]; + $operand = $this->parseUnary(); + return ["kind" => "unary", "op" => $op, "operand" => $operand]; + } + + return $this->parsePostfix(); + } + + private function parsePostfix(): array + { + $expr = $this->parsePrimary(); + + while (true) { + if ($this->accept("punc", ".")) { + $name = $this->expectName(); + $expr = ["kind" => "member", "object" => $expr, "name" => $name]; + } elseif ($this->accept("punc", "[")) { + $index = $this->parseExpression(); + $this->expect("punc", "]"); + $expr = ["kind" => "index", "object" => $expr, "index" => $index]; + } elseif ($this->accept("punc", "(")) { + $args = $this->parseArguments(); + $expr = ["kind" => "call", "callee" => $expr, "args" => $args]; + } elseif ($this->accept("punc", "@")) { + if ($this->accept("punc", ".")) { + $name = $this->expectName(); + $expr = ["kind" => "filter", "object" => $expr, "mode" => "member", "name" => $name]; + } elseif ($this->accept("punc", "[")) { + $index = $this->parseExpression(); + $this->expect("punc", "]"); + $expr = ["kind" => "filter", "object" => $expr, "mode" => "index", "index" => $index]; + } else { + throw new APIErrorException("Syntax error: '@' must be followed by '.' or '['", 12); + } + } else { + break; + } + } + + return $expr; + } + + private function parseArguments(): array + { + $args = []; + if (!$this->check("punc", ")")) { + do { + $args[] = $this->parseAssignment(); + } while ($this->accept("punc", ",")); + } + $this->expect("punc", ")"); + + return $args; + } + + private function parsePrimary(): array + { + $tok = $this->peek(); + + switch ($tok["type"]) { + case "num": + $this->advance(); + return ["kind" => "num", "value" => $tok["value"]]; + case "str": + $this->advance(); + return ["kind" => "str", "value" => $tok["value"]]; + case "name": + $this->advance(); + return ["kind" => "name", "name" => $tok["value"]]; + case "keyword": + if ($tok["value"] === "true") { + $this->advance(); + return ["kind" => "bool", "value" => true]; + } + if ($tok["value"] === "false") { + $this->advance(); + return ["kind" => "bool", "value" => false]; + } + if ($tok["value"] === "null") { + $this->advance(); + return ["kind" => "null"]; + } + break; + case "punc": + if ($tok["value"] === "(") { + $this->advance(); + $expr = $this->parseExpression(); + $this->expect("punc", ")"); + return $expr; + } + if ($tok["value"] === "[") { + return $this->parseArrayLiteral(); + } + if ($tok["value"] === "{") { + return $this->parseObjectLiteral(); + } + break; + } + + $desc = $tok["type"] === "eof" ? "end of script" : "'" . $tok["value"] . "'"; + throw new APIErrorException("Syntax error: unexpected $desc", 12); + } + + private function parseArrayLiteral(): array + { + $this->expect("punc", "["); + $elements = []; + if (!$this->check("punc", "]")) { + do { + if ($this->check("punc", "]")) { + break; // trailing comma + } + $elements[] = $this->parseAssignment(); + } while ($this->accept("punc", ",")); + } + $this->expect("punc", "]"); + + return ["kind" => "array", "elements" => $elements]; + } + + private function parseObjectLiteral(): array + { + $this->expect("punc", "{"); + $props = []; + if (!$this->check("punc", "}")) { + do { + if ($this->check("punc", "}")) { + break; // trailing comma + } + + $keyTok = $this->peek(); + if (in_array($keyTok["type"], ["str", "name", "keyword"], true)) { + $key = (string) $keyTok["value"]; + $this->advance(); + } elseif ($keyTok["type"] === "num") { + $key = (string) $keyTok["value"]; + $this->advance(); + } else { + throw new APIErrorException("Syntax error: invalid object key", 12); + } + + $this->expect("punc", ":"); + $value = $this->parseAssignment(); + $props[] = ["key" => $key, "value" => $value]; + } while ($this->accept("punc", ",")); + } + $this->expect("punc", "}"); + + return ["kind" => "object", "props" => $props]; + } + + /** Allows keywords (e.g. `do`, `for`) to be used as member/property names, like VK. */ + private function expectName(): string + { + $tok = $this->peek(); + if ($tok["type"] === "name" || $tok["type"] === "keyword") { + $this->advance(); + return (string) $tok["value"]; + } + + throw new APIErrorException("Syntax error: expected a name", 12); + } +} diff --git a/Web/Events/ILPEmitable.php b/Web/Events/ILPEmitable.php index 3da2843fa..739626c32 100644 --- a/Web/Events/ILPEmitable.php +++ b/Web/Events/ILPEmitable.php @@ -1,7 +1,10 @@ -payload = $message->simplify(); } - - function getLongPoolSummary(): object + + public function getLongPoolSummary(): object { return (object) [ "type" => "newMessage", "message" => $this->payload, ]; } - - function getVKAPISummary(int $userId): array + + public function getVKAPISummary(int $userId): array { - $msg = (new Messages)->get($this->payload["uuid"]); + $msg = (new Messages())->get($this->payload["uuid"]); $peer = $msg->getSender()->getId(); - if($peer === $userId) + if ($peer === $userId) { $peer = $msg->getRecipient()->getId(); - + } + /* * Source: * https://github.com/danyadev/longpoll-doc @@ -38,12 +43,12 @@ function getVKAPISummary(int $userId): array 256, # checked for spam flag $peer, # TODO calculate peer correctly $msg->getSendTime()->timestamp(), # creation time in unix - $msg->getText(), # text (formatted) - [], # empty additional info - [], # empty attachments + $msg->getText(false), # text (formatted) + (object) [], # empty additional info + (object) [], # empty attachments $msg->getId() << 2, # id as random_id $peer, # conversation id - 0 # not edited yet + 0, # not edited yet ]; } } diff --git a/Web/Events/TypingEvent.php b/Web/Events/TypingEvent.php new file mode 100644 index 000000000..faf4094b4 --- /dev/null +++ b/Web/Events/TypingEvent.php @@ -0,0 +1,42 @@ +payload = $userId; + } + + public function getLongPoolSummary(): object + { + return (object) [ + "type" => "typing", + "message" => $this->payload, + ]; + } + + public function getVKAPISummary(int $userId): array + { + /* + * $userId is intentionally not used to not break code + * + * Source: + * https://dev.vk.com/ru/api/user-long-poll/getting-started + */ + + return [ + 61, # event type + $this->payload, # userId + 1, + ]; + } +} diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index 0cc531487..b46916449 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -1,5 +1,9 @@ -get($this->getRecord()->user); + return (new Users())->get($this->getRecord()->user); } - - function getSecret(): string + + public function getSecret(): string { return $this->getRecord()->secret; } - - function getFormattedToken(): string + + public function getFormattedToken(): string { return $this->getId() . "-" . chunk_split($this->getSecret(), 8, "-") . "jill"; } - function getPlatform(): ?string + public function getPlatform(): ?string { return $this->getRecord()->platform; } - - function isRevoked(): bool + + public function isRevoked(): bool { return $this->isDeleted(); } - - function setUser(User $user): void + + public function setUser(User $user): void { $this->stateChanges("user", $user->getId()); } - - function setSecret(string $secret): void + + public function setSecret(string $secret): void { throw new ISE("Setting secret manually is prohbited"); } - - function revoke(): void + + public function revoke(): void { $this->delete(); } - - function save(?bool $log = false): void + + public function save(?bool $log = false): void { - if(is_null($this->getRecord())) + if (is_null($this->getRecord())) { $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); - + } + parent::save(); } } diff --git a/Web/Models/Entities/Album.php b/Web/Models/Entities/Album.php index 150cc6257..707b955ac 100644 --- a/Web/Models/Entities/Album.php +++ b/Web/Models/Entities/Album.php @@ -1,93 +1,116 @@ - "_avatar_album", 32 => "_wall_album", 64 => "_saved_photos_album", ]; - - function getCoverURL(): ?string + + public function getCoverURL(): ?string { $coverPhoto = $this->getCoverPhoto(); - if(!$coverPhoto) - return "/assets/packages/static/openvk/img/camera_200.png"; - + if (!$coverPhoto) { + $server_url = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; + + return $server_url . "/assets/packages/static/openvk/img/camera_200.png"; + } + return $coverPhoto->getURL(); } - - function getCoverPhoto(): ?Photo + + public function getCoverPhoto(): ?Photo { $cover = $this->getRecord()->cover_photo; - if(!$cover) { + if (!$cover) { $photos = iterator_to_array($this->getPhotos(1, 1)); - $photo = $photos[0] ?? NULL; - if(!$photo || $photo->isDeleted()) - return NULL; - else + $photo = $photos[0] ?? null; + if (!$photo || $photo->isDeleted()) { + return null; + } else { return $photo; + } } - - return (new Photos)->get($cover); + + return (new Photos())->get($cover); } - - function getPhotos(int $page = 1, ?int $perPage = NULL): \Traversable + + public function getPhotos(int $page = 1, ?int $perPage = null): \Traversable { return $this->fetch($page, $perPage); } - - function getPhotosCount(): int + + public function getPhotosCount(): int { return $this->size(); } - - function addPhoto(Photo $photo): void + + public function addPhoto(Photo $photo): void { $this->add($photo); } - - function removePhoto(Photo $photo): void + + public function removePhoto(Photo $photo): void { $this->remove($photo); } - - function hasPhoto(Photo $photo): bool + + public function hasPhoto(Photo $photo): bool { return $this->has($photo); } - function toVkApiStruct(?User $user = NULL, bool $need_covers = false, bool $photo_sizes = false): object + public function canBeViewedBy(?User $user = null): bool + { + if ($this->isDeleted()) { + return false; + } + + $owner = $this->getOwner(); + + if (get_class($owner) == "openvk\\Web\\Models\\Entities\\User") { + return $owner->canBeViewedBy($user) && $owner->getPrivacyPermission('photos.read', $user); + } else { + return $owner->canBeViewedBy($user); + } + } + + public function toVkApiStruct(?User $user = null, bool $need_covers = false, bool $photo_sizes = false): object { $res = (object) []; - $res->id = $this->getPrettyId(); - $res->thumb_id = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getPrettyId() : 0; - $res->owner_id = $this->getOwner()->getId(); + $res->id = $this->getId(); + $res->thumb_id = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getId() : '0'; + $res->owner_id = $this->getOwner()->getRealId(); $res->title = $this->getName(); $res->description = $this->getDescription(); $res->created = $this->getCreationTime()->timestamp(); - $res->updated = $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL; + $res->updated = $this->getEditTime() ? $this->getEditTime()->timestamp() : $res->created; $res->size = $this->size(); $res->privacy_comment = 1; $res->upload_by_admins_only = 1; $res->comments_disabled = 0; - $res->can_upload = $this->canBeModifiedBy($user); # thisUser недоступен в entities - if($need_covers) { + $res->can_upload = (int) $this->canBeModifiedBy($user); # thisUser недоступен в entities + if ($need_covers) { $res->thumb_src = $this->getCoverURL(); - if($photo_sizes) { - $res->sizes = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getVkApiSizes() : NULL; + if ($photo_sizes) { + $res->sizes = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getVkApiSizes() : null; } } diff --git a/Web/Models/Entities/Alias.php b/Web/Models/Entities/Alias.php index 99f7baae1..c9e0cd448 100644 --- a/Web/Models/Entities/Alias.php +++ b/Web/Models/Entities/Alias.php @@ -1,4 +1,7 @@ -getRecord()->owner_id; } - function getType(): string + public function getType(): string { - if ($this->getOwnerId() < 0) + if ($this->getOwnerId() < 0) { return "club"; + } return "user"; } - function getUser(): ?User + public function getUser(): ?User { - return (new Users)->get($this->getOwnerId()); + return (new Users())->get($this->getOwnerId()); } - function getClub(): ?Club + public function getClub(): ?Club { - return (new Clubs)->get($this->getOwnerId() * -1); + return (new Clubs())->get($this->getOwnerId() * -1); } } diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php index 489756453..7053dc41d 100644 --- a/Web/Models/Entities/Application.php +++ b/Web/Models/Entities/Application.php @@ -1,5 +1,9 @@ -getRecord()->id; } - - function getOwner(): User + + public function getOwner(): User { - return (new Users)->get($this->getRecord()->owner); + return (new Users())->get($this->getRecord()->owner); } - - function getName(): string + + public function getName(): string { return $this->getRecord()->name; } - - function getDescription(): string + + public function getDescription(): string { return $this->getRecord()->description; } - - function getAvatarUrl(): string + + public function getAvatarUrl(): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - if(is_null($this->getRecord()->avatar_hash)) + 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"]) { + switch (OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { default: case "default": case "basic": @@ -78,162 +84,175 @@ function getAvatarUrl(): string case "server": $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; return ( - $settings->protocol ?? ovk_scheme() . + ($settings->protocol ?? ovk_scheme()) . "://" . $settings->host . $settings->path . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png" ); } } - - function getNote(): ?Note + + public function getNote(): ?Note { - if(!$this->getRecord()->news) - return NULL; - - return (new Notes)->get($this->getRecord()->news); + if (!$this->getRecord()->news) { + return null; + } + + return (new Notes())->get($this->getRecord()->news); } - - function getNoteLink(): string + + public function getNoteLink(): string { $note = $this->getNote(); - if(!$note) + if (!$note) { return ""; - + } + return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/note" . $note->getPrettyId(); } - - function getBalance(): float + + public function getBalance(): float { return $this->getRecord()->coins; } - - function getURL(): string + + public function getURL(): string { return $this->getRecord()->address; } - - function getOrigin(): string + + public function getOrigin(): string { $parsed = parse_url($this->getURL()); - + return ( ($parsed["scheme"] ?? "https") . "://" . ($parsed["host"] ?? "127.0.0.1") . ":" . ($parsed["port"] ?? "443") ); } - - function getUsersCount(): int + + public function getUsersCount(): int { $cx = DatabaseConnection::i()->getContext(); return sizeof($cx->table("app_users")->where("app", $this->getId())); } - - function getInstallationEntry(User $user): ?array + + public 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; - + + if (!$entry) { + return null; + } + return $entry->toArray(); } - - function getPermissions(User $user): array + + public function getPermissions(User $user): array { $permMask = 0; $installInfo = $this->getInstallationEntry($user); - if(!$installInfo) + if (!$installInfo) { $this->install($user); - else + } else { $permMask = $installInfo["access"]; - + } + $res = []; - for($i = 0; $i < sizeof(self::PERMS); $i++) { + for ($i = 0; $i < sizeof(self::PERMS); $i++) { $checkVal = 1 << $i; - if(($permMask & $checkVal) > 0) + if (($permMask & $checkVal) > 0) { $res[] = self::PERMS[$i]; + } } - + return $res; } - - function isInstalledBy(User $user): bool + + public function isInstalledBy(User $user): bool { return !is_null($this->getInstallationEntry($user)); } - - function setNoteLink(?string $link): bool + + public function setNoteLink(?string $link): bool { - if(!$link) { - $this->stateChanges("news", NULL); - + if (!$link) { + $this->stateChanges("news", null); + return true; } - + preg_match("%note([0-9]+)_([0-9]+)$%", $link, $matches); - if(sizeof($matches) != 3) + if (sizeof($matches) != 3) { return false; - + } + $owner = is_null($this->getRecord()) ? $this->changes["owner"] : $this->getRecord()->owner; [, $ownerId, $vid] = $matches; - if($ownerId != $owner) + if ($ownerId != $owner) { return false; - - $note = (new Notes)->getNoteById((int) $ownerId, (int) $vid); - if(!$note) + } + + $note = (new Notes())->getNoteById((int) $ownerId, (int) $vid); + if (!$note) { return false; - + } + $this->stateChanges("news", $note->getId()); - + return true; } - - function setAvatar(array $file): int + + public function setAvatar(array $file): int { - if($file["error"] !== UPLOAD_ERR_OK) + 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))) + 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 + + public function setPermission(User $user, string $perm, bool $enabled): bool { $permMask = 0; $installInfo = $this->getInstallationEntry($user); - if(!$installInfo) + if (!$installInfo) { $this->install($user); - else + } else { $permMask = $installInfo["access"]; - + } + $index = array_search($perm, self::PERMS); - if($index === false) + 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(), @@ -241,30 +260,30 @@ function setPermission(User $user, string $perm, bool $enabled): bool ])->update([ "access" => $permMask, ]); - + return true; } - - function isEnabled(): bool + + public function isEnabled(): bool { return (bool) $this->getRecord()->enabled; } - - function enable(): void + + public function enable(): void { $this->stateChanges("enabled", 1); $this->save(); } - - function disable(): void + + public function disable(): void { $this->stateChanges("enabled", 0); $this->save(); } - - function install(User $user): void + + public function install(User $user): void { - if(!$this->getInstallationEntry($user)) { + if (!$this->getInstallationEntry($user)) { $cx = DatabaseConnection::i()->getContext(); $cx->table("app_users")->insert([ "app" => $this->getId(), @@ -272,8 +291,8 @@ function install(User $user): void ]); } } - - function uninstall(User $user): void + + public function uninstall(User $user): void { $cx = DatabaseConnection::i()->getContext(); $cx->table("app_users")->where([ @@ -281,39 +300,44 @@ function uninstall(User $user): void "user" => $user->getId(), ])->delete(); } - - function addCoins(float $coins): float + + public function addCoins(float $coins): float { $res = $this->getBalance() + $coins; $this->stateChanges("coins", $res); $this->save(); - + return $res; } - - function withdrawCoins(): void + + public 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 + + public function delete(bool $softly = true): void { - if($softly) - throw new \UnexpectedValueException("Can't delete apps softly."); // why - $cx = DatabaseConnection::i()->getContext(); - $cx->table("app_users")->where("app", $this->getId())->delete(); - - parent::delete(false); + $app_users = $cx->table("app_users")->where("app", $this->getId()); + + if ($softly) { + $app_users->update(["deleted" => 1]); + } else { + $app_users->delete(); + } + + parent::delete($softly); } - function getPublicationTime(): string - { return tr("recently"); } -} \ No newline at end of file + public function getPublicationTime(): string + { + return tr("recently"); + } +} diff --git a/Web/Models/Entities/Attachable.php b/Web/Models/Entities/Attachable.php index a83c73838..5cdd54af3 100644 --- a/Web/Models/Entities/Attachable.php +++ b/Web/Models/Entities/Attachable.php @@ -1,38 +1,42 @@ -getRecord()->id; } - - function getParents(): \Traversable + + public function getParents(): \Traversable { $sel = $this->getRecord() ->related("attachments.attachable_id") ->where("attachments.attachable_type", get_class($this)); - foreach($sel as $rel) { + foreach ($sel as $rel) { $repoName = $rel->target_type . "s"; $repoName = str_replace("Entities", "Repositories", $repoName); - $repo = new $repoName; - + $repo = new $repoName(); + yield $repo->get($rel->target_id); } } - + /** * Deletes together with all references. */ - function delete(bool $softly = true): void + public function delete(bool $softly = true): void { $this->getRecord() ->related("attachments.attachable_id") ->where("attachments.attachable_type", get_class($this)) ->delete(); - + parent::delete(); } } diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php new file mode 100644 index 000000000..27a302fe7 --- /dev/null +++ b/Web/Models/Entities/Audio.php @@ -0,0 +1,545 @@ + 1, + "Pop" => 2, + "Rap" => 3, + "Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK + "Easy Listening" => 4, + "House" => 5, + "Dance" => 5, + "Instrumental" => 6, + "Metal" => 7, + "Alternative" => 21, + "Dubstep" => 8, + "Jazz" => 1001, + "Blues" => 1001, + "Drum & Bass" => 10, + "Trance" => 11, + "Chanson" => 12, + "Ethnic" => 13, + "Acoustic" => 14, + "Vocal" => 14, + "Reggae" => 15, + "Classical" => 16, + "Indie Pop" => 17, + "Speech" => 19, + "Disco" => 22, + "Other" => 18, + ]; + + private function fileLength(string $filename): int + { + if (!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe")) { + throw new \Exception(); + } + + $error = null; + $streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error); + if ($error !== 0) { + throw new \DomainException("$filename is not recognized as media container"); + } elseif (empty($streams) || ctype_space($streams)) { + throw new \DomainException("$filename does not contain any audio streams"); + } + + $vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + # check if audio has cover (attached_pic) + preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover); + if (!empty($vstreams) && !ctype_space($vstreams) && ((int) ($hasCover[1]) !== 1)) { + throw new \DomainException("$filename is a video"); + } + + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + if (sizeof($durations[1]) === 0) { + throw new \DomainException("$filename does not contain any meaningful audio streams"); + } + + $length = 0; + foreach ($durations[1] as $duration) { + $duration = floatval($duration); + if ($duration < 1.0 || $duration > 65536.0) { + throw new \DomainException("$filename does not contain any meaningful audio streams"); + } else { + $length = max($length, $duration); + } + } + + return (int) round($length, 0, PHP_ROUND_HALF_EVEN); + } + + /** + * @throws \Exception + */ + protected function saveFile(string $filename, string $hash): bool + { + $duration = $this->fileLength($filename); + + $kid = openssl_random_pseudo_bytes(16); + $key = openssl_random_pseudo_bytes(16); + $tok = openssl_random_pseudo_bytes(28); + $ss = ceil($duration / 15); + + $this->stateChanges("kid", $kid); + $this->stateChanges("key", $key); + $this->stateChanges("token", $tok); + $this->stateChanges("segment_size", $ss); + $this->stateChanges("length", $duration); + + try { + $args = [ + str_replace("enabled", "available", OPENVK_ROOT), + str_replace("enabled", "available", $this->getBaseDir()), + $hash, + $filename, + + bin2hex($kid), + bin2hex($key), + bin2hex($tok), + $ss, + ]; + + if (Shell::isPowershell()) { + Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args) + ->start(); + } else { + Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk + ->start(); // idk, not tested :") + } + + # Wait until processAudio will consume the file + $start = time(); + while (file_exists($filename)) { + if (time() - $start > 5) { + throw new \RuntimeException("Timed out waiting FFMPEG"); + } + } + + } catch (UnknownCommandException $ucex) { + exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR); + } + + return true; + } + + public function getTitle(): string + { + return $this->getRecord()->name; + } + + public function getPerformer(): string + { + return $this->getRecord()->performer; + } + + public function getPerformers(): array + { + return explode(", ", $this->getRecord()->performer); + } + + public function getName(): string + { + return $this->getPerformer() . " — " . $this->getTitle(); + } + + public function getDownloadName(): string + { + return preg_replace('/[\\/:*?"<>|]/', '_', str_replace(' ', '_', $this->getName())); + } + + public function getGenre(): ?string + { + return $this->getRecord()->genre; + } + + public function getLyrics(): ?string + { + return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : null; + } + + public function getLength(): int + { + return $this->getRecord()->length; + } + + public function getFormattedLength(): string + { + $len = $this->getLength(); + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + public function getSegmentSize(): float + { + return $this->getRecord()->segment_size; + } + + public function getListens(): int + { + return $this->getRecord()->listens; + } + + public function getOriginalURL(bool $force = false): string + { + $disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force; + if (!$this->isAvailable() || $disallowed) { + return ovk_scheme(true) + . $_SERVER["HTTP_HOST"] . ":" + . $_SERVER["HTTP_PORT"] + . "/assets/packages/static/openvk/audio/api_unallowed.mp3"; + } + + $key = bin2hex($this->getRecord()->token); + + return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3"; + } + + public function getURL(?bool $force = false): string + { + if ($this->isWithdrawn()) { + return ""; + } + + return parent::getURL(); + } + + public function getKeys(): array + { + $keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key); + + return $keys; + } + + public function isAnonymous(): bool + { + return false; + } + + public function isExplicit(): bool + { + return (bool) $this->getRecord()->explicit; + } + + public function isWithdrawn(): bool + { + return (bool) $this->getRecord()->withdrawn; + } + + public function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + # NOTICE may flush model to DB if it was just processed + public function isAvailable(): bool + { + if ($this->getRecord()->processed) { + return true; + } + + # throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage + if (time() - $this->getRecord()->checked < 5) { + return false; + } + + try { + $fragments = str_replace(".mpd", "_fragments", $this->getFileName()); + $original = "original_" . bin2hex($this->getRecord()->token) . ".mp3"; + if (file_exists("$fragments/$original")) { + # Original gets uploaded after fragments + $this->stateChanges("processed", 0x01); + + return true; + } + } finally { + $this->stateChanges("checked", time()); + $this->save(); + } + + return false; + } + + public function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])) != 0; + } + + public function add($entity): bool + { + if ($this->isInLibraryOf($entity)) { + return false; + } + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("audio_relations"); + if (sizeof($audioRels->where("entity", $entityId)) > 65536) { + throw new \OverflowException("Can't have more than 65536 audios in a playlist"); + } + + $audioRels->insert([ + "entity" => $entityId, + "audio" => $this->getId(), + ]); + + return true; + } + + public function remove($entity): bool + { + if (!$this->isInLibraryOf($entity)) { + return false; + } + + DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])->delete(); + + return true; + } + + public function listen($entity, Playlist $playlist = null): bool + { + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + ])->order("index DESC")->fetch(); + + if (!$lastListen || (time() - $lastListen->time >= $this->getLength())) { + $listensTable->insert([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + "time" => time(), + "playlist" => $playlist ? $playlist->getId() : null, + ]); + + if ($entity instanceof User) { + $this->stateChanges("listens", ($this->getListens() + 1)); + $this->save(); + + if ($playlist) { + $playlist->incrementListens(); + $playlist->save(); + } + } + + $entity->setLast_played_track($this->getId()); + $entity->save(); + + return true; + } + + $lastListen->update([ + "time" => time(), + ]); + + return false; + } + + /** + * Returns compatible with VK API 4.x, 5.x structure. + * + * Always sets album(_id) to NULL at this time. + * If genre is not present in VK genre list, fallbacks to "Other". + * The url and manifest properties will be set to false if the audio can't be played (processing, removed). + * + * Aside from standard VK properties, this method will also return some OVK extended props: + * 1. added - Is in the library of $user? + * 2. editable - Can be edited by $user? + * 3. withdrawn - Removed due to copyright request? + * 4. ready - Can be played at this time? + * 5. genre_str - Full name of genre, NULL if it's undefined + * 6. manifest - URL to MPEG-DASH manifest + * 7. keys - ClearKey DRM keys + * 8. explicit - Marked as NSFW? + * 9. searchable - Can be found via search? + * 10. unique_id - Unique ID of audio + * + * @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to api_unallowed.mp3, + * unless $forceURLExposure is set to true. + * + * @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models. + * + * @param ?User $user user, relative to whom "added", "editable" will be set + * @param bool $forceURLExposure force set "url" regardless of config + */ + public function toVkApiStruct(?User $user = null, bool $forceURLExposure = false): object + { + $obj = (object) []; + $obj->unique_id = base64_encode((string) $this->getId()); + $obj->id = $obj->aid = $this->getVirtualId(); + $obj->artist = $this->getPerformer(); + $obj->title = $this->getTitle(); + $obj->duration = $this->getLength(); + $obj->url = false; + $obj->manifest = false; + $obj->keys = false; + $obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match + $obj->genre_str = $this->getGenre(); + $obj->owner_id = $this->getOwner()->getRealId(); + + if (!is_null($this->getLyrics())) { + $obj->lyrics_id = $this->getId(); + } + + $album = $this->getAlbum(); + if ($album) { + $obj->album = $album->toVkApiStruct($user); + $obj->album_id = $album->getPrettyId(); + } + + $obj->added = $user && $this->isInLibraryOf($user); + $obj->editable = $user && $this->canBeModifiedBy($user); + $obj->searchable = !$this->isUnlisted(); + $obj->explicit = $this->isExplicit(); + $obj->withdrawn = $this->isWithdrawn(); + $obj->ready = $this->isAvailable() && !$obj->withdrawn; + if ($obj->ready) { + $obj->url = $this->getOriginalURL($forceURLExposure); + $obj->manifest = $this->getURL(); + $obj->keys = $this->getKeys(); + } + + if ($obj->editable) { + $obj->listens = $this->getListens(); + } + + return $obj; + } + + public function setAlbum(Playlist $album): void + { + $this->stateChanges("playlist_id", $album->getId()); + } + + public function setAlbumId(int $album): void + { + $this->stateChanges("playlist_id", $album); + } + + public function getAlbum(): ?Playlist + { + $playlist_id = $this->getRecord()->playlist_id; + if (!$playlist_id) { + return null; + } + + $album = (new Audios())->getPlaylist($playlist_id); + + if (!$album || $album->isDeleted()) { + return null; + } + + return $album; + } + + public function getAlbumId(): ?int + { + return $this->getRecord()->playlist_id; + } + + public function setOwner(int $oid): void + { + # WARNING: API implementation won't be able to handle groups like that, don't remove + if ($oid <= 0) { + throw new \OutOfRangeException("Only users can be owners of audio!"); + } + + $this->stateChanges("owner", $oid); + } + + public function setGenre(string $genre): void + { + if (!in_array($genre, Audio::genres)) { + $this->stateChanges("genre", null); + return; + } + + $this->stateChanges("genre", $genre); + } + + public function setCopyrightStatus(bool $withdrawn = true): void + { + $this->stateChanges("withdrawn", $withdrawn); + } + + public function setSearchability(bool $searchable = true): void + { + $this->stateChanges("unlisted", !$searchable); + } + + public function setToken(string $tok): void + { + throw new \LogicException("Changing keys is not supported."); + } + + public function setKid(string $kid): void + { + throw new \LogicException("Changing keys is not supported."); + } + + public function setKey(string $key): void + { + throw new \LogicException("Changing keys is not supported."); + } + + public function setLength(int $len): void + { + throw new \LogicException("Changing length is not supported."); + } + + public function setSegment_Size(int $len): void + { + throw new \LogicException("Changing length is not supported."); + } + + public function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("audio_relations")->where("audio", $this->getId()) + ->delete(); + $ctx->table("audio_listens")->where("audio", $this->getId()) + ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} diff --git a/Web/Models/Entities/Ban.php b/Web/Models/Entities/Ban.php index 3962c6cbf..6d600a57f 100644 --- a/Web/Models/Entities/Ban.php +++ b/Web/Models/Entities/Ban.php @@ -1,5 +1,9 @@ -getRecord()->id; } - function getReason(): ?string + public function getReason(): ?string { return $this->getRecord()->reason; } - function getUser(): ?User + public function getUser(): ?User { - return (new Users)->get($this->getRecord()->user); + return (new Users())->get($this->getRecord()->user); } - function getInitiator(): ?User + public function getInitiator(): ?User { - return (new Users)->get($this->getRecord()->initiator); + return (new Users())->get($this->getRecord()->initiator); } - function getStartTime(): int + public function getStartTime(): int { return $this->getRecord()->iat; } - function getEndTime(): int + public function getEndTime(): int { return $this->getRecord()->exp; } - function getTime(): int + public function getTime(): int { return $this->getRecord()->time; } - function isPermanent(): bool + public function isPermanent(): bool { return $this->getEndTime() === 0; } - function isRemovedManually(): bool + public function isRemovedManually(): bool { return (bool) $this->getRecord()->removed_manually; } - function isOver(): bool + public function isOver(): bool { return $this->isRemovedManually(); } - function whoRemoved(): ?User + public function whoRemoved(): ?User { - return (new Users)->get($this->getRecord()->removed_by); + return (new Users())->get($this->getRecord()->removed_by); } } diff --git a/Web/Models/Entities/BannedLink.php b/Web/Models/Entities/BannedLink.php index 09c42e395..70793f9ea 100644 --- a/Web/Models/Entities/BannedLink.php +++ b/Web/Models/Entities/BannedLink.php @@ -1,5 +1,9 @@ -getRecord()->id; } - function getDomain(): string + public function getDomain(): string { return $this->getRecord()->domain; } - function getReason(): string + public function getReason(): string { return $this->getRecord()->reason ?? tr("url_is_banned_default_reason"); } - function getInitiator(): ?User + public function getInitiator(): ?User { - return (new Users)->get($this->getRecord()->initiator); + return (new Users())->get($this->getRecord()->initiator); } - function getComment(): string + public 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 + public function getRegexpRule(): string { - return addslashes("/" . $this->getDomain() . $this->getRawRegexp() . "/"); + return "/^" . $this->getDomain() . "\/" . $this->getRawRegexp() . "$/i"; } - function getRawRegexp(): string + public function getRawRegexp(): string { return $this->getRecord()->regexp_rule; } diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index fbdc503b8..509ef0ca2 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -1,220 +1,263 @@ -getRecord()->id; } - - function getAvatarPhoto(): ?Photo + + public function getAvatarPhoto(): ?Photo { - $avAlbum = (new Albums)->getClubAvatarAlbum($this); + $avAlbum = (new Albums())->getClubAvatarAlbum($this); $avCount = $avAlbum->getPhotosCount(); $avPhotos = $avAlbum->getPhotos($avCount, 1); - - return iterator_to_array($avPhotos)[0] ?? NULL; + + return iterator_to_array($avPhotos)[0] ?? null; } - - function getAvatarUrl(string $size = "miniscule"): string + + public function getAvatarUrl(string $size = "miniscule", $avPhoto = null): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - $avPhoto = $this->getAvatarPhoto(); - + if (!$avPhoto) { + $avPhoto = $this->getAvatarPhoto(); + } + return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size); } - - function getAvatarLink(): string + + public function getWallType(): int + { + return $this->getRecord()->wall; + } + + public function getAvatarLink(): string { $avPhoto = $this->getAvatarPhoto(); - if(!$avPhoto) return "javascript:void(0)"; - + if (!$avPhoto) { + return "javascript:void(0)"; + } + $pid = $avPhoto->getPrettyId(); - $aid = (new Albums)->getClubAvatarAlbum($this)->getId(); - + $aid = (new Albums())->getClubAvatarAlbum($this)->getId(); + return "/photo$pid?from=album$aid"; } - - function getURL(): string + + public function getURL(): string { - if(!is_null($this->getShortCode())) + if (!is_null($this->getShortCode())) { return "/" . $this->getShortCode(); - else + } else { return "/club" . $this->getId(); + } } - - function getName(): string + + public function getName(): string { return $this->getRecord()->name; } - - function getCanonicalName(): string + + public function getCanonicalName(): string { return $this->getName(); } - - function getOwner(): ?User + + public function getOwner(): ?User { - return (new Users)->get($this->getRecord()->owner); + return (new Users())->get($this->getRecord()->owner); } - function getOwnerComment(): string + public function getOwnerComment(): string { return is_null($this->getRecord()->owner_comment) ? "" : $this->getRecord()->owner_comment; } - function isOwnerHidden(): bool + public function isOwnerHidden(): bool { return (bool) $this->getRecord()->owner_hidden; } - - function isOwnerClubPinned(): bool + + public function isOwnerClubPinned(): bool { return (bool) $this->getRecord()->owner_club_pinned; } - function getDescription(): ?string + public function getDescription(): ?string { return $this->getRecord()->about; } - function getDescriptionHtml(): ?string + public function getDescriptionHtml(): ?string { - if(!is_null($this->getDescription())) + if (!is_null($this->getDescription())) { return nl2br(htmlspecialchars($this->getDescription(), ENT_DISALLOWED | ENT_XHTML)); - else - return NULL; + } else { + return null; + } } - - function getShortCode(): ?string + + public function getShortCode(): ?string { return $this->getRecord()->shortcode; } - - function getBanReason(): ?string + + public function getBanReason(): ?string { return $this->getRecord()->block_reason; } - - function getOpennesStatus(): int + + public function getOpennesStatus(): int { return $this->getRecord()->closed; } - function getAdministratorsListDisplay(): int + public function getAdministratorsListDisplay(): int { return $this->getRecord()->administrators_list_display; } - - function isEveryoneCanCreateTopics(): bool + + public function isEveryoneCanCreateTopics(): bool { return (bool) $this->getRecord()->everyone_can_create_topics; } - function isDisplayTopicsAboveWallEnabled(): bool + public function isDisplayTopicsAboveWallEnabled(): bool { return (bool) $this->getRecord()->display_topics_above_wall; } - function isHideFromGlobalFeedEnabled(): bool + public function isHideFromGlobalFeedEnabled(): bool { return (bool) $this->getRecord()->hide_from_global_feed; } - function getType(): int + public function isHidingFromGlobalFeedEnforced(): bool + { + return (bool) $this->getRecord()->enforce_hiding_from_global_feed; + } + + public function getType(): int { return $this->getRecord()->type; } - - function isVerified(): bool + + public function isVerified(): bool { return (bool) $this->getRecord()->verified; } - - function isBanned(): bool + + public function isBanned(): bool { return !is_null($this->getBanReason()); } - function canPost(): bool + public function canPost(): bool { - return (bool) $this->getRecord()->wall; + return (bool) $this->getRecord()->wall; } - - function setShortCode(?string $code = NULL): ?bool + + public function setShortCode(?string $code = null): ?bool { - if(!is_null($code)) { - if(!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code)) + if (!is_null($code)) { + if (!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code)) { return false; - if(in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"])) + } + if (in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"])) { return false; - if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") + } + if (\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") { return false; - + } + $pUser = DB::i()->getContext()->table("profiles")->where("shortcode", $code)->fetch(); - if(!is_null($pUser)) + if (!is_null($pUser)) { return false; + } } - + $this->stateChanges("shortcode", $code); return true; } - - function isSubscriptionAccepted(User $user): bool + + public function setWall(int $type) + { + if ($type > 2 || $type < 0) { + throw new \LogicException("Invalid wall"); + } + + $this->stateChanges("wall", $type); + } + + public function isSubscriptionAccepted(User $user): bool { return !is_null($this->getRecord()->related("subscriptions.follower")->where([ "follower" => $this->getId(), "target" => $user->getId(), - ])->fetch());; + ])->fetch()); + ; } - - function getPostViewStats(bool $unique = false): ?array + + public function getPostViewStats(bool $unique = false): ?array { $edb = eventdb(); - if(!$edb) - return NULL; - + if (!$edb) { + return null; + } + $subs = []; $viral = []; $total = []; - for($i = 1; $i < 8; $i++) { + for ($i = 1; $i < 8; $i++) { $begin = strtotime("-" . $i . "day midnight"); $end = $i === 1 ? time() + 10 : strtotime("-" . ($i - 1) . "day midnight"); - + $query = "SELECT COUNT(" . ($unique ? "DISTINCT profile" : "*") . ") AS cnt FROM postViews"; $query .= " WHERE `group`=1 AND owner=" . $this->getId(); $query .= " AND timestamp > $begin AND timestamp < $end"; - + $sub = $edb->getConnection()->query("$query AND NOT subscribed=0")->fetch()->cnt; $vir = $edb->getConnection()->query("$query AND subscribed=0")->fetch()->cnt; $subs[] = $sub; $viral[] = $vir; $total[] = $sub + $vir; } - + return [ "total" => [ "x" => array_reverse(range(1, 7)), @@ -250,81 +293,105 @@ function getPostViewStats(bool $unique = false): ?array ], ]; } - - function getSubscriptionStatus(User $user): bool + + public function getSubscriptionStatus(User $user): bool { $subbed = !is_null($this->getRecord()->related("subscriptions.target")->where([ "target" => $this->getId(), "model" => static::class, "follower" => $user->getId(), ])->fetch()); - + return $subbed && ($this->getOpennesStatus() === static::CLOSED ? $this->isSubscriptionAccepted($user) : true); } - - function getFollowersQuery(string $sort = "follower ASC"): GroupedSelection + + public function getFollowersQuery(string $sort = "follower ASC"): GroupedSelection { $query = $this->getRecord()->related("subscriptions.target"); - - if($this->getOpennesStatus() === static::OPEN) { + + if ($this->getOpennesStatus() === static::OPEN) { $query = $query->where("model", "openvk\\Web\\Models\\Entities\\Club")->order($sort); } else { return false; } - + return $query->group("follower"); } - - function getFollowersCount(): int + + public function getFollowersCount(): int { return sizeof($this->getFollowersQuery()); } - - function getFollowers(int $page = 1, int $perPage = 6, string $sort = "follower ASC"): \Traversable + + public function getFollowers(int $page = 1, int $perPage = 6, string $sort = "target DESC"): \Traversable { $rels = $this->getFollowersQuery($sort)->page($page, $perPage); - - foreach($rels as $rel) { - $rel = (new Users)->get($rel->follower); - if(!$rel) continue; - + + foreach ($rels as $rel) { + $rel = (new Users())->get($rel->follower); + if (!$rel) { + continue; + } + yield $rel; } } - - function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable + + public function getSuggestedPostsCount(User $user = null) + { + $count = 0; + + if (is_null($user)) { + return null; + } + + if ($this->canBeModifiedBy($user)) { + $count = (new Posts())->getSuggestedPostsCount($this->getId()); + } else { + $count = (new Posts())->getSuggestedPostsCountByUser($this->getId(), $user->getId()); + } + + return $count; + } + + public function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable { $rels = $this->getRecord()->related("group_coadmins.club")->page($page, 6); - if($ignoreHidden) - $rels = $rels->where("hidden", false); - - foreach($rels as $rel) { - $rel = (new Managers)->get($rel->id); - if(!$rel) continue; + if ($ignoreHidden) { + $rels = $rels->where("club_pinned", false); + } + + foreach ($rels as $rel) { + $rel = (new Managers())->get($rel->id); + if (!$rel) { + continue; + } yield $rel; } } - function getManager(User $user, bool $ignoreHidden = false): ?Manager + public function getManager(User $user, bool $ignoreHidden = false): ?Manager { - $manager = (new Managers)->getByUserAndClub($user->getId(), $this->getId()); + $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; } - - function getManagersCount(bool $ignoreHidden = false): int + + public function getManagersCount(bool $ignoreHidden = false): int { - if($ignoreHidden) + if ($ignoreHidden) { return sizeof($this->getRecord()->related("group_coadmins.club")->where("hidden", false)) + (int) !$this->isOwnerHidden(); + } return sizeof($this->getRecord()->related("group_coadmins.club")) + 1; } - - function addManager(User $user, ?string $comment = NULL): void + + public function addManager(User $user, ?string $comment = null): void { DB::i()->getContext()->table("group_coadmins")->insert([ "club" => $this->getId(), @@ -332,75 +399,191 @@ function addManager(User $user, ?string $comment = NULL): void "comment" => $comment, ]); } - - function removeManager(User $user): void + + public function removeManager(User $user): void { DB::i()->getContext()->table("group_coadmins")->where([ "club" => $this->getId(), "user" => $user->getId(), ])->delete(); } - - function canBeModifiedBy(User $user): bool + + public function canBeModifiedBy(User $user): bool { $id = $user->getId(); - if($this->getOwner()->getId() === $id) + if ($this->getOwner()->getId() === $id) { return true; - + } + return !is_null($this->getRecord()->related("group_coadmins.club")->where("user", $id)->fetch()); } - function getWebsite(): ?string - { - return $this->getRecord()->website; - } + public function getWebsite(): ?string + { + return $this->getRecord()->website; + } - function ban(string $reason): void + public function ban(string $reason): void { $this->setBlock_Reason($reason); $this->save(); } - function unban(): void + public function delete(bool $softly = true): void + { + $this->ban(""); + } + + public function unban(): void { $this->setBlock_Reason(null); $this->save(); } - function getAlert(): ?string + public function canBeViewedBy(?User $user = null) + { + return is_null($this->getBanReason()); + } + + public function getAlert(): ?string { return $this->getRecord()->alert; } - - function toVkApiStruct(?User $user = NULL): object + + public function getRealId(): int + { + return $this->getId() * -1; + } + + public function isEveryoneCanUploadAudios(): bool { - $res = []; + return (bool) $this->getRecord()->everyone_can_upload_audios; + } + + public function canUploadAudio(?User $user): bool + { + if (!$user) { + return null; + } + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + + public function canUploadDocs(?User $user): bool + { + if (!$user) { + return false; + } + + return $this->canBeModifiedBy($user); + } + + public function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios())->getClubCollectionSize($this); + } + + public function toVkApiStruct(?User $user = null, string $fields = ''): object + { + $res = (object) []; $res->id = $this->getId(); $res->name = $this->getName(); - $res->screen_name = $this->getShortCode(); + $res->screen_name = $this->getShortCode() ?? "club" . $this->getId(); $res->is_closed = 0; - $res->deactivated = NULL; - $res->is_admin = $this->canBeModifiedBy($user); + $res->type = 'group'; + $res->is_member = $user ? (int) $this->getSubscriptionStatus($user) : 0; + $res->is_admin = $user ? (int) $this->canBeModifiedBy($user) : 0; + $res->deactivated = null; + $res->can_access_closed = 1; - if($this->canBeModifiedBy($user)) { - $res->admin_level = 3; + if (!is_array($fields)) { + $fields = explode(',', $fields); } - $res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0; + $avatar_photo = $this->getAvatarPhoto(); + foreach ($fields as $field) { + switch ($field) { + case 'verified': + $res->verified = (int) $this->isVerified(); + break; + case 'site': + $res->site = $this->getWebsite(); + break; + case 'description': + $res->description = $this->getDescription() ?? ''; + break; + case 'background': + $res->background = $this->getBackDropPictureURLs(); + break; + case 'photo_50': + $res->photo_50 = $this->getAvatarUrl('miniscule', $avatar_photo); + break; + case 'photo_100': + $res->photo_100 = $this->getAvatarUrl('tiny', $avatar_photo); + break; + case 'photo_200': + $res->photo_200 = $this->getAvatarUrl('normal', $avatar_photo); + break; + case "photo_200_orig": + $res->photo_200_orig = $this->getAvatarURL("normal", $avatar_photo); + break; + case "photo_400_orig": + $res->photo_400_orig = $this->getAvatarURL("normal", $avatar_photo); + break; + case 'photo_max': + $res->photo_max = $this->getAvatarUrl('original', $avatar_photo); + break; + case 'members_count': + $res->members_count = $this->getFollowersCount(); + break; + case 'real_id': + $res->real_id = $this->getRealId(); + break; + case "can_suggest": + $res->can_suggest = !$this->canBeModifiedBy($user) && $this->getWallType() == 2; + break; + case "suggested_count": + if ($this->getWallType() != 2) { + $res->suggested_count = null; + break; + } - $res->type = "group"; - $res->photo_50 = $this->getAvatarUrl("miniscule"); - $res->photo_100 = $this->getAvatarUrl("tiny"); - $res->photo_200 = $this->getAvatarUrl("normal"); + $res->suggested_count = $this->getSuggestedPostsCount($user); + break; + case "contacts": + $contacts = []; + $contactTmp = $this->getManagers(1, true); - $res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); + foreach ($contactTmp as $contact) { + $contacts[] = [ + "user_id" => $contact->getUser()->getId(), + "desc" => $contact->getComment(), + ]; + } - $res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); + if ($this->isOwnerClubPinned()) { + $owner = (new Managers())->get($this->getOwner()->getId()); + array_unshift($contacts, [ + "user_id" => $owner->getUser()->getId(), + "desc" => $owner->getComment(), + ]); + } - return (object) $res; - } + $res->contacts = $contacts; + break; + case "can_post": + if (!is_null($user)) { + if ($this->canBeModifiedBy($user)) { + $res->can_post = true; + } else { + $res->can_post = $this->canPost(); + } + } + break; + } + } - use Traits\TBackDrops; - use Traits\TSubscribable; + return $res; + } } diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index 37b06dda3..ffb67e7a4 100644 --- a/Web/Models/Entities/Comment.php +++ b/Web/Models/Entities/Comment.php @@ -1,5 +1,9 @@ -getRecord()->id; + return (string) $this->getRecord()->id; } - - function getVirtualId(): int + + public function getVirtualId(): int { return 0; } - - function getTarget(): ?Postable + + public function getTarget(): ?Postable { $entityClassName = $this->getRecord()->model; $repoClassName = str_replace("Entities", "Repositories", $entityClassName) . "s"; - $entity = (new $repoClassName)->get($this->getRecord()->target); - + $entity = (new $repoClassName())->get($this->getRecord()->target); + return $entity; } + public function getPageURL(): string + { + return '#'; + } + /** * May return fake owner (group), if flags are [1, (*)] - * + * * @param bool $honourFlags - check flags */ - function getOwner(bool $honourFlags = true, bool $real = false): RowModel + public function getOwner(bool $honourFlags = true, bool $real = false): RowModel { - if($honourFlags && $this->isPostedOnBehalfOfGroup()) { - if($this->getTarget() instanceof Post) - return (new Clubs)->get(abs($this->getTarget()->getTargetWall())); + if ($honourFlags && $this->isPostedOnBehalfOfGroup()) { + if ($this->getTarget() instanceof Post) { + return (new Clubs())->get(abs($this->getTarget()->getTargetWall())); + } - if($this->getTarget() instanceof Topic) + if ($this->getTarget() instanceof Topic) { return $this->getTarget()->getClub(); + } } return parent::getOwner($honourFlags, $real); } - function canBeDeletedBy(User $user): bool + public function canBeDeletedBy(User $user = null): bool { + if (!$user) { + return false; + } + return $this->getOwner()->getId() == $user->getId() || $this->getTarget()->getOwner()->getId() == $user->getId() || - $this->getTarget() instanceof Post && $this->getTarget()->getTargetWall() < 0 && (new Clubs)->get(abs($this->getTarget()->getTargetWall()))->canBeModifiedBy($user) || + $this->getTarget() instanceof Post && $this->getTarget()->getTargetWall() < 0 && (new Clubs())->get(abs($this->getTarget()->getTargetWall()))->canBeModifiedBy($user) || $this->getTarget() instanceof Topic && $this->getTarget()->canBeModifiedBy($user); } - function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $extended = false, ?Note $note = NULL): object + public function toVkApiStruct(?User $user = null, bool $need_likes = false, bool $extended = false, ?Note $note = null): object { $res = (object) []; @@ -64,38 +79,111 @@ function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $exten $res->text = $this->getText(false); $res->attachments = []; $res->parents_stack = []; - - if(!is_null($note)) { + + if (!is_null($note)) { $res->uid = $this->getOwner()->getId(); $res->nid = $note->getId(); $res->oid = $note->getOwner()->getId(); } - foreach($this->getChildren() as $attachment) { - if($attachment->isDeleted()) + foreach ($this->getChildren() as $attachment) { + if ($attachment->isDeleted()) { continue; - - $res->attachments[] = $attachment->toVkApiStruct(); + } + + if ($attachment instanceof \openvk\Web\Models\Entities\Photo) { + $res->attachments[] = $attachment->toVkApiStruct(); + } elseif ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $res->attachments[] = $attachment->toVkApiStruct($this->getUser()); + } } - if($need_likes) { + if ($need_likes) { $res->count = $this->getLikesCount(); - $res->user_likes = (int)$this->hasLikeFrom($user); + $res->user_likes = (int) $this->hasLikeFrom($user); $res->can_like = 1; } return $res; } - function getURL(): string + public function getURL(): string { - return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId(); + return $this->getTargetURL() . "#_comment" . $this->getId(); } - function canBeEditedBy(?User $user = NULL): bool + public function canBeViewedBy(?User $user = null): bool { - if(!$user) + if ($this->isDeleted() || $this->getTarget()->isDeleted()) { return false; - + } + + return $this->getTarget()->canBeViewedBy($user); + } + + public function isFromPostAuthor($target = null) + { + if (!$target) { + $target = $this->getTarget(); + } + + $target_owner = $target->getOwner(); + $comment_owner = $this->getOwner(); + + if ($target_owner->getRealId() === $comment_owner->getRealId()) { + return true; + } + + # TODO: make it work with signer_id + + return false; + } + + public function toNotifApiStruct() + { + $res = (object) []; + + $res->id = $this->getId(); + $res->owner_id = $this->getOwner()->getId(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->text = $this->getText(false); + $res->post = null; # todo + + return $res; + } + + public function canBeEditedBy(?User $user = null): bool + { + if (!$user) { + return false; + } + return $user->getId() == $this->getOwner(false)->getId(); } + + public function getTargetURL(): string + { + $target = $this->getTarget(); + $target_name = 'wall'; + + if (!$target) { + return '/404'; + } + + switch (get_class($target)) { + case 'openvk\Web\Models\Entities\Note': + $target_name = 'note'; + break; + case 'openvk\Web\Models\Entities\Photo': + $target_name = 'photo'; + break; + case 'openvk\Web\Models\Entities\Video': + $target_name = 'video'; + break; + case 'openvk\Web\Models\Entities\Topic': + $target_name = 'topic'; + break; + } + + return $target_name . $target->getPrettyId(); + } } diff --git a/Web/Models/Entities/Correspondence.php b/Web/Models/Entities/Correspondence.php index a972425e5..bece63b30 100644 --- a/Web/Models/Entities/Correspondence.php +++ b/Web/Models/Entities/Correspondence.php @@ -1,9 +1,13 @@ -correspondents = [$correspondent, $anotherCorrespondent]; $this->messages = DatabaseConnection::i()->getContext()->table("messages"); } - + /** * Get /im?sel url. - * + * * @returns string - URL */ - function getURL(): string + public function getURL(): string { $id = $this->correspondents[1]->getId(); $id = get_class($this->correspondents[1]) === 'openvk\Web\Models\Entities\Club' ? $id * -1 : $id; - + return "/im?sel=$id"; } - - function getID(): int + + public function getID(): int { $id = $this->correspondents[1]->getId(); $id = get_class($this->correspondents[1]) === 'openvk\Web\Models\Entities\Club' ? $id * -1 : $id; - + return $id; } - + /** * Get correspondents as array. - * + * * @returns RowModel[] Array of correspondents (usually two) */ - function getCorrespondents(): array + public function getCorrespondents(): array { return $this->correspondents; } - + /** * Fetch messages. - * + * * Fetch messages on per page basis. - * + * * @param $cap - page (defaults to first) * @param $limit - messages per page (defaults to default per page count) * @returns \Traversable - iterable messages cursor */ - function getMessages(int $capBehavior = 1, ?int $cap = NULL, ?int $limit = NULL, ?int $padding = NULL, bool $reverse = false): array + public function getMessages(int $capBehavior = 1, ?int $cap = null, ?int $limit = null, ?int $padding = null, bool $reverse = false): array { $query = file_get_contents(__DIR__ . "/../sql/get-messages.tsql"); $params = [ [get_class($this->correspondents[0]), get_class($this->correspondents[1])], [$this->correspondents[0]->getId(), $this->correspondents[1]->getId()], - [$limit ?? OPENVK_DEFAULT_PER_PAGE] + [$limit ?? OPENVK_DEFAULT_PER_PAGE], ]; $params = array_merge($params[0], $params[1], array_reverse($params[0]), array_reverse($params[1]), $params[2]); - - if ($limit === NULL) - DatabaseConnection::i()->getConnection()->query("UPDATE messages SET unread = 0 WHERE sender_id = ".$this->correspondents[1]->getId()); - - if(is_null($cap)) { + + if ($limit === null) { + DatabaseConnection::i()->getConnection()->query("UPDATE messages SET unread = 0 WHERE sender_id = " . $this->correspondents[1]->getId()); + } + + if (is_null($cap)) { $query = str_replace("\n AND (`id` > ?)", "", $query); } else { - if($capBehavior === 1) + if ($capBehavior === 1) { $query = str_replace("\n AND (`id` > ?)", "\n AND (`id` < ?)", $query); - + } + array_unshift($params, $cap); } - - if(is_null($padding)) + + if (is_null($padding)) { $query = str_replace("\nOFFSET\n?", "", $query); - else + } else { $params[] = $padding; - - if($reverse) + } + + if ($reverse) { $query = str_replace("`created` DESC", "`created` ASC", $query); - + } + $msgs = DatabaseConnection::i()->getConnection()->query($query, ...$params); - $msgs = array_map(function($message) { + $msgs = array_map(function ($message) { $message = new ActiveRow((array) $message, $this->messages); #Directly creating ActiveRow is faster than making query - + return new Message($message); }, iterator_to_array($msgs)); - + return $msgs; } - + /** * Get last message from correspondence. - * + * * @returns Message|null - message, if any */ - function getPreviewMessage(): ?Message + public function getPreviewMessage(): ?Message { - $messages = $this->getMessages(1, NULL, 1, 0); - return $messages[0] ?? NULL; + $messages = $this->getMessages(1, null, 1, 0); + return $messages[0] ?? null; } - + + /** + * Get last message from correspondence from user. + * + * @returns Message|null - message, if any + */ + public function getLastReadedMessage(int $user_id): ?Message + { + $query = file_get_contents(__DIR__ . "/../sql/get-messages.tsql"); + $query = str_replace("\n AND (`id` > ?)", "\n AND (`unread` = 0)", $query); + $params = [ + [get_class($this->correspondents[0]), get_class($this->correspondents[1])], + [$this->correspondents[0]->getId(), $this->correspondents[1]->getId()], + [1], // limit + [0], // offset + ]; + + if ($user_id == $this->correspondents[0]->getId()) { + $params = array_merge($params[0], $params[1], $params[0], $params[1], $params[2], $params[3]); + } elseif ($user_id == $this->correspondents[1]->getId()) { + $params = array_merge(array_reverse($params[0]), array_reverse($params[1]), array_reverse($params[0]), array_reverse($params[1]), $params[2], $params[3]); + } + + $connection = DatabaseConnection::i()->getConnection(); + $msgs = $connection->query($query, ...$params); + $msgRow = $msgs->fetch(); + if ($msgRow !== null) { + $msg = new ActiveRow((array) $msgRow, $this->messages); + return new Message($msg); + } else { + return null; + } + } + /** * Send message. - * + * * @deprecated * @returns Message|false - resulting message, or false in case of non-successful transaction */ - function sendMessage(Message $message, bool $dontReverse = false) + public function sendMessage(Message $message, bool $dontReverse = false) { - if(!$dontReverse) { - $user = (new Users)->getByChandlerUser(Authenticator::i()->getUser()); - if(!$user) + if (!$dontReverse) { + $user = (new Users())->getByChandlerUser(Authenticator::i()->getUser()); + if (!$user) { return false; + } } - + $ids = [$this->correspondents[0]->getId(), $this->correspondents[1]->getId()]; $classes = [get_class($this->correspondents[0]), get_class($this->correspondents[1])]; - if(!$dontReverse && $ids[1] === $user->getId()) { + if (!$dontReverse && $ids[1] === $user->getId()) { $ids = array_reverse($ids); $classes = array_reverse($classes); } - + $message->setSender_Id($ids[0]); $message->setRecipient_Id($ids[1]); $message->setSender_Type($classes[0]); @@ -163,15 +205,32 @@ function sendMessage(Message $message, bool $dontReverse = false) $message->setCreated(time()); $message->setUnread(1); $message->save(); - - DatabaseConnection::i()->getConnection()->query("UPDATE messages SET unread = 0 WHERE sender_id = ".$this->correspondents[1]->getId()); - + + DatabaseConnection::i()->getConnection()->query("UPDATE messages SET unread = 0 WHERE sender_id = " . $this->correspondents[1]->getId()); + # да - if($ids[0] !== $ids[1]) { + if ($ids[0] !== $ids[1]) { $event = new NewMessageEvent($message); (SignalManager::i())->triggerEvent($event, $ids[1]); } - + return $message; } + + /** + * Send typing event. + * + * @returns true|false + */ + public function sendTypingEvent() + { + $ids = [$this->correspondents[0]->getId(), $this->correspondents[1]->getId()]; + + if ($ids[0] !== $ids[1]) { + $event = new TypingEvent($ids[0]); + (SignalManager::i())->triggerEvent($event, $ids[1]); + } + + return true; + } } diff --git a/Web/Models/Entities/Document.php b/Web/Models/Entities/Document.php new file mode 100644 index 000000000..dd51a3f84 --- /dev/null +++ b/Web/Models/Entities/Document.php @@ -0,0 +1,449 @@ +getBaseDir() . substr($hash, 0, 2); + if (!is_dir($dir)) { + mkdir($dir); + } + + return "$dir/$hash." . $this->getFileExtension(); + } + + public function getURL(): string + { + $hash = $this->getRecord()->hash; + $filetype = $this->getFileExtension(); + + switch (OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { + default: + case "default": + case "basic": + return "http://" . $_SERVER['HTTP_HOST'] . "/blob_" . substr($hash, 0, 2) . "/$hash.$filetype"; + break; + case "accelerated": + return "http://" . $_SERVER['HTTP_HOST'] . "/openvk-datastore/$hash.$filetype"; + break; + 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.$filetype" + ); + break; + } + } + + protected function saveFile(string $filename, string $hash): bool + { + move_uploaded_file($filename, $this->pathFromHash($hash)); + return true; + } + + protected function makePreview(string $tmp_name, string $filename, int $owner): bool + { + $preview_photo = new Photo(); + $preview_photo->setOwner($owner); + $preview_photo->setDescription("internal use"); + $preview_photo->setCreated(time()); + $preview_photo->setSystem(1); + $preview_photo->setFile([ + "tmp_name" => $tmp_name, + "error" => 0, + ]); + $preview_photo->save(); + $this->stateChanges("preview", "photo_" . $preview_photo->getId()); + + return true; + } + + private function updateHash(string $hash): bool + { + $this->stateChanges("hash", $hash); + + return true; + } + + public function setFile(array $file): void + { + if ($file["error"] !== UPLOAD_ERR_OK) { + throw new ISE("File uploaded is corrupted"); + } + + $original_name = $file["name"]; + $file_format = end(explode(".", $original_name)); + $file_size = $file["size"]; + $type = Document::detectTypeByFormat($file_format); + + if (!$file_format) { + throw new \TypeError("No file format"); + } + + if (!in_array(mb_strtolower($file_format), OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["allowedFormats"])) { + throw new \TypeError("Forbidden file format"); + } + + if ($file_size < 1 || $file_size > (OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["maxSize"] * 1024 * 1024)) { + throw new \ValueError("Invalid filesize"); + } + + $hash = hash_file("whirlpool", $file["tmp_name"]); + $this->stateChanges("original_name", ovk_proc_strtr($original_name, 255)); + $this->tmp_format = mb_strtolower($file_format); + $this->stateChanges("format", mb_strtolower($file_format)); + $this->stateChanges("filesize", $file_size); + $this->stateChanges("hash", $hash); + $this->stateChanges("access_key", bin2hex(random_bytes(9))); + $this->stateChanges("type", $type); + + if (in_array($type, [3, 4])) { + $this->makePreview($file["tmp_name"], $original_name, $file["preview_owner"]); + } + + $this->saveFile($file["tmp_name"], $hash); + } + + public function hasPreview(): bool + { + return $this->getRecord()->preview != null; + } + + public function isOwnerHidden(): bool + { + return (bool) $this->getRecord()->owner_hidden; + } + + public function isCopy(): bool + { + return $this->getRecord()->copy_of != null; + } + + public function isLicensed(): bool + { + return false; + } + + public function isUnsafe(): bool + { + return false; + } + + public function isAnonymous(): bool + { + return false; + } + + public function isPrivate(): bool + { + return $this->getFolder() == Document::VKAPI_FOLDER_PRIVATE; + } + + public function isImage(): bool + { + return in_array($this->getVKAPIType(), [3, 4]); + } + + public function isBook(): bool + { + return in_array($this->getFileExtension(), ["pdf"]); + } + + public function isAudio(): bool + { + return in_array($this->getVKAPIType(), [Document::VKAPI_TYPE_AUDIO]); + } + + public function isGif(): bool + { + return $this->getVKAPIType() == 3; + } + + public function isCopiedBy($user = null): bool + { + if (!$user) { + return false; + } + + if ($user->getRealId() === $this->getOwnerID()) { + return true; + } + + return DatabaseConnection::i()->getContext()->table("documents")->where([ + "owner" => $user->getRealId(), + "copy_of" => $this->getId(), + "deleted" => 0, + ])->count('*') > 0; + } + + public function copy(User $user): Document + { + $item = DatabaseConnection::i()->getContext()->table("documents")->where([ + "owner" => $user->getId(), + "copy_of" => $this->getId(), + ]); + if ($item->count() > 0) { + $older = new Document($item->fetch()); + } + + $this_document_array = $this->getRecord()->toArray(); + + $new_document = new Document(); + $new_document->setOwner($user->getId()); + $new_document->updateHash($this_document_array["hash"]); + $new_document->setOwner_hidden(1); + $new_document->setCopy_of($this->getId()); + $new_document->setName($this->getId()); + $new_document->setOriginal_name($this->getOriginalName()); + $new_document->setAccess_key(bin2hex(random_bytes(9))); + $new_document->setFormat($this_document_array["format"]); + $new_document->setType($this->getVKAPIType()); + $new_document->setFolder_id(0); + $new_document->setPreview($this_document_array["preview"]); + $new_document->setTags($this_document_array["tags"]); + $new_document->setFilesize($this_document_array["filesize"]); + + $new_document->save(); + + return $new_document; + } + + public function setTags(?string $tags): bool + { + if (is_null($tags)) { + $this->stateChanges("tags", null); + return true; + } + + $parsed = explode(",", $tags); + if (sizeof($parsed) < 1 || $parsed[0] == "") { + $this->stateChanges("tags", null); + return true; + } + + $result = ""; + foreach ($parsed as $tag) { + $result .= trim($tag) . ($tag != end($parsed) ? "," : ''); + } + + $this->stateChanges("tags", ovk_proc_strtr($result, 500)); + return true; + } + + public function getOwner(bool $real = false): RowModel + { + $oid = (int) $this->getRecord()->owner; + if ($oid > 0) { + return (new Users())->get($oid); + } else { + return (new Clubs())->get($oid * -1); + } + } + + public function getFileExtension(): string + { + if ($this->tmp_format) { + return $this->tmp_format; + } + + return $this->getRecord()->format; + } + + public function getPrettyId(): string + { + return $this->getVirtualId() . "_" . $this->getId(); + } + + public function getPrettiestId(): string + { + return $this->getVirtualId() . "_" . $this->getId() . "_" . $this->getAccessKey(); + } + + public function getOriginal(): Document + { + return $this->getRecord()->copy_of; + } + + public function getName(): string + { + return $this->getRecord()->name; + } + + public function getOriginalName(): string + { + return $this->getRecord()->original_name; + } + + public function getVKAPIType(): int + { + return $this->getRecord()->type; + } + + public function getFolder(): int + { + return $this->getRecord()->folder_id; + } + + public function getTags(): array + { + $tags = $this->getRecord()->tags; + if (!$tags) { + return []; + } + + return explode(",", $tags ?? ""); + } + + public function getFilesize(): int + { + return $this->getRecord()->filesize; + } + + public function getPreview(): ?RowModel + { + $preview_array = $this->getRecord()->preview; + $preview = explode(",", $this->getRecord()->preview)[0]; + $model = null; + $exploded = explode("_", $preview); + + switch ($exploded[0]) { + case "photo": + $model = (new Photos())->get((int) $exploded[1]); + break; + } + + return $model; + } + + public function getOwnerID(): int + { + return $this->getRecord()->owner; + } + + public function toApiPreview(): ?object + { + $preview = $this->getPreview(); + if ($preview instanceof Photo) { + return (object) [ + "photo" => [ + "sizes" => array_values($preview->getVkApiSizes()), + ], + ]; + } else { + return null; + } + } + + public function canBeModifiedBy(User $user = null): bool + { + if (!$user) { + return false; + } + + if ($this->getOwnerID() < 0) { + return (new Clubs())->get(abs($this->getOwnerID()))->canBeModifiedBy($user); + } + + return $this->getOwnerID() === $user->getId(); + } + + public function toVkApiStruct(?User $user = null, bool $return_tags = false): object + { + $res = new \stdClass(); + $res->id = $this->getId(); + if ($this->isOwnerHidden() && $user !== null && $this->getOwnerID() == $user->getId()) { + $res->owner_id = $this->getOwnerID(); + } elseif (!$this->isOwnerHidden()) { + $res->owner_id = $this->getOwnerID(); + } else { + $res->owner_id = 0; + } + $res->title = $this->getName(); + $res->size = $this->getFilesize(); + $res->ext = $this->getFileExtension(); + $res->url = $this->getURL(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->type = $this->getVKAPIType(); + $res->is_hidden = (int) $this->isOwnerHidden(); + $res->is_licensed = (int) $this->isLicensed(); + $res->is_unsafe = (int) $this->isUnsafe(); + $res->folder_id = (int) $this->getFolder(); + $res->access_key = $this->getAccessKey(); + $res->private_url = ""; + if ($user) { + $res->can_manage = $this->canBeModifiedBy($user); + } + + if ($this->hasPreview()) { + $res->preview = $this->toApiPreview(); + } + + if ($return_tags) { + $res->tags = $this->getTags(); + } + + return $res; + } + + public function delete(bool $softly = true, bool $all_copies = false): void + { + if ($all_copies) { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("documents")->where("copy_of", $this->getId())->delete(); + } + parent::delete($softly); + } + + public static function detectTypeByFormat(string $format) + { + switch (mb_strtolower($format)) { + case "txt": case "docx": case "doc": case "odt": case "pptx": case "ppt": case "xlsx": case "xls": case "md": + return 1; + case "zip": case "rar": case "7z": + return 2; + case "gif": case "apng": + return 3; + case "jpg": case "jpeg": case "png": case "psd": case "ps": case "webp": + return 4; + case "mp3": + return 5; + case "mp4": case "avi": + return 6; + case "pdf": case "djvu": case "epub": case "fb2": + return 7; + default: + return 8; + } + } +} diff --git a/Web/Models/Entities/EmailChangeVerification.php b/Web/Models/Entities/EmailChangeVerification.php index e9b92db1f..6e283efc6 100644 --- a/Web/Models/Entities/EmailChangeVerification.php +++ b/Web/Models/Entities/EmailChangeVerification.php @@ -1,5 +1,9 @@ -getRecord()->new_email; } diff --git a/Web/Models/Entities/EmailVerification.php b/Web/Models/Entities/EmailVerification.php index cfd057f9e..e3b690d0b 100755 --- a/Web/Models/Entities/EmailVerification.php +++ b/Web/Models/Entities/EmailVerification.php @@ -1,10 +1,14 @@ -getRecord()->internal_name; } - - function getPrice(): int + + public function getPrice(): int { return $this->getRecord()->price; } - - function getUsages(): int + + public function getUsages(): int { return $this->getRecord()->usages; } - - function getUsagesBy(User $user, ?int $since = NULL): int + + public function getUsagesBy(User $user, ?int $since = null): int { $sent = $this->getRecord() ->related("gift_user_relations.gift") ->where("sender", $user->getId()) ->where("sent >= ?", $since ?? $this->getRecord()->limit_period ?? 0); - + return sizeof($sent); } - - function getUsagesLeft(User $user): float + + public function getUsagesLeft(User $user): float { - if($this->getLimit() === INF) + if ($this->getLimit() === INF) { return INF; - + } + return max(0, $this->getLimit() - $this->getUsagesBy($user)); } - - function getImage(int $type = 0): /* ?binary */ string + + public function getImage(int $type = 0): /* ?binary */ string { - switch($type) { + switch ($type) { default: case static::IMAGE_BINARY: return $this->getRecord()->image ?? ""; @@ -66,99 +71,99 @@ function getImage(int $type = 0): /* ?binary */ string break; } } - - function getLimit(): float + + public function getLimit(): float { $limit = $this->getRecord()->limit; - + return !$limit ? INF : (float) $limit; } - - function getLimitResetTime(): ?DateTime + + public function getLimitResetTime(): ?DateTime { - return is_null($t = $this->getRecord()->limit_period) ? NULL : new DateTime($t); + return is_null($t = $this->getRecord()->limit_period) ? null : new DateTime($t); } - - function getUpdateDate(): DateTime + + public function getUpdateDate(): DateTime { return new DateTime($this->getRecord()->updated); } - - function canUse(User $user): bool + + public function canUse(User $user): bool { return $this->getUsagesLeft($user) > 0; } - - function isFree(): bool + + public function isFree(): bool { return $this->getPrice() === 0; } - - function used(): void + + public function used(): void { $this->stateChanges("usages", $this->getUsages() + 1); $this->save(); } - - function setName(string $name): void + + public function setName(string $name): void { $this->stateChanges("internal_name", $name); } - - function setImage(string $file): bool + + public function setImage(string $file): bool { - $imgBlob; try { $image = Image::fromFile($file); $image->resize(512, 512, Image::SHRINK_ONLY); - + $imgBlob = $image->toString(Image::PNG); - } catch(ImageException $ex) { + } catch (ImageException $ex) { return false; } - - if(strlen($imgBlob) > (2**24 - 1)) { + + if (strlen($imgBlob) > (2 ** 24 - 1)) { return false; } else { $this->stateChanges("updated", time()); $this->stateChanges("image", $imgBlob); } - + return true; } - - function setLimit(?float $limit = NULL, int $periodBehaviour = 0): void + + public function setLimit(?float $limit = null, int $periodBehaviour = 0): void { $limit ??= $this->getLimit(); - $limit = $limit === INF ? NULL : (int) $limit; + $limit = $limit === INF ? null : (int) $limit; $this->stateChanges("limit", $limit); - - if(!$limit) { - $this->stateChanges("limit_period", NULL); + + if (!$limit) { + $this->stateChanges("limit_period", null); return; } - - switch($periodBehaviour) { + + switch ($periodBehaviour) { default: case static::PERIOD_IGNORE: break; - + case static::PERIOD_SET: $this->stateChanges("limit_period", time()); break; - + case static::PERIOD_SET_IF_NONE: - if(is_null($this->getRecord()) || is_null($this->getRecord()->limit_period)) + if (is_null($this->getRecord()) || is_null($this->getRecord()->limit_period)) { $this->stateChanges("limit_period", time()); - + } + break; } } - - function delete(bool $softly = true): void + + public function delete(bool $softly = true): void { $this->getRecord()->related("gift_relations.gift")->delete(); - + parent::delete($softly); } } diff --git a/Web/Models/Entities/GiftCategory.php b/Web/Models/Entities/GiftCategory.php index 456870575..6154cafa0 100644 --- a/Web/Models/Entities/GiftCategory.php +++ b/Web/Models/Entities/GiftCategory.php @@ -1,5 +1,9 @@ -getRecord() ->related("gift_categories_locales.category") ->where("language", $language); } - + private function createLocalizationIfNotExists(string $language): void { - if(!is_null($this->getLocalization($language)->fetch())) + if (!is_null($this->getLocalization($language)->fetch())) { return; - + } + DB::i()->getContext()->table("gift_categories_locales")->insert([ "category" => $this->getId(), "language" => $language, @@ -28,8 +33,8 @@ private function createLocalizationIfNotExists(string $language): void "description" => "Sample Text", ]); } - - function getSlug(): string + + public function getSlug(): string { return str_replace("ʹ", "-", Transliterator::createFromRules( ":: Any-Latin;" @@ -41,116 +46,123 @@ function getSlug(): string . "[:Separator:] > '-'" )->transliterate($this->getName())); } - - function getThumbnailURL(): string + + public function getThumbnailURL(): string { $primeGift = iterator_to_array($this->getGifts(1, 1))[0]; $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - if(!$primeGift) + if (!$primeGift) { return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; - + } + return $primeGift->getImage(Gift::IMAGE_URL); } - - function getName(string $language = "_", bool $returnNull = false): ?string + + public function getName(string $language = "_", bool $returnNull = false): ?string { $loc = $this->getLocalization($language)->fetch(); - if(!$loc) { - if($returnNull) - return NULL; - + if (!$loc) { + if ($returnNull) { + return null; + } + return $language === "_" ? "Unlocalized" : $this->getName(); } - + return $loc->name; } - - function getDescription(string $language = "_", bool $returnNull = false): ?string + + public function getDescription(string $language = "_", bool $returnNull = false): ?string { $loc = $this->getLocalization($language)->fetch(); - if(!$loc) { - if($returnNull) - return NULL; - + if (!$loc) { + if ($returnNull) { + return null; + } + return $language === "_" ? "Unlocalized" : $this->getDescription(); } - + return $loc->description; } - - function getGifts(int $page = -1, ?int $perPage = NULL, &$count = nullptr): \Traversable + + public function getGifts(int $page = -1, ?int $perPage = null, &$count = nullptr): \Traversable { $gifts = $this->getRecord()->related("gift_relations.category"); - if($page !== -1) { + if ($page !== -1) { $count = $gifts->count(); $gifts = $gifts->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); } - - foreach($gifts as $rel) - yield (new Gifts)->get($rel->gift); + + foreach ($gifts as $rel) { + yield (new Gifts())->get($rel->gift); + } } - - function isMagical(): bool + + public function isMagical(): bool { return !is_null($this->getRecord()->autoquery); } - - function hasGift(Gift $gift): bool + + public function hasGift(Gift $gift): bool { $rels = $this->getRecord()->related("gift_relations.category"); - - return $rels->where("gift", $gift->getId())->count() > 0; + + return $rels->where("gift", $gift->getId())->count('*') > 0; } - - function addGift(Gift $gift): void + + public function addGift(Gift $gift): void { - if($this->hasGift($gift)) + if ($this->hasGift($gift)) { return; - + } + DB::i()->getContext()->table("gift_relations")->insert([ "category" => $this->getId(), "gift" => $gift->getId(), ]); } - - function removeGift(Gift $gift): void + + public function removeGift(Gift $gift): void { - if(!$this->hasGift($gift)) + if (!$this->hasGift($gift)) { return; - + } + DB::i()->getContext()->table("gift_relations")->where([ "category" => $this->getId(), "gift" => $gift->getId(), ])->delete(); } - - function setName(string $language, string $name): void + + public function setName(string $language, string $name): void { $this->createLocalizationIfNotExists($language); $this->getLocalization($language)->update([ "name" => $name, ]); } - - function setDescription(string $language, string $description): void + + public function setDescription(string $language, string $description): void { $this->createLocalizationIfNotExists($language); $this->getLocalization($language)->update([ "description" => $description, ]); } - - function setAutoQuery(?array $query = NULL): void + + public function setAutoQuery(?array $query = null): void { - if(is_null($query)) { - $this->stateChanges("autoquery", NULL); + if (is_null($query)) { + $this->stateChanges("autoquery", null); return; } - + $allowedColumns = ["price", "usages"]; - if(array_diff_key($query, array_flip($allowedColumns))) + if (array_diff_key($query, array_flip($allowedColumns))) { throw new \LogicException("Invalid query"); - + } + $this->stateChanges("autoquery", serialize($query)); } } diff --git a/Web/Models/Entities/IP.php b/Web/Models/Entities/IP.php index 0d9b8fd0f..4f3b65953 100644 --- a/Web/Models/Entities/IP.php +++ b/Web/Models/Entities/IP.php @@ -1,45 +1,49 @@ -getRecord()->ip); } - - function getDiscoveryDate(): DateTime + + public function getDiscoveryDate(): DateTime { return new DateTime($this->getRecord()->first_seen); } - - function isBanned(): bool + + public function isBanned(): bool { return (bool) $this->getRecord()->banned; } - - function ban(): void + + public function ban(): void { $this->stateChanges("banned", true); $this->save(); } - - function pardon(): void + + public function pardon(): void { $this->stateChanges("banned", false); $this->save(); } - - function clear(): void + + public function clear(): void { $this->stateChanges("rate_limit_counter_start", 0); $this->stateChanges("rate_limit_counter", 0); @@ -47,45 +51,45 @@ function clear(): void $this->stateChanges("rate_limit_violation_counter", 0); $this->save(); } - - function rateLimit(int $actionComplexity = 1): int + + public function rateLimit(int $actionComplexity = 1): int { $counterSessionStart = $this->getRecord()->rate_limit_counter_start; $vCounterSessionStart = $this->getRecord()->rate_limit_violation_counter_start; - + $aCounter = $this->getRecord()->rate_limit_counter; $vCounter = $this->getRecord()->rate_limit_violation_counter; - + $config = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]; - + try { - if((time() - $config->time) > $counterSessionStart) { + if ((time() - $config->time) > $counterSessionStart) { $counterSessionStart = time(); $aCounter = $actionComplexity; - + return static::RL_RESET; } - - if(($aCounter + $actionComplexity) <= $config->actions) { + + if (($aCounter + $actionComplexity) <= $config->actions) { $aCounter += $actionComplexity; - + return static::RL_CANEXEC; } - - if((time() - $config->maxViolationsAge) > $vCounterSessionStart) { + + if ((time() - $config->maxViolationsAge) > $vCounterSessionStart) { $vCounterSessionStart = time(); $vCounter = 1; - + return static::RL_VIOLATION; } - + $vCounter += 1; - if($vCounter >= $config->maxViolations) { + if ($vCounter >= $config->maxViolations) { $this->stateChanges("banned", true); - + return static::RL_BANNED; } - + return static::RL_VIOLATION; } finally { $this->stateChanges("rate_limit_counter_start", $counterSessionStart); @@ -95,21 +99,23 @@ function rateLimit(int $actionComplexity = 1): int $this->save(false); } } - - function setIp(string $ip): void + + public function setIp(string $ip): void { $ip = inet_pton($ip); - if(!$ip) + if (!$ip) { throw new \UnexpectedValueException("Malformed IP address"); - + } + $this->stateChanges("ip", $ip); } - - function save(?bool $log = false): void + + public function save(?bool $log = false): void { - if(is_null($this->getRecord())) + if (is_null($this->getRecord())) { $this->stateChanges("first_seen", time()); - + } + parent::save($log); } } diff --git a/Web/Models/Entities/Manager.php b/Web/Models/Entities/Manager.php index 0876e01ec..279c88a3e 100644 --- a/Web/Models/Entities/Manager.php +++ b/Web/Models/Entities/Manager.php @@ -1,5 +1,9 @@ -getRecord()->id; } - - function getUserId(): int + + public function getUserId(): int { return $this->getRecord()->user; } - function getUser(): ?User + public function getUser(): ?User { - return (new Users)->get($this->getRecord()->user); + return (new Users())->get($this->getRecord()->user); } - function getClubId(): int + public function getClubId(): int { return $this->getRecord()->club; } - function getClub(): ?Club + public function getClub(): ?Club { - return (new Clubs)->get($this->getRecord()->club); + return (new Clubs())->get($this->getRecord()->club); } - function getComment(): string + public function getComment(): string { return is_null($this->getRecord()->comment) ? "" : $this->getRecord()->comment; } - function isHidden(): bool + public function isHidden(): bool { return (bool) $this->getRecord()->hidden; } - function isClubPinned(): bool + public function isClubPinned(): bool { return (bool) $this->getRecord()->club_pinned; } - - use Traits\TSubscribable; } diff --git a/Web/Models/Entities/Media.php b/Web/Models/Entities/Media.php index 648d3564e..c688bb001 100644 --- a/Web/Models/Entities/Media.php +++ b/Web/Models/Entities/Media.php @@ -1,96 +1,108 @@ -changes["hash"])) + if (isset($this->changes["hash"])) { unlink($this->pathFromHash($this->changes["hash"])); + } } - + protected function getBaseDir(): string { $uploadSettings = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]; - if($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn") + if ($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn") { return $uploadSettings["server"]["directory"]; - else + } else { return OPENVK_ROOT . "/storage/"; + } } protected function checkIfFileIsProcessed(): bool { throw new \LogicException("checkIfFileIsProcessed is not implemented"); } - + abstract protected function saveFile(string $filename, string $hash): bool; - + protected function pathFromHash(string $hash): string { $dir = $this->getBaseDir() . substr($hash, 0, 2); - if(!is_dir($dir)) + if (!is_dir($dir)) { mkdir($dir); - + } + return "$dir/$hash." . $this->fileExtension; } - - function getFileName(): string + + public function getFileName(): string { return $this->pathFromHash($this->getRecord()->hash); } - - function getURL(): string + + public function getURL(): string { - if(!is_null($this->processingPlaceholder)) - if(!$this->isProcessed()) + 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"]) { + + switch (OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { default: case "default": case "basic": - return "http://" . $_SERVER['HTTP_HOST'] . "/blob_" . substr($hash, 0, 2) . "/$hash.$this->fileExtension"; - break; + return ovk_scheme(true) . $_SERVER['HTTP_HOST'] . "/blob_" . substr($hash, 0, 2) . "/$hash.$this->fileExtension"; + break; case "accelerated": - return "http://" . $_SERVER['HTTP_HOST'] . "/openvk-datastore/$hash.$this->fileExtension"; - break; + return ovk_scheme(true) . $_SERVER['HTTP_HOST'] . "/openvk-datastore/$hash.$this->fileExtension"; + break; case "server": $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; return ( - $settings->protocol ?? ovk_scheme() . + ($settings->protocol ?? ovk_scheme()) . "://" . $settings->host . $settings->path . substr($hash, 0, 2) . "/$hash.$this->fileExtension" ); - break; + break; } } - - function getDescription(): ?string + + public function getDescription(): ?string { return $this->getRecord()->description; } protected function isProcessed(): bool { - if(is_null($this->processingPlaceholder)) + if (is_null($this->processingPlaceholder)) { return true; + } - if($this->getRecord()->processed) + if ($this->getRecord()->processed) { return true; + } $timeDiff = time() - $this->getRecord()->last_checked; - if($timeDiff < $this->processingTime) + if ($timeDiff < $this->processingTime) { return false; + } $res = $this->checkIfFileIsProcessed(); $this->stateChanges("last_checked", time()); @@ -99,31 +111,32 @@ protected function isProcessed(): bool return $res; } - - function isDeleted(): bool + + public function isDeleted(): bool { return (bool) $this->getRecord()->deleted; } - - function setHash(string $hash): void + + public function setHash(string $hash): void { throw new ISE("Setting file hash manually is forbidden"); } - - function setFile(array $file): void + + public function setFile(array $file): void { - if($file["error"] !== UPLOAD_ERR_OK) + if ($file["error"] !== UPLOAD_ERR_OK) { throw new ISE("File uploaded is corrupted"); - + } + $hash = hash_file("whirlpool", $file["tmp_name"]); $this->saveFile($file["tmp_name"], $hash); - + $this->stateChanges("hash", $hash); } - function save(?bool $log = false): void + public function save(?bool $log = false): void { - if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { + if (!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { $this->stateChanges("processed", 0); $this->stateChanges("last_checked", time()); } @@ -131,20 +144,22 @@ function save(?bool $log = false): void parent::save($log); } - function delete(bool $softly = true): void + public function delete(bool $softly = true): void { $deleteQuirk = ovkGetQuirk("blobs.erase-upon-deletion"); - if($deleteQuirk === 2 || ($deleteQuirk === 1 && !$softly)) + if ($deleteQuirk === 2 || ($deleteQuirk === 1 && !$softly)) { @unlink($this->getFileName()); - + } + parent::delete($softly); } - - function undelete(): void + + public function undelete(): void { - if(ovkGetQuirk("blobs.erase-upon-deletion") === 2) + if (ovkGetQuirk("blobs.erase-upon-deletion") === 2) { throw new \LogicException("Can't undelete model which is tied to blob, because of config constraint (quriks.yml:blobs.erase-upon-deletion)"); - + } + parent::undelete(); } } diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c1..1f49766ac 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -1,5 +1,9 @@ -relations = DatabaseConnection::i()->getContext()->table($this->relTableName); } - + private function entitySuitable(RowModel $entity): bool { - if(($class = get_class($entity)) !== $this->entityClassName) + if (($class = get_class($entity)) !== $this->entityClassName) { throw new \UnexpectedValueException("This MediaCollection can only store '$this->entityClassName' (not '$class')."); - + } + return true; } - - function getOwner(): RowModel + + public function getOwner(): RowModel { $oid = $this->getRecord()->owner; - if($oid > 0) - return (new Users)->get($oid); - else - return (new Clubs)->get($oid * -1); + if ($oid > 0) { + return (new Users())->get($oid); + } else { + return (new Clubs())->get($oid * -1); + } + } + + public function getOwnerId(): int + { + return (int) $this->getRecord()->owner; } - - function getPrettyId(): string + + public function getPrettyId(): string { return $this->getRecord()->owner . "_" . $this->getRecord()->id; } - - function getName(): string + + public function getName(): string { $special = $this->getRecord()->special_type; - if($special === 0) + if ($special === 0) { return $this->getRecord()->name; - + } + $sName = $this->specialNames[$special]; - if(!$sName) + if (!$sName) { return $this->getRecord()->name; - - if($sName[0] === "_") + } + + if ($sName[0] === "_") { $sName = tr(substr($sName, 1)); - + } + return $sName; } - - function getDescription(): ?string + + public function getDescription(): ?string { return $this->getRecord()->description; } - - abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + abstract public function getCoverURL(): ?string; + + public function fetchClassic(int $offset = 0, ?int $limit = null): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); - foreach($related as $rel) { + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + + foreach ($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); - if(!$media) + if (!$media) { continue; - + } + yield new $this->entityClassName($media); } } - - function size(): int + + public function fetch(int $page = 1, ?int $perPage = null): \Traversable { - return sizeof($this->getRecord()->related("$this->relTableName.collection")); + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); } - - function getCreationTime(): DateTime + + public function size(): int + { + return $this->getRecord()->related("$this->relTableName.collection")->count("*"); + } + + public function getCreationTime(): DateTime { return new DateTime($this->getRecord()->created); } - - function getPublicationTime(): DateTime + + public function getPublicationTime(): DateTime { return $this->getCreationTime(); } - - function getEditTime(): ?DateTime + + public function getEditTime(): ?DateTime { $edited = $this->getRecord()->edited; - if(is_null($edited)) return NULL; - + if (is_null($edited)) { + return null; + } + return new DateTime($edited); } - - function isCreatedBySystem(): bool + + public function isCreatedBySystem(): bool { return $this->getRecord()->special_type !== 0; } - - function add(RowModel $entity): bool + + public function add(RowModel $entity): bool { $this->entitySuitable($entity); - - if(!$this->allowDuplicates) - if($this->has($entity)) + + if (!$this->allowDuplicates) { + if ($this->has($entity)) { return false; - + } + } + + if (self::MAX_ITEMS != INF) { + if (sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) { + throw new \OutOfBoundsException("Collection is full"); + } + } + $this->relations->insert([ "collection" => $this->getId(), "media" => $entity->getId(), ]); - + return true; } - - function remove(RowModel $entity): void + + public function remove(RowModel $entity): bool { $this->entitySuitable($entity); - - $this->relations->where([ + + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } - - function has(RowModel $entity): bool + + public function has(RowModel $entity): bool { $this->entitySuitable($entity); - + $rel = $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), ])->fetch(); - + return !is_null($rel); } - - use Traits\TOwnable; + + public function save(?bool $log = false): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if (self::MAX_COUNT != INF) { + if (isset($this->changes["owner"])) { + if (sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) { + throw new \OutOfBoundsException("Maximum amount of collections"); + } + } + } + + if (is_null($this->getRecord())) { + if (!isset($this->changes["created"])) { + $this->stateChanges("created", time()); + } else { + $this->stateChanges("edited", time()); + } + } + + parent::save($log); + } + + public function delete(bool $softly = true): void + { + if (!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } } diff --git a/Web/Models/Entities/Message.php b/Web/Models/Entities/Message.php index 97f3cf69b..052a4d409 100644 --- a/Web/Models/Entities/Message.php +++ b/Web/Models/Entities/Message.php @@ -1,5 +1,9 @@ -getRecord()->id; + } /** * Get origin of the message. - * + * * Returns either user or club. - * + * * @returns User|Club */ - function getSender(): ?RowModel + public function getSender(): ?RowModel { - if($this->getRecord()->sender_type === 'openvk\Web\Models\Entities\User') - return (new Users)->get($this->getRecord()->sender_id); - else if($this->getRecord()->sender_type === 'openvk\Web\Models\Entities\Club') - return (new Clubs)->get($this->getRecord()->sender_id); + if ($this->getRecord()->sender_type === 'openvk\Web\Models\Entities\User') { + 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; + } } - + /** * Get the destination of the message. - * + * * Returns either user or club. - * + * * @returns User|Club */ - function getRecipient(): ?RowModel + public function getRecipient(): ?RowModel { - if($this->getRecord()->recipient_type === 'openvk\Web\Models\Entities\User') - return (new Users)->get($this->getRecord()->recipient_id); - else if($this->getRecord()->recipient_type === 'openvk\Web\Models\Entities\Club') - return (new Clubs)->get($this->getRecord()->recipient_id); + if ($this->getRecord()->recipient_type === 'openvk\Web\Models\Entities\User') { + 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; + } } - - function getUnreadState(): int + + public function getUnreadState(): int { trigger_error("TODO: use isUnread", E_USER_DEPRECATED); - + return (int) $this->isUnread(); } - + /** * Get date of initial publication. - * + * * @returns DateTime */ - function getSendTime(): DateTime + public function getSendTime(): DateTime { return new DateTime($this->getRecord()->created); } - function getSendTimeHumanized(): string + public 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"); + if ($dateTime->format("%d.%m.%y") == ovk_strftime_safe("%d.%m.%y", time())) { + return $dateTime->format("%T"); } else { return $dateTime->format("%d.%m.%y"); } } - + /** * Get date of last edit, if any edits were made, otherwise null. - * + * * @returns DateTime|null */ - function getEditTime(): ?DateTime + public function getEditTime(): ?DateTime { $edited = $this->getRecord()->edited; - if(is_null($edited)) return NULL; - + if (is_null($edited)) { + return null; + } + return new DateTime($edited); } - + /** * Is this message an ad? - * + * * Messages can never be ads. - * + * * @returns false */ - function isAd(): bool + public function isAd(): bool { return false; } - - function isUnread(): bool + + public function isUnread(): bool { return (bool) $this->getRecord()->unread; } - + /** * Simplify to array - * + * * @returns array */ - function simplify(): array + public function simplify(): array { $author = $this->getSender(); - + $attachments = []; - foreach($this->getChildren() as $attachment) { - if($attachment instanceof Photo) { + foreach ($this->getChildren() as $attachment) { + if ($attachment instanceof Photo) { $attachments[] = [ "type" => "photo", "link" => "/photo" . $attachment->getPrettyId(), @@ -123,28 +146,29 @@ function simplify(): array ], ]; } else { - throw new \Exception("Unknown attachment type: " . get_class($attachment)); + $attachments[] = [ + "type" => "unknown", + ]; + + # throw new \Exception("Unknown attachment type: " . get_class($attachment)); } } - + return [ "uuid" => $this->getId(), "sender" => [ "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(), - "edited" => is_null($this->getEditTime()) ? NULL : (string) $this->getEditTime(), + "edited" => is_null($this->getEditTime()) ? null : (string) $this->getEditTime(), ], "text" => $this->getText(), "read" => !$this->isUnread(), "attachments" => $attachments, ]; } - - use Traits\TRichText; - use Traits\TAttachmentHost; } diff --git a/Web/Models/Entities/NoSpamLog.php b/Web/Models/Entities/NoSpamLog.php index 48d723c90..a44452cea 100644 --- a/Web/Models/Entities/NoSpamLog.php +++ b/Web/Models/Entities/NoSpamLog.php @@ -1,5 +1,9 @@ -getRecord()->id; } - function getUser(): ?User + public function getUser(): ?User { - return (new Users)->get($this->getRecord()->user); + return (new Users())->get($this->getRecord()->user); } - function getModel(): string + public function getModel(): string { return $this->getRecord()->model; } - function getRegex(): ?string + public function getRegex(): ?string { return $this->getRecord()->regex; } - function getRequest(): ?string + public function getRequest(): ?string { return $this->getRecord()->request; } - function getCount(): int + public function getCount(): int { return $this->getRecord()->count; } - function getTime(): DateTime + public function getTime(): DateTime { return new DateTime($this->getRecord()->time); } - function getItems(): ?array + public function getItems(): ?array { return explode(",", $this->getRecord()->items); } - function getTypeRaw(): int + public function getTypeRaw(): int { return $this->getRecord()->ban_type; } - function getType(): string + public function getType(): string { switch ($this->getTypeRaw()) { case 1: return "О"; @@ -64,7 +68,7 @@ function getType(): string } } - function isRollbacked(): bool + public function isRollbacked(): bool { return !is_null($this->getRecord()->rollback); } diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php index 83082bf30..b9829822d 100644 --- a/Web/Models/Entities/Note.php +++ b/Web/Models/Entities/Note.php @@ -1,13 +1,47 @@ -]*src\s*=\s*["\']([^"\']*)["\'][^>]*>/i', + function ($matches) { + $originalSrc = $matches[1]; + $src = $originalSrc; + + if (OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["disableHotlinking"] ?? true) { + if (!str_contains($src, "/image.php?url=")) { + $src = '/image.php?url=' . base64_encode($originalSrc); + } /*else { + $src = preg_replace_callback('/(.*)\/image\.php\?url=(.*)/i', function ($matches) { + return base64_decode($matches[2]); + }, $src); + }*/ + } + + return str_replace($originalSrc, $src, $matches[0]); + }, + $html + ); + + return $html; + } +} class Note extends Postable { protected $tableName = "notes"; - - protected function renderHTML(): string + + protected function renderHTML(?string $content = null): string { $config = HTMLPurifier_Config::createDefault(); $config->set("Attr.AllowedClasses", []); @@ -74,67 +108,76 @@ protected function renderHTML(): string $config->set("Attr.AllowedClasses", [ "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; + $config->set('Filter.Custom', [new SecurityFilter()]); + + $source = $content; + if (!$source) { + 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($source); } - - function getName(): string + + public function getName(): string { return $this->getRecord()->name; } - - function getPreview(int $length = 25): string + + public function getPreview(int $length = 25): string { return ovk_proc_strtr(strip_tags($this->getRecord()->source), $length); } - - function getText(): string + + public function getText(): string { - if(is_null($this->getRecord())) + if (is_null($this->getRecord())) { return $this->renderHTML(); - + } + $cached = $this->getRecord()->cached_content; - if(!$cached) { + if (!$cached) { $cached = $this->renderHTML(); $this->setCached_Content($cached); $this->save(); } - - return $cached; + + return $this->renderHTML($cached); } - function getSource(): string + public function getSource(): string { return $this->getRecord()->source; } - function toVkApiStruct(): object + public function canBeViewedBy(?User $user = null): bool + { + if ($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + return $this->getOwner()->getPrivacyPermission('notes.read', $user) && $this->getOwner()->canBeViewedBy($user); + } + + public function toVkApiStruct(): object { $res = (object) []; - $res->type = "note"; $res->id = $this->getVirtualId(); $res->owner_id = $this->getOwner()->getId(); $res->title = $this->getName(); $res->text = $this->getText(); $res->date = $this->getPublicationTime()->timestamp(); $res->comments = $this->getCommentsCount(); - $res->read_comments = $this->getCommentsCount(); - $res->view_url = "/note".$this->getOwner()->getId()."_".$this->getId(); - $res->privacy_view = 1; - $res->can_comment = 1; - $res->text_wiki = "r"; + $res->view_url = "/note" . $this->getOwner()->getId() . "_" . $this->getVirtualId(); return $res; } diff --git a/Web/Models/Entities/Notifications/ClubModeratorNotification.php b/Web/Models/Entities/Notifications/ClubModeratorNotification.php index cb89deea3..28563b692 100644 --- a/Web/Models/Entities/Notifications/ClubModeratorNotification.php +++ b/Web/Models/Entities/Notifications/ClubModeratorNotification.php @@ -1,12 +1,16 @@ -getText()), 400)); } diff --git a/Web/Models/Entities/Notifications/FriendRemovalNotification.php b/Web/Models/Entities/Notifications/FriendRemovalNotification.php index 17d68e0a3..d7cbcb267 100644 --- a/Web/Models/Entities/Notifications/FriendRemovalNotification.php +++ b/Web/Models/Entities/Notifications/FriendRemovalNotification.php @@ -1,12 +1,16 @@ - 1, + "items" => [(object) ["from_id" => $this->targetModel->getId()]], + ]; } } diff --git a/Web/Models/Entities/Notifications/MentionNotification.php b/Web/Models/Entities/Notifications/MentionNotification.php index 25680f57d..f01fb69a0 100644 --- a/Web/Models/Entities/Notifications/MentionNotification.php +++ b/Web/Models/Entities/Notifications/MentionNotification.php @@ -1,13 +1,17 @@ -recipient = $recipient; $this->originModel = $originModel; @@ -24,67 +28,70 @@ function __construct(User $recipient, $originModel, $targetModel, ?int $time = N $this->time = $time ?? time(); $this->data = $data; } - + private function encodeType(object $model): int { return (int) json_decode(file_get_contents(__DIR__ . "/../../../../data/modelCodes.json"), true)[get_class($model)]; } - - function reverseModelOrder(): bool + + public function reverseModelOrder(): bool { return false; } - - function getActionCode(): int + + public function getActionCode(): int { return $this->actionCode; } - - function setActionCode(int $code): void + + public function setActionCode(int $code): void { - $this->actionCode = $this->actionCode ?? $code; + $this->actionCode ??= $code; } - - function getTemplatePath(): string + + public function getTemplatePath(): string { return implode("_", [ "./../components/notifications/$this->actionCode/", $this->encodeType($this->originModel), $this->encodeType($this->targetModel), - ".xml" + ".latte", ]); } - - function getRecipient(): User + + public function getRecipient(): User { return $this->recipient; } - - function getModel(int $index): RowModel + + public function getModel(int $index): ?RowModel { - switch($index) { + switch ($index) { case 0: return $this->originModel; case 1: return $this->targetModel; + default: + return null; } } - - function getData(): string + + public function getData(): string { return $this->data; } - - function getDateTime(): DateTime + + public function getDateTime(): DateTime { return new DateTime($this->time); } - - function emit(): bool + + public function emit(): bool { - if(!($e = eventdb())) + if (!($e = eventdb())) { return false; - + } + $data = [ "recipient" => $this->recipient->getId(), "originModelType" => $this->encodeType($this->originModel), @@ -95,41 +102,170 @@ function emit(): bool "additionalPayload" => $this->data, "timestamp" => $this->time, ]; - + $edb = $e->getConnection(); - if($this->threshold !== -1) { + if ($this->threshold !== -1) { # Event is thersholded, check if there is similar event $query = <<<'QUERY' - SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?) -QUERY; + SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?) + QUERY; $result = $edb->query($query, ...array_merge(array_values($data), [ $this->threshold ])); - if($result->getRowCount() > 0) + if ($result->getRowCount() > 0) { return false; + } } - + $edb->query("INSERT INTO notifications VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?)", ...array_values($data)); - - $kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"]; - if($kafkaConf["enable"]) { - $kafkaConf = $kafkaConf["kafka"]; - $brokerConf = new Conf(); - $brokerConf->set("log_level", (string) LOG_DEBUG); - $brokerConf->set("debug", "all"); - - $producer = new Producer($brokerConf); - $producer->addBrokers($kafkaConf["addr"] . ":" . $kafkaConf["port"]); - - $descriptor = implode(",", [ - str_replace("\\", ".", get_class($this)), - $this->recipient->getId(), - base64_encode(serialize((object) $data)), + + + try { + $broker = NotificationBroker::i(); + $broker->push($this->recipient->getId(), [ + 'class' => str_replace("\\", ".", get_class($this)), + 'data' => $data, ]); - - $notifTopic = $producer->newTopic($kafkaConf["topic"]); - $notifTopic->produce(RD_KAFKA_PARTITION_UA, RD_KAFKA_MSG_F_BLOCK, $descriptor); - $producer->flush(100); + } catch (\Exception $e) { + error_log("NotificationBroker failure in emit(): " . $e->getMessage()); } - + return true; } + + public function getVkApiInfo() + { + $origin_m = $this->encodeType($this->originModel); + $target_m = $this->encodeType($this->targetModel); + + $info = [ + "type" => "", + "parent" => null, + "feedback" => null, + ]; + + switch ($this->getActionCode()) { + case 0: + $info["type"] = "like_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = $this->toFeedbackStruct(); + break; + case 1: + $info["type"] = "copy_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = null; + break; + case 2: + switch ($origin_m) { + case 19: + $info["type"] = "comment_video"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = null; # comment id is not saving at db + break; + case 13: + $info["type"] = "comment_photo"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = null; + break; + case 10: + $info["type"] = "comment_note"; + $info["parent"] = $this->getModel(0)->toVkApiStruct(); + $info["feedback"] = null; + break; + case 14: + $info["type"] = "comment_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = $this->getModel(1)->toVkApiStruct(); + break; + # unused (users don't have topics bruh) + case 21: + $info["type"] = "comment_topic"; + $info["parent"] = $this->getModel(0)->toVkApiStruct(0, 90); + break; + default: + $info["type"] = "comment_unknown"; + break; + } + + break; + case 3: + $info["type"] = "wall"; + $info["feedback"] = $this->getModel(0)->toNotifApiStruct(); + break; + case 4: + switch ($target_m) { + case 14: + $info["type"] = "mention"; + $info["feedback"] = $this->getModel(1)->toNotifApiStruct(); + break; + case 19: + $info["type"] = "mention_comment_video"; + $info["parent"] = $this->getModel(1)->toNotifApiStruct(); + break; + case 13: + $info["type"] = "mention_comment_photo"; + $info["parent"] = $this->getModel(1)->toNotifApiStruct(); + break; + # unstandart + case 10: + $info["type"] = "mention_comment_note"; + $info["parent"] = $this->getModel(1)->toVkApiStruct(); + break; + case 21: + $info["type"] = "mention_comments"; + break; + default: + $info["type"] = "mention_comment_unknown"; + break; + } + break; + case 5: + $info["type"] = "make_you_admin"; + $info["parent"] = $this->getModel(0)->toVkApiStruct($this->getModel(1)); + break; + # Нужно доделать после мержа #935 + case 6: + $info["type"] = "wall_publish"; + break; + # В вк не было такого уведомления, так что unstandart + case 7: + $info["type"] = "new_posts_in_club"; + break; + # В вк при передаче подарков приходит сообщение, а не уведомление, так что unstandart + case 9601: + $info["type"] = "sent_gift"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + break; + case 9602: + $info["type"] = "voices_transfer"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + break; + case 9603: + $info["type"] = "up_rating"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + $info["parent"]->count = $this->getData(); + break; + default: + $info["type"] = null; + break; + } + + return $info; + } + + public function toVkApiStruct() + { + $res = (object) []; + + $info = $this->getVkApiInfo(); + $res->type = $info["type"]; + $res->date = $this->getDateTime()->timestamp(); + $res->parent = $info["parent"]; + $res->feedback = $info["feedback"]; + $res->reply = null; # Ответы на комментарии не реализованы + return $res; + } + + public function toFeedbackStruct() + { + return (object) []; + } } diff --git a/Web/Models/Entities/Notifications/PostAcceptedNotification.php b/Web/Models/Entities/Notifications/PostAcceptedNotification.php new file mode 100644 index 000000000..2f456d8eb --- /dev/null +++ b/Web/Models/Entities/Notifications/PostAcceptedNotification.php @@ -0,0 +1,17 @@ +getText(), 10)); } diff --git a/Web/Models/Entities/PasswordReset.php b/Web/Models/Entities/PasswordReset.php index 372c63f81..6073f183f 100644 --- a/Web/Models/Entities/PasswordReset.php +++ b/Web/Models/Entities/PasswordReset.php @@ -1,5 +1,9 @@ -get($this->getRecord()->profile); + return (new Users())->get($this->getRecord()->profile); } - - function getKey(): string + + public function getKey(): string { return $this->getRecord()->key; } - - function getToken(): string + + public function getToken(): string { return $this->getKey(); } - - function getCreationTime(): DateTime + + public function getCreationTime(): DateTime { return new DateTime($this->getRecord()->timestamp); } - + /** * User can request password reset only if he does not have any "new" password resets. * Password reset becomes "old" after 5 minutes and one second. */ - function isNew(): bool + public function isNew(): bool { return $this->getRecord()->timestamp > (time() - (5 * MINUTE)); } - + /** * Token is valid only for 3 days. */ - function isStillValid(): bool + public function isStillValid(): bool { return $this->getRecord()->timestamp > (time() - (3 * DAY)); } - - function verify(string $token): bool + + public function verify(string $token): bool { try { return $this->isStillValid() ? sodium_memcmp($this->getKey(), $token) : false; - } catch(\SodiumException $ex) { + } catch (\SodiumException $ex) { return false; } } - - function save(?bool $log = false): void + + public function save(?bool $log = false): void { $this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("timestamp", time()); - + parent::save($log); } } diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index c1825b3a8..aee284b46 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -1,5 +1,9 @@ -getImageWidth() / $image->getImageHeight()) > ($px / $py)) { + if (($image->getImageWidth() / $image->getImageHeight()) > ($px / $py)) { $height = (int) ceil(($px * $image->getImageWidth()) / $py); $image->cropImage($image->getImageWidth(), $height, 0, 0); $res[0] = true; } } - - if(isset($size["maxSize"])) { + + if (isset($size["maxSize"])) { $maxSize = (int) $size["maxSize"]; $sizes = Image::calculateSize($image->getImageWidth(), $image->getImageHeight(), $maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); - } else if(isset($size["maxResolution"])) { + } elseif (isset($size["maxResolution"])) { $resolution = explode("x", (string) $size["maxResolution"]); $sizes = Image::calculateSize( - $image->getImageWidth(), $image->getImageHeight(), (int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT + $image->getImageWidth(), + $image->getImageHeight(), + (int) $resolution[0], + (int) $resolution[1], + Image::SHRINK_ONLY | Image::FIT ); $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); } else { throw new \RuntimeException("Malformed size description: " . (string) $size["id"]); } - + $res[1] = $image->getImageWidth(); $res[2] = $image->getImageHeight(); - if($res[1] <= 300 || $res[2] <= 300) + if ($res[1] <= 300 || $res[2] <= 300) { $image->writeImage("$outputDir/$size[id].gif"); - else + } else { $image->writeImage("$outputDir/$size[id].jpeg"); - + } + $res[3] = true; $image->destroy(); unset($image); - + return $res; } private function saveImageResizedCopies(?\Imagick $image, string $filename, string $hash): void { - if(!$image) { - $image = new \Imagick; + if (!$image) { + $image = new \Imagick(); $image->readImage($filename); } - + $dir = dirname($this->pathFromHash($hash)); $dir = "$dir/$hash" . "_cropped"; - if(!is_dir($dir)) { + if (!is_dir($dir)) { @unlink($dir); # Added to transparently bypass issues with dead pesudofolders summoned by buggy SWIFT impls (selectel) mkdir($dir); } $sizes = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); - if(!$sizes) + if (!$sizes) { throw new \RuntimeException("Could not load photosizes.xml!"); + } $sizesMeta = []; - if(OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["photoSaving"] === "quick") { - foreach($sizes->Size as $size) - $sizesMeta[(string)$size["id"]] = [false, false, false, false]; + if (OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["photoSaving"] === "quick") { + foreach ($sizes->Size as $size) { + $sizesMeta[(string) $size["id"]] = [false, false, false, false]; + } } else { - foreach($sizes->Size as $size) - $sizesMeta[(string)$size["id"]] = $this->resizeImage(clone $image, $dir, $size); + foreach ($sizes->Size as $size) { + $sizesMeta[(string) $size["id"]] = $this->resizeImage(clone $image, $dir, $size); + } } $sizesMeta = MessagePack::pack($sizesMeta); @@ -97,73 +109,92 @@ private function saveImageResizedCopies(?\Imagick $image, string $filename, stri protected function saveFile(string $filename, string $hash): bool { - $image = new \Imagick; - $image->readImage($filename); - $h = $image->getImageHeight(); - $w = $image->getImageWidth(); - if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) + $input_image = new \Imagick(); + $input_image->readImage($filename); + $h = $input_image->getImageHeight(); + $w = $input_image->getImageWidth(); + if (($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) { throw new ISE("Invalid layout: image is too wide/short"); - + } + + # gif fix 10.01.2025 + if ($input_image->getImageFormat() === 'GIF') { + $input_image->setIteratorIndex(0); + } + + # png workaround (transparency to white) + $image = new \Imagick(); + $bg = new \ImagickPixel('white'); + $image->newImage($w, $h, $bg); + $image->compositeImage($input_image, \Imagick::COMPOSITE_OVER, 0, 0); + $sizes = Image::calculateSize( - $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT + $image->getImageWidth(), + $image->getImageHeight(), + 8192, + 4320, + Image::SHRINK_ONLY | Image::FIT ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); $image->writeImage($this->pathFromHash($hash)); $this->saveImageResizedCopies($image, $filename, $hash); - + return true; } - - function crop(real $left, real $top, real $width, real $height): void + + public function crop(float $left, float $top, float $width, float $height): void { - if(isset($this->changes["hash"])) + if (isset($this->changes["hash"])) { $hash = $this->changes["hash"]; - else if(!is_null($this->getRecord())) + } elseif (!is_null($this->getRecord())) { $hash = $this->getRecord()->hash; - else + } else { throw new ISE("Cannot crop uninitialized image. Please call setFile(\$_FILES[...]) first."); - + } + $image = Image::fromFile($this->pathFromHash($hash)); $image->crop($left, $top, $width, $height); $image->save($this->pathFromHash($hash)); } - - function isolate(): void + + public function isolate(): void { - if(is_null($this->getRecord())) + if (is_null($this->getRecord())) { throw new ISE("Cannot isolate unpresisted image. Please save() it first."); - + } + DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete(); } - function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array + public function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array { $sizes = $this->getRecord()->sizes; - if(!$sizes || $forceUpdate) { - if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { + if (!$sizes || $forceUpdate) { + if ($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { $hash = $this->getRecord()->hash; - $this->saveImageResizedCopies(NULL, $this->pathFromHash($hash), $hash); + $this->saveImageResizedCopies(null, $this->pathFromHash($hash), $hash); $this->save(); return $this->getSizes(); } - return NULL; + return null; } $res = []; $sizes = MessagePack::unpack($sizes); - foreach($sizes as $id => $meta) { - if(isset($meta[3]) && !$meta[3]) { + foreach ($sizes as $id => $meta) { + if (isset($meta[3]) && !$meta[3]) { $res[$id] = (object) [ "url" => ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/photos/thumbnails/" . $this->getId() . "_$id.jpeg", - "width" => NULL, - "height" => NULL, - "crop" => NULL + "width" => null, + "height" => null, + "crop" => null, ]; continue; } - + $url = $this->getURL(); $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url); $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg"; @@ -172,7 +203,7 @@ function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array "url" => $url, "width" => $meta[1], "height" => $meta[2], - "crop" => $meta[0] + "crop" => $meta[0], ]; } @@ -181,69 +212,78 @@ function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array "url" => $this->getURL(), "width" => $x, "height" => $y, - "crop" => false + "crop" => false, ]; return $res; } - - function forceSize(string $sizeName): bool + + public function forceSize(string $sizeName): bool { $hash = $this->getRecord()->hash; $sizes = MessagePack::unpack($this->getRecord()->sizes); $size = $sizes[$sizeName] ?? false; - if(!$size) + if (!$size) { return $size; - - if(!isset($size[3]) || $size[3] === true) + } + + if (!isset($size[3]) || $size[3] === true) { return true; - + } + $path = $this->pathFromHash($hash); $dir = dirname($this->pathFromHash($hash)); $dir = "$dir/$hash" . "_cropped"; - if(!is_dir($dir)) { + if (!is_dir($dir)) { @unlink($dir); mkdir($dir); } - + $sizeMetas = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); - if(!$sizeMetas) + if (!$sizeMetas) { throw new \RuntimeException("Could not load photosizes.xml!"); - - $sizeInfo = NULL; - foreach($sizeMetas->Size as $size) - if($size["id"] == $sizeName) + } + + $sizeInfo = null; + foreach ($sizeMetas->Size as $size) { + if ($size["id"] == $sizeName) { $sizeInfo = $size; - - if(!$sizeInfo) + } + } + + if (!$sizeInfo) { return false; - - $pic = new \Imagick; + } + + $pic = new \Imagick(); $pic->readImage($path); $sizes[$sizeName] = $this->resizeImage($pic, $dir, $sizeInfo); - + $this->stateChanges("sizes", MessagePack::pack($sizes)); $this->save(); - + return $sizes[$sizeName][3]; } - function getVkApiSizes(): ?array + public function getVkApiSizes(): ?array { $res = []; $sizes = $this->getSizes(); - if(!$sizes) - return NULL; + if (!$sizes) { + return null; + } $manifest = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); - if(!$manifest) - return NULL; + if (!$manifest) { + return null; + } $mappings = []; - foreach($manifest->Size as $size) + foreach ($manifest->Size as $size) { $mappings[(string) $size["id"]] = (string) $size["vkId"]; + } - foreach($sizes as $id => $meta) { + foreach ($sizes as $id => $meta) { $type = $mappings[$id] ?? $id; $meta->type = $type; $res[$type] = $meta; @@ -252,24 +292,26 @@ function getVkApiSizes(): ?array return $res; } - function getURLBySizeId(string $size): string + public function getURLBySizeId(string $size): string { $sizes = $this->getSizes(); - if(!$sizes) + if (!$sizes) { return $this->getURL(); + } $size = $sizes[$size]; - if(!$size) + if (!$size) { return $this->getURL(); + } return $size->url; } - function getDimensions(): array + public function getDimensions(): array { $x = $this->getRecord()->width; $y = $this->getRecord()->height; - if(!$x) { # no sizes in database + if (!$x) { # no sizes in database $hash = $this->getRecord()->hash; $image = Image::fromFile($this->pathFromHash($hash)); @@ -283,32 +325,37 @@ function getDimensions(): array return [$x, $y]; } - function getPageURL(): string + public function getPageURL(): string { - if($this->isAnonymous()) + if ($this->isAnonymous()) { return "/photos/" . base_convert((string) $this->getId(), 10, 32); + } return "/photo" . $this->getPrettyId(); } - function getAlbum(): ?Album + public function getAlbum(): ?Album { - return (new Albums)->getAlbumByPhotoId($this); + $album = (new Albums())->getAlbumByPhotoId($this); + if (!$album || $album->isDeleted()) { + return null; + } + + return $album; } - function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object + public function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object { $res = (object) []; $res->id = $res->pid = $this->getVirtualId(); $res->owner_id = $res->user_id = $this->getOwner()->getId(); - $res->aid = $res->album_id = NULL; + $res->aid = $res->album_id = 0; $res->width = $this->getDimensions()[0]; $res->height = $this->getDimensions()[1]; $res->date = $res->created = $this->getPublicationTime()->timestamp(); - - if($photo_sizes) { - $res->sizes = $this->getVkApiSizes(); + if ($photo_sizes) { + $res->sizes = array_values($this->getVkApiSizes()); $res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule"); $res->src = $res->photo_130 = $this->getURLBySizeId("tiny"); $res->src_big = $res->photo_604 = $this->getURLBySizeId("normal"); @@ -316,22 +363,48 @@ function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object $res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger"); $res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original"); $res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES"); + $res->orig_photo = [ + "height" => $res->height, + "width" => $res->width, + "type" => "base", + "url" => $this->getURL(), + ]; } - if($extended) { - $res->likes = $this->getLikesCount(); # их нету но пусть будут - $res->comments = $this->getCommentsCount(); - $res->tags = 0; + if ($extended) { + $res->likes = [ + "likes" => $this->getLikesCount(), + "user_likes" => 0, + "can_like" => 1, + "can_publish" => 1, + ]; + $res->comments = [ + "count" => $this->getCommentsCount(), + "can_post" => 1, + ]; $res->can_comment = 1; - $res->can_repost = 0; + $res->can_repost = 1; } return $res; } - static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo + public function canBeViewedBy(?User $user = null): bool { - $photo = new static; + if ($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + if (!is_null($this->getAlbum())) { + return $this->getAlbum()->canBeViewedBy($user); + } else { + return $this->getOwner()->canBeViewedBy($user); + } + } + + public static function fastMake(int $owner, string $description, array $file, ?Album $album = null, bool $anon = false): Photo + { + $photo = new Photo(); $photo->setOwner($owner); $photo->setDescription(iconv_substr($description, 0, 36) . "..."); $photo->setAnonymous($anon); @@ -339,7 +412,7 @@ static function fastMake(int $owner, string $description = "", array $file, ?Alb $photo->setFile($file); $photo->save(); - if(!is_null($album)) { + if (!is_null($album)) { $album->addPhoto($photo); $album->setEdited(time()); $album->save(); @@ -347,4 +420,20 @@ static function fastMake(int $owner, string $description = "", array $file, ?Alb return $photo; } + + public function toNotifApiStruct() + { + $res = (object) []; + + $res->id = $this->getVirtualId(); + $res->owner_id = $this->getOwner()->getId(); + $res->aid = 0; + $res->src = $this->getURLBySizeId("tiny"); + $res->src_big = $this->getURLBySizeId("normal"); + $res->src_small = $this->getURLBySizeId("miniscule"); + $res->text = $this->getDescription(); + $res->created = $this->getPublicationTime()->timestamp(); + + return $res; + } } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 000000000..acbd70e69 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,319 @@ +importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + public function getCoverURL(string $size = "normal"): ?string + { + $photo = (new Photos())->get((int) $this->getRecord()->cover_photo_id); + return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size); + } + + public function getLength(): int + { + return $this->getRecord()->length; + } + + public function fetchClassic(int $offset = 0, ?int $limit = null): \Traversable + { + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("index ASC"); + + foreach ($related as $rel) { + $media = $rel->ref($this->entityTableName, "media"); + if (!$media) { + continue; + } + + yield new $this->entityClassName($media); + } + } + + public function getAudios(int $offset = 0, ?int $limit = null, ?int $shuffleSeed = null): \Traversable + { + if (!$shuffleSeed) { + foreach ($this->fetchClassic($offset, $limit) as $e) { + yield $e; + } # No, I can't return, it will break with [] + + return; + } + + $ids = []; + foreach ($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) { + $ids[] = $rel->i; + } + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach ($ids as $id) { + yield (new Audios())->get($id); + } + } + + public function add(RowModel $audio): bool + { + if ($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + public function remove(RowModel $audio): bool + { + if ($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + public function isBookmarkedBy(RowModel $entity = null): bool + { + if (!$entity) { + return false; + } + + $id = $entity->getId(); + if ($entity instanceof Club) { + $id *= -1; + } + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + public function bookmark(RowModel $entity): bool + { + if ($this->isBookmarkedBy($entity)) { + return false; + } + + $id = $entity->getId(); + if ($entity instanceof Club) { + $id *= -1; + } + + if ($this->importTable->where("entity", $id)->count('*') > self::MAX_COUNT) { + throw new \OutOfBoundsException("Maximum amount of playlists"); + } + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + public function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if ($entity instanceof Club) { + $id *= -1; + } + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + public function getDescription(): ?string + { + return $this->getRecord()->description; + } + + public function getDescriptionHTML(): ?string + { + return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML); + } + + public function getListens() + { + return $this->getRecord()->listens; + } + + public function toVkApiStruct(?User $user = null): object + { + $cover = $this->getCoverPhoto(); + $obj = (object) [ + "id" => $this->getId(), + "owner_id" => $this->getOwner()->getRealId(), + "title" => $this->getName(), + "description" => $this->getDescription(), + "size" => $this->size(), + "length" => $this->getLength(), + "created" => $this->getCreationTime()->timestamp(), + "modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : null, + "accessible" => $this->canBeViewedBy($user), + "editable" => $this->canBeModifiedBy($user), + "bookmarked" => $this->isBookmarkedBy($user), + "listens" => $this->getListens(), + "cover_url" => $this->getCoverURL(), + "searchable" => !$this->isUnlisted(), + ]; + + if ($cover) { + $dimensions = $cover->getDimensions(); + + $obj->thumb = (object) [ + "width" => $dimensions[0], + "height" => $dimensions[1], + "photo_34" => $cover->getURLBySizeId("miniscule"), + "photo_68" => $cover->getURLBySizeId("tiny"), + "photo_135" => $cover->getURLBySizeId("xsmall"), + "photo_270" => $cover->getURLBySizeId("small"), + "photo_300" => $cover->getURLBySizeId("medium"), + "photo_600" => $cover->getURLBySizeId("normal"), + "photo_1200" => $cover->getURLBySizeId("original"), + ]; + } + + return $obj; + } + + public function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + public function resetLength(): bool + { + $this->stateChanges("length", 0); + + return true; + } + + public function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } + + public function hasAudio(Audio $audio): bool + { + $ctx = DatabaseConnection::i()->getContext(); + return !is_null($ctx->table("playlist_relations")->where([ + "collection" => $this->getId(), + "media" => $audio->getId(), + ])->fetch()); + } + + public function getCoverPhotoId(): ?int + { + return $this->getRecord()->cover_photo_id; + } + + public function getCoverPhoto(): ?Photo + { + return (new Photos())->get((int) $this->getRecord()->cover_photo_id); + } + + public function canBeModifiedBy(User $user = null): bool + { + if (!$user) { + return false; + } + + if ($this->getOwner() instanceof User) { + return $user->getId() == $this->getOwner()->getId(); + } else { + return $this->getOwner()->canBeModifiedBy($user); + } + } + + public function getLengthInMinutes(): int + { + return (int) round($this->getLength() / 60, PHP_ROUND_HALF_DOWN); + } + + public function fastMakeCover(int $owner, array $file) + { + $cover = new Photo(); + $cover->setOwner($owner); + $cover->setDescription("Playlist cover image"); + $cover->setFile($file); + $cover->setCreated(time()); + $cover->setSystem(true); + $cover->save(); + + $this->setCover_photo_id($cover->getId()); + + return $cover; + } + + public function getURL(): string + { + return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId(); + } + + public function incrementListens() + { + $this->stateChanges("listens", ($this->getListens() + 1)); + } + + public function getMetaDescription(): string + { + $length = $this->getLengthInMinutes(); + + $props = []; + $props[] = tr("audios_count", $this->size()); + $props[] = "" . tr("listens_count", $this->getListens()) . ""; + if ($length > 0) { + $props[] = "" . tr("minutes_count", $length) . ""; + } + $props[] = "" . tr("created_playlist") . " " . $this->getPublicationTime() . ""; + # if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime(); + + return implode(" ", $props); + } + + public function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } +} diff --git a/Web/Models/Entities/Poll.php b/Web/Models/Entities/Poll.php index 7e32505ff..80a48d5ff 100644 --- a/Web/Models/Entities/Poll.php +++ b/Web/Models/Entities/Poll.php @@ -1,10 +1,14 @@ -getRecord()->title; } - - function getMetaDescription(): string + + public function getMetaDescription(): string { $props = []; $props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public"); - if($this->isMultipleChoice()) $props[] = tr("poll_multi"); - if(!$this->isRevotable()) $props[] = tr("poll_lock"); - if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt()); - + if ($this->isMultipleChoice()) { + $props[] = tr("poll_multi"); + } + if (!$this->isRevotable()) { + $props[] = tr("poll_lock"); + } + if (!is_null($this->endsAt())) { + $props[] = tr("poll_until", $this->endsAt()); + } + return implode(" • ", $props); } - - function getOwner(): User + + public function getOwner(): User { - return (new Users)->get($this->getRecord()->owner); + return (new Users())->get($this->getRecord()->owner); } - - function getOptions(): array + + public function getOptions(): array { $options = $this->getRecord()->related("poll_options.poll"); $res = []; - foreach($options as $opt) + foreach ($options as $opt) { $res[$opt->id] = $opt->name; - + } + return $res; } - - function getUserVote(User $user): ?array + + public function getUserVote(User $user): ?array { $ctx = DatabaseConnection::i()->getContext(); $votedOpts = $ctx->table("poll_votes") ->where(["user" => $user->getId(), "poll" => $this->getId()]); - - if($votedOpts->count() == 0) - return NULL; - + + if ($votedOpts->count() == 0) { + return null; + } + $res = []; - foreach($votedOpts as $votedOpt) { + foreach ($votedOpts as $votedOpt) { $option = $ctx->table("poll_options")->get($votedOpt->option); $res[] = [$option->id, $option->name]; } - + return $res; } - - function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array + + public function getVoters(int $optionId, int $page = 1, ?int $perPage = null): array { $res = []; $ctx = DatabaseConnection::i()->getContext(); - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $perPage ??= OPENVK_DEFAULT_PER_PAGE; $voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]); - foreach($voters->page($page, $perPage) as $vote) - $res[] = (new Users)->get($vote->user); - + foreach ($voters->page($page, $perPage) as $vote) { + $res[] = (new Users())->get($vote->user); + } + return $res; } - - function getVoterCount(?int $optionId = NULL): int + + public function getVoterCount(?int $optionId = null): int { $votes = DatabaseConnection::i()->getContext()->table("poll_votes"); - if(!$optionId) + if (!$optionId) { return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c; - - return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count(); + } + + return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count('*'); } - - function getResults(?User $user = NULL): object + + public function getResults(?User $user = null): object { $ctx = DatabaseConnection::i()->getContext(); - $voted = NULL; - if(!is_null($user)) + $voted = null; + if (!is_null($user)) { $voted = $this->getUserVote($user); - + } + $result = (object) []; $result->totalVotes = $this->getVoterCount(); - + $unsOptions = []; - foreach($this->getOptions() as $id => $title) { + foreach ($this->getOptions() as $id => $title) { $option = (object) []; $option->id = $id; $option->name = $title; - + $option->votes = $this->getVoterCount($id); $option->pct = $result->totalVotes == 0 ? 0 : min(100, floor(($option->votes / $result->totalVotes) * 100)); $option->voters = $this->getVoters($id, 1, 10); - if(!$user || !$voted) - $option->voted = NULL; - else + if (!$user || !$voted) { + $option->voted = null; + } else { $option->voted = in_array([$id, $title], $voted); - + } + $unsOptions[$id] = $option; } - + $optionsC = sizeof($unsOptions); $sOptions = $unsOptions; - usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; }); - for($i = 0; $i < $optionsC; $i++) + usort($sOptions, function ($a, $b) { + return $a->votes <=> $b->votes; + }); + for ($i = 0; $i < $optionsC; $i++) { $unsOptions[$id]->rate = $optionsC - $i - 1; - + } + $result->options = array_values($unsOptions); - + return $result; } - - function isAnonymous(): bool + + public function isAnonymous(): bool { return (bool) $this->getRecord()->is_anonymous; } - - function isMultipleChoice(): bool + + public function isMultipleChoice(): bool { return (bool) $this->getRecord()->allows_multiple; } - - function isRevotable(): bool + + public function isRevotable(): bool { return (bool) $this->getRecord()->can_revote; } - - function endsAt(): ?DateTime + + public function endsAt(): ?DateTime { - if(!$this->getRecord()->until) - return NULL; - + if (!$this->getRecord()->until) { + return null; + } + return new DateTime($this->getRecord()->until); } - - function hasEnded(): bool + + public function hasEnded(): bool { - if($this->getRecord()->ended) + if ($this->getRecord()->ended) { return true; - - if(!is_null($this->getRecord()->until)) + } + + if (!is_null($this->getRecord()->until)) { return time() >= $this->getRecord()->until; - + } + return false; } - - function hasVoted(User $user): bool + + public function hasVoted(User $user): bool { return !is_null($this->getUserVote($user)); } - - function canVote(User $user): bool + + public function canVote(User $user): bool { - return !$this->hasEnded() && !$this->hasVoted($user); + return !$this->hasEnded() && !$this->hasVoted($user) && !is_null($this->getAttachedPost()) && $this->getAttachedPost()->getSuggestionType() == 0; } - - function vote(User $user, array $optionIds): void + + public function vote(User $user, array $optionIds): void { - if($this->hasEnded()) - throw new PollLockedException; - - if($this->hasVoted($user)) - throw new AlreadyVotedException; - - $optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds)); + if ($this->hasEnded()) { + throw new PollLockedException(); + } + + if ($this->hasVoted($user)) { + throw new AlreadyVotedException(); + } + + $optionIds = array_map(function ($x) { + return (int) $x; + }, array_unique($optionIds)); $validOpts = array_keys($this->getOptions()); - if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice())) - throw new UnexpectedValueException; - - if(sizeof(array_diff($optionIds, $validOpts)) > 0) - throw new InvalidOptionException; - - foreach($optionIds as $opt) { + if (empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice())) { + throw new UnexpectedValueException(); + } + + if (sizeof(array_diff($optionIds, $validOpts)) > 0) { + throw new InvalidOptionException(); + } + + foreach ($optionIds as $opt) { DatabaseConnection::i()->getContext()->table("poll_votes")->insert([ "user" => $user->getId(), "poll" => $this->getId(), @@ -192,64 +220,69 @@ function vote(User $user, array $optionIds): void ]); } } - - function revokeVote(User $user): void + + public function revokeVote(User $user): void { - if(!$this->isRevotable()) - throw new PollLockedException; - + if (!$this->isRevotable()) { + throw new PollLockedException(); + } + $this->getRecord()->related("poll_votes.poll") ->where("user", $user->getId())->delete(); } - - function setOwner(User $owner): void + + public function setOwner(User $owner): void { $this->stateChanges("owner", $owner->getId()); } - - function setEndDate(int $timestamp): void + + public function setEndDate(int $timestamp): void { - if(!is_null($this->getRecord())) - throw new PollLockedException; - + if (!is_null($this->getRecord())) { + throw new PollLockedException(); + } + $this->stateChanges("until", $timestamp); } - - function setEnded(): void + + public function setEnded(): void { $this->stateChanges("ended", 1); } - - function setOptions(array $options): void + + public function setOptions(array $options): void { - if(!is_null($this->getRecord())) - throw new PollLockedException; - - if(sizeof($options) > ovkGetQuirk("polls.max-opts")) - throw new TooMuchOptionsException; - + if (!is_null($this->getRecord())) { + throw new PollLockedException(); + } + + if (sizeof($options) > ovkGetQuirk("polls.max-opts")) { + throw new TooMuchOptionsException(); + } + $this->choicesToPersist = $options; } - - function setRevotability(bool $canReVote): void + + public function setRevotability(bool $canReVote): void { - if(!is_null($this->getRecord())) - throw new PollLockedException; - + if (!is_null($this->getRecord())) { + throw new PollLockedException(); + } + $this->stateChanges("can_revote", $canReVote); } - - function setAnonymity(bool $anonymous): void + + public function setAnonymity(bool $anonymous): void { $this->stateChanges("is_anonymous", $anonymous); } - - function setMultipleChoice(bool $mc): void + + public function setMultipleChoice(bool $mc): void { $this->stateChanges("allows_multiple", $mc); } - - function importXML(User $owner, string $xml): void + + public function importXML(User $owner, string $xml): void { $xml = simplexml_load_string($xml); $this->setOwner($owner); @@ -257,39 +290,69 @@ function importXML(User $owner, string $xml): void $this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes"); $this->setAnonymity(($xml["anonymous"] ?? "no") == "yes"); $this->setRevotability(($xml["locked"] ?? "no") == "no"); - if(ctype_digit((string) ($xml["duration"] ?? ""))) + if (ctype_digit((string) ($xml["duration"] ?? ""))) { $this->setEndDate(time() + ((86400 * (int) $xml["duration"]))); - + } + $options = []; - foreach($xml->options->option as $opt) + foreach ($xml->options->option as $opt) { $options[] = (string) $opt; - - if(empty($options)) - throw new UnexpectedValueException; - + } + + if (empty($options)) { + throw new UnexpectedValueException(); + } + $this->setOptions($options); } - - static function import(User $owner, string $xml): Poll + + public static function import(User $owner, string $xml): Poll { - $poll = new Poll; + $poll = new Poll(); $poll->importXML($owner, $xml); $poll->save(); - + return $poll; } - - function save(?bool $log = false): void + + public function canBeViewedBy(?User $user = null): bool + { + # waiting for #935 :( + /*if(!is_null($this->getAttachedPost())) { + return $this->getAttachedPost()->canBeViewedBy($user); + } else {*/ + return true; + #} + + } + + public function save(?bool $log = false): void { - if(empty($this->choicesToPersist)) - throw new InvalidStateException; - + if (empty($this->choicesToPersist)) { + throw new InvalidStateException(); + } + parent::save($log); - foreach($this->choicesToPersist as $option) { + foreach ($this->choicesToPersist as $option) { DatabaseConnection::i()->getContext()->table("poll_options")->insert([ "poll" => $this->getId(), "name" => $option, ]); } } + + public function getAttachedPost() + { + $post = DatabaseConnection::i()->getContext()->table("attachments") + ->where( + ["attachable_type" => static::class, + "attachable_id" => $this->getId()] + )->fetch(); + + if (!is_null($post->target_id)) { + return (new Posts())->get($post->target_id); + } else { + return null; + } + } } diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 6d0fe8cf9..b7e409e80 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -1,5 +1,9 @@ - $this->getRecord()->id, ]; - if((sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0) !== $liked) { - if($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment) - (new LikeNotification($this->getOwner(false), $this, $user))->emit(); + if ((DB::i()->getContext()->table("likes")->where($searchData)->count("*") > 0) !== $liked) { + if ($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club)) { + (new LikeNotification($this->getOwner(false), $this, $user, time()))->emit(); + } parent::setLike($liked, $user); } - if($depth < ovkGetQuirk("wall.repost-liking-recursion-limit")) - foreach($this->getChildren() as $attachment) - if($attachment instanceof Post) + if ($depth < ovkGetQuirk("wall.repost-liking-recursion-limit")) { + foreach ($this->getChildren() as $attachment) { + if ($attachment instanceof Post) { $attachment->setLikeRecursively($liked, $user, $depth + 1); + } + } + } } - + /** * May return fake owner (group), if flags are [1, (*)] - * + * * @param bool $honourFlags - check flags */ - function getOwner(bool $honourFlags = true, bool $real = false): RowModel + public function getOwner(bool $honourFlags = true, bool $real = false): RowModel { - if($honourFlags && $this->isPostedOnBehalfOfGroup()) { - if($this->getRecord()->wall < 0) - return (new Clubs)->get(abs($this->getRecord()->wall)); + if ($honourFlags && $this->isPostedOnBehalfOfGroup()) { + if ($this->getRecord()->wall < 0) { + return (new Clubs())->get(abs($this->getRecord()->wall)); + } } - + return parent::getOwner($real); } - - function getPrettyId(): string + + public function getPrettyId(): string { return $this->getRecord()->wall . "_" . $this->getVirtualId(); } - - function getTargetWall(): int + + public function getTargetWall(): int { return $this->getRecord()->wall; } - function getWallOwner() + public function getWallOwner() { $w = $this->getRecord()->wall; - if($w < 0) - return (new Clubs)->get(abs($w)); + if ($w < 0) { + return (new Clubs())->get(abs($w)); + } - return (new Users)->get($w); + return (new Users())->get($w); } - - function getRepostCount(): int + + public function getRepostCount(): int { - return sizeof( - $this->getRecord() + return $this->getRecord() ->related("attachments.attachable_id") ->where("attachable_type", get_class($this)) - ); + ->count("*"); } - - function isPinned(): bool + + public function isPinned(): bool { return (bool) $this->getRecord()->pinned; } - - function isAd(): bool + + public function hasSource(): bool + { + return $this->getRecord()->source != null; + } + + public function getSource(bool $format = false) + { + $orig_source = $this->getRecord()->source; + if (!str_contains($orig_source, "https://") && !str_contains($orig_source, "http://")) { + $orig_source = "https://" . $orig_source; + } + + if (!$format) { + return $orig_source; + } + + return $this->formatLinks($orig_source); + } + + public function setSource(string $source) + { + $result = check_copyright_link($source); + + $this->stateChanges("source", $source); + } + + public function resetSource() + { + $this->stateChanges("source", null); + } + + public function getVkApiCopyright(): object + { + return (object) [ + 'id' => 0, + 'link' => $this->getSource(false), + 'name' => $this->getSource(false), + 'type' => 'link', + ]; + } + + public function isAd(): bool { return (bool) $this->getRecord()->ad; } - - function isPostedOnBehalfOfGroup(): bool + + public function isPostedOnBehalfOfGroup(): bool { return ($this->getRecord()->flags & 0b10000000) > 0; } - - function isSigned(): bool + + public function isSigned(): bool { return ($this->getRecord()->flags & 0b01000000) > 0; } - function isDeactivationMessage(): bool + public function isDeactivationMessage(): bool { - return (($this->getRecord()->flags & 0b00100000) > 0) && ($this->getRecord()->owner > 0); + return (($this->getRecord()->flags & 0b00100000) != 0x0) && ($this->getRecord()->wall > 0); } - - function isUpdateAvatarMessage(): bool + + public function isUpdateAvatarMessage(): bool { - return (($this->getRecord()->flags & 0b00010000) > 0) && ($this->getRecord()->owner > 0); + return (($this->getRecord()->flags & 0b00010000) != 0x0) && ($this->getRecord()->wall > 0); } - function isExplicit(): bool + public function isExplicit(): bool { return (bool) $this->getRecord()->nsfw; } - - function isDeleted(): bool + + public function isDeleted(): bool { return (bool) $this->getRecord()->deleted; } - - function getOwnerPost(): int + + public function getOwnerPost(): int { return $this->getOwner(false)->getId(); } - function getPlatform(bool $forAPI = false): ?string + public function getPlatform(bool $forAPI = false): ?string { $platform = $this->getRecord()->api_source_name; - if($forAPI) { + if ($forAPI) { switch ($platform) { + case 'openvk_native': + case 'openvk_flux_android': case 'openvk_refresh_android': case 'openvk_legacy_android': + case 'Kate Mobile': return 'android'; break; + case 'openvk_native_ios': case 'openvk_ios': case 'openvk_legacy_ios': + case 'VFeed': return 'iphone'; break; - + + case 'windows_phone': + return 'wphone'; + break; + case 'vika_touch': // кика хохотач ахахахаххахахахахах case 'vk4me': return 'mobile'; break; - case NULL: - return NULL; + case null: + return null; break; - + default: return 'api'; break; @@ -152,17 +212,17 @@ function getPlatform(bool $forAPI = false): ?string } } - function getPlatformDetails(): array + public function getPlatformDetails(): array { $clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml"); - foreach($clients as $client) { - if($client['tag'] == $this->getPlatform()) { + foreach ($clients as $client) { + if ($client['tag'] == $this->getPlatform()) { return [ "tag" => $client['tag'], "name" => $client['name'], "url" => $client['url'], - "img" => $client['img'] + "img" => $client['img'], ]; break; } @@ -170,13 +230,40 @@ function getPlatformDetails(): array return [ "tag" => $this->getPlatform(), - "name" => NULL, - "url" => NULL, - "img" => NULL + "name" => null, + "url" => null, + "img" => null, ]; } - - function pin(): void + + public function getPostSourceInfo(): array + { + $post_source = ["type" => "vk"]; + if ($this->getPlatform(true) !== null) { + $post_source = [ + "type" => "api", + "platform" => $this->getPlatform(true), + ]; + } + + if ($this->isUpdateAvatarMessage()) { + $post_source['data'] = 'profile_photo'; + } + + return $post_source; + } + + public function getVkApiType(): string + { + $type = 'post'; + if ($this->getSuggestionType() != 0) { + $type = 'suggest'; + } + + return $type; + } + + public function pin(): void { DB::i() ->getContext() @@ -186,79 +273,264 @@ function pin(): void "pinned" => true, ]) ->update(["pinned" => false]); - + $this->stateChanges("pinned", true); $this->save(); } - - function unpin(): void + + public function unpin(): void { $this->stateChanges("pinned", false); $this->save(); } - - function canBePinnedBy(User $user): bool + + public function canBePinnedBy(User $user = null): bool { - if($this->getTargetWall() < 0) - return (new Clubs)->get(abs($this->getTargetWall()))->canBeModifiedBy($user); - + if (!$user) { + return false; + } + + if ($this->getTargetWall() < 0) { + return (new Clubs())->get(abs($this->getTargetWall()))->canBeModifiedBy($user); + } + return $this->getTargetWall() === $user->getId(); } - - function canBeDeletedBy(User $user): bool + + public function canBeDeletedBy(User $user = null): bool { + if (!$user) { + return false; + } + + if ($this->getTargetWall() < 0 && !$this->getWallOwner()->canBeModifiedBy($user) && $this->getWallOwner()->getWallType() != 1 && $this->getSuggestionType() == 0) { + return false; + } + return $this->getOwnerPost() === $user->getId() || $this->canBePinnedBy($user); } - - function setContent(string $content): void + + public function setContent(string $content): void { - if(ctype_space($content)) + if (ctype_space($content)) { throw new \LengthException("Content length must be at least 1 character (not counting whitespaces)."); - else if(iconv_strlen($content) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxSize"]) + } elseif (iconv_strlen($content) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxSize"]) { throw new \LengthException("Content is too large."); - + } + $this->stateChanges("content", $content); } - function toggleLike(User $user): bool + public function toggleLike(User $user): bool { $liked = parent::toggleLike($user); - if($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment) - (new LikeNotification($this->getOwner(false), $this, $user))->emit(); + if (!$user->isPrivateLikes() && $this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club)) { + (new LikeNotification($this->getOwner(false), $this, $user, time()))->emit(); + } - foreach($this->getChildren() as $attachment) - if($attachment instanceof Post) + foreach ($this->getChildren() as $attachment) { + if ($attachment instanceof Post) { $attachment->setLikeRecursively($liked, $user, 2); + } + } return $liked; } - function setLike(bool $liked, User $user): void + public function setLike(bool $liked, User $user): void { $this->setLikeRecursively($liked, $user, 1); } - - function deletePost(): void + + public function deletePost(): void { $this->setDeleted(1); $this->unwire(); $this->save(); } - function canBeEditedBy(?User $user = NULL): bool + public function canBeViewedBy(?User $user = null): bool { - if(!$user) + if ($this->isDeleted()) { return false; + } + + return $this->getWallOwner()->canBeViewedBy($user); + } + + public function getSuggestionType() + { + return $this->getRecord()->suggested; + } + + public function getPageURL(): string + { + return "/wall" . $this->getPrettyId(); + } + + public function toNotifApiStruct() + { + $res = (object) []; + + $res->id = $this->getVirtualId(); + $res->to_id = $this->getWallOwner()->getRealId(); + $res->from_id = $this->getOwner()->getRealId(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->text = $this->getText(false); + $res->attachments = []; # todo - if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage()) + $res->copy_owner_id = null; # todo + $res->copy_post_id = null; # todo + + return $res; + } + + public function canBeEditedBy(?User $user = null): bool + { + if (!$user) { return false; + } - if($this->getTargetWall() > 0) + if ($this->isDeactivationMessage() || $this->isUpdateAvatarMessage()) { + return false; + } + + if ($this->getTargetWall() > 0) { return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId(); + } else { + if ($this->isPostedOnBehalfOfGroup()) { + return $this->getWallOwner()->canBeModifiedBy($user); + } else { + return $user->getId() == $this->getOwner(false)->getId(); + } + } return $user->getId() == $this->getOwner(false)->getId(); } - - use Traits\TRichText; + + public function toRss(): \Bhaktaraz\RSSGenerator\Item + { + $domain = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; + $description = $this->getText(false); + $title = str_replace("\n", "", ovk_proc_strtr($description, 79)); + $description_html = $description; + $url = $domain . "/wall" . $this->getPrettyId(); + + if ($this->isUpdateAvatarMessage()) { + $title = tr('upd_in_general'); + } + if ($this->isDeactivationMessage()) { + $title = tr('post_deact_in_general'); + } + + $author = $this->getOwner(); + $target_wall = $this->getWallOwner(); + $author_name = escape_html($author->getCanonicalName()); + if ($this->isExplicit()) { + $title = 'NSFW: ' . $title; + } + + foreach ($this->getChildren() as $child) { + if ($child instanceof Photo) { + $child_page = $domain . $child->getPageURL(); + $child_url = $child->getURL(); + $description_html .= "

"; + } elseif ($child instanceof Video) { + $child_page = $domain . '/video' . $child->getPrettyId(); + + if ($child->getType() != 1) { + $description_html .= "" . + "
" . + "
" . + "" . escape_html($child->getName()) . "
"; + } else { + $description_html .= "" . + "
" . + "getVideoDriver()->getURL() . "\">" . escape_html($child->getName()) . "
"; + } + } elseif ($child instanceof Audio) { + if (!$child->isWithdrawn()) { + $description_html .= "
" + . "" . escape_html($child->getName()) . ":" + . "
" + . "" + . "
"; + } + } elseif ($child instanceof Poll) { + $description_html .= "
" . tr('poll') . ": " . escape_html($child->getTitle()); + } elseif ($child instanceof Note) { + $description_html .= "
" . tr('note') . ": " . escape_html($child->getName()); + } + } + + $description_html .= "
" . tr('author') . ": " . $author_name . ""; + + if ($target_wall->getRealId() != $author->getRealId()) { + $description_html .= "
" . tr('on_wall') . ": " . escape_html($target_wall->getCanonicalName()) . ""; + } + + if ($this->isSigned()) { + $signer = $this->getOwner(false); + $description_html .= "
" . tr('sign_short') . ": " . escape_html($signer->getCanonicalName()) . ""; + } + + if ($this->hasSource()) { + $description_html .= "
" . tr('source') . ": " . escape_html($this->getSource()); + } + + $item = new \Bhaktaraz\RSSGenerator\Item(); + $item->title($title) + ->url($url) + ->guid($url) + ->creator($author_name) + ->pubDate($this->getPublicationTime()->timestamp()) + ->content(str_replace("\n", "
", $description_html)); + + return $item; + } + + public function getGeo(): ?object + { + if (!$this->getRecord()->geo) { + return null; + } + + return (object) json_decode($this->getRecord()->geo, true, JSON_UNESCAPED_UNICODE); + } + + public function setGeo($encoded_object): void + { + $final_geo = $encoded_object['name']; + $neutral_names = ["Россия", "Russia", "Росія", "Россія", "Украина", "Ukraine", "Україна", "Украіна"]; + foreach ($neutral_names as $name) { + if (str_contains($final_geo, $name . ", ")) { + $final_geo = str_replace($name . ", ", "", $final_geo); + } + } + + $encoded_object['name'] = ovk_proc_strtr($final_geo, 255); + $encoded = json_encode($encoded_object); + $this->stateChanges("geo", $encoded); + } + + public function getLat(): ?float + { + return (float) $this->getRecord()->geo_lat ?? null; + } + + public function getLon(): ?float + { + return (float) $this->getRecord()->geo_lon ?? null; + } + + public function getVkApiGeo(): object + { + return (object) [ + 'type' => 'point', + 'coordinates' => $this->getLat() . ',' . $this->getLon(), + 'name' => $this->getGeo()->name, + ]; + } } diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index 8f7832382..feb3dcb58 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -1,5 +1,9 @@ -getContext()->table($this->tableName); } - - function getOwner(bool $real = false): RowModel + + public function getOwner(bool $real = false): RowModel { $oid = (int) $this->getRecord()->owner; - if(!$real && $this->isAnonymous()) - $oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; + if (!$real && $this->isAnonymous()) { + $oid = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; + } $oid = abs($oid); - if($oid > 0) - return (new Users)->get($oid); - else - return (new Clubs)->get($oid * -1); + if ($oid > 0) { + return (new Users())->get($oid); + } else { + return (new Clubs())->get($oid * -1); + } } - - function getVirtualId(): int + + public function getVirtualId(): int { return $this->getRecord()->virtual_id; } - - function getPrettyId(): string + + public function getPrettyId(): string { return $this->getRecord()->owner . "_" . $this->getVirtualId(); } - - function getPublicationTime(): DateTime + + public function getPublicationTime(): DateTime { return new DateTime($this->getRecord()->created); } - - function getEditTime(): ?DateTime + + public function getEditTime(): ?DateTime { $edited = $this->getRecord()->edited; - if(is_null($edited)) return NULL; - + if (is_null($edited)) { + return null; + } + return new DateTime($edited); } - - function getComments(int $page, ?int $perPage = NULL): \Traversable + + public function getComments(int $page, ?int $perPage = null, string $sort = "ASC"): \Traversable { - return (new Comments)->getCommentsByTarget($this, $page, $perPage); + return (new Comments())->getCommentsByTarget($this, $page, $perPage, $sort); } - - function getCommentsCount(): int + + public function getCommentsCount(): int { - return (new Comments)->getCommentsCountByTarget($this); + return (new Comments())->getCommentsCountByTarget($this); } - function getLastComments(int $count): \Traversable + public function getLastComments(int $count): \Traversable { - return (new Comments)->getLastCommentsByTarget($this, $count); + return (new Comments())->getLastCommentsByTarget($this, $count); } - - function getLikesCount(): int + + public function getLikesCount(): int { - return sizeof(DB::i()->getContext()->table("likes")->where([ + return DB::i()->getContext()->table("likes")->where([ "model" => static::class, "target" => $this->getRecord()->id, - ])->group("origin")); + ])->count("DISTINCT origin"); } - - # TODO add pagination - function getLikers(): \Traversable + + public function getLikers(int $page = 1, ?int $perPage = null): \Traversable { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $sel = DB::i()->getContext()->table("likes")->where([ "model" => static::class, "target" => $this->getRecord()->id, - ]); - - foreach($sel as $like) - yield (new Users)->get($like->origin); + ])->page($page, $perPage); + + foreach ($sel as $like) { + $user = (new Users())->get($like->origin); + if ($user->isPrivateLikes() && OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]) { + $user = (new Users())->get((int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]); + } + + yield $user; + } } - - function isAnonymous(): bool + + public function getAccessKey(): string + { + return $this->getRecord()->access_key; + } + + public function checkAccessKey(?string $access_key): bool + { + if ($this->getAccessKey() === $access_key) { + return true; + } + + return !$this->isPrivate(); + } + + public function isPrivate(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + public function isAnonymous(): bool { return (bool) $this->getRecord()->anonymous; } - - function toggleLike(User $user): bool + + public function toggleLike(User $user): bool { $searchData = [ "origin" => $user->getId(), @@ -113,7 +149,7 @@ function toggleLike(User $user): bool "target" => $this->getRecord()->id, ]; - if(sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0) { + if (DB::i()->getContext()->table("likes")->where($searchData)->count("*") > 0) { DB::i()->getContext()->table("likes")->where($searchData)->delete(); return false; } @@ -122,7 +158,7 @@ function toggleLike(User $user): bool return true; } - function setLike(bool $liked, User $user): void + public function setLike(bool $liked, User $user): void { $searchData = [ "origin" => $user->getId(), @@ -130,50 +166,54 @@ function setLike(bool $liked, User $user): void "target" => $this->getRecord()->id, ]; - if($liked) - DB::i()->getContext()->table("likes")->insert($searchData); - else - DB::i()->getContext()->table("likes")->where($searchData)->delete(); + if ($liked) { + if (!$this->hasLikeFrom($user)) { + DB::i()->getContext()->table("likes")->insert($searchData); + } + } else { + if ($this->hasLikeFrom($user)) { + DB::i()->getContext()->table("likes")->where($searchData)->delete(); + } + } } - - function hasLikeFrom(User $user): bool + + public function hasLikeFrom(User $user): bool { $searchData = [ "origin" => $user->getId(), "model" => static::class, "target" => $this->getRecord()->id, ]; - - return sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0; + + return DB::i()->getContext()->table("likes")->where($searchData)->count("*") > 0; } - - function setVirtual_Id(int $id): void + + public function setVirtual_Id(int $id): void { throw new ISE("Setting virtual id manually is forbidden"); } - - function save(?bool $log = false): void + + public function save(?bool $log = false): void { $vref = $this->upperNodeReferenceColumnName; - + $vid = $this->getRecord()->{$vref} ?? $this->changes[$vref]; - if(!$vid) + if (!$vid) { throw new ISE("Can't presist post due to inability to calculate it's $vref post count. Have you set it?"); - + } + $pCount = sizeof($this->getTable()->where($vref, $vid)); - if(is_null($this->getRecord())) { + if (is_null($this->getRecord())) { # lol allow ppl to taint created value - if(!isset($this->changes["created"])) + if (!isset($this->changes["created"])) { $this->stateChanges("created", time()); - + } + $this->stateChanges("virtual_id", $pCount + 1); } /*else { $this->stateChanges("edited", time()); }*/ - + parent::save($log); } - - use Traits\TAttachmentHost; - use Traits\TOwnable; } diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index d449a2e8d..1d345fd40 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -1,154 +1,173 @@ -getRecord()->id; - } - - function getStatus(): int - { - return $this->getRecord()->status; - } - - function getContentType(): string - { - return $this->getRecord()->type; - } - - function getReason(): string - { - return $this->getRecord()->reason; - } - - function getTime(): DateTime - { - return new DateTime($this->getRecord()->date); - } - - function isDeleted(): bool - { - if ($this->getRecord()->deleted === 0) - { - return false; - } elseif ($this->getRecord()->deleted === 1) { - return true; - } - } - - function authorId(): int - { - return $this->getRecord()->user_id; - } - - function getUser(): User - { - return (new Users)->get((int) $this->getRecord()->user_id); - } - - function getContentId(): int - { - return (int) $this->getRecord()->target_id; - } - - function getContentObject() - { - if ($this->getContentType() == "post") return (new Posts)->get($this->getContentId()); - else if ($this->getContentType() == "photo") return (new Photos)->get($this->getContentId()); - else if ($this->getContentType() == "video") return (new Videos)->get($this->getContentId()); - else if ($this->getContentType() == "group") return (new Clubs)->get($this->getContentId()); - else if ($this->getContentType() == "comment") return (new Comments)->get($this->getContentId()); - else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); - else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); - else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); - else return null; - } - - function getAuthor(): RowModel - { - return (new Posts)->get($this->getContentId())->getOwner(); - } - - function getReportAuthor(): User - { - return (new Users)->get($this->getRecord()->user_id); - } - - function banUser($initiator) - { - $reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность"); - $this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator); - } - - function deleteContent() - { - if ($this->getContentType() !== "user") { - $pubTime = $this->getContentObject()->getPublicationTime(); - if (method_exists($this->getContentObject(), "getName")) { - $name = $this->getContentObject()->getName(); - $placeholder = "$pubTime ($name)"; - } else { - $placeholder = "$pubTime"; - } - - if ($this->getAuthor() instanceof Club) { - $name = $this->getAuthor()->getName(); - $this->getAuthor()->getOwner()->adminNotify("Ваш контент, который опубликовали $placeholder в созданной вами группе \"$name\" был удалён модераторами инстанса. За повторные или серьёзные нарушения группу могут заблокировать."); - } else { - $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); - } - $this->getContentObject()->delete($this->getContentType() !== "app"); - } - - $this->delete(); - } - - function getDuplicates(): \Traversable - { - return (new Reports)->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId()); - } - - function getDuplicatesCount(): int - { - return count(iterator_to_array($this->getDuplicates())); - } - - function hasDuplicates(): bool - { - return $this->getDuplicatesCount() > 0; - } - - function getContentName(): string - { - if (method_exists($this->getContentObject(), "getCanonicalName")) - return $this->getContentObject()->getCanonicalName(); - - return $this->getContentType() . " #" . $this->getContentId(); - } - - public function delete(bool $softly = true): void - { - if ($this->hasDuplicates()) { - foreach ($this->getDuplicates() as $duplicate) { - $duplicate->setDeleted(1); - $duplicate->save(); - } - } - - $this->setDeleted(1); - $this->save(); - } -} +getRecord()->id; + } + + public function getStatus(): int + { + return $this->getRecord()->status; + } + + public function getContentType(): string + { + return $this->getRecord()->type; + } + + public function getReason(): string + { + return $this->getRecord()->reason; + } + + public function getTime(): DateTime + { + return new DateTime($this->getRecord()->date); + } + + public function isDeleted(): bool + { + return $this->getRecord()->deleted === 1; + } + + public function authorId(): int + { + return $this->getRecord()->user_id; + } + + public function getUser(): User + { + return (new Users())->get((int) $this->getRecord()->user_id); + } + + public function getContentId(): int + { + return (int) $this->getRecord()->target_id; + } + + public function getContentObject() + { + if ($this->getContentType() == "post") { + return (new Posts())->get($this->getContentId()); + } elseif ($this->getContentType() == "photo") { + return (new Photos())->get($this->getContentId()); + } elseif ($this->getContentType() == "video") { + return (new Videos())->get($this->getContentId()); + } elseif ($this->getContentType() == "group") { + return (new Clubs())->get($this->getContentId()); + } elseif ($this->getContentType() == "comment") { + return (new Comments())->get($this->getContentId()); + } elseif ($this->getContentType() == "note") { + return (new Notes())->get($this->getContentId()); + } elseif ($this->getContentType() == "app") { + return (new Applications())->get($this->getContentId()); + } elseif ($this->getContentType() == "user") { + return (new Users())->get($this->getContentId()); + } elseif ($this->getContentType() == "audio") { + return (new Audios())->get($this->getContentId()); + } elseif ($this->getContentType() == "doc") { + return (new Documents())->get($this->getContentId()); + } else { + return null; + } + } + + public function getAuthor(): RowModel + { + return $this->getContentObject()->getOwner(); + } + + public function getReportAuthor(): User + { + return (new Users())->get($this->getRecord()->user_id); + } + + public function banUser($initiator) + { + $reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность"); + $this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator); + } + + public function deleteContent() + { + if ($this->getContentType() !== "user") { + $pubTime = $this->getContentObject()->getPublicationTime(); + if (method_exists($this->getContentObject(), "getName")) { + $name = $this->getContentObject()->getName(); + $placeholder = "$pubTime ($name)"; + } else { + $placeholder = "$pubTime"; + } + + if ($this->getAuthor() instanceof Club) { + $name = $this->getAuthor()->getName(); + $this->getAuthor()->getOwner()->adminNotify("Ваш контент, который опубликовали $placeholder в созданной вами группе \"$name\" был удалён модераторами инстанса. За повторные или серьёзные нарушения группу могут заблокировать."); + } else { + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + } + $this->getContentObject()->delete($this->getContentType() !== "app"); + } + + $this->delete(); + } + + public function getDuplicates(): \Traversable + { + return (new Reports())->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId()); + } + + public function getDuplicatesCount(): int + { + return count(iterator_to_array($this->getDuplicates())); + } + + public function hasDuplicates(): bool + { + return $this->getDuplicatesCount() > 0; + } + + public function getContentName(): string + { + $content_object = $this->getContentObject(); + if (!$content_object) { + return 'unknown'; + } + + if (method_exists($content_object, "getCanonicalName")) { + return $content_object->getCanonicalName(); + } + + return $this->getContentType() . " #" . $this->getContentId(); + } + + public function delete(bool $softly = true): void + { + if ($this->hasDuplicates()) { + foreach ($this->getDuplicates() as $duplicate) { + $duplicate->setDeleted(1); + $duplicate->save(); + } + } + + $this->setDeleted(1); + $this->save(); + } +} diff --git a/Web/Models/Entities/SupportAgent.php b/Web/Models/Entities/SupportAgent.php index 2f7fc21b2..3375ec128 100644 --- a/Web/Models/Entities/SupportAgent.php +++ b/Web/Models/Entities/SupportAgent.php @@ -1,5 +1,9 @@ -getRecord()->agent; } - function getName(): ?string + public function getName(): ?string { return $this->getRecord()->name; } - function getCanonicalName(): string + public function getCanonicalName(): string { return $this->getName(); } - function getAvatarURL(): ?string + public function getAvatarURL(): ?string { return $this->getRecord()->icon; } - function isShowNumber(): int + public function isShowNumber(): int { return $this->getRecord()->numerate; } - function getRealName(): string + public function getRealName(): string { - return (new Users)->get($this->getAgentId())->getCanonicalName(); + return (new Users())->get($this->getAgentId())->getCanonicalName(); } -} \ No newline at end of file +} diff --git a/Web/Models/Entities/SupportAlias.php b/Web/Models/Entities/SupportAlias.php index 9ad618a6b..49f817cea 100644 --- a/Web/Models/Entities/SupportAlias.php +++ b/Web/Models/Entities/SupportAlias.php @@ -1,38 +1,42 @@ -get($this->getRecord()->agent); + return (new Users())->get($this->getRecord()->agent); } - - function getName(): string + + public function getName(): string { return $this->getRecord()->name; } - - function getIcon(): ?string + + public function getIcon(): ?string { return $this->getRecord()->icon; } - - function shouldAppendNumber(): bool + + public function shouldAppendNumber(): bool { return (bool) $this->getRecord()->numerate; } - - function setAgent(User $agent): void + + public function setAgent(User $agent): void { $this->stateChanges("agent", $agent->getId()); } - - function setNumeration(bool $numerate): void + + public function setNumeration(bool $numerate): void { $this->stateChanges("numerate", $numerate); } diff --git a/Web/Models/Entities/Ticket.php b/Web/Models/Entities/Ticket.php index 31690ce3b..9c1abd5e8 100644 --- a/Web/Models/Entities/Ticket.php +++ b/Web/Models/Entities/Ticket.php @@ -1,36 +1,41 @@ -getRecord()->id; } - - function getStatus(): string + + public function getStatus(): string { return tr("support_status_" . $this->getRecord()->type); } - - function getType(): int + + public function getType(): int { return $this->getRecord()->type; } - - function getName(): string + + public function getName(): string { return ovk_proc_strtr($this->getRecord()->name, 100); } - - function getContext(): string + + public function getContext(): string { $text = $this->getRecord()->text; $text = $this->formatLinks($text); @@ -38,31 +43,29 @@ function getContext(): string $text = nl2br($text); return $text; } - - function getTime(): DateTime + + public function getTime(): DateTime { return new DateTime($this->getRecord()->created); } - - function isDeleted(): bool + + public function isDeleted(): bool { return (bool) $this->getRecord()->deleted; } - - function getUser(): user + + public function getUser(): user { - return (new Users)->get($this->getRecord()->user_id); + return (new Users())->get($this->getRecord()->user_id); } - function getUserId(): int + public function getUserId(): int { return $this->getRecord()->user_id; } - function isAd(): bool /* Эх, костыли... */ + public function isAd(): bool /* Эх, костыли... */ { - return false; + return false; } - - use Traits\TRichText; } diff --git a/Web/Models/Entities/TicketComment.php b/Web/Models/Entities/TicketComment.php index 2f1a5e8ff..ab8195c30 100644 --- a/Web/Models/Entities/TicketComment.php +++ b/Web/Models/Entities/TicketComment.php @@ -1,100 +1,112 @@ -get($this->getUser()->getId()); + return (new SupportAliases())->get($this->getUser()->getId()); } - - function getId(): int + + public function getId(): int { return $this->getRecord()->id; } - function getUType(): int + public function getUType(): int { return $this->getRecord()->user_type; } - - function getUser(): User - { - return (new Users)->get($this->getRecord()->user_id); + + public function getUser(): User + { + return (new Users())->get($this->getRecord()->user_id); } - function getTicket(): Ticket + public function getTicket(): Ticket { - return (new Tickets)->get($this->getRecord()->ticket_id); + return (new Tickets())->get($this->getRecord()->ticket_id); } - - function getAuthorName(): string + + public function getAuthorName(): string { - if($this->getUType() === 0) + if ($this->getUType() === 0) { return $this->getUser()->getCanonicalName(); - + } + $alias = $this->getSupportAlias(); - if(!$alias) + if (!$alias || mb_strlen(trim($alias->getName())) === 0) { return tr("helpdesk_agent") . " #" . $this->getAgentNumber(); - + } + $name = $alias->getName(); - if($alias->shouldAppendNumber()) + if ($alias->shouldAppendNumber()) { $name .= " №" . $this->getAgentNumber(); - + } + return $name; } - - function getAvatar(): string + + public function getAvatar(): string { - if($this->getUType() === 0) + if ($this->getUType() === 0) { return $this->getUser()->getAvatarUrl(); - + } + $default = "/assets/packages/static/openvk/img/support.jpeg"; $alias = $this->getSupportAlias(); - + return is_null($alias) ? $default : ($alias->getIcon() ?? $default); } - - function getAgentNumber(): ?string + + public function getAgentNumber(): ?string { - if($this->getUType() === 0) - return NULL; - + if ($this->getUType() === 0) { + return null; + } + $salt = "kiraMiki"; $hash = $this->getUser()->getId() . CHANDLER_ROOT_CONF["security"]["secret"] . $salt; $hash = hexdec(substr(hash("adler32", $hash), 0, 3)); $hash = ceil(($hash * 999) / 4096); # proportionalize to 0-999 - + return str_pad((string) $hash, 3, "0", STR_PAD_LEFT); } - - function getColorRotation(): ?int + + public function getColorRotation(): ?int { - if(is_null($agent = $this->getAgentNumber())) - return NULL; - - if(!is_null($this->getSupportAlias())) + if (is_null($agent = $this->getAgentNumber())) { + return null; + } + + if (!is_null($this->getSupportAlias())) { return 0; - + } + $agent = (int) $agent; - $rotation = $agent > 500 ? ( ($agent * 360) / 999 ) : $agent; # cap at 360deg + $rotation = $agent > 500 ? (($agent * 360) / 999) : $agent; # cap at 360deg $values = [0, 45, 160, 220, 310, 345]; # good looking colors - usort($values, function($x, $y) use ($rotation) { + usort($values, function ($x, $y) use ($rotation) { # find closest return abs($x - $rotation) - abs($y - $rotation); }); - + return array_shift($values); } - - function getContext(): string + + public function getContext(): string { $text = $this->getRecord()->text; $text = $this->formatLinks($text); @@ -102,35 +114,34 @@ function getContext(): string $text = nl2br($text); return $text; } - - function getTime(): DateTime + + public function getTime(): DateTime { return new DateTime($this->getRecord()->created); } - function isAd(): bool - { - return false; # Кооостыыыль!!! - } + public function isAd(): bool + { + return false; # Кооостыыыль!!! + } - function getMark(): ?int + public function getMark(): ?int { return $this->getRecord()->mark; } - function isLikedByUser(): ?bool + public function isLikedByUser(): ?bool { $mark = $this->getMark(); - if(is_null($mark)) - return NULL; - else + if (is_null($mark)) { + return null; + } else { return $mark === 1; + } } - function isDeleted(): bool + public function isDeleted(): bool { return (bool) $this->getRecord()->deleted; } - - use Traits\TRichText; } diff --git a/Web/Models/Entities/Topic.php b/Web/Models/Entities/Topic.php index d4257816e..30893b078 100644 --- a/Web/Models/Entities/Topic.php +++ b/Web/Models/Entities/Topic.php @@ -1,5 +1,9 @@ -isPostedOnBehalfOfGroup()) + if ($honourFlags && $this->isPostedOnBehalfOfGroup()) { return $this->getClub(); - + } + return parent::getOwner($real); } - function getClub(): Club + public function getClub(): Club { - return (new Clubs)->get($this->getRecord()->group); + return (new Clubs())->get($this->getRecord()->group); } - function getTitle(): string + public function getTitle(): string { return $this->getRecord()->title; } - function isClosed(): bool + public function isClosed(): bool { return (bool) $this->getRecord()->closed; } - function isPinned(): bool + public function isRestricted(): bool + { + return (bool) $this->getRecord()->restricted; + } + + public function isPinned(): bool { return (bool) $this->getRecord()->pinned; } - function getPrettyId(): string + public function getPrettyId(): string { return $this->getRecord()->group . "_" . $this->getVirtualId(); } - function isPostedOnBehalfOfGroup(): bool + public function isPostedOnBehalfOfGroup(): bool { return ($this->getRecord()->flags & 0b10000000) > 0; } - function isDeleted(): bool + public function isDeleted(): bool { return (bool) $this->getRecord()->deleted; } - function canBeModifiedBy(User $user): bool + public function canBeModifiedBy(User $user): bool { return $this->getOwner(false)->getId() === $user->getId() || $this->getClub()->canBeModifiedBy($user); } - function getLastComment(): ?Comment + public function getLastComment(): ?Comment { $array = iterator_to_array($this->getLastComments(1)); - return isset($array[0]) ? $array[0] : NULL; + return $array[0] ?? null; } - function getFirstComment(): ?Comment + public function getFirstComment(): ?Comment { $array = iterator_to_array($this->getComments(1)); - return $array[0] ?? NULL; + return $array[0] ?? null; } - function getUpdateTime(): DateTime + public function getUpdateTime(): DateTime { $lastComment = $this->getLastComment(); - if(!is_null($lastComment)) + if (!is_null($lastComment)) { return $lastComment->getPublicationTime(); - else + } else { return $this->getEditTime() ?? $this->getPublicationTime(); + } } - function deleteTopic(): void + public function deleteTopic(): void { $this->setDeleted(1); $this->unwire(); $this->save(); } - function toVkApiStruct(int $preview = 0, int $preview_length = 90): object + public function toVkApiStruct(int $preview = 0, int $preview_length = 90): object { - $res = (object)[]; + $res = (object) []; $res->id = $this->getId(); $res->title = $this->getTitle(); $res->created = $this->getPublicationTime()->timestamp(); - if($this->getOwner() instanceof User) { + if ($this->getOwner() instanceof User) { $res->created_by = $this->getOwner()->getId(); } else { $res->created_by = $this->getOwner()->getId() * -1; } - + $res->updated = $this->getUpdateTime()->timestamp(); - if($this->getLastComment()) { - if($this->getLastComment()->getOwner() instanceof User) { + if ($this->getLastComment()) { + if ($this->getLastComment()->getOwner() instanceof User) { $res->updated_by = $this->getLastComment()->getOwner()->getId(); } else { $res->updated_by = $this->getLastComment()->getOwner()->getId() * -1; } } - $res->is_closed = (int)$this->isClosed(); - $res->is_fixed = (int)$this->isPinned(); + $res->is_closed = (int) $this->isClosed(); + $res->is_fixed = (int) $this->isPinned(); $res->comments = $this->getCommentsCount(); - if($preview == 1) { - $res->first_comment = $this->getFirstComment() ? ovk_proc_strtr($this->getFirstComment()->getText(false), $preview_length) : NULL; - $res->last_comment = $this->getLastComment() ? ovk_proc_strtr($this->getLastComment()->getText(false), $preview_length) : NULL; + if ($preview == 1) { + $res->first_comment = $this->getFirstComment() ? ovk_proc_strtr($this->getFirstComment()->getText(false), $preview_length) : null; + $res->last_comment = $this->getLastComment() ? ovk_proc_strtr($this->getLastComment()->getText(false), $preview_length) : null; } return $res; diff --git a/Web/Models/Entities/Traits/TAttachmentHost.php b/Web/Models/Entities/Traits/TAttachmentHost.php index db814cce7..529fb65b1 100644 --- a/Web/Models/Entities/Traits/TAttachmentHost.php +++ b/Web/Models/Entities/Traits/TAttachmentHost.php @@ -1,6 +1,10 @@ - $attachment->getId(), ]; } - - function getChildren(): \Traversable + + public function getChildren(): \Traversable { $sel = DatabaseConnection::i()->getContext() ->table("attachments") ->where("target_id", $this->getId()) ->where("attachments.target_type", get_class($this)); - foreach($sel as $rel) { + foreach ($sel as $rel) { $repoName = $rel->attachable_type . "s"; $repoName = str_replace("Entities", "Repositories", $repoName); - $repo = new $repoName; - + $repo = new $repoName(); + yield $repo->get($rel->attachable_id); } } - function getChildrenWithLayout(int $w, int $h = -1): object + public function getChildrenWithLayout(int $w, int $h = -1): object { - if($h < 0) + if ($h < 0) { $h = $w; + } - $children = $this->getChildren(); + $children = iterator_to_array($this->getChildren()); $skipped = $photos = $result = []; - foreach($children as $child) { - if($child instanceof Photo) { + foreach ($children as $child) { + if ($child instanceof Photo || $child instanceof Video && $child->getDimensions()) { $photos[] = $child; continue; } @@ -49,15 +54,16 @@ function getChildrenWithLayout(int $w, int $h = -1): object $height = "unset"; $width = $w; - if(sizeof($photos) < 2) { - if(isset($photos[0])) + if (sizeof($photos) < 2) { + if (isset($photos[0])) { $result[] = ["100%", "unset", $photos[0], "unset"]; + } } else { $mak = new Makima($photos); $layout = $mak->computeMasonryLayout($w, $h); $height = $layout->height; $width = $layout->width; - for($i = 0; $i < sizeof($photos); $i++) { + for ($i = 0; $i < sizeof($photos); $i++) { $tile = $layout->tiles[$i]; $result[] = [$tile->width . "px", $tile->height . "px", $photos[$i], "left"]; } @@ -70,25 +76,25 @@ function getChildrenWithLayout(int $w, int $h = -1): object "extras" => $skipped, ]; } - - function attach(Attachable $attachment): void + + public function attach(Attachable $attachment): void { DatabaseConnection::i()->getContext() ->table("attachments") ->insert($this->composeAttachmentRequestData($attachment)); } - - function detach(Attachable $attachment): bool + + public function detach(Attachable $attachment): bool { $res = DatabaseConnection::i()->getContext() ->table("attachments") ->where($this->composeAttachmentRequestData($attachment)) ->delete(); - + return $res > 0; } - - function unwire(): void + + public function unwire(): void { $this->getRecord() ->related("attachments.target_id") diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php new file mode 100644 index 000000000..544e1e6ef --- /dev/null +++ b/Web/Models/Entities/Traits/TAudioStatuses.php @@ -0,0 +1,50 @@ +getRealId() < 0) { + return true; + } + return (bool) $this->getRecord()->audio_broadcast_enabled; + } + + public function getCurrentAudioStatus() + { + if (!$this->isBroadcastEnabled()) { + return null; + } + + $audioId = $this->getRecord()->last_played_track; + + if (!$audioId) { + return null; + } + $audio = (new Audios())->get($audioId); + + if (!$audio || $audio->isDeleted()) { + return null; + } + + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $this->getRealId(), + "audio" => $audio->getId(), + "time >" => (time() - $audio->getLength()) - 10, + ])->fetch(); + + if ($lastListen) { + return $audio; + } + + return null; + } +} diff --git a/Web/Models/Entities/Traits/TBackDrops.php b/Web/Models/Entities/Traits/TBackDrops.php index cab671386..c48c73fe6 100644 --- a/Web/Models/Entities/Traits/TBackDrops.php +++ b/Web/Models/Entities/Traits/TBackDrops.php @@ -1,44 +1,54 @@ -getRecord()->backdrop_1; $photo2 = $this->getRecord()->backdrop_2; - if(is_null($photo1) && is_null($photo2)) - return NULL; - - $photo1obj = $photo2obj = NULL; - if(!is_null($photo1)) - $photo1obj = (new Photos)->get($photo1); - if(!is_null($photo2)) - $photo2obj = (new Photos)->get($photo2); - - if(is_null($photo1obj) && is_null($photo2obj)) - return NULL; - + if (is_null($photo1) && is_null($photo2)) { + return null; + } + + $photo1obj = $photo2obj = null; + if (!is_null($photo1)) { + $photo1obj = (new Photos())->get($photo1); + } + if (!is_null($photo2)) { + $photo2obj = (new Photos())->get($photo2); + } + + if (is_null($photo1obj) && is_null($photo2obj)) { + return null; + } + return [ is_null($photo1obj) ? "" : $photo1obj->getURL(), is_null($photo2obj) ? "" : $photo2obj->getURL(), ]; } - - function setBackDropPictures(?Photo $first, ?Photo $second): void + + public function setBackDropPictures(?Photo $first, ?Photo $second): void { - if(!is_null($first)) + if (!is_null($first)) { $this->stateChanges("backdrop_1", $first->getId()); - - if(!is_null($second)) + } + + if (!is_null($second)) { $this->stateChanges("backdrop_2", $second->getId()); + } } - - function unsetBackDropPictures(): void + + public function unsetBackDropPictures(): void { - $this->stateChanges("backdrop_1", NULL); - $this->stateChanges("backdrop_2", NULL); + $this->stateChanges("backdrop_1", null); + $this->stateChanges("backdrop_2", null); } -} \ No newline at end of file +} diff --git a/Web/Models/Entities/Traits/TIgnorable.php b/Web/Models/Entities/Traits/TIgnorable.php new file mode 100644 index 000000000..0f7c3367c --- /dev/null +++ b/Web/Models/Entities/Traits/TIgnorable.php @@ -0,0 +1,60 @@ +getContext(); + $data = [ + "owner" => $user->getId(), + "source" => $this->getRealId(), + ]; + + $sub = $ctx->table("ignored_sources")->where($data); + return $sub->count('*') > 0; + } + + public function addIgnore(User $for_user): bool + { + DatabaseConnection::i()->getContext()->table("ignored_sources")->insert([ + "owner" => $for_user->getId(), + "source" => $this->getRealId(), + ]); + + return true; + } + + public function removeIgnore(User $for_user): bool + { + DatabaseConnection::i()->getContext()->table("ignored_sources")->where([ + "owner" => $for_user->getId(), + "source" => $this->getRealId(), + ])->delete(); + + return true; + } + + public function toggleIgnore(User $for_user): bool + { + if ($this->isIgnoredBy($for_user)) { + $this->removeIgnore($for_user); + + return false; + } else { + $this->addIgnore($for_user); + + return true; + } + } +} diff --git a/Web/Models/Entities/Traits/TOwnable.php b/Web/Models/Entities/Traits/TOwnable.php index 9dc9ce2a3..e4b27237c 100644 --- a/Web/Models/Entities/Traits/TOwnable.php +++ b/Web/Models/Entities/Traits/TOwnable.php @@ -1,18 +1,35 @@ -isDeleted()) { + return false; + } + + return true; + } + + public function canBeModifiedBy(User $user): bool { - if(method_exists($this, "isCreatedBySystem")) - if($this->isCreatedBySystem()) + if (method_exists($this, "isCreatedBySystem")) { + if ($this->isCreatedBySystem()) { return false; - - if($this->getRecord()->owner > 0) + } + } + + if ($this->getRecord()->owner > 0) { return $this->getRecord()->owner === $user->getId(); - else + } else { return $this->getOwner()->canBeModifiedBy($user); + } } } diff --git a/Web/Models/Entities/Traits/TRichText.php b/Web/Models/Entities/Traits/TRichText.php index dc78a0345..5bb46c9b7 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -1,5 +1,9 @@ -overrideContentColumn : "content"; - if(iconv_strlen($this->getRecord()->{$contentColumn}) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["emojiProcessingLimit"]) + if (iconv_strlen($this->getRecord()->{$contentColumn}) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["emojiProcessingLimit"]) { return $text; - + } + $emojis = \Emoji\detect_emoji($text); $replaced = []; # OVK-113 - foreach($emojis as $emoji) { + foreach ($emojis as $emoji) { $point = explode("-", strtolower($emoji["hex_str"]))[0]; - if(in_array($point, $replaced)) + if (in_array($point, $replaced)) { continue; - else + } else { $replaced[] = $point; - + } + $image = "https://abs.twimg.com/emoji/v2/72x72/$point.png"; $image = "$emoji[emoji] to be spawned $rel = $this->isAd() ? "sponsored" : "ugc"; - - return "$link" . htmlentities($matches[4]); + + return "$link" . htmlentities($matches[4]); }), $text ); } - + private function removeZalgo(string $text): string { return preg_replace("%\p{M}{3,}%Xu", "", $text); } - - function resolveMentions(array $skipUsers = []): \Traversable + + public function resolveMentions(array $skipUsers = []): \Traversable { $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; $text = $this->getRecord()->{$contentColumn}; $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%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); - + $resolvedUsers = $skipUsers; $resolvedClubs = []; preg_match_all("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", $text, $links, PREG_PATTERN_ORDER); - foreach($links[1] as $link) { - if(preg_match("%^id([0-9]++)$%", $link, $match)) { + foreach ($links[1] as $link) { + if (preg_match("%^id([0-9]++)$%", $link, $match)) { $uid = (int) $match[1]; - if(in_array($uid, $resolvedUsers)) + if (in_array($uid, $resolvedUsers)) { continue; - + } + $resolvedUsers[] = $uid; - $maybeUser = (new Users)->get($uid); - if($maybeUser) + $maybeUser = (new Users())->get($uid); + if ($maybeUser) { yield $maybeUser; - } else if(preg_match("%^(?:club|public|event)([0-9]++)$%", $link, $match)) { + } + } elseif (preg_match("%^(?:club|public|event)([0-9]++)$%", $link, $match)) { $cid = (int) $match[1]; - if(in_array($cid, $resolvedClubs)) + if (in_array($cid, $resolvedClubs)) { continue; - + } + $resolvedClubs[] = $cid; - $maybeClub = (new Clubs)->get($cid); - if($maybeClub) + $maybeClub = (new Clubs())->get($cid); + if ($maybeClub) { yield $maybeClub; + } } else { - $maybeUser = (new Users)->getByShortURL($link); - if($maybeUser) { + $maybeUser = (new Users())->getByShortURL($link); + if ($maybeUser) { $uid = $maybeUser->getId(); - if(in_array($uid, $resolvedUsers)) + if (in_array($uid, $resolvedUsers)) { continue; - else + } else { $resolvedUsers[] = $uid; - + } + yield $maybeUser; continue; } - - $maybeClub = (new Clubs)->getByShortURL($link); - if($maybeClub) { + + $maybeClub = (new Clubs())->getByShortURL($link); + if ($maybeClub) { $cid = $maybeClub->getId(); - if(in_array($cid, $resolvedClubs)) + if (in_array($cid, $resolvedClubs)) { continue; - else + } else { $resolvedClubs[] = $cid; - + } + yield $maybeClub; } } } } - - function getText(bool $html = true): string + + public function getText(bool $html = true): string { $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; - + $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) { + if ($html) { + if ($proc) { $text = $this->formatLinks($text); + // Mentions: @user, @user (name), [id1|name] $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%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&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", "$2", $text); - $text = preg_replace_callback("%([\n\r\s]|^)(\#([\p{L}_0-9][\p{L}_0-9\(\)\-\']+[\p{L}_0-9\(\)]|[\p{L}_0-9]{1,2}))%Xu", function($m) { + $text = preg_replace("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9\.\-\`\'@]\p{Mn}?)++)\]%Xu", "$2", $text); + $text = preg_replace_callback("%([\n\r\s]|^)(\#([\p{L}_0-9][\p{L}_0-9\(\)\-\']+[\p{L}_0-9\(\)]|[\p{L}_0-9]{1,2}))%Xu", function ($m) { $slug = rawurlencode($m[3]); - - return "$m[1]$m[2]"; + + return "$m[1]$m[2]"; }, $text); - + $text = $this->formatEmojis($text); } - + $text = $this->removeZalgo($text); $text = nl2br($text); } else { - $text = str_replace("\r\n","\n", $text); + $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%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 = str_replace("\r\n", "\n", $text); } - - if(OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["christian"]) + + if (OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["christian"]) { ObsceneCensorRus::filterText($text); - + } + return $text; } } diff --git a/Web/Models/Entities/Traits/TSubscribable.php b/Web/Models/Entities/Traits/TSubscribable.php index 802bc4272..2e3f3bcdf 100644 --- a/Web/Models/Entities/Traits/TSubscribable.php +++ b/Web/Models/Entities/Traits/TSubscribable.php @@ -1,5 +1,9 @@ - static::class, "target" => $this->getId(), ]); - + foreach($subs as $sub) { $sub = (new Users)->get($sub->follower); if(!$sub) continue; - + yield $sub; } }*/ - - function toggleSubscription(User $user): bool + + public function toggleSubscription(User $user): bool { $ctx = DatabaseConnection::i()->getContext(); $data = [ @@ -30,13 +34,35 @@ function toggleSubscription(User $user): bool "target" => $this->getId(), ]; $sub = $ctx->table("subscriptions")->where($data); - - if(!($sub->fetch())) { + if (!($sub->fetch())) { $ctx->table("subscriptions")->insert($data); + return true; } - + $sub->delete(); return false; } + + public function changeFlags(User $user, int $flags, bool $reverse): bool + { + $ctx = DatabaseConnection::i()->getContext(); + $data = [ + "follower" => $reverse ? $this->getId() : $user->getId(), + "model" => static::class, + "target" => $reverse ? $user->getId() : $this->getId(), + ]; + $sub = $ctx->table("subscriptions")->where($data); + + bdump($data); + + if (!$sub) { + return false; + } + + $sub->update([ + 'flags' => $flags, + ]); + return true; + } } diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index dc227039a..552231f80 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1,247 +1,306 @@ -getId(); $query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); - $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit ); - + $query .= "\n LIMIT " . $limit . " OFFSET " . (($page - 1) * $limit); + $ids = []; $rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id); - foreach($rels as $rel) { - $rel = (new Users)->get($rel->id); - if(!$rel) continue; - if(in_array($rel->getId(), $ids)) continue; + foreach ($rels as $rel) { + $rel = (new Users())->get($rel->id); + if (!$rel) { + continue; + } + if (in_array($rel->getId(), $ids)) { + continue; + } $ids[] = $rel->getId(); yield $rel; } } - + protected function _abstractRelationCount(string $filename): int { $id = $this->getId(); $query = "SELECT COUNT(*) AS cnt FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); - + return (int) DatabaseConnection::i()->getConnection()->query($query, $id, $id)->fetch()->cnt; } - - function getId(): int + + public function getId(): int { return $this->getRecord()->id; } - - function getStyle(): string + + public function getStyle(): string { return $this->getRecord()->style; } - - function getTheme(): ?Themepack + + public function getTheme(): ?Themepack { - return Themepacks::i()[$this->getStyle()] ?? NULL; + return Themepacks::i()[$this->getStyle()] ?? null; } - - function getStyleAvatar(): int + + public function getStyleAvatar(): int { return $this->getRecord()->style_avatar; } - - function hasMilkshakeEnabled(): bool + + public function hasMilkshakeEnabled(): bool { return (bool) $this->getRecord()->milkshake; } - function hasMicroblogEnabled(): bool + public function hasMicroblogEnabled(): bool { return (bool) $this->getRecord()->microblog; } - function getMainPage(): int + public function getMainPage(): int { return $this->getRecord()->main_page; } - - function getChandlerGUID(): string + + public function getChandlerGUID(): string { return $this->getRecord()->user; } - - function getChandlerUser(): ChandlerUser + + public function getChandlerUser(): ChandlerUser { return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user")); } - - function getURL(): string + + public function getURL(bool $trimBackslash = false): string { - if(!is_null($this->getShortCode())) - return "/" . $this->getShortCode(); - else - return "/id" . $this->getId(); + $backslash = $trimBackslash ? '' : '/'; + if (!is_null($this->getShortCode())) { + return $backslash . $this->getShortCode(); + } + + return $backslash . "id" . $this->getId(); } - - function getAvatarUrl(string $size = "miniscule"): string + + public function getAvatarUrl(string $size = "miniscule", $avPhoto = null): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - - if($this->getRecord()->deleted) + + if ($this->getRecord()->deleted) { return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; - else if($this->isBanned()) + } elseif ($this->isBanned()) { return "$serverUrl/assets/packages/static/openvk/img/banned.jpg"; - - $avPhoto = $this->getAvatarPhoto(); - if(is_null($avPhoto)) + } + + if (!$avPhoto) { + $avPhoto = $this->getAvatarPhoto(); + } + + if (is_null($avPhoto)) { return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; - else + } else { return $avPhoto->getURLBySizeId($size); + } } - - function getAvatarLink(): string + + public function getAvatarLink(): string { $avPhoto = $this->getAvatarPhoto(); - if(!$avPhoto) return "javascript:void(0)"; - + if (!$avPhoto) { + return "javascript:void(0)"; + } + $pid = $avPhoto->getPrettyId(); - $aid = (new Albums)->getUserAvatarAlbum($this)->getId(); - + $aid = $this->getAvatarAlbum()->getId(); + return "/photo$pid?from=album$aid"; } - - function getAvatarPhoto(): ?Photo + + public function getAvatarAlbum(): ?Album + { + return $this->_avatarAlbum ??= (new Albums())->getUserAvatarAlbum($this); + } + + public function getAvatarPhoto(): ?Photo { - $avAlbum = (new Albums)->getUserAvatarAlbum($this); + if ($this->_avatarPhoto !== false) { + return $this->_avatarPhoto; + } + + $avAlbum = $this->getAvatarAlbum(); $avCount = $avAlbum->getPhotosCount(); $avPhotos = $avAlbum->getPhotos($avCount, 1); - - return iterator_to_array($avPhotos)[0] ?? NULL; + + return $this->_avatarPhoto = iterator_to_array($avPhotos)[0] ?? null; } - - function getFirstName(bool $pristine = false): string + + public function getFirstName(bool $pristine = false): string { $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE)); - $tsn = tr("__transNames"); - if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine) + $tsn = tr("__transNames"); + if (($tsn !== "@__transNames" && !empty($tsn)) && !$pristine) { return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE); - else + } else { return $name; + } } - - function getLastName(bool $pristine = false): string + + public function getLastName(bool $pristine = false): string { $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE)); - $tsn = tr("__transNames"); - if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine) + $tsn = tr("__transNames"); + if (($tsn !== "@__transNames" && !empty($tsn)) && !$pristine) { return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE); - else + } else { return $name; + } } - - function getPseudo(): ?string + + public function getPseudo(): ?string { return ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : $this->getRecord()->pseudo); } - - function getFullName(): string + + public function getFullName(): string { - if($this->isDeleted() && !$this->isDeactivated()) + if ($this->isDeleted() && !$this->isDeactivated()) { return "DELETED"; - + } + $pseudo = $this->getPseudo(); - if(!$pseudo) + if (!$pseudo) { $pseudo = " "; - else + } else { $pseudo = " ($pseudo) "; - - return $this->getFirstName() . $pseudo . $this->getLastName(); + } + + $fullName = $this->getFirstName() . $pseudo . $this->getLastName(); + + return strip_tags($fullName); } - function getMorphedName(string $case = "genitive", bool $fullName = true): string + public function getMorphedName(string $case = "genitive", bool $fullName = true, bool $startWithLastName = true): string { - $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); - if(!preg_match("%^[А-яё\-]+$%", $name)) - return $name; # name is probably not russian + if ($fullName) { + if ($startWithLastName) { + $name = $this->getLastName() . " " . $this->getFirstName(); + } else { + $name = $this->getFirstName() . " " . $this->getLastName(); + } + } elseif ($startWithLastName == false) { + $name = $this->getFirstName(); + } else { + $name = $this->getLastName(); + } + + if (!preg_match("/^[А-Яа-яЁё\s-]+$/u", $name)) { + return $name; + } # name is probably not russian $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); return $inflected ?: $name; } - - function getCanonicalName(): string + + public function getCanonicalName(): string { - if($this->isDeleted() && !$this->isDeactivated()) + if ($this->isDeleted() && !$this->isDeactivated()) { return "DELETED"; - else + } else { return $this->getFirstName() . " " . $this->getLastName(); + } } - function getPhone(): ?string + public function getPhone(): ?string { return $this->getRecord()->phone; } - function getEmail(): ?string + public function getEmail(): ?string { return $this->getRecord()->email; } - function getOnline(): DateTime + public function getOnline(): DateTime { return new DateTime($this->getRecord()->online); } - function getDescription(): ?string + public function getDescription(): ?string { return $this->getRecord()->about; } - function getStatus(): ?string + public function getAbout(): ?string + { + return $this->getRecord()->about; + } + + public function getStatus(): ?string { return $this->getRecord()->status; } - function getShortCode(): ?string + public function getShortCode(): ?string { return $this->getRecord()->shortcode; } - function getAlert(): ?string + public function getAlert(): ?string { return $this->getRecord()->alert; } - function getTextForContentBan(string $type): string + public function getTextForContentBan(string $type): string { switch ($type) { case "post": return "за размещение от Вашего лица таких записей:"; @@ -255,37 +314,48 @@ function getTextForContentBan(string $type): string } } - function getRawBanReason(): ?string + public function getRawBanReason(): ?string { return $this->getRecord()->block_reason; } - function getBanReason(?string $for = null) + public function getBanReason(?string $for = null) { - $ban = (new Bans)->get((int) $this->getRecord()->block_reason); - if (!$ban || $ban->isOver()) return null; + $ban = (new Bans())->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver()) { + return null; + } $reason = $ban->getReason(); preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate|user)-(\d+)\*\*$/', $reason, $matches); if (sizeof($matches) === 3) { - $content_type = $matches[1]; $content_id = (int) $matches[2]; + $content_type = $matches[1]; + $content_id = (int) $matches[2]; if (in_array($content_type, ["noSpamTemplate", "user"])) { - $reason = "Подозрительная активность"; + $reason = $this->getRawBanReason(); } else { if ($for !== "banned") { - $reason = "Подозрительная активность"; + $reason = $this->getRawBanReason(); } else { $reason = [$this->getTextForContentBan($content_type), $content_type]; switch ($content_type) { - case "post": $reason[] = (new Posts)->get($content_id); break; - case "photo": $reason[] = (new Photos)->get($content_id); break; - case "video": $reason[] = (new Videos)->get($content_id); break; - case "group": $reason[] = (new Clubs)->get($content_id); break; - case "comment": $reason[] = (new Comments)->get($content_id); break; - case "note": $reason[] = (new Notes)->get($content_id); break; - case "app": $reason[] = (new Applications)->get($content_id); break; - case "user": $reason[] = (new Users)->get($content_id); break; + case "post": $reason[] = (new Posts())->get($content_id); + break; + case "photo": $reason[] = (new Photos())->get($content_id); + break; + case "video": $reason[] = (new Videos())->get($content_id); + break; + case "group": $reason[] = (new Clubs())->get($content_id); + break; + case "comment": $reason[] = (new Comments())->get($content_id); + break; + case "note": $reason[] = (new Notes())->get($content_id); + break; + case "app": $reason[] = (new Applications())->get($content_id); + break; + case "user": $reason[] = (new Users())->get($content_id); + break; default: $reason[] = null; } } @@ -295,166 +365,220 @@ function getBanReason(?string $for = null) return $reason; } - function getBanInSupportReason(): ?string + public function getBanInSupportReason(): ?string { return $this->getRecord()->block_in_support_reason; } - function getType(): int + public function getType(): int { return $this->getRecord()->type; } - function getCoins(): float + public function getCoins(): float { - if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { return 0.0; + } return $this->getRecord()->coins; } - function getRating(): int + public function getRating(): int { return OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"] ? $this->getRecord()->rating : 0; } - function getReputation(): int + public function getReputation(): int { return $this->getRecord()->reputation; } - function getRegistrationTime(): DateTime + public function getRegistrationTime(): DateTime { return new DateTime($this->getRecord()->since->getTimestamp()); } - function getRegistrationIP(): string + public function getRegistrationIP(): string { return $this->getRecord()->registering_ip; } - function getHometown(): ?string + public function getHometown(): ?string { return $this->getRecord()->hometown; } - function getPoliticalViews(): int + public function getPoliticalViews(): int { return $this->getRecord()->polit_views; } - function getMaritalStatus(): int + public function getMaritalStatus(): int { return $this->getRecord()->marital_status; } - - function getLocalizedMaritalStatus(): string + + public function getLocalizedMaritalStatus(?bool $prefix = false): string { $status = $this->getMaritalStatus(); $string = "relationship_$status"; - if($this->isFemale()) { + if ($prefix) { + $string .= "_prefix"; + } + if ($this->isFemale()) { $res = tr($string . "_fem"); - if($res != ("@" . $string . "_fem")) - return $res; # If fem version exists, return + if ($res != ("@" . $string . "_fem")) { + return $res; + } # If fem version exists, return } - + return tr($string); } - function getContactEmail(): ?string + public function getMaritalStatusUser(): ?User + { + if (!$this->getRecord()->marital_status_user) { + return null; + } + return (new Users())->get($this->getRecord()->marital_status_user); + } + + public function getMaritalStatusUserPrefix(): ?string + { + return $this->getLocalizedMaritalStatus(true); + } + + public function getContactEmail(): ?string { return $this->getRecord()->email_contact; } - function getTelegram(): ?string + public function getTelegram(): ?string { return $this->getRecord()->telegram; } - function getInterests(): ?string + public function getInterests(): ?string { return $this->getRecord()->interests; } - function getFavoriteMusic(): ?string + public function getFavoriteMusic(): ?string { return $this->getRecord()->fav_music; } - function getFavoriteFilms(): ?string + public function getFavoriteFilms(): ?string { return $this->getRecord()->fav_films; } - function getFavoriteShows(): ?string + public function getFavoriteShows(): ?string { return $this->getRecord()->fav_shows; } - function getFavoriteBooks(): ?string + public function getFavoriteBooks(): ?string { return $this->getRecord()->fav_books; } - function getFavoriteQuote(): ?string + public function getFavoriteQuote(): ?string { return $this->getRecord()->fav_quote; } - function getCity(): ?string + public function getFavoriteGames(): ?string + { + return $this->getRecord()->fav_games; + } + + public function getCity(): ?string { return $this->getRecord()->city; } - function getPhysicalAddress(): ?string + public function getPhysicalAddress(): ?string { return $this->getRecord()->address; } - function getNotificationOffset(): int + public function getAdditionalFields(bool $split = false): array + { + $all = \openvk\Web\Models\Entities\UserInfoEntities\AdditionalField::getByOwner($this->getId()); + $result = [ + "interests" => [], + "contacts" => [], + ]; + + if ($split) { + foreach ($all as $field) { + if ($field->getPlace() == "contact") { + $result["contacts"][] = $field; + } elseif ($field->getPlace() == "interest") { + $result["interests"][] = $field; + } + } + } else { + $result = []; + foreach ($all as $field) { + $result[] = $field; + } + } + + return $result; + } + + public function getNotificationOffset(): int { return $this->getRecord()->notification_offset; } - function getBirthday(): ?DateTime + public function getBirthday(): ?DateTime { - if(is_null($this->getRecord()->birthday)) - return NULL; - else + if (is_null($this->getRecord()->birthday)) { + return null; + } else { return new DateTime($this->getRecord()->birthday); + } } - function getBirthdayPrivacy(): int + public function getBirthdayPrivacy(): int { return $this->getRecord()->birthday_privacy; } - function getAge(): ?int + 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; } - function get2faSecret(): ?string + public function get2faSecret(): ?string { return $this->getRecord()["2fa_secret"]; } - function is2faEnabled(): bool + public function is2faEnabled(): bool { return !is_null($this->get2faSecret()); } - function updateNotificationOffset(): void + public function updateNotificationOffset(): void { $this->stateChanges("notification_offset", time()); } - function getLeftMenuItemStatus(string $id): bool + public function getLeftMenuItemStatus(string $id): bool { return (bool) bmask($this->getRecord()->left_menu, [ "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -462,12 +586,14 @@ function getLeftMenuItemStatus(string $id): bool "news", "links", "poster", - "apps" + "apps", + "docs", + "fave", ], ])->get($id); } - function getPrivacySetting(string $id): int + public function getPrivacySetting(string $id): int { return (int) bmask($this->getRecord()->privacy, [ "length" => 2, @@ -482,19 +608,26 @@ function getPrivacySetting(string $id): int "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ], ])->get($id); } - function getPrivacyPermission(string $permission, ?User $user = NULL): bool + public function getPrivacyPermission(string $permission, ?User $user = null): bool { $permStatus = $this->getPrivacySetting($permission); - if(!$user) + if (!$user) { return $permStatus === User::PRIVACY_EVERYONE; - else if($user->getId() === $this->getId()) + } elseif ($user->getId() === $this->getId()) { return true; + } + + if (/*$permission != "messages.write" && */!$this->canBeViewedBy($user, true)) { + return false; + } - switch($permStatus) { + switch ($permStatus) { case User::PRIVACY_ONLY_FRIENDS: return $this->getSubscriptionStatus($user) === User::SUBSCRIPTION_MUTUAL; case User::PRIVACY_ONLY_REGISTERED: @@ -505,38 +638,40 @@ function getPrivacyPermission(string $permission, ?User $user = NULL): bool } } - function getProfileCompletenessReport(): object + public function getProfileCompletenessReport(): object { $incompleteness = 0; $unfilled = []; - if(!$this->getRecord()->status) { + if (!$this->getRecord()->status) { $unfilled[] = "status"; $incompleteness += 15; } - if(!$this->getRecord()->telegram) { + if (!$this->getRecord()->telegram) { $unfilled[] = "telegram"; $incompleteness += 15; } - if(!$this->getRecord()->email) { + if (!$this->getRecord()->email) { $unfilled[] = "email"; $incompleteness += 20; } - if(!$this->getRecord()->city) { + if (!$this->getRecord()->city) { $unfilled[] = "city"; $incompleteness += 20; } - if(!$this->getRecord()->interests) { + if (!$this->getRecord()->interests) { $unfilled[] = "interests"; $incompleteness += 20; } $total = max(100 - $incompleteness + $this->getRating(), 0); - if(ovkGetQuirk("profile.rating-bar-behaviour") === 0) - if ($total >= 100) - $percent = round(($total / 10**strlen(strval($total))) * 100, 0); - else - $percent = min($total, 100); + if (ovkGetQuirk("profile.rating-bar-behaviour") === 0) { + if ($total >= 100) { + $percent = round(($total / 10 ** strlen(strval($total))) * 100, 0); + } else { + $percent = min($total, 100); + } + } return (object) [ "total" => $total, @@ -545,82 +680,116 @@ function getProfileCompletenessReport(): object ]; } - function getFriends(int $page = 1, int $limit = 6): \Traversable + public function getFriends(int $page = 1, int $limit = 6): \Traversable { return $this->_abstractRelationGenerator("get-friends", $page, $limit); } - function getFriendsCount(): int + public function getFriendsCount(): int { return $this->_abstractRelationCount("get-friends"); } - function getFriendsOnline(int $page = 1, int $limit = 6): \Traversable + public function getFriendsOnline(int $page = 1, int $limit = 6): \Traversable { return $this->_abstractRelationGenerator("get-online-friends", $page, $limit); } - function getFriendsOnlineCount(): int + public function getFriendsOnlineCount(): int { return $this->_abstractRelationCount("get-online-friends"); } - function getFollowers(int $page = 1, int $limit = 6): \Traversable + public function getFriendsBday(bool $today): array + { + $users = $this->_abstractRelationGenerator($today ? "get-bday-today" : "get-bday-tomorrow", 1, 3000); + $usersFiltered = []; + foreach ($users as $u) { + if ($u->getPrivacySetting("page.info.read") != 0) { + $usersFiltered[] = $u; + } + } + + if (sizeof($usersFiltered) > 0) { + return [ + "isToday" => $today, + "users" => $usersFiltered, + ]; + } + return []; + } + + public function getFollowers(int $page = 1, int $limit = 6): \Traversable { return $this->_abstractRelationGenerator("get-followers", $page, $limit); } - function getFollowersCount(): int + public function getFollowersCount(): int { return $this->_abstractRelationCount("get-followers"); } - function getSubscriptions(int $page = 1, int $limit = 6): \Traversable + public function getRequests(int $page = 1, int $limit = 6): \Traversable + { + return $this->_abstractRelationGenerator("get-requests", $page, $limit); + } + + public function getRequestsCount(): int + { + return $this->_abstractRelationCount("get-requests"); + } + + public function getSubscriptions(int $page = 1, int $limit = 6): \Traversable { return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit); } - function getSubscriptionsCount(): int + public function getSubscriptionsCount(): int { return $this->_abstractRelationCount("get-subscriptions-user"); } - function getUnreadMessagesCount(): int + public function getUnreadMessagesCount(): int { return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); } - function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable + public function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable { - if(!$offset) + if (!$offset) { $page = ($page - 1) * $count; + } - if($admin) { + if ($admin) { $id = $this->getId(); $query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?"; $query .= " LIMIT " . $count . " OFFSET " . $page; $sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id); - foreach($sel as $target) { - $target = (new Clubs)->get($target->id); - if(!$target) continue; + foreach ($sel as $target) { + $target = (new Clubs())->get($target->id); + if (!$target) { + continue; + } yield $target; } } else { $sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page); - foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { - $target = (new Clubs)->get($target->target); - if(!$target) continue; + foreach ($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { + $target = (new Clubs())->get($target->target); + if (!$target) { + continue; + } yield $target; } } } - function getClubCount(bool $admin = false): int + public function getClubCount(bool $admin = false): int { - if($admin) { + if ($admin) { $id = $this->getId(); $query = "SELECT COUNT(*) AS `cnt` FROM (SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?) u0;"; @@ -633,63 +802,72 @@ function getClubCount(bool $admin = false): int } } - function getPinnedClubs(): \Traversable + public function getPinnedClubs(): \Traversable { - foreach($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) { - $target = (new Clubs)->get($target->id); - if(!$target) continue; + foreach ($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) { + $target = (new Clubs())->get($target->id); + if (!$target) { + continue; + } yield $target; } - foreach($this->getRecord()->related("group_coadmins.user")->where("club_pinned", true) as $target) { - $target = (new Clubs)->get($target->club); - if(!$target) continue; + foreach ($this->getRecord()->related("group_coadmins.user")->where("club_pinned", true) as $target) { + $target = (new Clubs())->get($target->club); + if (!$target) { + continue; + } yield $target; } } - function getPinnedClubCount(): int + public function getPinnedClubCount(): int { return sizeof($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true)) + sizeof($this->getRecord()->related("group_coadmins.user")->where("club_pinned", true)); } - function isClubPinned(Club $club): bool + public function isClubPinned(Club $club): bool { - if($club->getOwner()->getId() === $this->getId()) + if ($club->getOwner()->getId() === $this->getId()) { return $club->isOwnerClubPinned(); + } $manager = $club->getManager($this); - if(!is_null($manager)) + if (!is_null($manager)) { return $manager->isClubPinned(); + } return false; } - function getMeetings(int $page = 1): \Traversable + public function getMeetings(int $page = 1): \Traversable { $sel = $this->getRecord()->related("event_turnouts.user")->page($page, OPENVK_DEFAULT_PER_PAGE); - foreach($sel as $target) { - $target = (new Clubs)->get($target->event); - if(!$target) continue; + foreach ($sel as $target) { + $target = (new Clubs())->get($target->event); + if (!$target) { + continue; + } yield $target; } } - function getMeetingCount(): int + public function getMeetingCount(): int { return sizeof($this->getRecord()->related("event_turnouts.user")); } - function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable + public function getGifts(int $page = 1, ?int $perPage = null): \Traversable { $gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); - foreach($gifts as $rel) { + foreach ($gifts as $rel) { yield (object) [ - "sender" => (new Users)->get($rel->sender), - "gift" => (new Gifts)->get($rel->gift), + "id" => $rel->id, + "sender" => (new Users())->get($rel->sender), + "gift" => (new Gifts())->get($rel->gift), "caption" => $rel->comment, "anon" => $rel->anonymous, "sent" => new DateTime($rel->sent), @@ -697,44 +875,46 @@ function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable } } - function getGiftCount(): int + public function getGiftCount(): int { - return sizeof($this->getRecord()->related("gift_user_relations.receiver")); + return $this->getRecord()->related("gift_user_relations.receiver")->count("*"); } - function get2faBackupCodes(): \Traversable + public function get2faBackupCodes(): \Traversable { $sel = $this->getRecord()->related("2fa_backup_codes.owner"); - foreach($sel as $target) + foreach ($sel as $target) { yield $target->code; + } } - function get2faBackupCodeCount(): int + public function get2faBackupCodeCount(): int { return sizeof($this->getRecord()->related("2fa_backup_codes.owner")); } - function generate2faBackupCodes(): void + public function generate2faBackupCodes(): void { $codes = []; - for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { + for ($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { $codes[] = [ "owner" => $this->getId(), - "code" => random_int(10000000, 99999999) + "code" => random_int(10000000, 99999999), ]; } - if(sizeof($codes) > 0) + if (sizeof($codes) > 0) { DatabaseConnection::i()->getContext()->table("2fa_backup_codes")->insert($codes); + } } - function use2faBackupCode(int $code): bool + public function use2faBackupCode(int $code): bool { return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete(); } - function getSubscriptionStatus(User $user): int + public function getSubscriptionStatus(User $user): int { $subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([ "model" => static::class, @@ -745,89 +925,133 @@ function getSubscriptionStatus(User $user): int "follower" => $user->getId(), ])->fetch()); - if($subbed && $followed) return User::SUBSCRIPTION_MUTUAL; - if($subbed) return User::SUBSCRIPTION_INCOMING; - if($followed) return User::SUBSCRIPTION_OUTGOING; + if ($subbed && $followed) { + return User::SUBSCRIPTION_MUTUAL; + } + if ($subbed) { + return User::SUBSCRIPTION_INCOMING; + } + if ($followed) { + return User::SUBSCRIPTION_OUTGOING; + } return User::SUBSCRIPTION_ABSENT; } - function getNotificationsCount(bool $archived = false): int + public function getNotificationsCount(bool $archived = false): int { - return (new Notifications)->getNotificationCountByUser($this, $this->getNotificationOffset(), $archived); + return (new Notifications())->getNotificationCountByUser($this, $this->getNotificationOffset(), $archived); } - function getNotifications(int $page, bool $archived = false): \Traversable + public function getNotifications(int $page, bool $archived = false): \Traversable { - return (new Notifications)->getNotificationsByUser($this, $this->getNotificationOffset(), $archived, $page); + return (new Notifications())->getNotificationsByUser($this, $this->getNotificationOffset(), $archived, $page); } - function getPendingPhoneVerification(): ?ActiveRow + public function getPendingPhoneVerification(): ?ActiveRow { return $this->getRecord()->ref("number_verification", "id"); } - function getRefLinkId(): string + public function getRefLinkId(): string { $hash = hash_hmac("Snefru", (string) $this->getId(), CHANDLER_ROOT_CONF["security"]["secret"], true); return dechex($this->getId()) . " " . base64_encode($hash); } - function getNsfwTolerance(): int + public function getNsfwTolerance(): int { return $this->getRecord()->nsfw_tolerance; } - function isFemale(): bool + public function isFemale(): bool + { + return $this->getRecord()->sex == 1; + } + + public function isNeutral(): bool + { + return (bool) $this->getRecord()->sex == 2; + } + + public function getLocalizedPronouns(): string { - return (bool) $this->getRecord()->sex; + switch ($this->getRecord()->sex) { + case 0: + return tr('male'); + case 1: + return tr('female'); + case 2: + default: + return tr('neutral'); + } + } + + public function getPronouns(): int + { + return $this->getRecord()->sex; } - function isVerified(): bool + public function isVerified(): bool { + if ($this->isDeleted() && !$this->isDeactivated()) { + if ($this->getRecord()->verified) { + $this->setVerified(0); + $this->save(); + } + + return false; + } + return (bool) $this->getRecord()->verified; } - function isBanned(): bool + public function isBanned(): bool { return !is_null($this->getBanReason()); } - function isBannedInSupport(): bool + public function isBannedInSupport(): bool { return !is_null($this->getBanInSupportReason()); } - function isOnline(): bool + public function isOnline(): bool { return time() - $this->getRecord()->online <= 300; } - function getOnlinePlatform(bool $forAPI = false): ?string + public function getOnlinePlatform(bool $forAPI = false): ?string { $platform = $this->getRecord()->client_name; - if($forAPI) { + if ($forAPI) { switch ($platform) { + case 'openvk_native': + case 'openvk_flux_android': case 'openvk_refresh_android': case 'openvk_legacy_android': + case 'Kate Mobile': + case 'VK for Android': return 'android'; break; + case 'openvk_native_ios': case 'openvk_ios': case 'openvk_legacy_ios': + case 'VK for iOS': return 'iphone'; break; - + case 'vika_touch': // кика хохотач ахахахаххахахахахах case 'vk4me': return 'mobile'; break; - case NULL: - return NULL; + case null: + return null; break; - + default: return 'api'; break; @@ -837,17 +1061,17 @@ function getOnlinePlatform(bool $forAPI = false): ?string } } - function getOnlinePlatformDetails(): array + public function getOnlinePlatformDetails(): array { $clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml"); - foreach($clients as $client) { - if($client['tag'] == $this->getOnlinePlatform()) { + foreach ($clients as $client) { + if ($client['tag'] == $this->getOnlinePlatform()) { return [ "tag" => $client['tag'], "name" => $client['name'], "url" => $client['url'], - "img" => $client['img'] + "img" => $client['img'], ]; break; } @@ -855,23 +1079,23 @@ function getOnlinePlatformDetails(): array return [ "tag" => $this->getOnlinePlatform(), - "name" => NULL, - "url" => NULL, - "img" => NULL + "name" => null, + "url" => null, + "img" => null, ]; } - function prefersNotToSeeRating(): bool + public function prefersNotToSeeRating(): bool { return !((bool) $this->getRecord()->show_rating); } - function hasPendingNumberChange(): bool + public function hasPendingNumberChange(): bool { return !is_null($this->getPendingPhoneVerification()); } - function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void + public function gift(User $sender, Gift $gift, ?string $comment = null, bool $anonymous = false): void { DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([ "sender" => $sender->getId(), @@ -883,9 +1107,9 @@ function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous ]); } - function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NULL, ?int $initiator = NULL): void + public function ban(string $reason, bool $deleteSubscriptions = false, $unban_time = null, ?int $initiator = null): void { - if($deleteSubscriptions) { + if ($deleteSubscriptions) { $subs = DatabaseConnection::i()->getContext()->table("subscriptions"); $subs = $subs->where( "follower = ? OR (target = ? AND model = ?)", @@ -897,7 +1121,7 @@ function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NUL } $iat = time(); - $ban = new Ban; + $ban = new Ban(); $ban->setUser($this->getId()); $ban->setReason($reason); $ban->setInitiator($initiator); @@ -911,22 +1135,23 @@ function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NUL $this->save(); } - function unban(int $removed_by): void + public function unban(int $removed_by): void { - $ban = (new Bans)->get((int) $this->getRawBanReason()); - if (!$ban || $ban->isOver()) + $ban = (new Bans())->get((int) $this->getRawBanReason()); + if (!$ban || $ban->isOver()) { return; + } $ban->setRemoved_Manually(true); $ban->setRemoved_By($removed_by); $ban->save(); - $this->setBlock_Reason(NULL); + $this->setBlock_Reason(null); // $user->setUnblock_time(NULL); $this->save(); } - function deactivate(?string $reason): void + public function deactivate(?string $reason): void { $this->setDeleted(1); $this->setDeact_Date(time() + (MONTH * 7)); @@ -934,7 +1159,7 @@ function deactivate(?string $reason): void $this->save(); } - function reactivate(): void + public function reactivate(): void { $this->setDeleted(0); $this->setDeact_Date(0); @@ -942,19 +1167,23 @@ function reactivate(): void $this->save(); } - function getDeactivationDate(): DateTime + public function getDeactivationDate(): DateTime { return new DateTime($this->getRecord()->deact_date); } - - function verifyNumber(string $code): bool + + public function verifyNumber(string $code): bool { $ver = $this->getPendingPhoneVerification(); - if(!$ver) return false; + if (!$ver) { + return false; + } try { - if(sodium_memcmp((string) $ver->code, $code) === -1) return false; - } catch(\SodiumException $ex) { + if (sodium_memcmp((string) $ver->code, $code) === -1) { + return false; + } + } catch (\SodiumException $ex) { return false; } @@ -969,33 +1198,34 @@ function verifyNumber(string $code): bool return true; } - function setFirst_Name(string $firstName): void + public function setFirst_Name(string $firstName): void { $firstName = mb_convert_case($firstName, MB_CASE_TITLE); - if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?(?:[\p{L&}\p{Lo}]\p{Mn}?){1,16}$%u', $firstName)) - throw new InvalidUserNameException; + if (!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?(?:[\p{L&}\p{Lo}\-\.]\p{Mn}?){1,64}$%u', $firstName)) { + throw new InvalidUserNameException(); + } $this->stateChanges("first_name", $firstName); } - function setLast_Name(string $lastName): void + public function setLast_Name(string $lastName): void { - if(!empty($lastName)) - { - $lastName = mb_convert_case($lastName, MB_CASE_TITLE); - if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?([\p{L&}\p{Lo}]\p{Mn}?){1,16}(\-\g<1>+)?$%u', $lastName)) - throw new InvalidUserNameException; + if (!empty($lastName)) { + $lastName = mb_convert_case($lastName, MB_CASE_TITLE); + if (!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?([\p{L&}\p{Lo}\-\.]\p{Mn}?){0,64}(\-\g<1>+)?$%u', $lastName)) { + throw new InvalidUserNameException(); + } } $this->stateChanges("last_name", $lastName); } - function setNsfwTolerance(int $tolerance): void + public function setNsfwTolerance(int $tolerance): void { $this->stateChanges("nsfw_tolerance", $tolerance); } - function setPrivacySetting(string $id, int $status): void + public function setPrivacySetting(string $id, int $status): void { $this->stateChanges("privacy", bmask($this->changes["privacy"] ?? $this->getRecord()->privacy, [ "length" => 2, @@ -1010,16 +1240,19 @@ function setPrivacySetting(string $id, int $status): void "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ], ])->set($id, $status)->toInteger()); } - function setLeftMenuItemStatus(string $id, bool $status): void + public function setLeftMenuItemStatus(string $id, bool $status): void { $mask = bmask($this->changes["left_menu"] ?? $this->getRecord()->left_menu, [ "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -1027,43 +1260,51 @@ function setLeftMenuItemStatus(string $id, bool $status): void "news", "links", "poster", - "apps" + "apps", + "docs", + "fave", ], ])->set($id, (int) $status)->toInteger(); $this->stateChanges("left_menu", $mask); } - function setShortCode(?string $code = NULL, bool $force = false): ?bool + public function setShortCode(?string $code = null, bool $force = false): ?bool { - if(!is_null($code)) { - if(strlen($code) < OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["minLength"] && !$force) + if (!is_null($code)) { + if (strlen($code) < OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["minLength"] && !$force) { return false; - if(!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code)) + } + if (!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code)) { return false; - if(in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"])) + } + if (in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"])) { return false; - if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") + } + if (\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") { return false; + } $pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch(); - if(!is_null($pClub)) - return false; + if (!is_null($pClub)) { + return false; + } $pAlias = DatabaseConnection::i()->getContext()->table("aliases")->where("shortcode", $code)->fetch(); - if(!is_null($pAlias)) - return false; + if (!is_null($pAlias)) { + return false; + } } $this->stateChanges("shortcode", $code); return true; } - function setPhoneWithVerification(string $phone): string + public function setPhoneWithVerification(string $phone): string { $code = unpack("S", openssl_random_pseudo_bytes(2))[1]; - if($this->hasPendingNumberChange()) { + if ($this->hasPendingNumberChange()) { DatabaseConnection::i()->getContext() ->table("number_verification") ->where("user", $this->getId()) @@ -1080,7 +1321,7 @@ function setPhoneWithVerification(string $phone): string # KABOBSQL temporary fix # Tuesday, the 7th of January 2020 @ 22:43 : implementing quick fix to this problem and monitoring # NOTICE: this is an ongoing conversation, add your comments just above this line. Thanks! - function setOnline(int $time): bool + public function setOnline(int $time): bool { $this->stateChanges("shortcode", $this->getRecord()->shortcode); #fix KABOBSQL $this->stateChanges("online", $time); @@ -1088,7 +1329,7 @@ function setOnline(int $time): bool return true; } - function updOnline(string $platform): bool + public function updOnline(string $platform): bool { $this->setOnline(time()); $this->setClient_name($platform); @@ -1097,47 +1338,50 @@ function updOnline(string $platform): bool return true; } - function changeEmail(string $email): void + public function changeEmail(string $email): void { DatabaseConnection::i()->getContext()->table("ChandlerUsers") ->where("id", $this->getChandlerUser()->getId())->update([ - "login" => $email + "login" => $email, ]); $this->stateChanges("email", $email); $this->save(); } - function adminNotify(string $message): bool + public function adminNotify(string $message): bool { $admId = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; - if(!$admId) + if (!$admId) { return false; - else if(is_null($admin = (new Users)->get($admId))) + } elseif (is_null($admin = (new Users())->get($admId))) { return false; + } $cor = new Correspondence($admin, $this); - $msg = new Message; + $msg = new Message(); $msg->setContent($message); $cor->sendMessage($msg, true); return true; } - function isDeleted(): bool + public function isDeleted(): bool { - if($this->getRecord()->deleted == 1) - return TRUE; - else - return FALSE; + if ($this->getRecord()->deleted == 1) { + return true; + } else { + return false; + } } - function isDeactivated(): bool + public function isDeactivated(): bool { - if($this->getDeactivationDate()->timestamp() > time()) - return TRUE; - else - return FALSE; + if ($this->getDeactivationDate()->timestamp() > time()) { + return true; + } else { + return false; + } } /** @@ -1145,7 +1389,7 @@ function isDeactivated(): bool * 1 - Incognito online status * 2 - Page of a dead person */ - function onlineStatus(): int + public function onlineStatus(): int { switch ($this->getRecord()->online) { case 1: @@ -1162,47 +1406,69 @@ function onlineStatus(): int } } - function getWebsite(): ?string - { - return $this->getRecord()->website; - } + public function getWebsite(): ?string + { + return $this->getRecord()->website; + } # ты устрица - function isActivated(): bool + public function isActivated(): bool { return (bool) $this->getRecord()->activated; } - function getUnbanTime(): ?string + public function isAdmin(): bool + { + return $this->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(null); + } + + public function isDead(): bool + { + return $this->onlineStatus() == 2; + } + + public function getUnbanTime(): ?string { - $ban = (new Bans)->get((int) $this->getRecord()->block_reason); - if (!$ban || $ban->isOver() || $ban->isPermanent()) return null; - if ($this->canUnbanThemself()) return tr("today"); + $ban = (new Bans())->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) { + return null; + } + if ($this->canUnbanThemself()) { + return tr("today"); + } return date('d.m.Y', $ban->getEndTime()); } - function canUnbanThemself(): bool + public function canUnbanThemself(): bool { - if (!$this->isBanned()) + if (!$this->isBanned()) { return false; + } - $ban = (new Bans)->get((int) $this->getRecord()->block_reason); - if (!$ban || $ban->isOver() || $ban->isPermanent()) return false; + $ban = (new Bans())->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) { + return false; + } return $ban->getEndTime() <= time() && !$ban->isPermanent(); } - function getNewBanTime() + public function getNewBanTime() { - $bans = iterator_to_array((new Bans)->getByUser($this->getid())); - if (!$bans || count($bans) === 0) + $bans = iterator_to_array((new Bans())->getByUser($this->getid())); + if (!$bans || count($bans) === 0) { return 0; + } $last_ban = end($bans); - if (!$last_ban) return 0; + if (!$last_ban) { + return 0; + } - if ($last_ban->isPermanent()) return "permanent"; + if ($last_ban->isPermanent()) { + return "0"; + } $values = [0, 3600, 7200, 86400, 172800, 604800, 1209600, 3024000, 9072000]; $response = 0; @@ -1210,36 +1476,379 @@ function getNewBanTime() foreach ($values as $value) { $i++; - if ($last_ban->getTime() === 0 && $value === 0) continue; + if ($last_ban->getTime() === 0 && $value === 0) { + continue; + } if ($last_ban->getTime() < $value) { $response = $value; break; - } else if ($last_ban->getTime() >= $value) { - if ($i < count($values)) continue; - $response = "permanent"; + } elseif ($last_ban->getTime() >= $value) { + if ($i < count($values)) { + continue; + } + $response = "0"; break; } } return $response; } - function toVkApiStruct(): object + public function getProfileType(): int + { + # 0 — открытый профиль, 1 — закрытый + return $this->getRecord()->profile_type; + } + + public function canBeViewedBy(?User $user = null, bool $blacklist_check = true): bool + { + if (!is_null($user)) { + if ($this->getId() == $user->getId()) { + return true; + } + + if ($user->isAdmin() && !(OPENVK_ROOT_CONF['openvk']['preferences']['blacklists']['applyToAdmins'] ?? true)) { + return true; + } + + if ($blacklist_check && ($this->isBlacklistedBy($user) || $user->isBlacklistedBy($this))) { + return false; + } + + if ($this->getProfileType() == 0) { + return true; + } else { + if ($user->getSubscriptionStatus($this) == User::SUBSCRIPTION_MUTUAL) { + return true; + } else { + return false; + } + } + + } else { + if ($this->getProfileType() == 0) { + if ($this->getPrivacySetting("page.read") == 3) { + return true; + } else { + return false; + } + } else { + return false; + } + } + + return true; + } + + public function isClosed(): bool + { + return (bool) $this->getProfileType(); + } + + public function isHideFromGlobalFeedEnabled(): bool + { + return $this->isClosed(); + } + + public function HideGlobalFeed(): bool + { + return (bool) $this->getRecord()->hide_global_feed; + } + + public function getRealId() + { + return $this->getId(); + } + + public function isPrivateLikes(): bool + { + return $this->getPrivacySetting("likes.read") == User::PRIVACY_NO_ONE; + } + + public function toVkApiStruct(?User $relation_user = null, string $fields = ''): object { $res = (object) []; $res->id = $this->getId(); - $res->first_name = $this->getFirstName(); - $res->last_name = $this->getLastName(); + $res->first_name = $this->getFirstName(); + $res->last_name = $this->getLastName(); $res->deactivated = $this->isDeactivated(); - $res->photo_50 = $this->getAvatarURL(); - $res->photo_100 = $this->getAvatarURL("tiny"); - $res->photo_200 = $this->getAvatarURL("normal"); - $res->photo_id = !is_null($this->getAvatarPhoto()) ? $this->getAvatarPhoto()->getPrettyId() : NULL; - # TODO: Perenesti syuda vsyo ostalnoyie + $res->is_closed = $this->isClosed(); + + if (!is_null($relation_user)) { + $res->can_access_closed = (int) $this->canBeViewedBy($relation_user); + } + + if (!is_array($fields)) { + $fields = explode(',', $fields); + } + + $avatar_photo = $this->getAvatarPhoto(); + foreach ($fields as $field) { + switch ($field) { + case 'is_dead': + $res->is_dead = $this->isDead(); + break; + case 'verified': + $res->verified = (int) $this->isVerified(); + break; + case 'sex': + $res->sex = $this->isFemale() ? 1 : ($this->isNeutral() ? 0 : 2); + break; + case 'photo_50': + $res->photo_50 = $this->getAvatarUrl('miniscule', $avatar_photo); + break; + case 'photo_100': + $res->photo_100 = $this->getAvatarUrl('tiny', $avatar_photo); + break; + case 'photo_200': + $res->photo_200 = $this->getAvatarUrl('normal', $avatar_photo); + break; + case 'photo_max': + $res->photo_max = $this->getAvatarUrl('original', $avatar_photo); + break; + case 'photo_id': + $res->photo_id = $avatar_photo ? $avatar_photo->getPrettyId() : null; + break; + case 'background': + $res->background = $this->getBackDropPictureURLs(); + break; + case 'reg_date': + $res->reg_date = $this->getRegistrationTime()->timestamp(); + break; + case 'nickname': + $res->nickname = $this->getPseudo(); + break; + case 'nickname': + $res->nickname = $this->getPseudo(); + break; + case 'rating': + $res->rating = $this->getRating(); + break; + case 'status': + $res->status = $this->getStatus(); + break; + case 'screen_name': + $res->screen_name = $this->getShortCode() ?? "id" . $this->getId(); + break; + case 'real_id': + $res->real_id = $this->getRealId(); + break; + case "blacklisted_by_me": + if (!$relation_user) { + break; + } + + $res->blacklisted_by_me = (int) $this->isBlacklistedBy($relation_user); + break; + case "blacklisted": + if (!$relation_user) { + break; + } + + $res->blacklisted = (int) $relation_user->isBlacklistedBy($this); + break; + case "games": + $res->games = $this->getFavoriteGames(); + break; + } + } return $res; } - - use Traits\TBackDrops; - use Traits\TSubscribable; + + public function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios())->getUserCollectionSize($this); + } + + public function getBroadcastList(string $filter = "friends", bool $shuffle = false) + { + $dbContext = DatabaseConnection::i()->getContext(); + $entityIds = []; + $query = $dbContext->table("subscriptions")->where("follower", $this->getRealId()); + + if ($filter != "all") { + $query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User")); + } + + foreach ($query as $_rel) { + $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; + } + + if ($shuffle) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $entityIds = knuth_shuffle($entityIds, $shuffleSeed); + } + + $entityIds = array_slice($entityIds, 0, 10); + + $returnArr = []; + + foreach ($entityIds as $id) { + $entit = $id > 0 ? (new Users())->get($id) : (new Clubs())->get(abs($id)); + + if ($id > 0 && $entit->isDeleted()) { + continue; + } + $returnArr[] = $entit; + } + + return $returnArr; + } + + public function getIgnoredSources(int $offset = 0, int $limit = 10, bool $onlyIds = false) + { + $sources = DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->limit($limit, $offset)->order('id DESC'); + $output_array = []; + + foreach ($sources as $source) { + if ($onlyIds) { + $output_array[] = (int) $source->source; + } else { + $ignored_source_model = null; + $ignored_source_id = (int) $source->source; + + if ($ignored_source_id > 0) { + $ignored_source_model = (new Users())->get($ignored_source_id); + } else { + $ignored_source_model = (new Clubs())->get(abs($ignored_source_id)); + } + + if (!$ignored_source_model) { + continue; + } + + $output_array[] = $ignored_source_model; + } + } + + return $output_array; + } + + public function getIgnoredSourcesCount() + { + return DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->count('*'); + } + + public function isBlacklistedBy(?User $user = null): bool + { + if (!$user) { + return false; + } + + $ctx = DatabaseConnection::i()->getContext(); + $data = [ + "author" => $user->getId(), + "target" => $this->getRealId(), + ]; + + $sub = $ctx->table("blacklist_relations")->where($data); + return $sub->count('*') > 0; + } + + public function addToBlacklist(?User $user) + { + DatabaseConnection::i()->getContext()->table("blacklist_relations")->insert([ + "author" => $this->getRealId(), + "target" => $user->getRealId(), + "created" => time(), + ]); + + DatabaseConnection::i()->getContext()->table("subscriptions")->where([ + "follower" => $user->getId(), + "model" => static::class, + "target" => $this->getId(), + ])->delete(); + + DatabaseConnection::i()->getContext()->table("subscriptions")->where([ + "follower" => $this->getId(), + "model" => static::class, + "target" => $user->getId(), + ])->delete(); + + return true; + } + + public function removeFromBlacklist(?User $user): bool + { + DatabaseConnection::i()->getContext()->table("blacklist_relations")->where([ + "author" => $this->getRealId(), + "target" => $user->getRealId(), + ])->delete(); + + return true; + } + + public function getBlacklist(int $offset = 0, int $limit = 10) + { + $sources = DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->limit($limit, $offset)->order('created ASC'); + $output_array = []; + + foreach ($sources as $source) { + $entity_id = (int) $source->target ; + $entity = (new Users())->get($entity_id); + if (!$entity) { + continue; + } + + $output_array[] = $entity; + } + + return $output_array; + } + + public function getBlacklistSize() + { + return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count('*'); + } + + public function getEventCounters(array $list): array + { + $count_of_keys = sizeof(array_keys($list)); + $ev_str = $this->getRecord()->events_counters; + $counters = []; + + if (!$ev_str) { + for ($i = 0; $i < sizeof(array_keys($list)); $i++) { + $counters[] = 0; + } + } else { + $counters = unpack("S" . $count_of_keys, base64_decode($ev_str, true)); + } + + return [ + 'counters' => array_combine(array_keys($list), $counters), + 'refresh_time' => $this->getRecord()->events_refresh_time, + ]; + } + + public function stateEvents(array $state_list): void + { + $pack_str = ""; + + foreach ($state_list as $item => $id) { + $pack_str .= "S"; + } + + $this->stateChanges("events_counters", base64_encode(pack($pack_str, ...array_values($state_list)))); + + if (!$this->getRecord()->events_refresh_time) { + $this->stateChanges("events_refresh_time", time()); + } + } + + public function resetEvents(array $list): void + { + $values = []; + + foreach ($list as $key => $val) { + $values[$key] = 0; + } + + $this->stateEvents($values); + $this->stateChanges("events_refresh_time", time()); + $this->save(); + } } diff --git a/Web/Models/Entities/UserInfoEntities/AdditionalField.php b/Web/Models/Entities/UserInfoEntities/AdditionalField.php new file mode 100644 index 000000000..f9685f88a --- /dev/null +++ b/Web/Models/Entities/UserInfoEntities/AdditionalField.php @@ -0,0 +1,102 @@ +getRecord()->owner; + } + + public function getName(bool $tr = true): string + { + $orig_name = $this->getRecord()->name; + $name = $orig_name; + if ($tr && $name[0] === "_") { + $name = tr("custom_field_" . substr($name, 1)); + } + + if (str_contains($name, "custom_field")) { + return $orig_name; + } + + return $name; + } + + public function getContent(): string + { + return $this->getRecord()->text; + } + + public function getPlace(): string + { + switch ($this->getRecord()->place) { + case AdditionalField::PLACE_CONTACTS: + return "contact"; + case AdditionalField::PLACE_INTERESTS: + return "interest"; + } + + return "contact"; + } + + public function isContact(): bool + { + return $this->getRecord()->place == AdditionalField::PLACE_CONTACTS; + } + + public function toVkApiStruct(): object + { + return (object) [ + "type" => $this->getRecord()->place, + "name" => $this->getName(), + "text" => $this->getContent(), + ]; + } + + public static function getById(int $id) + { + $ctx = DatabaseConnection::i()->getContext(); + $entry = $ctx->table("additional_fields")->where("id", $id)->fetch(); + + if (!$entry) { + return null; + } + + return new AdditionalField($entry); + } + + public static function getByOwner(int $owner): \Traversable + { + $ctx = DatabaseConnection::i()->getContext(); + $entries = $ctx->table("additional_fields")->where("owner", $owner); + + foreach ($entries as $entry) { + yield new AdditionalField($entry); + } + } + + public static function getCountByOwner(int $owner): \Traversable + { + return DatabaseConnection::i()->getContext()->table("additional_fields")->where("owner", $owner)->count('*'); + } + + public static function resetByOwner(int $owner): bool + { + DatabaseConnection::i()->getContext()->table("additional_fields")->where("owner", $owner)->delete(); + + return true; + } +} diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index cef48e27f..817176ef2 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -1,68 +1,93 @@ -execute($error); - if($error !== 0) + if ($error !== 0) { throw new \DomainException("$filename is not a valid video file"); - else if(empty($streams) || ctype_space($streams)) + } elseif (empty($streams) || ctype_space($streams)) { throw new \DomainException("$filename does not contain any video streams"); - + } + $durations = []; preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); - if(sizeof($durations[1]) === 0) + if (sizeof($durations[1]) === 0) { throw new \DomainException("$filename does not contain any meaningful video streams"); - - foreach($durations[1] as $duration) - if(floatval($duration) < 1.0) + } + + $length = 0; + foreach ($durations[1] as $duration) { + $duration = floatval($duration); + if ($duration < 1.0) { throw new \DomainException("$filename does not contain any meaningful video streams"); - + } else { + $length = max($length, $duration); + } + } + + $this->stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + if (!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("height", $height[1]); + } + try { - if(!is_dir($dirId = dirname($this->pathFromHash($hash)))) + if (!is_dir($dirId = dirname($this->pathFromHash($hash)))) { mkdir($dirId); - + } + $dir = $this->getBaseDir(); $ext = Shell::isPowershell() ? "ps1" : "sh"; $cmd = Shell::isPowershell() ? "powershell" : "bash"; Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD - } catch(ShellUnavailableException $suex) { + } catch (ShellUnavailableException $suex) { exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "Shell is unavailable" : VIDEOS_FRIENDLY_ERROR); - } catch(UnknownCommandException $ucex) { + } catch (UnknownCommandException $ucex) { exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash is not installed" : VIDEOS_FRIENDLY_ERROR); } - + usleep(200100); return true; } protected function checkIfFileIsProcessed(): bool { - if($this->getType() != Video::TYPE_DIRECT) + if ($this->getType() != Video::TYPE_DIRECT) { return true; + } - if(!file_exists($this->getFileName())) { - if((time() - $this->getRecord()->last_checked) > 3600) { + if (!file_exists($this->getFileName())) { + if ((time() - $this->getRecord()->last_checked) > 3600) { # TODO notify that video processor is probably dead } @@ -72,37 +97,42 @@ protected function checkIfFileIsProcessed(): bool return true; } - function getName(): string + public function getName(): string { return $this->getRecord()->name; } - - function getType(): int + + public function getType(): int { - if(!is_null($this->getRecord()->hash)) + if (!is_null($this->getRecord()->hash)) { return Video::TYPE_DIRECT; - else if(!is_null($this->getRecord()->link)) + } elseif (!is_null($this->getRecord()->link)) { return Video::TYPE_EMBED; + } + return Video::TYPE_UNKNOWN; } - - function getVideoDriver(): ?VideoDriver + + public function getVideoDriver(): ?VideoDriver { - if($this->getType() !== Video::TYPE_EMBED) - return NULL; - + if ($this->getType() !== Video::TYPE_EMBED) { + return null; + } + [$videoDriver, $pointer] = explode(":", $this->getRecord()->link); $videoDriver = "openvk\\Web\\Models\\VideoDrivers\\$videoDriver" . "VideoDriver"; - if(!class_exists($videoDriver)) - return NULL; - + if (!class_exists($videoDriver)) { + return null; + } + return new $videoDriver($pointer); } - - function getThumbnailURL(): string + + public function getThumbnailURL(): string { - if($this->getType() === Video::TYPE_DIRECT) { - if(!$this->isProcessed()) + if ($this->getType() === Video::TYPE_DIRECT) { + if (!$this->isProcessed()) { return "/assets/packages/static/openvk/video/rendering.apng"; + } return preg_replace("%\.[A-z0-9]++$%", ".gif", $this->getURL()); } else { @@ -110,37 +140,44 @@ function getThumbnailURL(): string } } - function getOwnerVideo(): int + public function getOwnerVideo(): int { return $this->getRecord()->owner; } - function getApiStructure(): object + public function getApiStructure(?User $user = null): object { $fromYoutube = $this->getType() == Video::TYPE_EMBED; - return (object)[ + $dimensions = $this->getDimensions(); + $res = (object) [ "type" => "video", "video" => [ "can_comment" => 1, - "can_like" => 0, // we don't h-have wikes in videos - "can_repost" => 0, + "can_like" => 1, // we don't h-have wikes in videos + "can_repost" => 1, "can_subscribe" => 1, "can_add_to_faves" => 0, "can_add" => 0, "comments" => $this->getCommentsCount(), "date" => $this->getPublicationTime()->timestamp(), "description" => $this->getDescription(), - "duration" => 0, // я хуй знает как получить длину видео + "duration" => $this->getLength(), "image" => [ - [ + (object) [ "url" => $this->getThumbnailURL(), "width" => 320, "height" => 240, - "with_padding" => 1 - ] + "with_padding" => 1, + ], + (object) [ + "url" => $this->getThumbnailURL(), + "width" => 130, + "height" => 100, + "with_padding" => 1, + ], ], - "width" => 640, - "height" => 480, + "width" => $dimensions ? $dimensions[0] : 640, + "height" => $dimensions ? $dimensions[1] : 480, "id" => $this->getVirtualId(), "owner_id" => $this->getOwner()->getId(), "user_id" => $this->getOwner()->getId(), @@ -148,66 +185,76 @@ function getApiStructure(): object "is_favorite" => false, "player" => !$fromYoutube ? $this->getURL() : $this->getVideoDriver()->getURL(), "files" => !$fromYoutube ? [ - "mp4_480" => $this->getURL() - ] : NULL, - "platform" => $fromYoutube ? "youtube" : NULL, + "mp4_480" => $this->getURL() . "#vkuservideo", + ] : [], "added" => 0, "repeat" => 0, "type" => "video", "views" => 0, - "likes" => [ - "count" => 0, - "user_likes" => 0 - ], + "is_processed" => $this->isProcessed(), "reposts" => [ "count" => 0, - "user_reposted" => 0 - ] - ] + "user_reposted" => 0, + ], + ], ]; + if ($fromYoutube) { + $res->video['platform'] = "youtube"; + } + + if (!is_null($user)) { + $res->video["likes"] = [ + "count" => $this->getLikesCount(), + "user_likes" => $this->hasLikeFrom($user), + ]; + } + + return $res; } - - function toVkApiStruct(): object + + public function toVkApiStruct(?User $user): object { - return $this->getApiStructure(); + return $this->getApiStructure($user); } - function setLink(string $link): string + public function setLink(string $link): string { - if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/youtube.txt"), $link, $matches)) { + if (preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/youtube.txt"), $link, $matches)) { $pointer = "YouTube:$matches[1]"; - } else if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/vimeo.txt"), $link, $matches)) { - $pointer = "Vimeo:$matches[1]"; + /*} else if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/vimeo.txt"), $link, $matches)) { + $pointer = "Vimeo:$matches[1]";*/ } else { throw new ISE("Invalid link"); } - + $this->stateChanges("link", $pointer); - + return $pointer; } - function isDeleted(): bool + public function isDeleted(): bool { - if ($this->getRecord()->deleted == 1) - return TRUE; - else - return FALSE; + if ($this->getRecord()->deleted == 1) { + return true; + } else { + return false; + } } - function deleteVideo(): void + public function deleteVideo(): void { $this->setDeleted(1); $this->unwire(); $this->save(); } - - 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']) + if (OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) { exit(VIDEOS_FRIENDLY_ERROR); + } - $video = new Video; + $video = new Video(); $video->setOwner($owner); $video->setName(ovk_proc_strtr($name, 61)); $video->setDescription(ovk_proc_strtr($description, 300)); @@ -216,7 +263,120 @@ static function fastMake(int $owner, string $name = "Unnamed Video.ogv", string $video->setFile($file); $video->setUnlisted($unlisted); $video->save(); - + return $video; } + + public function fillDimensions() + { + $hash = $this->getRecord()->hash; + $path = $this->pathFromHash($hash); + if (!file_exists($path)) { + $this->stateChanges("width", 0); + $this->stateChanges("height", 0); + $this->stateChanges("length", 0); + $this->save(); + return false; + } + + $streams = Shell::ffprobe("-i", $path, "-show_streams", "-select_streams v", "-loglevel error")->execute(); + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + + $length = 0; + foreach ($durations[1] as $duration) { + $duration = floatval($duration); + if ($duration < 1.0) { + continue; + } else { + $length = max($length, $duration); + } + } + $this->stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + + if (!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("height", $height[1]); + } + + $this->save(); + + return true; + } + + public function getDimensions() + { + if ($this->getType() == Video::TYPE_EMBED) { + return [320, 180]; + } + + $width = $this->getRecord()->width; + $height = $this->getRecord()->height; + + if (!$width) { + return null; + } + return $width != 0 ? [$width, $height] : null; + } + + public function getLength() + { + return $this->getRecord()->length; + } + + public function getFormattedLength(): string + { + $len = $this->getLength(); + if (!$len) { + return "00:00"; + } + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + public function getPageURL(): string + { + return "/video" . $this->getPrettyId(); + } + + public function canBeViewedBy(?User $user = null): bool + { + if ($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + if (get_class($this->getOwner()) == "openvk\\Web\\Models\\Entities\\User") { + return $this->getOwner()->canBeViewedBy($user) && $this->getOwner()->getPrivacyPermission('videos.read', $user); + } else { + # Groups doesn't have videos but ok + return $this->getOwner()->canBeViewedBy($user); + } + } + + public function toNotifApiStruct() + { + $fromYoutube = $this->getType() == Video::TYPE_EMBED; + $res = (object) []; + + $res->id = $this->getVirtualId(); + $res->owner_id = $this->getOwner()->getId(); + $res->title = $this->getName(); + $res->description = $this->getDescription(); + $res->duration = $this->getLength(); + $res->link = "/video" . $this->getOwner()->getId() . "_" . $this->getVirtualId(); + $res->image = $this->getThumbnailURL(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->views = 0; + $res->player = !$fromYoutube ? $this->getURL() : $this->getVideoDriver()->getURL(); + + return $res; + } } diff --git a/Web/Models/Entities/VideoAlbum.php b/Web/Models/Entities/VideoAlbum.php index 578ecf3d1..401b556d8 100644 --- a/Web/Models/Entities/VideoAlbum.php +++ b/Web/Models/Entities/VideoAlbum.php @@ -1,43 +1,49 @@ - "_added_album", 32 => "_uploaded_album", ]; - - function getCoverURL(): ?string + + public function getCoverURL(): ?string { $cover = $this->getCoverVideo(); - if(!$cover) + if (!$cover) { return "/assets/packages/static/openvk/img/camera_200.png"; - + } + return $cover->getThumbnailURL(); } - - function getCoverVideo(): ?Photo + + public function getCoverVideo(): ?Photo { $cover = $this->getRecord()->cover_video; - if(!$cover) { + if (!$cover) { $vids = iterator_to_array($this->fetch(1, 1)); - $vid = $vids[0] ?? NULL; - if(!$vid || $vid->isDeleted()) - return NULL; - else + $vid = $vids[0] ?? null; + if (!$vid || $vid->isDeleted()) { + return null; + } else { return $vid; + } } - - return (new Videos)->get($cover); + + return (new Videos())->get($cover); } } diff --git a/Web/Models/Entities/Voucher.php b/Web/Models/Entities/Voucher.php index 6469dddc3..fe988e73d 100644 --- a/Web/Models/Entities/Voucher.php +++ b/Web/Models/Entities/Voucher.php @@ -1,5 +1,9 @@ -getRecord()->coins; } - - function getRating(): int + + public function getRating(): int { return $this->getRecord()->rating; } - - function getToken(): string + + public function getToken(): string { return $this->getRecord()->token; } - - function getFormattedToken(): string + + public function getFormattedToken(): string { - $fmtTok = ""; + $fmtTok = ""; $token = $this->getRecord()->token; - foreach(array_chunk(str_split($token), 6) as $chunk) + foreach (array_chunk(str_split($token), 6) as $chunk) { $fmtTok .= implode("", $chunk) . "-"; - + } + return substr($fmtTok, 0, -1); } - - function getRemainingUsages(): float + + public function getRemainingUsages(): float { return (float) ($this->getRecord()->usages_left ?? INF); } - - function getUsers(int $page = -1, ?int $perPage = NULL): \Traversable + + public function getUsers(int $page = -1, ?int $perPage = null): \Traversable { $relations = $this->getRecord()->related("voucher_users.voucher"); - if($page !== -1) + if ($page !== -1) { $relations = $relations->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); - - foreach($relations as $relation) - yield (new Users)->get($relation->user); + } + + foreach ($relations as $relation) { + yield (new Users())->get($relation->user); + } } - - function isExpired(): bool + + public function isExpired(): bool { return $this->getRemainingUsages() < 1; } - - function wasUsedBy(User $user): bool + + public function wasUsedBy(User $user): bool { $record = $this->getRecord()->related("voucher_users.voucher")->where("user", $user->getId()); - + return sizeof($record) > 0; } - - function willUse(User $user): bool + + public function willUse(User $user): bool { - if($this->wasUsedBy($user)) + if ($this->wasUsedBy($user)) { return false; - - if($this->isExpired()) + } + + if ($this->isExpired()) { return false; - + } + $this->setRemainingUsages($this->getRemainingUsages() - 1); DB::i()->getContext()->table("voucher_users")->insert([ "voucher" => $this->getId(), "user" => $user->getId(), ]); - + return true; } - - function setRemainingUsages(float $usages): void + + public function setRemainingUsages(float $usages): void { - $this->stateChanges("usages_left", $usages === INF ? NULL : ((int) $usages)); + $this->stateChanges("usages_left", $usages === INF ? null : ((int) $usages)); $this->save(); } } diff --git a/Web/Models/Exceptions/AlreadyVotedException.php b/Web/Models/Exceptions/AlreadyVotedException.php index 08363b9a3..58cfa0df6 100644 --- a/Web/Models/Exceptions/AlreadyVotedException.php +++ b/Web/Models/Exceptions/AlreadyVotedException.php @@ -1,7 +1,7 @@ -get((int) $id); - if(!$token) - return NULL; - else if($token->getSecret() !== $secret) - return NULL; - else if($token->isRevoked() && !$withRevoked) - return NULL; - + if (!$token) { + return null; + } elseif ($token->getSecret() !== $secret) { + return null; + } elseif ($token->isRevoked() && !$withRevoked) { + return null; + } + return $token; } + + public function getStaleByUser(int $userId, string $platform, bool $withRevoked = false): ?APIToken + { + return $this->toEntity($this->table->where([ + 'user' => $userId, + 'platform' => $platform, + 'deleted' => $withRevoked, + ])->fetch()); + } } diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php index f99848c40..4a5348594 100644 --- a/Web/Models/Repositories/Albums.php +++ b/Web/Models/Repositories/Albums.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->albums = $this->context->table("albums"); } - + private function toAlbum(?ActiveRow $ar): ?Album { - return is_null($ar) ? NULL : new Album($ar); + return is_null($ar) ? null : new Album($ar); } - + private function getSpecialConditions(int $id, int $type): array { return [ @@ -31,106 +38,108 @@ private function getSpecialConditions(int $id, int $type): array "special_type" => $type, ]; } - - function get(int $id): ?Album + + public function get(int $id): ?Album { - return $this->toAlbum($this->albums->get($id)); + return self::$cache[$id] ??= $this->toAlbum($this->albums->get($id)); } - - function getUserAlbums(User $user, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getUserAlbums(User $user, int $page = 1, ?int $perPage = null): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $perPage ??= OPENVK_DEFAULT_PER_PAGE; $albums = $this->albums->where("owner", $user->getId())->where("deleted", false); - foreach($albums->page($page, $perPage) as $album) + foreach ($albums->page($page, $perPage) as $album) { yield new Album($album); + } } - - function getUserAlbumsCount(User $user): int + + public function getUserAlbumsCount(User $user): int { $albums = $this->albums->where("owner", $user->getId())->where("deleted", false); return sizeof($albums); } - - function getClubAlbums(Club $club, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getClubAlbums(Club $club, int $page = 1, ?int $perPage = null): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - $albums = $this->albums->where("owner", $club->getId() * -1)->where("special_type", 0)->where("deleted", false); - foreach($albums->page($page, $perPage) as $album) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $albums = $this->albums->where("owner", $club->getId() * -1)->where("special_type", 0)->where("deleted", false); + foreach ($albums->page($page, $perPage) as $album) { yield new Album($album); + } } - - function getClubAlbumsCount(Club $club): int + + public function getClubAlbumsCount(Club $club): int { - $albums = $this->albums->where("owner", $club->getId() * -1)->where("special_type", 0)->where("deleted", false); + $albums = $this->albums->where("owner", $club->getId() * -1)->where("special_type", 0)->where("deleted", false); return sizeof($albums); } - - function getAvatarAlbumById(int $id, int $regTime): Album + + public function getAvatarAlbumById(int $id, int $regTime): Album { $data = $this->getSpecialConditions($id, 16); $album = $this->albums->where([ "owner" => $id, "special_type" => 16, ])->fetch(); - if(!$album) { - $album = new Album; + if (!$album) { + $album = new Album(); $album->setName("[!!! internal album]"); $album->setOwner($id); $album->setSpecial_Type(16); $album->setCreated($regTime); $album->save(); - + return $album; } - + return new Album($album); } - - function getUserAvatarAlbum(User $user): Album + + public function getUserAvatarAlbum(User $user): Album { return $this->getAvatarAlbumById($user->getId(), $user->getRegistrationTime()->timestamp()); } - - function getClubAvatarAlbum(Club $club): Album + + public function getClubAvatarAlbum(Club $club): Album { return $this->getAvatarAlbumById($club->getId() * -1, time()); } - - function getUserWallAlbum(User $user): Album + + public function getUserWallAlbum(User $user): Album { $data = $this->getSpecialConditions($user->getId(), 32); $album = $this->albums->where([ "owner" => $user->getId(), "special_type" => 32, ])->fetch(); - if(!$album) { - $album = new Album; + if (!$album) { + $album = new Album(); $album->setName("[!!! internal album]"); $album->setOwner($user->getId()); $album->setSpecial_Type(32); $album->setCreated($user->getRegistrationTime()->timestamp()); $album->save(); - + return $album; } - + return new Album($album); } - function getAlbumByPhotoId(Photo $photo): ?Album + public function getAlbumByPhotoId(Photo $photo): ?Album { $dbalbum = $this->context->table("album_relations")->where(["media" => $photo->getId()])->fetch(); return $dbalbum->collection ? $this->get($dbalbum->collection) : null; } - function getAlbumByOwnerAndId(int $owner, int $id) + public function getAlbumByOwnerAndId(int $owner, int $id) { $album = $this->albums->where([ "owner" => $owner, - "id" => $id + "id" => $id, ])->fetch(); - return $album ? new Album($album) : NULL; + return $album ? new Album($album) : null; } } diff --git a/Web/Models/Repositories/Aliases.php b/Web/Models/Repositories/Aliases.php index e74532a14..1480da06d 100644 --- a/Web/Models/Repositories/Aliases.php +++ b/Web/Models/Repositories/Aliases.php @@ -1,4 +1,7 @@ -context = DB::i()->getContext(); $this->aliases = $this->context->table("aliases"); @@ -20,16 +26,16 @@ function __construct() private function toAlias(?ActiveRow $ar): ?Alias { - return is_null($ar) ? NULL : new Alias($ar); + return is_null($ar) ? null : new Alias($ar); } - function get(int $id): ?Alias + public function get(int $id): ?Alias { return $this->toAlias($this->aliases->get($id)); } - function getByShortcode(string $shortcode): ?Alias + public function getByShortcode(string $shortcode): ?Alias { - return $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch()); + return self::$cache[$shortcode] ??= $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch()); } } diff --git a/Web/Models/Repositories/Applications.php b/Web/Models/Repositories/Applications.php index 0687856e6..e46e1f54f 100644 --- a/Web/Models/Repositories/Applications.php +++ b/Web/Models/Repositories/Applications.php @@ -1,5 +1,9 @@ -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); + return is_null($ar) ? null : new Application($ar); } - - function get(int $id): ?Application + + public function get(int $id): ?Application { - return $this->toApp($this->apps->get($id)); + return self::$cache[$id] ??= $this->toApp($this->apps->get($id)); } - - function getList(int $page = 1, ?int $perPage = NULL): \Traversable + + public 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) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $apps = $this->apps->where(["enabled" => 1, "deleted" => 0])->page($page, $perPage); + foreach ($apps as $app) { yield new Application($app); + } } - - function getListCount(): int + + public function getListCount(): int { - return sizeof($this->apps->where("enabled", 1)); + return sizeof($this->apps->where(["enabled" => 1, "deleted" => 0])); } - - function getByOwner(User $owner, int $page = 1, ?int $perPage = NULL): \Traversable + + public 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) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $apps = $this->apps->where(["owner" => $owner->getId(), "deleted" => 0])->page($page, $perPage); + foreach ($apps as $app) { yield new Application($app); + } } - - function getOwnCount(User $owner): int + + public function getOwnCount(User $owner): int { - return sizeof($this->apps->where("owner", $owner->getId())); + return sizeof($this->apps->where(["owner" => $owner->getId(), "deleted" => 0])); } - - function getInstalled(User $user, int $page = 1, ?int $perPage = NULL): \Traversable + + public 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) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $apps = $this->appRels->where(["user" => $user->getId(), "deleted" => 0])->page($page, $perPage); + foreach ($apps as $appRel) { yield $this->get($appRel->app); + } } - - function getInstalledCount(User $user): int + + public function getInstalledCount(User $user): int { - return sizeof($this->appRels->where("user", $user->getId())); + return sizeof($this->appRels->where(["user" => $user->getId(), "deleted" => 0])); } - function find(string $query, array $pars = [], string $sort = "id"): Util\EntityStream + public function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; - $result = $this->apps->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("enabled", 1); - - return new Util\EntityStream("Application", $result->order("$sort")); + $query = "%$query%"; + $result = $this->apps->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where(["enabled" => 1, "deleted" => 0]); + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Application", $result); } -} \ No newline at end of file +} diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php new file mode 100644 index 000000000..0a7b07e81 --- /dev/null +++ b/Web/Models/Repositories/Audios.php @@ -0,0 +1,341 @@ +context = DatabaseConnection::i()->getContext(); + $this->audios = $this->context->table("audios"); + $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); + $this->playlistRels = $this->context->table("playlist_relations"); + } + + private function toAudio(?ActiveRow $ar): ?Audio + { + return is_null($ar) ? null : new Audio($ar); + } + + private function toPlaylist(?ActiveRow $ar): ?Playlist + { + return is_null($ar) ? null : new Playlist($ar); + } + + public function get(int $id): ?Audio + { + return self::$cache[$id] ??= $this->toAudio($this->audios->get($id)); + } + + public function getPlaylist(int $id): ?Playlist + { + return self::$cachePlaylist[$id] ??= $this->toPlaylist($this->playlists->get($id)); + } + + public function getByOwnerAndVID(int $owner, int $vId): ?Audio + { + $audio = $this->audios->where([ + "owner" => $owner, + "virtual_id" => $vId, + ])->fetch(); + + return $this->toAudio($audio); + } + + public function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist + { + $playlist = $this->playlists->where([ + "owner" => $owner, + "id" => $vId, + ])->fetch(); + + return $this->toPlaylist($playlist); + } + + public function getByEntityID(int $entity, int $offset = 0, ?int $limit = null, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("index DESC"); + foreach ($iter as $rel) { + $audio = $this->get($rel->audio); + if (!$audio || $audio->isDeleted()) { + $deleted++; + continue; + } + + yield $audio; + } + } + + public function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = null, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach ($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if (!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + + public function getByUser(User $user, int $page = 1, ?int $perPage = null, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + public function getRandomThreeAudiosByEntityId(int $id): array + { + $iter = $this->rels->where("entity", $id); + $ids = []; + + foreach ($iter as $it) { + $ids[] = $it->audio; + } + + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, 0, 3); + $audios = []; + + foreach ($ids as $id) { + $audio = $this->get((int) $id); + + if (!$audio || $audio->isDeleted()) { + continue; + } + + $audios[] = $audio; + } + + return $audios; + } + + public function getByClub(Club $club, int $page = 1, ?int $perPage = null, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + public function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = null, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + public function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = null, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + public function getCollectionSizeByEntityId(int $id): int + { + return sizeof($this->rels->where("entity", $id)); + } + + public function getUserCollectionSize(User $user): int + { + return sizeof($this->rels->where("entity", $user->getId())); + } + + public function getClubCollectionSize(Club $club): int + { + return sizeof($this->rels->where("entity", $club->getId() * -1)); + } + + public function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + public function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + + public function getByUploader(User $user): EntityStream + { + $search = $this->audios->where([ + "owner" => $user->getId(), + "deleted" => 0, + ]); + + return new EntityStream("Audio", $search); + } + + public function getGlobal(int $order, ?string $genreId = null): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + "unlisted" => 0, + "withdrawn" => 0, + ])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC"); + + if (!is_null($genreId)) { + $search = $search->where("genre", $genreId); + } + + return new EntityStream("Audio", $search); + } + + public function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream + { + $columns = $performerOnly ? "performer" : "performer, name"; + $order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC"; + + $search = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH ($columns) AGAINST (? IN BOOLEAN MODE)", "%$query%")->order($order); + + if ($withLyrics) { + $search = $search->where("lyrics IS NOT NULL"); + } + + return new EntityStream("Audio", $search); + } + + public function searchPlaylists(string $query): EntityStream + { + $search = $this->playlists->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } + + public function getNew(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25)); + } + + public function getPopular(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25)); + } + + public function isAdded(int $user_id, int $audio_id): bool + { + return !is_null($this->rels->where([ + "entity" => $user_id, + "audio" => $audio_id, + ])->fetch()); + } + + public function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false], int $page = 1, ?int $perPage = null): \Traversable + { + $query = "%$query%"; + $result = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + /*"withdrawn" => 0, + "processed" => 1,*/ + ]); + $order_str = (in_array($order['type'], ['id', 'length', 'listens']) ? $order['type'] : 'id') . ' ' . ($order['invert'] ? 'ASC' : 'DESC'); + ; + + if (($params["only_performers"] ?? null) == "1") { + $result->where("performer LIKE ?", $query); + } else { + $result->where("CONCAT_WS(' ', performer, name) LIKE ?", $query); + } + + foreach ($params as $paramName => $paramValue) { + if (is_null($paramValue) || $paramValue == '') { + continue; + } + + switch ($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case "with_lyrics": + $result->where("lyrics IS NOT NULL"); + break; + case 'genre': + if ($paramValue == 'any') { + break; + } + + $result->where("genre", $paramValue); + break; + } + } + + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Audio", $result); + } + + public function findPlaylists(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): \Traversable + { + $query = "%$query%"; + $result = $this->playlists->where([ + "deleted" => 0, + ])->where("CONCAT_WS(' ', name, description) LIKE ?", $query); + $order_str = (in_array($order['type'], ['id', 'length', 'listens']) ? $order['type'] : 'id') . ' ' . ($order['invert'] ? 'ASC' : 'DESC'); + + if (is_null($params['from_me']) || empty($params['from_me'])) { + $result->where(["unlisted" => 0]); + } + + foreach ($params as $paramName => $paramValue) { + if (is_null($paramValue) || $paramValue == '') { + continue; + } + + switch ($paramName) { + # БУДЬ МАКСИМАЛЬНО АККУРАТЕН С ДАННЫМ ПАРАМЕТРОМ + case "from_me": + $result->where("owner", $paramValue); + break; + } + } + + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Playlist", $result); + } +} diff --git a/Web/Models/Repositories/BannedLinks.php b/Web/Models/Repositories/BannedLinks.php index 8f93e6fd3..d18da8ed7 100644 --- a/Web/Models/Repositories/BannedLinks.php +++ b/Web/Models/Repositories/BannedLinks.php @@ -1,5 +1,9 @@ -context = DB::i()->getContext(); $this->bannedLinks = $this->context->table("links_banned"); } - function toBannedLink(?ActiveRow $ar): ?BannedLink + public function toBannedLink(?ActiveRow $ar): ?BannedLink { - return is_null($ar) ? NULL : new BannedLink($ar); + return is_null($ar) ? null : new BannedLink($ar); } - function get(int $id): ?BannedLink + public function get(int $id): ?BannedLink { - return $this->toBannedLink($this->bannedLinks->get($id)); + return self::$cache[$id] ??= $this->toBannedLink($this->bannedLinks->get($id)); } - function getList(?int $page = 1): \Traversable + public function getList(?int $page = 1): \Traversable { - foreach($this->bannedLinks->order("id DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $link) + foreach ($this->bannedLinks->order("id DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $link) { yield new BannedLink($link); + } } - function getCount(int $page = 1): int + public function getCount(int $page = 1): int { return sizeof($this->bannedLinks->fetch()); } - function getByDomain(string $domain): ?Selection + public function getByDomain(string $domain): ?Selection { return $this->bannedLinks->where("domain", $domain); } - function isDomainBanned(string $domain): bool + public function isDomainBanned(string $domain): bool { - return sizeof($this->bannedLinks->where(["link" => $domain, "regexp_rule" => ""])) > 0; + return sizeof($this->bannedLinks->where(["domain" => $domain, "regexp_rule" => ""])) > 0; } - function genLinks($rules): \Traversable + public function genLinks($rules): \Traversable { - foreach ($rules as $rule) + foreach ($rules as $rule) { yield $this->get($rule->id); + } } - function genEntries($links, $uri): \Traversable + public function genEntries($links, $uri): \Traversable { - foreach($links as $link) - if (preg_match($link->getRegexpRule(), $uri)) + foreach ($links as $link) { + if (preg_match($link->getRegexpRule(), $uri)) { + yield $link->getId(); + } elseif ($this->isDomainBanned($link->getDomain())) { yield $link->getId(); + } + } } - function check(string $url): ?array + public function check(string $url): ?array { - $uri = strstr(str_replace(["https://", "http://"], "", $url), "/", true); - $domain = str_replace("www.", "", $uri); + $uri = str_replace(["https://", "http://"], "", $url); + $domain = explode("/", str_replace("www.", "", $uri))[0]; $rules = $this->getByDomain($domain); - if (is_null($rules)) - return NULL; + if (is_null($rules)) { + return null; + } return iterator_to_array($this->genEntries($this->genLinks($rules), $uri)); } -} \ No newline at end of file +} diff --git a/Web/Models/Repositories/Bans.php b/Web/Models/Repositories/Bans.php index 7123459df..16ff848b5 100644 --- a/Web/Models/Repositories/Bans.php +++ b/Web/Models/Repositories/Bans.php @@ -1,5 +1,9 @@ -context = DB::i()->getContext(); $this->bans = $this->context->table("bans"); } - function toBan(?ActiveRow $ar): ?Ban + public function toBan(?ActiveRow $ar): ?Ban { - return is_null($ar) ? NULL : new Ban($ar); + return is_null($ar) ? null : new Ban($ar); } - function get(int $id): ?Ban + public function get(int $id): ?Ban { return $this->toBan($this->bans->get($id)); } - function getByUser(int $user_id): \Traversable + public function getByUser(int $user_id): \Traversable { - foreach ($this->bans->where("user", $user_id) as $ban) + foreach ($this->bans->where("user", $user_id) as $ban) { yield new Ban($ban); + } } -} \ No newline at end of file +} diff --git a/Web/Models/Repositories/ChandlerGroups.php b/Web/Models/Repositories/ChandlerGroups.php index 45af2a620..cc5b5a2f3 100644 --- a/Web/Models/Repositories/ChandlerGroups.php +++ b/Web/Models/Repositories/ChandlerGroups.php @@ -1,5 +1,9 @@ -perms = $this->context->table("ChandlerACLGroupsPermissions"); } - function get(string $UUID): ?ActiveRow + public function get(string $UUID): ?ActiveRow { - return $this->groups->where("id", $UUID)->fetch(); + return self::$cache[$UUID] ??= $this->groups->where("id", $UUID)->fetch(); } - function getList(): \Traversable + public function getList(): \Traversable { - foreach($this->groups as $group) yield $group; + foreach ($this->groups as $group) { + yield $group; + } } - function getMembersById(string $UUID): \Traversable + public function getMembersById(string $UUID): \Traversable { - foreach($this->members->where("group", $UUID) as $member) - yield (new Users)->getByChandlerUser( + foreach ($this->members->where("group", $UUID) as $member) { + yield (new Users())->getByChandlerUser( new ChandlerUser($this->context->table("ChandlerUsers")->where("id", $member->user)->fetch()) ); + } + } + + public function getUsersMemberships(string $UUID): \Traversable + { + foreach ($this->members->where("user", $UUID) as $member) { + yield $member; + } } - function getUsersMemberships(string $UUID): \Traversable + public function getPermissionsById(string $UUID): \Traversable { - foreach($this->members->where("user", $UUID) as $member) yield $member; + foreach ($this->perms->where("group", $UUID) as $perm) { + yield $perm; + } } - function getPermissionsById(string $UUID): \Traversable + public function isUserAMember(string $GID, string $UID): bool { - foreach($this->perms->where("group", $UUID) as $perm) yield $perm; + return $this->context->query("SELECT * FROM `ChandlerACLRelations` WHERE `group` = ? AND `user` = ?", $GID, $UID)->getRowCount() > 0; } } diff --git a/Web/Models/Repositories/ChandlerUsers.php b/Web/Models/Repositories/ChandlerUsers.php index 510e58604..7bd118a69 100644 --- a/Web/Models/Repositories/ChandlerUsers.php +++ b/Web/Models/Repositories/ChandlerUsers.php @@ -1,5 +1,9 @@ -context = DB::i()->getContext(); @@ -18,23 +24,24 @@ public function __construct() private function toUser(?ActiveRow $ar): ?ChandlerUser { - return is_null($ar) ? NULL : (new User($ar))->getChandlerUser(); + return is_null($ar) ? null : (new User($ar))->getChandlerUser(); } - function get(int $id): ?ChandlerUser + public function get(int $id): ?ChandlerUser { - return (new Users)->get($id)->getChandlerUser(); + return self::$cache[$id] ??= (new Users())->get($id)->getChandlerUser(); } - function getById(string $UUID): ?ChandlerUser + public function getById(string $UUID): ?ChandlerUser { $user = $this->users->where("id", $UUID)->fetch(); - return $user ? new ChandlerUser($user) : NULL; + return $user ? new ChandlerUser($user) : null; } - function getList(int $page = 1): \Traversable + public function getList(int $page = 1): \Traversable { - foreach($this->users as $user) + foreach ($this->users as $user) { yield new ChandlerUser($user); + } } } diff --git a/Web/Models/Repositories/Clubs.php b/Web/Models/Repositories/Clubs.php index 04bb30abd..2a8b3df51 100644 --- a/Web/Models/Repositories/Clubs.php +++ b/Web/Models/Repositories/Clubs.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->clubs = $this->context->table("groups"); $this->coadmins = $this->context->table("group_coadmins"); } - + private function toClub(?ActiveRow $ar): ?Club { - return is_null($ar) ? NULL : new Club($ar); + return is_null($ar) ? null : new Club($ar); } - - function getByShortURL(string $url): ?Club + + public function getByShortURL(string $url): ?Club { $shortcode = $this->toClub($this->clubs->where("shortcode", $url)->fetch()); - if ($shortcode) + if ($shortcode) { return $shortcode; + } - $alias = (new Aliases)->getByShortcode($url); + $alias = (new Aliases())->getByShortcode($url); - if (!$alias) return NULL; - if ($alias->getType() !== "club") return NULL; + if (!$alias) { + return null; + } + if ($alias->getType() !== "club") { + return null; + } return $alias->getClub(); } - - function get(int $id): ?Club + + public function get(int $id): ?Club + { + return self::$cache[$id] ??= $this->toClub($this->clubs->get($id)); + } + + public function getByIds(array $ids = []): array { - return $this->toClub($this->clubs->get($id)); + $clubs = $this->clubs->select('*')->where('id IN (?)', $ids); + $clubs_array = []; + + foreach ($clubs as $club) { + $clubs_array[] = $this->toClub($club); + } + + return $clubs_array; } - - function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable + + public function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false], int $page = 1, ?int $perPage = null): \Traversable { - $query = "%$query%"; - $result = $this->clubs->where("name LIKE ? OR about LIKE ?", $query, $query); - - return new Util\EntityStream("Club", $result->order($sort)); + $query = "%$query%"; + $result = $this->clubs; + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + $result = $result->where("name LIKE ? OR about LIKE ?", $query, $query); + + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Club", $result); } - function getCount(): int + public function getCount(): int { - return sizeof(clone $this->clubs); + return (clone $this->clubs)->count('*'); } - function getPopularClubs(): \Traversable + public function getPopularClubs(): ?\Traversable { // TODO rewrite - + /* $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); @@ -71,27 +109,27 @@ function getPopularClubs(): \Traversable "subscriptions" => $entry["subscriptions"], ]; */ + trigger_error("Clubs::getPopularClubs() is currently commented out and returns null", E_USER_WARNING); + return null; } - - function getWriteableClubs(int $id): \Traversable + + public function getWriteableClubs(int $id): \Traversable { $result = $this->clubs->where("owner", $id); $coadmins = $this->coadmins->where("user", $id); - - foreach($result as $entry) { + + foreach ($result as $entry) { yield new Club($entry); } - foreach($coadmins as $coadmin) { + foreach ($coadmins as $coadmin) { $cl = new Manager($coadmin); yield $cl->getClub(); } } - function getWriteableClubsCount(int $id): int + public function getWriteableClubsCount(int $id): int { return sizeof($this->clubs->where("owner", $id)) + sizeof($this->coadmins->where("user", $id)); } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Comments.php b/Web/Models/Repositories/Comments.php index f4b8e5ace..60be711b2 100644 --- a/Web/Models/Repositories/Comments.php +++ b/Web/Models/Repositories/Comments.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->comments = $this->context->table("comments"); } - + private function toComment(?ActiveRow $ar): ?Comment { - return is_null($ar) ? NULL : new Comment($ar); + return is_null($ar) ? null : new Comment($ar); } - - function get(int $id): ?Comment + + public function get(int $id): ?Comment { - return $this->toComment($this->comments->get($id)); + return self::$cache[$id] ??= $this->toComment($this->comments->get($id)); } - - function getCommentsByTarget(Postable $target, int $page, ?int $perPage = NULL, ?string $sort = "ASC"): \Traversable + + public 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)->order("created ".$sort);; - - foreach($comments as $comment) + ])->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("created " . $sort); + ; + + foreach ($comments as $comment) { yield $this->toComment($comment); + } } - function getLastCommentsByTarget(Postable $target, ?int $count = NULL): \Traversable + public function getLastCommentsByTarget(Postable $target, ?int $count = null): \Traversable { $comments = $this->comments->where([ "model" => get_class($target), "target" => $target->getId(), "deleted" => false, ])->page(1, $count ?? OPENVK_DEFAULT_PER_PAGE)->order("created DESC"); - + $comments = array_reverse(iterator_to_array($comments)); - foreach($comments as $comment) + foreach ($comments as $comment) { yield $this->toComment($comment); + } } - - function getCommentsCountByTarget(Postable $target): int + + public function getCommentsCountByTarget(Postable $target): int { return sizeof($this->comments->where([ "model" => get_class($target), @@ -60,34 +70,32 @@ function getCommentsCountByTarget(Postable $target): int ])); } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + public function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; - - $notNullParams = []; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; - - $result = $this->comments->where("content LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); - - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); - break; - } + $result = $this->comments->where("content LIKE ?", "%$query%")->where("deleted", 0); + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + foreach ($params as $paramName => $paramValue) { + switch ($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; } } - return new Util\EntityStream("Comment", $result->order("$sort")); + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Comment", $result); } } diff --git a/Web/Models/Repositories/ContentSearchRepository.php b/Web/Models/Repositories/ContentSearchRepository.php index 65f03d937..b7cd7ea15 100644 --- a/Web/Models/Repositories/ContentSearchRepository.php +++ b/Web/Models/Repositories/ContentSearchRepository.php @@ -1,5 +1,9 @@ -ctx = DatabaseConnection::i()->getContext(); $this->builder = $this->ctx; } - + private function markParameterAsPassed(string $param): void { - if(!in_array($param, $this->passedParams)) + if (!in_array($param, $this->passedParams)) { $this->passedParams[] = $param; + } } - - function setContentType() - { - - } + + public function setContentType() {} } diff --git a/Web/Models/Repositories/Conversations.php b/Web/Models/Repositories/Conversations.php deleted file mode 100644 index 191795214..000000000 --- a/Web/Models/Repositories/Conversations.php +++ /dev/null @@ -1,47 +0,0 @@ -context = DB::i()->getContext(); - $this->convos = $this->context->table("conversations"); - } - - private function toConversation(?ActiveRow $ar): ?M\AbstractConversation - { - if(is_null($ar)) - return NULL; - else if($ar->is_pm) - return new M\PrivateConversation($ar); - else - return new M\Conversation($ar); - } - - function get(int $id): ?M\AbstractConversation - { - return $this->toConversation($this->convos->get($id)); - } - - 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); - } - - function getPrivateConversation(User $user, int $peer): M\PrivateConversation - { - ; - } -} diff --git a/Web/Models/Repositories/CurrentUser.php b/Web/Models/Repositories/CurrentUser.php index c6cb942bf..cec41b813 100644 --- a/Web/Models/Repositories/CurrentUser.php +++ b/Web/Models/Repositories/CurrentUser.php @@ -1,5 +1,9 @@ -user = $user; + } - if ($ip) + if ($ip) { $this->ip = $ip; + } - if ($useragent) + if ($useragent) { $this->useragent = $useragent; + } } public static function get($user, $ip, $useragent) { - if (self::$instance === null) self::$instance = new self($user, $ip, $useragent); + if (self::$instance === null) { + self::$instance = new self($user, $ip, $useragent); + } return self::$instance; } diff --git a/Web/Models/Repositories/Documents.php b/Web/Models/Repositories/Documents.php new file mode 100644 index 000000000..57c957e9d --- /dev/null +++ b/Web/Models/Repositories/Documents.php @@ -0,0 +1,177 @@ +context = DatabaseConnection::i()->getContext(); + $this->documents = $this->context->table("documents"); + } + + private function toDocument(?ActiveRow $ar): ?Document + { + return is_null($ar) ? null : new Document($ar); + } + + public function get(int $id): ?Document + { + return self::$cache[$id] ??= $this->toDocument($this->documents->get($id)); + } + + # By "Virtual ID" and "Absolute ID" (to not leak owner's id). + public function getDocumentById(int $virtual_id, int $real_id, string $access_key = null): ?Document + { + $doc = $this->documents->where(['virtual_id' => $virtual_id, 'id' => $real_id]); + /*if($access_key) { + $doc->where("access_key", $access_key); + }*/ + + $doc = $doc->fetch(); + if (is_null($doc)) { + return null; + } + + $n_doc = new Document($doc); + if (!$n_doc->checkAccessKey($access_key)) { + return null; + } + + return $n_doc; + } + + public function getDocumentByIdUnsafe(int $virtual_id, int $real_id): ?Document + { + $doc = $this->documents->where(['virtual_id' => $virtual_id, 'id' => $real_id]); + + $doc = $doc->fetch(); + if (is_null($doc)) { + return null; + } + + $n_doc = new Document($doc); + + return $n_doc; + } + + public function getDocumentsByOwner(int $owner, int $order = 0, int $type = -1): EntityStream + { + $search = $this->documents->where([ + "owner" => $owner, + "unlisted" => 0, + "deleted" => 0, + ]); + + if (in_array($type, [1,2,3,4,5,6,7,8])) { + $search->where("type", $type); + } + + switch ($order) { + case 0: + $search->order("id DESC"); + break; + case 1: + $search->order("name DESC"); + break; + case 2: + $search->order("filesize DESC"); + break; + } + + return new EntityStream("Document", $search); + } + + public function getTypes(int $owner_id): array + { + $result = DatabaseConnection::i()->getConnection()->query("SELECT `type`, COUNT(*) AS `count` FROM `documents` WHERE `owner` = ? AND `deleted` = 0 AND `unlisted` = 0 GROUP BY `type` ORDER BY `type`", $owner_id); + $response = []; + foreach ($result as $res) { + if ($res->count < 1 || $res->type == 0) { + continue; + } + + $name = tr("document_type_" . $res->type); + $response[] = [ + "count" => $res->count, + "type" => $res->type, + "name" => $name, + ]; + } + + return $response; + } + + public function getTags(int $owner_id, ?int $type = 0): array + { + $query = "SELECT `tags` FROM `documents` WHERE `owner` = ? AND `deleted` = 0 AND `unlisted` = 0 "; + if ($type > 0 && $type < 9) { + $query .= "AND `type` = $type"; + } + + $query .= " AND `tags` IS NOT NULL ORDER BY `id`"; + $result = DatabaseConnection::i()->getConnection()->query($query, $owner_id); + $tags = []; + foreach ($result as $res) { + $tags[] = $res->tags; + } + $imploded_tags = implode(",", $tags); + $exploded_tags = array_values(array_unique(explode(",", $imploded_tags))); + if ($exploded_tags[0] == "") { + return []; + } + + return array_slice($exploded_tags, 0, 50); + } + + public function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream + { + $result = $this->documents->where("name LIKE ?", "%$query%")->where([ + "deleted" => 0, + "folder_id != " => 0, + ]); + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + foreach ($params as $paramName => $paramValue) { + switch ($paramName) { + case "type": + if ($paramValue < 1 || $paramValue > 8) { + break; + } + $result->where("type", $paramValue); + break; + case "tags": + $result->where("tags LIKE ?", "%$paramValue%"); + break; + case "from_me": + $result->where("owner", $paramValue); + break; + } + } + + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Document", $result); + } +} diff --git a/Web/Models/Repositories/EmailChangeVerifications.php b/Web/Models/Repositories/EmailChangeVerifications.php index 0e8c668bc..64cdd83f0 100644 --- a/Web/Models/Repositories/EmailChangeVerifications.php +++ b/Web/Models/Repositories/EmailChangeVerifications.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->verifications = $this->context->table("email_change_verifications"); } - function toEmailChangeVerification(?ActiveRow $ar): ?EmailChangeVerification + public function toEmailChangeVerification(?ActiveRow $ar): ?EmailChangeVerification { - return is_null($ar) ? NULL : new EmailChangeVerification($ar); + return is_null($ar) ? null : new EmailChangeVerification($ar); } - - function getByToken(string $token): ?EmailChangeVerification + + public function getByToken(string $token): ?EmailChangeVerification { return $this->toEmailChangeVerification($this->verifications->where("key", $token)->fetch()); } - - function getLatestByUser(User $user): ?EmailChangeVerification + + public function getLatestByUser(User $user): ?EmailChangeVerification { return $this->toEmailChangeVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch()); } diff --git a/Web/Models/Repositories/Faves.php b/Web/Models/Repositories/Faves.php new file mode 100644 index 000000000..48b404d77 --- /dev/null +++ b/Web/Models/Repositories/Faves.php @@ -0,0 +1,52 @@ +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('*'); + } +} diff --git a/Web/Models/Repositories/Gifts.php b/Web/Models/Repositories/Gifts.php index 3baa23976..ba6d9028f 100644 --- a/Web/Models/Repositories/Gifts.php +++ b/Web/Models/Repositories/Gifts.php @@ -1,51 +1,63 @@ -context = DatabaseConnection::i()->getContext(); $this->gifts = $this->context->table("gifts"); $this->cats = $this->context->table("gift_categories"); } - - function get(int $id): ?Gift + + private function toGift(?ActiveRow $ar): ?Gift + { + return is_null($ar) ? null : new Gift($ar); + } + + private function toGiftCategory(?ActiveRow $ar): ?GiftCategory { - $gift = $this->gifts->get($id); - if(!$gift) - return NULL; - - return new Gift($gift); + return is_null($ar) ? null : new GiftCategory($ar); } - - function getCat(int $id): ?GiftCategory + + public function get(int $id): ?Gift { - $cat = $this->cats->get($id); - if(!$cat) - return NULL; - - return new GiftCategory($cat); + return self::$cache[$id] ??= $this->toGift($this->gifts->get($id)); } - - function getCategories(int $page, ?int $perPage = NULL, &$count = nullptr): \Traversable + + public function getCat(int $id): ?GiftCategory + { + return self::$cache_category[$id] ??= $this->toGiftCategory($this->cats->get($id)); + } + + public function getCategories(int $page, ?int $perPage = null, &$count = nullptr): \Traversable { $cats = $this->cats->where("deleted", false); $count = $cats->count(); $cats = $cats->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); - foreach($cats as $cat) + foreach ($cats as $cat) { yield new GiftCategory($cat); + } } - function getCategoriesCount(): int + public function getCategoriesCount(): int { $cats = $this->cats->where("deleted", false); - return $cats->count(); + return $cats->count('*'); } } diff --git a/Web/Models/Repositories/IPs.php b/Web/Models/Repositories/IPs.php index 59fc65708..5991ccff4 100644 --- a/Web/Models/Repositories/IPs.php +++ b/Web/Models/Repositories/IPs.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->ips = $this->context->table("ip"); } - - function get(string $ip): ?IP + + public function get(string $ip): ?IP { $bip = inet_pton($ip); - if(!$bip) + if (!$bip) { throw new \UnexpectedValueException("Malformed IP address"); - + } + $res = $this->ips->where("ip", $bip)->fetch(); - if(!$res) { - $res = new IP; + if (!$res) { + $res = new IP(); $res->setIp($ip); $res->save(false); - + return $res; } - + return new IP($res); } } diff --git a/Web/Models/Repositories/Managers.php b/Web/Models/Repositories/Managers.php index 9f65cb189..a3cf636f0 100644 --- a/Web/Models/Repositories/Managers.php +++ b/Web/Models/Repositories/Managers.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); - $this->managers= $this->context->table("group_coadmins"); + $this->managers = $this->context->table("group_coadmins"); } - + private function toManager(?ActiveRow $ar): ?Manager { - return is_null($ar) ? NULL : new Manager($ar); + return is_null($ar) ? null : new Manager($ar); } - - function get(int $id): ?Manager + + public function get(int $id): ?Manager { return $this->toManager($this->managers->where("id", $id)->fetch()); } - function getByUserAndClub(int $user, int $club): ?Manager + public function getByUserAndClub(int $user, int $club): ?Manager { return $this->toManager($this->managers->where("user", $user)->where("club", $club)->fetch()); } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Messages.php b/Web/Models/Repositories/Messages.php index 538338ed5..ac89c63cb 100644 --- a/Web/Models/Repositories/Messages.php +++ b/Web/Models/Repositories/Messages.php @@ -1,51 +1,61 @@ -context = DatabaseConnection::i()->getContext(); $this->messages = $this->context->table("messages"); } - - function get(int $id): ?Message + + private function toMessage(?ActiveRow $ar): ?Message + { + return is_null($ar) ? null : new Message($ar); + } + + public function get(int $id): ?Message { - $msg = $this->messages->get($id); - if(!$msg) - return NULL; - - return new Message($msg); + return self::$cache[$id] ??= $this->toMessage($this->messages->get($id)); } - - function getCorrespondencies(RowModel $correspondent, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + + public function getCorrespondencies(RowModel $correspondent, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable { $id = $correspondent->getId(); $class = get_class($correspondent); $limit = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - $offset = $offset ?? ($page - 1) * $limit; + $offset ??= ($page - 1) * $limit; $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies.tsql"); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); $coresps = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class, $limit, $offset); - foreach($coresps as $c) { - if($c->class === 'openvk\Web\Models\Entities\User') - $anotherCorrespondent = (new Users)->get($c->id); - else if($c->class === 'openvk\Web\Models\Entities\Club') - $anotherCorrespondent = (new Clubs)->get($c->id); - + foreach ($coresps as $c) { + if ($c->class === 'openvk\Web\Models\Entities\User') { + $anotherCorrespondent = (new Users())->get($c->id); + } elseif ($c->class === 'openvk\Web\Models\Entities\Club') { + $anotherCorrespondent = (new Clubs())->get($c->id); + } + yield new Correspondence($correspondent, $anotherCorrespondent); } } - - function getCorrespondenciesCount(RowModel $correspondent): ?int + + public function getCorrespondenciesCount(RowModel $correspondent): ?int { $id = $correspondent->getId(); $class = get_class($correspondent); diff --git a/Web/Models/Repositories/NoSpamLogs.php b/Web/Models/Repositories/NoSpamLogs.php index f8dd49806..420cbc85c 100644 --- a/Web/Models/Repositories/NoSpamLogs.php +++ b/Web/Models/Repositories/NoSpamLogs.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->noSpamLogs = $this->context->table("noSpam_templates"); } - + private function toNoSpamLog(?ActiveRow $ar): ?NoSpamLog { - return is_null($ar) ? NULL : new NoSpamLog($ar); + return is_null($ar) ? null : new NoSpamLog($ar); } - - function get(int $id): ?NoSpamLog + + public function get(int $id): ?NoSpamLog { return $this->toNoSpamLog($this->noSpamLogs->get($id)); } - - function getList(array $filter = []): \Traversable + + public function getList(array $filter = []): \Traversable { - foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log) + foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log) { yield new NoSpamLog($log); + } } } diff --git a/Web/Models/Repositories/Notes.php b/Web/Models/Repositories/Notes.php index 0473070a6..73b2137ac 100644 --- a/Web/Models/Repositories/Notes.php +++ b/Web/Models/Repositories/Notes.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->notes = $this->context->table("notes"); } - + private function toNote(?ActiveRow $ar): ?Note { - return is_null($ar) ? NULL : new Note($ar); + return is_null($ar) ? null : new Note($ar); } - - function get(int $id): ?Note + + public function get(int $id): ?Note { - return $this->toNote($this->notes->get($id)); + return self::$cache[$id] ??= $this->toNote($this->notes->get($id)); } - - function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL, string $sort = "DESC"): \Traversable + + public function getUserNotes(User $user, int $page = 1, ?int $perPage = null, string $sort = "DESC"): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created $sort")->page($page, $perPage) as $album) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + foreach ($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created $sort")->page($page, $perPage) as $album) { yield new Note($album); + } } - function getNoteById(int $owner, int $note): ?Note + public function getNoteById(int $owner, int $note): ?Note { $note = $this->notes->where(['owner' => $owner, 'virtual_id' => $note])->fetch(); - if(!is_null($note)) + if (!is_null($note)) { return new Note($note); - else - return NULL; + } else { + return null; + } } - - function getUserNotesCount(User $user): int + + public function getUserNotesCount(User $user): int { return sizeof($this->notes->where("owner", $user->getId())->where("deleted", 0)); } diff --git a/Web/Models/Repositories/Notifications.php b/Web/Models/Repositories/Notifications.php index bec0b04f8..833a1313f 100644 --- a/Web/Models/Repositories/Notifications.php +++ b/Web/Models/Repositories/Notifications.php @@ -1,114 +1,128 @@ -modelCodes = array_flip(json_decode(file_get_contents(__DIR__ . "/../../../data/modelCodes.json"), true)); } - + private function getEDB(bool $throw = true): ?object { $eax = $this->edbc ?? eventdb(); - if(!$eax && $throw) + if (!$eax && $throw) { throw new \RuntimeException("Event database err!"); - - return is_null($eax) ? NULL : $eax->getConnection(); + } + + return is_null($eax) ? null : $eax->getConnection(); } - + private function getModel(int $code, int $id): object { $repoClassName = str_replace("Entities", "Repositories", "\\" . $this->modelCodes[$code]) . "s"; - - return (new $repoClassName)->get($id); + + return (new $repoClassName())->get($id); } - - private function getQuery(User $user, bool $count = false, int $offset, bool $archived = false, int $page = 1, ?int $perPage = NULL): string + + private function getQuery(User $user, bool $count, int $offset, bool $archived = false, int $page = 1, ?int $perPage = null): string { $query = "SELECT " . ($count ? "COUNT(*) AS cnt" : "*") . " FROM notifications WHERE recipientType=0 "; $query .= "AND timestamp " . ($archived ? "<" : ">") . "$offset AND recipientId=" . $user->getId(); - if(!$count) { + if (!$count) { $query .= " ORDER BY timestamp DESC"; $query .= " LIMIT " . ($perPage ?? OPENVK_DEFAULT_PER_PAGE); $query .= " OFFSET " . (($page - 1) * ($perPage ?? OPENVK_DEFAULT_PER_PAGE)); } - + return $query; } - - private function assemble(int $act, int $originModelType, int $originModelId, int $targetModelType, int $targetModelId, int $recipientId, int $timestamp, $data, ?string $class = NULL): Notification + + private function assemble(int $act, int $originModelType, int $originModelId, int $targetModelType, int $targetModelId, int $recipientId, int $timestamp, $data, ?string $class = null): Notification { $class ??= 'openvk\Web\Models\Entities\Notifications\Notification'; - + $originModel = $this->getModel($originModelType, $originModelId); $targetModel = $this->getModel($targetModelType, $targetModelId); - $recipient = (new Users)->get($recipientId); - + $recipient = (new Users())->get($recipientId); + $notification = new $class($recipient, $originModel, $targetModel, $timestamp, $data); $notification->setActionCode($act); return $notification; } - - function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int + + public function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int { $db = $this->getEDB(false); - if(!$db) + if (!$db) { return 0; - + } + $results = $db->query($this->getQuery($user, true, $offset, $archived)); - + return $results->fetch()->cnt; } - - function getNotificationsByUser(User $user, int $offset, bool $archived = false, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getNotificationsByUser(User $user, int $offset, bool $archived = false, int $page = 1, ?int $perPage = null): \Traversable { $db = $this->getEDB(false); - if(!$db) { + if (!$db) { yield from []; return; } - + $results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage)); - foreach($results->fetchAll() as $notif) { + foreach ($results->fetchAll() as $notif) { + $class = 'openvk\Web\Models\Entities\Notifications\\'; + + switch ($notif->modelAction) { + case 0: + $class .= 'LikeNotification'; + break; + default: + $class .= 'Notification'; + break; + } + yield $this->assemble( $notif->modelAction, $notif->originModelType, $notif->originModelId, - $notif->targetModelType, $notif->targetModelId, - $notif->recipientId, $notif->timestamp, - $notif->additionalData + $notif->additionalData, + $class ); } } - - function fromDescriptor(string $descriptor, ?object &$parsedData = nullptr) + + public function fromArray(array $payload): Notification { - [$class, $recv, $data] = explode(",", $descriptor); - $class = str_replace(".", "\\", $class); - - $parsedData = unserialize(base64_decode($data)); + $class = isset($payload['class']) ? str_replace(".", "\\", $payload['class']) : null; + $data = $payload['data'] ?? $payload; + return $this->assemble( - $parsedData->actionCode, - $parsedData->originModelType, - $parsedData->originModelId, - - $parsedData->targetModelType, - $parsedData->targetModelId, - - $parsedData->recipient, - $parsedData->timestamp, - $parsedData->additionalPayload, + (int) $data['actionCode'], + (int) $data['originModelType'], + (int) $data['originModelId'], + (int) $data['targetModelType'], + (int) $data['targetModelId'], + (int) $data['recipient'], + (int) $data['timestamp'], + (string) ($data['additionalPayload'] ?? ""), + $class ); } } diff --git a/Web/Models/Repositories/Photos.php b/Web/Models/Repositories/Photos.php index 0698c9148..2f9d4b8cc 100644 --- a/Web/Models/Repositories/Photos.php +++ b/Web/Models/Repositories/Photos.php @@ -1,58 +1,71 @@ -context = DatabaseConnection::i()->getContext(); $this->photos = $this->context->table("photos"); } - - function get(int $id): ?Photo + + private function toPhoto(?ActiveRow $ar): ?Photo + { + return is_null($ar) ? null : new Photo($ar); + } + + public function get(int $id): ?Photo { - $photo = $this->photos->get($id); - if(!$photo) return NULL; - - return new Photo($photo); + return self::$cache[$id] ??= $this->toPhoto($this->photos->get($id)); } - - function getByOwnerAndVID(int $owner, int $vId): ?Photo + + public function getByOwnerAndVID(int $owner, int $vId): ?Photo { $photo = $this->photos->where([ "owner" => $owner, "virtual_id" => $vId, + "system" => 0, + "private" => 0, ])->fetch(); - if(!$photo) return NULL; - - return new Photo($photo); + return $this->toPhoto($photo); } - function getEveryUserPhoto(User $user, int $page = 1, ?int $perPage = NULL): \Traversable + public function getEveryUserPhoto(User $user, int $offset = 0, int $limit = 10): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $perPage ??= OPENVK_DEFAULT_PER_PAGE; $photos = $this->photos->where([ - "owner" => $user->getId(), - "deleted" => 0 + "owner" => $user->getId(), + "deleted" => 0, + "system" => 0, + "private" => 0, + "anonymous" => 0, ])->order("id DESC"); - foreach($photos->page($page, $perPage) as $photo) { - yield new Photo($photo); + foreach ($photos->limit($limit, $offset) as $photo) { + yield $this->toPhoto($photo); } } - function getUserPhotosCount(User $user) + public function getUserPhotosCount(User $user) { - $photos = $this->photos->where([ - "owner" => $user->getId(), - "deleted" => 0 - ]); - - return sizeof($photos); + return $this->photos->where([ + "owner" => $user->getId(), + "deleted" => 0, + "system" => 0, + "private" => 0, + "anonymous" => 0, + ])->count("*"); } } diff --git a/Web/Models/Repositories/Polls.php b/Web/Models/Repositories/Polls.php index c2ba720b4..bbd32a617 100644 --- a/Web/Models/Repositories/Polls.php +++ b/Web/Models/Repositories/Polls.php @@ -1,23 +1,31 @@ -polls = DatabaseConnection::i()->getContext()->table("polls"); } - - function get(int $id): ?Poll + + private function toPoll(?ActiveRow $ar): ?Poll + { + return is_null($ar) ? null : new Poll($ar); + } + + public function get(int $id): ?Poll { - $poll = $this->polls->get($id); - if(!$poll) - return NULL; - - return new Poll($poll); + return self::$cache[$id] ??= $this->toPoll($this->polls->get($id)); } -} \ No newline at end of file +} diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php index c354f1525..50dc2b7fb 100644 --- a/Web/Models/Repositories/Posts.php +++ b/Web/Models/Repositories/Posts.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->posts = $this->context->table("posts"); } - + private function toPost(?ActiveRow $ar): ?Post { - return is_null($ar) ? NULL : new Post($ar); + return is_null($ar) ? null : new Post($ar); } - - function get(int $id): ?Post + + public function get(int $id): ?Post { - return $this->toPost($this->posts->get($id)); + return self::$cache[$id] ??= $this->toPost($this->posts->get($id)); } - - function getPinnedPost(int $user): ?Post + + public function getPinnedPost(int $user): ?Post { $post = (clone $this->posts)->where([ "wall" => $user, "pinned" => true, "deleted" => false, ])->fetch(); - + return $this->toPost($post); } - - function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + + public function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable { $perPage ??= OPENVK_DEFAULT_PER_PAGE; $offset ??= $perPage * ($page - 1); - + $pinPost = $this->getPinnedPost($user); - if(is_null($offset) || $offset == 0) { - if(!is_null($pinPost)) { - if($page === 1) { + if (is_null($offset) || $offset == 0) { + if (!is_null($pinPost)) { + if ($page === 1) { $perPage--; - + yield $pinPost; } else { $offset--; } } - } else if(!is_null($offset)) { + } elseif (!is_null($offset) && $pinPost) { $offset--; } - + $sel = $this->posts->where([ - "wall" => $user, - "pinned" => false, - "deleted" => false, + "wall" => $user, + "pinned" => false, + "deleted" => false, + "suggested" => 0, ])->order("created DESC")->limit($perPage, $offset); - - foreach($sel as $post) + + foreach ($sel as $post) { + yield new Post($post); + } + } + + public function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + if ($user > 0) { + $sel->where("owner", $user); + } else { + $sel->where("flags !=", 0); + } + + $sel->order("created DESC")->limit($perPage, $offset); + + foreach ($sel as $post) { yield new Post($post); + } } - - function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + if ($user > 0) { + $sel->where("owner !=", $user); + } else { + $sel->where("flags", 0); + } + + $sel->order("created DESC")->limit($perPage, $offset); + + foreach ($sel as $post) { + yield new Post($post); + } + } + + public function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = null): \Traversable { $hashtag = "#$hashtag"; $sel = $this->posts ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") ->where("deleted", 0) ->order("created DESC") + ->where("suggested", 0) ->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); - - foreach($sel as $post) + + foreach ($sel as $post) { yield new Post($post); + } } - - function getPostCountByHashtag(string $hashtag): int + + public function getPostCountByHashtag(string $hashtag): int { $hashtag = "#$hashtag"; $sel = $this->posts ->where("content LIKE ?", "%$hashtag%") - ->where("deleted", 0); - + ->where("deleted", 0) + ->where("suggested", 0); + return sizeof($sel); } - - function getPostById(int $wall, int $post): ?Post + + public function getPostById(int $wall, int $post, bool $forceSuggestion = false): ?Post { - $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); - if(!is_null($post)) + $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post]); + + if (!$forceSuggestion) { + $post->where("suggested", 0); + } + + $post = $post->fetch(); + + if (!is_null($post)) { return new Post($post); - else - return NULL; - + } else { + return null; + } + } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + public function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; - - $notNullParams = []; + $query = "%$query%"; + $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0)->where("suggested", 0); + $order_str = 'id'; - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; + switch ($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } - $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); + foreach ($params as $paramName => $paramValue) { + if (is_null($paramValue) || $paramValue == '') { + continue; + } - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); + switch ($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + /*case 'die_in_agony': + $result->where("nsfw", 1); break; - } + case 'ads': + $result->where("ad", 1); + break;*/ + # БУДЬ МАКСИМАЛЬНО АККУРАТЕН С ДАННЫМ ПАРАМЕТРОМ + case 'from_me': + $result->where("owner", $paramValue); + break; + case 'wall_id': + $result->where("wall", $paramValue); + break; } } + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Post", $result); + } + + public function getPostCountOnUserWall(int $user): int + { + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])); + } + + public function getOwnersCountOnUserWall(int $user): int + { + if ($user > 0) { + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "owner" => $user])); + } else { + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags !=", 0)); + } + } + + public function getOthersCountOnUserWall(int $user): int + { + if ($user > 0) { + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])->where("owner !=", $user)); + } else { + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags", 0)); + } + } + + public function getSuggestedPosts(int $club, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); - return new Util\EntityStream("Post", $result->order("$sort")); + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach ($sel as $post) { + yield new Post($post); + } + } + + public function getSuggestedPostsCount(int $club) + { + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1])); + } + + public function getSuggestedPostsByUser(int $club, int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->where("owner", $user) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach ($sel as $post) { + yield new Post($post); + } } - function getPostCountOnUserWall(int $user): int + public function getSuggestedPostsCountByUser(int $club, int $user): int { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])); + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1, "owner" => $user])); } - function getCount(): int + public function getCount(): int { - return sizeof(clone $this->posts); + return (clone $this->posts)->count('*'); } } diff --git a/Web/Models/Repositories/Reports.php b/Web/Models/Repositories/Reports.php index edce8980f..c6b90526b 100644 --- a/Web/Models/Repositories/Reports.php +++ b/Web/Models/Repositories/Reports.php @@ -1,67 +1,83 @@ -context = DatabaseConnection::i()->getContext(); $this->reports = $this->context->table("reports"); } - + private function toReport(?ActiveRow $ar): ?Report { - return is_null($ar) ? NULL : new Report($ar); + return is_null($ar) ? null : new Report($ar); } - - function getReports(int $state = 0, int $page = 1, ?string $type = NULL, ?bool $pagination = true): \Traversable + + public function getReports(int $state = 0, int $page = 1, ?string $type = null, ?bool $pagination = true): \Traversable { $filter = ["deleted" => 0]; - if ($type) $filter["type"] = $type; + if ($type) { + $filter["type"] = $type; + } $reports = $this->reports->where($filter)->order("created DESC")->group("target_id, type"); - if ($pagination) + if ($pagination) { $reports = $reports->page($page, 15); + } - foreach($reports as $t) + foreach ($reports as $t) { yield new Report($t); + } } - - function getReportsCount(int $state = 0): int + + public function getReportsCount(int $state = 0): int { return sizeof($this->reports->where(["deleted" => 0, "type" => $state])->group("target_id, type")); } - - function get(int $id): ?Report + + public function get(int $id): ?Report { - return $this->toReport($this->reports->get($id)); + return self::$cache[$id] ??= $this->toReport($this->reports->get($id)); } - - function getByContentId(int $id): ?Report + + public function getByContentId(int $id): ?Report { $post = $this->reports->where(["deleted" => 0, "content_id" => $id])->fetch(); - if($post) + if ($post) { return new Report($post); - else - return null; + } else { + return null; + } } - function getDuplicates(string $type, int $target_id, ?int $orig = NULL, ?int $user_id = NULL): \Traversable + public function getDuplicates(string $type, int $target_id, ?int $orig = null, ?int $user_id = null): \Traversable { $filter = ["deleted" => 0, "type" => $type, "target_id" => $target_id]; - if ($orig) $filter[] = "id != $orig"; - if ($user_id) $filter["user_id"] = $user_id; + if ($orig) { + $filter[] = "id != $orig"; + } + if ($user_id) { + $filter["user_id"] = $user_id; + } - foreach ($this->reports->where($filter) as $report) + foreach ($this->reports->where($filter) as $report) { yield new Report($report); + } } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Repository.php b/Web/Models/Repositories/Repository.php index fe3d6afe1..450b76f6d 100644 --- a/Web/Models/Repositories/Repository.php +++ b/Web/Models/Repositories/Repository.php @@ -1,46 +1,53 @@ -context = DatabaseConnection::i()->getContext(); $this->table = $this->context->table($this->tableName); } - - function toEntity(?ActiveRow $ar) + + public function toEntity(?ActiveRow $ar) { $entityName = "openvk\\Web\\Models\\Entities\\$this->modelName"; - return is_null($ar) ? NULL : new $entityName($ar); + return is_null($ar) ? null : new $entityName($ar); } - - function get(int $id) + + public function get(int $id) { - return $this->toEntity($this->table->get($id)); + return self::$cache[$id] ??= $this->toEntity($this->table->get($id)); } - - function size(bool $withDeleted = false): int + + public function size(bool $withDeleted = false): int { - return sizeof($this->table->where("deleted", $withDeleted)); + return $this->table->where("deleted", $withDeleted)->count("*"); } - - function enumerate(int $page, ?int $perPage = NULL, bool $withDeleted = false): \Traversable + + public function enumerate(int $page, ?int $perPage = null, bool $withDeleted = false): \Traversable { $perPage ??= OPENVK_DEFAULT_PER_PAGE; - - foreach($this->table->where("deleted", $withDeleted)->page($page, $perPage) as $entity) + + foreach ($this->table->where("deleted", $withDeleted)->page($page, $perPage) as $entity) { yield $this->toEntity($entity); + } } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Restores.php b/Web/Models/Repositories/Restores.php index df9105a8c..372ad16d4 100644 --- a/Web/Models/Repositories/Restores.php +++ b/Web/Models/Repositories/Restores.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->restores = $this->context->table("password_resets"); } - - function toPasswordReset(?ActiveRow $ar): ?PasswordReset + + public function toPasswordReset(?ActiveRow $ar): ?PasswordReset { - return is_null($ar) ? NULL : new PasswordReset($ar); + return is_null($ar) ? null : new PasswordReset($ar); } - - function getByToken(string $token): ?PasswordReset + + public function getByToken(string $token): ?PasswordReset { return $this->toPasswordReset($this->restores->where("key", $token)->fetch()); } - - function getLatestByUser(User $user): ?PasswordReset + + public function getLatestByUser(User $user): ?PasswordReset { return $this->toPasswordReset($this->restores->where("profile", $user->getId())->order("timestamp DESC")->fetch()); } diff --git a/Web/Models/Repositories/SupportAgents.php b/Web/Models/Repositories/SupportAgents.php index 7b3a1e7ec..d83d1974a 100644 --- a/Web/Models/Repositories/SupportAgents.php +++ b/Web/Models/Repositories/SupportAgents.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->agents = $this->context->table("support_names"); @@ -17,16 +21,16 @@ function __construct() private function toAgent(?ActiveRow $ar) { - return is_null($ar) ? NULL : new SupportAgent($ar); + return is_null($ar) ? null : new SupportAgent($ar); } - function get(int $id): ?SupportAgent + public function get(int $id): ?SupportAgent { return $this->toAgent($this->agents->where("agent", $id)->fetch()); } - function isExists(int $id): bool + public function isExists(int $id): bool { return !is_null($this->get($id)); } -} \ No newline at end of file +} diff --git a/Web/Models/Repositories/SupportAliases.php b/Web/Models/Repositories/SupportAliases.php index cb1a2521b..45f4ce3ed 100644 --- a/Web/Models/Repositories/SupportAliases.php +++ b/Web/Models/Repositories/SupportAliases.php @@ -1,12 +1,15 @@ -toEntity($this->table->where("agent", $agent)->fetch()); } diff --git a/Web/Models/Repositories/TicketComments.php b/Web/Models/Repositories/TicketComments.php index ee05bb553..f009d261e 100644 --- a/Web/Models/Repositories/TicketComments.php +++ b/Web/Models/Repositories/TicketComments.php @@ -1,39 +1,48 @@ -context = DatabaseConnection::i()->getContext(); $this->comments = $this->context->table("tickets_comments"); } - - function getCommentsById(int $ticket_id): \Traversable + + public function getCommentsById(int $ticket_id): \Traversable { - foreach($this->comments->where(['ticket_id' => $ticket_id, 'deleted' => 0]) as $comment) yield new TicketComment($comment); + foreach ($this->comments->where(['ticket_id' => $ticket_id, 'deleted' => 0]) as $comment) { + yield new TicketComment($comment); + } } - function get(int $id): ?TicketComment + private function toTicketComment(?ActiveRow $ar): ?TicketComment { - $comment = $this->comments->get($id);; - if (!is_null($comment)) - return new TicketComment($comment); - else - return NULL; + return is_null($ar) ? null : new TicketComment($ar); } - function getCountByAgent(int $agent_id, int $mark = NULL): int + public function get(int $id): ?TicketComment + { + return self::$cache[$id] ??= $this->toTicketComment($this->comments->get($id)); + } + + public function getCountByAgent(int $agent_id, int $mark = null): int { $filter = ['user_id' => $agent_id, 'user_type' => 1]; $mark && $filter['mark'] = $mark; return sizeof($this->comments->where($filter)); } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Tickets.php b/Web/Models/Repositories/Tickets.php index 1e84ebd82..fd7662786 100644 --- a/Web/Models/Repositories/Tickets.php +++ b/Web/Models/Repositories/Tickets.php @@ -1,63 +1,73 @@ -context = DatabaseConnection::i()->getContext(); $this->tickets = $this->context->table("tickets"); } - + private function toTicket(?ActiveRow $ar): ?Ticket { - return is_null($ar) ? NULL : new Ticket($ar); + return is_null($ar) ? null : new Ticket($ar); } - - function getTickets(int $state = 0, int $page = 1): \Traversable + + public function getTickets(int $state = 0, int $page = 1): \Traversable { - foreach($this->tickets->where(["deleted" => 0, "type" => $state])->order("created DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $ticket) + foreach ($this->tickets->where(["deleted" => 0, "type" => $state])->order("created DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $ticket) { yield new Ticket($ticket); + } } - - function getTicketCount(int $state = 0): int + + public function getTicketCount(int $state = 0): int { - return sizeof($this->tickets->where(["deleted" => 0, "type" => $state])); + return $this->tickets->where(["deleted" => 0, "type" => $state])->count("*"); } - - function getTicketsByUserId(int $userId, int $page = 1): \Traversable + + public function getTicketsByUserId(int $userId, int $page = 1): \Traversable { - foreach($this->tickets->where(["user_id" => $userId, "deleted" => 0])->order("created DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $ticket) yield new Ticket($ticket); + foreach ($this->tickets->where(["user_id" => $userId, "deleted" => 0])->order("created DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $ticket) { + yield new Ticket($ticket); + } } - function getTicketsCountByUserId(int $userId, int $type = NULL): int + public function getTicketsCountByUserId(int $userId, int $type = null): int { - if(is_null($type)) + if (is_null($type)) { return sizeof($this->tickets->where(["user_id" => $userId, "deleted" => 0])); - else + } else { return sizeof($this->tickets->where(["user_id" => $userId, "deleted" => 0, "type" => $type])); + } } - - function getRequestById(int $requestId): ?Ticket + + public function getRequestById(int $requestId): ?Ticket { $requests = $this->tickets->where(["id" => $requestId])->fetch(); - if(!is_null($requests)) - return new Req($requests); - else - return NULL; - + if (!is_null($requests)) { + return new Ticket($requests); + } else { + return null; + } + } - - function get(int $id): ?Ticket + + public function get(int $id): ?Ticket { - return $this->toTicket($this->tickets->get($id)); + return self::$cache[$id] ??= $this->toTicket($this->tickets->get($id)); } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Topics.php b/Web/Models/Repositories/Topics.php index 1c1763102..cbfea1f9b 100644 --- a/Web/Models/Repositories/Topics.php +++ b/Web/Models/Repositories/Topics.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->topics = $this->context->table("topics"); @@ -18,56 +24,59 @@ function __construct() private function toTopic(?ActiveRow $ar): ?Topic { - return is_null($ar) ? NULL : new Topic($ar); + return is_null($ar) ? null : new Topic($ar); } - - function get(int $id): ?Topic + + public function get(int $id): ?Topic { - return $this->toTopic($this->topics->get($id)); + return self::$cache[$id] ??= $this->toTopic($this->topics->get($id)); } - function getTopicById(int $club, int $topic): ?Topic + public function getTopicById(int $club, int $topic): ?Topic { return $this->toTopic($this->topics->where(["group" => $club, "virtual_id" => $topic, "deleted" => 0])->fetch()); } - - function getClubTopics(Club $club, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getClubTopics(Club $club, int $page = 1, ?int $perPage = null): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $perPage ??= OPENVK_DEFAULT_PER_PAGE; # 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; - foreach(DatabaseConnection::i()->getConnection()->query($query, $club->getId(), $club->getId()) as $topic) { + foreach (DatabaseConnection::i()->getConnection()->query($query, $club->getId(), $club->getId()) as $topic) { $topic = $this->get($topic->id); - if(!$topic) continue; - + if (!$topic) { + continue; + } + yield $topic; } } - - function getClubTopicsCount(Club $club): int + + public function getClubTopicsCount(Club $club): int { return sizeof($this->topics->where([ "group" => $club->getId(), - "deleted" => false + "deleted" => false, ])); } - function find(Club $club, string $query): \Traversable + public function find(Club $club, string $query): \Traversable { return new Util\EntityStream("Topic", $this->topics->where("title LIKE ? AND group = ? AND deleted = 0", "%$query%", $club->getId())); } - function getLastTopics(Club $club, ?int $count = NULL): \Traversable + public function getLastTopics(Club $club, ?int $count = null): \Traversable { $topics = $this->topics->where([ "group" => $club->getId(), - "deleted" => false + "deleted" => false, ])->page(1, $count ?? OPENVK_DEFAULT_PER_PAGE)->order("created DESC"); - - foreach($topics as $topic) + + foreach ($topics as $topic) { yield $this->toTopic($topic); + } } } diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index de0d341d5..147fb9b50 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -1,5 +1,9 @@ -context = DatabaseConnection::i()->getContext(); $this->users = $this->context->table("profiles"); $this->aliases = $this->context->table("aliases"); } - + private function toUser(?ActiveRow $ar): ?User { - return is_null($ar) ? NULL : new User($ar); + return is_null($ar) ? null : new User($ar); } - - function get(int $id): ?User + + public function get(int $id): ?User + { + return self::$cache[$id] ??= $this->toUser($this->users->get($id)); + } + + public function getByIds(array $ids = []): array { - return $this->toUser($this->users->get($id)); + $users = $this->users->select('*')->where('id IN (?)', $ids); + $users_array = []; + + foreach ($users as $user) { + $users_array[] = $this->toUser($user); + } + + return $users_array; } - - function getByShortURL(string $url): ?User + + public function getByShortURL(string $url): ?User { $shortcode = $this->toUser($this->users->where("shortcode", $url)->fetch()); - if ($shortcode) + if ($shortcode) { return $shortcode; + } - $alias = (new Aliases)->getByShortcode($url); + $alias = (new Aliases())->getByShortcode($url); + + if (!$alias) { + return null; + } + if ($alias->getType() !== "user") { + return null; + } - if (!$alias) return NULL; - if ($alias->getType() !== "user") return NULL; - return $alias->getUser(); } - - function getByChandlerUser(?ChandlerUser $user): ?User + + public function getByChandlerUserId(string $cid): ?User + { + return $this->toUser($this->users->where("user", $cid)->fetch()); + } + + public function getByChandlerUser(?ChandlerUser $user): ?User { - return $user ? $this->toUser($this->users->where("user", $user->getId())->fetch()) : NULL; + return $user ? $this->getByChandlerUserId($user->getId()) : null; } - - function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream + + public function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; + $query = "%$query%"; $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0); - - $notNullParams = []; - $nnparamsCount = 0; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after" && $paramName != "gender" && $paramName != "maritalstatus" && $paramName != "politViews" && $paramName != "doNotSearchMe") - $paramValue != NULL ? $notNullParams += ["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams += ["$paramName" => "$paramValue"] : NULL; - - $nnparamsCount = sizeof($notNullParams); - - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "hometown": - $result->where("hometown LIKE ?", $paramValue); - break; - case "city": - $result->where("city LIKE ?", $paramValue); - break; - case "maritalstatus": - $result->where("marital_status ?", $paramValue); - break; - case "status": - $result->where("status LIKE ?", $paramValue); - break; - case "politViews": - $result->where("polit_views ?", $paramValue); - break; - case "email": - $result->where("email_contact LIKE ?", $paramValue); - break; - case "telegram": - $result->where("telegram LIKE ?", $paramValue); - break; - case "site": - $result->where("telegram LIKE ?", $paramValue); - break; - case "address": - $result->where("address LIKE ?", $paramValue); - break; - case "is_online": - $result->where("online >= ?", time() - 900); - break; - case "interests": - $result->where("interests LIKE ?", $paramValue); - break; - case "fav_mus": - $result->where("fav_music LIKE ?", $paramValue); - break; - case "fav_films": - $result->where("fav_films LIKE ?", $paramValue); - break; - case "fav_shows": - $result->where("fav_shows LIKE ?", $paramValue); - break; - case "fav_books": - $result->where("fav_books LIKE ?", $paramValue); - break; - case "fav_quote": - $result->where("fav_quote LIKE ?", $paramValue); - break; - case "before": - $result->where("UNIX_TIMESTAMP(since) < ?", $paramValue); - break; - case "after": - $result->where("UNIX_TIMESTAMP(since) > ?", $paramValue); - break; - case "gender": - $result->where("sex ?", $paramValue); - break; - case "doNotSearchMe": - $result->where("id !=", $paramValue); - break; - } + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + case 'reg_date': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + case 'rating': + $order_str = 'rating DESC'; + break; + } + + foreach ($params as $paramName => $paramValue) { + if (is_null($paramValue) || $paramValue == '') { + continue; + } + + switch ($paramName) { + case "hometown": + $result->where("hometown LIKE ?", "%$paramValue%"); + break; + case "city": + $result->where("city LIKE ?", "%$paramValue%"); + break; + case "marital_status": + $result->where("marital_status ?", $paramValue); + break; + case "polit_views": + $result->where("polit_views ?", $paramValue); + break; + case "is_online": + $result->where("online >= ?", time() - 900); + break; + case "fav_mus": + $result->where("fav_music LIKE ?", "%$paramValue%"); + break; + case "fav_films": + $result->where("fav_films LIKE ?", "%$paramValue%"); + break; + case "fav_shows": + $result->where("fav_shows LIKE ?", "%$paramValue%"); + break; + case "fav_books": + $result->where("fav_books LIKE ?", "%$paramValue%"); + break; + case "before": + $result->where("UNIX_TIMESTAMP(since) < ?", $paramValue); + break; + case "after": + $result->where("UNIX_TIMESTAMP(since) > ?", $paramValue); + break; + case "gender": + if ((int) $paramValue == 3) { + break; + } + $result->where("sex ?", (int) $paramValue); + break; + case "ignore_id": + $result->where("id != ?", $paramValue); + break; + case "ignore_private": + $result->where("profile_type", 0); + break; } } + if ($order_str) { + $result->order($order_str); + } - return new Util\EntityStream("User", $result->order($sort)); + return new Util\EntityStream("User", $result); } - - function getStatistics(): object + + public function getStatistics(): object { return (object) [ - "all" => sizeof(clone $this->users), - "active" => sizeof((clone $this->users)->where("online > 0")), - "online" => sizeof((clone $this->users)->where("online >= ?", time() - 900)), + "all" => (clone $this->users)->count('*'), + "active" => (clone $this->users)->where("online >= ?", time() - MONTH)->count('*'), + "online" => (clone $this->users)->where("online >= ?", time() - 900)->count('*'), ]; } - function getByAddress(string $address): ?User + public function getByAddress(string $address): ?User { - if(substr_compare($address, "/", -1) === 0) + if (substr_compare($address, "/", -1) === 0) { $address = substr($address, 0, iconv_strlen($address) - 1); + } $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - if(strpos($address, $serverUrl . "/") === 0) + if (strpos($address, $serverUrl . "/") === 0) { $address = substr($address, iconv_strlen($serverUrl) + 1); + } - if(strpos($address, "id") === 0) { + if (strpos($address, "id") === 0) { $user = $this->get((int) substr($address, 2)); - if($user) return $user; + if ($user) { + return $user; + } } return $this->getByShortUrl($address); @@ -166,19 +190,19 @@ function getByAddress(string $address): ?User * If you need to check if the user is an instance administrator, use `$user->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)`. * This method is more suitable for instance administrators lists */ - function getInstanceAdmins(bool $excludeHidden = true): \Traversable + public function getInstanceAdmins(bool $excludeHidden = true): \Traversable { $query = "SELECT DISTINCT(`profiles`.`id`) FROM `ChandlerACLRelations` JOIN `profiles` ON `ChandlerACLRelations`.`user` = `profiles`.`user` COLLATE utf8mb4_unicode_520_ci WHERE `ChandlerACLRelations`.`group` IN (SELECT `group` FROM `ChandlerACLGroupsPermissions` WHERE `model` = \"admin\" AND `permission` = \"access\")"; - if($excludeHidden) - $query .= " AND `ChandlerACLRelations`.`user` NOT IN (SELECT `user` FROM `ChandlerACLRelations` WHERE `group` IN (SELECT `group` FROM `ChandlerACLGroupsPermissions` WHERE `model` = \"hidden_admin\" AND `permission` = \"be\"))"; + if ($excludeHidden) { + $query .= " AND `ChandlerACLRelations`.`user` NOT IN (SELECT `user` FROM `ChandlerACLRelations` WHERE `group` IN (SELECT `group` FROM `ChandlerACLGroupsPermissions` WHERE `model` = \"hidden_admin\" AND `permission` = \"be\"))"; + } $query .= " ORDER BY `profiles`.`id`;"; $result = DatabaseConnection::i()->getConnection()->query($query); - foreach($result as $entry) + foreach ($result as $entry) { yield $this->get($entry->id); + } } - - use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Util/EntityStream.php b/Web/Models/Repositories/Util/EntityStream.php index dd8f317f6..f9676efc5 100644 --- a/Web/Models/Repositories/Util/EntityStream.php +++ b/Web/Models/Repositories/Util/EntityStream.php @@ -1,18 +1,22 @@ -dbStream = $dbStream; $this->entityClass = $class[0] === "\\" ? $class : "openvk\\Web\\Models\\Entities\\$class"; } - + /** * Almost shorthand for (clone $this->dbStream) * Needed because it's used often in this class. And it's used often to prevent changing mutable dbStream. @@ -21,37 +25,38 @@ private function dbs(): \Traversable { return (clone $this->dbStream); } - + private function getEntity(ActiveRow $result) { return new $this->entityClass($result); } - + private function stream(\Traversable $iterator): \Traversable { - foreach($iterator as $result) + foreach ($iterator as $result) { yield $this->getEntity($result); + } } - - function getIterator(): \Traversable + + public function getIterator(): \Traversable { trigger_error("Trying to use EntityStream as iterator directly. Are you sure this is what you want?", E_USER_WARNING); - + return $this->stream($this->dbs()); } - - function page(int $page, ?int $perPage = NULL): \Traversable + + public function page(int $page, ?int $perPage = null): \Traversable { return $this->stream($this->dbs()->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)); } - - function offsetLimit(int $offset = 0, ?int $limit = NULL): \Traversable + + public function offsetLimit(int $offset = 0, ?int $limit = null): \Traversable { return $this->stream($this->dbs()->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset)); } - - function size(): int + + public function size(): int { - return sizeof($this->dbs()); + return $this->dbs()->count("*"); } } diff --git a/Web/Models/Repositories/Verifications.php b/Web/Models/Repositories/Verifications.php index 83c13e594..9e6322ad6 100755 --- a/Web/Models/Repositories/Verifications.php +++ b/Web/Models/Repositories/Verifications.php @@ -1,33 +1,37 @@ -context = DatabaseConnection::i()->getContext(); - $this->verifications = $this->context->table("email_verifications"); - } - - function toEmailVerification(?ActiveRow $ar): ?EmailVerification - { - return is_null($ar) ? NULL : new EmailVerification($ar); - } - - function getByToken(string $token): ?EmailVerification - { - return $this->toEmailVerification($this->verifications->where("key", $token)->fetch()); - } - - function getLatestByUser(User $user): ?EmailVerification - { - return $this->toEmailVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch()); - } -} +context = DatabaseConnection::i()->getContext(); + $this->verifications = $this->context->table("email_verifications"); + } + + public function toEmailVerification(?ActiveRow $ar): ?EmailVerification + { + return is_null($ar) ? null : new EmailVerification($ar); + } + + public function getByToken(string $token): ?EmailVerification + { + return $this->toEmailVerification($this->verifications->where("key", $token)->fetch()); + } + + public function getLatestByUser(User $user): ?EmailVerification + { + return $this->toEmailVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch()); + } +} diff --git a/Web/Models/Repositories/Videos.php b/Web/Models/Repositories/Videos.php index 632733496..fc3ad248a 100644 --- a/Web/Models/Repositories/Videos.php +++ b/Web/Models/Repositories/Videos.php @@ -1,80 +1,108 @@ -context = DatabaseConnection::i()->getContext(); $this->videos = $this->context->table("videos"); } - - function get(int $id): ?Video + + private function toVideo(?ActiveRow $ar): ?Video + { + return is_null($ar) ? null : new Video($ar); + } + + public function get(int $id): ?Video { - $videos = $this->videos->get($id); - if(!$videos) return NULL; - - return new Video($videos); + return self::$cache[$id] ??= $this->toVideo($this->videos->get($id)); } - - function getByOwnerAndVID(int $owner, int $vId): ?Video + + public function getByOwnerAndVID(int $owner, int $vId): ?Video { $videos = $this->videos->where([ "owner" => $owner, "virtual_id" => $vId, ])->fetch(); - if(!$videos) return NULL; - - return new Video($videos); + + return $this->toVideo($videos); + } + + public function getByUser(User $user, int $page = 1, ?int $perPage = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + foreach ($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->page($page, $perPage)->order("created DESC") as $video) { + yield new Video($video); + } } - - function getByUser(User $user, int $page = 1, ?int $perPage = NULL): \Traversable + + public function getByUserLimit(User $user, int $offset = 0, int $limit = 10): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - foreach($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->page($page, $perPage)->order("created DESC") as $video) + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + foreach ($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->limit($limit, $offset)->order("created DESC") as $video) { yield new Video($video); + } } - - function getUserVideosCount(User $user): int + + public function getUserVideosCount(User $user): int { return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])); } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + public function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; - - $notNullParams = []; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; - - $result = $this->videos->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); - - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); + $query = "%$query%"; + $result = $this->videos->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("deleted", 0)->where("unlisted", 0); + $order_str = 'id'; + + switch ($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + foreach ($params as $paramName => $paramValue) { + switch ($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case 'only_youtube': + if ((int) $paramValue != 1) { break; - } + } + $result->where("link != ?", 'NULL'); + break; } } + if ($order_str) { + $result->order($order_str); + } + + return new Util\EntityStream("Video", $result); + } + + public function getLastVideo(User $user) + { + $video = $this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->order("id DESC")->fetch(); - return new Util\EntityStream("Video", $result->order("$sort")); + return new Video($video); } } diff --git a/Web/Models/Repositories/Vouchers.php b/Web/Models/Repositories/Vouchers.php index 62d0a54e1..42e774171 100644 --- a/Web/Models/Repositories/Vouchers.php +++ b/Web/Models/Repositories/Vouchers.php @@ -1,19 +1,23 @@ -table->where([ "token" => $token, "deleted" => $withDeleted, ])->fetch(); - + return $this->toEntity($voucher); } } diff --git a/Web/Models/RowModel.php b/Web/Models/RowModel.php index 92baeaa6a..4e20b3171 100644 --- a/Web/Models/RowModel.php +++ b/Web/Models/RowModel.php @@ -1,7 +1,10 @@ -id = $id; } - - abstract function getThumbnailURL(): string; - - abstract function getURL(): string; - - abstract function getEmbed(string $w = "600", string $h = "340"): string; + + abstract public function getThumbnailURL(): string; + + abstract public function getURL(): string; + + abstract public function getEmbed(string $w = "600", string $h = "340"): string; } diff --git a/Web/Models/VideoDrivers/YouTubeVideoDriver.php b/Web/Models/VideoDrivers/YouTubeVideoDriver.php index 1b9940e6f..95f50d47f 100644 --- a/Web/Models/VideoDrivers/YouTubeVideoDriver.php +++ b/Web/Models/VideoDrivers/YouTubeVideoDriver.php @@ -1,29 +1,32 @@ -id/mqdefault.jpg"; } - - function getURL(): string + + public function getURL(): string { return "https://youtu.be/$this->id"; } - - function getEmbed(string $w = "600", string $h = "340"): string + + public function getEmbed(string $w = "600", string $h = "340"): string { return << -CODE; + + CODE; } } diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 new file mode 100644 index 000000000..f60a9aede --- /dev/null +++ b/Web/Models/shell/processAudio.ps1 @@ -0,0 +1,39 @@ +$ovkRoot = $args[0] +$storageDir = $args[1] +$fileHash = $args[2] +$hashPart = $fileHash.substring(0, 2) +$filename = $args[3] +$audioFile = [System.IO.Path]::GetTempFileName() +$temp = [System.IO.Path]::GetTempFileName() + +$keyID = $args[4] +$key = $args[5] +$token = $args[6] +$seg = $args[7] + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree + +Remove-Item $temp +Remove-Item $audioFile +New-Item -ItemType "directory" $temp +New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments') +New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments') +Set-Location -Path $temp + +Move-Item $filename $audioFile +ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` + -encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg ` + -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` + -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` + "$fileHash.mpd" + +ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3" +Move-Item "original_$token.mp3" ($fileHash + '_fragments') + +Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments') +Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart" + +cd .. +Remove-Item -Recurse $temp +Remove-Item $audioFile diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh new file mode 100644 index 000000000..fa8346e06 --- /dev/null +++ b/Web/Models/shell/processAudio.sh @@ -0,0 +1,35 @@ +ovkRoot=$1 +storageDir=$2 +fileHash=$3 +hashPart=$(echo $fileHash | cut -c1-2) +filename=$4 +audioFile=$(mktemp) +temp=$(mktemp -d) + +keyID=$5 +key=$6 +token=$7 +seg=$8 + +trap 'rm -f "$temp" "$audioFile"' EXIT + +mkdir -p "$temp/$fileHash"_fragments +mkdir -p "$storageDir/$hashPart/$fileHash"_fragments +cd "$temp" + +mv "$filename" "$audioFile" +ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ + -encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \ + -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ + -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ + "$fileHash.mpd" + +ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3" +mv "original_$token.mp3" "$fileHash"_fragments + +mv "$fileHash"_fragments "$storageDir/$hashPart" +mv "$fileHash.mpd" "$storageDir/$hashPart" + +cd .. +rm -rf "$temp" +rm -f "$audioFile" diff --git a/Web/Models/shell/processVideo.ps1 b/Web/Models/shell/processVideo.ps1 index f2cc99188..27bc2e064 100644 --- a/Web/Models/shell/processVideo.ps1 +++ b/Web/Models/shell/processVideo.ps1 @@ -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 libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2 +ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=iw*min(1\,if(gt(iw\,ih)\,640/iw\,(640*sar)/ih)):(floor((ow/dar)/2))*2" -y $temp2 Move-Item $temp2 "$dir$hashT/$hash.mp4" Remove-Item $temp diff --git a/Web/Models/shell/processVideo.sh b/Web/Models/shell/processVideo.sh index ca2c6d99d..9fc74b98b 100644 --- a/Web/Models/shell/processVideo.sh +++ b/Web/Models/shell/processVideo.sh @@ -3,7 +3,7 @@ tmpfile="$RANDOM-$(date +%s%N)" cp $2 "/tmp/vid_$tmpfile.bin" 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 libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.mp4" +nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=iw*min(1\,if(gt(iw\,ih)\,640/iw\,(640*sar)/ih)):(floor((ow/dar)/2))*2" -y "/tmp/ffmOi$tmpfile.mp4" rm -rf $3${4:0:2}/$4.mp4 mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4 diff --git a/Web/Models/sql/get-bday-today.tsql b/Web/Models/sql/get-bday-today.tsql new file mode 100644 index 000000000..8ecc17540 --- /dev/null +++ b/Web/Models/sql/get-bday-today.tsql @@ -0,0 +1,6 @@ + (SELECT DISTINCT(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 DATE_FORMAT(FROM_UNIXTIME(birthday), '%m-%d') IN (DATE_FORMAT(CURDATE(), '%m-%d')) \ No newline at end of file diff --git a/Web/Models/sql/get-bday-tomorrow.tsql b/Web/Models/sql/get-bday-tomorrow.tsql new file mode 100644 index 000000000..254883849 --- /dev/null +++ b/Web/Models/sql/get-bday-tomorrow.tsql @@ -0,0 +1,6 @@ + (SELECT DISTINCT(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 DATE_FORMAT(FROM_UNIXTIME(birthday), '%m-%d') IN (DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 DAY), '%m-%d')) \ No newline at end of file diff --git a/Web/Models/sql/get-followers.tsql b/Web/Models/sql/get-followers.tsql index 552aafb8d..b07b1c690 100644 --- a/Web/Models/sql/get-followers.tsql +++ b/Web/Models/sql/get-followers.tsql @@ -1,5 +1,5 @@ (SELECT DISTINCT(follower) AS __id FROM - (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 + (SELECT follower, flags FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 LEFT JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 ON u0.follower = u1.target WHERE u1.target IS NULL) u2 diff --git a/Web/Models/sql/get-nearest-posts.tsql b/Web/Models/sql/get-nearest-posts.tsql new file mode 100644 index 000000000..5da7faa8d --- /dev/null +++ b/Web/Models/sql/get-nearest-posts.tsql @@ -0,0 +1,11 @@ +SELECT *, + SQRT( + POW(69.1 * (? - geo_lat), 2) + + POW(69.1 * (? - geo_lon) * COS(RADIANS(geo_lat)), 2) + ) AS distance +FROM posts +WHERE id <> ? +AND FROM_UNIXTIME(created) >= DATE_SUB(NOW(), INTERVAL 1 MONTH) +HAVING distance < 1 AND distance IS NOT NULL +ORDER BY distance +LIMIT 25; diff --git a/Web/Models/sql/get-requests.tsql b/Web/Models/sql/get-requests.tsql new file mode 100755 index 000000000..0220e3400 --- /dev/null +++ b/Web/Models/sql/get-requests.tsql @@ -0,0 +1,6 @@ + (SELECT DISTINCT(follower) AS __id FROM + (SELECT follower FROM subscriptions WHERE target=? AND flags=0 AND model="openvk\\Web\\Models\\Entities\\User") u0 + LEFT JOIN + (SELECT target FROM subscriptions WHERE follower=? AND flags=0 AND model="openvk\\Web\\Models\\Entities\\User") u1 + ON u0.follower = u1.target WHERE u1.target IS NULL) u2 +INNER JOIN profiles ON profiles.id = u2.__id \ No newline at end of file diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php index 9115a019b..5e6ece92d 100644 --- a/Web/Presenters/AboutPresenter.php +++ b/Web/Presenters/AboutPresenter.php @@ -1,5 +1,9 @@ -user)) { - if($this->user->identity->getMainPage()) + if (!is_null($this->user->identity)) { + if ($this->user->identity->getMainPage()) { $this->redirect("/feed"); - else + } else { $this->redirect($this->user->identity->getURL()); + } } - - if($_SERVER['REQUEST_URI'] == "/id0") { + + if ($_SERVER['REQUEST_URI'] == "/id0") { $this->redirect("/"); } - - $this->template->stats = (new Users)->getStatistics(); + + $this->template->stats = (new Users())->getStatistics(); } - - function renderRules(): void + + public function renderRules(): void { $this->pass("openvk!Support->knowledgeBaseArticle", "rules"); } - - function renderHelp(): void - {} - - function renderBB(): void - {} - - function renderTour(): void - {} - - function renderInvite(): void + + public function renderHelp(): void {} + + public function renderBB(): void {} + + public function renderTour(): void {} + + public function renderInvite(): void { $this->assertUserLoggedIn(); } - - function renderDonate(): void + + public function renderDonate(): void { $this->pass("openvk!Support->knowledgeBaseArticle", "donate"); } - - function renderPrivacy(): void + + public function renderPrivacy(): void { $this->pass("openvk!Support->knowledgeBaseArticle", "privacy"); } - - function renderVersion(): void + + public function renderVersion(): void { $this->template->themes = Themepacks::i()->getAllThemes(); $this->template->languages = getLanguages(); } - function renderAboutInstance(): void + public function renderAboutInstance(): void { - $this->template->usersStats = (new Users)->getStatistics(); - $this->template->clubsCount = (new Clubs)->getCount(); - $this->template->postsCount = (new Posts)->getCount(); + $this->template->usersStats = (new Users())->getStatistics(); + $this->template->clubsCount = (new Clubs())->getCount(); + $this->template->postsCount = (new Posts())->getCount(); $this->template->popularClubs = []; - $this->template->admins = iterator_to_array((new Users)->getInstanceAdmins()); + $this->template->admins = iterator_to_array((new Users())->getInstanceAdmins()); } - - function renderLanguage(): void + + public function renderLanguage(): void { $this->template->languages = getLanguages(); - - if(!is_null($_GET['lg'])){ + + if (!is_null($_GET['lg'])) { $this->assertNoCSRF(); setLanguage($_GET['lg']); } - if(!is_null($_GET['jReturnTo'])) + if (!is_null($_GET['jReturnTo'])) { $this->redirect(rawurldecode($_GET['jReturnTo'])); + } } - function renderExportJSLanguage($lg = NULL): void + public function renderExportJSLanguage($lg = null): void { - $localizer = Localizator::i(); - $lang = $lg; - if(is_null($lg)) + if (is_null($lg) || !isLanguageAvailable($lg)) { $this->throwError(404, "Not found", "Language is not found"); + } + + $localizer = Localizator::i(); header("Content-Type: application/javascript"); - echo "window.lang = " . json_encode($localizer->export($lang)) . ";"; # привет хардкод :DDD + echo "window.lang = " . json_encode($localizer->export($lg)) . ";"; exit; } - function renderSandbox(): void + public function renderSandbox(): void { $this->template->languages = getLanguages(); } - function renderRobotsTxt(): void + public function renderRobotsTxt(): void { $text = "# robots.txt file for openvk\n" . "#\n" @@ -109,6 +113,10 @@ function renderRobotsTxt(): void . "# lack of rights to access the admin panel)\n\n" . "User-Agent: *\n" . "Disallow: /albums/create\n" + . "Disallow: /assets/packages/static/openvk/img/banned.jpg\n" + . "Disallow: /assets/packages/static/openvk/img/camera_200.png\n" + . "Disallow: /assets/packages/static/openvk/img/flags/\n" + . "Disallow: /assets/packages/static/openvk/img/oof.apng\n" . "Disallow: /videos/upload\n" . "Disallow: /invite\n" . "Disallow: /groups_create\n" @@ -128,19 +136,48 @@ function renderRobotsTxt(): void . "Disallow: *hash=\n" . "Disallow: *?jReturnTo=\n" . "Disallow: /method/*\n" - . "Disallow: /token*"; + . "Disallow: /token*\n" + . "Disallow: /oauth/token*"; header("Content-Type: text/plain"); exit($text); } - function renderHumansTxt(): void + public function renderHumansTxt(): void { # :D $this->redirect("https://github.com/openvk/openvk#readme"); } - function renderDev(): void + public function renderAssetLinksJSON(): void + { + # Необходимо любому андроид приложению для автоматического разрешения принимать ссылки с этого сайта. + # Не шарю как писать норм на php поэтому тут чутка на вайбкодил - искренне ваш, ZAZiOs. + header("Content-Type: application/json"); + + $data = [ + [ + "relation" => ["delegate_permission/common.handle_all_urls"], + "target" => [ + "namespace" => "android_app", + "package_name" => "oss.OpenVK.Native", + "sha256_cert_fingerprints" => [ + "79:67:14:23:DC:6E:FA:49:64:1F:F1:81:0E:B0:A3:AE:6E:88:AB:0D:CF:BC:02:96:F3:6D:76:6B:82:94:D6:9C", + ], + ], + ], + ]; + + echo json_encode($data, JSON_UNESCAPED_SLASHES); + exit; + } + + public function renderAPIBlank(): void + { + // well + } + + public function renderDev(): void { - $this->redirect("https://docs.openvk.uk/"); + $this->redirect("https://openvk.github.io/docs/"); } } diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 14fbbc742..00b625066 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -1,9 +1,25 @@ -users = $users; $this->clubs = $clubs; @@ -24,73 +42,184 @@ function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gift $this->gifts = $gifts; $this->bannedLinks = $bannedLinks; $this->chandlerGroups = $chandlerGroups; - $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); - + $this->audios = $audios; + + $this->context = DatabaseConnection::i()->getContext(); + $this->logs = $this->context->table("ChandlerLogs"); + parent::__construct(); } - + private function warnIfNoCommerce(): void { - if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { $this->flash("warn", tr("admin_commerce_disabled"), tr("admin_commerce_disabled_desc")); + } } - + + private function warnIfLongpoolBroken(): void + { + bdump(is_writable(CHANDLER_ROOT . '/tmp/events.bin')); + if (file_exists(CHANDLER_ROOT . '/tmp/events.bin') == false || is_writable(CHANDLER_ROOT . '/tmp/events.bin') == false) { + $this->flash("warn", tr("admin_longpool_broken"), tr("admin_longpool_broken_desc", CHANDLER_ROOT . '/tmp/events.bin')); + } + } + private function searchResults(object $repo, &$count) { $query = $this->queryParam("q") ?? ""; $page = (int) ($this->queryParam("p") ?? 1); - + $count = $repo->find($query)->size(); return $repo->find($query)->page($page, 20); } - - function onStartup(): void + + private function searchPlaylists(&$count) + { + $query = $this->queryParam("q") ?? ""; + $page = (int) ($this->queryParam("p") ?? 1); + + $count = $this->audios->findPlaylists($query)->size(); + return $this->audios->findPlaylists($query)->page($page, 20); + } + + public function onStartup(): void { parent::onStartup(); - + $this->assertPermission("admin", "access", -1); } - - function renderIndex(): void + + public function renderIndex(): void { - + $this->warnIfLongpoolBroken(); + + // Users: Registered Users + $this->template->usersStats = $this->users->getStatistics(); + $this->template->usersToday = $this->context->table("profiles")->where("UNIX_TIMESTAMP(since) >=", time() - DAY)->count('*'); + $this->template->usersVerifiedCount = $this->context->table("profiles")->where("verified", true)->count('*'); + $this->template->usersDeletedCount = $this->context->table("profiles")->where("deleted", true)->count('*'); + + // Users: Instance Admins + $admGroupUUID = $this->context->table("ChandlerACLGroupsPermissions")->where(["model" => "admin", "permission" => "access"])->fetch()->group; + $supGroupUUID = $this->context->table("ChandlerACLGroupsPermissions")->where(["model" => "openvk\\Web\\Models\\Entities\\TicketReply", "permission" => "write"])->fetch()->group; + $modGroupUUID = $this->context->table("ChandlerACLGroupsPermissions")->where(["model" => "openvk\\Web\\Models\\Entities\\Report", "permission" => "admin"])->fetch()->group; + $nspGroupUUID = $this->context->table("ChandlerACLGroupsPermissions")->where(["model" => "openvk\\Web\\Models\\Entities\\Ban", "permission" => "write"])->fetch()->group; + + $groupsMissingWarnings = []; + if (!$supGroupUUID) { + $groupsMissingWarnings[] = "agent"; + } + if (!$modGroupUUID) { + $groupsMissingWarnings[] = "moder"; + } + if (!$nspGroupUUID) { + $groupsMissingWarnings[] = "nsp"; + } + $this->template->groupsMissingWarnings = $groupsMissingWarnings; + + $this->template->empCnt = $this->context->table("ChandlerACLRelations")->where("group", [$admGroupUUID, $supGroupUUID, $modGroupUUID, $nspGroupUUID])->group('user')->count('*'); + $this->template->admCnt = $this->context->table("ChandlerACLRelations")->where("group", $admGroupUUID)->count('*'); + $this->template->supCnt = $this->context->table("ChandlerACLRelations")->where("group", $supGroupUUID)->count('*'); + $this->template->modCnt = $this->context->table("ChandlerACLRelations")->where("group", $modGroupUUID)->count('*'); + $this->template->nspCnt = $this->context->table("ChandlerACLRelations")->where("group", $nspGroupUUID)->count('*'); + + // Users: Banned Users + $this->template->bannedCount = $this->context->table("bans")->where("FLOOR(removed_by)", 0)->count('*'); + $this->template->bannedForeverCount = $this->context->table("bans")->where(["FLOOR(removed_by)" => 0, "exp" => 0])->count('*'); + $this->template->canBeUnbannedNowCount = $this->context->table("bans")->where(["FLOOR(removed_by)" => 0, "exp <=" => time(), "exp >" => 0])->count('*'); + + // Support and Moderation: Tickets + $ticketsCount = $this->context->table("tickets")->count('*'); + $ticketsCountToday = $this->context->table("tickets")->where("created >= ?", time() - DAY)->count('*'); + $ticketsProcessingCount = $this->context->table("tickets")->where("type", 0)->count('*'); + $ticketsWithAnswerCount = $this->context->table("tickets")->where("type", 1)->count('*'); + $ticketsClosedCount = $this->context->table("tickets")->where("type", 2)->count('*'); + + $this->template->ticketsCount = $ticketsCount; + $this->template->ticketsCountToday = $ticketsCountToday; + $this->template->ticketsProcessingCount = $ticketsProcessingCount; + $this->template->ticketsWithAnswerCount = $ticketsWithAnswerCount; + $this->template->ticketsClosedCount = $ticketsClosedCount; + + // Support and Moderation: Reports + $this->template->reportsCount = $this->context->table("reports")->count('*'); + $this->template->reportsCountToday = $this->context->table("reports")->where("created >=", time() - DAY)->count('*'); + + // Support and Moderation: noSpam + $nspTemplatesCount = 0; + $nspContentCount = 0; + foreach ((new NoSpamLogs())->getList() as $nsplog) { + $nspTemplatesCount++; + $nspContentCount += $nsplog->getCount(); + } + $this->template->nspTemplatesCount = $nspTemplatesCount; + $this->template->nspContentCount = $nspContentCount; + + // Content: Groups + $this->template->groupsCount = $this->context->table("groups")->count('*'); + $this->template->groupsVerifiedCount = $this->context->table("groups")->where("verified", true)->count('*'); + $this->template->groupsBannedCount = $this->context->table("groups")->where("block_reason !=", "")->count('*'); + + // Content: Other + $this->template->postsCount = (new Posts())->getCount(); + $this->template->messagesCount = $this->context->table("messages")->count('*'); + $this->template->photosCount = $this->context->table("photos")->count('*'); + $this->template->videosCount = $this->context->table("videos")->count('*'); + $this->template->audiosCount = $this->context->table("audios")->count('*'); + $this->template->notesCount = $this->context->table("notes")->count('*'); + $this->template->appsCount = $this->context->table("apps")->count('*'); + $this->template->documentsCount = $this->context->table("documents")->count('*'); } - - function renderUsers(): void + + public function renderUsers(): void { $this->template->users = $this->searchResults($this->users, $this->template->count); } - - function renderUser(int $id): void + + public function renderUser(int $id): void { $user = $this->users->get($id); - if(!$user) + if (!$user) { $this->notFound(); - + } + $this->template->user = $user; - $this->template->c_groups_list = (new ChandlerGroups)->getList(); + $this->template->c_groups_list = (new ChandlerGroups())->getList(); $this->template->c_memberships = $this->chandlerGroups->getUsersMemberships($user->getChandlerGUID()); + $this->template->sessions = iterator_to_array($user->getChandlerUser()->getSessions()); + + $this->template->mode = in_array($this->queryParam("act"), ["info", "sessions"]) ? $this->queryParam("act") : "info"; - if($_SERVER["REQUEST_METHOD"] !== "POST") + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - - switch($_POST["act"] ?? "info") { + } + + switch ($_POST["act"] ?? "info") { default: case "info": $user->setFirst_Name($this->postParam("first_name")); $user->setLast_Name($this->postParam("last_name")); $user->setPseudo($this->postParam("nickname")); $user->setStatus($this->postParam("status")); - if(!$user->setShortCode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"))) + $user->setHide_Global_Feed(empty($this->postParam("hide_global_feed") ? 0 : 1)); + 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"))); + if ($user->onlineStatus() != $this->postParam("online")) { + $user->setOnline(intval($this->postParam("online"))); + } $user->setVerified(empty($this->postParam("verify") ? 0 : 1)); - if($this->postParam("add-to-group")) { - $query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')"; - DatabaseConnection::i()->getConnection()->query($query); + if ($this->postParam("add-to-group")) { + if (!(new ChandlerGroups())->isUserAMember($this->postParam("add-to-group"), $user->getChandlerGUID())) { + $query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')"; + DatabaseConnection::i()->getConnection()->query($query); + } else { + $this->flash("err", tr("error"), tr("c_user_is_already_in_group")); + } } - if($this->postParam("password")) { + if ($this->postParam("password")) { $user->getChandlerUser()->updatePassword($this->postParam("password")); } @@ -99,28 +228,30 @@ function renderUser(int $id): void break; } } - - function renderClubs(): void + + public function renderClubs(): void { $this->template->clubs = $this->searchResults($this->clubs, $this->template->count); } - - function renderClub(int $id): void + + public function renderClub(int $id): void { $club = $this->clubs->get($id); - if(!$club) + if (!$club) { $this->notFound(); - + } + $this->template->mode = in_array($this->queryParam("act"), ["main", "ban", "followers"]) ? $this->queryParam("act") : "main"; $this->template->club = $club; $this->template->followers = $this->template->club->getFollowers((int) ($this->queryParam("p") ?? 1)); - if($_SERVER["REQUEST_METHOD"] !== "POST") + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - - switch($this->queryParam("act")) { + } + + switch ($this->queryParam("act")) { default: case "main": $club->setOwner($this->postParam("id_owner")); @@ -129,178 +260,192 @@ function renderClub(int $id): void $club->setShortCode($this->postParam("shortcode")); $club->setVerified(empty($this->postParam("verify") ? 0 : 1)); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed") ? 0 : 1)); + $club->setEnforce_Hiding_From_Global_Feed(empty($this->postParam("enforce_hiding_from_global_feed") ? 0 : 1)); $club->save(); break; case "ban": - $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? $this->postParam("ban_reason") : NULL; + $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? $this->postParam("ban_reason") : null; $club->setBlock_reason($reason); $club->save(); break; } } - - function renderVouchers(): void + + public function renderVouchers(): void { $this->warnIfNoCommerce(); - + $this->template->count = $this->vouchers->size(); $this->template->vouchers = iterator_to_array($this->vouchers->enumerate((int) ($this->queryParam("p") ?? 1))); } - - function renderVoucher(int $id): void + + public function renderVoucher(int $id): void { $this->warnIfNoCommerce(); - - $voucher = NULL; + + $voucher = null; $this->template->form = (object) []; - if($id === 0) { + if ($id === 0) { $this->template->form->id = 0; - $this->template->form->token = NULL; + $this->template->form->token = null; $this->template->form->coins = 0; $this->template->form->rating = 0; $this->template->form->usages = -1; $this->template->form->users = []; } else { $voucher = $this->vouchers->get($id); - if(!$voucher) + if (!$voucher) { $this->notFound(); - + } + $this->template->form->id = $voucher->getId(); $this->template->form->token = $voucher->getToken(); $this->template->form->coins = $voucher->getCoins(); $this->template->form->rating = $voucher->getRating(); $this->template->form->usages = $voucher->getRemainingUsages(); $this->template->form->users = iterator_to_array($voucher->getUsers()); - - if($this->template->form->usages === INF) + + if ($this->template->form->usages === INF) { $this->template->form->usages = -1; - else + } else { $this->template->form->usages = (int) $this->template->form->usages; + } } - - if($_SERVER["REQUEST_METHOD"] !== "POST") + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - - $voucher ??= new Voucher; + } + + $voucher ??= new Voucher(); $voucher->setCoins((int) $this->postParam("coins")); $voucher->setRating((int) $this->postParam("rating")); $voucher->setRemainingUsages($this->postParam("usages") === '-1' ? INF : ((int) $this->postParam("usages"))); - if(!empty($tok = $this->postParam("token")) && strlen($tok) === 24) + if (!empty($tok = $this->postParam("token")) && strlen($tok) === 24) { $voucher->setToken($tok); - + } + $voucher->save(); - + $this->redirect("/admin/vouchers/id" . $voucher->getId()); } - - function renderGiftCategories(): void + + public function renderGiftCategories(): void { $this->warnIfNoCommerce(); - + $this->template->act = $this->queryParam("act") ?? "list"; - $this->template->categories = iterator_to_array($this->gifts->getCategories((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count)); + $this->template->categories = iterator_to_array($this->gifts->getCategories((int) ($this->queryParam("p") ?? 1), null, $this->template->count)); } - - function renderGiftCategory(string $slug, int $id): void + + public function renderGiftCategory(string $slug, int $id): void { $this->warnIfNoCommerce(); - - $cat; + + $cat = null; $gen = false; - if($id !== 0) { + if ($id !== 0) { $cat = $this->gifts->getCat($id); - if(!$cat) + if (!$cat) { $this->notFound(); - else if($cat->getSlug() !== $slug) + } elseif ($cat->getSlug() !== $slug) { $this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta"); + } } else { $gen = true; - $cat = new GiftCategory; + $cat = new GiftCategory(); } - + $this->template->form = (object) []; $this->template->form->id = $id; $this->template->form->languages = []; - foreach(getLanguages() as $language) { + foreach (getLanguages() as $language) { $language = (object) $language; $this->template->form->languages[$language->code] = (object) []; - + $this->template->form->languages[$language->code]->name = $gen ? "" : ($cat->getName($language->code, true) ?? ""); $this->template->form->languages[$language->code]->description = $gen ? "" : ($cat->getDescription($language->code, true) ?? ""); } - + $this->template->form->languages["master"] = (object) [ "name" => $gen ? "Unknown Name" : $cat->getName(), - "description" => $gen ? "" : $cat->getDescription(), + "description" => $gen ? "" : $cat->getDescription(), ]; - - if($_SERVER["REQUEST_METHOD"] !== "POST") + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - - if($gen) { - $cat->setAutoQuery(NULL); + } + + if ($gen) { + $cat->setAutoQuery(null); $cat->save(); } - + $cat->setName("_", $this->postParam("name_master")); $cat->setDescription("_", $this->postParam("description_master")); - foreach(getLanguages() as $language) { + foreach (getLanguages() as $language) { $code = $language["code"]; - if(!empty($this->postParam("name_$code") ?? NULL)) + if (!empty($this->postParam("name_$code") ?? null)) { $cat->setName($code, $this->postParam("name_$code")); - - if(!empty($this->postParam("description_$code") ?? NULL)) + } + + if (!empty($this->postParam("description_$code") ?? null)) { $cat->setDescription($code, $this->postParam("description_$code")); + } } - + $this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta"); } - - function renderGifts(string $catSlug, int $catId): void + + public function renderGifts(string $catSlug, int $catId): void { $this->warnIfNoCommerce(); - + $cat = $this->gifts->getCat($catId); - if(!$cat) + if (!$cat) { $this->notFound(); - else if($cat->getSlug() !== $catSlug) + } elseif ($cat->getSlug() !== $catSlug) { $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)); + $this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), null, $this->template->count)); } - - function renderGift(int $id): void + + public function renderGift(int $id): void { $this->warnIfNoCommerce(); - + $gift = $this->gifts->get($id); $act = $this->queryParam("act") ?? "edit"; - switch($act) { + switch ($act) { case "delete": $this->assertNoCSRF(); - if(!$gift) + if (!$gift) { $this->notFound(); - + } + $gift->delete(); $this->flashFail("succ", tr("admin_gift_moved_successfully"), tr("admin_gift_moved_to_recycle")); break; case "copy": case "move": $this->assertNoCSRF(); - if(!$gift) + if (!$gift) { $this->notFound(); - + } + $catFrom = $this->gifts->getCat((int) ($this->queryParam("from") ?? 0)); $catTo = $this->gifts->getCat((int) ($this->queryParam("to") ?? 0)); - if(!$catFrom || !$catTo || !$catFrom->hasGift($gift)) + if (!$catFrom || !$catTo || !$catFrom->hasGift($gift)) { $this->badRequest(); - - if($act === "move") + } + + if ($act === "move") { $catFrom->removeGift($gift); - + } + $catTo->addGift($gift); - + $name = $catTo->getName(); $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in $name."); $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/"); @@ -308,126 +453,147 @@ function renderGift(int $id): void default: case "edit": $gen = false; - if(!$gift) { + if (!$gift) { $gen = true; - $gift = new Gift; + $gift = new Gift(); } - + $this->template->form = (object) []; $this->template->form->id = $id; $this->template->form->name = $gen ? "New Gift (1)" : $gift->getName(); - $this->template->form->price = $gen ? 0 : $gift->getPrice(); - $this->template->form->usages = $gen ? 0 : $gift->getUsages(); - $this->template->form->limit = $gen ? -1 : ($gift->getLimit() === INF ? -1 : $gift->getLimit()); - $this->template->form->pic = $gen ? NULL : $gift->getImage(Gift::IMAGE_URL); - - if($_SERVER["REQUEST_METHOD"] !== "POST") + $this->template->form->price = $gen ? 0 : $gift->getPrice(); + $this->template->form->usages = $gen ? 0 : $gift->getUsages(); + $this->template->form->limit = $gen ? -1 : ($gift->getLimit() === INF ? -1 : $gift->getLimit()); + $this->template->form->pic = $gen ? null : $gift->getImage(Gift::IMAGE_URL); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - + } + $limit = $this->postParam("limit") ?? $this->template->form->limit; $limit = $limit == "-1" ? INF : (float) $limit; $gift->setLimit($limit, is_null($this->postParam("reset_limit")) ? Gift::PERIOD_SET_IF_NONE : Gift::PERIOD_SET); - + $gift->setName($this->postParam("name")); $gift->setPrice((int) $this->postParam("price")); $gift->setUsages((int) $this->postParam("usages")); - if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) { - if(!$gift->setImage($_FILES["pic"]["tmp_name"])) + if (isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) { + if (!$gift->setImage($_FILES["pic"]["tmp_name"])) { $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_bad_image")); - } else if($gen) { + } + } elseif ($gen) { # If there's no gift pic but it's newly created $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_no_image")); } - + $gift->save(); - - if($gen && !is_null($cat = $this->postParam("_cat"))) { + + if ($gen && !is_null($cat = $this->postParam("_cat"))) { $cat = $this->gifts->getCat((int) $cat); - if(!is_null($cat)) + if (!is_null($cat)) { $cat->addGift($gift); + } } - + $this->redirect("/admin/gifts/id" . $gift->getId()); } } - - function renderFiles(): void - { - - } - - function renderQuickBan(int $id): void + + public function renderFiles(): void {} + + public function renderQuickBan(int $id): void { $this->assertNoCSRF(); - if (str_contains($this->queryParam("reason"), "*")) + if (str_contains($this->queryParam("reason"), "*")) { exit(json_encode([ "error" => "Incorrect reason" ])); + } $unban_time = strtotime($this->queryParam("date")) ?: "permanent"; $user = $this->users->get($id); - if(!$user) + if (!$user) { exit(json_encode([ "error" => "User does not exist" ])); + } - if ($this->queryParam("incr")) + if ($this->queryParam("incr")) { $unban_time = time() + $user->getNewBanTime(); + } - $user->ban($this->queryParam("reason"), true, $unban_time, $this->user->identity->getId()); + $user->ban($this->queryParam("reason"), false, $unban_time, $this->user->identity->getId()); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); } - function renderQuickUnban(int $id): void + public function renderQuickUnban(int $id): void { $this->assertNoCSRF(); - + $user = $this->users->get($id); - if(!$user) + if (!$user) { exit(json_encode([ "error" => "User does not exist" ])); + } - $ban = (new Bans)->get((int)$user->getRawBanReason()); - if (!$ban || $ban->isOver()) + $ban = (new Bans())->get((int) $user->getRawBanReason()); + if (!$ban || $ban->isOver()) { exit(json_encode([ "error" => "User is not banned" ])); + } $ban->setRemoved_Manually(true); $ban->setRemoved_By($this->user->identity->getId()); $ban->save(); - $user->setBlock_Reason(NULL); + $user->setBlock_Reason(null); // $user->setUnblock_time(NULL); $user->save(); exit(json_encode([ "success" => true ])); } - - function renderQuickWarn(int $id): void + + public function renderQuickWarn(int $id): void { $this->assertNoCSRF(); - + $user = $this->users->get($id); - if(!$user) + if (!$user) { exit(json_encode([ "error" => "User does not exist" ])); - + } + $user->adminNotify("⚠️ " . $this->queryParam("message")); exit(json_encode([ "message" => $this->queryParam("message") ])); } - function renderBannedLinks(): void + public function renderDeleteSession(): void + { + $this->assertNoCSRF(); + + $token = $this->requestParam("token"); + if (empty($token)) { + $this->flashFail("succ", tr("error_when_searching"), '😔'); + } + + DatabaseConnection::i()->getContext()->table("ChandlerTokens")->where("token", $token)->delete(); + + $this->flashFail("succ", tr("changes_saved"), '👍'); + } + + public function renderBannedLinks(): void { $this->template->links = $this->bannedLinks->getList((int) $this->queryParam("p") ?: 1); - $this->template->users = new Users; + $this->template->users = new Users(); } - function renderBannedLink(int $id): void + public function renderBannedLink(int $id): void { $this->template->form = (object) []; - if($id === 0) { + if ($id === 0) { $this->template->form->id = 0; - $this->template->form->link = NULL; - $this->template->form->reason = NULL; + $this->template->form->link = null; + $this->template->form->reason = null; } else { - $link = (new BannedLinks)->get($id); - if(!$link) + $link = (new BannedLinks())->get($id); + if (!$link) { $this->notFound(); + } $this->template->form->id = $link->getId(); $this->template->form->link = $link->getDomain(); @@ -435,29 +601,31 @@ function renderBannedLink(int $id): void $this->template->form->regexp = $link->getRawRegexp(); } - if($_SERVER["REQUEST_METHOD"] !== "POST") + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; + } - $link = (new BannedLinks)->get($id); + $link = (new BannedLinks())->get($id); $new_domain = parse_url($this->postParam("link"))["host"]; - $new_reason = $this->postParam("reason") ?: NULL; + $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->setRegexp_rule(mb_strlen(trim($this->postParam("regexp"))) > 0 ? $this->postParam("regexp") : ""); $link->save(); } else { - if (!$new_domain) + if (!$new_domain) { $this->flashFail("err", tr("error"), tr("admin_banned_link_not_specified")); + } - $link = new BannedLink; + $link = new BannedLink(); $link->setDomain($new_domain); $link->setReason($new_reason); - $link->setRegexp_rule($this->postParam("regexp")); + $link->setRegexp_rule(mb_strlen(trim($this->postParam("regexp"))) > 0 ? $this->postParam("regexp") : ""); $link->setInitiator($this->user->identity->getId()); $link->save(); @@ -467,45 +635,87 @@ function renderBannedLink(int $id): void $this->redirect("/admin/bannedLink/id" . $lid); } - function renderUnbanLink(int $id): void + public function renderUnbanLink(int $id): void { - $link = (new BannedLinks)->get($id); + $link = (new BannedLinks())->get($id); - if (!$link) + if (!$link) { $this->flashFail("err", tr("error"), tr("admin_banned_link_not_found")); + } $link->delete(false); $this->redirect("/admin/bannedLinks"); } - function renderBansHistory(int $user_id) :void + public function renderBansHistory(int $user_id): void { - $user = (new Users)->get($user_id); - if (!$user) $this->notFound(); + $user = (new Users())->get($user_id); + if (!$user) { + $this->notFound(); + } - $this->template->bans = (new Bans)->getByUser($user_id); + $this->template->bans = (new Bans())->getByUser($user_id); } - function renderChandlerGroups(): void + public function renderChandlerGroups(): void { - $this->template->groups = (new ChandlerGroups)->getList(); + $this->template->groups = (new ChandlerGroups())->getList(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + return; + } + + if ($this->postParam("fixChandlerGroups")) { + $guid = $this->user->identity->getChandlerGUID(); + + $req = ""; + if ($this->postParam("agent")) { + $req = $req . <<<'SQL' + INSERT INTO `ChandlerGroups` VALUES (NULL, "OVK\\SupportAgents", NULL); + INSERT INTO `ChandlerACLGroupsPermissions` VALUES ((SELECT id FROM ChandlerGroups WHERE name = "OVK\\SupportAgents"), "openvk\\Web\\Models\\Entities\\TicketReply", 0, "write", 1); + INSERT INTO `ChandlerACLRelations` VALUES ("{GUID}", (SELECT id FROM ChandlerGroups WHERE name = "OVK\\SupportAgents"), 64); + SQL; + } + + if ($this->postParam("moder")) { + $req = $req . <<<'SQL' + INSERT INTO `ChandlerGroups` VALUES (NULL, "OVK\\Moderators", NULL); + INSERT INTO `ChandlerACLGroupsPermissions` VALUES ((SELECT id FROM ChandlerGroups WHERE name = "OVK\\Moderators"), "openvk\\Web\\Models\\Entities\\Report", 0, "admin", 1); + INSERT INTO `ChandlerACLRelations` VALUES ("{GUID}", (SELECT id FROM ChandlerGroups WHERE name = "OVK\\Moderators"), 64); + SQL; + } + + if ($this->postParam("nsp")) { + $req = $req . <<<'SQL' + INSERT INTO `ChandlerGroups` VALUES (NULL, "OVK\\SpamAnalysts", NULL); + INSERT INTO `ChandlerACLGroupsPermissions` VALUES ((SELECT id FROM ChandlerGroups WHERE name = "OVK\\SpamAnalysts"), "openvk\\Web\\Models\\Entities\\Ban", 0, "write", 1); + INSERT INTO `ChandlerACLRelations` VALUES ("{GUID}", (SELECT id FROM ChandlerGroups WHERE name = "OVK\\SpamAnalysts"), 64); + SQL; + } + + if (mb_strlen($req) > 0) { + $req = str_replace('{GUID}', $guid, $req); + DatabaseConnection::i()->getConnection()->query($req); + $this->flashFail("succ", tr("changes_saved")); + } - if($_SERVER["REQUEST_METHOD"] !== "POST") return; + } $req = "INSERT INTO `ChandlerGroups` (`name`) VALUES ('" . $this->postParam("name") . "')"; DatabaseConnection::i()->getConnection()->query($req); } - function renderChandlerGroup(string $UUID): void + public function renderChandlerGroup(string $UUID): void { $DB = DatabaseConnection::i()->getConnection(); - if(is_null($DB->query("SELECT * FROM `ChandlerGroups` WHERE `id` = '$UUID'")->fetch())) + if (is_null($DB->query("SELECT * FROM `ChandlerGroups` WHERE `id` = '$UUID'")->fetch())) { $this->flashFail("err", tr("error"), tr("c_group_not_found")); + } - $this->template->group = (new ChandlerGroups)->get($UUID); + $this->template->group = (new ChandlerGroups())->get($UUID); $this->template->mode = in_array( $this->queryParam("act"), [ @@ -514,28 +724,31 @@ function renderChandlerGroup(string $UUID): void "permissions", "removeMember", "removePermission", - "delete" - ]) ? $this->queryParam("act") : "main"; - $this->template->members = (new ChandlerGroups)->getMembersById($UUID); - $this->template->perms = (new ChandlerGroups)->getPermissionsById($UUID); + "delete", + ] + ) ? $this->queryParam("act") : "main"; + $this->template->members = (new ChandlerGroups())->getMembersById($UUID); + $this->template->perms = (new ChandlerGroups())->getPermissionsById($UUID); - if($this->template->mode == "removeMember") { + if ($this->template->mode == "removeMember") { $where = "`user` = '" . $this->queryParam("uid") . "' AND `group` = '$UUID'"; - if(is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE " . $where)->fetch())) + if (is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE " . $where)->fetch())) { $this->flashFail("err", tr("error"), tr("c_user_is_not_in_group")); + } $DB->query("DELETE FROM `ChandlerACLRelations` WHERE " . $where); $this->flashFail("succ", tr("changes_saved"), tr("c_user_removed_from_group")); - } elseif($this->template->mode == "removePermission") { - $where = "`model` = '" . trim(addslashes($this->queryParam("model"))) . "' AND `permission` = '". $this->queryParam("perm") ."' AND `group` = '$UUID'"; + } elseif ($this->template->mode == "removePermission") { + $where = "`model` = '" . trim(addslashes($this->queryParam("model"))) . "' AND `permission` = '" . $this->queryParam("perm") . "' AND `group` = '$UUID'"; - if(is_null($DB->query("SELECT * FROM `ChandlerACLGroupsPermissions WHERE $where`"))) + if (is_null($DB->query("SELECT * FROM `ChandlerACLGroupsPermissions` WHERE $where"))) { $this->flashFail("err", tr("error"), tr("c_permission_not_found")); + } $DB->query("DELETE FROM `ChandlerACLGroupsPermissions` WHERE $where"); $this->flashFail("succ", tr("changes_saved"), tr("c_permission_removed_from_group")); - } elseif($this->template->mode == "delete") { + } elseif ($this->template->mode == "delete") { $DB->query("DELETE FROM `ChandlerGroups` WHERE `id` = '$UUID'"); $DB->query("DELETE FROM `ChandlerACLGroupsPermissions` WHERE `group` = '$UUID'"); $DB->query("DELETE FROM `ChandlerACLRelations` WHERE `group` = '$UUID'"); @@ -543,42 +756,111 @@ function renderChandlerGroup(string $UUID): void $this->flashFail("succ", tr("changes_saved"), tr("c_group_removed")); } - if ($_SERVER["REQUEST_METHOD"] !== "POST") return; + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + return; + } $req = ""; - if($this->template->mode == "main") - if($this->postParam("delete")) + if ($this->template->mode == "main") { + if ($this->postParam("delete")) { $req = "DELETE FROM `ChandlerGroups` WHERE `id`='$UUID'"; - else - $req = "UPDATE `ChandlerGroups` SET `name`='". $this->postParam('name') ."' , `color`='". $this->postParam("color") ."' WHERE `id`='$UUID'"; + } else { + $req = "UPDATE `ChandlerGroups` SET `name`='" . $this->postParam('name') . "' , `color`='" . $this->postParam("color") . "' WHERE `id`='$UUID'"; + } + } - if($this->template->mode == "members") - if($this->postParam("uid")) - if(!is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE `user` = '" . $this->postParam("uid") . "'"))) + if ($this->template->mode == "members") { + if ($this->postParam("uid")) { + if (is_null((new ChandlerUsers())->getById($this->postParam("uid")))) { + $this->flashFail("err", tr("error"), tr("profile_not_found")); + } + if ((new ChandlerGroups())->isUserAMember($UUID, $this->postParam("uid"))) { $this->flashFail("err", tr("error"), tr("c_user_is_already_in_group")); + } + } + } - $req = "INSERT INTO `ChandlerACLRelations` (`user`, `group`, `priority`) VALUES ('". $this->postParam("uid") ."', '$UUID', 32)"; + $req = "INSERT INTO `ChandlerACLRelations` (`user`, `group`, `priority`) VALUES ('" . $this->postParam("uid") . "', '$UUID', 32)"; - if($this->template->mode == "permissions") - $req = "INSERT INTO `ChandlerACLGroupsPermissions` (`group`, `model`, `permission`, `context`) VALUES ('$UUID', '". trim(addslashes($this->postParam("model"))) ."', '". $this->postParam("permission") ."', 0)"; + if ($this->template->mode == "permissions") { + $req = "INSERT INTO `ChandlerACLGroupsPermissions` (`group`, `model`, `permission`, `context`) VALUES ('$UUID', '" . trim(addslashes($this->postParam("model"))) . "', '" . $this->postParam("permission") . "', 0)"; + } $DB->query($req); $this->flashFail("succ", tr("changes_saved")); } - function renderChandlerUser(string $UUID): void + public function renderChandlerUser(string $UUID): void { - if(!$UUID) $this->notFound(); + if (!$UUID) { + $this->notFound(); + } $c_user = (new ChandlerUsers())->getById($UUID); $user = $this->users->getByChandlerUser($c_user); - if(!$user) $this->notFound(); + if (!$user) { + $this->notFound(); + } $this->redirect("/admin/users/id" . $user->getId()); } - function renderLogs(): void + public function renderMusic(): void + { + $this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios"; + if ($this->template->mode === "audios") { + $this->template->audios = $this->searchResults($this->audios, $this->template->count); + } else { + $this->template->playlists = $this->searchPlaylists($this->template->count); + } + } + + public function renderEditMusic(int $audio_id): void + { + $audio = $this->audios->get($audio_id); + $this->template->audio = $audio; + + try { + $this->template->owner = $audio->getOwner()->getId(); + } catch (\Throwable $e) { + $this->template->owner = 1; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $audio->setName($this->postParam("name")); + $audio->setPerformer($this->postParam("performer")); + $audio->setLyrics($this->postParam("text")); + $audio->setGenre($this->postParam("genre")); + $audio->setOwner((int) $this->postParam("owner")); + $audio->setExplicit(!empty($this->postParam("explicit"))); + $audio->setDeleted(!empty($this->postParam("deleted"))); + $audio->setWithdrawn(!empty($this->postParam("withdrawn"))); + + if (!empty($this->postParam("playlist_id"))) { + $audio->setAlbumId((int) $this->postParam("playlist_id")); + } + + $audio->save(); + } + } + + public function renderEditPlaylist(int $playlist_id): void + { + $playlist = $this->audios->getPlaylist($playlist_id); + $this->template->playlist = $playlist; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $playlist->setName($this->postParam("name")); + $playlist->setDescription($this->postParam("description")); + $playlist->setCover_Photo_Id((int) $this->postParam("photo")); + $playlist->setOwner((int) $this->postParam("owner")); + $playlist->setDeleted(!empty($this->postParam("deleted"))); + $playlist->save(); + } + } + + public function renderLogs(): void { $filter = []; @@ -587,7 +869,7 @@ function renderLogs(): void $filter["id"] = $id; $this->template->id = $id; } - if ($this->queryParam("type") !== NULL && $this->queryParam("type") !== "any") { + if ($this->queryParam("type") !== null && $this->queryParam("type") !== "any") { $type = in_array($this->queryParam("type"), [0, 1, 2, 3]) ? (int) $this->queryParam("type") : 0; $filter["type"] = $type; $this->template->type = $type; @@ -602,13 +884,14 @@ function renderLogs(): void $filter["object_id"] = $obj_id; $this->template->obj_id = $obj_id; } - if ($this->queryParam("obj_type") !== NULL && $this->queryParam("obj_type") !== "any") { + if ($this->queryParam("obj_type") !== null && $this->queryParam("obj_type") !== "any") { $obj_type = "openvk\\Web\\Models\\Entities\\" . $this->queryParam("obj_type"); $filter["object_model"] = $obj_type; $this->template->obj_type = $obj_type; } - $this->template->logs = (new Logs)->search($filter); - $this->template->object_types = (new Logs)->getTypes(); + $logs = iterator_to_array((new Logs())->search($filter)); + $this->template->logs = $logs; + $this->template->object_types = (new Logs())->getTypes(); } } diff --git a/Web/Presenters/AppsPresenter.php b/Web/Presenters/AppsPresenter.php index 8dcfb8a46..35bc87262 100644 --- a/Web/Presenters/AppsPresenter.php +++ b/Web/Presenters/AppsPresenter.php @@ -1,5 +1,9 @@ -apps = $apps; - + parent::__construct(); } - - function renderPlay(int $app): void + + public function renderPlay(int $app): void { $this->assertUserLoggedIn(); - + $app = $this->apps->get($app); - if(!$app || !$app->isEnabled()) + if (!$app || !$app->isEnabled() || $app->isDeleted()) { $this->notFound(); - + } + $this->template->id = $app->getId(); $this->template->name = $app->getName(); $this->template->desc = $app->getDescription(); @@ -31,70 +36,82 @@ function renderPlay(int $app): void $this->template->news = $app->getNote(); $this->template->perms = $app->getPermissions($this->user->identity); } - - function renderUnInstall(): void + + public function renderUnInstall(): void { $this->assertUserLoggedIn(); $this->assertNoCSRF(); - + $app = $this->apps->get((int) $this->queryParam("app")); - if(!$app) + if (!$app || $app->isDeleted()) { $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 + + public function renderEdit(): void { $this->assertUserLoggedIn(); - - $app = NULL; - if($this->queryParam("act") !== "create") { - if(empty($this->queryParam("app"))) + + $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) + if (!$app || $app->isDeleted()) { $this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc")); - - if($app->getOwner()->getId() != $this->user->identity->getId()) + } + + 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; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (!$app) { + $app = new Application(); $app->setOwner($this->user->id); + } elseif ($this->postParam("delete_app") && $app->getOwner()->getId() === $this->user->id) { + $app->delete(); + $this->redirect("/apps?act=dev"); + return; } - - if(!filter_var($this->postParam("url"), FILTER_VALIDATE_URL)) + + 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) + } + + 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); + + if (empty($this->postParam("note"))) { + $app->setNoteLink(null); } else { - if(!$app->setNoteLink($this->postParam("note"))) + 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") + if ($this->postParam("enable") === "on") { $app->enable(); - else - $app->disable(); # no need to save since enable/disable will call save() internally - + } 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)) { + + if (!is_null($app)) { $this->template->create = false; $this->template->id = $app->getId(); $this->template->name = $app->getName(); @@ -105,34 +122,36 @@ function renderEdit(): void $this->template->note = $app->getNoteLink(); $this->template->users = $app->getUsersCount(); $this->template->on = $app->isEnabled(); + $this->template->owner = $app->getOwner(); } else { $this->template->create = true; } } - - function renderList(): void + + public function renderList(): void { $this->assertUserLoggedIn(); - + $act = $this->queryParam("act"); - if(!in_array($act, ["list", "installed", "dev"])) + if (!in_array($act, ["list", "installed", "dev"])) { $act = "installed"; - + } + $page = (int) ($this->queryParam("p") ?? 1); - if($act == "list") { + if ($act == "list") { $apps = $this->apps->getList($page); $count = $this->apps->getListCount(); - } else if($act == "installed") { + } elseif ($act == "installed") { $apps = $this->apps->getInstalled($this->user->identity, $page); $count = $this->apps->getInstalledCount($this->user->identity); - } else if($act == "dev") { + } elseif ($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; } -} \ No newline at end of file +} diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php new file mode 100644 index 000000000..d774d1220 --- /dev/null +++ b/Web/Presenters/AudioPresenter.php @@ -0,0 +1,916 @@ +audios = $audios; + } + + public function renderPopular(): void + { + $this->renderList(null, "popular"); + } + + public function renderNew(): void + { + $this->renderList(null, "new"); + } + + public function renderList(?int $owner = null, ?string $mode = "list"): void + { + $this->assertUserLoggedIn(); + $this->template->_template = "Audio/List.latte"; + $page = (int) ($this->queryParam("p") ?? 1); + $audios = []; + + if ($mode === "list") { + $entity = null; + if ($owner < 0) { + $entity = (new Clubs())->get($owner * -1); + if (!$entity || $entity->isBanned()) { + $this->redirect("/audios" . $this->user->id); + } + + $audios = $this->audios->getByClub($entity, $page, 10); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } else { + $entity = (new Users())->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) { + $this->redirect("/audios" . $this->user->id); + } + + if (!$entity->getPrivacyPermission("audios.read", $this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + + $audios = $this->audios->getByUser($entity, $page, 10); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } + + if (!$entity) { + $this->notFound(); + } + + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : null; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } elseif ($mode === "new") { + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + } elseif ($mode === "uploaded") { + $stream = $this->audios->getByUploader($this->user->identity); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + } elseif ($mode === "playlists") { + if ($owner < 0) { + $entity = (new Clubs())->get(abs($owner)); + if (!$entity || $entity->isBanned()) { + $this->redirect("/playlists" . $this->user->id); + } + + $playlists = $this->audios->getPlaylistsByClub($entity, $page, OPENVK_DEFAULT_PER_PAGE); + $playlistsCount = $this->audios->getClubPlaylistsCount($entity); + } else { + $entity = (new Users())->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) { + $this->redirect("/playlists" . $this->user->id); + } + + if (!$entity->getPrivacyPermission("audios.read", $this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + + $playlists = $this->audios->getPlaylistsByUser($entity, $page, OPENVK_DEFAULT_PER_PAGE); + $playlistsCount = $this->audios->getUserPlaylistsCount($entity); + } + + $this->template->playlists = iterator_to_array($playlists); + $this->template->playlistsCount = $playlistsCount; + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : null; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } elseif ($mode === 'alone_audio') { + $audios = [$this->template->alone_audio]; + $audiosCount = 1; + + $this->template->owner = $this->user->identity; + $this->template->ownerId = $this->user->id; + } + + // $this->renderApp("owner=$owner"); + if ($audios !== []) { + $this->template->audios = iterator_to_array($audios); + $this->template->audiosCount = $audiosCount; + } + + $this->template->mode = $mode; + $this->template->page = $page; + + if (in_array($mode, ["list", "new", "popular"]) && $this->user->identity && $page < 2) { + $this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true); + } + } + + public function renderUploaded() + { + $this->renderList(null, "uploaded"); + } + + public function renderEmbed(int $owner, int $id): void + { + $audio = $this->audios->getByOwnerAndVID($owner, $id); + if (!$audio) { + header("HTTP/1.1 404 Not Found"); + exit("" . tr("audio_embed_not_found") . "."); + } elseif ($audio->isDeleted()) { + header("HTTP/1.1 410 Not Found"); + exit("" . tr("audio_embed_deleted") . "."); + } elseif ($audio->isWithdrawn()) { + header("HTTP/1.1 451 Unavailable for legal reasons"); + exit("" . tr("audio_embed_withdrawn") . "."); + } elseif (!$audio->canBeViewedBy(null)) { + header("HTTP/1.1 403 Forbidden"); + exit("" . tr("audio_embed_forbidden") . "."); + } elseif (!$audio->isAvailable()) { + header("HTTP/1.1 425 Too Early"); + exit("" . tr("audio_embed_processing") . "."); + } + + $this->template->audio = $audio; + } + + public function renderUpload(): void + { + $this->assertUserLoggedIn(); + + $group = null; + $playlist = null; + $isAjax = $this->postParam("ajax", false) == 1; + + if (!is_null($this->queryParam("gid")) && !is_null($this->queryParam("playlist"))) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if (!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs())->get($gid); + if (!$group) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if (!$group->canUploadAudio($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + } + + if (!is_null($this->queryParam("playlist"))) { + $playlist_id = (int) $this->queryParam("playlist"); + $playlist = (new Audios())->getPlaylist($playlist_id); + if (!$playlist || $playlist->isDeleted()) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if (!$playlist->canBeModifiedBy($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + $this->template->playlist = $playlist; + $this->template->owner = $playlist->getOwner(); + } + + $this->template->group = $group; + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + return; + } + + $upload = $_FILES["blob"]; + if (isset($upload) && file_exists($upload["tmp_name"])) { + if ($upload["size"] > self::MAX_AUDIO_SIZE) { + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax); + } + } else { + $err = !isset($upload) ? 65536 : $upload["error"]; + $err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT); + $readableError = tr("error_generic"); + + switch ($upload["error"]) { + default: + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $readableError = tr("file_too_big"); + break; + case UPLOAD_ERR_PARTIAL: + $readableError = tr("file_loaded_partially"); + break; + case UPLOAD_ERR_NO_FILE: + $readableError = tr("file_not_uploaded"); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $readableError = "Missing a temporary folder."; + break; + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + $readableError = "Failed to write file to disk. "; + break; + } + + $this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax); + } + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre"); + $nsfw = ($this->postParam("explicit") ?? "off") === "on"; + $is_unlisted = ($this->postParam("unlisted") ?? "off") === "on"; + + if (empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) { # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax); + } + + $audio = new Audio(); + $audio->setOwner($this->user->id); + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? null : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setUnlisted($is_unlisted); + + try { + $audio->setFile($upload); + } catch (\DomainException $ex) { + $e = $ex->getMessage(); + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax); + } catch (\RuntimeException $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax); + } catch (\BadMethodCallException $ex) { + $this->flashFail("err", tr("error"), "хз", null, $isAjax); + } catch (\Exception $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax); + } + + if ($playlist) { + $audio->setAlbum($playlist); + } + + $audio->save(); + + if ($playlist) { + $playlist->add($audio); + } else { + $audio->add($group ?? $this->user->identity); + } + + if (!$isAjax) { + $this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId()); + } else { + $redirectLink = "/audios"; + + if (!is_null($group)) { + $redirectLink .= $group->getRealId(); + } else { + $redirectLink .= $this->user->id; + } + + if ($playlist) { + $redirectLink = "/playlist" . $playlist->getPrettyId(); + } + + $this->returnJson([ + "success" => true, + "redirect_link" => $redirectLink, + ]); + } + } + + public function renderAloneAudio(int $owner_id, int $audio_id): void + { + $this->assertUserLoggedIn(); + + $found_audio = $this->audios->get($audio_id); + if (!$found_audio || $found_audio->isDeleted() || !$found_audio->canBeViewedBy($this->user->identity)) { + $this->notFound(); + } + + $this->template->alone_audio = $found_audio; + $this->renderList(null, 'alone_audio'); + } + + public function renderListen(int $id): void + { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->assertNoCSRF(); + + if (is_null($this->user->identity)) { + $this->returnJson(["success" => false]); + } + + $audio = $this->audios->get($id); + + if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) { + if (!empty($this->postParam("playlist"))) { + $playlist = (new Audios())->getPlaylist((int) $this->postParam("playlist")); + + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio)) { + $playlist = null; + } + } + + $listen = $audio->listen($this->user->identity, $playlist); + + $returnArr = ["success" => $listen]; + + if ($playlist) { + $returnArr["new_playlists_listens"] = $playlist->getListens(); + } + + $this->returnJson($returnArr); + } + + $this->returnJson(["success" => false]); + } else { + $this->redirect("/"); + } + } + + public function renderSearch(): void + { + $this->redirect("/search?section=audios"); + } + + public function renderNewPlaylist(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + $owner = $this->user->id; + + if ($this->requestParam("gid")) { + $club = (new Clubs())->get((int) abs((int) $this->requestParam("gid"))); + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) { + $this->redirect("/audios" . $this->user->id); + } + + $owner = ($club->getId() * -1); + + $this->template->club = $club; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $is_unlisted = (int) $this->postParam('is_unlisted'); + $is_ajax = (int) $this->postParam('ajax') == 1; + $audios = array_slice(explode(",", $this->postParam("audios")), 0, 1000); + + if (empty($title) || iconv_strlen($title) < 1) { + $this->flashFail("err", tr("error"), tr("set_playlist_name"), null, $is_ajax); + } + + $playlist = new Playlist(); + $playlist->setOwner($owner); + $playlist->setName(substr($title, 0, 125)); + $playlist->setDescription(substr($description, 0, 2045)); + if ($is_unlisted == 1) { + $playlist->setUnlisted(true); + } + + if ($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if (!str_starts_with($_FILES["cover"]["type"], "image")) { + $this->flashFail("err", tr("error"), tr("not_a_photo"), null, $is_ajax); + } + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch (\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo"), null, $is_ajax); + } + } + + $playlist->save(); + + foreach ($audios as $audio) { + $audio = $this->audios->get((int) $audio); + if (!$audio || $audio->isDeleted()) { + continue; + } + + $playlist->add($audio); + } + + $playlist->bookmark($club ?? $this->user->identity); + if ($is_ajax) { + $this->returnJson([ + 'success' => true, + 'redirect' => '/playlist' . $owner . "_" . $playlist->getId(), + ]); + } + $this->redirect("/playlist" . $owner . "_" . $playlist->getId()); + } else { + $this->template->owner = $owner; + } + } + + public function renderPlaylistAction(int $id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $playlist = $this->audios->getPlaylist($id); + + if (!$playlist || $playlist->isDeleted()) { + $this->flashFail("err", "error", tr("invalid_playlist"), null, true); + } + + switch ($this->queryParam("act")) { + case "bookmark": + if (!$playlist->isBookmarkedBy($this->user->identity)) { + $playlist->bookmark($this->user->identity); + } else { + $this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true); + } + + break; + case "unbookmark": + if ($playlist->isBookmarkedBy($this->user->identity)) { + $playlist->unbookmark($this->user->identity); + } else { + $this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true); + } + + break; + case "delete": + if ($playlist->canBeModifiedBy($this->user->identity)) { + $tmOwner = $playlist->getOwner(); + $playlist->delete(); + } else { + $this->flashFail("err", "error", tr("access_denied"), null, true); + } + + $this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]); + break; + default: + break; + } + + $this->returnJson(["success" => true]); + } + + public function renderEditPlaylist(int $owner_id, int $virtual_id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity)) { + $this->notFound(); + } + + $this->template->playlist = $playlist; + + $audios = iterator_to_array($playlist->fetch(1, $playlist->size())); + $this->template->audios = array_slice($audios, 0, 1000); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + return; + } + + $is_ajax = (int) $this->postParam('ajax') == 1; + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $is_unlisted = (int) $this->postParam('is_unlisted'); + $new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : null; + + if (empty($title) || iconv_strlen($title) < 1) { + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + } + + $playlist->setName(ovk_proc_strtr($title, 125)); + $playlist->setDescription(ovk_proc_strtr($description, 2045)); + $playlist->setEdited(time()); + $playlist->resetLength(); + $playlist->setUnlisted((bool) $is_unlisted); + + if ($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if (!str_starts_with($_FILES["cover"]["type"], "image")) { + $this->flashFail("err", tr("error"), tr("not_a_photo")); + } + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch (\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + DatabaseConnection::i()->getContext()->table("playlist_relations")->where([ + "collection" => $playlist->getId(), + ])->delete(); + + if (!is_null($new_audios)) { + foreach ($new_audios as $new_audio) { + $audio = (new Audios())->get((int) $new_audio); + if (!$audio || $audio->isDeleted()) { + continue; + } + + $playlist->add($audio); + } + } + + if ($is_ajax) { + $this->returnJson([ + 'success' => true, + 'redirect' => '/playlist' . $playlist->getPrettyId(), + ]); + } + $this->redirect("/playlist" . $playlist->getPrettyId()); + } + + public function renderPlaylist(int $owner_id, int $virtual_id): void + { + $this->assertUserLoggedIn(); + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int) ($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted()) { + $this->notFound(); + } + + $this->template->playlist = $playlist; + $this->template->page = $page; + $this->template->cover = $playlist->getCoverPhoto(); + $this->template->cover_url = $this->template->cover ? $this->template->cover->getURL() : "/assets/packages/static/openvk/img/song.jpg"; + $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity); + $this->template->count = $playlist->size(); + } + + public function renderAction(int $audio_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $audio = $this->audios->get($audio_id); + + if (!$audio || $audio->isDeleted()) { + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + } + + switch ($this->queryParam("act")) { + case "add": + if ($audio->isWithdrawn()) { + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + } + + if (!$audio->isInLibraryOf($this->user->identity)) { + $audio->add($this->user->identity); + } else { + $this->flashFail("err", "error", tr("do_have_audio"), null, true); + } + + break; + + case "remove": + if ($audio->isInLibraryOf($this->user->identity)) { + $audio->remove($this->user->identity); + } else { + $this->flashFail("err", "error", tr("do_not_have_audio"), null, true); + } + + break; + case "remove_club": + $club = (new Clubs())->get((int) $this->postParam("club")); + + if (!$club || !$club->canBeModifiedBy($this->user->identity)) { + $this->flashFail("err", "error", tr("access_denied"), null, true); + } + + if ($audio->isInLibraryOf($club)) { + $audio->remove($club); + } else { + $this->flashFail("err", "error", tr("group_hasnt_audio"), null, true); + } + + break; + case "add_to_club": + $detailed = []; + if ($audio->isWithdrawn()) { + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + } + + if (empty($this->postParam("clubs"))) { + $this->flashFail("err", "error", 'clubs not passed', null, true); + } + + $clubs_arr = explode(',', $this->postParam("clubs")); + $count = sizeof($clubs_arr); + if ($count < 1 || $count > 10) { + $this->flashFail("err", "error", tr('too_many_or_to_lack'), null, true); + } + + foreach ($clubs_arr as $club_id) { + $club = (new Clubs())->get((int) $club_id); + if (!$club || !$club->canBeModifiedBy($this->user->identity)) { + continue; + } + + if (!$audio->isInLibraryOf($club)) { + $detailed[$club_id] = true; + $audio->add($club); + } else { + $detailed[$club_id] = false; + continue; + } + } + + $this->returnJson(["success" => true, 'detailed' => $detailed]); + break; + case "add_to_playlist": + $detailed = []; + if ($audio->isWithdrawn()) { + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + } + + if (empty($this->postParam("playlists"))) { + $this->flashFail("err", "error", 'playlists not passed', null, true); + } + + $playlists_arr = explode(',', $this->postParam("playlists")); + $count = sizeof($playlists_arr); + if ($count < 1 || $count > 10) { + $this->flashFail("err", "error", tr('too_many_or_to_lack'), null, true); + } + + foreach ($playlists_arr as $playlist_id) { + $pid = explode('_', $playlist_id); + $playlist = (new Audios())->getPlaylistByOwnerAndVID((int) $pid[0], (int) $pid[1]); + if (!$playlist || !$playlist->canBeModifiedBy($this->user->identity)) { + continue; + } + + if (!$playlist->hasAudio($audio)) { + $playlist->add($audio); + $detailed[$playlist_id] = true; + } else { + $detailed[$playlist_id] = false; + continue; + } + } + + $this->returnJson(["success" => true, 'detailed' => $detailed]); + break; + case "delete": + if ($audio->canBeModifiedBy($this->user->identity)) { + $audio->delete(); + } else { + $this->flashFail("err", "error", tr("access_denied"), null, true); + } + + break; + case "edit": + $audio = $this->audios->get($audio_id); + if (!$audio || $audio->isDeleted() || $audio->isWithdrawn()) { + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + } + + if ($audio->getOwner()->getId() !== $this->user->id) { + $this->flashFail("err", "error", tr("access_denied"), null, true); + } + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre"); + $nsfw = (int) ($this->postParam("explicit") ?? 0) === 1; + $unlisted = (int) ($this->postParam("unlisted") ?? 0) === 1; + $album_id = (int) ($this->postParam("album_id") ?? 0); + if (empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) { # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true); + } + + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? null : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setSearchability($unlisted); + if ($album_id > 0) { + $audio->setAlbumId($album_id); + + $playlist = (new Audios())->getPlaylist($album_id); + if ($playlist && !$playlist->hasAudio($audio)) { + $playlist->add($audio); + } + } else { + $audio->setAlbumId(0); + } + $audio->setEdited(time()); + $audio->save(); + + $this->returnJson(["success" => true, "new_info" => [ + "name" => ovk_proc_strtr($audio->getTitle(), 40), + "performer" => ovk_proc_strtr($audio->getPerformer(), 40), + "lyrics" => nl2br($audio->getLyrics() ?? ""), + "lyrics_unformatted" => $audio->getLyrics() ?? "", + "explicit" => $audio->isExplicit(), + "genre" => $audio->getGenre(), + "unlisted" => $audio->isUnlisted(), + ]]); + break; + + default: + break; + } + + $this->returnJson(["success" => true]); + } + + public function renderPlaylists(int $owner) + { + $this->renderList($owner, "playlists"); + } + + public function renderApiGetContext() + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $ctx_type = $this->postParam("context"); + $ctx_id = (int) ($this->postParam("context_entity")); + $page = (int) ($this->postParam("page") ?? 1); + $perPage = 10; + + switch ($ctx_type) { + default: + case "entity_audios": + if ($ctx_id >= 0) { + $entity = $ctx_id != 0 ? (new Users())->get($ctx_id) : $this->user->identity; + + if (!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity)) { + $this->flashFail("err", "Error", "Can't get queue", 80, true); + } + + $audios = $this->audios->getByUser($entity, $page, $perPage); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } else { + $entity = (new Clubs())->get(abs($ctx_id)); + + if (!$entity || $entity->isBanned()) { + $this->flashFail("err", "Error", "Can't get queue", 80, true); + } + + $audios = $this->audios->getByClub($entity, $page, $perPage); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } + break; + case "new_audios": + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + break; + case "popular_audios": + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + break; + case "playlist_context": + $playlist = $this->audios->getPlaylist($ctx_id); + + if (!$playlist || $playlist->isDeleted()) { + $this->flashFail("err", "Error", "Can't get queue", 80, true); + } + + $audios = $playlist->fetch($page, 10); + $audiosCount = $playlist->size(); + break; + case "search_context": + $stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer"); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + case "classic_search_context": + $data = json_decode($this->postParam("context_entity"), true); + + $params = []; + $order = [ + "type" => $data['order'] ?? 'id', + "invert" => (int) $data['invert'] == 1 ? true : false, + ]; + + if ($data['genre'] && $data['genre'] != 'any') { + $params['genre'] = $data['genre']; + } + + if ($data['only_performers'] && (int) $data['only_performers'] == 1) { + $params['only_performers'] = '1'; + } + + if ($data['with_lyrics'] && (int) $data['with_lyrics'] == 1) { + $params['with_lyrics'] = '1'; + } + + $stream = $this->audios->find($data['query'], $params, $order); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + case 'alone_audio': + $found_audio = $this->audios->get($ctx_id); + if (!$found_audio || $found_audio->isDeleted() || !$found_audio->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", "Error", "Not found", 89, true); + } + + $audios = [$found_audio]; + $audiosCount = 1; + break; + case "uploaded": + $stream = $this->audios->getByUploader($this->user->identity); + $audios = $stream->page($page, $perPage); + $audiosCount = $stream->size(); + } + + $pagesCount = ceil($audiosCount / $perPage); + + if ((int) ($this->postParam("returnPlayers")) === 1) { + $this->template->audios = $audios; + $this->template->page = $page; + $this->template->pagesCount = $pagesCount; + $this->template->count = $audiosCount; + + return 0; + } + + $audiosArr = []; + + foreach ($audios as $audio) { + $obj = $audio->toVkApiStruct($this->user->identity); + $obj->id = $audio->getId(); + $obj->name = $audio->getTitle(); + $obj->performer = $audio->getPerformer(); + $obj->length = $audio->getLength(); + $obj->available = $audio->isAvailable(); + + if (!$audio->isWithdrawn()) { + $obj->keys = $audio->getKeys(); + $obj->url = $audio->getUrl(); + } + + $audiosArr[] = $obj; + } + + $resultArr = [ + "success" => true, + "page" => $page, + "perPage" => $perPage, + "pagesCount" => $pagesCount, + "count" => $audiosCount, + "items" => $audiosArr, + ]; + + $this->returnJson($resultArr); + } +} diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index c6a7f143f..e5f044be8 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -1,5 +1,9 @@ -authenticator = Authenticator::i(); $this->db = DatabaseConnection::i()->getContext(); - + $this->users = $users; $this->restores = $restores; $this->verifications = $verifications; - + parent::__construct(); } - + private function ipValid(): bool { - $ip = (new IPs)->get(CONNECTING_IP); + $ip = (new IPs())->get(CONNECTING_IP); $res = $ip->rateLimit(0); - + return $res === IP::RL_RESET || $res === IP::RL_CANEXEC; } - - function renderRegister(): void + + public function renderRegister(): void { - if(!is_null($this->user)) + if (!is_null($this->user->identity)) { $this->redirect($this->user->identity->getURL()); - - if(!$this->hasPermission("user", "register", -1)) exit("Вас забанили"); - - $referer = NULL; - if(!is_null($refLink = $this->queryParam("ref"))) { + } + + if (!$this->hasPermission("user", "register", -1)) { + exit("Вас забанили"); + } + + $referer = null; + if (!is_null($refLink = $this->queryParam("ref"))) { $pieces = explode(" ", $refLink, 2); - if(sizeof($pieces) !== 2) + if (sizeof($pieces) !== 2) { $this->flashFail("err", tr("error"), tr("referral_link_invalid")); - + } + [$ref, $hash] = $pieces; $ref = hexdec($ref); $hash = base64_decode($hash); - - $referer = (new Users)->get($ref); - if(!$referer) + + $referer = (new Users())->get($ref); + if (!$referer) { $this->flashFail("err", tr("error"), tr("referral_link_invalid")); - - if($referer->getRefLinkId() !== $refLink) + } + + if ($referer->getRefLinkId() !== $refLink) { $this->flashFail("err", tr("error"), tr("referral_link_invalid")); + } } - + $this->template->referer = $referer; - - if($_SERVER["REQUEST_METHOD"] === "POST") { + + $this->template->emailWhitelistEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['registration']['emailWhitelist']['enable'] ?? false; + $this->template->emailWhitelisted = implode(', ', OPENVK_ROOT_CONF['openvk']['preferences']['registration']['emailWhitelist']['allowedHosts'] ?? ['none']); + + + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->assertCaptchaCheckPassed(); - if(!OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable'] && !$referer) + if (!OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable'] && !$referer) { $this->flashFail("err", tr("failed_to_register"), tr("registration_disabled")); - - if(!$this->ipValid()) + } + + if (!$this->ipValid()) { $this->flashFail("err", tr("suspicious_registration_attempt"), tr("suspicious_registration_attempt_comment")); - - if(!Validator::i()->emailValid($this->postParam("email"))) + } + + if (OPENVK_ROOT_CONF['openvk']['preferences']['registration']['emailWhitelist']['enable'] ?? false) { + $domain = explode("@", $this->postParam("email")); + + if (!in_array($domain[1], OPENVK_ROOT_CONF['openvk']['preferences']['registration']['emailWhitelist']['allowedHosts'])) { + $this->flashFail("err", tr("failed_to_register"), tr("email_not_in_whitelist")); + } + } + + if (!Validator::i()->emailValid($this->postParam("email"))) { $this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment")); + } - if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['forceStrongPassword']) - if(!Validator::i()->passwordStrong($this->postParam("password"))) + if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['forceStrongPassword']) { + if (!Validator::i()->passwordStrong($this->postParam("password"))) { $this->flashFail("err", tr("error"), tr("error_weak_password")); + } + } - if (strtotime($this->postParam("birthday")) > time()) + if (strtotime($this->postParam("birthday")) > time()) { $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); + } - if (!$this->postParam("confirmation")) + if (!$this->postParam("confirmation")) { $this->flashFail("err", tr("error"), tr("checkbox_in_registration_unchecked")); + } try { - $user = new User; + $user = new User(); $user->setFirst_Name($this->postParam("first_name")); $user->setLast_Name($this->postParam("last_name")); - $user->setSex((int)($this->postParam("sex") === "female")); + switch ($this->postParam("pronouns")) { + case 'male': + $user->setSex(0); + break; + case 'female': + $user->setSex(1); + break; + case 'neutral': + $user->setSex(2); + break; + } $user->setEmail($this->postParam("email")); $user->setSince(date("Y-m-d H:i:s")); $user->setRegistering_Ip(CONNECTING_IP); - $user->setBirthday(empty($this->postParam("birthday")) ? NULL : strtotime($this->postParam("birthday"))); - $user->setActivated((int)!OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']); - } catch(InvalidUserNameException $ex) { + $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")); } $chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password")); - if(!$chUser) + if (!$chUser) { $this->flashFail("err", tr("failed_to_register"), tr("user_already_exists")); + } $user->setUser($chUser->getId()); $user->save(false); - - if(!is_null($referer)) { + + if (!is_null($referer)) { $user->toggleSubscription($referer); $referer->toggleSubscription($user); } if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']) { - $verification = new EmailVerification; + $verification = new EmailVerification(); $verification->setProfile($user->getId()); $verification->save(); - + $params = [ "key" => $verification->getKey(), "name" => $user->getCanonicalName(), ]; $this->sendmail($user->getEmail(), "verify-email", $params); #Vulnerability possible } - + $this->authenticator->authenticate($chUser->getId()); $this->redirect("/id" . $user->getId()); $user->save(); } } - - function renderLogin(): void + + public function renderLogin(): void { $redirUrl = $this->requestParam("jReturnTo"); - - if(!is_null($this->user)) + + if (!is_null($this->user->identity)) { $this->redirect($redirUrl ?? $this->user->identity->getURL()); - - if(!$this->hasPermission("user", "login", -1)) exit("Вас забанили"); - - if($_SERVER["REQUEST_METHOD"] === "POST") { + } + + 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) + if (!$user) { $this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password")); - - if(!$this->authenticator->verifyCredentials($user->id, $this->postParam("password"))) + } + + if (!$this->authenticator->verifyCredentials($user->id, $this->postParam("password"))) { $this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password")); + } $ovkUser = new User($user->related("profiles.user")->fetch()); - if($ovkUser->isDeleted() && !$ovkUser->isDeactivated()) + if ($ovkUser->isDeleted() && !$ovkUser->isDeactivated()) { $this->flashFail("err", tr("login_failed"), tr("invalid_username_or_password")); + } $secret = $user->related("profiles.user")->fetch()["2fa_secret"]; $code = $this->postParam("code"); - if(!is_null($secret)) { - $this->template->_template = "Auth/LoginSecondFactor.xml"; + if (!is_null($secret)) { + $this->template->_template = "Auth/LoginSecondFactor.latte"; $this->template->login = $this->postParam("login"); $this->template->password = $this->postParam("password"); - if(is_null($code)) + if (is_null($code)) { return; + } - if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $ovkUser->use2faBackupCode((int) $code))) { + if (!($code === (new Totp())->GenerateToken(Base32::decode($secret)) || $ovkUser->use2faBackupCode((int) $code))) { $this->flash("err", tr("login_failed"), tr("incorrect_2fa_code")); return; } } - + $this->authenticator->authenticate($user->id); $this->redirect($redirUrl ?? $ovkUser->getURL()); } } - - function renderSu(string $uuid): void + + public function renderSu(string $uuid): void { $this->assertNoCSRF(); $this->assertUserLoggedIn(); - - if($uuid === "unset") { - Session::i()->set("_su", NULL); + + if ($uuid === "unset") { + Session::i()->set("_su", null); $this->redirect("/"); } - - if(!$this->db->table("ChandlerUsers")->where("id", $uuid)) + + if (!$this->db->table("ChandlerUsers")->where("id", $uuid)) { $this->flashFail("err", tr("token_manipulation_error"), tr("profile_not_found")); - + } + $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("/"); } - - function renderLogout(): void + + public function renderLogout(): void { $this->assertUserLoggedIn(); $this->assertNoCSRF(); $this->authenticator->logout(); - Session::i()->set("_su", NULL); - + Session::i()->set("_su", null); + $this->redirect("/"); } - - function renderFinishRestoringPassword(): void + + public function renderFinishRestoringPassword(): void { - if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']) + if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']) { $this->notFound(); + } $request = $this->restores->getByToken(str_replace(" ", "+", $this->queryParam("key"))); - if(!$request || !$request->isStillValid()) { + if (!$request || !$request->isStillValid()) { $this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment")); $this->redirect("/"); return; } + $this->template->disable_ajax = 1; $this->template->is2faEnabled = $request->getUser()->is2faEnabled(); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if($request->getUser()->is2faEnabled()) { + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if ($request->getUser()->is2faEnabled()) { $user = $request->getUser(); $code = $this->postParam("code"); $secret = $user->get2faSecret(); - if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $user->use2faBackupCode((int) $code))) { + if (!($code === (new Totp())->GenerateToken(Base32::decode($secret)) || $user->use2faBackupCode((int) $code))) { $this->flash("err", tr("error"), tr("incorrect_2fa_code")); return; } @@ -233,88 +283,96 @@ function renderFinishRestoringPassword(): void $user = $request->getUser()->getChandlerUser(); $this->db->table("ChandlerTokens")->where("user", $user->getId())->delete(); #Logout from everywhere - + $user->updatePassword($this->postParam("password")); $this->authenticator->authenticate($user->getId()); - + $request->delete(false); $this->flash("succ", tr("information_-1"), tr("password_successfully_reset")); $this->redirect("/settings"); } } - - function renderRestore(): void + + public function renderRestore(): void { - if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']) + if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']) { $this->notFound(); + } - if(!is_null($this->user)) + if (!is_null($this->user->identity)) { $this->redirect($this->user->identity->getURL()); + } - if(($this->queryParam("act") ?? "default") === "finish") + if (($this->queryParam("act") ?? "default") === "finish") { $this->pass("openvk!Auth->finishRestoringPassword"); - - if($_SERVER["REQUEST_METHOD"] === "POST") { + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { $uRow = $this->db->table("ChandlerUsers")->where("login", $this->postParam("login"))->fetch(); - if(!$uRow) { + if (!$uRow) { #Privacy of users must be protected. We will not tell if email is bound to a user or not. $this->flashFail("succ", tr("information_-1"), tr("password_reset_email_sent")); } - + $user = $this->users->getByChandlerUser(new ChandlerUser($uRow)); - if(!$user || $user->isDeleted()) + if (!$user || $user->isDeleted()) { $this->flashFail("err", tr("error"), tr("password_reset_error")); - + } + $request = $this->restores->getLatestByUser($user); - if(!is_null($request) && $request->isNew()) + if (!is_null($request) && $request->isNew()) { $this->flashFail("err", tr("forbidden"), tr("password_reset_rate_limit_error")); - - $resetObj = new PasswordReset; + } + + $resetObj = new PasswordReset(); $resetObj->setProfile($user->getId()); $resetObj->save(); - + $params = [ "key" => $resetObj->getKey(), "name" => $user->getCanonicalName(), ]; $this->sendmail($uRow->login, "password-reset", $params); #Vulnerability possible - + $this->flashFail("succ", tr("information_-1"), tr("password_reset_email_sent")); } } - function renderResendEmail(): void + public function renderResendEmail(): void { - if(!is_null($this->user) && $this->user->identity->isActivated()) + if (!is_null($this->user->identity) && $this->user->identity->isActivated()) { $this->redirect($this->user->identity->getURL()); + } - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $user = $this->user->identity; - if(!$user || $user->isDeleted() || $user->isActivated()) + if (!$user || $user->isDeleted() || $user->isActivated()) { $this->flashFail("err", tr("error"), tr("email_error")); - + } + $request = $this->verifications->getLatestByUser($user); - if(!is_null($request) && $request->isNew()) + if (!is_null($request) && $request->isNew()) { $this->flashFail("err", tr("forbidden"), tr("email_rate_limit_error")); - - $verification = new EmailVerification; + } + + $verification = new EmailVerification(); $verification->setProfile($user->getId()); $verification->save(); - + $params = [ "key" => $verification->getKey(), "name" => $user->getCanonicalName(), ]; $this->sendmail($user->getEmail(), "verify-email", $params); #Vulnerability possible - + $this->flashFail("succ", tr("information_-1"), tr("email_sent")); } } - function renderVerifyEmail(): void + public function renderVerifyEmail(): void { $request = $this->verifications->getByToken(str_replace(" ", "+", $this->queryParam("key"))); - if(!$request || !$request->isStillValid()) { + if (!$request || !$request->isStillValid()) { $this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment")); $this->redirect("/"); } else { @@ -327,7 +385,7 @@ function renderVerifyEmail(): void } } - function renderReactivatePage(): void + public function renderReactivatePage(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); @@ -337,36 +395,38 @@ function renderReactivatePage(): void $this->redirect("/"); } - function renderUnbanThemself(): void + public function renderUnbanThemself(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!$this->user->identity->canUnbanThemself()) + if (!$this->user->identity->canUnbanThemself()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } $user = $this->users->get($this->user->id); - $ban = (new Bans)->get((int)$user->getRawBanReason()); - if (!$ban || $ban->isOver() || $ban->isPermanent()) + $ban = (new Bans())->get((int) $user->getRawBanReason()); + if (!$ban || $ban->isOver() || $ban->isPermanent()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } $ban->setRemoved_Manually(2); $ban->setRemoved_By($this->user->identity->getId()); $ban->save(); - $user->setBlock_Reason(NULL); + $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 + */ + public function renderRevokeAllTokens(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); @@ -378,4 +438,4 @@ function renderRevokeAllTokens(): void $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")); } -} +} diff --git a/Web/Presenters/AwayPresenter.php b/Web/Presenters/AwayPresenter.php index 18c7cca72..030934a8f 100644 --- a/Web/Presenters/AwayPresenter.php +++ b/Web/Presenters/AwayPresenter.php @@ -1,28 +1,47 @@ -check($this->queryParam("to") . "/"); - if (OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["warnings"]) - if (sizeof($checkBanEntries) > 0) + $redirTo = $this->queryParam("to"); + if (OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["warnings"]) { + $checkBanEntries = (new BannedLinks())->check($redirTo); + if (sizeof($checkBanEntries) > 0) { $this->pass("openvk!Away->view", $checkBanEntries[0]); + } + } + + if (isset(OPENVK_ROOT_CONF["openvk"]["mirrors"])) { + $uri = str_replace(["https://", "http://"], "", $redirTo); + $domainTo = explode("/", $uri)[0]; + $isMirror = in_array(str_replace("www.", "", $domainTo), OPENVK_ROOT_CONF["openvk"]["mirrors"]); + if ($isMirror) { + $currentDomain = $_SERVER["SERVER_NAME"]; + $redirTo = str_replace($domainTo, $currentDomain, $redirTo); + } + } header("HTTP/1.0 302 Found"); header("X-Robots-Tag: noindex, nofollow, noarchive"); - header("Location: " . $this->queryParam("to")); + header("Location: " . rawurldecode($redirTo)); exit; } - function renderView(int $lid) { - $this->template->link = (new BannedLinks)->get($lid); + public function renderView(int $lid) + { + $this->template->link = (new BannedLinks())->get($lid); - if (!$this->template->link) + if (!$this->template->link) { $this->notFound(); + } $this->template->to = $this->queryParam("to"); } diff --git a/Web/Presenters/BannedLinkPresenter.php b/Web/Presenters/BannedLinkPresenter.php index 6a11b3fce..3544dd822 100644 --- a/Web/Presenters/BannedLinkPresenter.php +++ b/Web/Presenters/BannedLinkPresenter.php @@ -1,4 +1,7 @@ -template->link = (new BannedLinks)->get($lid); + public function renderView(int $lid) + { + $this->template->link = (new BannedLinks())->get($lid); $this->template->to = $this->queryParam("to"); } } diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 7bb3e2be0..619d9d544 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -1,4 +1,7 @@ -getDirName($dir); $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 + if (!$path) { # Will also check if file exists since realpath fails on ENOENT $this->notFound(); - else if(strpos($path, $path) !== 0) # Prevent directory traversal and storage container escape + } elseif (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")); - + } + + if (isset($_SERVER["HTTP_IF_NONE_MATCH"])) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } + header("Content-Type: " . mime_content_type($path)); header("Content-Size: " . filesize($path)); header("Cache-Control: public, max-age=1210000"); header("X-Accel-Expires: 1210000"); header("ETag: W/\"" . hash_file("snefru", $path) . "\""); - + readfile($path); exit; } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index e005af866..9f291161f 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -1,8 +1,13 @@ - "openvk\\Web\\Models\\Repositories\\Notes", "topics" => "openvk\\Web\\Models\\Repositories\\Topics", ]; - - function renderLike(int $id): void + + public function renderLike(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - $comment = (new Comments)->get($id); - if(!$comment || $comment->isDeleted()) $this->notFound(); - if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + $comment = (new Comments())->get($id); + if (!$comment || $comment->isDeleted()) { + $this->notFound(); + } + + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - - if(!is_null($this->user)) $comment->toggleLike($this->user->identity); - + } + + if (!is_null($this->user->identity)) { + $comment->toggleLike($this->user->identity); + } + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + $this->redirect($_SERVER["HTTP_REFERER"]); } - - function renderMakeComment(string $repo, int $eId): void + + public function renderMakeComment(string $repo, int $eId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - $repoClass = $this->models[$repo] ?? NULL; - if(!$repoClass) chandler_http_panic(400, "Bad Request", "Unexpected $repo."); - - $repo = new $repoClass; + + $repoClass = $this->models[$repo] ?? null; + if (!$repoClass) { + chandler_http_panic(400, "Bad Request", "Unexpected $repo."); + } + + $repo = new $repoClass(); $entity = $repo->get($eId); - if(!$entity) $this->notFound(); + if (!$entity) { + $this->notFound(); + } + + if (!$entity->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } - if($entity instanceof Topic && $entity->isClosed()) + if ($entity instanceof Topic && $entity->isClosed()) { $this->notFound(); + } - if($entity instanceof Post && $entity->getTargetWall() < 0) - $club = (new Clubs)->get(abs($entity->getTargetWall())); - else if($entity instanceof Topic) + if ($entity instanceof Post && $entity->getTargetWall() < 0) { + $club = (new Clubs())->get(abs($entity->getTargetWall())); + } elseif ($entity instanceof Topic) { $club = $entity->getClub(); + } + + if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } - if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) + if ($entity instanceof Topic && $entity->isRestricted() && !$entity->getClub()->canBeModifiedBy($this->user->identity)) { $this->flashFail("err", tr("error"), tr("forbidden")); + } $flags = 0; - if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) + if ($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) { $flags |= 0b10000000; + } - $photo = NULL; - if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { + $photo = null; + if ($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { try { $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]); - } catch(ISE $ex) { + } catch (ISE $ex) { $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_when_publishing_comment_description")); } } - - $photos = []; - if(!empty($this->postParam("photos"))) { - $un = rtrim($this->postParam("photos"), ","); - $arr = explode(",", $un); - - if(sizeof($arr) < 11) { - foreach($arr as $dat) { - $ids = explode("_", $dat); - $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); - - if(!$photo || $photo->isDeleted()) - continue; - - $photos[] = $photo; - } + + $horizontal_attachments = []; + $vertical_attachments = []; + if (!empty($this->postParam("horizontal_attachments"))) { + $horizontal_attachments_array = array_slice(explode(",", $this->postParam("horizontal_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if (sizeof($horizontal_attachments_array) > 0) { + $horizontal_attachments = parseAttachments($horizontal_attachments_array, ['photo', 'video']); } } - $videos = []; - - if(!empty($this->postParam("videos"))) { - $un = rtrim($this->postParam("videos"), ","); - $arr = explode(",", $un); - - if(sizeof($arr) < 11) { - foreach($arr as $dat) { - $ids = explode("_", $dat); - $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); - - if(!$video || $video->isDeleted()) - continue; - - $videos[] = $video; - } + if (!empty($this->postParam("vertical_attachments"))) { + $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if (sizeof($vertical_attachments_array) > 0) { + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']); } } - - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1) + + if (empty($this->postParam("text")) && sizeof($horizontal_attachments) < 1 && sizeof($vertical_attachments) < 1) { $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); - + } + try { - $comment = new Comment; + $comment = new Comment(); $comment->setOwner($this->user->id); $comment->setModel(get_class($entity)); $comment->setTarget($entity->getId()); @@ -119,41 +130,59 @@ function renderMakeComment(string $repo, int $eId): void } catch (\LengthException $ex) { $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); } - - foreach($photos as $photo) - $comment->attach($photo); - - if(sizeof($videos) > 0) - foreach($videos as $vid) - $comment->attach($vid); - - if($entity->getOwner()->getId() !== $this->user->identity->getId()) - if(($owner = $entity->getOwner()) instanceof User) + + foreach ($horizontal_attachments as $horizontal_attachment) { + if (!$horizontal_attachment || $horizontal_attachment->isDeleted() || !$horizontal_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $comment->attach($horizontal_attachment); + } + + foreach ($vertical_attachments as $vertical_attachment) { + if (!$vertical_attachment || $vertical_attachment->isDeleted() || !$vertical_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $comment->attach($vertical_attachment); + } + + if ($entity->getOwner()->getId() !== $this->user->identity->getId()) { + if (($owner = $entity->getOwner()) instanceof User) { (new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit(); - + } + } + $excludeMentions = [$this->user->identity->getId()]; - if(($owner = $entity->getOwner()) instanceof User) + if (($owner = $entity->getOwner()) instanceof User) { $excludeMentions[] = $owner->getId(); + } $mentions = iterator_to_array($comment->resolveMentions($excludeMentions)); - foreach($mentions as $mentionee) - if($mentionee instanceof User) + foreach ($mentions as $mentionee) { + if ($mentionee instanceof User) { (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit(); - + } + } + $this->flashFail("succ", tr("comment_is_added"), tr("comment_is_added_desc")); } - - function renderDeleteComment(int $id): void + + public function renderDeleteComment(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - $comment = (new Comments)->get($id); - if(!$comment) $this->notFound(); - if(!$comment->canBeDeletedBy($this->user->identity)) + + $comment = (new Comments())->get($id); + if (!$comment) { + $this->notFound(); + } + if (!$comment->canBeDeletedBy($this->user->identity)) { $this->throwError(403, "Forbidden", tr("error_access_denied")); - if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + } + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } $comment->delete(); $this->flashFail( diff --git a/Web/Presenters/ContentSearchPresenter.php b/Web/Presenters/ContentSearchPresenter.php index f0898fe3e..e22f34fa7 100644 --- a/Web/Presenters/ContentSearchPresenter.php +++ b/Web/Presenters/ContentSearchPresenter.php @@ -1,21 +1,24 @@ -repo = $repo; + $this->repo = $repository; } - - function renderIndex(): void + + public function renderIndex(): void { - if($_SERVER["REQUEST_METHOD"] === "POST") - { - $this->template->results = $repo->find([ + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->template->results = $this->repo->find([ "query" => $this->postParam("query"), ]); } diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php new file mode 100644 index 000000000..de0e03f09 --- /dev/null +++ b/Web/Presenters/DocumentsPresenter.php @@ -0,0 +1,193 @@ +assertUserLoggedIn(); + + $this->template->_template = "Documents/List.latte"; + if ($owner_id > 0) { + $this->notFound(); + } + + if ($owner_id < 0) { + $owner = (new Clubs())->get(abs($owner_id)); + if (!$owner || $owner->isBanned()) { + $this->notFound(); + } else { + $this->template->group = $owner; + } + } + + if (!$owner_id) { + $owner_id = $this->user->id; + } + + $current_tab = (int) ($this->queryParam("tab") ?? 0); + $current_order = (int) ($this->queryParam("order") ?? 0); + $page = (int) ($this->queryParam("p") ?? 1); + $order = in_array($current_order, [0,1,2]) ? $current_order : 0; + $tab = in_array($current_tab, [0,1,2,3,4,5,6,7,8]) ? $current_tab : 0; + + $api_request = $this->queryParam("picker") == "1"; + if ($api_request && $_SERVER["REQUEST_METHOD"] === "POST") { + $ctx_type = $this->postParam("context"); + $docs = null; + + switch ($ctx_type) { + default: + case "list": + $docs = (new Documents())->getDocumentsByOwner($owner_id, (int) $order, (int) $tab); + break; + case "search": + $ctx_query = $this->postParam("ctx_query"); + $docs = (new Documents())->find($ctx_query); + break; + } + + $this->template->docs = $docs->page($page, OPENVK_DEFAULT_PER_PAGE); + $this->template->page = $page; + $this->template->count = $docs->size(); + $this->template->pagesCount = ceil($this->template->count / OPENVK_DEFAULT_PER_PAGE); + $this->template->_template = "Documents/ApiGetContext.latte"; + return; + } + + $docs = (new Documents())->getDocumentsByOwner($owner_id, (int) $order, (int) $tab); + $this->template->tabs = (new Documents())->getTypes($owner_id); + $this->template->tags = (new Documents())->getTags($owner_id, (int) $tab); + $this->template->current_tab = $tab; + $this->template->order = $order; + $this->template->count = $docs->size(); + $this->template->docs = iterator_to_array($docs->page($page, OPENVK_DEFAULT_PER_PAGE)); + $this->template->locale_string = "you_have_x_documents"; + if ($current_tab != 0) { + $this->template->locale_string = "x_documents_in_tab"; + } elseif ($owner_id < 0) { + $this->template->locale_string = "group_has_x_documents"; + } + + $this->template->canUpload = $owner_id == $this->user->id || $this->template->group->canBeModifiedBy($this->user->identity); + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $page, + "amount" => sizeof($this->template->docs), + "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, + ]; + } + + public function renderListGroup(?int $gid) + { + $this->renderList($gid); + } + + public function renderUpload() + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $group = null; + $isAjax = $this->postParam("ajax", false) == 1; + $ref = $this->postParam("referrer", false) ?? "user"; + + if (!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs())->get($gid); + if (!$group || $group->isBanned()) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if (!$group->canUploadDocs($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + } + + $this->template->group = $group; + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + return; + } + + $owner = $this->user->id; + if ($group) { + $owner = $group->getRealId(); + } + + $upload = $_FILES["blob"]; + $name = $this->postParam("name"); + $tags = $this->postParam("tags"); + $folder = $this->postParam("folder"); + $owner_hidden = ($this->postParam("owner_hidden") ?? "off") === "on"; + + try { + $document = new Document(); + $document->setOwner($owner); + $document->setName(ovk_proc_strtr($name, 255)); + $document->setFolder_id($folder); + $document->setTags(empty($tags) ? null : $tags); + $document->setOwner_hidden($owner_hidden); + $document->setFile([ + "tmp_name" => $upload["tmp_name"], + "error" => $upload["error"], + "name" => $upload["name"], + "size" => $upload["size"], + "preview_owner" => $this->user->id, + ]); + + $document->save(); + } catch (\TypeError $e) { + $this->flashFail("err", tr("forbidden"), $e->getMessage(), null, $isAjax); + } catch (ISE $e) { + $this->flashFail("err", tr("forbidden"), "corrupted file", null, $isAjax); + } catch (\ValueError $e) { + $this->flashFail("err", tr("forbidden"), $e->getMessage(), null, $isAjax); + } catch (\ImagickException $e) { + $this->flashFail("err", tr("forbidden"), tr("error_file_preview"), null, $isAjax); + } + + if (!$isAjax) { + $this->redirect("/docs" . (isset($group) ? $group->getRealId() : "")); + } else { + $this->returnJson([ + "success" => true, + "redirect" => "/docs" . (isset($group) ? $group->getRealId() : ""), + ]); + } + } + + public function renderPage(int $virtual_id, int $real_id): void + { + $this->assertUserLoggedIn(); + + $access_key = $this->queryParam("key"); + $doc = (new Documents())->getDocumentById((int) $virtual_id, (int) $real_id, $access_key); + if (!$doc || $doc->isDeleted()) { + $this->notFound(); + } + + if (!$doc->checkAccessKey($access_key)) { + $this->notFound(); + } + + $this->template->doc = $doc; + $this->template->type = $doc->getVKAPIType(); + $this->template->is_image = $doc->isImage(); + $this->template->tags = $doc->getTags(); + $this->template->copied = $doc->isCopiedBy($this->user->identity); + $this->template->copyImportance = true; + $this->template->modifiable = $doc->canBeModifiedBy($this->user->identity); + } +} diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php index 39359add8..7ea909d80 100644 --- a/Web/Presenters/GiftsPresenter.php +++ b/Web/Presenters/GiftsPresenter.php @@ -1,5 +1,9 @@ -gifts = $gifts; $this->users = $users; } - - function renderUserGifts(int $user): void + + public function renderUserGifts(int $user): void { $this->assertUserLoggedIn(); - + $user = $this->users->get($user); - if(!$user) + if (!$user || $user->isDeleted()) { $this->notFound(); - + } + + if (!$user->canBeViewedBy($this->user->identity ?? null)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + $this->template->user = $user; $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); $this->template->count = $user->getGiftCount(); $this->template->iterator = $user->getGifts($page); $this->template->hideInfo = $this->user->id !== $user->getId(); } - - function renderGiftMenu(): void + + public function renderGiftMenu(): void { $user = $this->users->get((int) ($this->queryParam("user") ?? 0)); - if(!$user) + if (!$user) { $this->notFound(); - + } + $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); - $cats = $this->gifts->getCategories($page, NULL, $this->template->count); - + $cats = $this->gifts->getCategories($page, null, $this->template->count); + $this->template->user = $user; $this->template->iterator = $cats; $this->template->count = $this->gifts->getCategoriesCount(); - $this->template->_template = "Gifts/Menu.xml"; + $this->template->_template = "Gifts/Menu.latte"; } - - function renderGiftList(): void + + public function renderGiftList(): void { $user = $this->users->get((int) ($this->queryParam("user") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); - if(!$user || !$cat) + if (!$user || !$cat) { $this->flashFail("err", tr("error_when_gifting"), tr("error_user_not_exists")); - + } + + if (!$user->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); $gifts = $cat->getGifts($page, null, $this->template->count); - + $this->template->user = $user; $this->template->cat = $cat; $this->template->gifts = iterator_to_array($gifts); - $this->template->_template = "Gifts/Pick.xml"; + $this->template->_template = "Gifts/Pick.latte"; } - - function renderConfirmGift(): void + + public function renderConfirmGift(): void { $user = $this->users->get((int) ($this->queryParam("user") ?? 0)); $gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); - if(!$user || !$cat || !$gift || !$cat->hasGift($gift)) + if (!$user || !$cat || !$gift || !$cat->hasGift($gift)) { $this->flashFail("err", tr("error_when_gifting"), tr("error_no_rights_gifts")); - - if(!$gift->canUse($this->user->identity)) + } + + if (!$gift->canUse($this->user->identity)) { $this->flashFail("err", tr("error_when_gifting"), tr("error_no_more_gifts")); - + } + + if (!$user->canBeViewedBy($this->user->identity ?? null)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + $coinsLeft = $this->user->identity->getCoins() - $gift->getPrice(); - if($coinsLeft < 0) + if ($coinsLeft < 0) { $this->flashFail("err", tr("error_when_gifting"), tr("error_no_money")); - - $this->template->_template = "Gifts/Confirm.xml"; - if($_SERVER["REQUEST_METHOD"] !== "POST") { + } + + $this->template->_template = "Gifts/Confirm.latte"; + if ($_SERVER["REQUEST_METHOD"] !== "POST") { $this->template->user = $user; $this->template->cat = $cat; $this->template->gift = $gift; return; } - - $comment = empty($c = $this->postParam("comment")) ? NULL : $c; + + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send")) { + $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); + } + + $comment = empty($c = $this->postParam("comment")) ? null : $c; $notification = new GiftNotification($user, $this->user->identity, $gift, $comment); $notification->emit(); $this->user->identity->setCoins($coinsLeft); $this->user->identity->save(); $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous"))); $gift->used(); - + $this->flash("succ", tr("gift_sent"), tr("gift_sent_desc", $user->getFirstName(), $gift->getPrice())); $this->redirect($user->getURL()); } - - function renderStub(): void + + public function renderStub(): void { $this->assertUserLoggedIn(); - + $act = $this->queryParam("act"); - switch($act) { + switch ($act) { case "pick": $this->renderGiftMenu(); break; - + case "menu": $this->renderGiftList(); break; - + case "confirm": $this->renderConfirmGift(); break; - + default: $this->notFound(); } } - - function renderGiftImage(int $id, int $timestamp): void + + public function renderGiftImage(int $id, int $timestamp): void { $gift = $this->gifts->get($id); - if(!$gift) + if (!$gift) { $this->notFound(); - + } + $image = $gift->getImage(); header("Cache-Control: no-transform, immutable"); header("Content-Length: " . strlen($image)); header("Content-Type: image/png"); exit($image); } - - function onStartup(): void + + public function onStartup(): void { - if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { $this->flashFail("err", tr("error"), tr("feature_disabled")); - + } + parent::onStartup(); } } diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index d3a46fd57..0a00ac8e7 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -1,239 +1,297 @@ -clubs = $clubs; - + parent::__construct(); } - - function renderView(int $id): void + + public function renderView(int $id): void { $club = $this->clubs->get($id); - if(!$club) { + if (!$club) { $this->notFound(); } else { if ($club->isBanned()) { - $this->template->_template = "Group/Banned.xml"; + $this->template->_template = "Group/Banned.latte"; } else { - $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); + $docs = (new Documents())->getDocumentsByOwner($club->getRealId()); + $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->audios = (new Audios())->getRandomThreeAudiosByEntityId($club->getRealId()); + $this->template->audiosCount = (new Audios())->getClubCollectionSize($club); + $this->template->docsCount = $docs->size(); + $this->template->docs = $docs->offsetLimit(0, 2); + } + + if (!is_null($this->user->identity) && $club->getWallType() == 2) { + if (!$club->canBeModifiedBy($this->user->identity)) { + $this->template->suggestedPostsCountByUser = (new Posts())->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + } else { + $this->template->suggestedPostsCountByEveryone = (new Posts())->getSuggestedPostsCount($club->getId()); + } } $this->template->club = $club; + $this->template->ignore_status = $club->isIgnoredBy($this->user->identity); } } - - function renderCreate(): void + + public function renderCreate(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) - { - $club = new Club; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) { + $club = new Club(); $club->setName($this->postParam("name")); - $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); + $club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about")); $club->setOwner($this->user->id); - + + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) { + $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); + } + try { $club->save(); - } catch(\PDOException $ex) { - if($ex->getCode() == 23000) + } catch (\PDOException $ex) { + if ($ex->getCode() == 23000) { $this->flashFail("err", tr("error"), tr("error_on_server_side")); - else + } else { throw $ex; + } } - + $club->toggleSubscription($this->user->identity); + $this->redirect("/club" . $club->getId()); - }else{ + } else { $this->flashFail("err", tr("error"), tr("error_no_group_name")); } } } - - function renderSub(): void + + public function renderSub(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if($_SERVER["REQUEST_METHOD"] !== "POST") exit("Invalid state"); - + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + exit("Invalid state"); + } + $club = $this->clubs->get((int) $this->postParam("id")); - if(!$club) exit("Invalid state"); - if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); - + if (!$club) { + exit("Invalid state"); + } + if ($club->isBanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + if (!$club->getSubscriptionStatus($this->user->identity)) { + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.sub")) { + $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); + } + } + $club->toggleSubscription($this->user->identity); - + $this->redirect($club->getURL()); } - - function renderFollowers(int $id): void + + public function renderFollowers(int $id): void { $this->assertUserLoggedIn(); $this->template->club = $this->clubs->get($id); - if ($this->template->club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + if ($this->template->club->isBanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } $this->template->onlyShowManagers = $this->queryParam("onlyAdmins") == "1"; - if($this->template->onlyShowManagers) { - $this->template->followers = NULL; + if ($this->template->onlyShowManagers) { + $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()) { + if ($this->template->club->canBeModifiedBy($this->user->identity) || !$this->template->club->isOwnerHidden()) { $this->template->managers = array_merge([$this->template->club->getOwner()], iterator_to_array($this->template->managers)); } $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(); } $this->template->paginatorConf = (object) [ "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, - "amount" => NULL, + "page" => (int) ($this->queryParam("p") ?? 1), + "amount" => 10, "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; } - - function renderModifyAdmin(int $id): void + + public function renderModifyAdmin(int $id): void { $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) + if (!$user) { $this->badRequest(); - + } + $club = $this->clubs->get($id); - if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + if ($club->isBanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } - $user = (new Users)->get((int) $user); - if(!$user || !$club) + $user = (new Users())->get((int) $user); + if (!$user || !$club) { $this->notFound(); - - if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) + } + + if (!$club->canBeModifiedBy($this->user->identity ?? null)) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } - if(!is_null($hidden)) { - if($club->getOwner()->getId() == $user->getId()) { + if (!is_null($hidden)) { + if ($club->getOwner()->getId() == $user->getId()) { $club->setOwner_Hidden($hidden); $club->save(); } else { - $manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId()); + $manager = (new Managers())->getByUserAndClub($user->getId(), $club->getId()); $manager->setHidden($hidden); $manager->save(); } - if($club->getManagersCount(true) == 0) { + if ($club->getManagersCount(true) == 0) { $club->setAdministrators_List_Display(2); $club->save(); } - if($hidden) { + if ($hidden) { $this->flashFail("succ", tr("success_action"), tr("x_is_now_hidden", $user->getCanonicalName())); } else { $this->flashFail("succ", tr("success_action"), tr("x_is_now_showed", $user->getCanonicalName())); } - } elseif($removeComment) { - if($club->getOwner()->getId() == $user->getId()) { + } elseif ($removeComment) { + if ($club->getOwner()->getId() == $user->getId()) { $club->setOwner_Comment(null); $club->save(); } else { - $manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId()); + $manager = (new Managers())->getByUserAndClub($user->getId(), $club->getId()); $manager->setComment(null); $manager->save(); } $this->flashFail("succ", tr("success_action"), tr("comment_is_deleted")); - } elseif($comment) { - if(mb_strlen($comment) > 36) { + } elseif ($comment) { + if (mb_strlen($comment) > 36) { $commentLength = (string) mb_strlen($comment); $this->flashFail("err", tr("error"), tr("comment_is_too_long", $commentLength)); } - if($club->getOwner()->getId() == $user->getId()) { + if ($club->getOwner()->getId() == $user->getId()) { $club->setOwner_Comment($comment); $club->save(); } else { - $manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId()); + $manager = (new Managers())->getByUserAndClub($user->getId(), $club->getId()); $manager->setComment($comment); $manager->save(); } $this->flashFail("succ", tr("success_action"), tr("comment_is_changed")); - }else{ - if($club->canBeModifiedBy($user)) { + } else { + if ($club->canBeModifiedBy($user)) { $club->removeManager($user); $this->flashFail("succ", tr("success_action"), tr("x_no_more_admin", $user->getCanonicalName())); } else { $club->addManager($user); - + (new ClubModeratorNotification($user, $club, $this->user->identity))->emit(); $this->flashFail("succ", tr("success_action"), tr("x_is_admin", $user->getCanonicalName())); } } - + } - - function renderEdit(int $id): void + + public function renderEdit(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $club = $this->clubs->get($id); - if(!$club || !$club->canBeModifiedBy($this->user->identity)) + if (!$club || !$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); - else if ($club->isBanned()) + } elseif ($club->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - else + } else { $this->template->club = $club; - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!$club->setShortcode( empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode") )) + } + + 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")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name")); - $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); - $club->setWall(empty($this->postParam("wall")) ? 0 : 1); + $club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about")); + try { + $club->setWall(empty($this->postParam("wall")) ? 0 : (int) $this->postParam("wall")); + } catch (\Exception $e) { + $this->flashFail("err", tr("error"), tr("error_invalid_wall_value")); + } + $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); + $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); - $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); - + $club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1); + + if (!$club->isHidingFromGlobalFeedEnforced()) { + $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed") ? 0 : 1)); + } + $website = $this->postParam("website") ?? ""; - if(empty($website)) - $club->setWebsite(NULL); - else + if (empty($website)) { + $club->setWebsite(null); + } else { $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website); - - if($_FILES["ava"]["error"] === UPLOAD_ERR_OK) { - $photo = new Photo; + } + + if ($_FILES["ava"]["error"] === UPLOAD_ERR_OK) { + $photo = new Photo(); try { $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; - if($anon && $this->user->id === $club->getOwner()->getId()) - $anon = $club->isOwnerHidden(); - else if($anon) + if ($anon && $this->user->id === $club->getOwner()->getId()) { + $anon = $club->isOwnerHidden(); + } elseif ($anon) { $anon = $club->getManager($this->user->identity)->isHidden(); + } $photo->setOwner($this->user->id); $photo->setDescription("Profile image"); @@ -241,158 +299,228 @@ function renderEdit(int $id): void $photo->setCreated(time()); $photo->setAnonymous($anon); $photo->save(); - - (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); - } catch(ISE $ex) { - $name = $album->getName(); + + (new Albums())->getClubAvatarAlbum($club)->addPhoto($photo); + } catch (ISE $ex) { $this->flashFail("err", tr("error"), tr("error_when_uploading_photo")); } } - + try { $club->save(); - } catch(\PDOException $ex) { - if($ex->getCode() == 23000) + } catch (\PDOException $ex) { + if ($ex->getCode() == 23000) { $this->flashFail("err", tr("error"), tr("error_on_server_side")); - else + } else { throw $ex; + } } - + $this->flash("succ", tr("changes_saved"), tr("new_changes_desc")); } } - - function renderSetAvatar(int $id) + + public function renderSetAvatar(int $id) { - $photo = new Photo; + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $club = $this->clubs->get($id); - if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); - if($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["ava"]["error"] === UPLOAD_ERR_OK) { + + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden"), null, true); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["blob"]["error"] === UPLOAD_ERR_OK) { try { + $photo = new Photo(); + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; - if($anon && $this->user->id === $club->getOwner()->getId()) - $anon = $club->isOwnerHidden(); - else if($anon) + if ($anon && $this->user->id === $club->getOwner()->getId()) { + $anon = $club->isOwnerHidden(); + } elseif ($anon) { $anon = $club->getManager($this->user->identity)->isHidden(); + } + $photo->setOwner($this->user->id); $photo->setDescription("Club image"); - $photo->setFile($_FILES["ava"]); + $photo->setFile($_FILES["blob"]); $photo->setCreated(time()); $photo->setAnonymous($anon); $photo->save(); - - (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); - - $flags = 0; - $flags |= 0b00010000; - $flags |= 0b10000000; - - $post = new Post; - $post->setOwner($this->user->id); - $post->setWall($club->getId()*-1); - $post->setCreated(time()); - $post->setContent(""); - $post->setFlags($flags); - $post->save(); - $post->attach($photo); - - } catch(ISE $ex) { - $name = $album->getName(); - $this->flashFail("err", tr("error"), tr("error_when_uploading_photo")); + + (new Albums())->getClubAvatarAlbum($club)->addPhoto($photo); + + if ($this->postParam("on_wall") == 1) { + $post = new Post(); + + $post->setOwner($this->user->id); + $post->setWall($club->getId() * -1); + $post->setCreated(time()); + $post->setContent(""); + + $flags = 0; + $flags |= 0b00010000; + $flags |= 0b10000000; + + $post->setFlags($flags); + $post->save(); + + $post->attach($photo); + } + + } catch (\Throwable $ex) { + $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"), null, true); } + + $this->returnJson([ + "success" => true, + "new_photo" => $photo->getPrettyId(), + "url" => $photo->getURL(), + ]); + } else { + return " "; + } + } + + public function renderDeleteAvatar(int $id) + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + $club = $this->clubs->get($id); + + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden"), null, true); + } + + $avatar = $club->getAvatarPhoto(); + + if (!$avatar) { + $this->flashFail("succ", tr("error"), "no avatar bro", null, true); + } + + $avatar->isolate(); + + $newAvatar = $club->getAvatarPhoto(); + + if (!$newAvatar) { + $this->returnJson([ + "success" => true, + "has_new_photo" => false, + "new_photo" => null, + "url" => "/assets/packages/static/openvk/img/camera_200.png", + ]); + } else { + $this->returnJson([ + "success" => true, + "has_new_photo" => true, + "new_photo" => $newAvatar->getPrettyId(), + "url" => $newAvatar->getURL(), + ]); } - $this->returnJson([ - "url" => $photo->getURL(), - "id" => $photo->getPrettyId() - ]); } - function renderEditBackdrop(int $id): void + + public function renderEditBackdrop(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $club = $this->clubs->get($id); - if(!$club || !$club->canBeModifiedBy($this->user->identity)) + if (!$club || !$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); - else + } else { $this->template->club = $club; - - if($_SERVER["REQUEST_METHOD"] !== "POST") + } + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { return; - - if($this->postParam("subact") === "remove") { + } + + if ($this->postParam("subact") === "remove") { $club->unsetBackDropPictures(); $club->save(); $this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit } - - $pic1 = $pic2 = NULL; + + $pic1 = $pic2 = null; try { - if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) + if ($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) { $pic1 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop1"]); - - if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) + } + + if ($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) { $pic2 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop2"]); - } catch(InvalidStateException $e) { + } + } catch (InvalidStateException $e) { $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); } - - if($pic1 == $pic2 && is_null($pic1)) + + if ($pic1 == $pic2 && is_null($pic1)) { $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); - + } + $club->setBackDropPictures($pic1, $pic2); $club->save(); $this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc")); } - - function renderStatistics(int $id): void + + public function renderStatistics(int $id): void { $this->assertUserLoggedIn(); - - if(!eventdb()) + + if (!eventdb()) { $this->flashFail("err", tr("connection_error"), tr("connection_error_desc")); - + } + $club = $this->clubs->get($id); - if(!$club->canBeModifiedBy($this->user->identity)) + if (!$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); - else if ($club->isBanned()) + } elseif ($club->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - else + } else { $this->template->club = $club; - + } + $this->template->reach = $club->getPostViewStats(true); $this->template->views = $club->getPostViewStats(false); } - function renderAdmin(int $clb, int $id): void + public function renderAdmin(int $clb, int $id): void { $this->assertUserLoggedIn(); - $manager = (new Managers)->get($id); - if($manager->getClub()->canBeModifiedBy($this->user->identity)){ + $manager = (new Managers())->get($id); + if ($manager->getClub()->canBeModifiedBy($this->user->identity)) { $this->template->manager = $manager; $this->template->club = $manager->getClub(); - }else{ + } else { $this->notFound(); } } - function renderChangeOwner(int $id, int $newOwnerId): void + public function renderChangeOwner(int $id, int $newOwnerId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if($_SERVER['REQUEST_METHOD'] !== "POST") + if ($_SERVER['REQUEST_METHOD'] !== "POST") { $this->redirect("/groups" . $this->user->id); + } - if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) + if (!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) { $this->flashFail("err", tr("error"), tr("incorrect_password")); + } $club = $this->clubs->get($id); - if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); - $newOwner = (new Users)->get($newOwnerId); - if($this->user->id !== $club->getOwner()->getId()) + if ($club->isBanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + $newOwner = (new Users())->get($newOwnerId); + if ($this->user->id !== $club->getOwner()->getId()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } $club->setOwner($newOwnerId); @@ -411,4 +539,38 @@ function renderChangeOwner(int $id, int $newOwnerId): void $this->flashFail("succ", tr("information_-1"), tr("group_owner_setted", $newOwner->getCanonicalName(), $club->getName())); } + + public function renderSuggested(int $id): void + { + $this->assertUserLoggedIn(); + + $club = $this->clubs->get($id); + if (!$club) { + $this->notFound(); + } else { + $this->template->club = $club; + } + + if ($club->getWallType() == 0) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_closed")); + $this->redirect("/club" . $club->getId()); + } + + if ($club->getWallType() == 1) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_open")); + $this->redirect("/club" . $club->getId()); + } + + if (!$club->canBeModifiedBy($this->user->identity)) { + $this->template->posts = iterator_to_array((new Posts())->getSuggestedPostsByUser($club->getId(), $this->user->id, (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts())->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + $this->template->type = "my"; + } else { + $this->template->posts = iterator_to_array((new Posts())->getSuggestedPosts($club->getId(), (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts())->getSuggestedPostsCount($club->getId()); + $this->template->type = "everyone"; + } + + $this->template->page = (int) ($this->queryParam("p") ?? 1); + } } diff --git a/Web/Presenters/HelloPresenter.php b/Web/Presenters/HelloPresenter.php index 32f0c74ef..f58ef8f67 100644 --- a/Web/Presenters/HelloPresenter.php +++ b/Web/Presenters/HelloPresenter.php @@ -1,10 +1,14 @@ -template->name = $name; } diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php index e2e6b50e9..4e01c2243 100644 --- a/Web/Presenters/InternalAPIPresenter.php +++ b/Web/Presenters/InternalAPIPresenter.php @@ -1,5 +1,9 @@ - 1, "error" => [ @@ -18,19 +24,20 @@ private function fail(int $code, string $message): void "id" => hexdec(hash("crc32b", (string) time())), ])); } - + private function succ($payload): void { + header("Content-Type: application/x-msgpack"); exit(MessagePack::pack([ "brpc" => 1, "result" => $payload, "id" => hexdec(hash("crc32b", (string) time())), ])); } - - function renderRoute(): void + + public function renderRoute(): void { - if($_SERVER["REQUEST_METHOD"] !== "POST") { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { header("HTTP/1.1 405 Method Not Allowed"); exit("ты дебил это точка апи"); } @@ -39,98 +46,156 @@ function renderRoute(): void } catch (\Exception $ex) { $this->fail(-32700, "Parse error"); } - - if(is_null($input->brpc ?? NULL) || is_null($input->method ?? NULL)) + + if (is_null($input->brpc ?? null) || is_null($input->method ?? null)) { $this->fail(-32600, "Invalid BIN-RPC"); - else if($input->brpc !== 1) + } elseif ($input->brpc !== 1) { $this->fail(-32610, "Invalid version"); - + } + $method = explode(".", $input->method); - if(sizeof($method) !== 2) + if (sizeof($method) !== 2) { $this->fail(-32601, "Procedure not found"); - + } + [$class, $method] = $method; $class = '\openvk\ServiceAPI\\' . $class; - if(!class_exists($class)) + if (!class_exists($class)) { $this->fail(-32601, "Procedure not found"); - - $handler = new $class(is_null($this->user) ? NULL : $this->user->identity); - if(!is_callable([$handler, $method])) + } + + $handler = new $class(is_null($this->user->identity) ? null : $this->user->identity); + if (!is_callable([$handler, $method])) { $this->fail(-32601, "Procedure not found"); - + } + try { - $params = array_merge($input->params ?? [], [function($data) { + $params = array_merge($input->params ?? [], [function ($data) { $this->succ($data); - }, function(int $errno, string $errstr) { + }, function (int $errno, string $errstr) { $this->fail($errno, $errstr); }]); $handler->{$method}(...$params); - } catch(\TypeError $te) { + } catch (\TypeError $te) { $this->fail(-32602, "Invalid params"); - } catch(\Exception $ex) { + } catch (\Exception $ex) { $this->fail(-32603, "Uncaught " . get_class($ex)); } } - function renderTimezone() { - if($_SERVER["REQUEST_METHOD"] !== "POST") { + public function renderTimezone() + { + 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))) { + 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 ); + 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 { $this->returnJson([ - "success" => 0 + "success" => 0, ]); } } - function renderGetPhotosFromPost(int $owner_id, int $post_id) { - if($_SERVER["REQUEST_METHOD"] !== "POST") { + public function renderGetPhotosFromPost(int $owner_id, int $post_id) + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { header("HTTP/1.1 405 Method Not Allowed"); exit("иди нахуй заебал"); } - if($this->postParam("parentType", false) == "post") { - $post = (new Posts)->getPostById($owner_id, $post_id); + if ($this->postParam("parentType", false) == "post") { + $post = (new Posts())->getPostById($owner_id, $post_id, true); } else { - $post = (new Comments)->get($post_id); + $post = (new Comments())->get($post_id); } - - if(is_null($post)) { + + if (is_null($post)) { $this->returnJson([ - "success" => 0 + "success" => 0, ]); } else { $response = []; $attachments = $post->getChildren(); - foreach($attachments as $attachment) - { - if($attachment instanceof \openvk\Web\Models\Entities\Photo) - { - $response[] = [ - "url" => $attachment->getURLBySizeId('normal'), - "id" => $attachment->getPrettyId() + foreach ($attachments as $attachment) { + if ($attachment instanceof \openvk\Web\Models\Entities\Photo) { + $response[$attachment->getPrettyId()] = [ + "url" => $attachment->getURLBySizeId('larger'), + "id" => $attachment->getPrettyId(), ]; } } $this->returnJson([ "success" => 1, - "body" => $response + "body" => $response, ]); } } + + public function renderGetPostTemplate(int $owner_id, int $post_id) + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $type = $this->queryParam("type", false); + if ($type == "post") { + $post = (new Posts())->getPostById($owner_id, $post_id, true); + } else { + $post = (new Comments())->get($post_id); + } + + if (!$post || !$post->canBeEditedBy($this->user->identity)) { + exit(''); + } + + header("Content-Type: text/plain"); + + if ($type == 'post') { + $this->template->_template = 'components/post.latte'; + $this->template->post = $post; + $this->template->commentSection = $this->queryParam("from_page") == "another"; + } elseif ($type == 'comment') { + $this->template->_template = 'components/comment.latte'; + $this->template->comment = $post; + } else { + exit(''); + } + } + + public function renderImageFilter() + { + $is_enabled = OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["disableHotlinking"] ?? true; + $allowed_hosts = OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["allowedHosts"] ?? []; + + $url = $this->requestParam("url"); + $url = base64_decode($url); + + if (!$is_enabled) { + $this->redirect($url); + } + + $url_parsed = parse_url($url); + $host = $url_parsed['host']; + + if (in_array($host, $allowed_hosts)) { + $this->redirect($url); + } else { + $this->redirect('/assets/packages/static/openvk/img/fn_placeholder.jpg'); + } + } } diff --git a/Web/Presenters/MaintenancePresenter.php b/Web/Presenters/MaintenancePresenter.php index d4a5a6efe..7f8144e6c 100644 --- a/Web/Presenters/MaintenancePresenter.php +++ b/Web/Presenters/MaintenancePresenter.php @@ -1,4 +1,5 @@ flashFail("err", tr("error"), tr("forbidden")); + } $this->template->name = [ "photos" => tr("my_photos"), @@ -24,12 +26,9 @@ function renderSection(string $name): void "notes" => tr("my_notes"), "notification" => tr("my_feedback"), "support" => tr("menu_support"), - "topics" => tr("topics") + "topics" => tr("topics"), ][$name] ?? $name; } - function renderAll(): void - { - - } + public function renderAll(): void {} } diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index e04e1adc6..5a1b819d1 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -1,5 +1,9 @@ -messages = $messages; $this->signaler = SignalManager::i(); parent::__construct(); } - + private function getCorrespondent(int $id): object { - if($id > 0) - return (new Users)->get($id); - else if($id < 0) - return (new Clubs)->get(abs($id)); - else if($id === 0) + if ($id > 0) { + return (new Users())->get($id); + } elseif ($id < 0) { + return (new Clubs())->get(abs($id)); + } elseif ($id === 0) { return $this->user->identity; + } } - - function renderIndex(): void + + public function renderIndex(): void { $this->assertUserLoggedIn(); - if(isset($_GET["sel"])) + if (isset($_GET["sel"])) { $this->pass("openvk!Messenger->app", $_GET["sel"]); - + } + $page = (int) ($_GET["p"] ?? 1); $correspondences = iterator_to_array($this->messages->getCorrespondencies($this->user->identity, $page)); @@ -47,68 +53,75 @@ function renderIndex(): void "page" => (int) ($_GET["p"] ?? 1), "amount" => sizeof($this->template->corresps), "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; } - - function renderApp(int $sel): void + + public function renderApp(int $sel): void { $this->assertUserLoggedIn(); - + $correspondent = $this->getCorrespondent($sel); - if(!$correspondent) + if (!$correspondent) { $this->notFound(); + } - if(!$this->user->identity->getPrivacyPermission('messages.write', $correspondent)) - { + if (!$this->user->identity->getPrivacyPermission('messages.write', $correspondent)) { $this->flash("err", tr("warning"), tr("user_may_not_reply")); } - + + $this->template->disable_ajax = 1; $this->template->selId = $sel; $this->template->correspondent = $correspondent; } - - function renderEvents(int $randNum): void + + public function renderEvents(int $randNum): void { $this->assertUserLoggedIn(); - + header("Content-Type: application/json"); - $this->signaler->listen(function($event, $id) { + $this->signaler->listen(function ($event, $id) { exit(json_encode([[ "UUID" => $id, "event" => $event->getLongPoolSummary(), ]])); }, $this->user->id); } - - function renderVKEvents(int $id): void + + public function renderVKEvents(int $id): void { header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json"); - - if($this->queryParam("act") !== "a_check") - exit(header("HTTP/1.1 400 Bad Request")); - else if(!$this->queryParam("key")) - exit(header("HTTP/1.1 403 Forbidden")); - + + if ($this->queryParam("act") !== "a_check") { + header("HTTP/1.1 400 Bad Request"); + exit(); + } elseif (!$this->queryParam("key")) { + header("HTTP/1.1 403 Forbidden"); + exit(); + } + $key = $this->queryParam("key"); $payload = hex2bin(substr($key, 0, 16)); $signature = hex2bin(substr($key, 16)); - if(($signature ^ ( ~CHANDLER_ROOT_CONF["security"]["secret"] | ((string) $id))) !== $payload) { + if (($signature ^ (~CHANDLER_ROOT_CONF["security"]["secret"] | ((string) $id))) !== $payload) { exit(json_encode([ "failed" => 3, ])); } - + $legacy = $this->queryParam("version") < 3; $time = intval($this->queryParam("wait")); - - if($time > 60) + + if ($time > 60) { $time = 60; - elseif($time == 0) - $time = 25; // default - - $this->signaler->listen(function($event, $eId) use ($id) { + } elseif ($time == 0) { + $time = 25; + } // default + + $this->signaler->listen(function ($event, $eId) use ($id) { exit(json_encode([ "ts" => time(), "updates" => [ @@ -117,45 +130,68 @@ function renderVKEvents(int $id): void ])); }, $id, $time); } - - function renderApiGetMessages(int $sel, int $lastMsg): void + + public function renderApiGetMessages(int $sel, int $lastMsg): void { $this->assertUserLoggedIn(); - + $correspondent = $this->getCorrespondent($sel); - if(!$correspondent) + if (!$correspondent) { $this->notFound(); - + } + $messages = []; $correspondence = new Correspondence($this->user->identity, $correspondent); - foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message) + foreach ($correspondence->getMessages(1, $lastMsg === 0 ? null : $lastMsg, null, 0) as $message) { $messages[] = $message->simplify(); - + } + header("Content-Type: application/json"); exit(json_encode($messages)); } - - function renderApiWriteMessage(int $sel): void + + public function renderApiWriteMessage(int $sel): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if(empty($this->postParam("content"))) { + + if (empty($this->postParam("content"))) { header("HTTP/1.1 400 Bad Request"); exit("Argument error: param 'content' expected to be string, undefined given."); } - + $sel = $this->getCorrespondent($sel); - if($sel->getId() !== $this->user->id && !$sel->getPrivacyPermission('messages.write', $this->user->identity)) - exit(header("HTTP/1.1 403 Forbidden")); - + if ($sel->getId() !== $this->user->id && !$sel->getPrivacyPermission('messages.write', $this->user->identity)) { + header("HTTP/1.1 403 Forbidden"); + exit(); + } + $cor = new Correspondence($this->user->identity, $sel); - $msg = new Message; + $msg = new Message(); $msg->setContent($this->postParam("content")); $cor->sendMessage($msg); - + header("HTTP/1.1 202 Accepted"); header("Content-Type: application/json"); exit(json_encode($msg->simplify())); } + + public function renderApiSendTypingStatus(int $sel): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $sel = $this->getCorrespondent($sel); + if ($sel->getId() !== $this->user->id && !$sel->getPrivacyPermission('messages.write', $this->user->identity)) { + header("HTTP/1.1 403 Forbidden"); + exit(); + } + + $cor = new Correspondence($this->user->identity, $sel); + $result = $cor->sendTypingEvent(); + + header("HTTP/1.1 202 Accepted"); + header("Content-Type: application/json"); + exit(json_encode($result)); + } } diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php index 8164d05e0..edc32852f 100644 --- a/Web/Presenters/NoSpamPresenter.php +++ b/Web/Presenters/NoSpamPresenter.php @@ -1,4 +1,6 @@ -assertUserLoggedIn(); - $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertPermission('openvk\Web\Models\Entities\Ban', 'write', 0); $targetDir = __DIR__ . '/../Models/Entities/'; $mode = in_array($this->queryParam("act"), ["form", "templates", "rollback", "reports"]) ? $this->queryParam("act") : "form"; if ($mode === "form") { $this->template->_template = "NoSpam/Index"; + $this->template->disable_ajax = 1; + + $excludedClasses = ["Alias", "APIToken", "Ban", "BannedLink", "EmailChangeVerification", "EmailVerification", "Gift", "GiftCategory", "IP", "Manager", "NoSpamLog", "PasswordReset", "Report", "SupportAgent", "SupportAlias", "Voucher"]; + $foundClasses = []; foreach (Finder::findFiles('*.php')->from($targetDir) as $file) { $content = file_get_contents($file->getPathname()); @@ -51,7 +57,7 @@ function renderIndex(): void $className = trim($classMatches[1]); $fullClassName = $classNamespace . '\\' . $className; - if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName)) { + if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName) && !in_array($className, $excludedClasses)) { $foundClasses[] = $className; } } @@ -61,23 +67,36 @@ function renderIndex(): void foreach ($foundClasses as $class) { $r = new \ReflectionClass(NoSpamPresenter::ENTITIES_NAMESPACE . "\\$class"); - if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") + if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { $models[] = $class; + } } - $this->template->models = $models; - } else if ($mode === "templates") { - $this->template->_template = "NoSpam/Templates.xml"; + + $sortedModels = []; + foreach ($models as $model) { + $sortedModels[] = [$model, tr("nospam_" . strtolower($model))]; + } + + usort($sortedModels, function ($a, $b) { + return strcmp($a[1], $b[1]); + }); + + $this->template->models = $sortedModels; + } elseif ($mode === "templates") { + $this->template->_template = "NoSpam/Templates.latte"; + $this->template->disable_ajax = 1; $filter = []; if ($this->queryParam("id")) { - $filter["id"] = (int)$this->queryParam("id"); + $filter["id"] = (int) $this->queryParam("id"); } - $this->template->templates = iterator_to_array((new NoSpamLogs)->getList($filter)); - } else if ($mode === "reports") { + $this->template->templates = iterator_to_array((new NoSpamLogs())->getList($filter)); + } elseif ($mode === "reports") { $this->redirect("/scumfeed"); } else { - $template = (new NoSpamLogs)->get((int)$this->postParam("id")); - if (!$template || $template->isRollbacked()) + $template = (new NoSpamLogs())->get((int) $this->postParam("id")); + if (!$template || $template->isRollbacked()) { $this->returnJson(["success" => false, "error" => "Шаблон не найден"]); + } $model = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $template->getModel(); $items = $template->getItems(); @@ -87,20 +106,22 @@ function renderIndex(): void $unbanned_ids = []; foreach ($items as $_item) { try { - $item = new $model; + $item = new $model(); $table_name = $item->getTableName(); - $item = $db->table($table_name)->get((int)$_item); - if (!$item) continue; + $item = $db->table($table_name)->get((int) $_item); + if (!$item) { + continue; + } $item = new $model($item); - if (key_exists("deleted", $item->unwrap()) && $item->isDeleted()) { + if (property_exists($item->unwrap(), "deleted") && $item->isDeleted()) { $item->setDeleted(0); $item->save(); } if (in_array($template->getTypeRaw(), [2, 3])) { - $owner = NULL; + $owner = null; $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; if (method_exists($item, "ban")) { @@ -136,74 +157,13 @@ function renderIndex(): void } } - function renderSearch(): void + public function renderSearch(): void { $this->assertUserLoggedIn(); - $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertPermission('openvk\Web\Models\Entities\Ban', 'write', 0); $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; @@ -212,16 +172,17 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, $ip = addslashes($this->postParam("ip")); $useragent = addslashes($this->postParam("useragent")); $searchTerm = addslashes($this->postParam("q")); - $ts = (int)$this->postParam("ts"); - $te = (int)$this->postParam("te"); + $ts = (int) $this->postParam("ts"); + $te = (int) $this->postParam("te"); $user = addslashes($this->postParam("user")); if ($where) { $where = explode(";", $where)[0]; } - if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) + if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) { $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); + } $models = explode(",", $this->postParam("models")); @@ -231,7 +192,7 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, continue; } - $model = new $model_name; + $model = new $model_name(); $c = new \ReflectionClass($model_name); if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { @@ -255,7 +216,9 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, $conditions = implode(" OR ", $conditions); $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); - if ($need_deleted) $where .= " AND (`deleted` = 0)"; + if ($need_deleted) { + $where .= " AND (`deleted` = 0)"; + } } $rows = []; @@ -269,7 +232,7 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, } 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 = []; @@ -279,9 +242,9 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, } } - if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { + if (!in_array((int) $this->postParam("ban"), [1, 2, 3])) { foreach ($rows as $key => $object) { - $object = (array)$object; + $object = (array) $object; $_obj = []; foreach ($object as $key => $value) { foreach ($columns as $column) { @@ -301,11 +264,13 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, foreach ($rows as $object) { $object = new $model_name($db->table($table)->get($object->id)); - if (!$object) continue; + if (!$object) { + continue; + } $ids[] = $object->getId(); } - $log = new NoSpamLog; + $log = new NoSpamLog(); $log->setUser($this->user->id); $log->setModel($_model); if ($searchTerm) { @@ -313,7 +278,7 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, } else { $log->setRequest($where); } - $log->setBan_Type((int)$this->postParam("ban")); + $log->setBan_Type((int) $this->postParam("ban")); $log->setCount(count($rows)); $log->setTime(time()); $log->setItems(implode(",", $ids)); @@ -322,9 +287,11 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, $banned_ids = []; foreach ($rows as $object) { $object = new $model_name($db->table($table)->get($object->id)); - if (!$object) continue; + if (!$object) { + continue; + } - $owner = NULL; + $owner = null; $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; if (method_exists($object, "ban")) { @@ -346,17 +313,18 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, } } - if (in_array((int)$this->postParam("ban"), [2, 3])) { + if (in_array((int) $this->postParam("ban"), [2, 3])) { $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? addslashes($this->postParam("ban_reason")) : ("**content-noSpamTemplate-" . $log->getId() . "**"); - $is_forever = (string)$this->postParam("is_forever") === "true"; - $unban_time = $is_forever ? 0 : (int)$this->postParam("unban_time") ?? NULL; + $is_forever = (string) $this->postParam("is_forever") === "true"; + $unban_time = $is_forever ? 0 : (int) $this->postParam("unban_time") ?? null; if ($owner) { $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); if (!in_array($_id, $banned_ids)) { if ($owner instanceof User) { - if (!$unban_time && !$is_forever) + if (!$unban_time && !$is_forever) { $unban_time = time() + $owner->getNewBanTime(); + } $owner->ban($reason, false, $unban_time, $this->user->id); } else { @@ -368,8 +336,9 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, } } - if (in_array((int)$this->postParam("ban"), [1, 3])) + if (in_array((int) $this->postParam("ban"), [1, 3])) { $object->delete(); + } } $processed++; @@ -381,4 +350,75 @@ function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, $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; + } + } } diff --git a/Web/Presenters/NotesPresenter.php b/Web/Presenters/NotesPresenter.php index 4b71c8b1d..2e0341191 100644 --- a/Web/Presenters/NotesPresenter.php +++ b/Web/Presenters/NotesPresenter.php @@ -1,5 +1,9 @@ -notes = $notes; - + parent::__construct(); } - - function renderList(int $owner): void + + public function renderList(int $owner): void { - $user = (new Users)->get($owner); - if(!$user) $this->notFound(); - if(!$user->getPrivacyPermission('notes.read', $this->user->identity ?? NULL)) + $user = (new Users())->get($owner); + if (!$user) { + $this->notFound(); + } + if (!$user->getPrivacyPermission('notes.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - - $this->template->notes = $this->notes->getUserNotes($user, (int)($this->queryParam("p") ?? 1)); + } + + $this->template->page = (int) ($this->queryParam("p") ?? 1); + $this->template->notes = $this->notes->getUserNotes($user, $this->template->page); $this->template->count = $this->notes->getUserNotesCount($user); $this->template->owner = $user; - $this->template->paginatorConf = (object) [ - "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, - "amount" => NULL, - "perPage" => OPENVK_DEFAULT_PER_PAGE, - ]; } - - function renderView(int $owner, int $note_id): void + + public function renderView(int $owner, int $note_id): void { $note = $this->notes->getNoteById($owner, $note_id); - if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) + if (!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) { $this->notFound(); - if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->user->identity ?? NULL)) + } + if (!$note->getOwner()->getPrivacyPermission('notes.read', $this->user->identity ?? null)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + if (!$note->canBeViewedBy($this->user->identity)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - + } + $this->template->cCount = $note->getCommentsCount(); $this->template->cPage = (int) ($this->queryParam("p") ?? 1); $this->template->comments = iterator_to_array($note->getComments($this->template->cPage)); $this->template->note = $note; } - - function renderPreView(): void + + public function renderPreView(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if($_SERVER["REQUEST_METHOD"] !== "POST") { + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { header("HTTP/1.1 400 Bad Request"); exit; } - - if(empty($this->postParam("html")) || empty($this->postParam("title"))) { + + 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 = 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 + + public function renderCreate(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $id = $this->user->id; #TODO: when ACL'll be done, allow admins to edit users via ?GUID=(chandler guid) - - if(!$id) + + if (!$id) { $this->notFound(); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(empty($this->postParam("name"))) { - $this->flashFail("err", tr("error"), tr("error_segmentation")); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (empty($this->postParam("name"))) { + $this->flashFail("err", tr("error"), tr("error_segmentation")); } - $note = new Note; + $note = new Note(); $note->setOwner($this->user->id); $note->setCreated(time()); $note->setName($this->postParam("name")); $note->setSource($this->postParam("html")); $note->setEdited(time()); $note->save(); - + $this->redirect("/note" . $this->user->id . "_" . $note->getVirtualId()); } } - function renderEdit(int $owner, int $note_id): void + public function renderEdit(int $owner, int $note_id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $note = $this->notes->getNoteById($owner, $note_id); - if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) + if (!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) { $this->notFound(); - if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) + } + if (is_null($this->user->identity) || !$note->canBeModifiedBy($this->user->identity)) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } $this->template->note = $note; - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(empty($this->postParam("name"))) { - $this->flashFail("err", tr("error"), tr("error_segmentation")); + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (empty($this->postParam("name"))) { + $this->flashFail("err", tr("error"), tr("error_segmentation")); } $note->setName($this->postParam("name")); $note->setSource($this->postParam("html")); - $note->setCached_Content(NULL); + $note->setCached_Content(null); $note->setEdited(time()); $note->save(); - + $this->redirect("/note" . $this->user->id . "_" . $note->getVirtualId()); } } - - function renderDelete(int $owner, int $id): void + + public function renderDelete(int $owner, int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); $this->assertNoCSRF(); - + $note = $this->notes->get($id); - if(!$note) $this->notFound(); - if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound(); - if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) + if (!$note) { + $this->notFound(); + } + if ($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) { + $this->notFound(); + } + if (is_null($this->user->identity) || !$note->canBeModifiedBy($this->user->identity)) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - + } + $name = $note->getName(); $note->delete(); $this->flash("succ", tr("note_is_deleted"), tr("note_x_is_now_deleted", $name)); diff --git a/Web/Presenters/NotificationPresenter.php b/Web/Presenters/NotificationPresenter.php index 3bd7a321d..fda3e3a42 100644 --- a/Web/Presenters/NotificationPresenter.php +++ b/Web/Presenters/NotificationPresenter.php @@ -1,18 +1,21 @@ -assertUserLoggedIn(); $archive = $this->queryParam("act") === "archived"; $count = $this->user->identity->getNotificationsCount($archive); - if($count == 0 && $this->queryParam("act") == NULL) { + if ($count == 0 && $this->queryParam("act") == null) { $mode = "archived"; $archive = true; } else { @@ -23,7 +26,7 @@ function renderFeed(): void $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 = $count; - + $this->user->identity->updateNotificationOffset(); $this->user->identity->save(); } diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index a171c2e33..129321ccd 100644 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -1,13 +1,18 @@ -path; - + return "$path?" . http_build_query(array_merge($_GET, $data)); } - - protected function flash(string $type, string $title, ?string $message = NULL, ?int $code = NULL): void + + protected function flash(string $type, string $title, ?string $message = null, ?int $code = null): void { Session::i()->set("_error", json_encode([ "type" => $type, @@ -40,15 +45,16 @@ protected function flash(string $type, string $title, ?string $message = NULL, ? protected function setSessionTheme(string $theme, bool $once = false): void { - if($once) + if ($once) { Session::i()->set("_tempTheme", $theme); - else + } else { Session::i()->set("_sessionTheme", $theme); + } } - - protected function flashFail(string $type, string $title, ?string $message = NULL, ?int $code = NULL, bool $json = false): void + + protected function flashFail(string $type, string $title, ?string $message = null, ?int $code = null, bool $json = false): void { - if($json) { + if ($json) { $this->returnJson([ "success" => $type !== "err", "flash" => [ @@ -61,25 +67,24 @@ protected function flashFail(string $type, string $title, ?string $message = NUL } else { $this->flash($type, $title, $message, $code); $referer = $_SERVER["HTTP_REFERER"] ?? "/"; - + $this->redirect($referer); } } - + protected function logInUserWithToken(): void { $header = $_SERVER["HTTP_AUTHORIZATION"] ?? ""; - $token; - + preg_match("%Bearer (.*)$%", $header, $matches); $token = $matches[1] ?? ""; - $token = (new APITokens)->getByCode($token); - if(!$token) { + $token = (new APITokens())->getByCode($token); + if (!$token) { header("HTTP/1.1 401 Unauthorized"); header("Content-Type: application/json"); exit(json_encode(["error" => "The access token is invalid"])); } - + $this->user = (object) []; $this->user->identity = $token->getUser(); $this->user->raw = $this->user->identity->getChandlerUser(); @@ -87,95 +92,100 @@ protected function logInUserWithToken(): void $this->template->thisUser = $this->user->identity; $this->template->userTainted = false; } - + protected function assertUserLoggedIn(bool $returnUrl = true): void { - if(is_null($this->user)) { + if (is_null($this->user->identity)) { $loginUrl = "/login"; - if($returnUrl && $_SERVER["REQUEST_METHOD"] === "GET") { + if ($returnUrl && $_SERVER["REQUEST_METHOD"] === "GET") { $currentUrl = function_exists("get_current_url") ? get_current_url() : $_SERVER["REQUEST_URI"]; $loginUrl .= "?jReturnTo=" . rawurlencode($currentUrl); } - + $this->flash("err", tr("login_required_error"), tr("login_required_error_comment")); - + $this->redirect($loginUrl); } } - + protected function hasPermission(string $model, string $action, int $context): bool { - if(is_null($this->user)) { - if($model !== "user") { + if (is_null($this->user->identity)) { + if ($model !== "user") { $this->flash("info", tr("login_required_error"), tr("login_required_error_comment")); - + $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 { - if($this->hasPermission($model, $action, $context)) return; - - if($throw) - throw new SecurityPolicyViolationException("Permission error"); - else + if ($this->hasPermission($model, $action, $context)) { + return; + } + + if ($throw) { + throw new ISE("Permission error"); + } else { $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); + } } - + protected function assertCaptchaCheckPassed(): void { - if(!check_captcha()) + if (!check_captcha()) { $this->flashFail("err", tr("captcha_error"), tr("captcha_error_comment")); + } } - + protected function willExecuteWriteAction(bool $json = false): void { - $ip = (new IPs)->get(CONNECTING_IP); + $ip = (new IPs())->get(CONNECTING_IP); $res = $ip->rateLimit(); - - if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) { - if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) { + + if (!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) { + if ($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) { $this->user->identity->ban("Account has possibly been stolen", false); exit("Хакеры? Интересно..."); } - - $this->flashFail("err", tr("rate_limit_error"), tr("rate_limit_error_comment", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"], $res), NULL, $json); + + $this->flashFail("err", tr("rate_limit_error"), tr("rate_limit_error_comment", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"], $res), null, $json); } } - + protected function signal(object $event): bool { return (SignalManager::i())->triggerEvent($event, $this->user->id); } - + protected function logEvent(string $type, array $data): bool { $db = eventdb(); - if(!$db) + if (!$db) { return false; - + } + $data = array_merge([ "timestamp" => time(), "verified" => (int) true, ], $data); - $columns = implode(", ", array_map(function($col) { + $columns = implode(", ", array_map(function ($col) { return "`" . addslashes($col) . "`"; }, array_keys($data))); - $values = implode(", ", array_map(function($val) { + $values = implode(", ", array_map(function ($val) { return "'" . addslashes((string) (int) $val) . "'"; }, array_values($data))); - + $db->getConnection()->query("INSERT INTO " . $type . "s($columns) VALUES ($values);"); - + return true; } - + /** * @override */ @@ -183,59 +193,69 @@ protected function sendmail(string $to, string $template, array $params = []): v { parent::sendmail($to, __DIR__ . "/../../Email/$template", $params); } - - function getTemplatingEngine(): TemplatingEngine + + public function getTemplatingEngine(): TemplatingEngine { $latte = parent::getTemplatingEngine(); - $latte->addFilter("translate", function($s) { - return tr($s); - }); - + $latte->addExtension(new \Latte\Essential\TranslatorExtension(tr(...))); + return $latte; } - - function onStartup(): void + + public function onStartup(): void { $user = Authenticator::i()->getUser(); - if(!$this->template) - $this->template = new \stdClass; - + if (!$this->template) { + $this->template = new \stdClass(); + } + $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isTimezoned = Session::i()->get("_timezoneOffset"); $userValidated = 0; $cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0; - if(!is_null($user)) { + if (OPENVK_ROOT_CONF['openvk']['preferences']['news']['show']) { + $post = (new Posts())->getPostsFromUsersWall(-OPENVK_ROOT_CONF['openvk']['preferences']['news']['groupId'], 1, 1); + $post = iterator_to_array($post)[0]; + + $text = wordwrap($post->getText(false), 150, '\n', false); + $text = explode('\n', $text)[0]; + + $this->template->newsText = $text; + $this->template->newsLink = '/wall' . $post->getPrettyId(); + } + + if (!is_null($user)) { $this->user = (object) []; $this->user->raw = $user; - $this->user->identity = (new Users)->getByChandlerUser($user); + $this->user->identity = (new Users())->getByChandlerUser($user); $this->user->id = $this->user->identity->getId(); $this->template->thisUser = $this->user->identity; $this->template->userTainted = $user->isTainted(); CurrentUser::get($this->user->identity, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]); - if($this->user->identity->isDeleted() && !$this->deactivationTolerant) { - if($this->user->identity->isDeactivated()) { + 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", [ + $this->getTemplatingEngine()->render(__DIR__ . "/templates/@deactivated.latte", [ "thisUser" => $this->user->identity, "csrfToken" => $GLOBALS["csrfToken"], "isTimezoned" => Session::i()->get("_timezoneOffset"), ]); } else { Authenticator::i()->logout(); - Session::i()->set("_su", NULL); + Session::i()->set("_su", null); $this->flashFail("err", tr("error"), tr("profile_not_found")); $this->redirect("/"); } exit; } - if($this->user->identity->isBanned() && !$this->banTolerant) { + if ($this->user->identity->isBanned() && !$this->banTolerant) { header("HTTP/1.1 403 Forbidden"); - $this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [ + $this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.latte", [ "thisUser" => $this->user->identity, "csrfToken" => $GLOBALS["csrfToken"], "isTimezoned" => Session::i()->get("_timezoneOffset"), @@ -244,9 +264,9 @@ function onStartup(): void } # ето для емейл уже надо (и по хорошему надо бы избавится от повторяющегося кода мда) - if(!$this->user->identity->isActivated() && !$this->activationTolerant) { + if (!$this->user->identity->isActivated() && !$this->activationTolerant) { header("HTTP/1.1 403 Forbidden"); - $this->getTemplatingEngine()->render(__DIR__ . "/templates/@email.xml", [ + $this->getTemplatingEngine()->render(__DIR__ . "/templates/@email.latte", [ "thisUser" => $this->user->identity, "csrfToken" => $GLOBALS["csrfToken"], "isTimezoned" => Session::i()->get("_timezoneOffset"), @@ -256,25 +276,54 @@ function onStartup(): void $userValidated = 1; $cacheTime = 0; # Force no cache - if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { + if (!property_exists($this, 'silent') && $this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { $this->user->identity->setOnline(time()); - $this->user->identity->setClient_name(NULL); + $this->user->identity->setClient_name(null); $this->user->identity->save(false); } - $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); - if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) { - $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); - $this->template->reportNotAnsweredCount = (new Reports)->getReportsCount(0); + $this->template->ticketAnsweredCount = (new Tickets())->getTicketsCountByUserId($this->user->id, 1); + if ($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) { + $this->template->helpdeskTicketNotAnsweredCount = (new Tickets())->getTicketCount(0); + $this->template->reportNotAnsweredCount = (new Reports())->getReportsCount(0); + } + + if ($user->can("admin")->model("openvk\Web\Models\Entities\Report")->whichBelongsTo(0)) { + $this->template->reportNotAnsweredCount = (new Reports())->getReportsCount(0); + } + + $bdays = $this->user->identity->getFriendsBday(true); + if (sizeof($bdays) == 0) { + $bdays = $this->user->identity->getFriendsBday(false); + } + + if (sizeof($bdays) > 0) { + $this->template->showBday = true; + $this->template->isBdayToday = $bdays["isToday"]; + $this->template->bdayUsers = $bdays["users"]; + $this->template->bdayCount = sizeof($bdays["users"]); + } else { + $this->template->showBday = false; } + } else { + $this->user = (object) []; + $this->user->identity = null; + $this->user->id = null; + $this->template->thisUser = null; } + $this->template->baseUrl = ovk_scheme(true) . $_SERVER['HTTP_HOST']; + $this->template->instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']; + header("X-OpenVK-User-Validated: $userValidated"); header("X-Accel-Expires: $cacheTime"); - setlocale(LC_TIME, ...(explode(";", tr("__locale")))); + $localeStr = tr("__locale"); + if (!str_starts_with($localeStr, "@")) { + setlocale(LC_TIME, ...(explode(";", $localeStr))); + } if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"]["all"]) { - if (OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) { + if ($this->presenterName && OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) { $this->pass("openvk!Maintenance->section", $this->presenterName); } } else { @@ -283,41 +332,48 @@ function onStartup(): void } } + if (isset($_SERVER['HTTP_X_OPENVK_AJAX_QUERY']) && $_SERVER['HTTP_X_OPENVK_AJAX_QUERY'] == '1' && $this->user->identity) { + error_reporting(0); + header('Content-Type: text/plain; charset=UTF-8'); + } + parent::onStartup(); } - - function onBeforeRender(): void + + public function onBeforeRender(): void { parent::onBeforeRender(); - + $whichbrowser = new WhichBrowser\Parser(getallheaders()); $featurephonetheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultFeaturePhoneTheme"]; $mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"]; - - if($featurephonetheme && $this->isOldThing($whichbrowser) && Session::i()->get("_tempTheme") == NULL) { + + if ($featurephonetheme && $this->isOldThing($whichbrowser) && Session::i()->get("_tempTheme") == null) { $this->setSessionTheme($featurephonetheme); - } elseif($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL) + } elseif ($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == null) { $this->setSessionTheme($mobiletheme); - - $theme = NULL; - if(Session::i()->get("_tempTheme")) { + } + + $theme = null; + if (Session::i()->get("_tempTheme")) { $theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")]; - Session::i()->set("_tempTheme", NULL); - } else if(Session::i()->get("_sessionTheme")) { + Session::i()->set("_tempTheme", null); + } elseif (Session::i()->get("_sessionTheme")) { $theme = Themepacks::i()[Session::i()->get("_sessionTheme", "ovk")]; - } else if($this->requestParam("themePreview")) { + } elseif ($this->requestParam("themePreview")) { $theme = Themepacks::i()[$this->requestParam("themePreview")]; - } else if($this->user->identity !== NULL && $this->user->identity->getTheme()) { + } elseif ($this->user !== null && $this->user->identity !== null && $this->user->identity->getTheme()) { $theme = $this->user->identity->getTheme(); } - + $this->template->theme = $theme; - if(!is_null($theme) && $theme->overridesTemplates()) + if (!is_null($theme) && $theme->overridesTemplates()) { $this->template->_templatePath = $theme->getBaseDir() . "/tpl"; - - if(!is_null(Session::i()->get("_error"))) { + } + + if (!is_null(Session::i()->get("_error"))) { $this->template->flashMessage = json_decode(Session::i()->get("_error")); - Session::i()->set("_error", NULL); + Session::i()->set("_error", null); } } @@ -330,32 +386,39 @@ protected function returnJson(array $json): void exit($payload); } - protected function isOldThing($whichbrowser) { - if($whichbrowser->isOs('Series60') || - $whichbrowser->isOs('Series40') || - $whichbrowser->isOs('Series80') || - $whichbrowser->isOs('Windows CE') || - $whichbrowser->isOs('Windows Mobile') || - $whichbrowser->isOs('Nokia Asha Platform') || - $whichbrowser->isOs('UIQ') || + protected function isOldThing($whichbrowser) + { + if ($whichbrowser->isOs('Series60') || + $whichbrowser->isOs('Series40') || + $whichbrowser->isOs('Series80') || + $whichbrowser->isOs('Windows CE') || + $whichbrowser->isOs('Windows Mobile') || + $whichbrowser->isOs('Nokia Asha Platform') || + $whichbrowser->isOs('UIQ') || $whichbrowser->isEngine('NetFront') || // PSP and other japanese portable systems - $whichbrowser->isOs('Android') || + $whichbrowser->isOs('Android') || $whichbrowser->isOs('iOS') || - $whichbrowser->isBrowser('Internet Explorer', '<=', '8')) { + $whichbrowser->isBrowser('BlackBerry Browser') || + $whichbrowser->isBrowser('Internet Explorer', '<=', '8') || + $whichbrowser->isBrowser('Firefox', '<=', '47') || + $whichbrowser->isBrowser('Safari', '<=', '7') || + $whichbrowser->isBrowser('Google Chrome', '<=', '35')) { // yeah, it's old, but ios and android are? - if($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9')) + if ($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9')) { return true; - elseif($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9')) + } elseif ($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9')) { return false; - - if($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5')) + } + + if ($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5')) { return true; - elseif($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5')) + } elseif ($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5')) { return false; + } return true; } else { return false; } } -} +} diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index aeb8ba1ec..63abc53d5 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -1,5 +1,9 @@ -users = $users; $this->photos = $photos; $this->albums = $albums; - + parent::__construct(); } - - function renderAlbumList(int $owner): void + + public function renderAlbumList(int $owner): void { - if($owner > 0) { + if ($owner > 0) { $user = $this->users->get($owner); - if(!$user) $this->notFound(); - if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) + if (!$user) { + $this->notFound(); + } + if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->albums = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1)); + } + + $this->template->albums = $this->albums->getUserAlbums($user, (int) ($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getUserAlbumsCount($user); $this->template->owner = $user; $this->template->canEdit = false; - if(!is_null($this->user)) + if (!is_null($this->user->identity)) { $this->template->canEdit = $this->user->id === $user->getId(); + } } else { - $club = (new Clubs)->get(abs($owner)); - if(!$club) $this->notFound(); - $this->template->albums = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1)); + $club = (new Clubs())->get(abs($owner)); + if (!$club) { + $this->notFound(); + } + $this->template->albums = $this->albums->getClubAlbums($club, (int) ($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getClubAlbumsCount($club); $this->template->owner = $club; $this->template->canEdit = false; - if(!is_null($this->user)) + if (!is_null($this->user->identity)) { $this->template->canEdit = $club->canBeModifiedBy($this->user->identity); + } } - + $this->template->paginatorConf = (object) [ "count" => $this->template->count, - "page" => (int)($this->queryParam("p") ?? 1), - "amount" => NULL, + "page" => (int) ($this->queryParam("p") ?? 1), + "amount" => null, "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; } - - function renderCreateAlbum(): void + + public function renderCreateAlbum(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if(!is_null($gpid = $this->queryParam("gpid"))) { - $club = (new Clubs)->get((int) $gpid); - if(!$club->canBeModifiedBy($this->user->identity)) + + if (!is_null($gpid = $this->queryParam("gpid"))) { + $club = (new Clubs())->get((int) $gpid); + if (!$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); - + } + $this->template->club = $club; } - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) - $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; + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) { + $this->flashFail("err", tr("error"), tr("error_segmentation")); + } elseif (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); $album->setName($this->postParam("name")); $album->setDescription($this->postParam("desc")); $album->setCreated(time()); $album->save(); - - if(isset($club)) + + if (isset($club)) { $this->redirect("/album-" . $album->getOwner()->getId() . "_" . $album->getId()); - else + } else { $this->redirect("/album" . $album->getOwner()->getId() . "_" . $album->getId()); + } } } - - function renderEditAlbum(int $owner, int $id): void + + public function renderEditAlbum(int $owner, int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $album = $this->albums->get($id); - if(!$album) $this->notFound(); - if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); - if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted()) + if (!$album) { + $this->notFound(); + } + if ($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) { + $this->notFound(); + } + if (is_null($this->user->identity) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted()) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } $this->template->album = $album; - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(strlen($this->postParam("name")) > 36) + + 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")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $album->getName() : $this->postParam("name")); - $album->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); + $album->setDescription(empty($this->postParam("desc")) ? null : $this->postParam("desc")); $album->setEdited(time()); $album->save(); - + $this->flash("succ", tr("changes_saved"), tr("new_data_accepted")); } } - - function renderDeleteAlbum(int $owner, int $id): void + + public function renderDeleteAlbum(int $owner, int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); $this->assertNoCSRF(); - + $album = $this->albums->get($id); - if(!$album) $this->notFound(); - if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); - if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) + if (!$album) { + $this->notFound(); + } + if ($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) { + $this->notFound(); + } + if (is_null($this->user->identity) || !$album->canBeModifiedBy($this->user->identity)) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - + } + $name = $album->getName(); $owner = $album->getOwner(); $album->delete(); @@ -129,152 +157,187 @@ function renderDeleteAlbum(int $owner, int $id): void $this->flash("succ", tr("album_is_deleted"), tr("album_x_is_deleted", $name)); $this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId()); } - - function renderAlbum(int $owner, int $id): void + + public function renderAlbum(int $owner, int $id): void { $album = $this->albums->get($id); - if(!$album) $this->notFound(); - if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) + if (!$album) { $this->notFound(); - - if($owner > 0 /* bc we currently don't have perms for clubs */) { - $ownerObject = (new Users)->get($owner); - if(!$ownerObject->getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) + } + if ($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) { + $this->notFound(); + } + + if (!$album->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + + if ($owner > 0 /* bc we currently don't have perms for clubs */) { + $ownerObject = (new Users())->get($owner); + if (!$ownerObject->getPrivacyPermission('photos.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } } - + $this->template->album = $album; - $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) ); + $this->template->photos = iterator_to_array($album->getPhotos((int) ($this->queryParam("p") ?? 1), 20)); $this->template->paginatorConf = (object) [ "count" => $album->getPhotosCount(), - "page" => (int)($this->queryParam("p") ?? 1), + "page" => (int) ($this->queryParam("p") ?? 1), "amount" => sizeof($this->template->photos), "perPage" => 20, - "atBottom" => true + "atBottom" => true, + "tidy" => false, + "atTop" => false, ]; } - - function renderPhoto(int $ownerId, int $photoId): void + + public function renderPhoto(int $ownerId, int $photoId): void { $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); - if(!$photo || $photo->isDeleted()) $this->notFound(); - - if(!is_null($this->queryParam("from"))) { - if(preg_match("%^album([0-9]++)$%", $this->queryParam("from"), $matches) === 1) { + if (!$photo || $photo->isDeleted()) { + $this->notFound(); + } + + if (!$photo->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } + + if (!is_null($this->queryParam("from"))) { + if (preg_match("%^album([0-9]++)$%", $this->queryParam("from"), $matches) === 1) { $album = $this->albums->get((int) $matches[1]); - if($album) - if($album->hasPhoto($photo) && !$album->isDeleted()) + if ($album) { + if ($album->hasPhoto($photo) && !$album->isDeleted()) { $this->template->album = $album; + } + } } } - + $this->template->photo = $photo; $this->template->cCount = $photo->getCommentsCount(); $this->template->cPage = (int) ($this->queryParam("p") ?? 1); $this->template->comments = iterator_to_array($photo->getComments($this->template->cPage)); + $this->template->owner = $photo->getOwner(); } - - function renderAbsolutePhoto($id): void + + public function renderAbsolutePhoto($id): void { $id = (int) base_convert((string) $id, 32, 10); $photo = $this->photos->get($id); - if(!$photo || $photo->isDeleted()) + if (!$photo || $photo->isDeleted()) { $this->notFound(); - - $this->template->_template = "Photos/Photo.xml"; + } + + $this->template->_template = "Photos/Photo.latte"; $this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId()); } - - function renderThumbnail($id, $size): void + + public function renderThumbnail($id, $size): void { $photo = $this->photos->get($id); - if(!$photo || $photo->isDeleted()) + if (!$photo || $photo->isDeleted()) { $this->notFound(); - - if(!$photo->forceSize($size)) + } + + if (!$photo->forceSize($size)) { chandler_http_panic(588, "Gone", "This thumbnail cannot be generated due to server misconfiguration"); - + } + $this->redirect($photo->getURLBySizeId($size), 8); } - - function renderEditPhoto(int $ownerId, int $photoId): void + + public function renderEditPhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); - if(!$photo) $this->notFound(); - if(is_null($this->user) || $this->user->id != $ownerId) + if (!$photo) { + $this->notFound(); + } + if (is_null($this->user->identity) || $this->user->id != $ownerId) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - $photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $photo->setDescription(empty($this->postParam("desc")) ? null : $this->postParam("desc")); $photo->save(); - + $this->flash("succ", tr("changes_saved"), tr("new_description_will_appear")); $this->redirect("/photo" . $photo->getPrettyId()); - } - + } + $this->template->photo = $photo; } - - function renderUploadPhoto(): void + + public function renderUploadPhoto(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(true); - if(is_null($this->queryParam("album"))) { - $album = $this->albums->getUserWallAlbum($this->user->identity); + $upload_context = $this->queryParam("upload_context"); + + if (is_null($this->queryParam("album"))) { + if ((int) $upload_context == $this->user->id) { + $album = $this->albums->getUserWallAlbum($this->user->identity); + } } else { [$owner, $id] = explode("_", $this->queryParam("album")); $album = $this->albums->get((int) $id); } - if(!$album) - $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); + if ($_SERVER["REQUEST_METHOD"] == "GET" || $this->queryParam("act") == "finish") { + if (!$album || $album->isCreatedBySystem()) { + $this->flashFail("err", tr("error"), tr("error_adding_to_deleted")); + } + } + + if ($album && !$album->canBeModifiedBy($this->user->identity)) { + if ($album->getOwnerId() != $this->user->id) { + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } + } - # Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото - # в системные альбомы, так что так. - if(is_null($this->user) || !is_null($this->queryParam("album")) && !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if($this->queryParam("act") == "finish") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if ($this->queryParam("act") == "finish") { $result = json_decode($this->postParam("photos"), true); - - foreach($result as $photoId => $description) { + + foreach ($result as $photoId => $description) { $phot = $this->photos->get($photoId); - if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id) + if (!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id) { continue; - - if(iconv_strlen($description) > 255) + } + + if (iconv_strlen($description) > 255) { $this->flashFail("err", tr("error"), tr("description_too_long"), 500, true); + } $phot->setDescription($description); $phot->save(); - - $album = $phot->getAlbum(); } $this->returnJson(["success" => true, - "album" => $album->getId(), - "owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]); + "album" => $album->getId(), + "owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]); } - if(!isset($_FILES)) + if (!isset($_FILES)) { $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true); - + } + $photos = []; - if((int)$this->postParam("count") > 10) - $this->flashFail("err", tr("no_photo"), "ты еблан", 500, true); + if ((int) $this->postParam("count") > 10) { + $this->flashFail("err", tr("no_photo"), "Too many photos (max is 7-8)", 500, true); + } - for($i = 0; $i < $this->postParam("count"); $i++) { + for ($i = 0; $i < $this->postParam("count"); $i++) { try { - $photo = new Photo; + $photo = new Photo(); $photo->setOwner($this->user->id); $photo->setDescription(""); - $photo->setFile($_FILES["photo_".$i]); + $photo->setFile($_FILES["photo_" . $i]); $photo->setCreated(time()); $photo->save(); @@ -283,16 +346,19 @@ function renderUploadPhoto(): void "id" => $photo->getId(), "vid" => $photo->getVirtualId(), "owner" => $photo->getOwner()->getId(), - "link" => $photo->getURL() + "link" => $photo->getURL(), + "pretty_id" => $photo->getPrettyId(), ]; - } catch(ISE $ex) { + } catch (ISE $ex) { $name = $album->getName(); $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true); } - $album->addPhoto($photo); - $album->setEdited(time()); - $album->save(); + if ($album != null) { + $album->addPhoto($photo); + $album->setEdited(time()); + $album->save(); + } } $this->returnJson(["success" => true, @@ -301,53 +367,87 @@ function renderUploadPhoto(): void $this->template->album = $album; } } - - function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void + + public function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $album = $this->albums->get($albumId); $photo = $this->photos->get($photoId); - if(!$album || !$photo) $this->notFound(); - if(!$album->hasPhoto($photo)) $this->notFound(); - if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) + if (!$album || !$photo) { + $this->notFound(); + } + if (!$album->hasPhoto($photo)) { + $this->notFound(); + } + if (is_null($this->user->identity) || !$album->canBeModifiedBy($this->user->identity)) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - - if($_SERVER["REQUEST_METHOD"] === "POST") { + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->assertNoCSRF(); $album->removePhoto($photo); $album->setEdited(time()); $album->save(); - + $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); $this->redirect("/album" . $album->getPrettyId()); } } - - function renderDeletePhoto(int $ownerId, int $photoId): void + + public function renderDeletePhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST"); $this->assertNoCSRF(); - + $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); - if(!$photo) $this->notFound(); - if(is_null($this->user) || $this->user->id != $ownerId) + if (!$photo) { + $this->notFound(); + } + if (is_null($this->user->identity) || $this->user->id != $ownerId) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } - if(!is_null($album = $photo->getAlbum())) - $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; - else + if (!is_null($album = $photo->getAlbum())) { + $redirect = '/album' . $album->getPrettyId(); + } else { $redirect = "/id0"; + } $photo->isolate(); $photo->delete(); - - if($_SERVER["REQUEST_METHOD"] === "POST") + + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->returnJson(["success" => true]); + } $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); $this->redirect($redirect); } + + public function renderLike(int $wall, int $post_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertNoCSRF(); + + $photo = $this->photos->getByOwnerAndVID($wall, $post_id); + if (!$photo || $photo->isDeleted() || !$photo->canBeViewedBy($this->user->identity)) { + $this->notFound(); + } + + if (!is_null($this->user->identity)) { + $photo->toggleLike($this->user->identity); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + + $this->redirect("$_SERVER[HTTP_REFERER]"); + } } diff --git a/Web/Presenters/PollPresenter.php b/Web/Presenters/PollPresenter.php index 9c75e3bf4..4bdce8320 100644 --- a/Web/Presenters/PollPresenter.php +++ b/Web/Presenters/PollPresenter.php @@ -1,25 +1,30 @@ -polls = $polls; - + parent::__construct(); } - - function renderView(int $id): void + + public function renderView(int $id): void { $poll = $this->polls->get($id); - if(!$poll) + if (!$poll) { $this->notFound(); - + } + $this->template->id = $poll->getId(); $this->template->title = $poll->getTitle(); $this->template->isAnon = $poll->isAnonymous(); @@ -29,41 +34,44 @@ function renderView(int $id): void $this->template->votes = $poll->getVoterCount(); $this->template->meta = $poll->getMetaDescription(); $this->template->ended = $ended = $poll->hasEnded(); - if((is_null($this->user) || $poll->canVote($this->user->identity)) && !$ended) { + if ((is_null($this->user->identity) || $poll->canVote($this->user->identity)) && !$ended) { $this->template->options = $poll->getOptions(); - - $this->template->_template = "Poll/Poll.xml"; + + $this->template->_template = "Poll/Poll.latte"; return; } - - if(is_null($this->user)) { + + if (is_null($this->user->identity)) { $this->template->voted = false; $this->template->results = $poll->getResults(); } else { $this->template->voted = $poll->hasVoted($this->user->identity); $this->template->results = $poll->getResults($this->user->identity); } - - $this->template->_template = "Poll/PollResults.xml"; + + $this->template->_template = "Poll/PollResults.latte"; } - - function renderVoters(int $pollId): void + + public function renderVoters(int $pollId): void { $poll = $this->polls->get($pollId); - if(!$poll) + if (!$poll) { $this->notFound(); - - if($poll->isAnonymous()) + } + + if ($poll->isAnonymous()) { $this->flashFail("err", tr("forbidden"), tr("poll_err_anonymous")); - + } + $options = $poll->getOptions(); $option = (int) base_convert($this->queryParam("option"), 32, 10); - if(!in_array($option, array_keys($options))) + if (!in_array($option, array_keys($options))) { $this->notFound(); - + } + $page = (int) ($this->queryParam("p") ?? 1); $voters = $poll->getVoters($option, $page); - + $this->template->pollId = $pollId; $this->template->options = $options; $this->template->option = [$option, $options[$option]]; @@ -72,4 +80,4 @@ function renderVoters(int $pollId): void $this->template->count = $poll->getVoterCount($option); $this->template->page = $page; } -} \ No newline at end of file +} diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index a87154c80..afc02ba22 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -1,5 +1,9 @@ -reports = $reports; - + parent::__construct(); } - - function renderList(): void + + public function renderList(): void { $this->assertUserLoggedIn(); - $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); - if ($_SERVER["REQUEST_METHOD"] === "POST") + $this->assertPermission('openvk\Web\Models\Entities\Report', 'admin', 0); + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->assertNoCSRF(); + } - $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"]) ? $this->queryParam("act") : null; if (!$this->queryParam("orig")) { - $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); + $this->template->reports = $this->reports->getReports(0, (int) ($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); $this->template->count = $this->reports->getReportsCount(); } else { $orig = $this->reports->get((int) $this->queryParam("orig")); - if (!$orig) $this->redirect("/scumfeed"); + if (!$orig) { + $this->redirect("/scumfeed"); + } $this->template->reports = $orig->getDuplicates(); $this->template->count = $orig->getDuplicatesCount(); @@ -39,10 +46,13 @@ function renderList(): void $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => $this->queryParam("p") ?? 1, - "amount" => NULL, + "amount" => null, "perPage" => 15, + "tidy" => false, + "atTop" => false, ]; $this->template->mode = $act ?? "all"; + $this->template->disable_ajax = 1; if ($_SERVER["REQUEST_METHOD"] === "POST") { $reports = []; @@ -53,13 +63,13 @@ function renderList(): void "id" => $report->getReportAuthor()->getId(), "url" => $report->getReportAuthor()->getURL(), "name" => $report->getReportAuthor()->getCanonicalName(), - "is_female" => $report->getReportAuthor()->isFemale() + "is_female" => $report->getReportAuthor()->isFemale(), ], "content" => [ "name" => $report->getContentName(), "type" => $report->getContentType(), "id" => $report->getContentId(), - "url" => $report->getContentType() === "user" ? (new Users)->get((int) $report->getContentId())->getURL() : NULL + "url" => $report->getContentType() === "user" ? (new Users())->get((int) $report->getContentId())->getURL() : null, ], "duplicates" => $report->getDuplicatesCount(), ]; @@ -67,30 +77,41 @@ function renderList(): void $this->returnJson(["reports" => $reports]); } } - - function renderView(int $id): void + + public function renderView(int $id): void { $this->assertUserLoggedIn(); - $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertPermission('openvk\Web\Models\Entities\Report', 'admin', 0); $report = $this->reports->get($id); - if(!$report || $report->isDeleted()) + if (!$report || $report->isDeleted()) { $this->notFound(); - + } + $this->template->report = $report; + $this->template->disable_ajax = 1; } - - function renderCreate(int $id): void + + public function renderCreate(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!$id) + if (!$id) { exit(json_encode([ "error" => tr("error_segmentation") ])); + } + + if ($this->queryParam("type") === "user" && $id === $this->user->id) { + exit(json_encode([ "error" => "You can't report yourself" ])); + } + + if ($this->user->identity->isBannedInSupport()) { + exit(json_encode([ "reason" => $this->queryParam("reason") ])); + } - if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { - if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { - $report = new Report; + if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) { + if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) { + $report = new Report(); $report->setUser_id($this->user->id); $report->setTarget_id($id); $report->setType($this->queryParam("type")); @@ -98,42 +119,46 @@ function renderCreate(int $id): void $report->setCreated(time()); $report->save(); } - + exit(json_encode([ "reason" => $this->queryParam("reason") ])); } else { exit(json_encode([ "error" => "Unable to submit a report on this content type" ])); } } - - function renderAction(int $id): void + + public function renderAction(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertPermission('openvk\Web\Models\Entities\Report', 'admin', 0); $report = $this->reports->get($id); - if(!$report || $report->isDeleted()) $this->notFound(); + if (!$report || $report->isDeleted()) { + $this->notFound(); + } if ($this->postParam("ban")) { $report->deleteContent(); $report->banUser($this->user->identity->getId()); $this->flash("suc", tr("death"), tr("user_successfully_banned")); - } else if ($this->postParam("delete")) { + } elseif ($this->postParam("delete")) { $report->deleteContent(); $this->flash("suc", tr("nehay"), tr("content_is_deleted")); - } else if ($this->postParam("ignore")) { + } elseif ($this->postParam("ignore")) { $report->delete(); $this->flash("suc", tr("nehay"), tr("report_is_ignored")); - } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { - if ($report->getContentType() !== "group") + } elseif ($this->postParam("banClubOwner") || $this->postParam("banClub")) { + if ($report->getContentType() !== "group") { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } $club = $report->getContentObject(); - if (!$club || $club->isBanned()) + if (!$club || $club->isBanned()) { $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + } if ($this->postParam("banClubOwner")) { $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index fadf9954c..e9161229f 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,11 @@ -users = $users; - $this->clubs = $clubs; - $this->posts = new Posts; - $this->comments = new Comments; - $this->videos = new Videos; - $this->apps = new Applications; - $this->notes = new Notes; - + $this->users = new Users(); + $this->clubs = new Clubs(); + $this->posts = new Posts(); + $this->videos = new Videos(); + $this->apps = new Applications(); + $this->audios = new Audios(); + $this->documents = new Documents(); + parent::__construct(); } - - function renderIndex(): void + + public function renderIndex(): void { - $query = $this->queryParam("query") ?? ""; - $type = $this->queryParam("type") ?? "users"; - $sorter = $this->queryParam("sort") ?? "id"; - $invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC"; - $page = (int) ($this->queryParam("p") ?? 1); - - $this->willExecuteWriteAction(); - if($query != "") - $this->assertUserLoggedIn(); - + $this->assertUserLoggedIn(); + + $query = $this->queryParam("q") ?? ""; + $section = $this->queryParam("section") ?? "users"; + $order = $this->queryParam("order") ?? "id"; + $invert = (int) ($this->queryParam("invert") ?? 0) == 1; + $page = (int) ($this->queryParam("p") ?? 1); + # https://youtu.be/pSAWM5YuXx8 - $repos = [ - "groups" => "clubs", + $repos = [ + "groups" => "clubs", "users" => "users", "posts" => "posts", - "comments" => "comments", "videos" => "videos", - "audios" => "posts", + "audios" => "audios", "apps" => "apps", - "notes" => "notes" + "audios_playlists" => "audios", + "docs" => "documents", + ]; + $parameters = [ + "ignore_private" => true, ]; - switch($sorter) { + foreach ($_REQUEST as $param_name => $param_value) { + if (is_null($param_value)) { + continue; + } + + switch ($param_name) { + default: + $parameters[$param_name] = $param_value; + break; + case 'marital_status': + case 'polit_views': + if ((int) $param_value == 0) { + break; + } + $parameters[$param_name] = $param_value; + + break; + case 'is_online': + if ((int) $param_value == 1) { + $parameters['is_online'] = 1; + } + + break; + case 'only_performers': + if ((int) $param_value == 1 || $param_value == 'on') { + $parameters['only_performers'] = true; + } + + break; + case 'with_lyrics': + if ($param_value == 'on' || $param_value == '1') { + $parameters['with_lyrics'] = true; + } + + break; + # дай бог работал этот case + case 'from_me': + if ((int) $param_value != 1) { + break; + } + $parameters['from_me'] = $this->user->id; + + break; + } + } + + $repo = $repos[$section] or $this->throwError(400, "Bad Request", "Invalid search entity $section."); + + $results = null; + switch ($section) { default: - case "id": - $sort = "id " . $invert; + $results = $this->{$repo}->find($query, $parameters, ['type' => $order, 'invert' => $invert]); + break; + case 'audios_playlists': + $results = $this->{$repo}->findPlaylists($query, $parameters, ['type' => $order, 'invert' => $invert]); break; - case "name": - $sort = "first_name " . $invert; - break; - case "rating": - $sort = "rating " . $invert; - break; } - $parameters = [ - "type" => $this->queryParam("type"), - "city" => $this->queryParam("city") != "" ? $this->queryParam("city") : NULL, - "maritalstatus" => $this->queryParam("maritalstatus") != 0 ? $this->queryParam("maritalstatus") : NULL, - "with_photo" => $this->queryParam("with_photo"), - "status" => $this->queryParam("status") != "" ? $this->queryParam("status") : NULL, - "politViews" => $this->queryParam("politViews") != 0 ? $this->queryParam("politViews") : NULL, - "email" => $this->queryParam("email"), - "telegram" => $this->queryParam("telegram"), - "site" => $this->queryParam("site") != "" ? "https://".$this->queryParam("site") : NULL, - "address" => $this->queryParam("address"), - "is_online" => $this->queryParam("is_online") == 1 ? 1 : NULL, - "interests" => $this->queryParam("interests") != "" ? $this->queryParam("interests") : NULL, - "fav_mus" => $this->queryParam("fav_mus") != "" ? $this->queryParam("fav_mus") : NULL, - "fav_films" => $this->queryParam("fav_films") != "" ? $this->queryParam("fav_films") : NULL, - "fav_shows" => $this->queryParam("fav_shows") != "" ? $this->queryParam("fav_shows") : NULL, - "fav_books" => $this->queryParam("fav_books") != "" ? $this->queryParam("fav_books") : NULL, - "fav_quote" => $this->queryParam("fav_quote") != "" ? $this->queryParam("fav_quote") : NULL, - "hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL, - "before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL, - "after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL, - "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL - ]; - - $repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type."); - - $results = $this->{$repo}->find($query, $parameters, $sort); - $iterator = $results->page($page); + $iterator = $results->page($page, OPENVK_DEFAULT_PER_PAGE); $count = $results->size(); - - $this->template->iterator = iterator_to_array($iterator); + + $this->template->order = $order; + $this->template->invert = $invert; + $this->template->data = $this->template->iterator = iterator_to_array($iterator); $this->template->count = $count; - $this->template->type = $type; + $this->template->section = $section; $this->template->page = $page; + $this->template->perPage = OPENVK_DEFAULT_PER_PAGE; + $this->template->query = $query; + $this->template->atSearch = true; + + $this->template->paginatorConf = (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($this->template->data), + "perPage" => $this->template->perPage, + "atTop" => false, + "atBottom" => false, + "tidy" => true, + "space" => 6, + 'pageCount' => ceil($count / $this->template->perPage), + ]; + $this->template->extendedPaginatorConf = clone $this->template->paginatorConf; + $this->template->extendedPaginatorConf->space = 11; + $this->template->paginatorConf->atTop = true; } } diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index b834d7126..a947d71e4 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -1,5 +1,9 @@ -tickets = $tickets; $this->comments = $ticketComments; - + parent::__construct(); } - - function renderIndex(): void + + public function renderIndex(): void { $this->assertUserLoggedIn(); $this->template->mode = in_array($this->queryParam("act"), ["faq", "new", "list"]) ? $this->queryParam("act") : "faq"; - if($this->template->mode === "faq") { + if ($this->template->mode === "faq") { $lang = Session::i()->get("lang", "ru"); $base = OPENVK_ROOT . "/data/knowledgebase/faq"; - if(file_exists("$base.$lang.md")) + if (file_exists("$base.$lang.md")) { $file = "$base.$lang.md"; - else if(file_exists("$base.md")) + } elseif (file_exists("$base.md")) { $file = "$base.md"; - else - $file = NULL; + } else { + $file = null; + } - if(is_null($file)) { + if (is_null($file)) { $this->template->faq = []; } else { $lines = file($file); $faq = []; $index = 0; - foreach($lines as $line) { - if(strpos($line, "# ") === 0) + foreach ($lines as $line) { + if (strpos($line, "# ") === 0) { ++$index; + } $faq[$index][] = $line; } - $this->template->faq = array_map(function($section) { + $this->template->faq = array_map(function ($section) { $title = substr($section[0], 2); array_shift($section); return [ $title, - (new Parsedown())->text(implode("\n", $section)) + (new Parsedown())->text(implode("\n", $section)), ]; }, $faq); } } $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); - if($this->template->mode === "list") { + if ($this->template->mode === "list") { $this->template->page = (int) ($this->queryParam("p") ?? 1); $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page)); } - if($this->template->mode === "new") + if ($this->template->mode === "new") { $this->template->banReason = $this->user->identity->getBanInSupportReason(); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if($this->user->identity->isBannedInSupport()) + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if ($this->user->identity->isBannedInSupport()) { $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); + } - if(!empty($this->postParam("name")) && !empty($this->postParam("text"))) { + if (!empty($this->postParam("name")) && !empty($this->postParam("text"))) { $this->willExecuteWriteAction(); - $ticket = new Ticket; + $ticket = new Ticket(); $ticket->setType(0); $ticket->setUser_Id($this->user->id); $ticket->setName($this->postParam("name")); @@ -89,7 +97,7 @@ function renderIndex(): void $ticket->save(); $helpdeskChat = OPENVK_ROOT_CONF["openvk"]["credentials"]["telegram"]["helpdeskChat"]; - if($helpdeskChat) { + if ($helpdeskChat) { $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; $ticketText = ovk_proc_strtr($this->postParam("text"), 1500); $telegramText = "📬 Новый тикет!\n\n"; @@ -105,14 +113,14 @@ function renderIndex(): void } } } - - function renderList(): void + + public function renderList(): void { $this->assertUserLoggedIn(); $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); - + $act = $this->queryParam("act") ?? "open"; - switch($act) { + switch ($act) { default: # NOTICE falling through case "open": @@ -124,87 +132,106 @@ function renderList(): void case "closed": $state = 2; } - + $this->template->act = $act; $this->template->page = (int) ($this->queryParam("p") ?? 1); $this->template->count = $this->tickets->getTicketCount($state); $this->template->iterator = $this->tickets->getTickets($state, $this->template->page); } - - function renderView(int $id): void + + public function renderView(int $id): void { $this->assertUserLoggedIn(); $ticket = $this->tickets->get($id); $ticketComments = $this->comments->getCommentsById($id); - if(!$ticket || $ticket->isDeleted() != 0 || $ticket->getUserId() !== $this->user->id) { + if (!$ticket || $ticket->isDeleted() != 0 || $ticket->getUserId() !== $this->user->id) { $this->notFound(); } else { - $this->template->ticket = $ticket; - $this->template->comments = $ticketComments; - $this->template->id = $id; + $this->template->ticket = $ticket; + $this->template->comments = $ticketComments; + $this->template->id = $id; } } - - function renderDelete(int $id): void + + public function renderDelete(int $id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!empty($id)) { + if (!empty($id)) { $ticket = $this->tickets->get($id); - if(!$ticket || $ticket->isDeleted() != 0 || $ticket->getUserId() !== $this->user->id && !$this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0)) { + if (!$ticket || $ticket->isDeleted() != 0 || $ticket->getUserId() !== $this->user->id && !$this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0)) { $this->notFound(); } else { - if($ticket->getUserId() !== $this->user->id && $this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0)) + if ($ticket->getUserId() !== $this->user->id && $this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0)) { $_redirect = "/support/tickets"; - else + } else { $_redirect = "/support?act=list"; + } + + $helpdeskChat = OPENVK_ROOT_CONF["openvk"]["credentials"]["telegram"]["helpdeskChat"]; + if ($helpdeskChat) { + $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; + $telegramText = "❌ Тикет под названием "{$ticket->getName()}" от {$ticket->getUser()->getCanonicalName()} ({$ticket->getUser()->getRegistrationIP()}) был удалён.\n"; + Telegram::send($helpdeskChat, $telegramText); + } $ticket->delete(); $this->redirect($_redirect); } } } - - function renderMakeComment(int $id): void + + public function renderMakeComment(int $id): void { $ticket = $this->tickets->get($id); - - if($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { + + if ($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { header("HTTP/1.1 403 Forbidden"); header("Location: /support/view/" . $id); exit; } - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!empty($this->postParam("text"))) { + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (!empty($this->postParam("text"))) { $ticket->setType(0); $ticket->save(); $this->willExecuteWriteAction(); - - $comment = new TicketComment; + + $comment = new TicketComment(); $comment->setUser_id($this->user->id); $comment->setUser_type(0); $comment->setText($this->postParam("text")); $comment->setTicket_id($id); $comment->setCreated(time()); $comment->save(); - + + $helpdeskChat = OPENVK_ROOT_CONF["openvk"]["credentials"]["telegram"]["helpdeskChat"]; + if ($helpdeskChat) { + $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; + $commentText = ovk_proc_strtr($this->postParam("text"), 1500); + $telegramText = "💬 Новый комментарий от автора тикета "{$ticket->getName()}"\n"; + $telegramText .= "$commentText\n\n"; + $telegramText .= "Автор: {$ticket->getUser()->getCanonicalName()} ({$ticket->getUser()->getRegistrationIP()})\n"; + Telegram::send($helpdeskChat, $telegramText); + } + $this->redirect("/support/view/" . $id); } else { $this->flashFail("err", tr("error"), tr("you_have_not_entered_text")); } } } - - function renderAnswerTicket(int $id): void + + public function renderAnswerTicket(int $id): void { $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); $ticket = $this->tickets->get($id); - if(!$ticket || $ticket->isDeleted() != 0) + if (!$ticket || $ticket->isDeleted() != 0) { $this->notFound(); + } $ticketComments = $this->comments->getCommentsById($id); $this->template->ticket = $ticket; @@ -212,78 +239,133 @@ function renderAnswerTicket(int $id): void $this->template->id = $id; $this->template->fastAnswers = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["fastAnswers"]; } - - function renderAnswerTicketReply(int $id): void + + public function renderAnswerTicketReply(int $id): void { $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); - + + $support_names = new SupportAgents(); + $agent = $support_names->get($this->user->id); $ticket = $this->tickets->get($id); - - if($_SERVER["REQUEST_METHOD"] === "POST") { + + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction(); - - if(!empty($this->postParam("text")) && !empty($this->postParam("status"))) { - $ticket->setType($this->postParam("status")); + + $helpdeskChat = OPENVK_ROOT_CONF["openvk"]["credentials"]["telegram"]["helpdeskChat"]; + + if (!empty($this->postParam("text")) && !empty($this->postParam("status"))) { + $status = $this->postParam("status"); + $ticket->setType($status); $ticket->save(); - $comment = new TicketComment; + switch ($status) { + default: + # NOTICE falling through + case 0: + $state = "Вопрос на рассмотрении"; + break; + case 1: + $state = "Есть ответ"; + break; + case 2: + $state = "Закрыто"; + } + + $comment = new TicketComment(); $comment->setUser_id($this->user->id); $comment->setUser_type(1); $comment->setText($this->postParam("text")); $comment->setTicket_Id($id); $comment->setCreated(time()); $comment->save(); - } elseif(empty($this->postParam("text"))) { - $ticket->setType($this->postParam("status")); + + if ($helpdeskChat) { + $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"] . "/support/agent" . $this->user->id; + $ticketUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"] . "/support/reply/" . $id; + $commentText = ovk_proc_strtr($this->postParam("text"), 1500); + $telegramText = "💬 Новый комментарий от агента к тикету "{$ticket->getName()}"\n"; + $telegramText .= "Статус: {$state}\n\n"; + $telegramText .= "$commentText\n\n"; + $telegramText .= "Агент: {$this->user->identity->getFullName()}\n"; + Telegram::send($helpdeskChat, $telegramText); + } + } elseif (empty($this->postParam("text"))) { + $status = $this->postParam("status"); + $ticket->setType($status); $ticket->save(); + + switch ($status) { + default: + # NOTICE falling through + case 0: + $state = "Вопрос на рассмотрении"; + break; + case 1: + $state = "Есть ответ"; + break; + case 2: + $state = "Закрыто"; + } + + if ($helpdeskChat) { + $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"] . "/support/agent" . $this->user->id; + $ticketUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"] . "/support/reply/" . $id; + $telegramText = "🔔 Изменён статус тикета "{$ticket->getName()}": {$state}\n\n"; + $telegramText .= "Агент: {$this->user->identity->getFullName()}\n"; + Telegram::send($helpdeskChat, $telegramText); + } } - + $this->flashFail("succ", tr("ticket_changed"), tr("ticket_changed_comment")); } } - - function renderKnowledgeBaseArticle(string $name): void + + public function renderKnowledgeBaseArticle(string $name): void { $lang = Session::i()->get("lang", "ru"); $base = OPENVK_ROOT . "/data/knowledgebase"; - if(file_exists("$base/$name.$lang.md")) + if (file_exists("$base/$name.$lang.md")) { $file = "$base/$name.$lang.md"; - else if(file_exists("$base/$name.md")) + } elseif (file_exists("$base/$name.md")) { $file = "$base/$name.md"; - else + } else { $this->notFound(); - + } + $lines = file($file); - if(!preg_match("%^OpenVK-KB-Heading: (.+)$%", $lines[0], $matches)) { + if (!preg_match("%^OpenVK-KB-Heading: (.+)$%", $lines[0], $matches)) { $heading = "Article $name"; } else { $heading = $matches[1]; array_shift($lines); } - + $content = implode($lines); - + $parser = new Parsedown(); $this->template->heading = $heading; $this->template->content = $parser->text($content); } - function renderDeleteComment(int $id): void + public function renderDeleteComment(int $id): void { $this->assertUserLoggedIn(); $this->assertNoCSRF(); $comment = $this->comments->get($id); - if(is_null($comment)) + if (is_null($comment)) { $this->notFound(); + } $ticket = $comment->getTicket(); - if($ticket->isDeleted()) + if ($ticket->isDeleted()) { $this->notFound(); + } - if(!($ticket->getUserId() === $this->user->id && $comment->getUType() === 0)) + if (!($ticket->getUserId() === $this->user->id && $comment->getUType() === 0)) { $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); + } $this->willExecuteWriteAction(); $comment->delete(); @@ -291,7 +373,7 @@ function renderDeleteComment(int $id): void $this->flashFail("succ", tr("ticket_changed"), tr("ticket_changed_comment")); } - function renderRateAnswer(int $id, int $mark): void + public function renderRateAnswer(int $id, int $mark): void { $this->willExecuteWriteAction(); $this->assertUserLoggedIn(); @@ -299,105 +381,135 @@ function renderRateAnswer(int $id, int $mark): void $comment = $this->comments->get($id); - if($this->user->id !== $comment->getTicket()->getUser()->getId()) - exit(header("HTTP/1.1 403 Forbidden")); + if ($this->user->id !== $comment->getTicket()->getUser()->getId()) { + header("HTTP/1.1 403 Forbidden"); + exit(); + } - if($mark !== 1 && $mark !== 2) - exit(header("HTTP/1.1 400 Bad Request")); + if ($mark !== 1 && $mark !== 2) { + 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(); } - function renderQuickBanInSupport(int $id): void + public function renderQuickBanInSupport(int $id): void { $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); $this->assertNoCSRF(); - $user = (new Users)->get($id); - if(!$user) + $user = (new Users())->get($id); + if (!$user) { exit(json_encode([ "error" => "User does not exist" ])); - + } + $user->setBlock_In_Support_Reason($this->queryParam("reason")); $user->save(); - if($this->queryParam("close_tickets")) - DatabaseConnection::i()->getConnection()->query("UPDATE tickets SET type = 2 WHERE user_id = ".$id); + 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") ]); } - function renderQuickUnbanInSupport(int $id): void + public function renderQuickUnbanInSupport(int $id): void { $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); $this->assertNoCSRF(); - - $user = (new Users)->get($id); - if(!$user) + + $user = (new Users())->get($id); + if (!$user) { exit(json_encode([ "error" => "User does not exist" ])); - + } + $user->setBlock_In_Support_Reason(null); $user->save(); $this->returnJson([ "success" => true ]); } - function renderAgent(int $id): void + public function renderAgent(int $id): void { $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); - $support_names = new SupportAgents; + $support_names = new SupportAgents(); - if(!$support_names->isExists($id)) + if (!$support_names->isExists($id)) { $this->template->mode = "edit"; + } $this->template->agent_id = $id; $this->template->mode = in_array($this->queryParam("act"), ["info", "edit"]) ? $this->queryParam("act") : "info"; - $this->template->agent = $support_names->get($id) ?? NULL; + $this->template->agent = $support_names->get($id) ?? null; $this->template->counters = [ - "all" => (new TicketComments)->getCountByAgent($id), - "good" => (new TicketComments)->getCountByAgent($id, 1), - "bad" => (new TicketComments)->getCountByAgent($id, 2) + "all" => (new TicketComments())->getCountByAgent($id), + "good" => (new TicketComments())->getCountByAgent($id, 1), + "bad" => (new TicketComments())->getCountByAgent($id, 2), ]; - if($id != $this->user->identity->getId()) - if ($support_names->isExists($id)) + if ($id != $this->user->identity->getId()) { + if ($support_names->isExists($id)) { $this->template->mode = "info"; - else + } else { $this->redirect("/support/agent" . $this->user->identity->getId()); + } + } } - function renderEditAgent(int $id): void + public function renderEditAgent(int $id): void { $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); $this->assertNoCSRF(); - $support_names = new SupportAgents; + $support_names = new SupportAgents(); $agent = $support_names->get($id); - if($agent) - if($agent->getAgentId() != $this->user->identity->getId()) $this->flashFail("err", tr("error"), tr("forbidden")); + if ($agent) { + if ($agent->getAgentId() != $this->user->identity->getId()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + } + + + $isNameEmpty = mb_strlen(trim($this->postParam("name"))) === 0; + $avatarUrl = mb_strlen(trim($this->postParam("avatar"))) > 0 ? $this->postParam("avatar") : "/assets/packages/static/openvk/img/support.jpeg"; if ($support_names->isExists($id)) { $agent = $support_names->get($id); - $agent->setName($this->postParam("name") ?? tr("helpdesk_agent")); - $agent->setNumerate((int) $this->postParam("number") ?? NULL); - $agent->setIcon($this->postParam("avatar")); + + if ($isNameEmpty) { + $agent->delete(false); + $this->redirect("/support/tickets"); + return; + } + + $agent->setName($this->postParam("name")); + $agent->setNumerate((int) $this->postParam("number") ?? null); + $agent->setIcon($avatarUrl); $agent->save(); $this->flashFail("succ", tr("agent_profile_edited")); } else { - $agent = new SupportAgent; + if ($isNameEmpty) { + $this->flashFail("err", tr("helpdesk_agent_name_empty")); + return; + } + + $agent = new SupportAgent(); $agent->setAgent($this->user->identity->getId()); - $agent->setName($this->postParam("name") ?? tr("helpdesk_agent")); - $agent->setNumerate((int) $this->postParam("number") ?? NULL); - $agent->setIcon($this->postParam("avatar")); + $agent->setName($this->postParam("name")); + $agent->setNumerate((int) $this->postParam("number") ?? null); + $agent->setIcon($avatarUrl); $agent->save(); $this->flashFail("succ", tr("agent_profile_created_1"), tr("agent_profile_created_2")); } } - function renderCloseTicket(int $id): void + public function renderCloseTicket(int $id): void { $this->assertUserLoggedIn(); $this->assertNoCSRF(); @@ -405,7 +517,7 @@ function renderCloseTicket(int $id): void $ticket = $this->tickets->get($id); - if($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { + if ($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { header("HTTP/1.1 403 Forbidden"); header("Location: /support/view/" . $id); exit; @@ -414,6 +526,13 @@ function renderCloseTicket(int $id): void $ticket->setType(2); $ticket->save(); + $helpdeskChat = OPENVK_ROOT_CONF["openvk"]["credentials"]["telegram"]["helpdeskChat"]; + if ($helpdeskChat) { + $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; + $telegramText = "🔒 Тикет под названием "{$ticket->getName()}" от {$ticket->getUser()->getCanonicalName()} ({$ticket->getUser()->getRegistrationIP()}) был закрыт автором.\n"; + Telegram::send($helpdeskChat, $telegramText); + } + $this->flashFail("succ", tr("ticket_changed"), tr("ticket_changed_comment")); } } diff --git a/Web/Presenters/ThemepacksPresenter.php b/Web/Presenters/ThemepacksPresenter.php index 37ababc91..c9ba2bd76 100644 --- a/Web/Presenters/ThemepacksPresenter.php +++ b/Web/Presenters/ThemepacksPresenter.php @@ -1,32 +1,39 @@ -notFound(); - else + } else { $theme = Themepacks::i()[$themepack]; - - if($resClass === "resource") { + } + + if ($resClass === "resource") { $data = $theme->fetchStaticResource(chandler_escape_url($resource)); - } else if($resClass === "stylesheet") { - if($resource !== "styles.css") + } elseif ($resClass === "stylesheet") { + if ($resource !== "styles.css") { $this->notFound(); - else + } else { $data = $theme->fetchStyleSheet(); + } } else { $this->notFound(); } - - if(!$data) + + if (!$data) { $this->notFound(); - + } + header("Content-Type: " . system_extension_mime_type($resource) ?? "text/plain; charset=unknown-8bit"); header("Content-Size: " . strlen($data)); header("Cache-Control: public, no-transform, max-age=31536000"); diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php index 92d67e841..da3784d14 100644 --- a/Web/Presenters/TopicsPresenter.php +++ b/Web/Presenters/TopicsPresenter.php @@ -1,7 +1,12 @@ -topics = $topics; $this->clubs = $clubs; - + parent::__construct(); } - function renderBoard(int $id): void + public function renderBoard(int $id): void { $this->assertUserLoggedIn(); $club = $this->clubs->get($id); - if(!$club) + if (!$club) { $this->notFound(); + } $this->template->club = $club; $page = (int) ($this->queryParam("p") ?? 1); $query = $this->queryParam("query"); - if($query) { + if ($query) { $results = $this->topics->find($club, $query); $this->template->topics = $results->page($page); $this->template->count = $results->size(); @@ -41,18 +47,21 @@ function renderBoard(int $id): void $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => $page, - "amount" => NULL, + "amount" => null, "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; } - function renderTopic(int $clubId, int $topicId): void + public function renderTopic(int $clubId, int $topicId): void { $this->assertUserLoggedIn(); $topic = $this->topics->getTopicById($clubId, $topicId); - if(!$topic) + if (!$topic) { $this->notFound(); + } $this->template->topic = $topic; $this->template->club = $topic->getClub(); @@ -61,63 +70,66 @@ function renderTopic(int $clubId, int $topicId): void $this->template->comments = iterator_to_array($topic->getComments($this->template->page)); } - function renderCreate(int $clubId): void + public function renderCreate(int $clubId): void { $this->assertUserLoggedIn(); $club = $this->clubs->get($clubId); - if(!$club) + if (!$club) { $this->notFound(); + } - if(!$club->isEveryoneCanCreateTopics() && !$club->canBeModifiedBy($this->user->identity)) + if (!$club->isEveryoneCanCreateTopics() && !$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); + } + - - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction(); $title = $this->postParam("title"); - if(!$title) + if (!$title) { $this->flashFail("err", tr("failed_to_create_topic"), tr("no_title_specified")); + } $flags = 0; - if($this->postParam("as_group") === "on" && $club->canBeModifiedBy($this->user->identity)) + if ($this->postParam("as_group") === "on" && $club->canBeModifiedBy($this->user->identity)) { $flags |= 0b10000000; + } - if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) + if ($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) { $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); + } - $topic = new Topic; + $topic = new Topic(); $topic->setGroup($club->getId()); $topic->setOwner($this->user->id); $topic->setTitle(ovk_proc_strtr($title, 127)); $topic->setCreated(time()); $topic->setFlags($flags); $topic->save(); - + # TODO move to trait try { - $photo = NULL; - $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 = null; + $video = null; + if ($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { + $album = null; + $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); } - - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { + + if ($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); } - } catch(ISE $ex) { + } catch (ISE $ex) { $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } - - if(!empty($this->postParam("text")) || $photo || $video) { + + if (!empty($this->postParam("text")) || $photo || $video) { try { - $comment = new Comment; + $comment = new Comment(); $comment->setOwner($this->user->id); $comment->setModel(get_class($topic)); $comment->setTarget($topic->getId()); @@ -129,12 +141,14 @@ function renderCreate(int $clubId): void $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } - - if(!is_null($photo)) + + if (!is_null($photo)) { $comment->attach($photo); - - if(!is_null($video)) + } + + if (!is_null($video)) { $comment->attach($video); + } } $this->redirect("/topic" . $topic->getPrettyId()); @@ -144,32 +158,37 @@ function renderCreate(int $clubId): void $this->template->graffiti = (bool) ovkGetQuirk("comments.allow-graffiti"); } - function renderEdit(int $clubId, int $topicId): void + public function renderEdit(int $clubId, int $topicId): void { $this->assertUserLoggedIn(); $topic = $this->topics->getTopicById($clubId, $topicId); - if(!$topic) + if (!$topic) { $this->notFound(); + } - if(!$topic->canBeModifiedBy($this->user->identity)) + if (!$topic->canBeModifiedBy($this->user->identity)) { $this->notFound(); + } - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction(); $title = $this->postParam("title"); - if(!$title) + if (!$title) { $this->flashFail("err", tr("failed_to_change_topic"), tr("no_title_specified")); + } $topic->setTitle(ovk_proc_strtr($title, 127)); $topic->setClosed(empty($this->postParam("close")) ? 0 : 1); - if($topic->getClub()->canBeModifiedBy($this->user->identity)) + if ($topic->getClub()->canBeModifiedBy($this->user->identity)) { + $topic->setRestricted((empty($this->postParam("restrict")) || !$topic->isPostedOnBehalfOfGroup()) ? 0 : 1); $topic->setPinned(empty($this->postParam("pin")) ? 0 : 1); + } $topic->save(); - + $this->flash("succ", tr("changes_saved"), tr("topic_changes_saved_comment")); $this->redirect("/topic" . $topic->getPrettyId()); } @@ -178,21 +197,23 @@ function renderEdit(int $clubId, int $topicId): void $this->template->club = $topic->getClub(); } - function renderDelete(int $clubId, int $topicId): void + public function renderDelete(int $clubId, int $topicId): void { $this->assertUserLoggedIn(); $this->assertNoCSRF(); $topic = $this->topics->getTopicById($clubId, $topicId); - if(!$topic) + if (!$topic) { $this->notFound(); + } - if(!$topic->canBeModifiedBy($this->user->identity)) + if (!$topic->canBeModifiedBy($this->user->identity)) { $this->notFound(); + } $this->willExecuteWriteAction(); $topic->deleteTopic(); - + $this->redirect("/board" . $topic->getClub()->getId()); } } diff --git a/Web/Presenters/UnknownTextRouteStrategyPresenter.php b/Web/Presenters/UnknownTextRouteStrategyPresenter.php index 004da043d..fe23986d2 100644 --- a/Web/Presenters/UnknownTextRouteStrategyPresenter.php +++ b/Web/Presenters/UnknownTextRouteStrategyPresenter.php @@ -1,20 +1,26 @@ -= 2) { - $user = (new Users)->getByShortURL($data); - if($user) + if (strlen($data) >= 2) { + $user = (new Users())->getByShortURL($data); + if ($user) { $this->pass("openvk!User->view", $user->getId()); - $club = (new Clubs)->getByShortURL($data); - if($club) + } + $club = (new Clubs())->getByShortURL($data); + if ($club) { $this->pass("openvk!Group->view", "public", $club->getId()); + } } - + $this->notFound(); } } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index e645f888a..7029e470d 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -1,11 +1,15 @@ -users = $users; parent::__construct(); } - - function renderView(int $id): void + + public function renderView(int $id): void { $user = $this->users->get($id); - if(!$user || $user->isDeleted()) { - if(!is_null($user) && $user->isDeactivated()) { - $this->template->_template = "User/deactivated.xml"; - + + if (!$user || $user->isDeleted() || !$user->canBeViewedBy($this->user->identity)) { + if (!is_null($user) && $user->isDeactivated()) { + $this->template->_template = "User/deactivated.latte"; + + $this->template->user = $user; + } elseif (!is_null($user) && $this->user->identity && $this->user->identity->isBlacklistedBy($user)) { + $this->template->_template = "User/blacklisted.latte"; + + $this->template->blacklist_status = $user->isBlacklistedBy($this->user->identity); + $this->template->ignore_status = $user->isIgnoredBy($this->user->identity); + $this->template->user = $user; + } elseif (!is_null($user) && $user->isBlacklistedBy($this->user->identity)) { + $this->template->_template = "User/blacklisted_pov.latte"; + + $this->template->ignore_status = $user->isIgnoredBy($this->user->identity); + $this->template->user = $user; + } elseif (!is_null($user) && !$user->canBeViewedBy($this->user->identity)) { + $this->template->_template = "User/private.latte"; + $this->template->user = $user; } else { - $this->template->_template = "User/deleted.xml"; + $this->template->_template = "User/deleted.latte"; } } else { - $this->template->albums = (new Albums)->getUserAlbums($user); - $this->template->avatarAlbum = (new Albums)->getUserAvatarAlbum($user); - $this->template->albumsCount = (new Albums)->getUserAlbumsCount($user); - $this->template->videos = (new Videos)->getByUser($user, 1, 2); - $this->template->videosCount = (new Videos)->getUserVideosCount($user); - $this->template->notes = (new Notes)->getUserNotes($user, 1, 4); - $this->template->notesCount = (new Notes)->getUserNotesCount($user); - + $this->template->avatarAlbum = (new Albums())->getUserAvatarAlbum($user); + $this->template->albums = array_values(array_filter(iterator_to_array((new Albums())->getUserAlbums($user)), function ($album) { + return !$album->isCreatedBySystem(); + })); + $this->template->albumsCount = count($this->template->albums); + $this->template->videos = (new Videos())->getByUser($user, 1, 2); + $this->template->videosCount = (new Videos())->getUserVideosCount($user); + $this->template->notes = (new Notes())->getUserNotes($user, 1, 4); + $this->template->notesCount = (new Notes())->getUserNotesCount($user); + $this->template->audios = (new Audios())->getRandomThreeAudiosByEntityId($user->getId()); + $this->template->audiosCount = (new Audios())->getUserCollectionSize($user); + $this->template->audioStatus = $user->getCurrentAudioStatus(); + $this->template->additionalFields = $user->getAdditionalFields(true); + $this->template->user = $user; + + if ($id !== $this->user->id) { + $this->template->ignore_status = $user->isIgnoredBy($this->user->identity); + $this->template->blacklist_status = $user->isBlacklistedBy($this->user->identity); + } } } - - function renderFriends(int $id): void + + public function renderFriends(int $id): void { $this->assertUserLoggedIn(); - + $user = $this->users->get($id); - $page = abs((int)($this->queryParam("p") ?? 1)); - if(!$user) + $page = abs((int) ($this->queryParam("p") ?? 1)); + if (!$user) { $this->notFound(); - elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL)) + } elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - else + } else { $this->template->user = $user; - + } + $this->template->mode = in_array($this->queryParam("act"), [ - "incoming", "outcoming", "friends" + "incoming", "outcoming", "friends", ]) ? $this->queryParam("act") : "friends"; $this->template->page = $page; - - if(!is_null($this->user)) { - if($this->template->mode !== "friends" && $this->user->id !== $id) { + + if (!is_null($this->user->identity)) { + if ($this->template->mode !== "friends" && $this->user->id !== $id) { $name = $user->getFullName(); $this->flash("err", tr("error_access_denied_short"), tr("error_viewing_subs", $name)); - + $this->redirect($user->getURL()); } } } - - function renderGroups(int $id): void + + public function renderGroups(int $id): void { $this->assertUserLoggedIn(); - + $user = $this->users->get($id); - if(!$user) + if (!$user) { $this->notFound(); - elseif (!$user->getPrivacyPermission('groups.read', $this->user->identity ?? NULL)) + } 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()) + } 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); @@ -98,227 +131,326 @@ function renderGroups(int $id): void } } - function renderPinClub(): void + public function renderPinClub(): void { $this->assertUserLoggedIn(); - $club = (new Clubs)->get((int) $this->queryParam("club")); - if(!$club) + $club = (new Clubs())->get((int) $this->queryParam("club")); + if (!$club) { $this->notFound(); + } - if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) - $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), NULL, true); + if (!$club->canBeModifiedBy($this->user->identity ?? null)) { + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), null, true); + } $isClubPinned = $this->user->identity->isClubPinned($club); - if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10) - $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), NULL, true); + if (!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10) { + $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), null, true); + } - if($club->getOwner()->getId() === $this->user->identity->getId()) { + if ($club->getOwner()->getId() === $this->user->identity->getId()) { $club->setOwner_Club_Pinned(!$isClubPinned); $club->save(); } else { $manager = $club->getManager($this->user->identity); - if(!is_null($manager)) { + if (!is_null($manager)) { $manager->setClub_Pinned(!$isClubPinned); $manager->save(); } } $this->returnJson([ - "success" => true + "success" => true, ]); } - - function renderEdit(): void + + public function renderEdit(): void { $this->assertUserLoggedIn(); - + $id = $this->user->id; #TODO: when ACL'll be done, allow admins to edit users via ?GUID=(chandler guid) - - if(!$id) + + if (!$id) { $this->notFound(); + } $user = $this->users->get($id); - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction($_GET['act'] === "status"); - - if($_GET['act'] === "main" || $_GET['act'] == NULL) { + + if ($_GET['act'] === "main" || $_GET['act'] == null) { try { $user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); $user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name")); - } catch(InvalidUserNameException $ex) { + } catch (InvalidUserNameException $ex) { $this->flashFail("err", tr("error"), tr("invalid_real_name")); } - - $user->setPseudo(empty($this->postParam("pseudo")) ? NULL : $this->postParam("pseudo")); - $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); - $user->setHometown(empty($this->postParam("hometown")) ? NULL : $this->postParam("hometown")); - - - if (strtotime($this->postParam("birthday")) < time()) - $user->setBirthday(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")); - - if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0) - $user->setPolit_Views($this->postParam("politViews")); - - if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) - $user->setSex($this->postParam("gender")); - - if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { - if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) + + $user->setPseudo(empty($this->postParam("pseudo")) ? null : $this->postParam("pseudo")); + $user->setStatus(empty($this->postParam("status")) ? null : $this->postParam("status")); + $user->setHometown(empty($this->postParam("hometown")) ? null : $this->postParam("hometown")); + + + if (strtotime($this->postParam("birthday")) < time()) { + $user->setBirthday(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) { + $maritalStatus = (int) $this->postParam("marialstatus"); + $user->setMarital_Status($maritalStatus); + + if (in_array($maritalStatus, [0, 1, 8], true)) { + $user->setMarital_Status_User(null); + } else { + $partnerAddress = trim((string) $this->postParam("maritalstatus-user")); + if (empty($partnerAddress)) { + $user->setMarital_Status_User(null); + } else { + $mUser = (new Users())->getByAddress($partnerAddress); + if ($mUser && $mUser->getId() !== $this->user->id) { + $user->setMarital_Status_User($mUser->getId()); + } + } + } + } + + if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0) { + $user->setPolit_Views($this->postParam("politViews")); + } + + if ($this->postParam("pronouns") <= 2 && $this->postParam("pronouns") >= 0) { + switch ($this->postParam("pronouns")) { + case '0': + $user->setSex(0); + break; + case '1': + $user->setSex(1); + break; + case '2': + $user->setSex(2); + break; + } + } + $user->setAudio_broadcast_enabled($this->checkbox("broadcast_music")); + + if (!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { + if (!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) { $this->flashFail("err", tr("error_segmentation"), "котлетки"); - + } + $code = $user->setPhoneWithVerification($this->postParam("phone")); - - if(!Sms::send($this->postParam("phone"), "OPENVK - Your verification code is: $code")) + + if (!Sms::send($this->postParam("phone"), "OPENVK - Your verification code is: $code")) { $this->flashFail("err", tr("error_segmentation"), "котлетки: Remote err!"); + } } - } elseif($_GET['act'] === "contacts") { - if(empty($this->postParam("email_contact")) || Validator::i()->emailValid($this->postParam("email_contact"))) - $user->setEmail_Contact(empty($this->postParam("email_contact")) ? NULL : $this->postParam("email_contact")); - else + } elseif ($_GET['act'] === "contacts") { + if (empty($this->postParam("email_contact")) || Validator::i()->emailValid($this->postParam("email_contact"))) { + $user->setEmail_Contact(empty($this->postParam("email_contact")) ? null : $this->postParam("email_contact")); + } else { $this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment")); + } $telegram = $this->postParam("telegram"); - if(empty($telegram) || Validator::i()->telegramValid($telegram)) - if(strpos($telegram, "t.me/") === 0) - $user->setTelegram(empty($telegram) ? NULL : substr($telegram, 5)); - else - $user->setTelegram(empty($telegram) ? NULL : ltrim($telegram, "@")); - else + if (empty($telegram) || Validator::i()->telegramValid($telegram)) { + if (strpos($telegram, "t.me/") === 0) { + $user->setTelegram(empty($telegram) ? null : substr($telegram, 5)); + } else { + $user->setTelegram(empty($telegram) ? null : ltrim($telegram, "@")); + } + } else { $this->flashFail("err", tr("invalid_telegram_name"), tr("invalid_telegram_name_comment")); + } + + $user->setCity(empty($this->postParam("city")) ? null : $this->postParam("city")); + $user->setAddress(empty($this->postParam("address")) ? null : $this->postParam("address")); - $user->setCity(empty($this->postParam("city")) ? NULL : $this->postParam("city")); - $user->setAddress(empty($this->postParam("address")) ? NULL : $this->postParam("address")); - $website = $this->postParam("website") ?? ""; - if(empty($website)) - $user->setWebsite(NULL); - else + if (empty($website)) { + $user->setWebsite(null); + } else { $user->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website); - } elseif($_GET['act'] === "interests") { - $user->setInterests(empty($this->postParam("interests")) ? NULL : ovk_proc_strtr($this->postParam("interests"), 300)); - $user->setFav_Music(empty($this->postParam("fav_music")) ? NULL : ovk_proc_strtr($this->postParam("fav_music"), 300)); - $user->setFav_Films(empty($this->postParam("fav_films")) ? NULL : ovk_proc_strtr($this->postParam("fav_films"), 300)); - $user->setFav_Shows(empty($this->postParam("fav_shows")) ? NULL : ovk_proc_strtr($this->postParam("fav_shows"), 300)); - $user->setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300)); - $user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300)); - $user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300)); - } elseif($_GET["act"] === "backdrop") { - if($this->postParam("subact") === "remove") { + } + } elseif ($_GET['act'] === "interests") { + $user->setInterests(empty($this->postParam("interests")) ? null : ovk_proc_strtr($this->postParam("interests"), 1000)); + $user->setFav_Music(empty($this->postParam("fav_music")) ? null : ovk_proc_strtr($this->postParam("fav_music"), 1000)); + $user->setFav_Films(empty($this->postParam("fav_films")) ? null : ovk_proc_strtr($this->postParam("fav_films"), 1000)); + $user->setFav_Shows(empty($this->postParam("fav_shows")) ? null : ovk_proc_strtr($this->postParam("fav_shows"), 1000)); + $user->setFav_Books(empty($this->postParam("fav_books")) ? null : ovk_proc_strtr($this->postParam("fav_books"), 1000)); + $user->setFav_Quote(empty($this->postParam("fav_quote")) ? null : ovk_proc_strtr($this->postParam("fav_quote"), 1000)); + $user->setFav_Games(empty($this->postParam("fav_games")) ? null : ovk_proc_strtr($this->postParam("fav_games"), 1000)); + $user->setAbout(empty($this->postParam("about")) ? null : ovk_proc_strtr($this->postParam("about"), 1000)); + } elseif ($_GET["act"] === "backdrop") { + if ($this->postParam("subact") === "remove") { $user->unsetBackDropPictures(); $user->save(); $this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit } - - $pic1 = $pic2 = NULL; + + $pic1 = $pic2 = null; try { - if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) + if ($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) { $pic1 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop1"]); - - if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) + } + + if ($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) { $pic2 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop2"]); - } catch(InvalidStateException $e) { + } + } catch (InvalidStateException $e) { $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); } - - if($pic1 == $pic2 && is_null($pic1)) + + if ($pic1 == $pic2 && is_null($pic1)) { $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); - + } + $user->setBackDropPictures($pic1, $pic2); $user->save(); $this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc")); - } elseif($_GET['act'] === "status") { - if(mb_strlen($this->postParam("status")) > 255) { + } elseif ($_GET['act'] === "status") { + if (mb_strlen($this->postParam("status")) > 255) { $statusLength = (string) mb_strlen($this->postParam("status")); - $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), NULL, true); + $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), null, true); } - $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setStatus(empty($this->postParam("status")) ? null : $this->postParam("status")); + $user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1); $user->save(); $this->returnJson([ - "success" => true + "success" => true, ]); + } elseif ($_GET['act'] === "additional") { + $maxAddFields = ovkGetQuirk("users.max-fields"); + $items = []; + + for ($i = 0; $i < $maxAddFields; $i++) { + if (!$this->postParam("name_" . $i)) { + continue; + } + + $items[] = [ + "name" => $this->postParam("name_" . $i), + "text" => $this->postParam("text_" . $i), + "place" => $this->postParam("place_" . $i), + ]; + } + + \openvk\Web\Models\Entities\UserInfoEntities\AdditionalField::resetByOwner($this->user->id); + foreach ($items as $new_field_info) { + $name = ovk_proc_strtr($new_field_info["name"], 50); + $text = ovk_proc_strtr($new_field_info["text"], 1000); + if (ctype_space($name) || ctype_space($text)) { + continue; + } + + $place = (int) ($new_field_info["place"]); + + $new_field = new \openvk\Web\Models\Entities\UserInfoEntities\AdditionalField(); + $new_field->setOwner($this->user->id); + $new_field->setName($name); + $new_field->setText($text); + $new_field->setPlace([0, 1][$place] ? $place : 0); + + $new_field->save(); + } } - + try { - $user->save(); - } catch(\PDOException $ex) { - if($ex->getCode() == 23000) + if ($_GET['act'] !== "additional") { + $user->save(); + } + } catch (\PDOException $ex) { + if ($ex->getCode() == 23000) { $this->flashFail("err", tr("error"), tr("error_shorturl")); - else + } else { throw $ex; + } } - + $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); } - + $this->template->mode = in_array($this->queryParam("act"), [ - "main", "contacts", "interests", "avatar", "backdrop" + "main", "contacts", "interests", "avatar", "backdrop", "additional", ]) ? $this->queryParam("act") : "main"; - + $this->template->user = $user; } - - function renderVerifyPhone(): void + + public function renderVerifyPhone(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $user = $this->user->identity; - if(!$user->hasPendingNumberChange()) + if (!$user->hasPendingNumberChange()) { exit; - else + } else { $this->template->change = $user->getPendingPhoneVerification(); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!$user->verifyNumber($this->postParam("code") ?? 0)) + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (!$user->verifyNumber($this->postParam("code") ?? 0)) { $this->flashFail("err", tr("error"), tr("invalid_code")); - + } + $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); } } - - function renderSub(): void + + public function renderSub(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - if($_SERVER["REQUEST_METHOD"] !== "POST") exit("Invalid state"); - + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + exit("Invalid state"); + } + $user = $this->users->get((int) $this->postParam("id")); - if(!$user) exit("Invalid state"); - - $user->toggleSubscription($this->user->identity); - - $this->redirect($user->getURL()); + if (!$user) { + exit("Invalid state"); + } + + if ($this->postParam("act") == "rej") { + $user->changeFlags($this->user->identity, 0b10000000, true); + } else { + if ($user->getSubscriptionStatus($this->user->identity) == \openvk\Web\Models\Entities\User::SUBSCRIPTION_ABSENT) { + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "friends.outgoing_sub")) { + $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); + } + } + + $user->toggleSubscription($this->user->identity); + } + + $this->redirect($_SERVER['HTTP_REFERER']); } - - function renderSetAvatar() + + public function renderSetAvatar() { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - $photo = new Photo; + + $photo = new Photo(); try { $photo->setOwner($this->user->id); $photo->setDescription("Profile image"); $photo->setFile($_FILES["blob"]); $photo->setCreated(time()); $photo->save(); - } catch(ISE $ex) { - $this->flashFail("err", tr("error"), tr("error_upload_failed")); + } catch (\Throwable $ex) { + $this->flashFail("err", tr("error"), tr("error_upload_failed"), null, (int) $this->postParam("ajax", true) == 1); } - - $album = (new Albums)->getUserAvatarAlbum($this->user->identity); + + $album = (new Albums())->getUserAvatarAlbum($this->user->identity); $album->addPhoto($photo); $album->setEdited(time()); $album->save(); @@ -326,80 +458,126 @@ function renderSetAvatar() $flags = 0; $flags |= 0b00010000; - $post = new Post; - $post->setOwner($this->user->id); - $post->setWall($this->user->id); - $post->setCreated(time()); - $post->setContent(""); - $post->setFlags($flags); - $post->save(); - $post->attach($photo); - if($this->postParam("ava", true) == (int)1) { + if ($this->postParam("on_wall") == 1) { + $post = new Post(); + $post->setOwner($this->user->id); + $post->setWall($this->user->id); + $post->setCreated(time()); + $post->setContent(""); + $post->setFlags($flags); + $post->save(); + + $post->attach($photo); + } + + if ((int) $this->postParam("ajax", true) == 1) { $this->returnJson([ - "url" => $photo->getURL(), - "id" => $photo->getPrettyId() + "success" => true, + "new_photo" => $photo->getPrettyId(), + "url" => $photo->getURL(), ]); } else { $this->flashFail("succ", tr("photo_saved"), tr("photo_saved_comment")); } } - - function renderSettings(): void + + public function renderDeleteAvatar() + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + $avatar = $this->user->identity->getAvatarPhoto(); + + if (!$avatar) { + $this->flashFail("succ", tr("error"), "no avatar bro", null, true); + } + + $avatar->isolate(); + + $newAvatar = $this->user->identity->getAvatarPhoto(); + + if (!$newAvatar) { + $this->returnJson([ + "success" => true, + "has_new_photo" => false, + "new_photo" => null, + "url" => "/assets/packages/static/openvk/img/camera_200.png", + ]); + } else { + $this->returnJson([ + "success" => true, + "has_new_photo" => true, + "new_photo" => $newAvatar->getPrettyId(), + "url" => $newAvatar->getURL(), + ]); + } + } + + public function renderSettings(): void { $this->assertUserLoggedIn(); - + $id = $this->user->id; #TODO: when ACL'll be done, allow admins to edit users via ?GUID=(chandler guid) - - if(!$id) + + if (!$id) { $this->notFound(); + } - if(in_array($this->queryParam("act"), ["finance", "finance.top-up"]) && !OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (in_array($this->queryParam("act"), ["finance", "finance.top-up"]) && !OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { $this->flashFail("err", tr("error"), tr("feature_disabled")); - + } + $user = $this->users->get($id); - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction(); - - if($_GET['act'] === "main" || $_GET['act'] == NULL) { - 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()) { + + if ($_GET['act'] === "main" || $_GET['act'] == null) { + 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("password_change_code"); - if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $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")); + } } - if(!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass"))) + if (!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass"))) { $this->flashFail("err", tr("error"), tr("error_old_password")); + } } else { $this->flashFail("err", tr("error"), tr("error_new_password")); } } - if($this->postParam("new_email")) { - if(!Validator::i()->emailValid($this->postParam("new_email"))) + 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)) + if (!Authenticator::verifyHash($this->postParam("email_change_pass"), $user->getChandlerUser()->getRaw()->passwordHash)) { $this->flashFail("err", tr("error"), tr("incorrect_password")); - - if($user->is2faEnabled()) { + } + + if ($user->is2faEnabled()) { $code = $this->postParam("email_change_code"); - if(!($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $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()) + 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 = new EmailChangeVerification(); $verification->setProfile($user->getId()); $verification->setNew_Email($this->postParam("new_email")); $verification->save(); - + $params = [ "key" => $verification->getKey(), "name" => $user->getCanonicalName(), @@ -407,18 +585,19 @@ function renderSettings(): void $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) { + } catch (UniqueConstraintViolationException $ex) { $this->flashFail("err", tr("error"), tr("user_already_exists")); - } + } } } - - if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc"))) + + 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") { + } + } elseif ($_GET['act'] === "privacy") { $settings = [ "page.read", "page.info.read", @@ -430,50 +609,65 @@ function renderSettings(): void "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ]; - foreach($settings as $setting) { + foreach ($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); - $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting)))); + $user->setPrivacySetting($setting, min(3, (int) abs((int) $input ?? $user->getPrivacySetting($setting)))); } - } else if($_GET['act'] === "finance.top-up") { + + $prof = $this->postParam("profile_type") == 1 || $this->postParam("profile_type") == 0 ? (int) $this->postParam("profile_type") : 0; + $user->setProfile_type($prof); + + } elseif ($_GET['act'] === "finance.top-up") { $token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3"); - $voucher = (new Vouchers)->getByToken($token); - if(!$voucher) + $voucher = (new Vouchers())->getByToken($token); + if (!$voucher) { $this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad")); - + } + $perm = $voucher->willUse($user); - if(!$perm) + if (!$perm) { $this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad")); - + } + $user->setCoins($user->getCoins() + $voucher->getCoins()); $user->setRating($user->getRating() + $voucher->getRating()); $user->save(); - + $this->flashFail("succ", tr("voucher_good"), tr("voucher_redeemed")); - } else if($_GET['act'] === "interface") { - if (isset(Themepacks::i()[$this->postParam("style")]) || $this->postParam("style") === Themepacks::DEFAULT_THEME_ID) - { - if ($this->postParam("theme_for_session") != "1") $user->setStyle($this->postParam("style")); - $this->setSessionTheme($this->postParam("style")); - } - - if ($this->postParam("style_avatar") <= 2 && $this->postParam("style_avatar") >= 0) - $user->setStyle_Avatar((int)$this->postParam("style_avatar")); - - if (in_array($this->postParam("rating"), [0, 1])) + } elseif ($_GET['act'] === "interface") { + if (isset(Themepacks::i()[$this->postParam("style")]) || $this->postParam("style") === Themepacks::DEFAULT_THEME_ID) { + if ($this->postParam("theme_for_session") != "1") { + $user->setStyle($this->postParam("style")); + } + $this->setSessionTheme($this->postParam("style")); + } + + if ($this->postParam("style_avatar") <= 2 && $this->postParam("style_avatar") >= 0) { + $user->setStyle_Avatar((int) $this->postParam("style_avatar")); + } + + if (in_array($this->postParam("rating"), [0, 1])) { $user->setShow_Rating((int) $this->postParam("rating")); + } - if (in_array($this->postParam("microblog"), [0, 1])) + if (in_array($this->postParam("microblog"), [0, 1])) { $user->setMicroblog((int) $this->postParam("microblog")); - - if(in_array($this->postParam("nsfw"), [0, 1, 2])) + } + + if (in_array($this->postParam("nsfw"), [0, 1, 2])) { $user->setNsfwTolerance((int) $this->postParam("nsfw")); + } - if(in_array($this->postParam("main_page"), [0, 1])) + if (in_array($this->postParam("main_page"), [0, 1])) { $user->setMain_Page((int) $this->postParam("main_page")); - } else if($_GET['act'] === "lMenu") { + } + } elseif ($_GET['act'] === "lMenu") { $settings = [ "menu_bildoj" => "photos", + "menu_muziko" => "audios", "menu_filmetoj" => "videos", "menu_mesagoj" => "messages", "menu_notatoj" => "notes", @@ -481,44 +675,63 @@ function renderSettings(): void "menu_novajoj" => "news", "menu_ligiloj" => "links", "menu_standardo" => "poster", - "menu_aplikoj" => "apps" + "menu_aplikoj" => "apps", + "menu_doxc" => "docs", + "menu_feva" => "fave", ]; - foreach($settings as $checkbox => $setting) + foreach ($settings as $checkbox => $setting) { $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); + } } - + try { $user->save(); - } catch(\PDOException $ex) { - if($ex->getCode() == 23000) + } catch (\PDOException $ex) { + if ($ex->getCode() == 23000) { $this->flashFail("err", tr("error"), tr("error_shorturl")); - else + } else { throw $ex; + } } - - $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); + + $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); } $this->template->mode = in_array($this->queryParam("act"), [ - "main", "security", "privacy", "finance", "finance.top-up", "interface" + "main", "security", "privacy", "finance", "finance.top-up", "interface", "blacklist", ]) ? $this->queryParam("act") : "main"; - if($this->template->mode == "finance") { + 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 + "imageTransparent" => false, ])))->render("ton://transfer/$address?text=$text")); $this->template->qrCodeType = substr($qrCode[0], 5); $this->template->qrCodeData = $qrCode[1]; + } elseif ($this->template->mode === "blacklist") { + $page = (int) ($this->queryParam('p') ?? 1); + $count = 10; + $offset = ($page - 1) * $count; + + $this->template->blSize = $this->user->identity->getBlacklistSize(); + $this->template->blItems = $this->user->identity->getBlacklist($offset, $count); + $this->template->paginatorConf = (object) [ + "count" => $this->template->blSize, + "page" => $page, + "amount" => sizeof($this->template->blItems), + "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, + ]; } - + $this->template->user = $user; $this->template->themes = Themepacks::i()->getThemeList(); } - function renderDeactivate(): void + public function renderDeactivate(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); @@ -527,10 +740,10 @@ function renderDeactivate(): void $reason = $this->postParam("deactivate_reason"); $share = $this->postParam("deactivate_share"); - if($share) { + if ($share) { $flags |= 0b00100000; - $post = new Post; + $post = new Post(); $post->setOwner($this->user->id); $post->setWall($this->user->id); $post->setCreated(time()); @@ -544,17 +757,18 @@ function renderDeactivate(): void $this->redirect("/"); } - function renderTwoFactorAuthSettings(): void + public function renderTwoFactorAuthSettings(): void { $this->assertUserLoggedIn(); - if($this->user->identity->is2faEnabled()) { - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) + if ($this->user->identity->is2faEnabled()) { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + if (!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) { $this->flashFail("err", tr("error"), tr("incorrect_password")); + } $this->user->identity->generate2faBackupCodes(); - $this->template->_template = "User/TwoFactorAuthCodes.xml"; + $this->template->_template = "User/TwoFactorAuthCodes.latte"; $this->template->codes = $this->user->identity->get2faBackupCodes(); return; } @@ -563,16 +777,17 @@ function renderTwoFactorAuthSettings(): void } $secret = Base32::encode(Totp::GenerateSecret(16)); - if($_SERVER["REQUEST_METHOD"] === "POST") { + if ($_SERVER["REQUEST_METHOD"] === "POST") { $this->willExecuteWriteAction(); - if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) + if (!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) { $this->flashFail("err", tr("error"), tr("incorrect_password")); + } $secret = $this->postParam("secret"); $code = $this->postParam("code"); - if($code === (new Totp)->GenerateToken(Base32::decode($secret))) { + if ($code === (new Totp())->GenerateToken(Base32::decode($secret))) { $this->user->identity->set2fa_secret($secret); $this->user->identity->save(); @@ -591,33 +806,34 @@ function renderTwoFactorAuthSettings(): void $issuer = OPENVK_ROOT_CONF["openvk"]["appearance"]["name"]; $email = $this->user->identity->getEmail(); $qrCode = explode("base64,", (new QRCode(new QROptions([ - "imageTransparent" => false + "imageTransparent" => false, ])))->render("otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer")); $this->template->qrCodeType = substr($qrCode[0], 5); $this->template->qrCodeData = $qrCode[1]; } - function renderDisableTwoFactorAuth(): void + public function renderDisableTwoFactorAuth(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) + if (!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash)) { $this->flashFail("err", tr("error"), tr("incorrect_password")); + } - $this->user->identity->set2fa_secret(NULL); + $this->user->identity->set2fa_secret(null); $this->user->identity->save(); $this->flashFail("succ", tr("information_-1"), tr("two_factor_authentication_disabled_message")); } - function renderResetThemepack(): void + public function renderResetThemepack(): void { $this->assertNoCSRF(); $this->setSessionTheme(Themepacks::DEFAULT_THEME_ID); - if($this->user) { + if ($this->user) { $this->willExecuteWriteAction(); $this->user->identity->setStyle(Themepacks::DEFAULT_THEME_ID); @@ -627,35 +843,41 @@ function renderResetThemepack(): void $this->redirect("/"); } - function renderCoinsTransfer(): void + public function renderCoinsTransfer(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { $this->flashFail("err", tr("error"), tr("feature_disabled")); + } $receiverAddress = $this->postParam("receiver"); $value = (int) $this->postParam("value"); $message = $this->postParam("message"); - if(!$receiverAddress || !$value) + if (!$receiverAddress || !$value) { $this->flashFail("err", tr("failed_to_tranfer_points"), tr("not_all_information_has_been_entered")); + } - if($value < 0) + if ($value < 0) { $this->flashFail("err", tr("failed_to_tranfer_points"), tr("negative_transfer_value")); + } - if(iconv_strlen($message) > 255) + if (iconv_strlen($message) > 255) { $this->flashFail("err", tr("failed_to_tranfer_points"), tr("message_is_too_long")); + } $receiver = $this->users->getByAddress($receiverAddress); - if(!$receiver) + if (!$receiver || !$receiver->canBeViewedBy($this->user->identity)) { $this->flashFail("err", tr("failed_to_tranfer_points"), tr("receiver_not_found")); + } - if($this->user->identity->getCoins() < $value) + if ($this->user->identity->getCoins() < $value) { $this->flashFail("err", tr("failed_to_tranfer_points"), tr("you_dont_have_enough_points")); + } - if($this->user->id !== $receiver->getId()) { + if ($this->user->id !== $receiver->getId()) { $this->user->identity->setCoins($this->user->identity->getCoins() - $value); $this->user->identity->save(); @@ -668,33 +890,39 @@ function renderCoinsTransfer(): void $this->flashFail("succ", tr("information_-1"), tr("points_transfer_successful", tr("points_amount", $value), $receiver->getURL(), htmlentities($receiver->getCanonicalName()))); } - function renderIncreaseRating(): void + public function renderIncreaseRating(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) { $this->flashFail("err", tr("error"), tr("feature_disabled")); + } $receiverAddress = $this->postParam("receiver"); $value = (int) $this->postParam("value"); $message = $this->postParam("message"); - if(!$receiverAddress || !$value) + if (!$receiverAddress || !$value) { $this->flashFail("err", tr("failed_to_increase_rating"), tr("not_all_information_has_been_entered")); + } - if($value < 0) + if ($value < 0) { $this->flashFail("err", tr("failed_to_increase_rating"), tr("negative_rating_value")); + } - if(iconv_strlen($message) > 255) + if (iconv_strlen($message) > 255) { $this->flashFail("err", tr("failed_to_increase_rating"), tr("message_is_too_long")); + } $receiver = $this->users->getByAddress($receiverAddress); - if(!$receiver) + if (!$receiver) { $this->flashFail("err", tr("failed_to_increase_rating"), tr("receiver_not_found")); + } - if($this->user->identity->getCoins() < $value) + if ($this->user->identity->getCoins() < $value) { $this->flashFail("err", tr("failed_to_increase_rating"), tr("you_dont_have_enough_points")); + } $this->user->identity->setCoins($this->user->identity->getCoins() - $value); $this->user->identity->save(); @@ -702,16 +930,17 @@ function renderIncreaseRating(): void $receiver->setRating($receiver->getRating() + $value); $receiver->save(); - if($this->user->id !== $receiver->getId()) + if ($this->user->id !== $receiver->getId()) { (new RatingUpNotification($receiver, $this->user->identity, $value, $message))->emit(); + } $this->flashFail("succ", tr("information_-1"), tr("rating_increase_successful", $receiver->getURL(), htmlentities($receiver->getCanonicalName()), $value)); } - function renderEmailChangeFinish(): void + public function renderEmailChangeFinish(): void { - $request = (new EmailChangeVerifications)->getByToken(str_replace(" ", "+", $this->queryParam("key"))); - if(!$request || !$request->isStillValid()) { + $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 { @@ -719,7 +948,7 @@ function renderEmailChangeFinish(): void try { $request->getUser()->changeEmail($request->getNewEmail()); - } catch(UniqueConstraintViolationException $ex) { + } catch (UniqueConstraintViolationException $ex) { $this->flashFail("err", tr("error"), tr("user_already_exists")); } @@ -727,4 +956,66 @@ function renderEmailChangeFinish(): void $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, + "atTop" => 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; + } } diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 963c9cccb..57a934417 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -1,5 +1,9 @@ - $code, "error_msg" => $message, @@ -37,59 +42,62 @@ private function fail(int $code, string $message, string $object, string $method ], ], ]; - - foreach($_GET as $key => $value) + + foreach ($_GET as $key => $value) { array_unshift($payload["request_params"], [ "key" => $key, "value" => $value ]); - + } + exit(json_encode($payload)); } - private function twofaFail(int $userId): void + private function twofaFail(int $userId, string $data): 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", + "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" + "redirect_uri" => ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/2fa?data=" . base64_encode($data), + "validation_resend" => "nowhere", ]; - + exit(json_encode($payload)); } - + private function badMethod(string $object, string $method): void { $this->fail(3, "Unknown method passed.", $object, $method); } - + private function badMethodCall(string $object, string $method, string $param): void { $this->fail(100, "Required parameter '$param' missing.", $object, $method); } - - function onStartup(): void + + public function onStartup(): void { parent::onStartup(); - + # idk, but in case we will ever support non-standard HTTP credential authflow $origin = "*"; - if(isset($_SERVER["HTTP_REFERER"])) { + if (isset($_SERVER["HTTP_REFERER"])) { $refOrigin = parse_url($_SERVER["HTTP_REFERER"], PHP_URL_SCHEME) . "://" . parse_url($_SERVER["HTTP_REFERER"], PHP_URL_HOST); - if($refOrigin !== false) + if ($refOrigin !== false) { $origin = $refOrigin; + } } - - if(!is_null($this->queryParam("requestPort"))) + + if (!is_null($this->queryParam("requestPort"))) { $origin .= ":" . ((int) $this->queryParam("requestPort")); - + } + header("Access-Control-Allow-Origin: $origin"); - - if($_SERVER["REQUEST_METHOD"] === "OPTIONS") { + + if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") { header("Access-Control-Allow-Methods: POST, PUT, DELETE"); header("Access-Control-Allow-Headers: " . $_SERVER["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"]); header("Access-Control-Max-Age: -1"); @@ -97,18 +105,18 @@ function onStartup(): void } } - function renderPhotoUpload(string $signature): void + public function renderPhotoUpload(string $signature): void { $secret = CHANDLER_ROOT_CONF["security"]["secret"]; $queryString = rawurldecode($_SERVER["QUERY_STRING"]); $computedSignature = hash_hmac("sha3-224", $queryString, $secret); - if(!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) { + if (!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) { header("HTTP/1.1 422 Unprocessable Entity"); exit("Try harder <3"); } $data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($queryString)); - if((time() - $data["TIME"]) > 600) { + if ((time() - $data["TIME"]) > 600) { header("HTTP/1.1 422 Unprocessable Entity"); exit("Expired"); } @@ -117,21 +125,27 @@ function renderPhotoUpload(string $signature): void $maxSize = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFileSize"]; $maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"]; $usrFiles = sizeof(glob("$folder/$data[USER]_*.oct")); - if($usrFiles >= $maxFiles) { + if ($usrFiles >= $maxFiles) { + $pendingInfo = $this->getPendingUploadInfo($folder, $data["USER"]); header("HTTP/1.1 507 Insufficient Storage"); - exit("There are $maxFiles pending already. Please save them before uploading more :3"); + header("Content-Type: application/json"); + exit(json_encode([ + "error" => "insufficient_storage", + "error_description" => "There are $maxFiles pending already. Please save them before uploading more :3", + "pending_uploads" => $pendingInfo, + ])); } # Not multifile - if($data["MF"] === 0) { + if ($data["MF"] === 0) { $file = $_FILES[$data["FIELD"]]; - if(!$file) { + if (!$file) { header("HTTP/1.0 400"); exit("No file"); - } else if($file["error"] != UPLOAD_ERR_OK) { + } elseif ($file["error"] != UPLOAD_ERR_OK) { header("HTTP/1.0 500"); exit("File could not be consumed"); - } else if($file["size"] > $maxSize) { + } elseif ($file["size"] > $maxSize) { header("HTTP/1.0 507 Insufficient Storage"); exit("File is too big"); } @@ -148,30 +162,38 @@ function renderPhotoUpload(string $signature): void } $files = []; - for($i = 1; $i <= 5; $i++) { - $file = $_FILES[$data["FIELD"] . $i] ?? NULL; + for ($i = 1; $i <= 5; $i++) { + $file = $_FILES[$data["FIELD"] . $i] ?? null; if (!$file || $file["error"] != UPLOAD_ERR_OK || $file["size"] > $maxSize) { continue; - } else if((sizeof($files) + $usrFiles) > $maxFiles) { + } elseif ((sizeof($files) + $usrFiles) > $maxFiles) { # Clear uploaded files since they can't be saved anyway - foreach($files as $f) + foreach ($files as $f) { unlink($f); + } + $pendingInfo = $this->getPendingUploadInfo($folder, $data["USER"]); header("HTTP/1.1 507 Insufficient Storage"); - exit("There are $maxFiles pending already. Please save them before uploading more :3"); + header("Content-Type: application/json"); + exit(json_encode([ + "error" => "insufficient_storage", + "error_description" => "There are $maxFiles pending already. Please save them before uploading more :3", + "pending_uploads" => $pendingInfo, + ])); } $files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct"); } - if(sizeof($files) === 0) { + if (sizeof($files) === 0) { header("HTTP/1.0 400"); exit("No file"); } $filesManifest = []; - foreach($files as $id => $file) + foreach ($files as $id => $file) { $filesManifest[] = ["keyholder" => $data["USER"], "resource" => $id, "club" => $data["GROUP"]]; + } $filesManifest = json_encode($filesManifest); $manifestHash = hash_hmac("sha3-224", $filesManifest, $secret); @@ -183,125 +205,436 @@ function renderPhotoUpload(string $signature): void "hash" => $manifestHash, ])); } - - function renderRoute(string $object, string $method): void + + private function getPendingUploadInfo(string $folder, string $userId): array + { + $pendingFiles = glob("$folder/$userId" . "_*.oct"); + $pendingInfo = []; + + foreach ($pendingFiles as $file) { + $filename = basename($file); + $uploadId = str_replace([$userId . "_", ".oct"], "", $filename); + $fileTime = filemtime($file); + $fileSize = filesize($file); + $ageHours = round((time() - $fileTime) / 3600, 1); + + $pendingInfo[] = [ + "upload_id" => $uploadId, + "filename" => $filename, + "size" => $fileSize, + "age_hours" => $ageHours, + "uploaded_at" => date("Y-m-d H:i:s", $fileTime), + ]; + } + + return $pendingInfo; + } + + /** + * Resolves the calling identity (and client platform) from the request, exactly as the + * normal API entrypoint does. On authorization problems it emits an error and exits. + * + * @return array{0: ?User, 1: ?string} [identity, platform] + */ + private function resolveIdentity(string $object, string $method): array { $authMechanism = $this->queryParam("auth_mechanism") ?? "token"; - if($authMechanism === "roaming") { - if(!$this->user->identity) + if ($authMechanism === "roaming") { + if ($this->queryParam("callback")) { + $this->fail(-1, "User authorization failed: roaming mechanism is unavailable with jsonp.", $object, $method); + } + + if (!$this->user->identity) { $this->fail(5, "User authorization failed: roaming mechanism is selected, but user is not logged in.", $object, $method); - else - $identity = $this->user->identity; + } + + $identity = $this->user->identity; + $platform = null; } else { - if(is_null($this->requestParam("access_token"))) { - $identity = NULL; - } else { - $token = (new APITokens)->getByCode($this->requestParam("access_token")); - if(!$token) { - $identity = NULL; - } else { + $identity = null; + $platform = null; + if (!is_null($this->requestParam("access_token"))) { + $token = (new APITokens())->getByCode($this->requestParam("access_token")); + if ($token) { + $identity = $token->getUser(); + $platform = $token->getPlatform(); + } + } elseif (!is_null($_SERVER['HTTP_AUTHORIZATION'])) { + $token = str_replace('Bearer ', '', $_SERVER['HTTP_AUTHORIZATION']); + $token = (new APITokens())->getByCode($token); + if ($token) { $identity = $token->getUser(); $platform = $token->getPlatform(); } } } - - if(!is_null($identity) && $identity->isBanned()) + + if (!is_null($identity) && ($identity->isBanned() || $identity->isDeleted())) { $this->fail(18, "User account is deactivated", $object, $method); - + } + + return [$identity, $platform]; + } + + /** + * Instantiates the handler for $object, binds $params (name => value) to the target + * method's signature and invokes it, returning the raw result. Reused by both the normal + * API entrypoint and the `execute` method. Errors are thrown as APIErrorException + * (unknown method => 3, missing required param => 100) rather than emitted directly. + * + * @param array $params + */ + private function callAPIMethod(string $object, string $method, array $params, $identity, $platform, ?bool &$hasRss = null) + { $object = ucfirst(strtolower($object)); $handlerClass = "openvk\\VKAPI\\Handlers\\$object"; - if(!class_exists($handlerClass)) - $this->badMethod($object, $method); - + if (!class_exists($handlerClass)) { + throw new APIErrorException("Unknown method passed.", 3); + } + $handler = new $handlerClass($identity, $platform); - if(!is_callable([$handler, $method])) - $this->badMethod($object, $method); - + if (!is_callable([$handler, $method])) { + throw new APIErrorException("Unknown method passed.", 3); + } + + $hasRss = false; $route = new \ReflectionMethod($handler, $method); - $params = []; - foreach($route->getParameters() as $parameter) { - $val = $this->requestParam($parameter->getName()); - if(is_null($val)) { - if($parameter->allowsNull()) - $val = NULL; - else if($parameter->isDefaultValueAvailable()) + $args = []; + foreach ($route->getParameters() as $parameter) { + if ($parameter->getName() == 'rss') { + $hasRss = true; + } + + $val = $params[$parameter->getName()] ?? null; + if (is_null($val)) { + if ($parameter->allowsNull()) { + $val = null; + } elseif ($parameter->isDefaultValueAvailable()) { $val = $parameter->getDefaultValue(); - else if($parameter->isOptional()) - $val = NULL; - else - $this->badMethodCall($object, $method, $parameter->getName()); + } elseif ($parameter->isOptional()) { + $val = null; + } else { + throw new APIErrorException("Required parameter '" . $parameter->getName() . "' missing.", 100); + } } - + try { - settype($val, $parameter->getType()->getName()); - $params[] = $val; + // Проверка типа параметра + $type = $parameter->getType(); + if (($type && !$type->isBuiltin()) || is_null($val)) { + $args[] = $val; + } else { + settype($val, $parameter->getType()->getName()); + $args[] = $val; + } } catch (\Throwable $e) { // Just ignore the exception, since // some args are intended for internal use } } - - define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); - + + if (!defined("VKAPI_DECL_VER")) { + $version = $this->requestParam("v") ?? "5.9999"; // 9999 for ovk apps + define("VKAPI_DECL_VER", $version); + define("VKAPI_DECL_VER_MAJOR", intval(explode('.', $version)[0] ?? "5")); + define("VKAPI_DECL_VER_MINOR", intval(explode('.', $version)[1] ?? "100")); + } + + return $handler->{$method}(...$args); + } + + public function renderRoute(string $object, string $method): void + { + $callback = $this->queryParam("callback"); + [$identity, $platform] = $this->resolveIdentity($object, $method); + + $has_rss = false; try { - $res = $handler->{$method}(...$params); - } catch(APIErrorException $ex) { + $res = $this->callAPIMethod($object, $method, $_REQUEST, $identity, $platform, $has_rss); + } catch (APIErrorException $ex) { $this->fail($ex->getCode(), $ex->getMessage(), $object, $method); } - - $result = json_encode([ - "response" => $res, - ]); - + + $result = null; + + if ($this->queryParam("rss") == '1' && $has_rss) { + $feed = new \Bhaktaraz\RSSGenerator\Feed(); + $res->appendTo($feed); + + $result = strval($feed); + + header("Content-Type: application/rss+xml;charset=UTF-8"); + } else { + $result = json_encode([ + "response" => $res, + ]); + + if ($callback) { + $result = $callback . '(' . $result . ')'; + header('Content-Type: application/javascript'); + } else { + header("Content-Type: application/json"); + } + } + + $size = strlen($result); + header("Content-Length: $size"); + + exit($result); + } + + public function renderExecute(): void + { + $callback = $this->queryParam("callback"); + [$identity, $platform] = $this->resolveIdentity("execute", ""); + + $code = $this->requestParam("code"); + if (is_null($code)) { + $this->fail(100, "Required parameter 'code' missing.", "execute", ""); + } + + // Everything except the reserved keys is exposed to the script via Args. + $reserved = ["code", "access_token", "v", "callback", "auth_mechanism", "requestPort"]; + $args = []; + foreach ($_REQUEST as $key => $value) { + if (!in_array($key, $reserved, true)) { + $args[$key] = $value; + } + } + + try { + $tokens = (new \openvk\VKAPI\VKScript\Lexer($code))->tokenize(); + $ast = (new \openvk\VKAPI\VKScript\Parser($tokens))->parse(); + + $interpreter = new \openvk\VKAPI\VKScript\Interpreter( + function (string $object, string $method, array $params) use ($identity, $platform) { + return $this->callAPIMethod($object, $method, $params, $identity, $platform); + }, + $args + ); + + $res = $interpreter->run($ast); + $errors = $interpreter->getExecuteErrors(); + } catch (APIErrorException $ex) { + $this->fail($ex->getCode(), $ex->getMessage(), "execute", ""); + } + + $payload = ["response" => $res]; + if (!empty($errors)) { + $payload["execute_errors"] = $errors; + } + + $result = json_encode($payload); + if ($callback) { + $result = $callback . '(' . $result . ')'; + header('Content-Type: application/javascript'); + } else { + header("Content-Type: application/json"); + } + $size = strlen($result); - header("Content-Type: application/json"); header("Content-Length: $size"); + exit($result); } - - function renderTokenLogin(): void + + public function renderTokenLogin(): void { - if($this->requestParam("grant_type") !== "password") + if ($this->requestParam("grant_type") !== "password") { $this->fail(7, "Invalid grant type", "internal", "acquireToken"); - else if(is_null($this->requestParam("username")) || is_null($this->requestParam("password"))) + } elseif (is_null($this->requestParam("username")) || is_null($this->requestParam("password"))) { $this->fail(100, "Password and username not passed", "internal", "acquireToken"); - + } + $chUser = DB::i()->getContext()->table("ChandlerUsers")->where("login", $this->requestParam("username"))->fetch(); - if(!$chUser) + if (!$chUser) { $this->fail(28, "Invalid username or password", "internal", "acquireToken"); - + } + $auth = Authenticator::i(); - if(!$auth->verifyCredentials($chUser->id, $this->requestParam("password"))) + if (!$auth->verifyCredentials($chUser->id, $this->requestParam("password"))) { $this->fail(28, "Invalid username or password", "internal", "acquireToken"); - + } + $uId = $chUser->related("profiles.user")->fetch()->id; - $user = (new Users)->get($uId); + $user = (new Users())->get($uId); + + $platform = $this->requestParam("client_name"); + $platform ??= $this->resolveAppIdToString($this->requestParam("client_id")); $code = $this->requestParam("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 + if ($user->is2faEnabled() && !($code === (new Totp())->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code))) { + if (empty($code)) { + $data = (object) [ + "login" => $this->requestParam("username"), + "password" => $this->requestParam("password"), + "client_name" => $platform, + ]; + $this->twofaFail($user->getId(), json_encode($data)); + } else { $this->fail(28, "Invalid 2FA code", "internal", "acquireToken"); + } + } + + $token = null; + $tokenIsStale = true; + $acceptsStale = $this->requestParam("accepts_stale"); + if ($acceptsStale == "1") { + if (is_null($platform)) { + $this->fail(101, "accepts_stale can only be used with explicitly set client_name", "internal", "acquireToken"); + } + + $token = (new APITokens())->getStaleByUser($uId, $platform); + } + + if (is_null($token)) { + $tokenIsStale = false; + + $token = new APIToken(); + $token->setUser($user); + $token->setPlatform($platform ?? (new WhichBrowser\Parser(getallheaders()))->toString()); + $token->save(); } - - $platform = $this->requestParam("client_name"); - $token = new APIToken; - $token->setUser($user); - $token->setPlatform($platform ?? (new WhichBrowser\Parser(getallheaders()))->toString()); - $token->save(); - $payload = json_encode([ "access_token" => $token->getFormattedToken(), "expires_in" => 0, "user_id" => $uId, + "is_stale" => $tokenIsStale, + "secret" => "super_secret_value", ]); - + $size = strlen($payload); header("Content-Type: application/json"); header("Content-Length: $size"); exit($payload); } + + public function renderOAuthLogin() + { + $this->assertUserLoggedIn(); + + $client = $this->queryParam("client_name"); + $postmsg = $this->queryParam("prefers_postMessage") ?? '0'; + $stale = $this->queryParam("accepts_stale") ?? '0'; + $origin = null; + $url = $this->queryParam("redirect_uri"); + $responseType = $this->queryParam("response_type") ?? 'php'; + + if (!empty($this->queryParam("client_id")) && empty($client)) { + $client = $this->resolveAppIdToString($this->queryParam("client_id")); + } + + if (is_null($url) || is_null($client)) { + exit("Error: redirect_uri and client_name (or client_id) params are required."); + } + + if ($url != "about:blank") { + if (!filter_var($url, FILTER_VALIDATE_URL)) { + exit("Error: Invalid URL passed to redirect_uri."); + } + + $parsedUrl = (object) parse_url($url); + if ($parsedUrl->scheme != 'https' && $parsedUrl->scheme != 'http') { + exit("Error: redirect_uri should either point to about:blank or to a web resource."); + } + + $origin = "$parsedUrl->scheme://$parsedUrl->host"; + if (!is_null($parsedUrl->port ?? null)) { + $origin .= ":$parsedUrl->port"; + } + + $url .= strpos($url, '?') === false ? '?' : '&'; + } else { + $url .= "#"; + if ($postmsg == '1') { + exit("Error: prefers_postMessage can only be set if redirect_uri is not about:blank"); + } + } + + if (!in_array($responseType, ['php', 'token'])) { + exit("Error: response_type can equal 'php' or 'token' only."); + } + + $this->template->clientName = $client; + $this->template->usePostMessage = $postmsg == '1'; + $this->template->acceptsStale = $stale == '1'; + $this->template->origin = $origin; + $this->template->redirectUri = $url; + $this->template->responseType = $responseType; + } + + public function renderTwoFactorLogin() + { + $base64 = $this->requestParam("data"); + if (empty($base64)) { + exit("Error: Empty request."); + } + + $decoded = base64_decode($base64); + + if ($decoded == false) { + exit("Error: Invalid base64 data."); + } + + $parsed = json_decode($decoded); + + if (!is_array($parsed) && empty($parsed->login) && empty($parsed->password) && empty($parsed->client_name)) { + exit("Error: Invalid login data."); + } + + $chUser = DB::i()->getContext()->table("ChandlerUsers")->where("login", $parsed->login)->fetch(); + if (!$chUser) { + exit("Error: Invalid login and password."); + } + + $auth = Authenticator::i(); + if (!$auth->verifyCredentials($chUser->id, $parsed->password)) { + exit("Error: Invalid login and password."); + } + + $uId = $chUser->related("profiles.user")->fetch()->id; + $user = (new Users())->get($uId); + $platform = $parsed->client_name; + + $this->template->base64 = $base64; + $this->template->platform = $platform; + + $code = $this->requestParam("code"); + if ($user->is2faEnabled() && empty($code)) { + // intended + } elseif ($user->is2faEnabled() && !empty($code)) { + if ($code === (new Totp())->GenerateToken(Base32::decode($user->get2faSecret())) || !empty($user->use2faBackupCode((int) $code))) { + $token = new APIToken(); + $token->setUser($user); + $token->setPlatform($platform ?? "api"); // since this is a browser we will just throw "api" + $token->save(); + $this->redirect('/blank.html#access_token=' . $token->getFormattedToken() . '&expires_in=0&user_id=' . $uId); + } else { + $this->flashFail("err", tr('incorrect_code'), tr('incorrect_2fa_code')); + } + } else { + $token = new APIToken(); + $token->setUser($user); + $token->setPlatform($platform ?? "api"); + $token->save(); + $this->redirect('/blank.html#access_token=' . $token->getFormattedToken() . '&expires_in=0&user_id=' . $uId); + } + } + + private function resolveAppIdToString(?string $id = ""): ?string + { + switch ($id) { + case '4083558': + return "VFeed"; + case '2685278': + return "Kate Mobile"; + case '3680547': + return "VK for iOS"; + case '2274003': + return "VK for Android"; + default: + return "unknown"; + } + } } diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php index 9d2fddc6a..8bad65c6f 100644 --- a/Web/Presenters/VideosPresenter.php +++ b/Web/Presenters/VideosPresenter.php @@ -1,5 +1,9 @@ -videos = $videos; $this->users = $users; - + parent::__construct(); } - - function renderList(int $id): void + + public function renderList(int $id): void { $user = $this->users->get($id); - if(!$user) $this->notFound(); - if(!$user->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) + if (!$user) { + $this->notFound(); + } + if (!$user->getPrivacyPermission('videos.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - + } + $this->template->user = $user; $this->template->videos = $this->videos->getByUser($user, (int) ($this->queryParam("p") ?? 1)); $this->template->count = $this->videos->getUserVideosCount($user); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($this->queryParam("p") ?? 1), - "amount" => NULL, + "amount" => null, "perPage" => 7, + "tidy" => false, + "atTop" => false, ]; } - - function renderView(int $owner, int $vId): void + + public function renderView(int $owner, int $vId): void { $user = $this->users->get($owner); - if(!$user) $this->notFound(); - if(!$user->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) + $video = $this->videos->getByOwnerAndVID($owner, $vId); + + if (!$user) { + $this->notFound(); + } + if (!$video || $video->isDeleted()) { + $this->notFound(); + } + if (!$user->getPrivacyPermission('videos.read', $this->user->identity ?? null)) { $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + } - if($this->videos->getByOwnerAndVID($owner, $vId)->isDeleted()) $this->notFound(); - $this->template->user = $user; $this->template->video = $this->videos->getByOwnerAndVID($owner, $vId); $this->template->cCount = $this->template->video->getCommentsCount(); $this->template->cPage = (int) ($this->queryParam("p") ?? 1); $this->template->comments = iterator_to_array($this->template->video->getComments($this->template->cPage)); } - - function renderUpload(): void + + public function renderUpload(): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) + if (OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) { $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!empty($this->postParam("name"))) { - $video = new Video; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $is_ajax = (int) ($this->postParam('ajax') ?? '0') == 1; + if (!empty($this->postParam("name"))) { + $video = new Video(); $video->setOwner($this->user->id); $video->setName(ovk_proc_strtr($this->postParam("name"), 61)); $video->setDescription(ovk_proc_strtr($this->postParam("desc"), 300)); $video->setCreated(time()); - + try { - if(isset($_FILES["blob"]) && file_exists($_FILES["blob"]["tmp_name"])) + if (isset($_FILES["blob"]) && file_exists($_FILES["blob"]["tmp_name"])) { $video->setFile($_FILES["blob"]); - else if(!empty($this->postParam("link"))) + } elseif (!empty($this->postParam("link"))) { $video->setLink($this->postParam("link")); - else - $this->flashFail("err", tr("no_video"), tr("no_video_desc")); - } catch(\DomainException $ex) { - $this->flashFail("err", tr("error_occured"), tr("error_video_damaged_file")); - } catch(ISE $ex) { - $this->flashFail("err", tr("error_occured"), tr("error_video_incorrect_link")); + } else { + $this->flashFail("err", tr("no_video_error"), tr("no_video_description"), 10, $is_ajax); + } + } catch (\DomainException $ex) { + $this->flashFail("err", tr("error_video"), tr("file_corrupted"), 10, $is_ajax); + } catch (ISE $ex) { + $this->flashFail("err", tr("error_video"), tr("link_incorrect"), 10, $is_ajax); } - + + if ((int) ($this->postParam("unlisted") ?? '0') == 1) { + $video->setUnlisted(true); + } + $video->save(); - + + if ($is_ajax) { + $object = $video->getApiStructure(); + $this->returnJson([ + 'payload' => $object->video, + ]); + } + $this->redirect("/video" . $video->getPrettyId()); } else { - $this->flashFail("err", tr("error_occured"), tr("error_video_no_title")); + $this->flashFail("err", tr("error_video"), tr("no_name_error"), 10, $is_ajax); } } } - - function renderEdit(int $owner, int $vId): void + + public function renderEdit(int $owner, int $vId): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $video = $this->videos->getByOwnerAndVID($owner, $vId); - if(!$video) + if (!$video) { $this->notFound(); - if(is_null($this->user) || $this->user->id !== $owner) - $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - - if($_SERVER["REQUEST_METHOD"] === "POST") { - $video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name")); - $video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); + } + if (is_null($this->user->identity) || $this->user->id !== $owner) { + $this->flashFail("err", tr("access_denied_error"), tr("access_denied_error_description")); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $video->setName(empty($this->postParam("name")) ? null : $this->postParam("name")); + $video->setDescription(empty($this->postParam("desc")) ? null : $this->postParam("desc")); + $video->setUnlisted(false); $video->save(); - - $this->flash("succ", tr("changes_saved"), tr("new_data_video")); + + $this->flash("succ", tr("changes_saved"), tr("changes_saved_video_comment")); $this->redirect("/video" . $video->getPrettyId()); - } - + } + $this->template->video = $video; } - function renderRemove(int $owner, int $vid): void + public function renderRemove(int $owner, int $vid): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $video = $this->videos->getByOwnerAndVID($owner, $vid); - if(!$video) + if (!$video) { $this->notFound(); + } $user = $this->user->id; - - if(!is_null($user)) { - if($video->getOwnerVideo() == $user) { + + if (!is_null($user)) { + if ($video->getOwnerVideo() == $user) { $video->deleteVideo($owner, $vid); } } else { - $this->flashFail("err", tr("error_deleting_video"), tr("login_please")); + $this->flashFail("err", tr("cant_delete_video"), tr("cant_delete_video_comment")); } - + $this->redirect("/videos" . $owner); } + + public function renderLike(int $owner, int $video_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertNoCSRF(); + + $video = $this->videos->getByOwnerAndVID($owner, $video_id); + if (!$video || $video->isDeleted() || $video->getOwner()->isDeleted()) { + $this->notFound(); + } + + if (method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + if (!is_null($this->user->identity)) { + $video->toggleLike($this->user->identity); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + + $this->redirect("$_SERVER[HTTP_REFERER]"); + } } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index d89b722a4..f09455c30 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -1,9 +1,13 @@ -posts = $posts; - + parent::__construct(); } - + private function logPostView(Post $post, int $wall): void { - if(is_null($this->user)) + if (is_null($this->user->id)) { return; - + } + $this->logEvent("postView", [ "profile" => $this->user->identity->getId(), "post" => $post->getId(), @@ -34,76 +39,120 @@ private function logPostView(Post $post, int $wall): void "subscribed" => $wall < 0 ? $post->getOwner()->getSubscriptionStatus($this->user->identity) : false, ]); } - + private function logPostsViewed(array &$posts, int $wall): void { $x = array_values($posts); # clone array (otherwise Nette DB objects will become kinda gay) - - foreach($x as $post) + + foreach ($x as $post) { $this->logPostView($post, $wall); + } } - - function renderWall(int $user, bool $embedded = false): void + + public function renderWall(int $user, bool $embedded = false): void { - $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); - if ($owner->isBanned()) + $owner = ($user < 0 ? (new Clubs()) : (new Users()))->get(abs($user)); + if (!$owner || $owner->isBanned() || !$owner->canBeViewedBy($this->user->identity)) { $this->flashFail("err", tr("error"), tr("forbidden")); + } - if(is_null($this->user)) { + if ($user > 0 && $owner->isDeleted()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + if (is_null($this->user->identity)) { $canPost = false; - } else if($user > 0) { + } elseif ($user > 0) { $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); - } else if($user < 0) { - if($owner->canBeModifiedBy($this->user->identity)) + } elseif ($user < 0) { + if ($owner->canBeModifiedBy($this->user->identity)) { $canPost = true; - else + } else { $canPost = $owner->canPost(); + } } else { $canPost = false; } - - if ($embedded == true) $this->template->_template = "components/wall.xml"; + + if ($embedded == true) { + $this->template->_template = "components/wall.latte"; + } $this->template->oObj = $owner; - if($user < 0) + if ($user < 0) { $this->template->club = $owner; - + } + + $iterator = null; + $count = 0; + $type = $this->queryParam("type") ?? "all"; + $page = (int) ($_GET["p"] ?? 1); + if ($page <= 0) { + $page = 1; + } + + switch ($type) { + default: + case "all": + $iterator = $this->posts->getPostsFromUsersWall($user, $page); + $count = $this->posts->getPostCountOnUserWall($user); + break; + case "owners": + $iterator = $this->posts->getOwnersPostsFromWall($user, $page); + $count = $this->posts->getOwnersCountOnUserWall($user); + break; + case "others": + $iterator = $this->posts->getOthersPostsFromWall($user, $page); + $count = $this->posts->getOthersCountOnUserWall($user); + break; + case "search": + $foundPosts = $this->posts->find($_GET["q"] ?? "", ["wall_id" => $user], ['type' => 'id', 'invert' => false]); + $iterator = $foundPosts->page($page); + $count = $foundPosts->size(); + break; + } + $this->template->owner = $user; $this->template->canPost = $canPost; - $this->template->count = $this->posts->getPostCountOnUserWall($user); - $this->template->posts = iterator_to_array($this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1))); + $this->template->count = $count; + $this->template->type = $type; + $this->template->posts = iterator_to_array($iterator); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($_GET["p"] ?? 1), "amount" => sizeof($this->template->posts), "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; - + $this->logPostsViewed($this->template->posts, $user); } - function renderWallEmbedded(int $user): void + public function renderWallEmbedded(int $user): void { $this->renderWall($user, true); } - function renderRSS(int $user): void + public function renderRSS(int $user): void { - $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); - if(is_null($this->user)) { + $owner = ($user < 0 ? (new Clubs()) : (new Users()))->get(abs($user)); + if (is_null($this->user->identity)) { $canPost = false; - } else if($user > 0) { - if(!$owner->isBanned()) + } elseif ($user > 0) { + if (!$owner->isBanned() && $owner->canBeViewedBy($this->user->identity)) { $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); - else + } else { $this->flashFail("err", tr("error"), tr("forbidden")); - } else if($user < 0) { - if($owner->canBeModifiedBy($this->user->identity)) + } + } elseif ($user < 0) { + if ($owner->canBeModifiedBy($this->user->identity)) { $canPost = true; - else if ($owner->isBanned()) + } elseif ($owner->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - else + } else { $canPost = $owner->canPost(); + } } else { $canPost = false; } @@ -115,88 +164,119 @@ function renderRSS(int $user): void $channel = new Channel(); $channel->title($owner->getCanonicalName() . " — " . OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"])->appendTo($feed); - foreach($posts as $post) { + foreach ($posts as $post) { $item = new Item(); $item ->title($post->getOwner()->getCanonicalName()) ->description($post->getText()) - ->url(ovk_scheme(true).$_SERVER["HTTP_HOST"]."/wall{$post->getPrettyId()}") + ->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/wall{$post->getPrettyId()}") ->pubDate($post->getPublicationTime()->timestamp()) ->appendTo($channel); } header("Content-Type: application/rss+xml"); - exit($feed); + exit((string) $feed); } - - function renderFeed(): void + + public function renderFeed(): void { $this->assertUserLoggedIn(); - + $id = $this->user->id; $subs = DatabaseConnection::i() ->getContext() ->table("subscriptions") ->where("follower", $id); - $ids = array_map(function($rel) { + $ids = array_map(function ($rel) { return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1); }, iterator_to_array($subs)); $ids[] = $this->user->id; - + $perPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); + $withAlienWallPosts = (int) ($_GET["with_alien_wall_posts"] ?? 0); + $posts = DatabaseConnection::i() ->getContext() ->table("posts") ->select("id") ->where("wall IN (?)", $ids) ->where("deleted", 0) + ->where("suggested", 0) ->order("created DESC"); + + if ($withAlienWallPosts === 0) { + $posts->where("(`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`)"); + } $this->template->paginatorConf = (object) [ "count" => sizeof($posts), "page" => (int) ($_GET["p"] ?? 1), "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->count(), "perPage" => $perPage, + "tidy" => false, + "atTop" => false, ]; $this->template->posts = []; - foreach($posts->page((int) ($_GET["p"] ?? 1), $perPage) as $post) + foreach ($posts->page((int) ($_GET["p"] ?? 1), $perPage) as $post) { $this->template->posts[] = $this->posts->get($post->id); + } } - - function renderGlobalFeed(): void + + public function renderGlobalFeed(): void { $this->assertUserLoggedIn(); - + $page = (int) ($_GET["p"] ?? 1); $pPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0"; + $withAlienWallPosts = (int) ($_GET["with_alien_wall_posts"] ?? 0); + + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; + $queryBase .= " WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND ((`profiles`.`profile_type` = 0 AND `profiles`.`hide_global_feed` = 0) OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; - if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) + if ($withAlienWallPosts === 0) { + $queryBase .= " AND ((`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`))"; + } + + if ($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) { $queryBase .= " AND `nsfw` = 0"; + } + + if (((int) $this->queryParam('return_banned')) == 0) { + $ignored_sources_ids = $this->user->identity->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); + + if (sizeof($ignored_sources_ids) > 0) { + $imploded_ids = implode("', '", $ignored_sources_ids); + + $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; + } + } $posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " ORDER BY `created` DESC LIMIT " . $pPage . " OFFSET " . ($page - 1) * $pPage); $count = DatabaseConnection::i()->getConnection()->query("SELECT COUNT(*) " . $queryBase)->fetch()->{"COUNT(*)"}; - - $this->template->_template = "Wall/Feed.xml"; + + $this->template->_template = "Wall/Feed.latte"; $this->template->globalFeed = true; $this->template->paginatorConf = (object) [ "count" => $count, "page" => (int) ($_GET["p"] ?? 1), "amount" => $posts->getRowCount(), "perPage" => $pPage, + "tidy" => false, + "atTop" => false, ]; - foreach($posts as $post) + foreach ($posts as $post) { $this->template->posts[] = $this->posts->get($post->id); + } } - - function renderHashtagFeed(string $hashtag): void + + public function renderHashtagFeed($hashtag): void { - $hashtag = rawurldecode($hashtag); - + $hashtag = rawurldecode('' . $hashtag); // simpler than converting it with countless ifs + $page = (int) ($_GET["p"] ?? 1); $posts = $this->posts->getPostsByHashtag($hashtag, $page); $count = $this->posts->getPostCountByHashtag($hashtag); - + $this->template->hashtag = $hashtag; $this->template->posts = $posts; $this->template->paginatorConf = (object) [ @@ -204,119 +284,115 @@ function renderHashtagFeed(string $hashtag): void "page" => $page, "amount" => $count, "perPage" => OPENVK_DEFAULT_PER_PAGE, + "tidy" => false, + "atTop" => false, ]; } - - function renderMakePost(int $wall): void + + public function renderMakePost(int $wall): void { $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")); - if ($wallOwner->isBanned()) + $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")); + } - if($wall > 0) { + if ($wall > 0) { $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); - } else if($wall < 0) { - if($wallOwner->canBeModifiedBy($this->user->identity)) + } elseif ($wall < 0) { + if ($wallOwner->canBeModifiedBy($this->user->identity)) { $canPost = true; - else + } else { $canPost = $wallOwner->canPost(); + } } else { - $canPost = false; + $canPost = false; } - - if(!$canPost) + + if (!$canPost) { $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); - + } + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; - if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { + if ($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { $manager = $wallOwner->getManager($this->user->identity); - if($manager) + if ($manager) { $anon = $manager->isHidden(); - elseif($this->user->identity->getId() === $wallOwner->getOwner()->getId()) + } elseif ($this->user->identity->getId() === $wallOwner->getOwner()->getId()) { $anon = $wallOwner->isOwnerHidden(); + } } else { $anon = $anon && $this->postParam("anon") === "on"; } - + $flags = 0; - if($this->postParam("as_group") === "on" && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->user->identity)) + if ($this->postParam("as_group") === "on" && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->user->identity)) { $flags |= 0b10000000; - if($this->postParam("force_sign") === "on") - $flags |= 0b01000000; - - $photos = []; - - if(!empty($this->postParam("photos"))) { - $un = rtrim($this->postParam("photos"), ","); - $arr = explode(",", $un); - - if(sizeof($arr) < 11) { - foreach($arr as $dat) { - $ids = explode("_", $dat); - $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); - - if(!$photo || $photo->isDeleted()) - continue; - - $photos[] = $photo; - } + + if ($this->postParam("force_sign") === "on") { + $flags |= 0b01000000; } } - + + $horizontal_attachments = []; + $vertical_attachments = []; + if (!empty($this->postParam("horizontal_attachments"))) { + $horizontal_attachments_array = array_slice(explode(",", $this->postParam("horizontal_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if (sizeof($horizontal_attachments_array) > 0) { + $horizontal_attachments = parseAttachments($horizontal_attachments_array, ['photo', 'video']); + } + } + + if (!empty($this->postParam("vertical_attachments"))) { + $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if (sizeof($vertical_attachments_array) > 0) { + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']); + } + } + try { - $poll = NULL; + $poll = null; $xml = $this->postParam("poll"); - if (!is_null($xml) && $xml != "none") + if (!is_null($xml) && $xml != "none") { $poll = Poll::import($this->user->identity, $xml); - } catch(TooMuchOptionsException $e) { + } + } catch (TooMuchOptionsException $e) { $this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options")); - } catch(\UnexpectedValueException $e) { + } catch (\UnexpectedValueException $e) { $this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid"); } - $note = NULL; - - if(!is_null($this->postParam("note")) && $this->postParam("note") != "none") { - $note = (new Notes)->get((int)$this->postParam("note")); + $geo = null; - if(!$note || $note->isDeleted() || $note->getOwner()->getId() != $this->user->id) { - $this->flashFail("err", tr("error"), tr("error_attaching_note")); - } - - if($note->getOwner()->getPrivacySetting("notes.read") < 1) { - $this->flashFail("err", " "); + if (!is_null($this->postParam("geo")) && $this->postParam("geo") != "") { + $geo = json_decode($this->postParam("geo"), true, JSON_UNESCAPED_UNICODE); + if ($geo["lat"] && $geo["lng"] && $geo["name"]) { + $latitude = number_format((float) $geo["lat"], 8, ".", ''); + $longitude = number_format((float) $geo["lng"], 8, ".", ''); + if ($latitude > 90 || $latitude < -90 || $longitude > 180 || $longitude < -180) { + $this->flashFail("err", tr("error"), "Invalid latitude or longitude"); + } } } - $videos = []; - - if(!empty($this->postParam("videos"))) { - $un = rtrim($this->postParam("videos"), ","); - $arr = explode(",", $un); + if (empty($this->postParam("text")) && sizeof($horizontal_attachments) < 1 && sizeof($vertical_attachments) < 1 && !$poll) { + $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); + } - if(sizeof($arr) < 11) { - foreach($arr as $dat) { - $ids = explode("_", $dat); - $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); - - if(!$video || $video->isDeleted()) - continue; - - $videos[] = $video; - } - } + if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "wall.post")) { + $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); } - - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note) - $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); - + + $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2; try { - $post = new Post; + $post = new Post(); $post->setOwner($this->user->id); $post->setWall($wall); $post->setCreated(time()); @@ -324,124 +400,190 @@ function renderMakePost(int $wall): void $post->setAnonymous($anon); $post->setFlags($flags); $post->setNsfw($this->postParam("nsfw") === "on"); + + if (!empty($this->postParam("source")) && $this->postParam("source") != 'none') { + try { + $post->setSource($this->postParam("source")); + } catch (\Throwable) { + } + } + + if ($should_be_suggested) { + $post->setSuggested(1); + } + + if ($geo) { + $post->setGeo($geo); + $post->setGeo_Lat($latitude); + $post->setGeo_Lon($longitude); + } $post->save(); } catch (\LengthException $ex) { $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); } - - foreach($photos as $photo) - $post->attach($photo); - - if(sizeof($videos) > 0) - foreach($videos as $vid) - $post->attach($vid); - - if(!is_null($poll)) + + foreach ($horizontal_attachments as $horizontal_attachment) { + if (!$horizontal_attachment || $horizontal_attachment->isDeleted() || !$horizontal_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $post->attach($horizontal_attachment); + } + + foreach ($vertical_attachments as $vertical_attachment) { + if (!$vertical_attachment || $vertical_attachment->isDeleted() || !$vertical_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $post->attach($vertical_attachment); + } + + if (!is_null($poll)) { $post->attach($poll); + } + + if ($wall > 0 && $wall !== $this->user->identity->getId()) { + $disturber = $this->user->identity; + if ($anon) { + $disturber = $post->getOwner(); + } + + (new WallPostNotification($wallOwner, $post, $disturber))->emit(); + } - if(!is_null($note)) - $post->attach($note); - - if($wall > 0 && $wall !== $this->user->identity->getId()) - (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); - $excludeMentions = [$this->user->identity->getId()]; - if($wall > 0) + if ($wall > 0) { $excludeMentions[] = $wall; + } + + if (!$should_be_suggested) { + $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); - $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); - foreach($mentions as $mentionee) - if($mentionee instanceof User) - (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); - - $this->redirect($wallOwner->getURL()); + foreach ($mentions as $mentionee) { + if ($mentionee instanceof User) { + (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + } + } + } + + if ($should_be_suggested) { + $this->redirect("/club" . $wallOwner->getId() . "/suggested"); + } else { + $this->redirect($wallOwner->getURL()); + } } - - function renderPost(int $wall, int $post_id): void + + public function renderPost(int $wall, int $post_id): void { $post = $this->posts->getPostById($wall, $post_id); - if(!$post || $post->isDeleted()) + if (!$post || $post->isDeleted()) { $this->notFound(); - + } + + if (!$post->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + $this->logPostView($post, $wall); - + $this->template->post = $post; if ($post->getTargetWall() > 0) { - $this->template->wallOwner = (new Users)->get($post->getTargetWall()); - $this->template->isWallOfGroup = false; - if($this->template->wallOwner->isBanned()) + $this->template->wallOwner = (new Users())->get($post->getTargetWall()); + $this->template->isWallOfGroup = false; + if ($this->template->wallOwner->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - } else { - $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); - $this->template->isWallOfGroup = true; + } + } else { + $this->template->wallOwner = (new Clubs())->get(abs($post->getTargetWall())); + $this->template->isWallOfGroup = true; - if ($this->template->wallOwner->isBanned()) + if ($this->template->wallOwner->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - } + } + } $this->template->cCount = $post->getCommentsCount(); $this->template->cPage = (int) ($_GET["p"] ?? 1); - $this->template->comments = iterator_to_array($post->getComments($this->template->cPage)); + $this->template->sort = $this->queryParam("sort") ?? "asc"; + + $input_sort = $this->template->sort == "asc" ? "ASC" : "DESC"; + + $this->template->comments = iterator_to_array($post->getComments($this->template->cPage, null, $input_sort)); } - - function renderLike(int $wall, int $post_id): void + + public function renderLike(int $wall, int $post_id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); $this->assertNoCSRF(); - + $post = $this->posts->getPostById($wall, $post_id); - if(!$post || $post->isDeleted()) $this->notFound(); + if (!$post || $post->isDeleted()) { + $this->notFound(); + } - if ($post->getWallOwner()->isBanned()) + if ($post->getWallOwner()->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } - if(!is_null($this->user)) { + if (!is_null($this->user->identity)) { $post->toggleLike($this->user->identity); } - + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + $this->redirect("$_SERVER[HTTP_REFERER]#postGarter=" . $post->getId()); } - - function renderShare(int $wall, int $post_id): void + + public function renderShare(int $wall, int $post_id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); $this->assertNoCSRF(); - + $post = $this->posts->getPostById($wall, $post_id); - if(!$post || $post->isDeleted()) + if (!$post || $post->isDeleted()) { $this->notFound(); + } - if ($post->getWallOwner()->isBanned()) + if ($post->getWallOwner()->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - + } + $where = $this->postParam("type") ?? "wall"; - $groupId = NULL; + $groupId = null; $flags = 0; - if($where == "group") + if ($where == "group") { $groupId = $this->postParam("groupId"); + } - if(!is_null($this->user)) { - $nPost = new Post; + if (!is_null($this->user->identity)) { + $nPost = new Post(); - if($where == "wall") { + if ($where == "wall") { $nPost->setOwner($this->user->id); $nPost->setWall($this->user->id); - } elseif($where == "group") { + } elseif ($where == "group") { $nPost->setOwner($this->user->id); - $club = (new Clubs)->get((int)$groupId); + $club = (new Clubs())->get((int) $groupId); - if(!$club || !$club->canBeModifiedBy($this->user->identity)) + if (!$club || !$club->canBeModifiedBy($this->user->identity)) { $this->notFound(); - - if($this->postParam("asGroup") == 1) + } + + if ($this->postParam("asGroup") == 1) { $flags |= 0b10000000; + } - if($this->postParam("signed") == 1) + if ($this->postParam("asGroup") == 1 && $this->postParam("signed") == 1) { $flags |= 0b01000000; - + } + $nPost->setWall($groupId * -1); } @@ -450,129 +592,228 @@ function renderShare(int $wall, int $post_id): void $nPost->save(); $nPost->attach($post); - - if($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club)) + + if ($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club)) { (new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit(); + } }; - + $this->returnJson([ - "wall_owner" => $where == "wall" ? $this->user->identity->getId() : $groupId * -1 + "wall_owner" => $where == "wall" ? $this->user->identity->getId() : $groupId * -1, ]); } - - function renderDelete(int $wall, int $post_id): void + + public function renderDelete(int $wall, int $post_id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - - $post = $this->posts->getPostById($wall, $post_id); - if(!$post) + + $post = $this->posts->getPostById($wall, $post_id, true); + if (!$post) { $this->notFound(); + } $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->isBanned()) + if ($wallOwner === null) { + $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4")); + } + + if ($wallOwner->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); + } - if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); - else $canBeDeletedByOtherUser = false; + if ($wall < 0) { + $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); + } else { + $canBeDeletedByOtherUser = false; + } + + if (!is_null($user)) { + if ($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->user->identity) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) { + $this->flashFail("err", tr("failed_to_delete_post"), tr("error_deleting_suggested")); + } - if(!is_null($user)) { - if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) { + if ($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) { $post->unwire(); $post->delete(); } } else { $this->flashFail("err", tr("failed_to_delete_post"), tr("login_required_error_comment")); } - - $this->redirect($wall < 0 ? "/club" . ($wall*-1) : "/id" . $wall); + + $this->redirect($wall < 0 ? "/club" . ($wall * -1) : "/id" . $wall); } - - function renderPin(int $wall, int $post_id): void + + public function renderPin(int $wall, int $post_id): void { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - + $post = $this->posts->getPostById($wall, $post_id); - if(!$post) + if (!$post) { $this->notFound(); + } - if ($post->getWallOwner()->isBanned()) + if ($post->getWallOwner()->isBanned()) { $this->flashFail("err", tr("error"), tr("forbidden")); - - if(!$post->canBePinnedBy($this->user->identity)) + } + + if (!$post->canBePinnedBy($this->user->identity)) { $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); - - if(($this->queryParam("act") ?? "pin") === "pin") { + } + + if (($this->queryParam("act") ?? "pin") === "pin") { $post->pin(); } else { $post->unpin(); } - + # TODO localize message based on language and ?act=(un)pin $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); } - function renderEdit() + public function renderAccept() { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction(true); - if($_SERVER["REQUEST_METHOD"] !== "POST") - $this->redirect("/id0"); + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это точка апи."); + } - if($this->postParam("type") == "post") - $post = $this->posts->get((int)$this->postParam("postid")); - else - $post = (new Comments)->get((int)$this->postParam("postid")); + $id = $this->postParam("id"); + $sign = $this->postParam("sign") == 1; + $content = $this->postParam("new_content"); - if(!$post || $post->isDeleted()) - $this->returnJson(["error" => "Invalid post"]); + $post = (new Posts())->get((int) $id); - if(!$post->canBeEditedBy($this->user->identity)) - $this->returnJson(["error" => "Access denied"]); + if (!$post || $post->isDeleted()) { + $this->flashFail("err", "Error", tr("error_accepting_invalid_post"), null, true); + } - $attachmentsCount = sizeof(iterator_to_array($post->getChildren())); + if ($post->getSuggestionType() == 0) { + $this->flashFail("err", "Error", tr("error_accepting_not_suggested_post"), null, true); + } - if(empty($this->postParam("newContent")) && $attachmentsCount < 1) - $this->returnJson(["error" => "Empty post"]); + if ($post->getSuggestionType() == 2) { + $this->flashFail("err", "Error", tr("error_accepting_declined_post"), null, true); + } - $post->setEdited(time()); + if (!$post->canBePinnedBy($this->user->identity)) { + $this->flashFail("err", "Error", "Can't accept this post.", null, true); + } - try { - $post->setContent($this->postParam("newContent")); - } catch(\LengthException $e) { - $this->returnJson(["error" => $e->getMessage()]); + $author = $post->getOwner(); + + $flags = 0; + $flags |= 0b10000000; + + if ($sign) { + $flags |= 0b01000000; } - if($this->postParam("type") === "post") { - $post->setNsfw($this->postParam("nsfw") == "true"); - $flags = 0; + $post->setSuggested(0); + $post->setCreated(time()); + $post->setApi_Source_Name(null); + $post->setFlags($flags); - if($post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($this->user->identity)) { - if($this->postParam("fromgroup") == "true") { - $flags |= 0b10000000; - $post->setFlags($flags); - } else - $post->setFlags($flags); - } + if (mb_strlen($content) > 0) { + $post->setContent($content); + } + + $post->save(); + + if ($author->getId() != $this->user->id) { + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + } + + $this->returnJson([ + "success" => true, + "id" => $post->getPrettyId(), + "new_count" => (new Posts())->getSuggestedPostsCount($post->getWallOwner()->getId()), + ]); + } + + public function renderDecline() + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это метод апи."); + } + + $id = $this->postParam("id"); + $post = (new Posts())->get((int) $id); + + if (!$post || $post->isDeleted()) { + $this->flashFail("err", "Error", tr("error_declining_invalid_post"), null, true); + } + + if ($post->getSuggestionType() == 0) { + $this->flashFail("err", "Error", tr("error_declining_not_suggested_post"), null, true); + } + + if ($post->getSuggestionType() == 2) { + $this->flashFail("err", "Error", tr("error_declining_declined_post"), null, true); + } + + if (!$post->canBePinnedBy($this->user->identity)) { + $this->flashFail("err", "Error", "Can't decline this post.", null, true); + } + + $post->setSuggested(2); + $post->setDeleted(1); + $post->save(); + + $this->returnJson([ + "success" => true, + "new_count" => (new Posts())->getSuggestedPostsCount($post->getWallOwner()->getId()), + ]); + } + + public function renderLikers(string $type, int $owner_id, int $item_id) + { + $this->assertUserLoggedIn(); + + $item = null; + $display_name = $type; + switch ($type) { + default: + $this->notFound(); + break; + case 'wall': + $item = $this->posts->getPostById($owner_id, $item_id); + $display_name = 'post'; + break; + case 'comment': + $item = (new \openvk\Web\Models\Repositories\Comments())->get($item_id); + break; + case 'photo': + $item = (new \openvk\Web\Models\Repositories\Photos())->getByOwnerAndVID($owner_id, $item_id); + break; + case 'video': + $item = (new \openvk\Web\Models\Repositories\Videos())->getByOwnerAndVID($owner_id, $item_id); + break; + } + + if (!$item || $item->isDeleted() || !$item->canBeViewedBy($this->user->identity)) { + $this->notFound(); } - $post->save(true); + $page = (int) ($this->queryParam('p') ?? 1); + $count = $item->getLikesCount(); + $likers = iterator_to_array($item->getLikers($page, OPENVK_DEFAULT_PER_PAGE)); - $this->returnJson(["error" => "no", - "new_content" => $post->getText(), - "new_edited" => (string)$post->getEditTime(), - "nsfw" => $this->postParam("type") === "post" ? (int)$post->isExplicit() : 0, - "from_group" => $this->postParam("type") === "post" && $post->getTargetWall() < 0 ? - ((int)$post->isPostedOnBehalfOfGroup()) : "false", - "new_text" => $post->getText(false), - "author" => [ - "name" => $post->getOwner()->getCanonicalName(), - "avatar" => $post->getOwner()->getAvatarUrl() - ]]); + $this->template->item = $item; + $this->template->type = $display_name; + $this->template->iterator = $likers; + $this->template->count = $count; + $this->template->page = $page; + $this->template->perPage = OPENVK_DEFAULT_PER_PAGE; } } diff --git a/Web/Presenters/templates/@CanonicalListView.xml b/Web/Presenters/templates/@CanonicalListView.latte similarity index 85% rename from Web/Presenters/templates/@CanonicalListView.xml rename to Web/Presenters/templates/@CanonicalListView.latte index a0c8f7d79..014aa4891 100644 --- a/Web/Presenters/templates/@CanonicalListView.xml +++ b/Web/Presenters/templates/@CanonicalListView.latte @@ -32,18 +32,20 @@
- {include "components/paginator.xml", conf => (object) [ + {include "components/paginator.latte", conf => (object) [ "page" => $page, "count" => $count, "amount" => sizeof($data), - "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "perPage" => $perPage ?? \OPENVK_DEFAULT_PER_PAGE, + "atTop" => false, + "tidy" => false ]}
{else} {ifset customErrorMessage} {include customErrorMessage} {else} - {include "components/nothing.xml"} + {include "components/nothing.latte"} {/ifset} {/if} diff --git a/Web/Presenters/templates/@MilkshakeListView.xml b/Web/Presenters/templates/@MilkshakeListView.latte similarity index 87% rename from Web/Presenters/templates/@MilkshakeListView.xml rename to Web/Presenters/templates/@MilkshakeListView.latte index 699da82b3..d5883570a 100644 --- a/Web/Presenters/templates/@MilkshakeListView.xml +++ b/Web/Presenters/templates/@MilkshakeListView.latte @@ -1,4 +1,4 @@ -{extends "@layout.xml"} +{extends "@layout.latte"} {block wrap}
@@ -32,18 +32,20 @@
- {include "components/paginator.xml", conf => (object) [ + {include "components/paginator.latte", conf => (object) [ "page" => $page, "count" => $count, "amount" => sizeof($data), - "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "perPage" => $perPage ?? \OPENVK_DEFAULT_PER_PAGE, + "atTop" => false, + "tidy" => false ]}
{else} {ifset customErrorMessage} {include customErrorMessage} {else} - {include "components/nothing.xml"} + {include "components/nothing.latte"} {/ifset} {/if}
diff --git a/Web/Presenters/templates/@banned.xml b/Web/Presenters/templates/@banned.latte similarity index 91% rename from Web/Presenters/templates/@banned.xml rename to Web/Presenters/templates/@banned.latte index 7640838c1..0b3be2f00 100644 --- a/Web/Presenters/templates/@banned.xml +++ b/Web/Presenters/templates/@banned.latte @@ -1,4 +1,4 @@ -{extends "@layout.xml"} +{extends "@layout.latte"} {block title}{_banned_title}{/block} {block header} @@ -19,7 +19,7 @@
Эта страница была заморожена {$ban[0]|noescape} {if $ban[1] !== "app"} - {include "Report/ViewContent.xml", type => $ban[1], object => $ban[2]} + {include "Report/ViewContent.latte", type => $ban[1], object => $ban[2]} {/if}
{/if} diff --git a/Web/Presenters/templates/@deactivated.xml b/Web/Presenters/templates/@deactivated.latte similarity index 89% rename from Web/Presenters/templates/@deactivated.xml rename to Web/Presenters/templates/@deactivated.latte index 0be411411..05ae632e7 100644 --- a/Web/Presenters/templates/@deactivated.xml +++ b/Web/Presenters/templates/@deactivated.latte @@ -1,4 +1,4 @@ -{extends "@layout.xml"} +{extends "@layout.latte"} {block title}{$thisUser->getCanonicalName()}{/block} {block header} @@ -6,7 +6,7 @@ {/block} {block content} -
+
{tr("profile_deactivated_msg", $thisUser->getDeactivationDate()->format("%e %B %G" . tr("time_at_sp") . "%R"))|noescape}
diff --git a/Web/Presenters/templates/@email.xml b/Web/Presenters/templates/@email.latte similarity index 91% rename from Web/Presenters/templates/@email.xml rename to Web/Presenters/templates/@email.latte index 3de8b2e1c..d864fe9b2 100755 --- a/Web/Presenters/templates/@email.xml +++ b/Web/Presenters/templates/@email.latte @@ -1,4 +1,4 @@ -{extends "@layout.xml"} +{extends "@layout.latte"} {block title}{_ec_header}{/block} {block header} diff --git a/Web/Presenters/templates/@error.xml b/Web/Presenters/templates/@error.latte similarity index 96% rename from Web/Presenters/templates/@error.xml rename to Web/Presenters/templates/@error.latte index 64359f7ec..a8c35151d 100644 --- a/Web/Presenters/templates/@error.xml +++ b/Web/Presenters/templates/@error.latte @@ -1,4 +1,4 @@ -{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} +{var $instance_name = \OPENVK_ROOT_CONF['openvk']['appearance']['name']} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.latte similarity index 52% rename from Web/Presenters/templates/@layout.xml rename to Web/Presenters/templates/@layout.latte index f8a975e01..57ea20c62 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.latte @@ -1,41 +1,57 @@ -{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} +{capture $pageTitle}{ifset title}{include title}{/ifset}{/capture} {if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'} - {ifset title}{include title} - {/ifset}{$instance_name} + {ifset title}{$pageTitle} - {/ifset}{$instance_name} + {script "js/node_modules/jquery/dist/jquery.min.js"} + {script "js/node_modules/jquery-ui/dist/jquery-ui.min.js"} {script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/l10n.js"} {script "js/openvk.cls.js"} + {script "js/utils.js"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {css "js/node_modules/tippy.js/dist/backdrop.css"} + {css "js/node_modules/cropperjs/dist/cropper.css"} {css "js/node_modules/tippy.js/dist/border.css"} {css "js/node_modules/tippy.js/dist/svg-arrow.css"} {css "js/node_modules/tippy.js/themes/light.css"} + {css "js/node_modules/jquery-ui/themes/base/resizable.css"} {script "js/node_modules/@popperjs/core/dist/umd/popper.min.js"} {script "js/node_modules/tippy.js/dist/tippy-bundle.umd.min.js"} {script "js/node_modules/handlebars/dist/handlebars.min.js"} + {script "js/node_modules/react/dist/react-with-addons.min.js"} + {script "js/node_modules/react-dom/dist/react-dom.min.js"} + {script "js/vnd_literallycanvas.js"} + {css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"} {if $isTimezoned == NULL} {script "js/timezone.js"} {/if} - {include "_includeCSS.xml"} + {include "_includeCSS.latte"} + + + {ifset headIncludes} {include headIncludes} {/ifset} - + + {include "components/globalalert.latte"} +

{_you_entered_as} {$thisUser->getCanonicalName()}. {_please_rights} @@ -43,10 +59,13 @@

-
FOR TESTING PURPOSES ONLY
+
FOR TESTING PURPOSES ONLY
-
+
+
+
+
{_close} @@ -76,14 +95,41 @@
{/if} -
- ⬆ {_to_top} +
-
- {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if} +
+ {if $instance_name != \OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if} +
+ + {ifset title} + {$pageTitle} + {/ifset} + +
+ {ifset $thisUser} + {var $notificationsCount = $thisUser->getNotificationsCount()} + {var $unreadMessagesCount = $thisUser->getUnreadMessagesCount()} + + + + {$notificationsCount} + + + {$unreadMessagesCount} + + + {/ifset}
{ifset $thisUser} {if $thisUser->isDeactivated()} @@ -91,66 +137,47 @@ {_header_log_out}
{else} -