Merge branch 'master' into textarea-refactor

This commit is contained in:
veselcraft 2022-12-07 03:04:43 +03:00
commit 0fd5958443
No known key found for this signature in database
GPG key ID: AED66BC1AC628A4E
198 changed files with 7905 additions and 4240 deletions

58
.github/workflows/build-base.yaml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Build base images
on:
schedule:
- cron: '0 0 * * *'
env:
BASE_IMAGE_NAME: php
BASE_IMAGE_VERSION: "8.1"
jobs:
build-cli:
runs-on: ubuntu-latest
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
- 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
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
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
- 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
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

64
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,64 @@
name: Build images
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
BASE_IMAGE_NAME: openvk
DB_IMAGE_NAME: mariadb
EVENT_IMAGE_NAME: mariadb
DB_VERSION: "10.9"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch: ['x86_64']
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
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- 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
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
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
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-eventdb.Dockerfile --build-arg VERSION=$DB_VERSION

2
.gitignore vendored
View file

@ -1,5 +1,6 @@
vendor vendor
openvk.yml openvk.yml
chandler.yml
update.pid update.pid
update.pid.old update.pid.old
Web/static/js/node_modules Web/static/js/node_modules
@ -10,5 +11,6 @@ tmp/*
themepacks/* themepacks/*
!themepacks/.gitkeep !themepacks/.gitkeep
!themepacks/openvk_modern !themepacks/openvk_modern
!themepacks/midnight
storage/* storage/*
!storage/.gitkeep !storage/.gitkeep

2
.idea/.gitignore vendored
View file

@ -5,4 +5,4 @@
/httpRequests/ /httpRequests/
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml

View file

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

View file

@ -1,290 +1,290 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE latte PUBLIC "-//LATTE//Latte plugin XML V0.0.1//EN" "Latte.dtd"> <!DOCTYPE latte PUBLIC "-//LATTE//Latte plugin XML V0.0.1//EN" "Latte.dtd">
<latte vendor="latte" version="1"> <latte vendor="latte" version="1">
<tags> <tags>
<tag name="_" type="AUTO_EMPTY" allowedFilters="true"> <tag name="_" type="AUTO_EMPTY" allowedFilters="true">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" /> <argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="=" type="UNPAIRED" allowedFilters="true"> <tag name="=" type="UNPAIRED" allowedFilters="true">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" /> <argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="block" type="AUTO_EMPTY" allowedFilters="true" multiLine="true"> <tag name="block" type="AUTO_EMPTY" allowedFilters="true" multiLine="true">
<arguments> <arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" /> <argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="breakIf" type="UNPAIRED"> <tag name="breakIf" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="capture" type="PAIR" allowedFilters="true" multiLine="true"> <tag name="capture" type="PAIR" allowedFilters="true" multiLine="true">
<arguments> <arguments>
<argument name="variable" types="VARIABLE_DEFINITION" required="true" /> <argument name="variable" types="VARIABLE_DEFINITION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="case" type="UNPAIRED"> <tag name="case" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" required="true" repeatable="true" /> <argument name="condition" types="PHP_CONDITION" required="true" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="catch" type="UNPAIRED"> <tag name="catch" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="contentType" type="UNPAIRED"> <tag name="contentType" type="UNPAIRED">
<arguments> <arguments>
<argument name="content-type" types="CONTENT_TYPE" validType="string" required="true" /> <argument name="content-type" types="CONTENT_TYPE" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="continueIf" type="UNPAIRED"> <tag name="continueIf" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="debugbreak" type="UNPAIRED"> <tag name="debugbreak" type="UNPAIRED">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="default" type="UNPAIRED"> <tag name="default" type="UNPAIRED">
<arguments> <arguments>
<argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" /> <argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="define" type="PAIR" multiLine="true"> <tag name="define" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" required="true" /> <argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" required="true" />
<argument name="variable" types="VARIABLE_DEFINITION_ITEM" repeatable="true" /> <argument name="variable" types="VARIABLE_DEFINITION_ITEM" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="do" type="UNPAIRED"> <tag name="do" type="UNPAIRED">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="dump" type="UNPAIRED"> <tag name="dump" type="UNPAIRED">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="else" type="UNPAIRED_ATTR" /> <tag name="else" type="UNPAIRED_ATTR" />
<tag name="elseif" type="UNPAIRED"> <tag name="elseif" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="elseifset" type="UNPAIRED"> <tag name="elseifset" type="UNPAIRED">
<arguments> <arguments>
<argument name="var" types="VARIABLE,BLOCK" validType="string" required="true" /> <argument name="var" types="VARIABLE,BLOCK" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="extends" type="UNPAIRED"> <tag name="extends" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" /> <argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="first" type="PAIR"> <tag name="first" type="PAIR">
<arguments> <arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" /> <argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="for" type="PAIR" arguments="initialization; condition; afterthought" multiLine="true" /> <tag name="for" type="PAIR" arguments="initialization; condition; afterthought" multiLine="true" />
<tag name="foreach" type="PAIR" arguments="expression as [$key =>] $value" allowedFilters="true" multiLine="true" /> <tag name="foreach" type="PAIR" arguments="expression as [$key =>] $value" allowedFilters="true" multiLine="true" />
<tag name="if" type="PAIR"> <tag name="if" type="PAIR">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="ifset" type="PAIR"> <tag name="ifset" type="PAIR">
<arguments> <arguments>
<argument name="var" types="VARIABLE,BLOCK,PHP_EXPRESSION" validType="string" required="true" /> <argument name="var" types="VARIABLE,BLOCK,PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="import" type="UNPAIRED"> <tag name="import" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" /> <argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="include" type="UNPAIRED" allowedFilters="true"> <tag name="include" type="UNPAIRED" allowedFilters="true">
<arguments> <arguments>
<argument name="file" types="BLOCK,IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" /> <argument name="file" types="BLOCK,IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="KEY_VALUE" repeatable="true" /> <argument name="arguments" types="KEY_VALUE" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="includeblock" type="UNPAIRED"> <tag name="includeblock" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" /> <argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="l" type="UNPAIRED" /> <tag name="l" type="UNPAIRED" />
<tag name="last" type="PAIR"> <tag name="last" type="PAIR">
<arguments> <arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" /> <argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="layout" type="UNPAIRED"> <tag name="layout" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" /> <argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="class" type="ATTR_ONLY" arguments="class" /> <tag name="class" type="ATTR_ONLY" arguments="class" />
<tag name="attr" type="ATTR_ONLY" arguments="attr" /> <tag name="attr" type="ATTR_ONLY" arguments="attr" />
<tag name="ifcontent" type="ATTR_ONLY" /> <tag name="ifcontent" type="ATTR_ONLY" />
<tag name="php" type="UNPAIRED"> <tag name="php" type="UNPAIRED">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="r" type="UNPAIRED" /> <tag name="r" type="UNPAIRED" />
<tag name="sandbox" type="UNPAIRED"> <tag name="sandbox" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="BLOCK,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" /> <argument name="file" types="BLOCK,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
<argument name="key-value" types="KEY_VALUE" repeatable="true" /> <argument name="key-value" types="KEY_VALUE" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="sep" type="PAIR"> <tag name="sep" type="PAIR">
<arguments> <arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" /> <argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" />
</arguments> </arguments>
</tag> </tag>
<tag name="snippet" type="PAIR" multiLine="true"> <tag name="snippet" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" /> <argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" />
</arguments> </arguments>
</tag> </tag>
<tag name="snippetArea" type="PAIR" multiLine="true"> <tag name="snippetArea" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="name" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" /> <argument name="name" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="spaceless" type="PAIR" /> <tag name="spaceless" type="PAIR" />
<tag name="switch" type="PAIR" multiLine="true"> <tag name="switch" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" /> <argument name="expression" types="PHP_EXPRESSION" />
</arguments> </arguments>
</tag> </tag>
<tag name="syntax" type="PAIR" arguments="off | double | latte" multiLine="true" /> <tag name="syntax" type="PAIR" arguments="off | double | latte" multiLine="true" />
<tag name="templatePrint" type="UNPAIRED"> <tag name="templatePrint" type="UNPAIRED">
<arguments> <arguments>
<argument name="class-name" types="PHP_CLASS_NAME" /> <argument name="class-name" types="PHP_CLASS_NAME" />
</arguments> </arguments>
</tag> </tag>
<tag name="templateType" type="UNPAIRED"> <tag name="templateType" type="UNPAIRED">
<arguments> <arguments>
<argument name="class-name" types="PHP_CLASS_NAME" required="true" /> <argument name="class-name" types="PHP_CLASS_NAME" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="try" type="PAIR" /> <tag name="try" type="PAIR" />
<tag name="rollback" type="UNPAIRED" /> <tag name="rollback" type="UNPAIRED" />
<tag name="tag" type="ATTR_ONLY"> <tag name="tag" type="ATTR_ONLY">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" validType="string" repeatable="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" validType="string" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="ifchanged" type="PAIR"> <tag name="ifchanged" type="PAIR">
<arguments> <arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" repeatable="true" /> <argument name="expression" types="PHP_EXPRESSION" required="true" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="skipIf" type="UNPAIRED"> <tag name="skipIf" type="UNPAIRED">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="var" type="UNPAIRED"> <tag name="var" type="UNPAIRED">
<arguments> <arguments>
<argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" /> <argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="trace" type="UNPAIRED" /> <tag name="trace" type="UNPAIRED" />
<tag name="varPrint" type="UNPAIRED" arguments="all" /> <tag name="varPrint" type="UNPAIRED" arguments="all" />
<tag name="varType" type="UNPAIRED"> <tag name="varType" type="UNPAIRED">
<arguments> <arguments>
<argument name="file" types="PHP_TYPE" required="true" /> <argument name="file" types="PHP_TYPE" required="true" />
<argument name="variable" types="VARIABLE_DEFINITION" required="true" /> <argument name="variable" types="VARIABLE_DEFINITION" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="while" type="PAIR" multiLine="true"> <tag name="while" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" /> <argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments> </arguments>
</tag> </tag>
<tag name="iterateWhile" type="PAIR" multiLine="true" /> <tag name="iterateWhile" type="PAIR" multiLine="true" />
<tag name="embed" type="PAIR" multiLine="true"> <tag name="embed" type="PAIR" multiLine="true">
<arguments> <arguments>
<argument name="file" types="BLOCK_USAGE,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" /> <argument name="file" types="BLOCK_USAGE,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
<argument name="key-value" types="KEY_VALUE" repeatable="true" /> <argument name="key-value" types="KEY_VALUE" repeatable="true" />
</arguments> </arguments>
</tag> </tag>
<!-- @deprecated - latte --> <!-- @deprecated - latte -->
<tag name="assign" type="UNPAIRED" arguments="$variable = expr" /> <tag name="assign" type="UNPAIRED" arguments="$variable = expr" />
<tag name="truncate" type="UNPAIRED" arguments="expression" deprecatedMessage="Tag {? ...} is deprecated in Latte 2.4. For variable definitions use {var ...} or {php ...} in other cases." /> <tag name="truncate" type="UNPAIRED" arguments="expression" deprecatedMessage="Tag {? ...} is deprecated in Latte 2.4. For variable definitions use {var ...} or {php ...} in other cases." />
</tags> </tags>
<filters> <filters>
<filter name="truncate" arguments=":($length, $append = '…')" description="shortens the length preserving whole words" insertColons=":" /> <filter name="truncate" arguments=":($length, $append = '…')" description="shortens the length preserving whole words" insertColons=":" />
<filter name="substr" arguments=":($offset [, $length])" description="returns part of the string" insertColons=":" /> <filter name="substr" arguments=":($offset [, $length])" description="returns part of the string" insertColons=":" />
<filter name="trim" arguments=":($charset = mezery)" description="strips whitespace or other characters from the beginning and end of the string" /> <filter name="trim" arguments=":($charset = mezery)" description="strips whitespace or other characters from the beginning and end of the string" />
<filter name="stripHtml" arguments="" description="removes HTML tags and converts HTML entities to text" /> <filter name="stripHtml" arguments="" description="removes HTML tags and converts HTML entities to text" />
<filter name="strip" arguments="" description="removes whitespace" /> <filter name="strip" arguments="" description="removes whitespace" />
<filter name="indent" arguments=":($level = 1, $char = '\t')" description="indents the text from left with number of tabs" /> <filter name="indent" arguments=":($level = 1, $char = '\t')" description="indents the text from left with number of tabs" />
<filter name="replace" arguments=":($search, $replace = '')" description="replaces all occurrences of the search string with the replacement" insertColons=":" /> <filter name="replace" arguments=":($search, $replace = '')" description="replaces all occurrences of the search string with the replacement" insertColons=":" />
<filter name="replaceRE" arguments=":($pattern, $replace = '')" description="replaces all occurrences according to regular expression" insertColons=":" /> <filter name="replaceRE" arguments=":($pattern, $replace = '')" description="replaces all occurrences according to regular expression" insertColons=":" />
<filter name="padLeft" arguments=":($length, $pad = ' ')" description="completes the string to given length from left" insertColons=":" /> <filter name="padLeft" arguments=":($length, $pad = ' ')" description="completes the string to given length from left" insertColons=":" />
<filter name="padRight" arguments=":($length, $pad = ' ')" description="completes the string to given length from right" insertColons=":" /> <filter name="padRight" arguments=":($length, $pad = ' ')" description="completes the string to given length from right" insertColons=":" />
<filter name="repeat" arguments=":($count)" description="repeats the string" insertColons=":" /> <filter name="repeat" arguments=":($count)" description="repeats the string" insertColons=":" />
<filter name="implode" arguments=":($glue = '')" description="joins an array to a string" /> <filter name="implode" arguments=":($glue = '')" description="joins an array to a string" />
<filter name="webalize" description="adjusts the UTF-8 string to the shape used in the URL" /> <filter name="webalize" description="adjusts the UTF-8 string to the shape used in the URL" />
<filter name="breaklines" description="inserts HTML line breaks before all newlines" /> <filter name="breaklines" description="inserts HTML line breaks before all newlines" />
<filter name="reverse" description="reverse an UTF-8 string or array" /> <filter name="reverse" description="reverse an UTF-8 string or array" />
<filter name="length" description="returns length of a string or array" /> <filter name="length" description="returns length of a string or array" />
<filter name="sort" description="simply sorts array" /> <filter name="sort" description="simply sorts array" />
<filter name="reverse" description="array sorted in reverse order (used with |sort)" /> <filter name="reverse" description="array sorted in reverse order (used with |sort)" />
<filter name="batch" arguments=":($array, $length [, $item])" description="returns length of a string or array" insertColons="::" /> <filter name="batch" arguments=":($array, $length [, $item])" description="returns length of a string or array" insertColons="::" />
<filter name="clamp" description="returns value clamped to the inclusive range of min and max." insertColons="::" /> <filter name="clamp" description="returns value clamped to the inclusive range of min and max." insertColons="::" />
<filter name="lower" description="makes a string lower case" /> <filter name="lower" description="makes a string lower case" />
<filter name="upper" description="makes a string upper case" /> <filter name="upper" description="makes a string upper case" />
<filter name="firstUpper" description="makes the first letter upper case" /> <filter name="firstUpper" description="makes the first letter upper case" />
<filter name="capitalize" description="lower case, the first letter of each word upper case" /> <filter name="capitalize" description="lower case, the first letter of each word upper case" />
<filter name="date" arguments=":($format)" description="formats date" insertColons=":" /> <filter name="date" arguments=":($format)" description="formats date" insertColons=":" />
<filter name="number" arguments=":($decimals = 0, $decPoint = '.', $thousandsSep = ',')" description="format number" /> <filter name="number" arguments=":($decimals = 0, $decPoint = '.', $thousandsSep = ',')" description="format number" />
<filter name="bytes" arguments=":($precision = 2)" description="formats size in bytes" /> <filter name="bytes" arguments=":($precision = 2)" description="formats size in bytes" />
<filter name="dataStream" arguments=":($mimetype = 'detect')" description="Data URI protocol conversion" /> <filter name="dataStream" arguments=":($mimetype = 'detect')" description="Data URI protocol conversion" />
<filter name="noescape" description="prints a variable without escaping" /> <filter name="noescape" description="prints a variable without escaping" />
<filter name="escapeurl" description="escapes parameter in URL" /> <filter name="escapeurl" description="escapes parameter in URL" />
<filter name="nocheck" description="prevents automatic URL sanitization" /> <filter name="nocheck" description="prevents automatic URL sanitization" />
<filter name="checkurl" description="sanitizes string for use inside href attribute" /> <filter name="checkurl" description="sanitizes string for use inside href attribute" />
<filter name="query" description="generates a query string in the URL" /> <filter name="query" description="generates a query string in the URL" />
<filter name="ceil" arguments=":(int $precision = 0)" description="rounds a number up to a given precision" /> <filter name="ceil" arguments=":(int $precision = 0)" description="rounds a number up to a given precision" />
<filter name="explode" arguments=":(string $separator = '')" description="splits a string by the given delimiter" /> <filter name="explode" arguments=":(string $separator = '')" description="splits a string by the given delimiter" />
<filter name="first" description="returns first element of array or character of string" /> <filter name="first" description="returns first element of array or character of string" />
<filter name="floor" arguments=":(int $precision = 0)" description="rounds a number down to a given precision" /> <filter name="floor" arguments=":(int $precision = 0)" description="rounds a number down to a given precision" />
<filter name="join" arguments=":(string $glue = '')" description="joins an array to a string" /> <filter name="join" arguments=":(string $glue = '')" description="joins an array to a string" />
<filter name="last" description="returns last element of array or character of string" /> <filter name="last" description="returns last element of array or character of string" />
<filter name="random" description="returns random element of array or character of string" /> <filter name="random" description="returns random element of array or character of string" />
<filter name="round" arguments=":(int $precision = 0)" description="rounds a number to a given precision" /> <filter name="round" arguments=":(int $precision = 0)" description="rounds a number to a given precision" />
<filter name="slice" arguments=":(int $start, int $length = null, bool $preserveKeys = false)" description="extracts a slice of an array or a string" insertColons=":" /> <filter name="slice" arguments=":(int $start, int $length = null, bool $preserveKeys = false)" description="extracts a slice of an array or a string" insertColons=":" />
<filter name="spaceless" description="removes whitespace" /> <filter name="spaceless" description="removes whitespace" />
<filter name="split" arguments=":(string $separator = '')" description="splits a string by the given delimiter" /> <filter name="split" arguments=":(string $separator = '')" description="splits a string by the given delimiter" />
</filters> </filters>
<functions> <functions>
<function name="clamp" returnType="int|float" arguments="(int|float $value, int|float $min, int|float $max)" description="clamps value to the inclusive range of min and max" /> <function name="clamp" returnType="int|float" arguments="(int|float $value, int|float $min, int|float $max)" description="clamps value to the inclusive range of min and max" />
<function name="divisibleBy" returnType="bool" arguments="(int $value)" description="checks if a variable is divisible by a number" /> <function name="divisibleBy" returnType="bool" arguments="(int $value)" description="checks if a variable is divisible by a number" />
<function name="even" returnType="bool" arguments="(int $value)" description="checks if the given number is even" /> <function name="even" returnType="bool" arguments="(int $value)" description="checks if the given number is even" />
<function name="first" returnType="mixed" arguments="(string|array $value)" description="returns first element of array or character of string" /> <function name="first" returnType="mixed" arguments="(string|array $value)" description="returns first element of array or character of string" />
<function name="last" returnType="mixed" arguments="(string|array $value)" description="returns last element of array or character of string" /> <function name="last" returnType="mixed" arguments="(string|array $value)" description="returns last element of array or character of string" />
<function name="odd" returnType="bool" arguments="(int $value)" description="checks if the given number is odd" /> <function name="odd" returnType="bool" arguments="(int $value)" description="checks if the given number is odd" />
<function name="slice" returnType="string|array" arguments="(string|array $value, int $start, int $length = null, bool $preserveKeys = false)" description="extracts a slice of an array or a string" /> <function name="slice" returnType="string|array" arguments="(string|array $value, int $start, int $length = null, bool $preserveKeys = false)" description="extracts a slice of an array or a string" />
</functions> </functions>
</latte> </latte>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager"> <component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/openvk.iml" filepath="$PROJECT_DIR$/.idea/openvk.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/openvk.iml" filepath="$PROJECT_DIR$/.idea/openvk.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View file

@ -1,46 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4"> <module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/vendor/rybakit/msgpack" /> <excludeFolder url="file://$MODULE_DIR$/vendor/rybakit/msgpack" />
<excludeFolder url="file://$MODULE_DIR$/vendor/chillerlan/php-qrcode" /> <excludeFolder url="file://$MODULE_DIR$/vendor/chillerlan/php-qrcode" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/chillerlan/php-settings-container" /> <excludeFolder url="file://$MODULE_DIR$/vendor/chillerlan/php-settings-container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vearutop/php-obscene-censor-rus" /> <excludeFolder url="file://$MODULE_DIR$/vendor/vearutop/php-obscene-censor-rus" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php72" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php72" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/scssphp/scssphp" /> <excludeFolder url="file://$MODULE_DIR$/vendor/scssphp/scssphp" />
<excludeFolder url="file://$MODULE_DIR$/vendor/bhaktaraz/php-rss-generator" /> <excludeFolder url="file://$MODULE_DIR$/vendor/bhaktaraz/php-rss-generator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/erusev/parsedown" /> <excludeFolder url="file://$MODULE_DIR$/vendor/erusev/parsedown" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ezyang/htmlpurifier" /> <excludeFolder url="file://$MODULE_DIR$/vendor/ezyang/htmlpurifier" />
<excludeFolder url="file://$MODULE_DIR$/vendor/whichbrowser/parser" /> <excludeFolder url="file://$MODULE_DIR$/vendor/whichbrowser/parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/komeiji-satori/curl" /> <excludeFolder url="file://$MODULE_DIR$/vendor/komeiji-satori/curl" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/james-heinrich/getid3" /> <excludeFolder url="file://$MODULE_DIR$/vendor/james-heinrich/getid3" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" /> <excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/binary-stream" /> <excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/binary-stream" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/al/emoji-detector" /> <excludeFolder url="file://$MODULE_DIR$/vendor/al/emoji-detector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/lfkeitel/phptotp" /> <excludeFolder url="file://$MODULE_DIR$/vendor/lfkeitel/phptotp" />
<excludeFolder url="file://$MODULE_DIR$/vendor/zadarma/user-api-v1" /> <excludeFolder url="file://$MODULE_DIR$/vendor/zadarma/user-api-v1" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php73" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php73" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/morphos" /> <excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/morphos" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View file

@ -1,48 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/vendor/rybakit/msgpack" /> <path value="$PROJECT_DIR$/vendor/rybakit/msgpack" />
<path value="$PROJECT_DIR$/vendor/chillerlan/php-qrcode" /> <path value="$PROJECT_DIR$/vendor/chillerlan/php-qrcode" />
<path value="$PROJECT_DIR$/vendor/psr/cache" /> <path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/chillerlan/php-settings-container" /> <path value="$PROJECT_DIR$/vendor/chillerlan/php-settings-container" />
<path value="$PROJECT_DIR$/vendor/vearutop/php-obscene-censor-rus" /> <path value="$PROJECT_DIR$/vendor/vearutop/php-obscene-censor-rus" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" /> <path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php72" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php72" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/scssphp/scssphp" /> <path value="$PROJECT_DIR$/vendor/scssphp/scssphp" />
<path value="$PROJECT_DIR$/vendor/bhaktaraz/php-rss-generator" /> <path value="$PROJECT_DIR$/vendor/bhaktaraz/php-rss-generator" />
<path value="$PROJECT_DIR$/vendor/erusev/parsedown" /> <path value="$PROJECT_DIR$/vendor/erusev/parsedown" />
<path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" /> <path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" />
<path value="$PROJECT_DIR$/vendor/whichbrowser/parser" /> <path value="$PROJECT_DIR$/vendor/whichbrowser/parser" />
<path value="$PROJECT_DIR$/vendor/komeiji-satori/curl" /> <path value="$PROJECT_DIR$/vendor/komeiji-satori/curl" />
<path value="$PROJECT_DIR$/vendor/composer" /> <path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/james-heinrich/getid3" /> <path value="$PROJECT_DIR$/vendor/james-heinrich/getid3" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" /> <path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" /> <path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/wapmorgan/binary-stream" /> <path value="$PROJECT_DIR$/vendor/wapmorgan/binary-stream" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" /> <path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/al/emoji-detector" /> <path value="$PROJECT_DIR$/vendor/al/emoji-detector" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" /> <path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
<path value="$PROJECT_DIR$/vendor/lfkeitel/phptotp" /> <path value="$PROJECT_DIR$/vendor/lfkeitel/phptotp" />
<path value="$PROJECT_DIR$/vendor/zadarma/user-api-v1" /> <path value="$PROJECT_DIR$/vendor/zadarma/user-api-v1" />
<path value="$PROJECT_DIR$/../../../chandler" /> <path value="$PROJECT_DIR$/../../../chandler" />
<path value="$PROJECT_DIR$/../../../vendor" /> <path value="$PROJECT_DIR$/../../../vendor" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/symfony/string" /> <path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php73" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/vendor/psr/container" /> <path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/console" /> <path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/wapmorgan/morphos" /> <path value="$PROJECT_DIR$/vendor/wapmorgan/morphos" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4"> <component name="PhpProjectSharedConfiguration" php_language_level="7.4">
<option name="suggestChangeDefaultLanguageLevel" value="false" /> <option name="suggestChangeDefaultLanguageLevel" value="false" />
</component> </component>
</project> </project>

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="RunConfigurationProducerService"> <component name="RunConfigurationProducerService">
<option name="ignoredProducers"> <option name="ignoredProducers">
<set> <set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" /> <option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set> </set>
</option> </option>
</component> </component>
</project> </project>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>
</project> </project>

View file

@ -1,82 +0,0 @@
FROM fedora:33
#update and install httpd
RUN dnf -y update && dnf -y autoremove && dnf install -y httpd
#Let's install Remi repos for PHP 7.4:
RUN dnf -y install https://rpms.remirepo.net/fedora/remi-release-$(rpm -E %fedora).rpm
#Then enable modules that we need:
RUN dnf -y module enable php:remi-7.4 && \
dnf -y module enable nodejs:14
#And install dependencies:
RUN dnf -y --skip-broken install php php-cli php-common unzip php-zip php-yaml php-gd php-pdo_mysql nodejs git
#Don't forget about Yarn and Composer:
RUN npm i -g yarn && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --filename=composer2 --install-dir=/bin --snapshot && \
rm composer-setup.php
#We will use Mariadb for DB:
RUN dnf -y install mysql mysql-server && \
systemctl enable mariadb && \
echo 'skip-grant-tables' >> /etc/my.cnf
#Additionally, you can install ffmpeg for processing videos.
#You will need to use RPMFusion repo to install it:
RUN dnf -y install --nogpgcheck https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm && \
dnf -y install --nogpgcheck https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
#Then install SDL2 and ffmpeg:
RUN dnf -y install --nogpgcheck SDL2 ffmpeg
#Install Chandler and OpenVk/Capcha-extention in /opt:
RUN cd /opt && \
git clone https://github.com/samukhin/chandler.git && \
cd chandler/ && \
composer2 install && \
mv chandler-example.yml chandler.yml && \
cd extensions/available/ && \
git clone https://github.com/samukhin/commitcaptcha.git && \
cd commitcaptcha/ && \
composer2 install && \
cd .. && \
git clone https://github.com/samukhin/openvk.git && \
cd openvk/ && \
composer2 install && \
cd Web/static/js && \
yarn install && \
cd ../../../ && \
mv openvk-example.yml openvk.yml && \
ln -s /opt/chandler/extensions/available/commitcaptcha/ /opt/chandler/extensions/enabled/commitcaptcha && \
ln -s /opt/chandler/extensions/available/openvk/ /opt/chandler/extensions/enabled/openvk
#Create database
RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/create_db.service /etc/systemd/system/ && \
chmod 644 /etc/systemd/system/create_db.service && \
chmod 777 /opt/chandler/extensions/available/openvk/install/automated/common/autoexec && \
systemctl enable create_db
#Make the user apache owner of the chandler folder:
RUN cd /opt && \
chown -R apache: chandler/
#Now let's create config file /etc/httpd/conf.d/10-openvk.conf and
#Also enable rewrite_module by creating /etc/httpd/conf.modules.d/02-rewrite.conf
RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/10-openvk.conf /etc/httpd/conf.d/ && \
cp /opt/chandler/extensions/available/openvk/install/automated/common/02-rewrite.conf /etc/httpd/conf.modules.d/
#Make directory for OpenVK logs and make the user apache owner of it:
RUN mkdir /var/log/openvk && \
chown apache: /var/log/openvk/
#And start Apache:
#RUN systemctl enable httpd
#For login
RUN dnf -y install passwd && passwd -d root
#Start systemd
CMD ["/sbin/init"]

View file

@ -72,6 +72,9 @@ Once you are done, you can login as a system administrator on the network itself
💡Confused? Full installation walkthrough is available [here](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)). 💡Confused? Full installation walkthrough is available [here](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)).
### 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 it's 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're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc).

View file

@ -72,6 +72,9 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). 💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)).
# Установка в Docker/Kubernetes
Подробные иструкции можно найти в `install/automated/docker/README.md` и `install/automated/kubernetes/README.md` соответственно.
### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты?
Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.). Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.).

View file

@ -54,6 +54,11 @@ class Apps implements Handler
$reject("No application with this id found"); $reject("No application with this id found");
return; return;
} }
if($amount < 0) {
$reject(552, "Payment amount is invalid");
return;
}
$coinsLeft = $this->user->getCoins() - $amount; $coinsLeft = $this->user->getCoins() - $amount;
if($coinsLeft < 0) { if($coinsLeft < 0) {

50
ServiceAPI/Mentions.php Normal file
View file

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Users, Clubs};
class Mentions implements Handler
{
protected $user;
function __construct(?User $user)
{
$this->user = $user;
}
function resolve(int $id, callable $resolve, callable $reject): void
{
if($id > 0) {
$user = (new Users)->get($id);
if(!$user) {
$reject("Not found");
return;
}
$resolve([
"url" => $user->getURL(),
"name" => $user->getFullName(),
"ava" => $user->getAvatarURL("miniscule"),
"about" => $user->getStatus() ?? "",
"online" => ($user->isFemale() ? tr("was_online_f") : tr("was_online_m")) . " " . $user->getOnline(),
"verif" => $user->isVerified(),
]);
return;
}
$club = (new Clubs)->get(abs($id));
if(!$club) {
$reject("Not found");
return;
}
$resolve([
"url" => $club->getURL(),
"name" => $club->getName(),
"ava" => $club->getAvatarURL("miniscule"),
"about" => $club->getDescription() ?? "",
"online" => tr("participants", $club->getFollowersCount()),
"verif" => $club->isVerified(),
]);
}
}

70
ServiceAPI/Polls.php Normal file
View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Chandler\MVC\Routing\Router;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Exceptions\{AlreadyVotedException, InvalidOptionException, PollLockedException};
use openvk\Web\Models\Repositories\Polls as PollRepo;
use UnexpectedValueException;
class Polls implements Handler
{
protected $user;
protected $polls;
function __construct(?User $user)
{
$this->user = $user;
$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
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$options = explode(",", $options);
$poll->vote($this->user, $options);
} catch(AlreadyVotedException $ex) {
$reject("Poll state changed: user has already voted.");
return;
} catch(PollLockedException $ex) {
$reject("Poll state changed: poll has ended.");
return;
} catch(InvalidOptionException $ex) {
$reject("Foreign options passed.");
return;
} catch(UnexpectedValueException $ex) {
$reject("Too much options passed.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
function unvote(int $pollId, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$poll->revokeVote($this->user);
} catch(PollLockedException $ex) {
$reject("Votes can't be revoked from this poll.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
}

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Exceptions\InvalidUserNameException;
final class Account extends VKAPIRequestHandler final class Account extends VKAPIRequestHandler
{ {
@ -13,7 +14,7 @@ final class Account extends VKAPIRequestHandler
"last_name" => $this->getUser()->getLastName(), "last_name" => $this->getUser()->getLastName(),
"home_town" => $this->getUser()->getHometown(), "home_town" => $this->getUser()->getHometown(),
"status" => $this->getUser()->getStatus(), "status" => $this->getUser()->getStatus(),
"bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'), "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", # TODO "phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(), "relation" => $this->getUser()->getMaritalStatus(),
@ -74,4 +75,77 @@ final class Account extends VKAPIRequestHandler
# TODO: Filter # TODO: Filter
} }
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
{
$this->requireUser();
$user = $this->getUser();
$output = [
"changed" => 0,
];
if(!empty($first_name) || !empty($last_name)) {
$output["name_request"] = [
"id" => random_int(1, 2048), # For compatibility with original VK API
"status" => "success",
"first_name" => !empty($first_name) ? $first_name : $user->getFirstName(),
"last_name" => !empty($last_name) ? $last_name : $user->getLastName(),
];
try {
if(!empty($first_name))
$user->setFirst_name($first_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))
$this->fail(1260, "Invalid screen name");
# For compatibility with original VK API
if($sex > 0)
$user->setSex($sex == 1 ? 1 : 0);
if($relation > -1)
$user->setMarital_Status($relation);
if(!empty($bdate)) {
$birthday = strtotime($bdate);
if (!is_int($birthday))
$this->fail(100, "invalid value of bdate.");
$user->setBirthday($birthday);
}
# For compatibility with original VK API
switch($bdate_visibility) {
case 0:
$this->fail(946, "Hiding date of birth is not implemented.");
break;
case 1:
$user->setBirthday_privacy(0);
break;
case 2:
$user->setBirthday_privacy(1);
}
if(!empty($home_town))
$user->setHometown($home_town);
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")) {
$output["changed"] = 1;
$user->save();
}
return (object) $output;
}
} }

View file

@ -107,7 +107,7 @@ final class Friends extends VKAPIRequestHandler
return 1; return 1;
default: default:
fail(15, "Access denied: No friend or friend request found."); $this->fail(15, "Access denied: No friend or friend request found.");
} }
} }
@ -133,15 +133,18 @@ final class Friends extends VKAPIRequestHandler
return $response; return $response;
} }
function getRequests(string $fields = "", int $offset = 0, int $count = 100): object 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.");
$this->requireUser(); $this->requireUser();
$i = 0; $i = 0;
$offset++; $offset++;
$followers = []; $followers = [];
foreach($this->getUser()->getFollowers() as $follower) { foreach($this->getUser()->getFollowers($offset, $count) as $follower) {
$followers[$i] = $follower->getId(); $followers[$i] = $follower->getId();
$i++; $i++;
} }
@ -149,8 +152,10 @@ final class Friends extends VKAPIRequestHandler
$response = $followers; $response = $followers;
$usersApi = new Users($this->getUser()); $usersApi = new Users($this->getUser());
if(!is_null($fields)) if($extended == 1)
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count); # FIXME $response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
else
$response = $usersApi->get(implode(',', $followers), "", 0, $count);
foreach($response as $user) foreach($response as $user)
$user->user_id = $user->id; $user->user_id = $user->id;

View file

@ -10,7 +10,7 @@ final class Groups extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
if($user_id == 0) { if($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club) foreach($this->getUser()->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $this->getUser()->getClubCount(); $clbsCount = $this->getUser()->getClubCount();
} else { } else {
@ -20,7 +20,7 @@ final class Groups extends VKAPIRequestHandler
if(is_null($user)) if(is_null($user))
$this->fail(15, "Access denied"); $this->fail(15, "Access denied");
foreach($user->getClubs($offset+1) as $club) foreach($user->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $user->getClubCount(); $clbsCount = $user->getClubCount();
@ -33,17 +33,9 @@ final class Groups extends VKAPIRequestHandler
$ic = $count; $ic = $count;
if(!empty($clbs)) { if(!empty($clbs)) {
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
$usr = $clbs[$i]; $usr = $clbs[$i];
if(is_null($usr)) { if(is_null($usr)) {
$rClubs[$i] = (object)[
"id" => $clbs[$i],
"name" => "DELETED",
"deactivated" => "deleted"
];
} else if($clbs[$i] == NULL) {
} else { } else {
$rClubs[$i] = (object) [ $rClubs[$i] = (object) [
@ -102,23 +94,32 @@ final class Groups extends VKAPIRequestHandler
]; ];
} }
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array 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,
not in your app or script, since it's not oficially documented by VK */
$clubs = new ClubsRepo; $clubs = new ClubsRepo;
if($group_ids == NULL && $group_id != NULL) if(empty($group_ids) && !empty($group_id))
$group_ids = $group_id; $group_ids = $group_id;
if($group_ids == NULL && $group_id == NULL) if(empty($group_ids) && empty($group_id))
$this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
$clbs = explode(',', $group_ids); $clbs = explode(',', $group_ids);
$response; $response = array();
$ic = sizeof($clbs); $ic = sizeof($clbs);
if(sizeof($clbs) > $count)
$ic = $count;
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
if($i > 500) if($i > 500 || $clbs[$i] == 0)
break; break;
if($clbs[$i] < 0) if($clbs[$i] < 0)
@ -142,6 +143,7 @@ final class Groups extends VKAPIRequestHandler
"screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
"is_closed" => false, "is_closed" => false,
"type" => "group", "type" => "group",
"is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0,
"can_access_closed" => true, "can_access_closed" => true,
]; ];
@ -204,10 +206,6 @@ final class Groups extends VKAPIRequestHandler
else else
$response[$i]->can_post = $clb->canPost(); $response[$i]->can_post = $clb->canPost();
break; break;
case "is_member":
if(!is_null($this->getUser()))
$response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser());
break;
} }
} }
} }
@ -215,4 +213,52 @@ final class Groups extends VKAPIRequestHandler
return $response; return $response;
} }
function search(string $q, int $offset = 0, int $count = 100)
{
$clubs = new ClubsRepo;
$array = [];
$find = $clubs->find($q);
foreach ($find 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
*/
];
}
function join(int $group_id)
{
$this->requireUser();
$club = (new ClubsRepo)->get($group_id);
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if($isMember == 0)
$club->toggleSubscription($this->getUser());
return 1;
}
function leave(int $group_id)
{
$this->requireUser();
$club = (new ClubsRepo)->get($group_id);
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if($isMember == 1)
$club->toggleSubscription($this->getUser());
return 1;
}
} }

