Merge branch 'master' into disco

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

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,45 +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" />
</content> <excludeFolder url="file://$MODULE_DIR$/vendor/wapmorgan/morphos" />
<orderEntry type="inheritedJdk" /> </content>
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="inheritedJdk" />
</component> <orderEntry type="sourceFolder" forTests="false" />
</component>
</module> </module>

View file

@ -1,47 +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" />
</include_path> <path value="$PROJECT_DIR$/vendor/wapmorgan/morphos" />
</component> </include_path>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4"> </component>
<option name="suggestChangeDefaultLanguageLevel" value="false" /> <component name="PhpProjectSharedConfiguration" php_language_level="7.4">
</component> <option name="suggestChangeDefaultLanguageLevel" value="false" />
</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>

103
CLI/FetchToncoinTransactions.php Executable file
View file

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

11
CODE_OF_CONFLICT.md Normal file
View file

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

277
CODE_STYLE.md Normal file
View file

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

View file

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

View file

@ -12,13 +12,15 @@ To be honest, we don't know whether it even works. However, this version is main
We will release OpenVK as soon as it's ready. As for now you can: We will release OpenVK as soon as it's ready. As for now you can:
* `git clone` this repo's master branch (use `git pull` to update) * `git clone` this repo's master branch (use `git pull` to update)
* Grab a prebuilt OpenVK distro from [GitHub artifacts](https://github.com/openvk/archive/actions/workflows/nightly.yml) * Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip)
## Instances ## Instances
* **[openvk.su](https://openvk.su/)** * **[openvk.su](https://openvk.su/)**
* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su (<https://t.me/openvkch/1609>) * **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/) * [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Can I create my own OpenVK instance? ## Can I create my own OpenVK instance?
@ -32,29 +34,35 @@ If you want, you can add your instance to the list above so that people can regi
1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler)
* PHP 8 has **not** yet been tested, so you should not expect it to work. (edit: it does not work). * PHP 8.1 is supported, but it was not tested carefully, be aware of that.
2. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this: 2. Install MySQL-compatible database.
* We recommend using Percona Server, but any MySQL-compatible server should work
* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ recommended.
* Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs.
3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this:
```bash ```bash
git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk
git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha
``` ```
3. And enable them: 4. And enable them:
```bash ```bash
ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/
ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/
``` ```
4. Import `install/init-static-db.sql` to the **same database** you installed Chandler to and import all sqls from `install/sqls` to the **same database** 5. Import `install/init-static-db.sql` to the **same database** you installed Chandler to and import all sqls from `install/sqls` to the **same database**
5. Import `install/init-event-db.sql` to a **separate database** (Yandex.Clickhouse can also be used, higly recommended) 6. Import `install/init-event-db.sql` to a **separate database** (Yandex.Clickhouse can also be used, highly recommended)
6. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking 7. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking
7. Run `composer install` in OpenVK directory 8. Run `composer install` in OpenVK directory
8. Run `composer install` in commitcaptcha directory 9. Run `composer install` in commitcaptcha directory
9. Move to `Web/static/js` and execute `yarn install` 10. Move to `Web/static/js` and execute `yarn install`
10. Set `openvk` as your root app in `chandler.yml` 11. Set `openvk` as your root app in `chandler.yml`
Once you are done, you can login as a system administrator on the network itself (no registration required): Once you are done, you can login as a system administrator on the network itself (no registration required):

View file

@ -2,23 +2,25 @@
_[English](README.md)_ _[English](README.md)_
**OpenVK** это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. Представленный здесь код пока не стабилен. **OpenVK** - это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент представленный здесь исходный код проекта пока не является стабильным.
ВКонтакте принадлежит Павлу Дурову и VK Group. ВКонтакте принадлежит Павлу Дурову и VK Group.
Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK). Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK).
## Когда релиз? ## Когда выйдет релизная версия?
Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете: Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете:
* Сделать `git clone` master ветки этой репозитории (используйте `git pull` для обновления) * Склонировать master ветку репозитория командой `git clone` (используйте `git pull` для обновления)
* Взять готовую сборку OpenVK из [GitHub Actions](https://github.com/openvk/archive/actions/workflows/nightly.yml) * Взять готовую сборку OpenVK из [GitHub Actions](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip)
## Инстанции ## Инстанции
* **[openvk.su](https://openvk.su/)** * **[openvk.su](https://openvk.su/)**
* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su (<https://t.me/openvkch/1609>) * **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/) * [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Могу ли я создать свою собственную инстанцию OpenVK? ## Могу ли я создать свою собственную инстанцию OpenVK?
@ -32,35 +34,41 @@ _[English](README.md)_
1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler)
* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (обновление: он не работает). * PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает).
2. Установите [commitcaptcha](https://github.com/openvk/commitcaptcha) и OpenVK в качестве расширений Chandler следующим образом: 2. Установите MySQL-совместимую базу данных.
* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать
* Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+.
* Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно.
3. Установите [commitcaptcha](https://github.com/openvk/commitcaptcha) и OpenVK в качестве расширений Chandler:
```bash ```bash
git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk git clone https://github.com/openvk/openvk /path/to/chandler/extensions/available/openvk
git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha git clone https://github.com/openvk/commitcaptcha /path/to/chandler/extensions/available/commitcaptcha
``` ```
3. И включите их: 4. И включите их:
```bash ```bash
ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/extensions/enabled/
ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/
``` ```
4. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler, и импортируйте все SQL файлы из папки `install/sqls` в **ту же базу данных** 5. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler, и импортируйте все SQL файлы из папки `install/sqls` в **ту же базу данных**
5. Импортируйте `install/init-event-db.sql` в **отдельную базу данных** (Яндекс.Clickhouse также может быть использован, настоятельно рекомендуется) 6. Импортируйте `install/init-event-db.sql` в **отдельную базу данных** (Яндекс.Clickhouse также может быть использован, настоятельно рекомендуется)
6. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры 7. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры под свои нужды
7. Запустите `composer install` в директории OpenVK 8. Запустите `composer install` в директории OpenVK
8. Запустите `composer install` в директории commitcaptcha 9. Запустите `composer install` в директории commitcaptcha
9. Перейдите в `Web/static/js` и выполните `yarn install` 10. Перейдите в `Web/static/js` и выполните `yarn install`
10. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` 11. Установите `openvk` в качестве корневого приложения в файле `chandler.yml`
После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется):
* **Логин**: `admin@localhost.localdomain6` * **Логин**: `admin@localhost.localdomain6`
* **Пароль**: `admin` * **Пароль**: `admin`
* Перед использованием встроенной учетной записи рекомендуется сменить пароль. * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её.
💡Запутались? Полное руководство по установке доступно [здесь](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)).
@ -74,7 +82,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Баг-трекер](https://github.com/openvk/openvk/projects/1) * [Баг-трекер](https://github.com/openvk/openvk/projects/1)
* [Помощь в OVK](https://openvk.su/support?act=new) * [Помощь в OVK](https://openvk.su/support?act=new)
* Telegram-чат: Перейдите на [наш канал](https://t.me/openvkch) и откройте обсуждение в меню нашего канала. * Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала.
* [Reddit](https://www.reddit.com/r/openvk/) * [Reddit](https://www.reddit.com/r/openvk/)
* [Обсуждения](https://github.com/openvk/openvk/discussions) * [Обсуждения](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org * Чат в Matrix: #ovk:matrix.org
@ -82,5 +90,5 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**. **Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
<a href="https://codeberg.org/OpenVK/openvk"> <a href="https://codeberg.org/OpenVK/openvk">
<img alt="Получить на Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60"> <img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">
</a> </a>

87
ServiceAPI/Apps.php Normal file
View file

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

View file

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

View file

@ -8,16 +8,16 @@ final class Account extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
return (object) [ return (object) [
"first_name" => $this->getUser()->getFirstName(), "first_name" => $this->getUser()->getFirstName(),
"id" => $this->getUser()->getId(), "id" => $this->getUser()->getId(),
"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" => "1.1.1970", // TODO "bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => 0, // TODO "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", // TODO "phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(), "relation" => $this->getUser()->getMaritalStatus(),
"sex" => $this->getUser()->isFemale() ? 1 : 2 "sex" => $this->getUser()->isFemale() ? 1 : 2
]; ];
} }
@ -25,20 +25,18 @@ final class Account extends VKAPIRequestHandler
{ {
$this->requireUser(); $this->requireUser();
// Цiй метод є заглушка
return (object) [ return (object) [
"2fa_required" => 0, "2fa_required" => $this->getUser()->is2faEnabled() ? 1 : 0,
"country" => "CZ", // TODO "country" => "CZ", # TODO
"eu_user" => false, // TODO "eu_user" => false, # TODO
"https_required" => 1, "https_required" => 1,
"intro" => 0, "intro" => 0,
"community_comments" => false, "community_comments" => false,
"is_live_streaming_enabled" => false, "is_live_streaming_enabled" => false,
"is_new_live_streaming_enabled" => false, "is_new_live_streaming_enabled" => false,
"lang" => 1, "lang" => 1,
"no_wall_replies" => 0, "no_wall_replies" => 0,
"own_posts_default" => 0 "own_posts_default" => 0
]; ];
} }
@ -47,7 +45,8 @@ final class Account extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$this->getUser()->setOnline(time()); $this->getUser()->setOnline(time());
$this->getUser()->save();
return 1; return 1;
} }
@ -55,7 +54,7 @@ final class Account extends VKAPIRequestHandler
{ {
$this->requireUser(); $this->requireUser();
// Цiй метод є заглушка # Цiй метод є заглушка
return 1; return 1;
} }
@ -68,11 +67,11 @@ final class Account extends VKAPIRequestHandler
function getCounters(string $filter = ""): object function getCounters(string $filter = ""): object
{ {
return (object) [ return (object) [
"friends" => $this->getUser()->getFollowersCount(), "friends" => $this->getUser()->getFollowersCount(),
"notifications" => $this->getUser()->getNotificationsCount(), "notifications" => $this->getUser()->getNotificationsCount(),
"messages" => $this->getUser()->getUnreadMessagesCount() "messages" => $this->getUser()->getUnreadMessagesCount()
]; ];
// TODO: Filter # TODO: Filter
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

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

View file

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

View file

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

16
Vagrantfile vendored
View file

@ -1,16 +1,22 @@
# -*- mode: ruby -*- # -*- mode: ruby -*-
# vi: set ft=ruby : # vi: set ft=ruby :
Vagrant.configure("2") do |config| Vagrant.configure("2") do |config|
config.vm.box = "freebsd/FreeBSD-12.1-STABLE" config.vm.box = "freebsd/FreeBSD-13.1-RC2"
config.vm.box_version = "2022.04.07"
config.vm.network "forwarded_port", guest: 80, host: 4000 config.vm.network "forwarded_port", guest: 80, host: 4000
config.vm.synced_folder ".", "/.ovk_release"
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
vb.gui = true vb.gui = true
vb.memory = "1024" vb.cpus = 4
vb.memory = "1568"
end
config.vm.provider "vmware_workstation" do |vwx|
vwx.gui = true
vwx.vmx["memsize"] = "1568"
vwx.vmx["numvcpus"] = "4"
end end
config.vm.provision "shell", inline: "/bin/tcsh /.ovk_release/install/automated/freebsd-12/install" config.vm.provision "shell", inline: "/bin/tcsh /.ovk_release/install/automated/freebsd-13/install"
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,9 @@ abstract class Media extends Postable
{ {
protected $fileExtension = "oct"; #octet stream xddd protected $fileExtension = "oct"; #octet stream xddd
protected $upperNodeReferenceColumnName = "owner"; protected $upperNodeReferenceColumnName = "owner";
protected $processingPlaceholder = NULL;
protected $processingTime = 30;
function __destruct() function __destruct()
{ {
#Remove data, if model wasn't presisted #Remove data, if model wasn't presisted
@ -22,6 +24,11 @@ abstract class Media extends Postable
else else
return OPENVK_ROOT . "/storage/"; return OPENVK_ROOT . "/storage/";
} }
protected function checkIfFileIsProcessed(): bool
{
throw new \LogicException("checkIfFileIsProcessed is not implemented");
}
abstract protected function saveFile(string $filename, string $hash): bool; abstract protected function saveFile(string $filename, string $hash): bool;
@ -41,6 +48,10 @@ abstract class Media extends Postable
function getURL(): string function getURL(): string
{ {
if(!is_null($this->processingPlaceholder))
if(!$this->isProcessed())
return "/assets/packages/static/openvk/$this->processingPlaceholder.$this->fileExtension";
$hash = $this->getRecord()->hash; $hash = $this->getRecord()->hash;
switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) {
@ -55,7 +66,7 @@ abstract class Media extends Postable
case "server": case "server":
$settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"];
return ( return (
$settings->protocol . $settings->protocol ?? ovk_scheme() .
"://" . $settings->host . "://" . $settings->host .
$settings->path . $settings->path .
substr($hash, 0, 2) . "/$hash.$this->fileExtension" substr($hash, 0, 2) . "/$hash.$this->fileExtension"
@ -68,6 +79,26 @@ abstract class Media extends Postable
{ {
return $this->getRecord()->description; return $this->getRecord()->description;
} }
protected function isProcessed(): bool
{
if(is_null($this->processingPlaceholder))
return true;
if($this->getRecord()->processed)
return true;
$timeDiff = time() - $this->getRecord()->last_checked;
if($timeDiff < $this->processingTime)
return false;
$res = $this->checkIfFileIsProcessed();
$this->stateChanges("last_checked", time());
$this->stateChanges("processed", $res);
$this->save();
return $res;
}
function isDeleted(): bool function isDeleted(): bool
{ {
@ -89,7 +120,17 @@ abstract class Media extends Postable
$this->stateChanges("hash", $hash); $this->stateChanges("hash", $hash);
} }
function save(): void
{
if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) {
$this->stateChanges("processed", 0);
$this->stateChanges("last_checked", time());
}
parent::save();
}
function delete(bool $softly = true): void function delete(bool $softly = true): void
{ {
$deleteQuirk = ovkGetQuirk("blobs.erase-upon-deletion"); $deleteQuirk = ovkGetQuirk("blobs.erase-upon-deletion");

View file

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

View file

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

View file

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

View file

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

View file

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

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
); );
@ -55,21 +55,28 @@ trait TRichText
{ {
$contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content";
$text = htmlentities($this->getRecord()->{$contentColumn}, ENT_DISALLOWED | ENT_XHTML); $text = htmlspecialchars($this->getRecord()->{$contentColumn}, ENT_DISALLOWED | ENT_XHTML);
$proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"]; $proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"];
if($html) { if($html) {
if($proc) { if($proc) {
$rel = $this->isAd() ? "sponsored" : "ugc"; $rel = $this->isAd() ? "sponsored" : "ugc";
$text = $this->formatLinks($text); $text = $this->formatLinks($text);
$text = preg_replace("%@([A-Za-z0-9]++) \(([\p{L} 0-9]+)\)%Xu", "[$1|$2]", $text); $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text);
$text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text); $text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text);
$text = preg_replace("%\[([A-Za-z0-9]++)\|([\p{L} 0-9@]+)\]%Xu", "<a href='/$1'>$2</a>", $text); $text = preg_replace("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", "<a href='/$1'>$2</a>", $text);
$text = preg_replace("%([\n\r\s]|^)(#([\p{L}_-]++[0-9]*[\p{L}_-]*))%Xu", "$1<a href='/feed/hashtag/$3'>$2</a>", $text); $text = preg_replace_callback("%([\n\r\s]|^)(\#([\p{L}_0-9][\p{L}_0-9\(\)\-\']+[\p{L}_0-9\(\)]|[\p{L}_0-9]{1,2}))%Xu", function($m) {
$slug = rawurlencode($m[3]);
return "$m[1]<a href='/feed/hashtag/$slug'>$m[2]</a>";
}, $text);
$text = $this->formatEmojis($text); $text = $this->formatEmojis($text);
} }
$text = $this->removeZalgo($text); $text = $this->removeZalgo($text);
$text = nl2br($text); $text = nl2br($text);
} else {
$text = str_replace("\r\n","\n", $text);
} }
if(OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["christian"]) if(OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["christian"])

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use morphos\Gender;
use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
@ -9,6 +10,7 @@ use openvk\Web\Models\Exceptions\InvalidUserNameException;
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;
use function morphos\Russian\inflectName;
class User extends RowModel class User extends RowModel
{ {
@ -83,6 +85,11 @@ class User extends RowModel
{ {
return (bool) $this->getRecord()->microblog; return (bool) $this->getRecord()->microblog;
} }
function getMainPage(): int
{
return $this->getRecord()->main_page;
}
function getChandlerGUID(): string function getChandlerGUID(): string
{ {
@ -138,24 +145,32 @@ class User extends RowModel
return iterator_to_array($avPhotos)[0] ?? NULL; return iterator_to_array($avPhotos)[0] ?? NULL;
} }
function getFirstName(): string function getFirstName(bool $pristine = false): string
{ {
return $this->getRecord()->deleted ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine)
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE);
else
return $name;
} }
function getLastName(): string function getLastName(bool $pristine = false): string
{ {
return $this->getRecord()->deleted ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine)
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE);
else
return $name;
} }
function getPseudo(): ?string function getPseudo(): ?string
{ {
return $this->getRecord()->deleted ? "DELETED" : $this->getRecord()->pseudo; return ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : $this->getRecord()->pseudo);
} }
function getFullName(): string function getFullName(): string
{ {
if($this->getRecord()->deleted) if($this->isDeleted() && !$this->isDeactivated())
return "DELETED"; return "DELETED";
$pseudo = $this->getPseudo(); $pseudo = $this->getPseudo();
@ -166,50 +181,61 @@ class User extends RowModel
return $this->getFirstName() . $pseudo . $this->getLastName(); return $this->getFirstName() . $pseudo . $this->getLastName();
} }
function getMorphedName(string $case = "genitive", bool $fullName = true): string
{
$name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName();
if(!preg_match("%^[А-яё\-]+$%", $name))
return $name; # name is probably not russian
$inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE);
return $inflected ?: $name;
}
function getCanonicalName(): string function getCanonicalName(): string
{ {
if($this->getRecord()->deleted) if($this->isDeleted() && !$this->isDeactivated())
return "DELETED"; return "DELETED";
else else
return $this->getFirstName() . ' ' . $this->getLastName(); return $this->getFirstName() . " " . $this->getLastName();
} }
function getPhone(): ?string function getPhone(): ?string
{ {
return $this->getRecord()->phone; return $this->getRecord()->phone;
} }
function getEmail(): ?string function getEmail(): ?string
{ {
return $this->getRecord()->email; return $this->getRecord()->email;
} }
function getOnline(): DateTime function getOnline(): DateTime
{ {
return new DateTime($this->getRecord()->online); return new DateTime($this->getRecord()->online);
} }
function getDescription(): ?string function getDescription(): ?string
{ {
return $this->getRecord()->about; return $this->getRecord()->about;
} }
function getStatus(): ?string function getStatus(): ?string
{ {
return $this->getRecord()->status; return $this->getRecord()->status;
} }
function getShortCode(): ?string function getShortCode(): ?string
{ {
return $this->getRecord()->shortcode; return $this->getRecord()->shortcode;
} }
function getAlert(): ?string function getAlert(): ?string
{ {
return $this->getRecord()->alert; return $this->getRecord()->alert;
} }
function getBanReason(): ?string function getBanReason(): ?string
{ {
return $this->getRecord()->block_reason; return $this->getRecord()->block_reason;
@ -219,105 +245,118 @@ class User extends RowModel
{ {
return $this->getRecord()->block_in_support_reason; return $this->getRecord()->block_in_support_reason;
} }
function getType(): int function getType(): int
{ {
return $this->getRecord()->type; return $this->getRecord()->type;
} }
function getCoins(): float function getCoins(): float
{ {
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
return 0.0; return 0.0;
return $this->getRecord()->coins; return $this->getRecord()->coins;
} }
function getRating(): int function getRating(): int
{ {
return OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"] ? $this->getRecord()->rating : 0; return OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"] ? $this->getRecord()->rating : 0;
} }
function getReputation(): int function getReputation(): int
{ {
return $this->getRecord()->reputation; return $this->getRecord()->reputation;
} }
function getRegistrationTime(): DateTime function getRegistrationTime(): DateTime
{ {
return new DateTime($this->getRecord()->since->getTimestamp()); return new DateTime($this->getRecord()->since->getTimestamp());
} }
function getRegistrationIP(): string function getRegistrationIP(): string
{ {
return $this->getRecord()->registering_ip; return $this->getRecord()->registering_ip;
} }
function getHometown(): ?string function getHometown(): ?string
{ {
return $this->getRecord()->hometown; return $this->getRecord()->hometown;
} }
function getPoliticalViews(): int function getPoliticalViews(): int
{ {
return $this->getRecord()->polit_views; return $this->getRecord()->polit_views;
} }
function getMaritalStatus(): int function getMaritalStatus(): int
{ {
return $this->getRecord()->marital_status; return $this->getRecord()->marital_status;
} }
function getLocalizedMaritalStatus(): string
{
$status = $this->getMaritalStatus();
$string = "relationship_$status";
if($this->isFemale()) {
$res = tr($string . "_fem");
if($res != ("@" . $string . "_fem"))
return $res; # If fem version exists, return
}
return tr($string);
}
function getContactEmail(): ?string function getContactEmail(): ?string
{ {
return $this->getRecord()->email_contact; return $this->getRecord()->email_contact;
} }
function getTelegram(): ?string function getTelegram(): ?string
{ {
return $this->getRecord()->telegram; return $this->getRecord()->telegram;
} }
function getInterests(): ?string function getInterests(): ?string
{ {
return $this->getRecord()->interests; return $this->getRecord()->interests;
} }
function getFavoriteMusic(): ?string function getFavoriteMusic(): ?string
{ {
return $this->getRecord()->fav_music; return $this->getRecord()->fav_music;
} }
function getFavoriteFilms(): ?string function getFavoriteFilms(): ?string
{ {
return $this->getRecord()->fav_films; return $this->getRecord()->fav_films;
} }
function getFavoriteShows(): ?string function getFavoriteShows(): ?string
{ {
return $this->getRecord()->fav_shows; return $this->getRecord()->fav_shows;
} }
function getFavoriteBooks(): ?string function getFavoriteBooks(): ?string
{ {
return $this->getRecord()->fav_books; return $this->getRecord()->fav_books;
} }
function getFavoriteQuote(): ?string function getFavoriteQuote(): ?string
{ {
return $this->getRecord()->fav_quote; return $this->getRecord()->fav_quote;
} }
function getCity(): ?string function getCity(): ?string
{ {
return $this->getRecord()->city; return $this->getRecord()->city;
} }
function getPhysicalAddress(): ?string function getPhysicalAddress(): ?string
{ {
return $this->getRecord()->address; return $this->getRecord()->address;
} }
function getNotificationOffset(): int function getNotificationOffset(): int
{ {
return $this->getRecord()->notification_offset; return $this->getRecord()->notification_offset;
@ -325,14 +364,22 @@ class User extends RowModel
function getBirthday(): ?DateTime function getBirthday(): ?DateTime
{ {
return new DateTime($this->getRecord()->birthday); if(is_null($this->getRecord()->birthday))
return NULL;
else
return new DateTime($this->getRecord()->birthday);
}
function getBirthdayPrivacy(): int
{
return $this->getRecord()->birthday_privacy;
} }
function getAge(): ?int function getAge(): ?int
{ {
return (int)floor((time() - $this->getBirthday()->timestamp()) / YEAR); return (int)floor((time() - $this->getBirthday()->timestamp()) / YEAR);
} }
function get2faSecret(): ?string function get2faSecret(): ?string
{ {
return $this->getRecord()["2fa_secret"]; return $this->getRecord()["2fa_secret"];
@ -347,7 +394,7 @@ class User extends RowModel
{ {
$this->stateChanges("notification_offset", time()); $this->stateChanges("notification_offset", time());
} }
function getLeftMenuItemStatus(string $id): bool function getLeftMenuItemStatus(string $id): bool
{ {
return (bool) bmask($this->getRecord()->left_menu, [ return (bool) bmask($this->getRecord()->left_menu, [
@ -364,7 +411,7 @@ class User extends RowModel
], ],
])->get($id); ])->get($id);
} }
function getPrivacySetting(string $id): int function getPrivacySetting(string $id): int
{ {
return (int) bmask($this->getRecord()->privacy, [ return (int) bmask($this->getRecord()->privacy, [
@ -383,7 +430,7 @@ class User extends RowModel
], ],
])->get($id); ])->get($id);
} }
function getPrivacyPermission(string $permission, ?User $user = NULL): bool function getPrivacyPermission(string $permission, ?User $user = NULL): bool
{ {
$permStatus = $this->getPrivacySetting($permission); $permStatus = $this->getPrivacySetting($permission);
@ -391,7 +438,7 @@ class User extends RowModel
return $permStatus === User::PRIVACY_EVERYONE; return $permStatus === User::PRIVACY_EVERYONE;
else if($user->getId() === $this->getId()) else if($user->getId() === $this->getId())
return true; return true;
switch($permStatus) { switch($permStatus) {
case User::PRIVACY_ONLY_FRIENDS: case User::PRIVACY_ONLY_FRIENDS:
return $this->getSubscriptionStatus($user) === User::SUBSCRIPTION_MUTUAL; return $this->getSubscriptionStatus($user) === User::SUBSCRIPTION_MUTUAL;
@ -402,12 +449,12 @@ class User extends RowModel
return false; return false;
} }
} }
function getProfileCompletenessReport(): object function getProfileCompletenessReport(): object
{ {
$incompleteness = 0; $incompleteness = 0;
$unfilled = []; $unfilled = [];
if(!$this->getRecord()->status) { if(!$this->getRecord()->status) {
$unfilled[] = "status"; $unfilled[] = "status";
$incompleteness += 15; $incompleteness += 15;
@ -428,46 +475,56 @@ class User extends RowModel
$unfilled[] = "interests"; $unfilled[] = "interests";
$incompleteness += 20; $incompleteness += 20;
} }
$total = max(100 - $incompleteness + $this->getRating(), 0); $total = max(100 - $incompleteness + $this->getRating(), 0);
if(ovkGetQuirk("profile.rating-bar-behaviour") === 0) if(ovkGetQuirk("profile.rating-bar-behaviour") === 0)
if ($total >= 100) if ($total >= 100)
$percent = round(($total / 10**strlen(strval($total))) * 100, 0); $percent = round(($total / 10**strlen(strval($total))) * 100, 0);
else else
$percent = min($total, 100); $percent = min($total, 100);
return (object) [ return (object) [
"total" => $total, "total" => $total,
"percent" => $percent, "percent" => $percent,
"unfilled" => $unfilled, "unfilled" => $unfilled,
]; ];
} }
function getFriends(int $page = 1, int $limit = 6): \Traversable function getFriends(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-friends", $page, $limit); return $this->_abstractRelationGenerator("get-friends", $page, $limit);
} }
function getFriendsCount(): int function getFriendsCount(): int
{ {
return $this->_abstractRelationCount("get-friends"); return $this->_abstractRelationCount("get-friends");
} }
function getFriendsOnline(int $page = 1, int $limit = 6): \Traversable
{
return $this->_abstractRelationGenerator("get-online-friends", $page, $limit);
}
function getFriendsOnlineCount(): int
{
return $this->_abstractRelationCount("get-online-friends");
}
function getFollowers(int $page = 1, int $limit = 6): \Traversable function getFollowers(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-followers", $page, $limit); return $this->_abstractRelationGenerator("get-followers", $page, $limit);
} }
function getFollowersCount(): int function getFollowersCount(): int
{ {
return $this->_abstractRelationCount("get-followers"); return $this->_abstractRelationCount("get-followers");
} }
function getSubscriptions(int $page = 1, int $limit = 6): \Traversable function getSubscriptions(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit); return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit);
} }
function getSubscriptionsCount(): int function getSubscriptionsCount(): int
{ {
return $this->_abstractRelationCount("get-subscriptions-user"); return $this->_abstractRelationCount("get-subscriptions-user");
@ -477,7 +534,7 @@ 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): \Traversable
{ {
if($admin) { if($admin) {
@ -502,7 +559,7 @@ class User extends RowModel
} }
} }
} }
function getClubCount(bool $admin = false): int function getClubCount(bool $admin = false): int
{ {
if($admin) { if($admin) {
@ -517,7 +574,7 @@ class User extends RowModel
return sizeof($sel); return sizeof($sel);
} }
} }
function getPinnedClubs(): \Traversable function getPinnedClubs(): \Traversable
{ {
foreach($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) { foreach($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) {
@ -558,16 +615,16 @@ class User extends RowModel
foreach($sel as $target) { foreach($sel as $target) {
$target = (new Clubs)->get($target->event); $target = (new Clubs)->get($target->event);
if(!$target) continue; if(!$target) continue;
yield $target; yield $target;
} }
} }
function getMeetingCount(): int function getMeetingCount(): int
{ {
return sizeof($this->getRecord()->related("event_turnouts.user")); return sizeof($this->getRecord()->related("event_turnouts.user"));
} }
function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); $gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
@ -581,7 +638,7 @@ class User extends RowModel
]; ];
} }
} }
function getGiftCount(): int function getGiftCount(): int
{ {
return sizeof($this->getRecord()->related("gift_user_relations.receiver")); return sizeof($this->getRecord()->related("gift_user_relations.receiver"));
@ -616,9 +673,9 @@ class User extends RowModel
function use2faBackupCode(int $code): bool function use2faBackupCode(int $code): bool
{ {
return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete(); return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete();
} }
function getSubscriptionStatus(User $user): int function getSubscriptionStatus(User $user): int
{ {
$subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([ $subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([
@ -629,51 +686,51 @@ class User extends RowModel
"model" => static::class, "model" => static::class,
"follower" => $user->getId(), "follower" => $user->getId(),
])->fetch()); ])->fetch());
if($subbed && $followed) return User::SUBSCRIPTION_MUTUAL; if($subbed && $followed) return User::SUBSCRIPTION_MUTUAL;
if($subbed) return User::SUBSCRIPTION_INCOMING; if($subbed) return User::SUBSCRIPTION_INCOMING;
if($followed) return User::SUBSCRIPTION_OUTGOING; if($followed) return User::SUBSCRIPTION_OUTGOING;
return User::SUBSCRIPTION_ABSENT; return User::SUBSCRIPTION_ABSENT;
} }
function getNotificationsCount(bool $archived = false): int function getNotificationsCount(bool $archived = false): int
{ {
return (new Notifications)->getNotificationCountByUser($this, $this->getNotificationOffset(), $archived); return (new Notifications)->getNotificationCountByUser($this, $this->getNotificationOffset(), $archived);
} }
function getNotifications(int $page, bool $archived = false): \Traversable function getNotifications(int $page, bool $archived = false): \Traversable
{ {
return (new Notifications)->getNotificationsByUser($this, $this->getNotificationOffset(), $archived, $page); return (new Notifications)->getNotificationsByUser($this, $this->getNotificationOffset(), $archived, $page);
} }
function getPendingPhoneVerification(): ?ActiveRow function getPendingPhoneVerification(): ?ActiveRow
{ {
return $this->getRecord()->ref("number_verification", "id"); return $this->getRecord()->ref("number_verification", "id");
} }
function getRefLinkId(): string function getRefLinkId(): string
{ {
$hash = hash_hmac("Snefru", (string) $this->getId(), CHANDLER_ROOT_CONF["security"]["secret"], true); $hash = hash_hmac("Snefru", (string) $this->getId(), CHANDLER_ROOT_CONF["security"]["secret"], true);
return dechex($this->getId()) . " " . base64_encode($hash); return dechex($this->getId()) . " " . base64_encode($hash);
} }
function getNsfwTolerance(): int function getNsfwTolerance(): int
{ {
return $this->getRecord()->nsfw_tolerance; return $this->getRecord()->nsfw_tolerance;
} }
function isFemale(): bool function isFemale(): bool
{ {
return (bool) $this->getRecord()->sex; return (bool) $this->getRecord()->sex;
} }
function isVerified(): bool function isVerified(): bool
{ {
return (bool) $this->getRecord()->verified; return (bool) $this->getRecord()->verified;
} }
function isBanned(): bool function isBanned(): bool
{ {
return !is_null($this->getBanReason()); return !is_null($this->getBanReason());
@ -683,22 +740,22 @@ class User extends RowModel
{ {
return !is_null($this->getBanInSupportReason()); return !is_null($this->getBanInSupportReason());
} }
function isOnline(): bool function isOnline(): bool
{ {
return time() - $this->getRecord()->online <= 300; return time() - $this->getRecord()->online <= 300;
} }
function prefersNotToSeeRating(): bool function prefersNotToSeeRating(): bool
{ {
return !((bool) $this->getRecord()->show_rating); return !((bool) $this->getRecord()->show_rating);
} }
function hasPendingNumberChange(): bool function hasPendingNumberChange(): bool
{ {
return !is_null($this->getPendingPhoneVerification()); return !is_null($this->getPendingPhoneVerification());
} }
function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void
{ {
DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([ DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([
@ -710,8 +767,8 @@ class User extends RowModel
"sent" => time(), "sent" => time(),
]); ]);
} }
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");
@ -723,42 +780,64 @@ class User extends RowModel
); );
$subs->delete(); $subs->delete();
} }
$this->setBlock_Reason($reason); $this->setBlock_Reason($reason);
$this->setUnblock_time($unban_time);
$this->save(); $this->save();
} }
function deactivate(?string $reason): void
{
$this->setDeleted(1);
$this->setDeact_Date(time() + (MONTH * 7));
$this->setDeact_Reason($reason);
$this->save();
}
function reactivate(): void
{
$this->setDeleted(0);
$this->setDeact_Date(0);
$this->setDeact_Reason("");
$this->save();
}
function getDeactivationDate(): DateTime
{
return new DateTime($this->getRecord()->deact_date);
}
function verifyNumber(string $code): bool function verifyNumber(string $code): bool
{ {
$ver = $this->getPendingPhoneVerification(); $ver = $this->getPendingPhoneVerification();
if(!$ver) return false; if(!$ver) return false;
try { try {
if(sodium_memcmp((string) $ver->code, $code) === -1) return false; if(sodium_memcmp((string) $ver->code, $code) === -1) return false;
} catch(\SodiumException $ex) { } catch(\SodiumException $ex) {
return false; return false;
} }
$this->setPhone($ver->number); $this->setPhone($ver->number);
$this->save(); $this->save();
DatabaseConnection::i()->getContext() DatabaseConnection::i()->getContext()
->table("number_verification") ->table("number_verification")
->where("user", $this->getId()) ->where("user", $this->getId())
->delete(); ->delete();
return true; return true;
} }
function setFirst_Name(string $firstName): void function setFirst_Name(string $firstName): void
{ {
$firstName = mb_convert_case($firstName, MB_CASE_TITLE); $firstName = mb_convert_case($firstName, MB_CASE_TITLE);
if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?(?:[\p{L&}\p{Lo}]\p{Mn}?){1,16}$%u', $firstName)) if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?(?:[\p{L&}\p{Lo}]\p{Mn}?){1,16}$%u', $firstName))
throw new InvalidUserNameException; throw new InvalidUserNameException;
$this->stateChanges("first_name", $firstName); $this->stateChanges("first_name", $firstName);
} }
function setLast_Name(string $lastName): void function setLast_Name(string $lastName): void
{ {
if(!empty($lastName)) if(!empty($lastName))
@ -767,15 +846,15 @@ class User extends RowModel
if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?([\p{L&}\p{Lo}]\p{Mn}?){1,16}(\-\g<1>+)?$%u', $lastName)) if(!preg_match('%^[\p{Lu}\p{Lo}]\p{Mn}?([\p{L&}\p{Lo}]\p{Mn}?){1,16}(\-\g<1>+)?$%u', $lastName))
throw new InvalidUserNameException; throw new InvalidUserNameException;
} }
$this->stateChanges("last_name", $lastName); $this->stateChanges("last_name", $lastName);
} }
function setNsfwTolerance(int $tolerance): void function setNsfwTolerance(int $tolerance): void
{ {
$this->stateChanges("nsfw_tolerance", $tolerance); $this->stateChanges("nsfw_tolerance", $tolerance);
} }
function setPrivacySetting(string $id, int $status): void function setPrivacySetting(string $id, int $status): void
{ {
$this->stateChanges("privacy", bmask($this->changes["privacy"] ?? $this->getRecord()->privacy, [ $this->stateChanges("privacy", bmask($this->changes["privacy"] ?? $this->getRecord()->privacy, [
@ -794,7 +873,7 @@ class User extends RowModel
], ],
])->set($id, $status)->toInteger()); ])->set($id, $status)->toInteger());
} }
function setLeftMenuItemStatus(string $id, bool $status): void function setLeftMenuItemStatus(string $id, bool $status): void
{ {
$mask = bmask($this->changes["left_menu"] ?? $this->getRecord()->left_menu, [ $mask = bmask($this->changes["left_menu"] ?? $this->getRecord()->left_menu, [
@ -810,10 +889,10 @@ class User extends RowModel
"poster", "poster",
], ],
])->set($id, (int) $status)->toInteger(); ])->set($id, (int) $status)->toInteger();
$this->stateChanges("left_menu", $mask); $this->stateChanges("left_menu", $mask);
} }
function setShortCode(?string $code = NULL, bool $force = false): ?bool function setShortCode(?string $code = NULL, bool $force = false): ?bool
{ {
if(!is_null($code)) { if(!is_null($code)) {
@ -825,20 +904,20 @@ class User extends RowModel
return false; return false;
if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy") if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$code")[0]->presenter !== "UnknownTextRouteStrategy")
return false; return false;
$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;
} }
$this->stateChanges("shortcode", $code); $this->stateChanges("shortcode", $code);
return true; return true;
} }
function setPhoneWithVerification(string $phone): string function setPhoneWithVerification(string $phone): string
{ {
$code = unpack("S", openssl_random_pseudo_bytes(2))[1]; $code = unpack("S", openssl_random_pseudo_bytes(2))[1];
if($this->hasPendingNumberChange()) { if($this->hasPendingNumberChange()) {
DatabaseConnection::i()->getContext() DatabaseConnection::i()->getContext()
->table("number_verification") ->table("number_verification")
@ -849,10 +928,10 @@ class User extends RowModel
->table("number_verification") ->table("number_verification")
->insert(["user" => $this->getId(), "number" => $phone, "code" => $code]); ->insert(["user" => $this->getId(), "number" => $phone, "code" => $code]);
} }
return (string) $code; return (string) $code;
} }
# KABOBSQL temporary fix # KABOBSQL temporary fix
# Tuesday, the 7th of January 2020 @ 22:43 <Menhera>: implementing quick fix to this problem and monitoring # Tuesday, the 7th of January 2020 @ 22:43 <Menhera>: implementing quick fix to this problem and monitoring
# NOTICE: this is an ongoing conversation, add your comments just above this line. Thanks! # NOTICE: this is an ongoing conversation, add your comments just above this line. Thanks!
@ -860,10 +939,21 @@ class User extends RowModel
{ {
$this->stateChanges("shortcode", $this->getRecord()->shortcode); #fix KABOBSQL $this->stateChanges("shortcode", $this->getRecord()->shortcode); #fix KABOBSQL
$this->stateChanges("online", $time); $this->stateChanges("online", $time);
return true; return true;
} }
function changeEmail(string $email): void
{
DatabaseConnection::i()->getContext()->table("ChandlerUsers")
->where("id", $this->getChandlerUser()->getId())->update([
"login" => $email
]);
$this->stateChanges("email", $email);
$this->save();
}
function adminNotify(string $message): bool function adminNotify(string $message): bool
{ {
$admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; $admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"];
@ -871,18 +961,26 @@ class User extends RowModel
return false; return false;
else if(is_null($admin = (new Users)->get($admId))) else if(is_null($admin = (new Users)->get($admId)))
return false; return false;
$cor = new Correspondence($admin, $this); $cor = new Correspondence($admin, $this);
$msg = new Message; $msg = new Message;
$msg->setContent($message); $msg->setContent($message);
$cor->sendMessage($msg, true); $cor->sendMessage($msg, true);
return true; return true;
} }
function isDeleted(): bool function isDeleted(): bool
{ {
if ($this->getRecord()->deleted == 1) if($this->getRecord()->deleted == 1)
return TRUE;
else
return FALSE;
}
function isDeactivated(): bool
{
if($this->getDeactivationDate()->timestamp() > time())
return TRUE; return TRUE;
else else
return FALSE; return FALSE;
@ -903,7 +1001,7 @@ class User extends RowModel
case 2: case 2:
return 2; return 2;
break; break;
default: default:
return 0; return 0;
break; break;
@ -915,11 +1013,27 @@ class User extends RowModel
return $this->getRecord()->website; return $this->getRecord()->website;
} }
// ты устрица # ты устрица
function isActivated(): bool function isActivated(): bool
{ {
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

@ -14,6 +14,8 @@ class Video extends Media
protected $tableName = "videos"; protected $tableName = "videos";
protected $fileExtension = "ogv"; protected $fileExtension = "ogv";
protected $processingPlaceholder = "video/rendering";
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
@ -37,7 +39,7 @@ class Video extends Media
throw new \DomainException("$filename does not contain any meaningful video streams"); throw new \DomainException("$filename does not contain any meaningful video streams");
try { try {
if(!is_dir($dirId = $this->pathFromHash($hash))) if(!is_dir($dirId = dirname($this->pathFromHash($hash))))
mkdir($dirId); mkdir($dirId);
$dir = $this->getBaseDir(); $dir = $this->getBaseDir();
@ -53,7 +55,23 @@ class Video extends Media
usleep(200100); usleep(200100);
return true; return true;
} }
protected function checkIfFileIsProcessed(): bool
{
if($this->getType() != Video::TYPE_DIRECT)
return true;
if(!file_exists($this->getFileName())) {
if((time() - $this->getRecord()->last_checked) > 3600) {
# TODO notify that video processor is probably dead
}
return false;
}
return true;
}
function getName(): string function getName(): string
{ {
return $this->getRecord()->name; return $this->getRecord()->name;
@ -83,6 +101,9 @@ class Video extends Media
function getThumbnailURL(): string function getThumbnailURL(): string
{ {
if($this->getType() === Video::TYPE_DIRECT) { if($this->getType() === Video::TYPE_DIRECT) {
if(!$this->isProcessed())
return "/assets/packages/static/openvk/video/rendering.apng";
return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL()); return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL());
} else { } else {
return $this->getVideoDriver()->getThumbnailURL(); return $this->getVideoDriver()->getThumbnailURL();

View file

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

View file

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

View file

@ -45,7 +45,7 @@ class Clubs
function getPopularClubs(): \Traversable function getPopularClubs(): \Traversable
{ {
$query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 10;"; $query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;";
$entries = DatabaseConnection::i()->getConnection()->query($query); $entries = DatabaseConnection::i()->getConnection()->query($query);
foreach($entries as $entry) foreach($entries as $entry)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once # video stub logic was implicitly deprecated, so we start processing at once
ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif" ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf scale=640x360,setsar=1:1 -y $temp2 ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2
Move-Item $temp2 "$dir$hashT/$hash.ogv" Move-Item $temp2 "$dir$hashT/$hash.ogv"
Remove-Item $temp Remove-Item $temp

View file

@ -5,7 +5,7 @@ cp ../files/video/rendering.apng $3${4:0:2}/$4.gif
cp ../files/video/rendering.ogv $3/${4:0:2}/$4.ogv cp ../files/video/rendering.ogv $3/${4:0:2}/$4.ogv
nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif
nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf scale=640x360,setsar=1:1 -y "/tmp/ffmOi$tmpfile.ogv" nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.ogv"
rm -rf $3${4:0:2}/$4.ogv rm -rf $3${4:0:2}/$4.ogv
mv "/tmp/ffmOi$tmpfile.ogv" $3${4:0:2}/$4.ogv mv "/tmp/ffmOi$tmpfile.ogv" $3${4:0:2}/$4.ogv

View file

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

View file

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

View file

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

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\{Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
{ {
@ -9,13 +10,15 @@ 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) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks)
{ {
$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;
parent::__construct(); parent::__construct();
} }
@ -23,7 +26,7 @@ final class AdminPresenter extends OpenVKPresenter
private function warnIfNoCommerce(): void private function warnIfNoCommerce(): void
{ {
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flash("warn", "Коммерция отключена системным администратором", "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния."); $this->flash("warn", tr("admin_commerce_disabled"), tr("admin_commerce_disabled_desc"));
} }
private function searchResults(object $repo, &$count) private function searchResults(object $repo, &$count)
@ -70,14 +73,14 @@ final class AdminPresenter extends OpenVKPresenter
$user->setLast_Name($this->postParam("last_name")); $user->setLast_Name($this->postParam("last_name"));
$user->setPseudo($this->postParam("nickname")); $user->setPseudo($this->postParam("nickname"));
$user->setStatus($this->postParam("status")); $user->setStatus($this->postParam("status"));
$user->setVerified(empty($this->postParam("verify") ? 0 : 1));
if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online")));
if(!$user->setShortCode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"))) if(!$user->setShortCode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode")))
$this->flash("err", tr("error"), tr("error_shorturl_incorrect")); $this->flash("err", tr("error"), tr("error_shorturl_incorrect"));
$user->changeEmail($this->postParam("email"));
if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online")));
$user->setVerified(empty($this->postParam("verify") ? 0 : 1));
$user->save(); $user->save();
break; break;
} }
} }
@ -170,8 +173,7 @@ final class AdminPresenter extends OpenVKPresenter
$voucher->save(); $voucher->save();
$this->redirect("/admin/vouchers/id" . $voucher->getId(), static::REDIRECT_TEMPORARY); $this->redirect("/admin/vouchers/id" . $voucher->getId());
exit;
} }
function renderGiftCategories(): void function renderGiftCategories(): void
@ -193,7 +195,7 @@ final class AdminPresenter extends OpenVKPresenter
if(!$cat) if(!$cat)
$this->notFound(); $this->notFound();
else if($cat->getSlug() !== $slug) else if($cat->getSlug() !== $slug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta", static::REDIRECT_TEMPORARY); $this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta");
} else { } else {
$gen = true; $gen = true;
$cat = new GiftCategory; $cat = new GiftCategory;
@ -234,7 +236,7 @@ final class AdminPresenter extends OpenVKPresenter
$cat->setDescription($code, $this->postParam("description_$code")); $cat->setDescription($code, $this->postParam("description_$code"));
} }
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta", static::REDIRECT_TEMPORARY); $this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta");
} }
function renderGifts(string $catSlug, int $catId): void function renderGifts(string $catSlug, int $catId): void
@ -245,7 +247,7 @@ final class AdminPresenter extends OpenVKPresenter
if(!$cat) if(!$cat)
$this->notFound(); $this->notFound();
else if($cat->getSlug() !== $catSlug) else if($cat->getSlug() !== $catSlug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/", static::REDIRECT_TEMPORARY); $this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/");
$this->template->cat = $cat; $this->template->cat = $cat;
$this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count)); $this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
@ -284,7 +286,7 @@ final class AdminPresenter extends OpenVKPresenter
$name = $catTo->getName(); $name = $catTo->getName();
$this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>."); $this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>.");
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/", static::REDIRECT_TEMPORARY); $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/");
break; break;
default: default:
case "edit": case "edit":
@ -328,7 +330,7 @@ final class AdminPresenter extends OpenVKPresenter
$cat->addGift($gift); $cat->addGift($gift);
} }
$this->redirect("/admin/gifts/id" . $gift->getId(), static::REDIRECT_TEMPORARY); $this->redirect("/admin/gifts/id" . $gift->getId());
} }
} }
@ -340,12 +342,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") ]));
} }
@ -357,7 +361,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 ]));
} }
@ -373,4 +378,73 @@ 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");
}
} }

View file

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

View file

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

View file

@ -1,13 +1,29 @@
<?php declare(strict_types=1); <?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

@ -19,20 +19,23 @@ final class BlobPresenter extends OpenVKPresenter
header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Origin: *");
$dir = $this->getDirName($dir); $dir = $this->getDirName($dir);
$name = preg_replace("%[^a-zA-Z0-9_\-]++%", "", $name); $base = realpath(OPENVK_ROOT . "/storage/$dir");
$path = OPENVK_ROOT . "/storage/$dir/$name.$format"; $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format");
if(!file_exists($path)) { if(!$path) # Will also check if file exists since realpath fails on ENOENT
$this->notFound(); $this->notFound();
} else { else if(strpos($path, $path) !== 0) # Prevent directory traversal and storage container escape
if(isset($_SERVER["HTTP_IF_NONE_MATCH"])) $this->notFound();
exit(header("HTTP/1.1 304 Not Modified"));
if(isset($_SERVER["HTTP_IF_NONE_MATCH"]))
header("Content-Type: " . mime_content_type($path)); exit(header("HTTP/1.1 304 Not Modified"));
header("Content-Size: " . filesize($path));
header("ETag: W/\"" . hash_file("snefru", $path) . "\""); header("Content-Type: " . mime_content_type($path));
header("Content-Size: " . filesize($path));
readfile($path); header("Cache-Control: public, max-age=1210000");
exit; header("X-Accel-Expires: 1210000");
} header("ETag: W/\"" . hash_file("snefru", $path) . "\"");
}
readfile($path);
exit;
}
} }

View file

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

View file

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

View file

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

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,20 +72,21 @@ 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));
if ($postTZ != $sessionOffset || $sessionOffset == null) { if ($postTZ != $sessionOffset || $sessionOffset == null) {
Session::i()->set("_timezoneOffset", $postTZ ? $postTZ : 3 * MINUTE ); Session::i()->set("_timezoneOffset", $postTZ ? $postTZ : 3 * MINUTE );
$this->returnJson([ $this->returnJson([
"success" => 1 // If it's new value "success" => 1 # If it's new value
]); ]);
} else { } else {
$this->returnJson([ $this->returnJson([
"success" => 2 // If it's the same value (if for some reason server will call this func) "success" => 2 # If it's the same value (if for some reason server will call this func)
]); ]);
} }
} else { } else {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,27 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block title}{_"banned_title"}{/block} {block title}{_banned_title}{/block}
{block header} {block header}
{_"banned_header"} {_banned_header}
{/block} {/block}
{block content} {block content}
<center> <center>
<img src="/assets/packages/static/openvk/img/oof.apng" alt="{_'banned_alt'}" style="width: 20%;" /> <img src="/assets/packages/static/openvk/img/oof.apng" alt="{_banned_alt}" style="width: 20%;" />
</center> </center>
<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

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

View file

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

View file

@ -1,7 +1,7 @@
{var instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']} {var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']}
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<!DOCTYPE html> <!DOCTYPE html>
<html n:if="!isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'"> <html>
<head> <head>
<title> <title>
{ifset title}{include title} - {/ifset}{$instance_name} {ifset title}{include title} - {/ifset}{$instance_name}
@ -17,7 +17,7 @@
{script "js/l10n.js"} {script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{if $isTimezoned == null} {if $isTimezoned == NULL}
{script "js/timezone.js"} {script "js/timezone.js"}
{/if} {/if}
@ -26,7 +26,7 @@
{css "css/nsfw-posts.css"} {css "css/nsfw-posts.css"}
{/if} {/if}
{if $theme !== null} {if $theme !== NULL}
{if $theme->inheritDefault()} {if $theme->inheritDefault()}
{css "css/style.css"} {css "css/style.css"}
{css "css/dialog.css"} {css "css/dialog.css"}
@ -102,29 +102,35 @@
<a href="/" class="home_button{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME} home_button_custom{/if}" title="{$instance_name}">{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if}</a> <a href="/" class="home_button{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME} home_button_custom{/if}" title="{$instance_name}">{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if}</a>
<div n:if="isset($thisUser) ? (!$thisUser->isBanned() XOR !$thisUser->isActivated()) : true" class="header_navigation"> <div n:if="isset($thisUser) ? (!$thisUser->isBanned() XOR !$thisUser->isActivated()) : true" class="header_navigation">
{ifset $thisUser} {ifset $thisUser}
<div class="link"> {if $thisUser->isDeactivated()}
<a href="/" title="[Alt+Shift+,]" accesskey=",">{_header_home}</a> <div class="link">
</div> <a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a>
<div class="link"> </div>
<a href="/search?type=groups">{_header_groups}</a> {else}
</div> <div class="link">
<div class="link"> <a href="/">{_header_home}</a>
<a href="/search">{_header_search}</a> </div>
</div> <div class="link">
<div class="link"> <a href="/search?type=groups">{_header_groups}</a>
<a href="/invite">{_header_invite}</a> </div>
</div> <div class="link">
<div class="link"> <a href="/search">{_header_search}</a>
<a href="/support">{_header_help} <b n:if="$ticketAnsweredCount > 0">({$ticketAnsweredCount})</b></a> </div>
</div> <div class="link">
<div class="link"> <a href="/invite">{_header_invite}</a>
<a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a> </div>
</div> <div class="link">
<div class="link"> <a href="/support">{_header_help} <b n:if="$ticketAnsweredCount > 0">({$ticketAnsweredCount})</b></a>
<form action="/search" method="get"> </div>
<input type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" /> <div class="link">
</form> <a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a>
</div> </div>
<div class="link">
<form action="/search" method="get">
<input type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" />
</form>
</div>
{/if}
{else} {else}
<div class="link"> <div class="link">
<a href="/login">{_header_login}</a> <a href="/login">{_header_login}</a>
@ -142,7 +148,7 @@
<div class="sidebar"> <div class="sidebar">
<div class="navigation"> <div class="navigation">
{ifset $thisUser} {ifset $thisUser}
{if !$thisUser->isBanned() XOR !$thisUser->isActivated()} {if !$thisUser->isBanned() XOR !$thisUser->isActivated() XOR $thisUser->isDeactivated()}
<a href="/edit" class="link edit-button">{_edit_button}</a> <a href="/edit" class="link edit-button">{_edit_button}</a>
<a href="{$thisUser->getURL()}" class="link" title="{_my_page} [Alt+Shift+.]" accesskey=".">{_my_page}</a> <a href="{$thisUser->getURL()}" class="link" title="{_my_page} [Alt+Shift+.]" accesskey=".">{_my_page}</a>
<a href="/friends{$thisUser->getId()}" class="link">{_my_friends} <a href="/friends{$thisUser->getId()}" class="link">{_my_friends}
@ -161,19 +167,20 @@
</a> </a>
<a n:if="$thisUser->getLeftMenuItemStatus('notes')" href="/notes{$thisUser->getId()}" class="link">{_my_notes}</a> <a n:if="$thisUser->getLeftMenuItemStatus('notes')" href="/notes{$thisUser->getId()}" class="link">{_my_notes}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('groups')" href="/groups{$thisUser->getId()}" class="link">{_my_groups}</a> <a n:if="$thisUser->getLeftMenuItemStatus('groups')" href="/groups{$thisUser->getId()}" class="link">{_my_groups}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link" title="{_my_feed} [Alt+Shift+W]" accesskey="w">{_my_feed}</a> <a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link" title="{_my_feed} [Alt+Shift+,]" accesskey=",">{_my_feed}</a>
<a href="/notifications" class="link" title="{_my_feedback} [Alt+Shift+N]" accesskey="n">{_my_feedback} <a href="/notifications" class="link" title="{_my_feedback} [Alt+Shift+N]" accesskey="n">{_my_feedback}
{if $thisUser->getNotificationsCount() > 0} {if $thisUser->getNotificationsCount() > 0}
(<b>{$thisUser->getNotificationsCount()}</b>) (<b>{$thisUser->getNotificationsCount()}</b>)
{/if} {/if}
</a> </a>
<a href="/apps?act=installed" class="link">{_my_apps}</a>
<a href="/settings" class="link">{_my_settings}</a> <a href="/settings" class="link">{_my_settings}</a>
{var canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
{var canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {var $canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
{var menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} {var $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="Админ-панель [Alt+Shift+A]" accesskey="a">Админ-панель</a> <a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a>
<a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">Helpdesk <a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">Helpdesk
{if $helpdeskTicketNotAnsweredCount > 0} {if $helpdeskTicketNotAnsweredCount > 0}
(<b>{$helpdeskTicketNotAnsweredCount}</b>) (<b>{$helpdeskTicketNotAnsweredCount}</b>)
@ -186,9 +193,50 @@
<div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div> <div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div>
<a n:foreach="$thisUser->getPinnedClubs() as $club" href="{$club->getURL()}" class="link group_link">{$club->getName()}</a> <a n:foreach="$thisUser->getPinnedClubs() as $club" href="{$club->getURL()}" class="link group_link">{$club->getName()}</a>
</div> </div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $thisUser->getCoins() != 0" id="votesBalance">
{tr("you_still_have_x_points", $thisUser->getCoins())|noescape}
<br /><br />
<a href="/settings?act=finance">{_top_up_your_account} &#xbb;</a>
</div>
<a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['enable'] && $thisUser->getLeftMenuItemStatus('poster')" href="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['link']}" > <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['enable'] && $thisUser->getLeftMenuItemStatus('poster')" href="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['link']}" >
<img src="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['src']}" alt="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['caption']}" class="psa-poster" style="max-width: 100%; margin-top: 50px;" /> <img src="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['src']}" alt="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['caption']}" class="psa-poster" style="max-width: 100%; margin-top: 10px;" />
</a> </a>
<div class="floating_sidebar">
<a class="minilink" href="/friends{$thisUser->getId()}">
<object type="internal/link" n:if="$thisUser->getFollowersCount() > 0">
<div class="counter">
+{$thisUser->getFollowersCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/friends.svg">
</a>
<a class="minilink" href="/albums{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/photos.svg">
</a>
<a class="minilink" href="/im">
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
<div class="counter">
+{$thisUser->getUnreadMessagesCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/messages.svg">
</a>
<a class="minilink" href="/groups{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/groups.svg">
</a>
<a class="minilink" href="/notifications">
<object type="internal/link" n:if="$thisUser->getNotificationsCount() > 0">
<div class="counter">
+{$thisUser->getNotificationsCount()}
</div>
</object>
<img src="/assets/packages/static/openvk/img/feedback.svg">
</a>
</div>
{elseif !$thisUser->isActivated()} {elseif !$thisUser->isActivated()}
<a href="/logout?hash={urlencode($csrfToken)}" class="link">{_menu_logout}</a> <a href="/logout?hash={urlencode($csrfToken)}" class="link">{_menu_logout}</a>
{else} {else}
@ -251,7 +299,7 @@
</div> </div>
<div class="page_footer"> <div class="page_footer">
{var dbVersion = \Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)} {var $dbVersion = \Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)}
<div class="navigation_footer"> <div class="navigation_footer">
<a href="/about" class="link">{_footer_about_instance}</a> <a href="/about" class="link">{_footer_about_instance}</a>
@ -267,6 +315,8 @@
</p> </p>
</div> </div>
{include "components/cookies.xml"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"} {script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/node_modules/soundjs/lib/soundjs.min.js"} {script "js/node_modules/soundjs/lib/soundjs.min.js"}
{script "js/node_modules/ky/umd.js"} {script "js/node_modules/ky/umd.js"}
@ -325,6 +375,7 @@
{/ifset} {/ifset}
</body> </body>
</html> </html>
{/if}
{if isset($parentModule) && substr($parentModule, 0, 21) !== 'libchandler:absolute.'} {if isset($parentModule) && substr($parentModule, 0, 21) !== 'libchandler:absolute.'}
<!-- INCLUDING TEMPLATE FROM PARENTMODULE: {$parentModule} --> <!-- INCLUDING TEMPLATE FROM PARENTMODULE: {$parentModule} -->

View file

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

View file

@ -9,7 +9,7 @@
<table width="100%" cellspacing="0" cellpadding="0"> <table width="100%" cellspacing="0" cellpadding="0">
<tbody> <tbody>
<tr valign="top"> <tr valign="top">
<td width="250" {if sizeof($admins) > 0}style="padding-right: 10px;"{/if}> <td width="250"{if sizeof($admins) > 0} style="padding-right: 10px;"{/if}>
<h4>{_statistics}</h4> <h4>{_statistics}</h4>
<div style="margin-top: 5px;"> <div style="margin-top: 5px;">
{_on_this_instance_are} {_on_this_instance_are}
@ -21,6 +21,15 @@
<li><span>{tr("about_wall_posts", $postsCount)|noescape}</span></li> <li><span>{tr("about_wall_posts", $postsCount)|noescape}</span></li>
</ul> </ul>
</div> </div>
{if OPENVK_ROOT_CONF['openvk']['preferences']['about']['links']}
<h4>{_about_links}</h4>
<div style="margin-top: 5px;">
{_instance_links}
<ul>
<li n:foreach="OPENVK_ROOT_CONF['openvk']['preferences']['about']['links'] as $aboutLink"><a href="{$aboutLink['url']}" target="_blank" class="link">{$aboutLink["name"]}</a></li>
</ul>
</div>
{/if}
</td> </td>
<td n:if="sizeof($admins) > 0"> <td n:if="sizeof($admins) > 0">
<h4>{_administrators}</h4> <h4>{_administrators}</h4>
@ -44,14 +53,23 @@
{if sizeof($popularClubs) !== 0} {if sizeof($popularClubs) !== 0}
<h4>{_most_popular_groups}</h4> <h4>{_most_popular_groups}</h4>
<ol> {var $entries = array_chunk($popularClubs, 10, true)}
<li n:foreach="$popularClubs as $entry" style="margin-top: 5px;"> <table width="100%" cellspacing="0" cellpadding="0">
<a href="{$entry->club->getURL()}">{$entry->club->getName()}</a> <tbody>
<div> <tr valign="top">
{tr("participants", $entry->subscriptions)} <td n:foreach="$entries as $chunk">
</div> <ol>
</li> <li value="{$num+1}" style="margin-top: 5px;" n:foreach="$chunk as $num => $club">
</ol> <a href="{$club->club->getURL()}">{$club->club->getName()}</a>
<div>
{tr("participants", $club->subscriptions)}
</div>
</li>
</ol>
</td>
</tr>
</tbody>
</table>
{/if} {/if}
<h4>{_rules}</h4> <h4>{_rules}</h4>

View file

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

View file

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

View file

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

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>
@ -474,7 +474,7 @@
</tr> </tr>
<tr> <tr>
<td class="e">Best barmaid</td> <td class="e">Best barmaid</td>
<td class="v">Jill</td> {* I can agree ~~ dsrev *} <td class="v">Jill</td>
</tr> </tr>
<tr> <tr>
<td class="e">Initial Helpdesk implementation</td> <td class="e">Initial Helpdesk implementation</td>

View file

@ -1,12 +1,13 @@
{var $instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']}
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<style> <style>
{var css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")} {var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")}
{str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape} {str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape}
</style> </style>
<title>{include title} - Админ-панель {=OPENVK_ROOT_CONF['openvk']['appearance']['name']}</title> <title>{include title} - {_admin} {$instance_name}</title>
</head> </head>
<body> <body>
<div id="page"> <div id="page">
@ -15,24 +16,16 @@
<div class="aui-header-inner"> <div class="aui-header-inner">
<div class="aui-header-primary"> <div class="aui-header-primary">
<h1 id="logo" class="aui-header-logo aui-header-logo-textonly"> <h1 id="logo" class="aui-header-logo aui-header-logo-textonly">
<a href="/admin"> <a href="/">
<span class="aui-header-logo-device">{=OPENVK_ROOT_CONF['openvk']['appearance']['name']}</span> <span class="aui-header-logo-device">{$instance_name}</span>
</a> </a>
</h1> </h1>
</div> </div>
<div n:if="$search ?? false" class="aui-header-secondary"> <div n:if="$search ?? false" class="aui-header-secondary">
<ul class="aui-nav"> <ul class="aui-nav">
<form class="aui-quicksearch dont-default-focus ajs-dirty-warning-exempt"> <form class="aui-quicksearch dont-default-focus ajs-dirty-warning-exempt">
<input <input id="quickSearchInput" autocomplete="off" class="search" type="text" placeholder="{include searchTitle}" value="{$_GET['q'] ?? ''}" name="q" accesskey="Q" />
id="quickSearchInput" <input type="hidden" value=1 name=p />
autocomplete="off"
class="search"
type="text"
placeholder="{include searchTitle}"
value="{$_GET['q'] ?? ''}"
name="q"
accesskey="Q" />
<input type="hidden" value=1 name=p />
</form> </form>
</ul> </ul>
</div> </div>
@ -46,83 +39,67 @@
<div class="aui-navgroup-inner"> <div class="aui-navgroup-inner">
<div class="aui-navgroup-primary"> <div class="aui-navgroup-primary">
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Обзор</strong> <strong>{_admin_overview}</strong>
</div> </div>
<ul class="aui-nav"> <ul class="aui-nav">
<li> <li>
<a href="/admin"> <a href="/admin">{_admin_overview_summary}</a>
Сводка
</a>
</li> </li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Пользовательский контент</strong> <strong>{_admin_content}</strong>
</div> </div>
<ul class="aui-nav"> <ul class="aui-nav">
<li> <li>
<a href="/admin/users"> <a href="/admin/users">{_users}</a>
Пользователи
</a>
</li> </li>
<li> <li>
<a href="/admin/clubs"> <a href="/admin/clubs">{_groups}</a>
Группы </li>
</a> <li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li> </li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Платные услуги</strong> <strong>{_admin_services}</strong>
</div> </div>
<ul class="aui-nav"> <ul class="aui-nav">
<li> <li>
<a href="/admin/vouchers"> <a href="/admin/vouchers">{_vouchers}</a>
{_vouchers}
</a>
</li> </li>
<li> <li>
<a href="/admin/gifts"> <a href="/admin/gifts">{_gifts}</a>
Подарки
</a>
</li> </li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Настройки</strong> <strong>{_admin_settings}</strong>
</div> </div>
<ul class="aui-nav"> <ul class="aui-nav">
<li> <li>
<a href="/admin/settings/tuning"> <a href="/admin/settings/tuning">{_admin_settings_tuning}</a>
Общие
</a>
</li> </li>
<li> <li>
<a href="/admin/settings/appearance"> <a href="/admin/settings/appearance">{_admin_settings_appearance}</a>
Внешний вид
</a>
</li> </li>
<li> <li>
<a href="/admin/settings/security"> <a href="/admin/settings/security">{_admin_settings_security}</a>
Безопасность
</a>
</li> </li>
<li> <li>
<a href="/admin/settings/integrations"> <a href="/admin/settings/integrations">{_admin_settings_integrations}</a>
Интеграции
</a>
</li> </li>
<li> <li>
<a href="/admin/settings/system"> <a href="/admin/settings/system">{_admin_settings_system}</a>
Система
</a>
</li> </li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Об OpenVK</strong> <strong>{_admin_about}</strong>
</div> </div>
<ul class="aui-nav"> <ul class="aui-nav">
<li> <li>
<a href="/about:openvk"> <a href="/about:openvk">{_admin_about_version}</a>
Версия </li>
</a> <li>
<a href="/about">{_admin_about_instance}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -131,7 +108,7 @@
</div> </div>
<section class="aui-page-panel-content"> <section class="aui-page-panel-content">
{ifset $flashMessage} {ifset $flashMessage}
{var type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]} {var $type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]}
<div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;"> <div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;">
<p class="title"> <p class="title">
<strong>{$flashMessage->title}</strong> <strong>{$flashMessage->title}</strong>
@ -139,11 +116,11 @@
<p>{$flashMessage->msg|noescape}</p> <p>{$flashMessage->msg|noescape}</p>
</div> </div>
{/ifset} {/ifset}
{ifset preHeader} {ifset preHeader}
{include preHeader} {include preHeader}
{/ifset} {/ifset}
<header class="aui-page-header"> <header class="aui-page-header">
<div class="aui-page-header-inner"> <div class="aui-page-header-inner">
<div class="aui-page-header-main"> <div class="aui-page-header-main">
@ -167,11 +144,11 @@
</section> </section>
</footer> </footer>
</div> </div>
{script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"} {script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"}
<script>AJS.tabs.setup();</script> <script>AJS.tabs.setup();</script>
{ifset scripts} {ifset scripts}
{include scripts} {include scripts}
{/ifset} {/ifset}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,9 @@
{block title} {block title}
{if $form->id === 0} {if $form->id === 0}
Новый подарок {_admin_newgift}
{else} {else}
Подарок "{$form->name}" {_gift} "{$form->name}"
{/if} {/if}
{/block} {/block}
@ -16,7 +16,7 @@
<form class="aui" method="POST" enctype="multipart/form-data"> <form class="aui" method="POST" enctype="multipart/form-data">
<div class="field-group"> <div class="field-group">
<label for="avatar"> <label for="avatar">
Изображение {_admin_image}
<span n:if="$form->id === 0" class="aui-icon icon-required"></span> <span n:if="$form->id === 0" class="aui-icon icon-required"></span>
</label> </label>
{if $form->id === 0} {if $form->id === 0}
@ -29,43 +29,39 @@
</span> </span>
<input style="display: none;" id="picInput" type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" /> <input style="display: none;" id="picInput" type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" />
<div class="description"> <div class="description">
<a id="picChange" href="javascript:false">Заменить изображение?</a> <a id="picChange" href="javascript:false">{_admin_image_replace}</a>
</div> </div>
{/if} {/if}
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="id"> <label for="id">ID</label>
ID
</label>
<input class="text long-field" type="number" id="id" disabled="disabled" value="{$form->id}" /> <input class="text long-field" type="number" id="id" disabled="disabled" value="{$form->id}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="putin"> <label for="usages">{_admin_uses}</label>
Использований <input class="text long-field" type="number" id="usages" disabled="disabled" value="{$form->usages}" />
</label>
<input class="text long-field" type="number" id="putin" disabled="disabled" value="{$form->usages}" />
<div n:if="$form->usages > 0" class="description"> <div n:if="$form->usages > 0" class="description">
<a href="javascript:$('#putin').value(0);">Обнулить?</a> <a href="javascript:$('#usages').value(0);">{_admin_uses_reset}</a>
</div> </div>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="name"> <label for="name">
Внутренее имя {_admin_name}
<span class="aui-icon icon-required"></span> <span class="aui-icon icon-required"></span>
</label> </label>
<input class="text long-field" type="text" id="name" name="name" value="{$form->name}" /> <input class="text long-field" type="text" id="name" name="name" value="{$form->name}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="price"> <label for="price">
Цена {_admin_price}
<span class="aui-icon icon-required"></span> <span class="aui-icon icon-required"></span>
</label> </label>
<input class="text long-field" type="number" id="price" name="price" min="0" value="{$form->price}" /> <input class="text long-field" type="number" id="price" name="price" min="0" value="{$form->price}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="limit"> <label for="limit">
Ограничение {_admin_limits}
<span class="aui-icon icon-required"></span> <span class="aui-icon icon-required"></span>
</label> </label>
<input class="text long-field" type="number" min="-1" id="limit" name="limit" value="{$form->limit}" /> <input class="text long-field" type="number" min="-1" id="limit" name="limit" value="{$form->limit}" />
@ -75,13 +71,13 @@
<div class="checkbox" resolved=""> <div class="checkbox" resolved="">
<input n:attr="disabled => $form->id === 0, checked => $form->id === 0" class="checkbox" type="checkbox" name="reset_limit" id="reset_limit" /> <input n:attr="disabled => $form->id === 0, checked => $form->id === 0" class="checkbox" type="checkbox" name="reset_limit" id="reset_limit" />
<span class="aui-form-glyph"></span> <span class="aui-form-glyph"></span>
<label for="reset_limit">Сбросить счётчик ограничений</label> <label for="reset_limit">{_admin_limits_reset}</label>
</div> </div>
</fieldset> </fieldset>
<input n:if="$form->id === 0" type="hidden" name="_cat" value="{$_GET['cat'] ?? 1}" /> <input n:if="$form->id === 0" type="hidden" name="_cat" value="{$_GET['cat'] ?? 1}" />
<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}" />
@ -94,12 +90,12 @@
{block scripts} {block scripts}
<script> <script>
const TRANS_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; const TRANS_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
$("#picChange").click(_ => $("#picInput").click()); $("#picChange").click(_ => $("#picInput").click());
$("#picInput").bind("change", e => { $("#picInput").bind("change", e => {
if(typeof e.target.files[0] === "undefined") if(typeof e.target.files[0] === "undefined")
$("#pic").prop("src", URL.createObjectURL(TRANS_GIF)); $("#pic").prop("src", URL.createObjectURL(TRANS_GIF));
$("#pic").prop("src", URL.createObjectURL(e.target.files[0])); $("#pic").prop("src", URL.createObjectURL(e.target.files[0]));
}); });
</script> </script>

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