View file

@ -152,6 +152,7 @@ final class Messages extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset); $convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset);
$convosCount = (new MSGRepo)->getCorrespondenciesCount($this->getUser());
$list = []; $list = [];
$users = []; $users = [];
@ -195,9 +196,8 @@ final class Messages extends VKAPIRequestHandler
$lastMessagePreview->body = $lastMessage->getText(false); $lastMessagePreview->body = $lastMessage->getText(false);
$lastMessagePreview->text = $lastMessage->getText(false); $lastMessagePreview->text = $lastMessage->getText(false);
$lastMessagePreview->emoji = true; $lastMessagePreview->emoji = true;
if($extended == 1) { if($extended == 1) {
$users[] = $lastMessage->getSender()->getId();
$users[] = $author; $users[] = $author;
} }
} }
@ -210,16 +210,17 @@ final class Messages extends VKAPIRequestHandler
if($extended == 0){ if($extended == 0){
return (object) [ return (object) [
"count" => sizeof($list), "count" => $convosCount,
"items" => $list, "items" => $list,
]; ];
} else { } else {
$users[] = $this->getUser()->getId();
$users = array_unique($users); $users = array_unique($users);
return (object) [ return (object) [
"count" => sizeof($list), "count" => $convosCount,
"items" => $list, "items" => $list,
"profiles" => (!empty($users) ? (new APIUsers)->get(implode(',', $users), $fields, $offset, $count) : []) "profiles" => (!empty($users) ? (new APIUsers)->get(implode(',', $users), $fields, 0, $count+1) : [])
]; ];
} }
} }
@ -246,32 +247,34 @@ final class Messages extends VKAPIRequestHandler
$user = (new USRRepo)->get((int) $peer); $user = (new USRRepo)->get((int) $peer);
$dialogue = new Correspondence($this->getUser(), $user); if($user) {
$iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false); $dialogue = new Correspondence($this->getUser(), $user);
$msg = $iterator[0]->unwrap(); // шоб удобнее было $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false);
$output['items'][] = [ $msg = $iterator[0]->unwrap(); // шоб удобнее было
"peer" => [ $output['items'][] = [
"id" => $user->getId(), "peer" => [
"type" => "user", "id" => $user->getId(),
"local_id" => $user->getId() "type" => "user",
], "local_id" => $user->getId()
"last_message_id" => $msg->id, ],
"in_read" => $msg->id, "last_message_id" => $msg->id,
"out_read" => $msg->id, "in_read" => $msg->id,
"sort_id" => [ "out_read" => $msg->id,
"major_id" => 0, "sort_id" => [
"minor_id" => $msg->id, // КОНЕЧНО ЖЕ "major_id" => 0,
], "minor_id" => $msg->id, // КОНЕЧНО ЖЕ
"last_conversation_message_id" => $user->getId(), ],
"in_read_cmid" => $user->getId(), "last_conversation_message_id" => $user->getId(),
"out_read_cmid" => $user->getId(), "in_read_cmid" => $user->getId(),
"is_marked_unread" => $iterator[0]->isUnread(), "out_read_cmid" => $user->getId(),
"important" => false, // целестора когда релиз "is_marked_unread" => $iterator[0]->isUnread(),
"can_write" => [ "important" => false, // целестора когда релиз
"allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true) "can_write" => [
] "allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true)
]; ]
$userslist[] = $user->getId(); ];
$userslist[] = $user->getId();
}
} }
if($extended == 1) { if($extended == 1) {
@ -283,7 +286,7 @@ final class Messages extends VKAPIRequestHandler
return (object) $output; 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): object 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(); $this->requireUser();
@ -315,10 +318,18 @@ final class Messages extends VKAPIRequestHandler
$results[] = $rMsg; $results[] = $rMsg;
} }
return (object) [ $output = [
"count" => sizeof($results), "count" => sizeof($results),
"items" => $results, "items" => $results,
]; ];
if ($extended == 1) {
$users[] = $this->getUser()->getId();
$users[] = $user_id;
$output["profiles"] = (!empty($users) ? (new APIUsers($this->getUser()))->get(implode(',', $users), $fields) : []);
}
return (object) $output;
} }
function getLongPollHistory(int $ts = -1, int $preview_length = 0, int $events_limit = 1000, int $msgs_limit = 1000): object function getLongPollHistory(int $ts = -1, int $preview_length = 0, int $events_limit = 1000, int $msgs_limit = 1000): object

View file

@ -2,6 +2,7 @@
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Entities\User;
use openvk\VKAPI\Handlers\Wall; use openvk\VKAPI\Handlers\Wall;
final class Newsfeed extends VKAPIRequestHandler final class Newsfeed extends VKAPIRequestHandler
@ -26,7 +27,7 @@ final class Newsfeed extends VKAPIRequestHandler
->select("id") ->select("id")
->where("wall IN (?)", $ids) ->where("wall IN (?)", $ids)
->where("deleted", 0) ->where("deleted", 0)
->where("created < (?)", empty($start_from) ? time()+1 : $start_from) ->where("id < (?)", empty($start_from) ? PHP_INT_MAX : $start_from)
->order("created DESC"); ->order("created DESC");
$rposts = []; $rposts = [];
@ -34,7 +35,33 @@ final class Newsfeed extends VKAPIRequestHandler
$rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId();
$response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser());
$response->next_from = end($response->items)->date; $response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно)
return $response;
}
function getGlobal(string $fields = "", int $start_from = 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;
$posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " AND `posts`.`id` < " . $start_from . " 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; return $response;
} }
} }

105
VKAPI/Handlers/Polls.php Executable file
View file

@ -0,0 +1,105 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Repositories\Polls as PollsRepo;
final class Polls extends VKAPIRequestHandler
{
function getById(int $poll_id, bool $extended = false, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online")
{
$poll = (new PollsRepo)->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();
$poll = (new PollsRepo)->get($poll_id);
if(!$poll)
$this->fail(251, "Invalid poll id");
try {
$poll->vote($this->getUser(), explode(",", $answers_ids));
return 0;
} catch(AlreadyVotedException $ex) {
return 0;
} catch(PollLockedException $ex) {
return 0;
} catch(InvalidOptionException $ex) {
$this->fail(8, "бдсм вибратор купить в киеве");
}
}
function deleteVote(int $poll_id)
{
$this->requireUser();
$poll = (new PollsRepo)->get($poll_id);
if(!$poll)
$this->fail(251, "Invalid poll id");
try {
$poll->revokeVote($this->getUser());
return 0;
} 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.");
}
}
}

View file

@ -21,10 +21,17 @@ final class Wall extends VKAPIRequestHandler
$groups = []; $groups = [];
$cnt = $posts->getPostCountOnUserWall($owner_id); $cnt = $posts->getPostCountOnUserWall($owner_id);
$wallOnwer = (new UsersRepo)->get($owner_id); if ($owner_id > 0)
$wallOnwer = (new UsersRepo)->get($owner_id);
else
$wallOnwer = (new ClubsRepo)->get($owner_id * -1);
if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted()) if ($owner_id > 0)
$this->fail(18, "User was deleted or banned"); 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) { foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
@ -37,12 +44,14 @@ final class Wall extends VKAPIRequestHandler
continue; continue;
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) { foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted()) if($repostAttachment->isDeleted())
continue; continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment); $repostAttachments[] = $this->getApiPhoto($repostAttachment);
@ -50,6 +59,11 @@ final class Wall extends VKAPIRequestHandler
} }
} }
if ($attachment->isPostedOnBehalfOfGroup())
$groups[] = $attachment->getOwner()->getId();
else
$profiles[] = $attachment->getOwner()->getId();
$repost[] = [ $repost[] = [
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
"owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
@ -178,6 +192,8 @@ final class Wall extends VKAPIRequestHandler
foreach($post->getChildren() as $attachment) { foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -189,7 +205,12 @@ final class Wall extends VKAPIRequestHandler
$repostAttachments[] = $this->getApiPhoto($repostAttachment); $repostAttachments[] = $this->getApiPhoto($repostAttachment);
/* Рекурсии, сука! Заказывали? */ /* Рекурсии, сука! Заказывали? */
} }
} }
if ($attachment->isPostedOnBehalfOfGroup())
$groups[] = $attachment->getOwner()->getId();
else
$profiles[] = $attachment->getOwner()->getId();
$repost[] = [ $repost[] = [
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
@ -420,9 +441,14 @@ final class Wall extends VKAPIRequestHandler
$profiles = []; $profiles = [];
foreach($comments as $comment) { foreach($comments as $comment) {
$owner = $comment->getOwner();
$oid = $owner->getId();
if($owner instanceof Club)
$oid *= -1;
$item = [ $item = [
"id" => $comment->getId(), "id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(), "from_id" => $oid,
"date" => $comment->getPublicationTime()->timestamp(), "date" => $comment->getPublicationTime()->timestamp(),
"text" => $comment->getText(false), "text" => $comment->getText(false),
"post_id" => $post->getVirtualId(), "post_id" => $post->getVirtualId(),
@ -561,7 +587,7 @@ final class Wall extends VKAPIRequestHandler
return 1; return 1;
} }
private function getApiPhoto($attachment) { private function getApiPhoto($attachment) {
return [ return [
"type" => "photo", "type" => "photo",
@ -576,4 +602,44 @@ final class Wall extends VKAPIRequestHandler
] ]
]; ];
} }
private function getApiPoll($attachment, $user) {
$answers = array();
foreach($attachment->getResults()->options as $answer) {
$answers[] = (object)[
"id" => $answer->id,
"rate" => $answer->pct,
"text" => $answer->name,
"votes" => $answer->votes
];
}
$userVote = array();
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(),
"closed" => $attachment->hasEnded(),
"is_board" => false,
"can_edit" => false,
"can_vote" => $attachment->canVote($user),
"can_report" => false,
"can_share" => true,
"created" => 0,
"id" => $attachment->getId(),
"owner_id" => $attachment->getOwner()->getId(),
"question" => $attachment->getTitle(),
"votes" => $attachment->getVoterCount(),
"disable_unvote" => $attachment->isRevotable(),
"anonymous" => $attachment->isAnonymous(),
"answer_ids" => $userVote,
"answers" => $answers,
"author_id" => $attachment->getOwner()->getId(),
]
];
}
} }

View file

@ -1,10 +1,12 @@
# VK API Compatability layer for OpenVK # VK API Compatability layer for OpenVK
This directory contains VK api handlers, structures and relared This directory contains VK API handlers, structures and relared
exceptions. It is still a work-in-progress functionality. exceptions. It is still a work-in-progress functionality.
**Note**: requests to api are routed through **Note**: requests to API are routed through
openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers.
[Documentation for API clients](https://docs.openvk.su/openvk_engine/api/description/)
## Implementing API methods ## Implementing API methods
VK API methods have names like this: `example.test`. To implement a VK API methods have names like this: `example.test`. To implement a

View file

@ -27,14 +27,23 @@ class NewMessageEvent implements ILPEmitable
if($peer === $userId) if($peer === $userId)
$peer = $msg->getRecipient()->getId(); $peer = $msg->getRecipient()->getId();
/*
* Source:
* https://github.com/danyadev/longpoll-doc
*/
return [ return [
4, # event type 4, # event type
$msg->getId(), # messageId
256, # checked for spam flag 256, # checked for spam flag
$peer, # TODO calculate peer correctly $peer, # TODO calculate peer correctly
$msg->getSendTime()->timestamp(), # creation time in unix $msg->getSendTime()->timestamp(), # creation time in unix
$msg->getText(), # text (formatted) $msg->getText(), # text (formatted)
[], # empty additional info
[], # empty attachments [], # empty attachments
$msg->getId() << 2, # id as random_id $msg->getId() << 2, # id as random_id
$peer, # conversation id
0 # not edited yet
]; ];
} }
} }

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs};
class Alias extends RowModel
{
protected $tableName = "aliases";
function getOwnerId(): int
{
return $this->getRecord()->owner_id;
}
function getType(): string
{
if ($this->getOwnerId() < 0)
return "club";
return "user";
}
function getUser(): ?User
{
return (new Users)->get($this->getOwnerId());
}
function getClub(): ?Club
{
return (new Clubs)->get($this->getOwnerId() * -1);
}
}

View file

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

View file

@ -0,0 +1,295 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Util\DateTime;
use \UnexpectedValueException;
use Nette\InvalidStateException;
use openvk\Web\Models\Repositories\Users;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
class Poll extends Attachable
{
protected $tableName = "polls";
private $choicesToPersist = [];
function getTitle(): string
{
return $this->getRecord()->title;
}
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());
return implode("", $props);
}
function getOwner(): User
{
return (new Users)->get($this->getRecord()->owner);
}
function getOptions(): array
{
$options = $this->getRecord()->related("poll_options.poll");
$res = [];
foreach($options as $opt)
$res[$opt->id] = $opt->name;
return $res;
}
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;
$res = [];
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
{
$res = [];
$ctx = DatabaseConnection::i()->getContext();
$perPage = $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);
return $res;
}
function getVoterCount(?int $optionId = NULL): int
{
$votes = DatabaseConnection::i()->getContext()->table("poll_votes");
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();
}
function getResults(?User $user = NULL): object
{
$ctx = DatabaseConnection::i()->getContext();
$voted = NULL;
if(!is_null($user))
$voted = $this->getUserVote($user);
$result = (object) [];
$result->totalVotes = $this->getVoterCount();
$unsOptions = [];
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
$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++)
$unsOptions[$id]->rate = $optionsC - $i - 1;
$result->options = array_values($unsOptions);
return $result;
}
function isAnonymous(): bool
{
return (bool) $this->getRecord()->is_anonymous;
}
function isMultipleChoice(): bool
{
return (bool) $this->getRecord()->allows_multiple;
}
function isRevotable(): bool
{
return (bool) $this->getRecord()->can_revote;
}
function endsAt(): ?DateTime
{
if(!$this->getRecord()->until)
return NULL;
return new DateTime($this->getRecord()->until);
}
function hasEnded(): bool
{
if($this->getRecord()->ended)
return true;
if(!is_null($this->getRecord()->until))
return time() >= $this->getRecord()->until;
return false;
}
function hasVoted(User $user): bool
{
return !is_null($this->getUserVote($user));
}
function canVote(User $user): bool
{
return !$this->hasEnded() && !$this->hasVoted($user);
}
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));
$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) {
DatabaseConnection::i()->getContext()->table("poll_votes")->insert([
"user" => $user->getId(),
"poll" => $this->getId(),
"option" => $opt,
]);
}
}
function revokeVote(User $user): void
{
if(!$this->isRevotable())
throw new PollLockedException;
$this->getRecord()->related("poll_votes.poll")
->where("user", $user->getId())->delete();
}
function setOwner(User $owner): void
{
$this->stateChanges("owner", $owner->getId());
}
function setEndDate(int $timestamp): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("until", $timestamp);
}
function setEnded(): void
{
$this->stateChanges("ended", 1);
}
function setOptions(array $options): void
{
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
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("can_revote", $canReVote);
}
function setAnonymity(bool $anonymous): void
{
$this->stateChanges("is_anonymous", $anonymous);
}
function setMultipleChoice(bool $mc): void
{
$this->stateChanges("allows_multiple", $mc);
}
function importXML(User $owner, string $xml): void
{
$xml = simplexml_load_string($xml);
$this->setOwner($owner);
$this->setTitle($xml["title"] ?? "Untitled");
$this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes");
$this->setAnonymity(($xml["anonymous"] ?? "no") == "yes");
$this->setRevotability(($xml["locked"] ?? "no") == "no");
if(ctype_digit((string) ($xml["duration"] ?? "")))
$this->setEndDate(time() + ((86400 * (int) $xml["duration"])));
$options = [];
foreach($xml->options->option as $opt)
$options[] = (string) $opt;
if(empty($options))
throw new UnexpectedValueException;
$this->setOptions($options);
}
static function import(User $owner, string $xml): Poll
{
$poll = new Poll;
$poll->importXML($owner, $xml);
$poll->save();
return $poll;
}
function save(): void
{
if(empty($this->choicesToPersist))
throw new InvalidStateException;
parent::save();
foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(),
"name" => $option,
]);
}
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\{Clubs, Users};
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Notifications\LikeNotification; use openvk\Web\Models\Entities\Notifications\LikeNotification;
@ -55,6 +55,15 @@ class Post extends Postable
{ {
return $this->getRecord()->wall; return $this->getRecord()->wall;
} }
function getWallOwner()
{
$w = $this->getRecord()->wall;
if($w < 0)
return (new Clubs)->get(abs($w));
return (new Users)->get($w);
}
function getRepostCount(): int function getRepostCount(): int
{ {
@ -87,7 +96,7 @@ class Post extends Postable
function isDeactivationMessage(): bool function isDeactivationMessage(): bool
{ {
return ($this->getRecord()->flags & 0b00100000) > 0; return (($this->getRecord()->flags & 0b00100000) > 0) && ($this->getRecord()->owner > 0);
} }
function isExplicit(): bool function isExplicit(): bool

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
class SupportAgent extends RowModel
{
protected $tableName = "support_names";
function getAgentId(): int
{
return $this->getRecord()->agent;
}
function getName(): ?string
{
return $this->getRecord()->name;
}
function getCanonicalName(): string
{
return $this->getName();
}
function getAvatarURL(): ?string
{
return $this->getRecord()->icon;
}
function isShowNumber(): int
{
return $this->getRecord()->numerate;
}
function getRealName(): string
{
return (new Users)->get($this->getAgentId())->getCanonicalName();
}
}

View file

@ -42,7 +42,7 @@ class TicketComment extends RowModel
$alias = $this->getSupportAlias(); $alias = $this->getSupportAlias();
if(!$alias) if(!$alias)
return OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"] . "" . $this->getAgentNumber(); return tr("helpdesk_agent") . " #" . $this->getAgentNumber();
$name = $alias->getName(); $name = $alias->getName();
if($alias->shouldAppendNumber()) if($alias->shouldAppendNumber())

View file

@ -35,12 +35,12 @@ trait TRichText
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%", "%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string { (function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]); $href = str_replace("#", "&num;", $matches[1]);
$href = str_replace(";", "&#59;", $matches[1]); $href = rawurlencode(str_replace(";", "&#59;", $matches[1]));
$link = str_replace("#", "&num;", $matches[3]); $link = str_replace("#", "&num;", $matches[3]);
$link = str_replace(";", "&#59;", $matches[3]); $link = str_replace(";", "&#59;", $matches[3]);
$rel = $this->isAd() ? "sponsored" : "ugc"; $rel = $this->isAd() ? "sponsored" : "ugc";
return "<a href='$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]); return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
}), }),
$text $text
); );

View file

@ -148,8 +148,9 @@ class User extends RowModel
function getFirstName(bool $pristine = false): string function getFirstName(bool $pristine = false): string
{ {
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE)); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) $tsn = tr("__transNames");
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine)
return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE);
else else
return $name; return $name;
} }
@ -157,8 +158,9 @@ class User extends RowModel
function getLastName(bool $pristine = false): string function getLastName(bool $pristine = false): string
{ {
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE)); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) $tsn = tr("__transNames");
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine)
return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE);
else else
return $name; return $name;
} }
@ -535,12 +537,15 @@ class User extends RowModel
return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1]));
} }
function getClubs(int $page = 1, bool $admin = false): \Traversable function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable
{ {
if(!$offset)
$page = ($page - 1) * $count;
if($admin) { if($admin) {
$id = $this->getId(); $id = $this->getId();
$query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?"; $query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?";
$query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE; $query .= " LIMIT " . $count . " OFFSET " . $page;
$sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id); $sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($sel as $target) { foreach($sel as $target) {
@ -550,7 +555,7 @@ class User extends RowModel
yield $target; yield $target;
} }
} else { } else {
$sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE); $sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page);
foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) {
$target = (new Clubs)->get($target->target); $target = (new Clubs)->get($target->target);
if(!$target) continue; if(!$target) continue;
@ -768,7 +773,7 @@ class User extends RowModel
]); ]);
} }
function ban(string $reason, bool $deleteSubscriptions = true): void function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void
{ {
if($deleteSubscriptions) { if($deleteSubscriptions) {
$subs = DatabaseConnection::i()->getContext()->table("subscriptions"); $subs = DatabaseConnection::i()->getContext()->table("subscriptions");
@ -782,6 +787,7 @@ class User extends RowModel
} }
$this->setBlock_Reason($reason); $this->setBlock_Reason($reason);
$this->setUnblock_time($unban_time);
$this->save(); $this->save();
} }
@ -907,6 +913,10 @@ class User extends RowModel
$pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch(); $pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch();
if(!is_null($pClub)) if(!is_null($pClub))
return false; return false;
$pAlias = DatabaseConnection::i()->getContext()->table("aliases")->where("shortcode", $code)->fetch();
if(!is_null($pAlias))
return false;
} }
$this->stateChanges("shortcode", $code); $this->stateChanges("shortcode", $code);
@ -1017,6 +1027,22 @@ class User extends RowModel
{ {
return (bool) $this->getRecord()->activated; return (bool) $this->getRecord()->activated;
} }
function getUnbanTime(): ?string
{
return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL;
}
function canUnbanThemself(): bool
{
if (!$this->isBanned())
return false;
if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0)
return false;
return true;
}
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class AlreadyVotedException extends \RuntimeException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class InvalidOptionException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
use Nette\InvalidStateException;
final class PollLockedException extends InvalidStateException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class TooMuchOptionsException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Alias;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\{Club, User};
use openvk\Web\Models\Repositories\{Clubs, Users};
class Aliases
{
private $context;
private $aliases;
function __construct()
{
$this->context = DB::i()->getContext();
$this->aliases = $this->context->table("aliases");
}
private function toAlias(?ActiveRow $ar): ?Alias
{
return is_null($ar) ? NULL : new Alias($ar);
}
function get(int $id): ?Alias
{
return $this->toAlias($this->aliases->get($id));
}
function getByShortcode(string $shortcode): ?Alias
{
return $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch());
}
}

View file

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

View file

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\User;
use Chandler\Security\User as ChandlerUser;
class ChandlerGroups
{
private $context;
private $groups;
public function __construct()
{
$this->context = DB::i()->getContext();
$this->groups = $this->context->table("ChandlerGroups");
$this->members = $this->context->table("ChandlerACLRelations");
$this->perms = $this->context->table("ChandlerACLGroupsPermissions");
}
function get(string $UUID): ?ActiveRow
{
return $this->groups->where("id", $UUID)->fetch();
}
function getList(): \Traversable
{
foreach($this->groups as $group) yield $group;
}
function getMembersById(string $UUID): \Traversable
{
foreach($this->members->where("group", $UUID) as $member)
yield (new Users)->getByChandlerUser(
new ChandlerUser($this->context->table("ChandlerUsers")->where("id", $member->user)->fetch())
);
}
function getUsersMemberships(string $UUID): \Traversable
{
foreach($this->members->where("user", $UUID) as $member) yield $member;
}
function getPermissionsById(string $UUID): \Traversable
{
foreach($this->perms->where("group", $UUID) as $perm) yield $perm;
}
}

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\User;
use Chandler\Security\User as ChandlerUser;
class ChandlerUsers
{
private $context;
private $users;
public function __construct()
{
$this->context = DB::i()->getContext();
$this->users = $this->context->table("ChandlerUsers");
}
private function toUser(?ActiveRow $ar): ?ChandlerUser
{
return is_null($ar) ? NULL : (new User($ar))->getChandlerUser();
}
function get(int $id): ?ChandlerUser
{
return (new Users)->get($id)->getChandlerUser();
}
function getById(string $UUID): ?ChandlerUser
{
return new ChandlerUser($this->users->where("id", $UUID)->fetch());
}
function getList(int $page = 1): \Traversable
{
foreach($this->users as $user)
yield new ChandlerUser($user);
}
}

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -22,7 +23,17 @@ class Clubs
function getByShortURL(string $url): ?Club function getByShortURL(string $url): ?Club
{ {
return $this->toClub($this->clubs->where("shortcode", $url)->fetch()); $shortcode = $this->toClub($this->clubs->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "club") return NULL;
return $alias->getClub();
} }
function get(int $id): ?Club function get(int $id): ?Club
@ -45,6 +56,9 @@ class Clubs
function getPopularClubs(): \Traversable 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;"; $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); $entries = DatabaseConnection::i()->getConnection()->query($query);
@ -54,6 +68,7 @@ class Clubs
"club" => $this->get($entry["id"]), "club" => $this->get($entry["id"]),
"subscriptions" => $entry["subscriptions"], "subscriptions" => $entry["subscriptions"],
]; ];
*/
} }
use \Nette\SmartObject; use \Nette\SmartObject;

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Poll;
class Polls
{
private $polls;
function __construct()
{
$this->polls = DatabaseConnection::i()->getContext()->table("polls");
}
function get(int $id): ?Poll
{
$poll = $this->polls->get($id);
if(!$poll)
return NULL;
return new Poll($poll);
}
}

View file

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\{User, SupportAgent};
class SupportAgents
{
private $context;
private $tickets;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->agents = $this->context->table("support_names");
}
private function toAgent(?ActiveRow $ar)
{
return is_null($ar) ? NULL : new SupportAgent($ar);
}
function get(int $id): ?SupportAgent
{
return $this->toAgent($this->agents->where("agent", $id)->fetch());
}
function isExists(int $id): bool
{
return !is_null($this->get($id));
}
}

View file

@ -27,6 +27,13 @@ class TicketComments
else else
return NULL; return NULL;
} }
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; use \Nette\SmartObject;
} }

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser; use Chandler\Security\User as ChandlerUser;
@ -9,11 +10,13 @@ class Users
{ {
private $context; private $context;
private $users; private $users;
private $aliases;
function __construct() function __construct()
{ {
$this->context = DatabaseConnection::i()->getContext(); $this->context = DatabaseConnection::i()->getContext();
$this->users = $this->context->table("profiles"); $this->users = $this->context->table("profiles");
$this->aliases = $this->context->table("aliases");
} }
private function toUser(?ActiveRow $ar): ?User private function toUser(?ActiveRow $ar): ?User
@ -28,7 +31,17 @@ class Users
function getByShortURL(string $url): ?User function getByShortURL(string $url): ?User
{ {
return $this->toUser($this->users->where("shortcode", $url)->fetch()); $shortcode = $this->toUser($this->users->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "user") return NULL;
return $alias->getUser();
} }
function getByChandlerUser(ChandlerUser $user): ?User function getByChandlerUser(ChandlerUser $user): ?User

View file

@ -10,6 +10,8 @@ SELECT DISTINCT id, class FROM
sender_id = ? sender_id = ?
AND AND
sender_type = ? sender_type = ?
AND
deleted = 0
) UNION ( ) UNION (
SELECT SELECT
sender_id AS id, sender_id AS id,
@ -20,6 +22,8 @@ SELECT DISTINCT id, class FROM
recipient_id = ? recipient_id = ?
AND AND
recipient_type = ? recipient_type = ?
AND
deleted = 0
) )
ORDER BY ORDER BY
time time

View file

@ -64,7 +64,7 @@ final class AboutPresenter extends OpenVKPresenter
$this->template->usersStats = (new Users)->getStatistics(); $this->template->usersStats = (new Users)->getStatistics();
$this->template->clubsCount = (new Clubs)->getCount(); $this->template->clubsCount = (new Clubs)->getCount();
$this->template->postsCount = (new Posts)->getCount(); $this->template->postsCount = (new Posts)->getCount();
$this->template->popularClubs = iterator_to_array((new Clubs)->getPopularClubs()); $this->template->popularClubs = [];
$this->template->admins = iterator_to_array((new Users)->getInstanceAdmins()); $this->template->admins = iterator_to_array((new Users)->getInstanceAdmins());
} }
@ -76,6 +76,9 @@ final class AboutPresenter extends OpenVKPresenter
$this->assertNoCSRF(); $this->assertNoCSRF();
setLanguage($_GET['lg']); setLanguage($_GET['lg']);
} }
if(!is_null($_GET['jReturnTo']))
$this->redirect(rawurldecode($_GET['jReturnTo']));
} }
function renderExportJSLanguage($lg = NULL): void function renderExportJSLanguage($lg = NULL): void

View file

@ -1,7 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User}; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts}; use openvk\Web\Models\Repositories\{ChandlerGroups, ChandlerUsers, Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
{ {
@ -9,13 +10,17 @@ final class AdminPresenter extends OpenVKPresenter
private $clubs; private $clubs;
private $vouchers; private $vouchers;
private $gifts; private $gifts;
private $bannedLinks;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts) private $chandlerGroups;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
$this->vouchers = $vouchers; $this->vouchers = $vouchers;
$this->gifts = $gifts; $this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups;
parent::__construct(); parent::__construct();
} }
@ -59,7 +64,9 @@ final class AdminPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
$this->template->user = $user; $this->template->user = $user;
$this->template->c_groups_list = (new ChandlerGroups)->getList();
$this->template->c_memberships = $this->chandlerGroups->getUsersMemberships($user->getChandlerGUID());
if($_SERVER["REQUEST_METHOD"] !== "POST") if($_SERVER["REQUEST_METHOD"] !== "POST")
return; return;
@ -75,8 +82,13 @@ final class AdminPresenter extends OpenVKPresenter
$user->changeEmail($this->postParam("email")); $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)); $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);
}
$user->save(); $user->save();
break; break;
} }
} }
@ -339,12 +351,14 @@ final class AdminPresenter extends OpenVKPresenter
function renderQuickBan(int $id): void function renderQuickBan(int $id): void
{ {
$this->assertNoCSRF(); $this->assertNoCSRF();
$unban_time = strtotime($this->queryParam("date")) ?: NULL;
$user = $this->users->get($id); $user = $this->users->get($id);
if(!$user) if(!$user)
exit(json_encode([ "error" => "User does not exist" ])); exit(json_encode([ "error" => "User does not exist" ]));
$user->ban($this->queryParam("reason")); $user->ban($this->queryParam("reason"), true, $unban_time);
exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ]));
} }
@ -356,7 +370,8 @@ final class AdminPresenter extends OpenVKPresenter
if(!$user) if(!$user)
exit(json_encode([ "error" => "User does not exist" ])); exit(json_encode([ "error" => "User does not exist" ]));
$user->setBlock_Reason(null); $user->setBlock_Reason(NULL);
$user->setUnblock_time(NULL);
$user->save(); $user->save();
exit(json_encode([ "success" => true ])); exit(json_encode([ "success" => true ]));
} }
@ -372,4 +387,164 @@ final class AdminPresenter extends OpenVKPresenter
$user->adminNotify("⚠️ " . $this->queryParam("message")); $user->adminNotify("⚠️ " . $this->queryParam("message"));
exit(json_encode([ "message" => $this->queryParam("message") ])); exit(json_encode([ "message" => $this->queryParam("message") ]));
} }
function renderBannedLinks(): void
{
$this->template->links = $this->bannedLinks->getList((int) $this->queryParam("p") ?: 1);
$this->template->users = new Users;
}
function renderBannedLink(int $id): void
{
$this->template->form = (object) [];
if($id === 0) {
$this->template->form->id = 0;
$this->template->form->link = NULL;
$this->template->form->reason = NULL;
} else {
$link = (new BannedLinks)->get($id);
if(!$link)
$this->notFound();
$this->template->form->id = $link->getId();
$this->template->form->link = $link->getDomain();
$this->template->form->reason = $link->getReason();
$this->template->form->regexp = $link->getRawRegexp();
}
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$link = (new BannedLinks)->get($id);
$new_domain = parse_url($this->postParam("link"))["host"];
$new_reason = $this->postParam("reason") ?: NULL;
$lid = $id;
if ($link) {
$link->setDomain($new_domain ?? $this->postParam("link"));
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->save();
} else {
if (!$new_domain)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_specified"));
$link = new BannedLink;
$link->setDomain($new_domain);
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->setInitiator($this->user->identity->getId());
$link->save();
$lid = $link->getId();
}
$this->redirect("/admin/bannedLink/id" . $lid);
}
function renderUnbanLink(int $id): void
{
$link = (new BannedLinks)->get($id);
if (!$link)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_found"));
$link->delete(false);
$this->redirect("/admin/bannedLinks");
}
function renderChandlerGroups(): void
{
$this->template->groups = (new ChandlerGroups)->getList();
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
{
$DB = DatabaseConnection::i()->getConnection();
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->mode = in_array(
$this->queryParam("act"),
[
"main",
"members",
"permissions",
"removeMember",
"removePermission",
"delete"
]) ? $this->queryParam("act") : "main";
$this->template->members = (new ChandlerGroups)->getMembersById($UUID);
$this->template->perms = (new ChandlerGroups)->getPermissionsById($UUID);
if($this->template->mode == "removeMember") {
$where = "`user` = '" . $this->queryParam("uid") . "' AND `group` = '$UUID'";
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'";
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") {
$DB->query("DELETE FROM `ChandlerGroups` WHERE `id` = '$UUID'");
$DB->query("DELETE FROM `ChandlerACLGroupsPermissions` WHERE `group` = '$UUID'");
$DB->query("DELETE FROM `ChandlerACLRelations` WHERE `group` = '$UUID'");
$this->flashFail("succ", tr("changes_saved"), tr("c_group_removed"));
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") return;
$req = "";
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'";
if($this->template->mode == "members")
if($this->postParam("uid"))
if(!is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE `user` = '" . $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)";
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
{
if(!$UUID) $this->notFound();
$c_user = (new ChandlerUsers())->getById($UUID);
$user = $this->users->getByChandlerUser($c_user);
if(!$user) $this->notFound();
$this->redirect("/admin/users/id" . $user->getId());
}
} }

View file

@ -6,7 +6,7 @@ use openvk\Web\Models\Repositories\Applications;
final class AppsPresenter extends OpenVKPresenter final class AppsPresenter extends OpenVKPresenter
{ {
private $apps; private $apps;
protected $presenterName = "apps";
function __construct(Applications $apps) function __construct(Applications $apps)
{ {
$this->apps = $apps; $this->apps = $apps;

View file

@ -84,6 +84,9 @@ final class AuthPresenter extends OpenVKPresenter
if (strtotime($this->postParam("birthday")) > time()) if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment"));
if (!$this->postParam("confirmation"))
$this->flashFail("err", tr("error"), tr("checkbox_in_registration_unchecked"));
try { try {
$user = new User; $user = new User;
$user->setFirst_Name($this->postParam("first_name")); $user->setFirst_Name($this->postParam("first_name"));
@ -322,4 +325,39 @@ final class AuthPresenter extends OpenVKPresenter
$this->redirect("/"); $this->redirect("/");
} }
function renderUnbanThemself(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if(!$this->user->identity->canUnbanThemself())
$this->flashFail("err", tr("error"), tr("forbidden"));
$user = $this->users->get($this->user->id);
$user->setBlock_Reason(NULL);
$user->setUnblock_Time(NULL);
$user->save();
$this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description"));
}
/*
* This function will revoke all tokens, including API and Web tokens and except active one
*
* OF COURSE it requires CSRF
*/
function renderRevokeAllTokens(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
// API tokens
$this->db->table("api_tokens")->where("user", $this->user->identity->getId())->delete();
// Web tokens
$this->db->table("ChandlerTokens")->where("user", $this->user->identity->getChandlerGUID())->where("token != ?", Session::i()->get("tok"))->delete();
$this->flashFail("succ", tr("information_-1"), tr("end_all_sessions_done"));
}
} }

View file

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

View file

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

View file

@ -6,6 +6,7 @@ use openvk\Web\Models\Repositories\{Comments, Clubs};
final class CommentPresenter extends OpenVKPresenter final class CommentPresenter extends OpenVKPresenter
{ {
protected $presenterName = "comment";
private $models = [ private $models = [
"posts" => "openvk\\Web\\Models\\Repositories\\Posts", "posts" => "openvk\\Web\\Models\\Repositories\\Posts",
"photos" => "openvk\\Web\\Models\\Repositories\\Photos", "photos" => "openvk\\Web\\Models\\Repositories\\Photos",

View file

@ -7,6 +7,7 @@ final class GiftsPresenter extends OpenVKPresenter
{ {
private $gifts; private $gifts;
private $users; private $users;
protected $presenterName = "gifts";
function __construct(Gifts $gifts, Users $users) function __construct(Gifts $gifts, Users $users)
{ {

View file

@ -8,7 +8,8 @@ use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter final class GroupPresenter extends OpenVKPresenter
{ {
private $clubs; private $clubs;
protected $presenterName = "group";
function __construct(Clubs $clubs) function __construct(Clubs $clubs)
{ {
$this->clubs = $clubs; $this->clubs = $clubs;

View file

@ -29,9 +29,10 @@ final class InternalAPIPresenter extends OpenVKPresenter
function renderRoute(): void function renderRoute(): void
{ {
if($_SERVER["REQUEST_METHOD"] !== "POST") if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("ты дебил это точка апи"); exit("ты дебил это точка апи");
}
try { try {
$input = (object) MessagePack::unpack(file_get_contents("php://input")); $input = (object) MessagePack::unpack(file_get_contents("php://input"));
} catch (\Exception $ex) { } catch (\Exception $ex) {
@ -71,9 +72,10 @@ final class InternalAPIPresenter extends OpenVKPresenter
} }
function renderTimezone() { function renderTimezone() {
if($_SERVER["REQUEST_METHOD"] !== "POST") if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("ты дебил это метод апи"); exit("ты дебил это метод апи");
}
$sessionOffset = Session::i()->get("_timezoneOffset"); $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)); $postTZ = intval($this->postParam("timezone", false));

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Presenters;
final class MaintenancePresenter extends OpenVKPresenter
{
protected $presenterName = "maintenance";
function renderSection(string $name): void
{
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$name])
$this->flashFail("err", tr("error"), tr("forbidden"));
$this->template->name = [
"photos" => tr("my_photos"),
"videos" => tr("my_videos"),
"messenger" => tr("my_messages"),
"user" => tr("users"),
"group" => tr("my_groups"),
"comment" => tr("comments"),
"gifts" => tr("gifts"),
"apps" => tr("apps"),
"notes" => tr("my_notes"),
"notification" => tr("my_feedback"),
"support" => tr("menu_support"),
"topics" => tr("topics")
][$name] ?? $name;
}
function renderAll(): void
{
}
}

View file

@ -9,11 +9,13 @@ final class MessengerPresenter extends OpenVKPresenter
{ {
private $messages; private $messages;
private $signaler; private $signaler;
protected $presenterName = "messenger";
function __construct(Messages $messages) function __construct(Messages $messages)
{ {
$this->messages = $messages; $this->messages = $messages;
$this->signaler = SignalManager::i(); $this->signaler = SignalManager::i();
parent::__construct(); parent::__construct();
} }
@ -30,7 +32,7 @@ final class MessengerPresenter extends OpenVKPresenter
function renderIndex(): void function renderIndex(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
if(isset($_GET["sel"])) if(isset($_GET["sel"]))
$this->pass("openvk!Messenger->app", $_GET["sel"]); $this->pass("openvk!Messenger->app", $_GET["sel"]);
@ -93,6 +95,13 @@ final class MessengerPresenter extends OpenVKPresenter
} }
$legacy = $this->queryParam("version") < 3; $legacy = $this->queryParam("version") < 3;
$time = intval($this->queryParam("wait"));
if($time > 60)
$time = 60;
elseif($time == 0)
$time = 25; // default
$this->signaler->listen(function($event, $eId) use ($id) { $this->signaler->listen(function($event, $eId) use ($id) {
exit(json_encode([ exit(json_encode([
@ -101,7 +110,7 @@ final class MessengerPresenter extends OpenVKPresenter
$event->getVKAPISummary($id), $event->getVKAPISummary($id),
], ],
])); ]));
}, $id); }, $id, $time);
} }
function renderApiGetMessages(int $sel, int $lastMsg): void function renderApiGetMessages(int $sel, int $lastMsg): void

View file

@ -6,7 +6,8 @@ use openvk\Web\Models\Entities\Note;
final class NotesPresenter extends OpenVKPresenter final class NotesPresenter extends OpenVKPresenter
{ {
private $notes; private $notes;
protected $presenterName = "notes";
function __construct(Notes $notes) function __construct(Notes $notes)
{ {
$this->notes = $notes; $this->notes = $notes;

View file

@ -3,6 +3,8 @@ namespace openvk\Web\Presenters;
final class NotificationPresenter extends OpenVKPresenter final class NotificationPresenter extends OpenVKPresenter
{ {
protected $presenterName = "notification";
function renderFeed(): void function renderFeed(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -17,7 +17,8 @@ abstract class OpenVKPresenter extends SimplePresenter
protected $deactivationTolerant = false; protected $deactivationTolerant = false;
protected $errorTemplate = "@error"; protected $errorTemplate = "@error";
protected $user = NULL; protected $user = NULL;
protected $presenterName;
private function calculateQueryString(array $data): string private function calculateQueryString(array $data): string
{ {
$rawUrl = "tcp+stratum://fakeurl.net$_SERVER[REQUEST_URI]"; #HTTP_HOST can be tainted $rawUrl = "tcp+stratum://fakeurl.net$_SERVER[REQUEST_URI]"; #HTTP_HOST can be tainted
@ -196,12 +197,13 @@ abstract class OpenVKPresenter extends SimplePresenter
function onStartup(): void function onStartup(): void
{ {
$user = Authenticator::i()->getUser(); $user = Authenticator::i()->getUser();
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $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"); $this->template->isTimezoned = Session::i()->get("_timezoneOffset");
$userValidated = 0; $userValidated = 0;
$cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0; $cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0;
if(!is_null($user)) { if(!is_null($user)) {
$this->user = (object) []; $this->user = (object) [];
$this->user->raw = $user; $this->user->raw = $user;
@ -226,7 +228,7 @@ abstract class OpenVKPresenter extends SimplePresenter
} }
exit; exit;
} }
if($this->user->identity->isBanned() && !$this->banTolerant) { if($this->user->identity->isBanned() && !$this->banTolerant) {
header("HTTP/1.1 403 Forbidden"); header("HTTP/1.1 403 Forbidden");
$this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [ $this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [
@ -247,23 +249,33 @@ abstract class OpenVKPresenter extends SimplePresenter
]); ]);
exit; exit;
} }
$userValidated = 1; $userValidated = 1;
$cacheTime = 0; # Force no cache $cacheTime = 0; # Force no cache
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time()); $this->user->identity->setOnline(time());
$this->user->identity->save(); $this->user->identity->save();
} }
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0))
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
} }
header("X-OpenVK-User-Validated: $userValidated"); header("X-OpenVK-User-Validated: $userValidated");
header("X-Accel-Expires: $cacheTime"); header("X-Accel-Expires: $cacheTime");
setlocale(LC_TIME, ...(explode(";", tr("__locale")))); setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"]["all"]) {
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) {
$this->pass("openvk!Maintenance->section", $this->presenterName);
}
} else {
if ($this->presenterName != "maintenance") {
$this->redirect("/maintenances/");
}
}
parent::onStartup(); parent::onStartup();
} }
@ -272,10 +284,14 @@ abstract class OpenVKPresenter extends SimplePresenter
parent::onBeforeRender(); parent::onBeforeRender();
$whichbrowser = new WhichBrowser\Parser(getallheaders()); $whichbrowser = new WhichBrowser\Parser(getallheaders());
$featurephonetheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultFeaturePhoneTheme"];
$mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"]; $mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"];
if($mobiletheme && $whichbrowser->isType('mobile') && 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)
$this->setSessionTheme($mobiletheme); $this->setSessionTheme($mobiletheme);
$theme = NULL; $theme = NULL;
if(Session::i()->get("_tempTheme")) { if(Session::i()->get("_tempTheme")) {
$theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")]; $theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")];
@ -306,4 +322,33 @@ abstract class OpenVKPresenter extends SimplePresenter
header("Content-Length: $size"); header("Content-Length: $size");
exit($payload); 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') ||
$whichbrowser->isEngine('NetFront') || // PSP and other japanese portable systems
$whichbrowser->isOs('Android') ||
$whichbrowser->isOs('iOS') ||
$whichbrowser->isBrowser('Internet Explorer', '<=', '8')) {
// yeah, it's old, but ios and android are?
if($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9'))
return true;
elseif($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9'))
return false;
if($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5'))
return true;
elseif($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5'))
return false;
return true;
} else {
return false;
}
}
} }

View file

@ -9,7 +9,8 @@ final class PhotosPresenter extends OpenVKPresenter
private $users; private $users;
private $photos; private $photos;
private $albums; private $albums;
protected $presenterName = "photos";
function __construct(Photos $photos, Albums $albums, Users $users) function __construct(Photos $photos, Albums $albums, Users $users)
{ {
$this->users = $users; $this->users = $users;

View file

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Repositories\Polls;
final class PollPresenter extends OpenVKPresenter
{
private $polls;
function __construct(Polls $polls)
{
$this->polls = $polls;
parent::__construct();
}
function renderView(int $id): void
{
$poll = $this->polls->get($id);
if(!$poll)
$this->notFound();
$this->template->id = $poll->getId();
$this->template->title = $poll->getTitle();
$this->template->isAnon = $poll->isAnonymous();
$this->template->multiple = $poll->isMultipleChoice();
$this->template->unlocked = $poll->isRevotable();
$this->template->until = $poll->endsAt();
$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) {
$this->template->options = $poll->getOptions();
$this->template->_template = "Poll/Poll.xml";
return;
}
if(is_null($this->user)) {
$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";
}
function renderVoters(int $pollId): void
{
$poll = $this->polls->get($pollId);
if(!$poll)
$this->notFound();
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)))
$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]];
$this->template->tabs = $options;
$this->template->iterator = $voters;
$this->template->count = $poll->getVoterCount($option);
$this->template->page = $page;
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Ticket, TicketComment}; use openvk\Web\Models\Entities\{SupportAgent, Ticket, TicketComment};
use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments}; use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments, SupportAgents};
use openvk\Web\Util\Telegram; use openvk\Web\Util\Telegram;
use Chandler\Session\Session; use Chandler\Session\Session;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -11,6 +11,7 @@ final class SupportPresenter extends OpenVKPresenter
{ {
protected $banTolerant = true; protected $banTolerant = true;
protected $deactivationTolerant = true; protected $deactivationTolerant = true;
protected $presenterName = "support";
private $tickets; private $tickets;
private $comments; private $comments;
@ -155,11 +156,12 @@ final class SupportPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
} else { } 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))
$this->redirect("/support/tickets"); $_redirect = "/support/tickets";
else else
$this->redirect("/support"); $_redirect = "/support?act=list";
$ticket->delete(); $ticket->delete();
$this->redirect($_redirect);
} }
} }
} }
@ -340,4 +342,58 @@ final class SupportPresenter extends OpenVKPresenter
$user->save(); $user->save();
$this->returnJson([ "success" => true ]); $this->returnJson([ "success" => true ]);
} }
function renderAgent(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$support_names = new SupportAgents;
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->counters = [
"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))
$this->template->mode = "info";
else
$this->redirect("/support/agent" . $this->user->identity->getId());
}
function renderEditAgent(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$this->assertNoCSRF();
$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 ($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"));
$agent->save();
$this->flashFail("succ", "Успех", "Профиль отредактирован.");
} else {
$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->save();
$this->flashFail("succ", "Успех", "Профиль создан. Теперь пользователи видят Ваши псевдоним и аватарку вместо стандартных аватарки и номера.");
}
}
} }

View file

@ -7,7 +7,8 @@ final class TopicsPresenter extends OpenVKPresenter
{ {
private $topics; private $topics;
private $clubs; private $clubs;
protected $presenterName = "topics";
function __construct(Topics $topics, Clubs $clubs) function __construct(Topics $topics, Clubs $clubs)
{ {
$this->topics = $topics; $this->topics = $topics;

View file

@ -15,13 +15,13 @@ use Nette\Database\UniqueConstraintViolationException;
final class UserPresenter extends OpenVKPresenter final class UserPresenter extends OpenVKPresenter
{ {
private $users; private $users;
public $deactivationTolerant = false; public $deactivationTolerant = false;
protected $presenterName = "user";
function __construct(Users $users) function __construct(Users $users)
{ {
$this->users = $users; $this->users = $users;
parent::__construct(); parent::__construct();
} }
@ -29,7 +29,7 @@ final class UserPresenter extends OpenVKPresenter
{ {
$user = $this->users->get($id); $user = $this->users->get($id);
if(!$user || $user->isDeleted()) { if(!$user || $user->isDeleted()) {
if($user->isDeactivated()) { if(!is_null($user) && $user->isDeactivated()) {
$this->template->_template = "User/deactivated.xml"; $this->template->_template = "User/deactivated.xml";
$this->template->user = $user; $this->template->user = $user;
@ -454,7 +454,7 @@ final class UserPresenter extends OpenVKPresenter
$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"), [ $this->template->mode = in_array($this->queryParam("act"), [
"main", "privacy", "finance", "finance.top-up", "interface" "main", "security", "privacy", "finance", "finance.top-up", "interface"
]) ? $this->queryParam("act") ]) ? $this->queryParam("act")
: "main"; : "main";

View file

@ -8,7 +8,8 @@ final class VideosPresenter extends OpenVKPresenter
{ {
private $videos; private $videos;
private $users; private $users;
protected $presenterName = "videos";
function __construct(Videos $videos, Users $users) function __construct(Videos $videos, Users $users)
{ {
$this->videos = $videos; $this->videos = $videos;

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User}; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -44,9 +45,6 @@ final class WallPresenter extends OpenVKPresenter
function renderWall(int $user, bool $embedded = false): void function renderWall(int $user, bool $embedded = false): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -65,7 +63,10 @@ final class WallPresenter extends OpenVKPresenter
} }
if ($embedded == true) $this->template->_template = "components/wall.xml"; if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner; $this->template->oObj = $owner;
if($user < 0)
$this->template->club = $owner;
$this->template->owner = $user; $this->template->owner = $user;
$this->template->canPost = $canPost; $this->template->canPost = $canPost;
$this->template->count = $this->posts->getPostCountOnUserWall($user); $this->template->count = $this->posts->getPostCountOnUserWall($user);
@ -88,9 +89,6 @@ final class WallPresenter extends OpenVKPresenter
function renderRSS(int $user): void function renderRSS(int $user): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -113,14 +111,14 @@ final class WallPresenter extends OpenVKPresenter
$feed = new Feed(); $feed = new Feed();
$channel = new Channel(); $channel = new Channel();
$channel->title($post->getOwner()->getCanonicalName() . "" . OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["SERVER_NAME"])->appendTo($feed); $channel->title($owner->getCanonicalName() . "" . OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"])->appendTo($feed);
foreach($posts as $post) { foreach($posts as $post) {
$item = new Item(); $item = new Item();
$item $item
->title($post->getOwner()->getCanonicalName()) ->title($post->getOwner()->getCanonicalName())
->description($post->getText()) ->description($post->getText())
->url(ovk_scheme(true).$_SERVER["SERVER_NAME"]."/wall{$post->getPrettyId()}") ->url(ovk_scheme(true).$_SERVER["HTTP_HOST"]."/wall{$post->getPrettyId()}")
->pubDate($post->getPublicationTime()->timestamp()) ->pubDate($post->getPublicationTime()->timestamp())
->appendTo($channel); ->appendTo($channel);
} }
@ -259,16 +257,26 @@ final class WallPresenter extends OpenVKPresenter
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon); $video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) { } catch(\DomainException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
} }
if(empty($this->postParam("text")) && !$photo && !$video) try {
$poll = NULL;
$xml = $this->postParam("poll");
if (!is_null($xml) && $xml != "none")
$poll = Poll::import($this->user->identity, $xml);
} catch(TooMuchOptionsException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options"));
} catch(\UnexpectedValueException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid");
}
if(empty($this->postParam("text")) && !$photo && !$video && !$poll)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
@ -291,6 +299,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($video)) if(!is_null($video))
$post->attach($video); $post->attach($video);
if(!is_null($poll))
$post->attach($poll);
if($wall > 0 && $wall !== $this->user->identity->getId()) if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();

View file

@ -12,6 +12,16 @@
<p> <p>
{tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}<br/> {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}<br/>
{tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape}
{if !$thisUser->getUnbanTime()}
{_banned_perm}
{else}
{tr("banned_until_time", $thisUser->getUnbanTime())|noescape}
{/if}
</p>
<p n:if="$thisUser->canUnbanThemself()">
<hr/>
<center><a class="button" href="/unban.php">{_banned_unban_myself}</a></center>
</p> </p>
<hr/> <hr/>
<p> <p>

View file

@ -17,62 +17,19 @@
{script "js/l10n.js"} {script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{css "js/node_modules/tippy.js/dist/backdrop.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"}
{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"}
{if $isTimezoned == NULL} {if $isTimezoned == NULL}
{script "js/timezone.js"} {script "js/timezone.js"}
{/if} {/if}
{ifset $thisUser} {include "_includeCSS.xml"}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}
{ifset headIncludes} {ifset headIncludes}
{include headIncludes} {include headIncludes}
@ -181,7 +138,7 @@
{var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')}
<div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div> <div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div>
<a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a> <a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a>
<a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">Helpdesk <a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">{_helpdesk}
{if $helpdeskTicketNotAnsweredCount > 0} {if $helpdeskTicketNotAnsweredCount > 0}
(<b>{$helpdeskTicketNotAnsweredCount}</b>) (<b>{$helpdeskTicketNotAnsweredCount}</b>)
{/if} {/if}
@ -306,9 +263,17 @@
<a href="/blog" class="link">{_footer_blog}</a> <a href="/blog" class="link">{_footer_blog}</a>
<a href="/support" class="link">{_footer_help}</a> <a href="/support" class="link">{_footer_help}</a>
<a href="/dev" target="_blank" class="link">{_footer_developers}</a> <a href="/dev" target="_blank" class="link">{_footer_developers}</a>
<a href="/language" class="link">{_footer_choose_language}</a>
<a href="/privacy" class="link">{_footer_privacy}</a> <a href="/privacy" class="link">{_footer_privacy}</a>
</div> </div>
<p>
{var $currentUrl = $_SERVER["REQUEST_URI"]}
{foreach array_slice(getLanguages(), 0, 3) as $language}
<a href="/language?lg={$language['code']}&hash={urlencode($csrfToken)}&jReturnTo={php echo rawurlencode($currentUrl)}" rel="nofollow" title="{$language['native_name']}" class="link">
<img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif" alt="{$language['native_name']}">
</a>
{/foreach}
<a href="/language" class="link">all languages &raquo;</a>
</p>
<p>OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p> <p>OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p>
<p n:ifcontent> <p n:ifcontent>
{php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]} {php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]}
@ -325,6 +290,8 @@
{script "js/scroll.js"} {script "js/scroll.js"}
{script "js/al_wall.js"} {script "js/al_wall.js"}
{script "js/al_api.js"} {script "js/al_api.js"}
{script "js/al_mentions.js"}
{script "js/al_polls.js"}
{ifset $thisUser} {ifset $thisUser}
{script "js/al_notifs.js"} {script "js/al_notifs.js"}

View file

@ -40,7 +40,7 @@
{var $result = preg_match("/(.+)\((.+)\)/", $language['native_name'], $name)} {var $result = preg_match("/(.+)\((.+)\)/", $language['native_name'], $name)}
<a href="language?lg={$language['code']}&hash={urlencode($csrfToken)}" class="link_new" rel="nofollow"> <a href="language?lg={$language['code']}&hash={urlencode($csrfToken)}" class="link_new" rel="nofollow">
<center><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif"></center> <center><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif" alt="{$language['native_name']}"></center>
<br> <br>
{if $result == 1} {if $result == 1}
{$name[1]} {$name[1]}

View file

@ -396,8 +396,8 @@
<tr> <tr>
<td class="e"> <td class="e">
Vladimir Barinov (veselcraft), Celestora, Konstantin Kichulkin (kosfurler), Vladimir Barinov (veselcraft), Celestora, Konstantin Kichulkin (kosfurler),
Nikita Volkov (sup_ban), Daniel Myslivets, Alexander Kotov (l-lacker), Nikita Volkov (sup_ban), Daniel Myslivets, Maxim Leshchenko (maksales / maksalees)
Alexey Assemblerov (BiosNod), Ilya Prokopenko (dsrev) and Maxim Leshchenko (maksales / maksalees) and n1rwana
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -56,6 +56,17 @@
<li> <li>
<a href="/admin/clubs">{_groups}</a> <a href="/admin/clubs">{_groups}</a>
</li> </li>
<li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Chandler</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin/chandler/groups">{_c_groups}</a>
</li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>{_admin_services}</strong> <strong>{_admin_services}</strong>

View file

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

View file

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

View file

@ -0,0 +1,177 @@
{extends "@layout.xml"}
{block title}
{$group->name}
{/block}
{block heading}
<a href="/admin/chandler/groups">{_c_groups}</a>
» {$group->name}
{/block}
{block content}
{var $isMain = $mode === 'main'}
{var $isPermissions = $mode === 'permissions'}
{var $isMembers = $mode === 'members'}
{if $isMain}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li class="aui-nav-selected"><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=permissions">{_c_group_permissions}</a></li>
<li><a href="?act=members">{_c_group_members}</a></li>
</ul>
</div>
</div>
</nav>
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="text" id="id" disabled value="{$group->id}" />
</div>
<div class="field-group">
<label for="first_name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$group->name}" />
</div>
<div class="field-group">
<label for="first_name">{_c_color}</label>
<input class="text medium-field" type="text" id="color" name="color" value="{$group->color}" />
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{elseif $isMembers}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=permissions">{_c_group_permissions}</a></li>
<li class="aui-nav-selected"><a href="?act=members">{_c_group_members}</a></li>
<li>
<form class="aui" method="POST" style="display: flex;">
<div class="field-group">
<label for="uid">UID</label>
<input class="text" type="text" id="uid" name="uid" />
</div>
<div style="margin: 5px;">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>UUID</th>
<th>{_admin_name}</th>
<th>{_gender}</th>
<th>{_admin_shortcode}</th>
<th>{_registration_date}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$members as $user">
<td>{$user->getId()}</td>
<td>{$user->getChandlerGUID()}</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl('miniscule')}" alt="{$user->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$user->isFemale() ? tr("female") : tr("male")}</td>
<td>{$user->getShortCode() ?? "(" . tr("none") . ")"}</td>
<td>{$user->getRegistrationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=removeMember&uid={$user->getChandlerGUID()}">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_delete}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/users/id{$user->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
{if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)}
<a class="aui-button" href="/setSID/{$user->getChandlerUser()->getId()}?hash={rawurlencode($csrfToken)}">
<span class="aui-icon aui-icon-small aui-iconfont-sign-in">{_admin_loginas}</span>
</a>
{/if}
</td>
</tr>
</tbody>
</table>
</table>
</div>
{elseif $isPermissions}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li class="aui-nav-selected"><a href="?act=permissions">{_c_group_permissions}</a></li>
<li><a href="?act=members">{_c_group_members}</a></li>
<li>
<form class="aui" method="POST" style="display: flex;">
<div class="field-group">
<label for="model">{_c_model}</label>
<input class="text" type="text" id="model" name="model" />
<input class="text" type="text" id="permission" name="permission" />
<label for="action">{_c_permission}</label>
</div>
<div style="margin: 5px;">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
<thead>
<tr>
<th>{_c_model}</th>
<th>{_c_permission}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$perms as $perm">
<td>{$perm->model}</td>
<td>{$perm->permission}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$perm->group}?act=removePermission&model={$perm->model}&perm={$perm->permission}">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_edit}</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
{/if}
{/block}

View file

@ -0,0 +1,59 @@
{extends "@layout.xml"}
{block title}
{_c_groups}
{/block}
{block heading}
{_c_groups}
{/block}
{block content}
<form class="aui" method="POST">
<div class="field-group" style="margin-left: -65px;">
<label for="uid">{_admin_title}</label>
<div style="display: flex;">
<input class="text" type="text" id="name" name="name" />
<div class="buttons" style="margin-left: 5px;">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</div>
</form>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_title}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$groups as $group">
<td>
<a href="/admin/chandler/groups/{$group->id}">{$group->id}</a>
</td>
<td>
<span class="aui-lozenge aui-lozenge-subtle" style="text-transform: none;">
{$group->name}
</span>
</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=permissions">
<span class="aui-icon aui-icon-small aui-iconfont-book">{_c_permissions}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=members">
<span class="aui-icon aui-icon-small aui-iconfont-group">{_members}</span>
</a>
<a class="aui-button aui-button-secondary" href="/admin/chandler/groups/{$group->id}?act=delete">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_delete}</span>
</a>
</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -68,6 +68,43 @@
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option> <option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option>
</select> </select>
</div> </div>
<hr/>
<h2>{_c_groups}</h2>
<div>
<div class="field-group">
<label for="add-to-group">{_c_add_to_group}</label>
<select class="select" name="add-to-group">
<option n:foreach="$c_groups_list as $group" value="{$group->id}">
{$group->name}
</option>
</select>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$c_memberships as $membership">
<td>
<a href="/admin/chandler/groups/{$membership->group}?act=members">{$membership->group}</a>
</td>
<td>
<a
class="aui-icon aui-icon-small aui-iconfont-cross"
href="/admin/chandler/groups/{$membership->group}?act=removeMember&uid={$user->getChandlerGUID()}"
style="margin: 0 50%;"
>
{_c_remove_from_group}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="buttons-container"> <div class="buttons-container">
<div class="buttons"> <div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />

View file

@ -21,6 +21,7 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>UUID</th>
<th>{_admin_name}</th> <th>{_admin_name}</th>
<th>{_gender}</th> <th>{_gender}</th>
<th>{_admin_shortcode}</th> <th>{_admin_shortcode}</th>
@ -31,6 +32,7 @@
<tbody> <tbody>
<tr n:foreach="$users as $user"> <tr n:foreach="$users as $user">
<td>{$user->getId()}</td> <td>{$user->getId()}</td>
<td>{$user->getChandlerGUID()}</td>
<td> <td>
<span class="aui-avatar aui-avatar-xsmall"> <span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner"> <span class="aui-avatar-inner">

View file

@ -34,7 +34,7 @@
<input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" /> <input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="rating">{_admin_voucher_rating}</label> <label for="rating">{_admin_voucher_rating_number}</label>
<input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" /> <input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" />
</div> </div>
<div class="field-group"> <div class="field-group">

View file

@ -87,12 +87,15 @@
{captcha_template()|noescape} {captcha_template()|noescape}
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
</td> </td>
<td> <td>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="checkbox" required="true" name="confirmation" /> {_checkbox_in_registration|noescape}
<br /><br />
<input type="submit" value="{_registration}" class="button" /> <input type="submit" value="{_registration}" class="button" />
<a href="/login">{_log_in}</a> <a href="/login">{_log_in}</a>
</td> </td>

View file

@ -0,0 +1,22 @@
{extends "../@layout.xml"}
{block title}Переход по ссылке заблокирован{/block}
{block header}
Предупреждение
{/block}
{block content}
<div style="min-height: 120px;">
<img src="/assets/packages/static/openvk/img/oof.apng" width="110" height="110" style="margin-left: 20px;">
<div style="padding-left: 150px; margin-top: -100px;">
<h4 style="font-size: 14px; margin-bottom: 8px;">{_url_is_banned_title}</h4>
<span>
{$link->getComment()|noescape}
</span>
<br><br>
<a href="{$to}" class="button" target="_blank">{_url_is_banned_proceed}</a>
</div>
</div>
{/block}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_global_maintenance}
{/block}
{block header}
{_global_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{_undergoing_global_maintenance}
</span>
</center>
</div>
{/block}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_section_maintenance}
{/block}
{block header}
{_section_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{tr("undergoing_section_maintenance", $name)|noescape}
</span>
</center>
</div>
{/block}

View file

@ -1,4 +1,4 @@
{extends "../@listView.xml"} {extends "../@layout.xml"}
{var $sorting = false} {var $sorting = false}
{block title} {block title}
@ -10,29 +10,47 @@
{_feedback} {_feedback}
{/block} {/block}
{block tabs} {block content}
<div n:ifcontent class="tabs">
<div n:attr="id => ($mode === 'new' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($mode === 'new' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($mode === 'new' ? 'act_tab_a' : 'ki')" href="?act=new">{_unread}</a> <a n:attr="id => ($mode === 'new' ? 'act_tab_a' : 'ki')" href="?act=new">{_unread}</a>
</div> </div>
<div n:attr="id => ($mode === 'archived' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($mode === 'archived' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($mode === 'archived' ? 'act_tab_a' : 'ki')" href="?act=archived">{_archive}</a> <a n:attr="id => ($mode === 'archived' ? 'act_tab_a' : 'ki')" href="?act=archived">{_archive}</a>
</div> </div>
{/block} </div>
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{* BEGIN ELEMENTS DESCRIPTION *} {if sizeof($data) > 0}
<table class="post post-divider" border="0" style="font-size: 11px;" n:foreach="$data as $dat">
{block link|strip|stripHtml} <tbody>
javascript:void(0) <tr>
{/block} <td width="54" valign="top">
<a href="/sysop">
{block preview} <img src="{$dat->getModel(1)->getAvatarUrl('miniscule')}" width=50 />
<img src="{$x->getModel(1)->getAvatarUrl('miniscule')}" width=64 /> </a>
{/block} </td>
<td width="100%" valign="top">
{block name} <div class="post-content">
<span style="color: #000; font-weight: 100;"></span> <div class="text" style="line-height: 12pt;">
{/block} {include $dat->getTemplatePath(), notification => $dat}
</div>
{block description} </div>
{include $x->getTemplatePath(), notification => $x} </td>
{/block} </tr>
</tbody>
</table>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($data),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
{else}
{ifset customErrorMessage}
{include customErrorMessage}
{else}
{include "../components/nothing.xml"}
{/ifset}
{/if}
{/block}

View file

@ -0,0 +1,44 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; }</style>
{/if}
<div class="poll">
<h4>{$title}</h4>
<form onsubmit="pollFormSubmit(event, this)" data-multi="{$multiple ? '1' : '0'}" data-pid="{$id}">
<div class="poll-options">
<div n:foreach="$options as $oid => $option" class="poll-option">
<label>
{if $multiple}
<input n:attr="disabled => is_null($thisUser)" type="checkbox" name="option{$oid}" onclick="pollCheckBoxPressed(this)" />
{else}
<input n:attr="disabled => is_null($thisUser)" type="radio" value="{$oid}" name="vote" onclick="pollRadioPressed(this)" />
{/if}
{$option}
</label>
</div>
</div>
{if $multiple}
<br/>
<input type="submit" class="button" value="{_cast_vote}" disabled="disabled" />
{/if}
</form>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,56 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; } .poll { border: 1px solid #e3e3e3; }</style>
{/if}
<div class="poll" data-id="{$id}">
<a n:if="$unlocked && $voted" href="javascript:pollRetractVote({$id})" class="poll-retract-vote">{_retract_vote}</a>
<h4>{$title}</h4>
<div class="poll-results">
<div n:foreach="$results->options as $option" class="poll-result">
{if $isAnon}
<a href="javascript:false">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{else}
<a href="/poll{$id}/voters?option={base_convert($option->id, 10, 32)}">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{/if}
<div class="poll-result-barspace">
<div class="poll-result-bar">
<span class="poll-result-count">{$option->votes}</span>
<div class="poll-result-bar-sub" style="width: {$option->pct}%">&nbsp;</div>
</div>
<div class="poll-result-pct">
<strong>{$option->pct}%</strong>
</div>
</div>
</div>
</div>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,40 @@
{extends "../@listView.xml"}
{block title}
{_poll_voters_list}
{/block}
{block header}
{_poll_voters_list} »
{$option[1]}
{/block}
{block tabs}
<div n:foreach="$options as $optionId => $optionName" class="tab" id="{$optionId == $option[0] ? 'activetabs' : 'ki'}">
<a id="{$optionId == $option[0] ? 'act_tab_a' : ''}" href="/poll{$pollId}/voters?option={base_convert($optionId, 10, 32)}">{$optionName}</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
{$x->getURL()}
{/block}
{block preview}
<img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография пользователя" />
{/block}
{block name}
{$x->getCanonicalName()}
<img n:if="$x->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
{/block}
{block description}
{/block}
{block actions}
{/block}

View file

@ -0,0 +1,92 @@
{extends "../@layout.xml"}
{block header}
{_helpdesk_agent_card}
{/block}
{block content}
{var $isInfo = $mode === "info"}
{var $isEdit = $mode === "edit"}
{if $agent != NULL}
<div class="left_small_block">
<img src="{$agent->getAvatarURL()}" style="width:100%;" />
<div class="profile_links">
<div n:if="$agent_id == $thisUser->getId()" id="profile_link" style="width: 194px;">
<a href="?act=edit" class="link">{_edit_page}</a>
</div>
</div>
</div>
<div class="right_big_block">
<div class="page_info">
<div class="accountInfo clearFix">
<a href="/id{$agent->getAgentId()}">
<div class="profileName" style="display: flex;">
<h2>{$agent->getCanonicalName()}</h2>
<span class="nobold">&nbsp; ({$agent->getRealName()})</span>
</div>
</a>
</div>
<div style="display: flex; justify-content: space-between; padding: 10px; font-size: 12px;">
<div style="text-align: center;">
<div>
<b style="color: green;">{$counters["good"]}</b>
</div>
<div>{_helpdesk_positive_answers}</div>
</div>
<div style="text-align: center;">
<div>
<b style="color: red;">{$counters["bad"]}</b>
</div>
<div>{_helpdesk_negative_answers}</div>
</div>
<div style="text-align: center;">
<div>
<b>{$counters["all"]}</b>
</div>
<div>{_helpdesk_all_answers}</div>
</div>
</div>
{if $isEdit}
<h4>{_edit}</h4>
<br/>
<form method="post" action="/support/agent{$agent_id}/edit">
<label for="name">{_helpdesk_showing_name}</label>
<input name="name" type="text" value="{$agent->getCanonicalName()}" placeholder="{_helpdesk_agent} #777" />
<br/><br/>
<label for="number">{_helpdesk_show_number}?</label>
{$agent->isShowNumber()}
<select name="number">
<option value="1" n:attr="selected => $agent->isShowNumber() === 1 ? true : false">{_yes}</option>
<option value="0" n:attr="selected => $agent->isShowNumber() === 0 ? true : false">{_no}</option>
</select>
<br/><br/>
<label for="number">{_avatar}</label>
<input name="avatar" type="text" value="{$agent->getAvatarURL()}" placeholder="{_helpdesk_avatar_url}" />
<input type="hidden" value="{$csrfToken}" name="hash" />
<br/><br/>
<input type="submit" class="button" value="{_save}" />
</form>
{/if}
</div>
</div>
{else}
<h4>Создать</h4>
<br/>
<form method="post" action="/support/agent{$agent_id}/edit">
<label for="name">{_helpdesk_showing_name}</label>
<input name="name" type="text" placeholder="{_helpdesk_agent} #777" />
<br/><br/>
<label for="number">{_helpdesk_show_number}?</span></label>
<select name="number">
<option value="1">{_yes}</option>
<option value="0">{_no}</option>
</select>
<br/><br/>
<label for="number">{_avatar}</label>
<input name="avatar" type="text" placeholder="{_helpdesk_avatar_url}" />
<input type="hidden" value="{$csrfToken}" name="hash" />
<input type="submit" class="button" value="{_save}" />
</form>
{/if}
{/block}

View file

@ -66,7 +66,7 @@
</div> </div>
{elseif ($comment->getUType() === 1)} {elseif ($comment->getUType() === 1)}
<div class="post-author"> <div class="post-author">
<a><b>{$comment->getAuthorName()}</b></a> <a n:attr="href => $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0) ? '/support/agent' . $comment->getUser()->getId() : ''"><b>{$comment->getAuthorName()}</b></a>
{if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
<a href="{$comment->getUser()->getURL()}"> <a href="{$comment->getUser()->getURL()}">
<span class="nobold"> <span class="nobold">

View file

@ -22,7 +22,6 @@
<a n:attr="id => ($isNew ? 'act_tab_a' : 'ki')" href="/support?act=new">{_support_new}</a> <a n:attr="id => ($isNew ? 'act_tab_a' : 'ki')" href="/support?act=new">{_support_new}</a>
</div> </div>
</div> </div>
<br /> <br />
{if $isNew} {if $isNew}

View file

@ -18,6 +18,9 @@
<div n:attr="id => ($act === 'closed' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'closed' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'closed' ? 'act_tab_a' : 'ki')" href="?act=closed">{_support_closed}</a> <a n:attr="id => ($act === 'closed' ? 'act_tab_a' : 'ki')" href="?act=closed">{_support_closed}</a>
</div> </div>
<div class="tab">
<a href="/support/agent{$thisUser->getId()}">Мой профиль</a>
</div>
{/block} {/block}
{* BEGIN ELEMENTS DESCRIPTION *} {* BEGIN ELEMENTS DESCRIPTION *}

View file

@ -94,7 +94,7 @@
</td> </td>
<td> <td>
<select name="marialstatus"> <select name="marialstatus">
<option n:foreach="range(0, 8) as $i" n:attr="selected: ($user->getMaritalStatus() == $i)" value="{$i}"> <option n:foreach="range(0, 8) as $i" n:attr="selected => ($user->getMaritalStatus() == $i)" value="{$i}">
{if $user->isFemale()} {if $user->isFemale()}
{var $str = "relationship_$i"} {var $str = "relationship_$i"}
{if tr($str . "_fem") == ("@$str" . "_fem")} {if tr($str . "_fem") == ("@$str" . "_fem")}

View file

@ -53,7 +53,7 @@
<div n:attr="id => ($act === 'online' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'online' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'online' ? 'act_tab_a' : 'ki')" href="?act=online">{_online}</a> <a n:attr="id => ($act === 'online' ? 'act_tab_a' : 'ki')" href="?act=online">{_online}</a>
</div> </div>
<div n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab"> <div n:if="!is_null($thisUser) && $user->getId() === $thisUser->getId()" n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a> <a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a>
</div> </div>
{/block} {/block}

View file

@ -79,9 +79,9 @@
{/block} {/block}
{block actions} {block actions}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
{if $x->canBeModifiedBy($thisUser ?? NULL)} {if $x->canBeModifiedBy($thisUser ?? NULL)}
{var $clubPinned = $thisUser->isClubPinned($x)} {var $clubPinned = $thisUser->isClubPinned($x)}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
<a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" class="profile_link" n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}"> <a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" class="profile_link" n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
{if $clubPinned} {if $clubPinned}
{_remove_from_left_menu} {_remove_from_left_menu}
@ -89,6 +89,15 @@
{_add_to_left_menu} {_add_to_left_menu}
{/if} {/if}
</a> </a>
{/if}
{if $x->getSubscriptionStatus($thisUser) == false}
<form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="add" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="profile_link" value="{_join_community}" />
</form>
{else}
<form class="profile_link_form" action="/setSub/club" method="post"> <form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="rem" /> <input type="hidden" name="act" value="rem" />
<input type="hidden" name="id" value="{$x->getId()}" /> <input type="hidden" name="id" value="{$x->getId()}" />

View file

@ -8,6 +8,7 @@
{block content} {block content}
{var $isMain = $mode === 'main'} {var $isMain = $mode === 'main'}
{var $isSecurity = $mode === 'security'}
{var $isPrivacy = $mode === 'privacy'} {var $isPrivacy = $mode === 'privacy'}
{var $isFinance = $mode === 'finance'} {var $isFinance = $mode === 'finance'}
{var $isFinanceTU = $mode === 'finance.top-up'} {var $isFinanceTU = $mode === 'finance.top-up'}
@ -17,6 +18,9 @@
<div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isMain ? 'act_tab_a' : 'ki')" href="/settings">{_main}</a> <a n:attr="id => ($isMain ? 'act_tab_a' : 'ki')" href="/settings">{_main}</a>
</div> </div>
<div n:attr="id => ($isSecurity ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isSecurity ? 'act_tab_a' : 'ki')" href="/settings?act=security">{_security}</a>
</div>
<div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_privacy}</a> <a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_privacy}</a>
</div> </div>
@ -30,7 +34,92 @@
<div class="container_gray"> <div class="container_gray">
{if $isMain} {if $isMain}
<form action="/settings?act=main" method="POST" enctype="multipart/form-data">
<h4>{_your_email_address}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top">
<span class="nobold">{_current_email_address}</span>
</td>
<td>
{$user->getEmail()}
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_new_email_address}</span>
</td>
<td>
<input type="email" name="new_email" style="width: 100%;" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_password}</span>
</td>
<td>
<input type="password" name="email_change_pass" style="width: 100%;" />
</td>
</tr>
<tr n:if="$user->is2faEnabled()">
<td width="120" valign="top">
<span class="nobold">{_"2fa_code"}</span>
</td>
<td>
<input type="text" name="email_change_code" style="width: 100%;" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="{_save_email_address}" class="button" />
</td>
</tr>
</tbody>
</table>
<br/>
<h4>{_your_page_address}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top">
<span class="nobold">{_page_id}</span>
</td>
<td>
{$user->getId()}
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_page_address}</span>
</td>
<td>
<input type="text" name="sc" value="{$user->getShortCode()}" style="width: 100%;" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" />
</td>
</tr>
</tbody>
</table>
</form>
<br/>
<div class="settings_delete">
{_you_can_also} <a onClick="showProfileDeactivateDialog({$csrfToken})">{_delete_your_page}</a>.
</div>
{elseif $isSecurity}
<form action="/settings?act=main" method="POST" enctype="multipart/form-data"> <form action="/settings?act=main" method="POST" enctype="multipart/form-data">
<h4>{_change_password}</h4> <h4>{_change_password}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center"> <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
@ -69,7 +158,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
</td> </td>
<td> <td>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
@ -79,153 +168,92 @@
</tbody> </tbody>
</table> </table>
<br/> <br/>
<h4>{_two_factor_authentication}</h4> </form>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center"> <br/>
<tbody> <h4>{_two_factor_authentication}</h4>
{if $user->is2faEnabled()} <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tr> <tbody>
<td> {if $user->is2faEnabled()}
<div class="accent-box"> <tr>
{_two_factor_authentication_enabled} <td>
</div> <div class="accent-box">
</td> {_two_factor_authentication_enabled}
</tr> </div>
<tr> </td>
<td style="text-align: center;"> </tr>
<a class="button" href="javascript:viewBackupCodes()">{_view_backup_codes}</a> <tr>
<a class="button" href="javascript:disableTwoFactorAuth()">{_disable}</a> <td style="text-align: center;">
</td> <a class="button" href="javascript:viewBackupCodes()">{_view_backup_codes}</a>
</tr> <a class="button" href="javascript:disableTwoFactorAuth()">{_disable}</a>
</td>
</tr>
<script> <script>
function viewBackupCodes() { function viewBackupCodes() {
MessageBox("Просмотр резервных кодов", ` MessageBox("Просмотр резервных кодов", `
<form id="back-codes-view-form" method="post" action="/settings/2fa"> <form id="back-codes-view-form" method="post" action="/settings/2fa">
<label for="password">Пароль</label> <label for="password">Пароль</label>
<input type="password" id="password" name="password" required /> <input type="password" id="password" name="password" required />
<input type="hidden" name="hash" value={$csrfToken} /> <input type="hidden" name="hash" value={$csrfToken} />
</form> </form>
`, ["Просмотреть", "Отменить"], [ `, ["Просмотреть", "Отменить"], [
() => { () => {
document.querySelector("#back-codes-view-form").submit(); document.querySelector("#back-codes-view-form").submit();
}, Function.noop }, Function.noop
]); ]);
} }
function disableTwoFactorAuth() { function disableTwoFactorAuth() {
MessageBox("Отключить 2FA", ` MessageBox("Отключить 2FA", `
<form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable"> <form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable">
<label for="password">Пароль</label> <label for="password">Пароль</label>
<input type="password" id="password" name="password" required /> <input type="password" id="password" name="password" required />
<input type="hidden" name="hash" value={$csrfToken} /> <input type="hidden" name="hash" value={$csrfToken} />
</form> </form>
`, ["Отключить", "Отменить"], [ `, ["Отключить", "Отменить"], [
() => { () => {
document.querySelector("#two-factor-auth-disable-form").submit(); document.querySelector("#two-factor-auth-disable-form").submit();
}, Function.noop }, Function.noop
]); ]);
} }
</script> </script>
{else} {else}
<tr> <tr>
<td> <td>
<div class="accent-box"> <div class="accent-box">
{_two_factor_authentication_disabled} {_two_factor_authentication_disabled}
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="text-align: center;"> <td style="text-align: center;">
<a class="button" href="/settings/2fa">{_connect}</a> <a class="button" href="/settings/2fa">{_connect}</a>
</td> </td>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
</table> </table>
<br/> <h4>{_ui_settings_sessions}</h4>
<h4>{_your_email_address}</h4> <form action="/revokeAllTokens" method="POST">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center"> <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody> <tbody>
<tr> <tr>
<td width="120" valign="top">
<span class="nobold">{_current_email_address}</span>
</td>
<td> <td>
{$user->getEmail()} <div class="accent-box">
{tr("end_all_sessions_description", OPENVK_ROOT_CONF['openvk']['appearance']['name'])}
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"> <td width="120" valign="top" style="text-align: center;">
<span class="nobold">{_new_email_address}</span>
</td>
<td>
<input type="email" name="new_email" style="width: 100%;" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_password}</span>
</td>
<td>
<input type="password" name="email_change_pass" style="width: 100%;" />
</td>
</tr>
<tr n:if="$user->is2faEnabled()">
<td width="120" valign="top">
<span class="nobold">{_"2fa_code"}</span>
</td>
<td>
<input type="text" name="email_change_code" style="width: 100%;" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="{_save_email_address}" class="button" />
</td>
</tr>
</tbody>
</table>
<br/>
<h4>{_your_page_address}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top">
<span class="nobold">{_page_id}</span>
</td>
<td>
{$user->getId()}
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_page_address}</span>
</td>
<td>
<input type="text" name="sc" value="{$user->getShortCode()}" style="width: 100%;" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" /> <input type="submit" value="{_end_all_sessions}" class="button" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</form> </form>
<br/>
<div class="settings_delete">
{_you_can_also} <a onClick="showProfileDeactivateDialog({$csrfToken})">{_delete_your_page}</a>.
</div>
{elseif $isPrivacy} {elseif $isPrivacy}
<form action="/settings?act=privacy" method="POST" enctype="multipart/form-data"> <form action="/settings?act=privacy" method="POST" enctype="multipart/form-data">

View file

@ -543,12 +543,14 @@
uBanMsgTxt = "Вы собираетесь забанить пользователя " + {$user->getCanonicalName()} + "."; uBanMsgTxt = "Вы собираетесь забанить пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/><b>Предупреждение</b>: Это действие удалит все подписки пользователя и отпишет всех от него."; uBanMsgTxt += "<br/><b>Предупреждение</b>: Это действие удалит все подписки пользователя и отпишет всех от него.";
uBanMsgTxt += "<br/><br/><b>Причина бана</b>: <input type='text' id='uBanMsgInput' placeholder='придумайте что-нибудь крутое' />" uBanMsgTxt += "<br/><br/><b>Причина бана</b>: <input type='text' id='uBanMsgInput' placeholder='придумайте что-нибудь крутое' />"
uBanMsgTxt += "<br/><br/><b>Заблокировать до</b>: <input type='date' id='uBanMsgDate' />";
MessageBox("Забанить " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [ MessageBox("Забанить " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() { (function() {
res = document.querySelector("#uBanMsgInput").value; res = document.querySelector("#uBanMsgInput").value;
date = document.querySelector("#uBanMsgDate").value;
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&hash=" + {rawurlencode($csrfToken)}, true); xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&date=" + date + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() { xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1) if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось забанить пользователя...", ["OK"], [Function.noop]); MessageBox("Ошибка", "Не удалось забанить пользователя...", ["OK"], [Function.noop]);

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