Merge branch 'master' into textarea-refactor

This commit is contained in:
veselcraft 2022-04-26 10:16:24 +03:00
commit 9f699336f7
No known key found for this signature in database
GPG key ID: AED66BC1AC628A4E
175 changed files with 5882 additions and 1523 deletions

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ update.pid.old
Web/static/js/node_modules Web/static/js/node_modules
tmp/* tmp/*
!tmp/.gitkeep !tmp/api-storage
!tmp/themepack_artifacts/.gitkeep !tmp/themepack_artifacts/.gitkeep
themepacks/* themepacks/*
!themepacks/.gitkeep !themepacks/.gitkeep

8
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

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

View file

@ -0,0 +1,290 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE latte PUBLIC "-//LATTE//Latte plugin XML V0.0.1//EN" "Latte.dtd">
<latte vendor="latte" version="1">
<tags>
<tag name="_" type="AUTO_EMPTY" allowedFilters="true">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="=" type="UNPAIRED" allowedFilters="true">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="block" type="AUTO_EMPTY" allowedFilters="true" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="breakIf" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="capture" type="PAIR" allowedFilters="true" multiLine="true">
<arguments>
<argument name="variable" types="VARIABLE_DEFINITION" required="true" />
</arguments>
</tag>
<tag name="case" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" required="true" repeatable="true" />
</arguments>
</tag>
<tag name="catch" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="contentType" type="UNPAIRED">
<arguments>
<argument name="content-type" types="CONTENT_TYPE" validType="string" required="true" />
</arguments>
</tag>
<tag name="continueIf" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="debugbreak" type="UNPAIRED">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments>
</tag>
<tag name="default" type="UNPAIRED">
<arguments>
<argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" />
</arguments>
</tag>
<tag name="define" type="PAIR" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" required="true" />
<argument name="variable" types="VARIABLE_DEFINITION_ITEM" repeatable="true" />
</arguments>
</tag>
<tag name="do" type="UNPAIRED">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments>
</tag>
<tag name="dump" type="UNPAIRED">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments>
</tag>
<tag name="else" type="UNPAIRED_ATTR" />
<tag name="elseif" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="elseifset" type="UNPAIRED">
<arguments>
<argument name="var" types="VARIABLE,BLOCK" validType="string" required="true" />
</arguments>
</tag>
<tag name="extends" type="UNPAIRED">
<arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" />
</arguments>
</tag>
<tag name="first" type="PAIR">
<arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" />
</arguments>
</tag>
<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="if" type="PAIR">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="ifset" type="PAIR">
<arguments>
<argument name="var" types="VARIABLE,BLOCK,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="import" type="UNPAIRED">
<arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="include" type="UNPAIRED" allowedFilters="true">
<arguments>
<argument name="file" types="BLOCK,IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="KEY_VALUE" repeatable="true" />
</arguments>
</tag>
<tag name="includeblock" type="UNPAIRED">
<arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="l" type="UNPAIRED" />
<tag name="last" type="PAIR">
<arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" required="true" />
</arguments>
</tag>
<tag name="layout" type="UNPAIRED">
<arguments>
<argument name="file" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION,NONE" validType="string" required="true" />
</arguments>
</tag>
<tag name="class" type="ATTR_ONLY" arguments="class" />
<tag name="attr" type="ATTR_ONLY" arguments="attr" />
<tag name="ifcontent" type="ATTR_ONLY" />
<tag name="php" type="UNPAIRED">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" />
</arguments>
</tag>
<tag name="r" type="UNPAIRED" />
<tag name="sandbox" type="UNPAIRED">
<arguments>
<argument name="file" types="BLOCK,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
<argument name="key-value" types="KEY_VALUE" repeatable="true" />
</arguments>
</tag>
<tag name="sep" type="PAIR">
<arguments>
<argument name="width" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="int" />
</arguments>
</tag>
<tag name="snippet" type="PAIR" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" />
</arguments>
</tag>
<tag name="snippetArea" type="PAIR" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="spaceless" type="PAIR" />
<tag name="switch" type="PAIR" multiLine="true">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" />
</arguments>
</tag>
<tag name="syntax" type="PAIR" arguments="off | double | latte" multiLine="true" />
<tag name="templatePrint" type="UNPAIRED">
<arguments>
<argument name="class-name" types="PHP_CLASS_NAME" />
</arguments>
</tag>
<tag name="templateType" type="UNPAIRED">
<arguments>
<argument name="class-name" types="PHP_CLASS_NAME" required="true" />
</arguments>
</tag>
<tag name="try" type="PAIR" />
<tag name="rollback" type="UNPAIRED" />
<tag name="tag" type="ATTR_ONLY">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" validType="string" repeatable="true" />
</arguments>
</tag>
<tag name="ifchanged" type="PAIR">
<arguments>
<argument name="expression" types="PHP_EXPRESSION" required="true" repeatable="true" />
</arguments>
</tag>
<tag name="skipIf" type="UNPAIRED">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="var" type="UNPAIRED">
<arguments>
<argument name="variable" types="VARIABLE_DEFINITION_EXPRESSION" required="true" repeatable="true" />
</arguments>
</tag>
<tag name="trace" type="UNPAIRED" />
<tag name="varPrint" type="UNPAIRED" arguments="all" />
<tag name="varType" type="UNPAIRED">
<arguments>
<argument name="file" types="PHP_TYPE" required="true" />
<argument name="variable" types="VARIABLE_DEFINITION" required="true" />
</arguments>
</tag>
<tag name="while" type="PAIR" multiLine="true">
<arguments>
<argument name="condition" types="PHP_CONDITION" validType="bool" required="true" />
</arguments>
</tag>
<tag name="iterateWhile" type="PAIR" multiLine="true" />
<tag name="embed" type="PAIR" multiLine="true">
<arguments>
<argument name="file" types="BLOCK_USAGE,PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
<argument name="key-value" types="KEY_VALUE" repeatable="true" />
</arguments>
</tag>
<!-- @deprecated - latte -->
<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." />
</tags>
<filters>
<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="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="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="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="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="repeat" arguments=":($count)" description="repeats the string" insertColons=":" />
<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="breaklines" description="inserts HTML line breaks before all newlines" />
<filter name="reverse" description="reverse an UTF-8 string or array" />
<filter name="length" description="returns length of a string or array" />
<filter name="sort" description="simply sorts array" />
<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="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="upper" description="makes a string 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="date" arguments=":($format)" description="formats date" insertColons=":" />
<filter name="number" arguments=":($decimals = 0, $decPoint = '.', $thousandsSep = ',')" description="format number" />
<filter name="bytes" arguments=":($precision = 2)" description="formats size in bytes" />
<filter name="dataStream" arguments=":($mimetype = 'detect')" description="Data URI protocol conversion" />
<filter name="noescape" description="prints a variable without escaping" />
<filter name="escapeurl" description="escapes parameter in URL" />
<filter name="nocheck" description="prevents automatic URL sanitization" />
<filter name="checkurl" description="sanitizes string for use inside href attribute" />
<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="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="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="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="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="spaceless" description="removes whitespace" />
<filter name="split" arguments=":(string $separator = '')" description="splits a string by the given delimiter" />
</filters>
<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="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="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="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" />
</functions>
</latte>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE latte PUBLIC "-//LATTE//Latte plugin configuration XML V0.0.1//EN" "Latte.dtd">
<latte version="1" vendor="nette/application">
<tags>
<!-- nette/application tags -->
<tag name="cache" type="PAIR" arguments="if => expr, key, …">
<arguments>
<argument name="name[:part]" types="KEY_VALUE" validType="string" required="true" />
<argument name="arguments" types="PHP_EXPRESSION" repeatable="true" />
</arguments>
</tag>
<tag name="control" type="UNPAIRED">
<arguments>
<argument name="name[:part]" types="PHP_IDENTIFIER,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="PHP_EXPRESSION" repeatable="true" />
</arguments>
</tag>
<tag name="link" type="UNPAIRED">
<arguments>
<argument name="destination" types="LINK_DESTINATION,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="LINK_PARAMETERS" repeatable="true" />
</arguments>
</tag>
<tag name="href" type="ATTR_ONLY">
<arguments>
<argument name="destination" types="LINK_DESTINATION,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="LINK_PARAMETERS" repeatable="true" />
</arguments>
</tag>
<tag name="nonce" type="ATTR_ONLY" />
<tag name="plink" type="UNPAIRED">
<arguments>
<argument name="destination" types="LINK_DESTINATION,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="LINK_PARAMETERS" repeatable="true" />
</arguments>
</tag>
<!-- @deprecated - nette/application -->
<tag name="ifCurrent" type="PAIR" deprecatedMessage="Tag {ifCurrent} is deprecated in Latte 2.6. Use custom function isLinkCurrent() instead.">
<arguments>
<argument name="destination" types="LINK_DESTINATION,PHP_EXPRESSION" validType="string" required="true" />
<argument name="arguments" types="LINK_PARAMETERS" repeatable="true" />
</arguments>
</tag>
</tags>
<variables>
<variable name="control" type="\Nette\Application\UI\Control" />
<variable name="basePath" type="string" />
<variable name="baseUrl" type="string" />
<variable name="flashes" type="mixed[]" />
<variable name="presenter" type="\Nette\Application\UI\Presenter" />
<variable name="iterator" type="\Latte\Runtime\CachingIterator" />
<variable name="form" type="\Nette\Application\UI\Form" />
<variable name="user" type="\Nette\Security\User" />
</variables>
<functions>
<function name="isLinkCurrent" returnType="bool" arguments="(string $destination = null, $args = [])" />
<function name="isModuleCurrent" returnType="bool" arguments="(string $moduleName)" />
</functions>
</latte>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE latte PUBLIC "-//LATTE//Latte plugin configuration XML V0.0.1//EN" "Latte.dtd">
<latte version="1" vendor="nette/forms">
<tags>
<tag name="form" type="PAIR" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="formContainer" type="PAIR" multiLine="true">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="formPrint" type="UNPAIRED">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="input" type="UNPAIRED">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,CONTROL,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="inputError" type="UNPAIRED">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,CONTROL,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="label" type="AUTO_EMPTY">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,CONTROL,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
<tag name="name" type="ATTR_ONLY">
<arguments>
<argument name="name" types="PHP_IDENTIFIER,VARIABLE,CONTROL,PHP_EXPRESSION" validType="string" required="true" />
</arguments>
</tag>
</tags>
</latte>

6
.idea/misc.xml Normal file
View file

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

8
.idea/modules.xml Normal file
View file

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

46
.idea/openvk.iml Normal file
View file

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

48
.idea/php.xml Normal file
View file

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

View file

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

6
.idea/vcs.xml Normal file
View file

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

View file

@ -0,0 +1,86 @@
<?php declare(strict_types=1);
namespace openvk\CLI;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Photos;
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;
class RebuildImagesCommand extends Command
{
private $images;
protected static $defaultName = "build-images";
function __construct()
{
$this->images = DatabaseConnection::i()->getContext()->table("photos");
parent::__construct();
}
protected function configure(): void
{
$this->setDescription("Create resized versions of images")
->setHelp("This command allows you to resize all your images after configuration change")
->addOption("upgrade-only", "U", InputOption::VALUE_NEGATABLE, "Only upgrade images which aren't resized?");
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$header = $output->section();
$counter = $output->section();
$header->writeln([
"Image Rebuild Utility",
"=====================",
"",
]);
$filter = ["deleted" => false];
if($input->getOption("upgrade-only"))
$filter["sizes"] = NULL;
$selection = $this->images->select("id")->where($filter);
$totalPics = $selection->count();
$header->writeln([
"Total of $totalPics images found.",
"",
]);
$errors = 0;
$count = 0;
$avgTime = NULL;
$begin = new \DateTimeImmutable("now");
foreach($selection as $idHolder) {
$start = microtime(true);
try {
$photo = (new Photos)->get($idHolder->id);
$photo->getSizes(true, true);
$photo->getDimensions();
} catch(ImageException $ex) {
$errors++;
}
$timeConsumed = microtime(true) - $start;
if(!$avgTime)
$avgTime = $timeConsumed;
else
$avgTime = ($avgTime + $timeConsumed) / 2;
$eta = $begin->getTimestamp() + ceil($totalPics * $avgTime);
$int = (new \DateTimeImmutable("now"))->diff(new \DateTimeImmutable("@$eta"));
$int = $int->d . "d" . $int->h . "h" . $int->i . "m" . $int->s . "s";
$pct = floor(100 * ($count / $totalPics));
$counter->overwrite("Processed " . ++$count . " images... ($pct% $int left $errors/$count fail)");
}
$counter->overwrite("Processing finished :3");
return Command::SUCCESS;
}
}

View file

@ -6,25 +6,27 @@ _[Русский](README_RU.md)_
VKontakte belongs to Pavel Durov and VK Group. VKontakte belongs to Pavel Durov and VK Group.
To be honest, we don't even know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this). To be honest, we don't know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this).
## When's the release? ## When's the release?
Please use the master branch, as it has the most changes. 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)
Updating the source code is done with this command: `git pull` * 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.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS (<https://t.me/openvkch/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/) * [social.fetbuk.ru](http://social.fetbuk.ru/)
* [openvk.zavsc.pw](https://openvk.zavsc.pw/) * [vepurovk.xyz](http://vepurovk.xyz/)
## Can I create my own OpenVK instance? ## Can I create my own OpenVK instance?
Yes! And you're very welcome to. Yes! And you're very welcome to.
However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. this extensions are available on most of ISPManager hostings). However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings).
If you want, you can add your instance to the list above so that people can register there. If you want, you can add your instance to the list above so that people can register there.
@ -32,42 +34,47 @@ 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. * PHP 8 has **not** yet been tested, so you should not expect it to work. (edit: it does not work).
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 **same database** you installed Chandler to and import all sqls from `install/sqls` to **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 **separate database** 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 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. Move to `Web/static/js` and execute `yarn install` 9. Run `composer install` in commitcaptcha directory
9. Set `openvk` as your root app in `chandler.yml` 10. Move to `Web/static/js` and execute `yarn install`
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):
* **Login**: `admin@localhost.localdomain6` * **Login**: `admin@localhost.localdomain6`
* **Password**: `admin` * **Password**: `admin`
* It is recommended to change the password before using the built-in account. * It is recommended to change the password of the built-in account or disable it.
Full example installation instruction for CentOS 8 is also available [here](https://docs.openvk.su/openvk_engine/centos8_installation/). 💡Confused? Full installation walkthrough is available [here](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)).
### If my website uses OpenVK, should I publish it's sources? ### If my website uses OpenVK, should I release it's sources?
You are encouraged to do so. We don't enforce this though. You can keep your sources to yourself (unless you distribute your OpenVK distro to other people). It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc).
You also not required to publish source texts of your themepacks and plugins.
## Where can I get assistance? ## Where can I get assistance?
@ -80,7 +87,7 @@ You may reach out to us via:
* [Discussions](https://github.com/openvk/openvk/discussions) * [Discussions](https://github.com/openvk/openvk/discussions)
* Matrix chat: #openvk:matrix.org * Matrix chat: #openvk:matrix.org
**Attention**: bug tracker, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com** **Attention**: bug tracker, board, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com**
<a href="https://codeberg.org/OpenVK/openvk"> <a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on 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">

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).
## Когда релиз? ## Когда выйдет релизная версия?
Пожалуйста, используйте ветку master, так как в ней больше всего изменений. Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете:
* Склонировать master ветку репозитория командой `git clone` (используйте `git pull` для обновления)
Обновление исходного кода выполняется с помощью этой команды: `git pull`. * Взять готовую сборку 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.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS (<https://t.me/openvkch/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/) * [social.fetbuk.ru](http://social.fetbuk.ru/)
* [openvk.zavsc.pw](https://openvk.zavsc.pw/) * [vepurovk.xyz](http://vepurovk.xyz/)
## Могу ли я создать свою собственную инстанцию OpenVK? ## Могу ли я создать свою собственную инстанцию OpenVK?
@ -32,42 +34,47 @@ _[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` в **отдельную базу данных** 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. Перейдите в `Web/static/js` и выполните `yarn install` 9. Запустите `composer install` в директории commitcaptcha
9. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` 10. Перейдите в `Web/static/js` и выполните `yarn install`
11. Установите `openvk` в качестве корневого приложения в файле `chandler.yml`
После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется):
* **Логин**: `admin@localhost.localdomain6` * **Логин**: `admin@localhost.localdomain6`
* **Пароль**: `admin` * **Пароль**: `admin`
* Перед использованием встроенной учетной записи рекомендуется сменить пароль. * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её.
Полный пример инструкции по установке CentOS 8 также доступен [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/). 💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)).
### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты?
Вам рекомендуется это делать. Однако мы не следим за этим. Вы можете держать свои исходные тексты при себе (если только вы не распространяете свой дистрибутив OpenVK среди других людей). Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.).
Вы также не обязаны публиковать исходные тексты ваших тематических пакетов и плагинов.
## Где я могу получить помощь? ## Где я могу получить помощь?
@ -80,7 +87,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Обсуждения](https://github.com/openvk/openvk/discussions) * [Обсуждения](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org * Чат в Matrix: #ovk:matrix.org
**Внимание**: баг-трекер, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [at] tutanota [dot] com**. **Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
<a href="https://codeberg.org/OpenVK/openvk"> <a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on 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">

View file

@ -60,7 +60,7 @@ final class Account extends VKAPIRequestHandler
return 1; return 1;
} }
function getAppPermissions(): object function getAppPermissions(): int
{ {
return 9355263; return 9355263;
} }

View file

@ -25,7 +25,7 @@ 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, true); // FIXME $response = $usersApi->get(implode(',', $friends), $fields, 0, $count); // FIXME
} }
return (object) [ return (object) [

View file

@ -88,4 +88,100 @@ final class Groups extends VKAPIRequestHandler
"items" => $rClubs "items" => $rClubs
]; ];
} }
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array
{
$this->requireUser();
$clubs = new ClubsRepo;
if ($group_ids == null && $group_id != null)
$group_ids = $group_id;
if ($group_ids == null && $group_id == null)
$this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
$clbs = explode(',', $group_ids);
$response;
$ic = sizeof($clbs);
for ($i=0; $i < $ic; $i++) {
if($i > 500)
break;
if($clbs[$i] < 0)
$this->fail(100, "ты ошибся чутка, у айди группы убери минус");
$clb = $clubs->get((int) $clbs[$i]);
if(is_null($clb))
{
$response[$i] = (object)[
"id" => intval($clbs[$i]),
"name" => "DELETED",
"screen_name" => "club".intval($clbs[$i]),
"type" => "group",
"description" => "This group was deleted or it doesn't exist"
];
}else if($clbs[$i] == null){
}else{
$response[$i] = (object)[
"id" => $clb->getId(),
"name" => $clb->getName(),
"screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
"is_closed" => false,
"type" => "group",
"can_access_closed" => true,
];
$flds = explode(',', $fields);
foreach($flds as $field) {
switch ($field) {
case 'verified':
$response[$i]->verified = intval($clb->isVerified());
break;
case 'has_photo':
$response[$i]->has_photo = is_null($clb->getAvatarPhoto()) ? 0 : 1;
break;
case 'photo_max_orig':
$response[$i]->photo_max_orig = $clb->getAvatarURL();
break;
case 'photo_max':
$response[$i]->photo_max = $clb->getAvatarURL();
break;
case 'members_count':
$response[$i]->members_count = $clb->getFollowersCount();
break;
case 'site':
$response[$i]->site = $clb->getWebsite();
break;
case 'description':
$response[$i]->desctiption = $clb->getDescription();
break;
case 'contacts':
$contacts;
$contactTmp = $clb->getManagers(1, true);
foreach($contactTmp as $contact) {
$contacts[] = array(
'user_id' => $contact->getUser()->getId(),
'desc' => $contact->getComment()
);
}
$response[$i]->contacts = $contacts;
break;
case 'can_post':
if($clb->canBeModifiedBy($this->getUser()))
$response[$i]->can_post = true;
else
$response[$i]->can_post = $clb->canPost();
break;
}
}
}
}
return $response;
}
} }

74
VKAPI/Handlers/Likes.php Normal file
View file

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Posts as PostsRepo;
final class Likes extends VKAPIRequestHandler
{
function add(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
switch ($type) {
case 'post':
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
$post->setLike(true, $this->getUser());
return (object)[
"likes" => $post->getLikesCount()
];
break;
default:
$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
{
$this->requireUser();
switch ($type) {
case 'post':
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
$post->setLike(false, $this->getUser());
return (object)[
"likes" => $post->getLikesCount()
];
break;
default:
$this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
break;
}
}
function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
switch ($type) {
case 'post':
$user = (new UsersRepo)->get($user_id);
if (is_null($user)) return (object)[
"liked" => 0,
"copied" => 0,
"sex" => 0
];
$post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
return (object)[
"liked" => (int) $post->hasLikeFrom($user),
"copied" => 0 // TODO: handle this
];
break;
default:
$this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
break;
}
}
}

View file

@ -4,6 +4,7 @@ use openvk\Web\Events\NewMessageEvent;
use openvk\Web\Models\Entities\{Correspondence, Message}; use openvk\Web\Models\Entities\{Correspondence, Message};
use openvk\Web\Models\Repositories\{Messages as MSGRepo, Users as USRRepo}; use openvk\Web\Models\Repositories\{Messages as MSGRepo, Users as USRRepo};
use openvk\VKAPI\Structures\{Message as APIMsg, Conversation as APIConvo}; use openvk\VKAPI\Structures\{Message as APIMsg, Conversation as APIConvo};
use openvk\VKAPI\Handlers\Users as APIUsers;
use Chandler\Signaling\SignalManager; use Chandler\Signaling\SignalManager;
final class Messages extends VKAPIRequestHandler final class Messages extends VKAPIRequestHandler
@ -48,10 +49,12 @@ final class Messages extends VKAPIRequestHandler
$rMsg->read_state = 1; $rMsg->read_state = 1;
$rMsg->out = (int) ($message->getSender()->getId() === $this->getUser()->getId()); $rMsg->out = (int) ($message->getSender()->getId() === $this->getUser()->getId());
$rMsg->body = $message->getText(false); $rMsg->body = $message->getText(false);
$rMsg->text = $message->getText(false);
$rMsg->emoji = true; $rMsg->emoji = true;
if($preview_length > 0) if($preview_length > 0)
$rMsg->body = ovk_proc_strtr($rMsg->body, $preview_length); $rMsg->body = ovk_proc_strtr($rMsg->body, $preview_length);
$rMsg->text = ovk_proc_strtr($rMsg->text, $preview_length);
$items[] = $rMsg; $items[] = $rMsg;
} }
@ -145,12 +148,14 @@ final class Messages extends VKAPIRequestHandler
return 1; return 1;
} }
function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0): object function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0, string $fields = ""): object
{ {
$this->requireUser(); $this->requireUser();
$convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset); $convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset);
$list = []; $list = [];
$users = [];
foreach($convos as $convo) { foreach($convos as $convo) {
$correspondents = $convo->getCorrespondents(); $correspondents = $convo->getCorrespondents();
if($correspondents[0]->getId() === $this->getUser()->getId()) if($correspondents[0]->getId() === $this->getUser()->getId())
@ -189,7 +194,13 @@ final class Messages extends VKAPIRequestHandler
$lastMessagePreview->read_state = 1; $lastMessagePreview->read_state = 1;
$lastMessagePreview->out = (int) ($lastMessage->getSender()->getId() === $this->getUser()->getId()); $lastMessagePreview->out = (int) ($lastMessage->getSender()->getId() === $this->getUser()->getId());
$lastMessagePreview->body = $lastMessage->getText(false); $lastMessagePreview->body = $lastMessage->getText(false);
$lastMessagePreview->text = $lastMessage->getText(false);
$lastMessagePreview->emoji = true; $lastMessagePreview->emoji = true;
if($extended == 1) {
$users[] = $lastMessage->getSender()->getId();
$users[] = $author;
}
} }
$list[] = [ $list[] = [
@ -198,10 +209,20 @@ final class Messages extends VKAPIRequestHandler
]; ];
} }
return (object) [ if($extended == 0){
"count" => sizeof($list), return (object) [
"items" => $list, "count" => sizeof($list),
]; "items" => $list,
];
} else {
$users = array_unique($users);
return (object) [
"count" => sizeof($list),
"items" => $list,
"profiles" => (new APIUsers)->get(implode(',', $users), $fields, $offset, $count)
];
}
} }
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): object
@ -230,6 +251,7 @@ final class Messages extends VKAPIRequestHandler
$rMsg->read_state = 1; $rMsg->read_state = 1;
$rMsg->out = (int) ($msgU->sender_id === $this->getUser()->getId()); $rMsg->out = (int) ($msgU->sender_id === $this->getUser()->getId());
$rMsg->body = $message->getText(false); $rMsg->body = $message->getText(false);
$rMsg->text = $message->getText(false);
$rMsg->emoji = true; $rMsg->emoji = true;
$results[] = $rMsg; $results[] = $rMsg;

View file

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
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 openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\VKAPI\Handlers\Wall;
final class Newsfeed extends VKAPIRequestHandler
{
function get(string $fields = "", int $start_from = 0, int $offset = 0, int $count = 30, int $extended = 0)
{
$this->requireUser();
if($offset != 0) $start_from = $offset;
$id = $this->getUser()->getId();
$subs = DatabaseConnection::i()
->getContext()
->table("subscriptions")
->where("follower", $id);
$ids = array_map(function($rel) {
return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1);
}, iterator_to_array($subs));
$ids[] = $this->getUser()->getId();
$posts = DatabaseConnection::i()
->getContext()
->table("posts")
->select("id")
->where("wall IN (?)", $ids)
->where("deleted", 0)
->order("created DESC");
$rposts = [];
foreach($posts->page((int) ($offset + 1), $count) as $post)
$rposts[] = (new PostsRepo)->get($post->id)->getPrettyId();
return (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser());
}
}

230
VKAPI/Handlers/Photos.php Normal file
View file

@ -0,0 +1,230 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use Nette\InvalidStateException;
use Nette\Utils\ImageException;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Clubs;
final class Photos extends VKAPIRequestHandler
{
private function getPhotoUploadUrl(string $field, int $group = 0, bool $multifile = false): string
{
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
$uploadInfo = [
1,
$field,
(int) $multifile,
0,
time(),
$this->getUser()->getId(),
$group,
0, # this is unused but stays here base64 reasons (X2 doesn't work, so there's dummy value for short)
];
$uploadInfo = pack("vZ10v2P3S", ...$uploadInfo);
$uploadInfo = base64_encode($uploadInfo);
$uploadHash = hash_hmac("sha3-224", $uploadInfo, $secret);
$uploadInfo = rawurlencode($uploadInfo);
return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/upload/photo/$uploadHash?$uploadInfo";
}
private function getImagePath(string $photo, string $hash, ?string& $up = NULL, ?string& $group = NULL): string
{
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
if(!hash_equals(hash_hmac("sha3-224", $photo, $secret), $hash))
$this->fail(121, "Incorrect hash");
[$up, $image, $group] = explode("|", $photo);
$imagePath = __DIR__ . "/../../tmp/api-storage/photos/$up" . "_$image.oct";
if(!file_exists($imagePath))
$this->fail(10, "Invalid image");
return $imagePath;
}
function getOwnerPhotoUploadServer(int $owner_id = 0): object
{
$this->requireUser();
if($owner_id < 0) {
$club = (new Clubs)->get(abs($owner_id));
if(!$club)
$this->fail(0404, "Club not found");
else if(!$club->canBeModifiedBy($this->getUser()))
$this->fail(200, "Access: Club can't be 'written' by user");
}
return (object) [
"upload_url" => $this->getPhotoUploadUrl("photo", isset($club) ? 0 : $club->getId()),
];
}
function saveOwnerPhoto(string $photo, string $hash): object
{
$imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
if($group == 0) {
$user = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
$album = (new Albums)->getUserAvatarAlbum($user);
} else {
$club = (new Clubs)->get((int) $group);
$album = (new Albums)->getClubAvatarAlbum($club);
}
try {
$avatar = new Photo;
$avatar->setOwner((int) $uploader);
$avatar->setDescription("Profile photo");
$avatar->setCreated(time());
$avatar->setFile([
"tmp_name" => $imagePath,
"error" => 0,
]);
$avatar->save();
$album->addPhoto($avatar);
unlink($imagePath);
} catch(ImageException | InvalidStateException $e) {
unlink($imagePath);
$this->fail(129, "Invalid image file");
}
return (object) [
"photo_hash" => NULL,
"photo_src" => $avatar->getURL(),
];
}
function getWallUploadServer(?int $group_id = NULL): object
{
$this->requireUser();
$album = NULL;
if(!is_null($group_id)) {
$club = (new Clubs)->get(abs($group_id));
if(!$club)
$this->fail(0404, "Club not found");
else if(!$club->canBeModifiedBy($this->getUser()))
$this->fail(200, "Access: Club can't be 'written' by user");
} else {
$album = (new Albums)->getUserWallAlbum($this->getUser());
}
return (object) [
"upload_url" => $this->getPhotoUploadUrl("photo", $group_id ?? 0),
"album_id" => $album,
"user_id" => $this->getUser()->getId(),
];
}
function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = NULL): array
{
$imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
if($group_id != $group)
$this->fail(8, "group_id doesn't match");
$album = NULL;
if($group_id != 0) {
$uploader = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
$album = (new Albums)->getUserWallAlbum($uploader);
}
try {
$photo = new Photo;
$photo->setOwner((int) $uploader);
$photo->setCreated(time());
$photo->setFile([
"tmp_name" => $imagePath,
"error" => 0,
]);
if (!is_null($caption))
$photo->setDescription($caption);
$photo->save();
unlink($imagePath);
} catch(ImageException | InvalidStateException $e) {
unlink($imagePath);
$this->fail(129, "Invalid image file");
}
if(!is_null($album))
$album->addPhoto($photo);
return [
$photo->toVkApiStruct(),
];
}
function getUploadServer(?int $album_id = NULL): object
{
$this->requireUser();
# Not checking rights to album because save() method will do so anyways
return (object) [
"upload_url" => $this->getPhotoUploadUrl("photo", 0, true),
"album_id" => $album_id,
"user_id" => $this->getUser()->getId(),
];
}
function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = NULL): object
{
$this->requireUser();
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
if(!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash))
$this->fail(121, "Incorrect hash");
$album = NULL;
if($album_id != 0) {
$album_ = (new Albums)->get($album_id);
if(!$album_)
$this->fail(0404, "Invalid album");
else if(!$album_->canBeModifiedBy($this->getUser()))
$this->fail(15, "Access: Album can't be 'written' by user");
$album = $album_;
}
$pList = json_decode($photos_list);
$imagePaths = [];
foreach($pList as $pDesc)
$imagePaths[] = __DIR__ . "/../../tmp/api-storage/photos/$pDesc->keyholder" . "_$pDesc->resource.oct";
$images = [];
try {
foreach($imagePaths as $imagePath) {
$photo = new Photo;
$photo->setOwner($this->getUser()->getId());
$photo->setCreated(time());
$photo->setFile([
"tmp_name" => $imagePath,
"error" => 0,
]);
if (!is_null($caption))
$photo->setDescription($caption);
$photo->save();
unlink($imagePath);
if(!is_null($album))
$album->addPhoto($photo);
$images[] = $photo->toVkApiStruct();
}
} catch(ImageException | InvalidStateException $e) {
foreach($imagePaths as $imagePath)
unlink($imagePath);
$this->fail(129, "Invalid image file");
}
return (object) [
"count" => sizeof($images),
"items" => $images,
];
}
}

View file

@ -5,13 +5,15 @@ use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Users extends VKAPIRequestHandler final class Users extends VKAPIRequestHandler
{ {
function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100): array function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100, User $authuser = null /* костыль(( */): array
{ {
$this->requireUser(); // $this->requireUser();
if($authuser == null) $authuser = $this->getUser();
$users = new UsersRepo; $users = new UsersRepo;
if($user_ids == "0") if($user_ids == "0")
$user_ids = (string) $this->getUser()->getId(); $user_ids = (string) $authuser->getId();
$usrs = explode(',', $user_ids); $usrs = explode(',', $user_ids);
$response; $response;
@ -51,7 +53,7 @@ final class Users extends VKAPIRequestHandler
$response[$i]->verified = intval($usr->isVerified()); $response[$i]->verified = intval($usr->isVerified());
break; break;
case 'sex': case 'sex':
$response[$i]->sex = $this->getUser()->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;
@ -60,8 +62,26 @@ final class Users extends VKAPIRequestHandler
$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 'photo_50':
$response[$i]->photo_50 = $usr->getAvatarURL();
break;
case 'photo_100':
$response[$i]->photo_50 = $usr->getAvatarURL("tiny");
break;
case 'photo_200':
$response[$i]->photo_50 = $usr->getAvatarURL("normal");
break;
case 'photo_200_orig': // вообще не ебу к чему эта строка ну пусть будет кек
$response[$i]->photo_50 = $usr->getAvatarURL("normal");
break;
case 'photo_400_orig':
$response[$i]->photo_50 = $usr->getAvatarURL("normal");
break;
// Она хочет быть выебанной видя матан
// Покайфу когда ты Виет а вокруг лишь дискриминант
case 'status': case 'status':
if($usr->getStatus() != null) if($usr->getStatus() != null)
$response[$i]->status = $usr->getStatus(); $response[$i]->status = $usr->getStatus();
@ -71,10 +91,10 @@ final class Users extends VKAPIRequestHandler
$response[$i]->screen_name = $usr->getShortCode(); $response[$i]->screen_name = $usr->getShortCode();
break; break;
case 'friend_status': case 'friend_status':
switch($usr->getSubscriptionStatus($this->getUser())) { switch($usr->getSubscriptionStatus($authuser)) {
case 3: case 3:
case 0: case 0:
$response[$i]->friend_status = $usr->getSubscriptionStatus($this->getUser()); $response[$i]->friend_status = $usr->getSubscriptionStatus($authuser);
break; break;
case 1: case 1:
$response[$i]->friend_status = 2; $response[$i]->friend_status = 2;
@ -158,13 +178,14 @@ final class Users extends VKAPIRequestHandler
$users = new UsersRepo; $users = new UsersRepo;
$array = []; $array = [];
$find = $users->find($q);
foreach ($users->find($q) as $user) { foreach ($find as $user) {
$array[] = $user->getId(); $array[] = $user->getId();
} }
return (object)[ return (object)[
"count" => $users->getFoundCount($q), "count" => $find->size(),
"items" => $this->get(implode(',', $array), $fields, $offset, $count) "items" => $this->get(implode(',', $array), $fields, $offset, $count)
]; ];
} }

View file

@ -22,6 +22,27 @@ final class Wall extends VKAPIRequestHandler
foreach ($posts->getPostsFromUsersWall((int)$owner_id, 1, $count, $offset) as $post) { foreach ($posts->getPostsFromUsersWall((int)$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;
foreach($post->getChildren() as $attachment)
{
if($attachment instanceof \openvk\Web\Models\Entities\Photo)
{
$attachments[] = [
"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
]
];
}
}
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $from_id, "from_id" => $from_id,
@ -35,6 +56,7 @@ final class Wall extends VKAPIRequestHandler
"can_archive" => false, // TODO MAYBE "can_archive" => false, // TODO MAYBE
"is_archived" => false, "is_archived" => false,
"is_pinned" => $post->isPinned(), "is_pinned" => $post->isPinned(),
"attachments" => $attachments,
"post_source" => (object)["type" => "vk"], "post_source" => (object)["type" => "vk"],
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
@ -56,6 +78,8 @@ final class Wall extends VKAPIRequestHandler
$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
} }
if($extended == 1) if($extended == 1)
@ -110,6 +134,169 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
function getById(string $posts, int $extended = 0, string $fields = "", User $user = null)
{
if($user == null) $user = $this->getUser(); // костыли костыли крылышки
$items = [];
$profiles = [];
$groups = [];
# $count = $posts->getPostCountOnUserWall((int) $owner_id);
$psts = explode(",", $posts);
foreach($psts as $pst)
{
$id = explode("_", $pst);
$post = (new PostsRepo)->getPostById(intval($id[0]), intval($id[1]));
if($post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
$attachments;
foreach($post->getChildren() as $attachment)
{
if($attachment instanceof \openvk\Web\Models\Entities\Photo)
{
$attachments[] = [
"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(
[
"height" => 2560,
"url" => $attachment->getURLBySizeId("normal"),
"type" => "m",
"width" => 2560,
],
[
"height" => 130,
"url" => $attachment->getURLBySizeId("tiny"),
"type" => "o",
"width" => 130,
],
[
"height" => 604,
"url" => $attachment->getURLBySizeId("normal"),
"type" => "p",
"width" => 604,
],
[
"height" => 807,
"url" => $attachment->getURLBySizeId("large"),
"type" => "q",
"width" => 807,
],
[
"height" => 1280,
"url" => $attachment->getURLBySizeId("larger"),
"type" => "r",
"width" => 1280,
],
[
"height" => 75, // Для временного компросима оставляю статическое число. Если каждый раз обращаться к файлу за количеством пикселов, то наступает пuпuська полная с производительностью, так что пока так
"url" => $attachment->getURLBySizeId("miniscule"),
"type" => "s",
"width" => 75,
]),
"text" => "",
"has_tags" => false
]
];
}
}
$items[] = (object)[
"id" => $post->getVirtualId(),
"from_id" => $from_id,
"owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post",
"text" => $post->getText(),
"can_edit" => 0, // TODO
"can_delete" => $post->canBeDeletedBy($user),
"can_pin" => $post->canBePinnedBy($user),
"can_archive" => false, // TODO MAYBE
"is_archived" => false,
"is_pinned" => $post->isPinned(),
"post_source" => (object)["type" => "vk"],
"attachments" => $attachments,
"comments" => (object)[
"count" => $post->getCommentsCount(),
"can_post" => 1
],
"likes" => (object)[
"count" => $post->getLikesCount(),
"user_likes" => (int) $post->hasLikeFrom($user),
"can_like" => 1,
"can_publish" => 1,
],
"reposts" => (object)[
"count" => $post->getRepostCount(),
"user_reposted" => 0
]
];
if ($from_id > 0)
$profiles[] = $from_id;
else
$groups[] = $from_id * -1;
$attachments = null; // free attachments so it will not clone everythingg
}
}
if($extended == 1)
{
$profiles = array_unique($profiles);
$groups = array_unique($groups);
$profilesFormatted = [];
$groupsFormatted = [];
foreach ($profiles as $prof) {
$user = (new UsersRepo)->get($prof);
$profilesFormatted[] = (object)[
"first_name" => $user->getFirstName(),
"id" => $user->getId(),
"last_name" => $user->getLastName(),
"can_access_closed" => false,
"is_closed" => false,
"sex" => $user->isFemale() ? 1 : 2,
"screen_name" => $user->getShortCode(),
"photo_50" => $user->getAvatarUrl(),
"photo_100" => $user->getAvatarUrl(),
"online" => $user->isOnline()
];
}
foreach($groups as $g) {
$group = (new ClubsRepo)->get($g);
$groupsFormatted[] = (object)[
"id" => $group->getId(),
"name" => $group->getName(),
"screen_name" => $group->getShortCode(),
"is_closed" => 0,
"type" => "group",
"photo_50" => $group->getAvatarUrl(),
"photo_100" => $group->getAvatarUrl(),
"photo_200" => $group->getAvatarUrl(),
];
}
return (object)[
"items" => (array)$items,
"profiles" => (array)$profilesFormatted,
"groups" => (array)$groupsFormatted
];
}
else
return (object)[
"items" => (array)$items
];
}
function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object
{ {
$this->requireUser(); $this->requireUser();

View file

@ -11,10 +11,11 @@ final class Message
public $out; public $out;
public $title = ""; public $title = "";
public $body; public $body;
public $text;
public $attachments = []; public $attachments = [];
public $fwd_messages = []; public $fwd_messages = [];
public $emoji; public $emoji;
public $important = 1; public $important = true;
public $deleted = 0; public $deleted = 0;
public $random_id = NULL; public $random_id = NULL;
} }

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

@ -38,12 +38,12 @@ class Club extends RowModel
return iterator_to_array($avPhotos)[0] ?? NULL; return iterator_to_array($avPhotos)[0] ?? NULL;
} }
function getAvatarUrl(): string function getAvatarUrl(string $size = "miniscule"): string
{ {
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
$avPhoto = $this->getAvatarPhoto(); $avPhoto = $this->getAvatarPhoto();
return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURL(); return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size);
} }
function getAvatarLink(): string function getAvatarLink(): string
@ -346,6 +346,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

@ -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

@ -48,6 +48,7 @@ class Note extends Postable
"acronym", "acronym",
"blockquote", "blockquote",
"cite", "cite",
"span",
]); ]);
$config->set("HTML.AllowedAttributes", [ $config->set("HTML.AllowedAttributes", [
"table.summary", "table.summary",
@ -59,6 +60,8 @@ class Note extends Postable
"img.style", "img.style",
"div.style", "div.style",
"div.title", "div.title",
"span.class",
"p.class",
]); ]);
$config->set("CSS.AllowedProperties", [ $config->set("CSS.AllowedProperties", [
"float", "float",
@ -68,6 +71,9 @@ class Note extends Postable
"max-width", "max-width",
"font-weight", "font-weight",
]); ]);
$config->set("Attr.AllowedClasses", [
"underline",
]);
$purifier = new HTMLPurifier($config); $purifier = new HTMLPurifier($config);
return $purifier->purify($this->getRecord()->source); return $purifier->purify($this->getRecord()->source);

View file

@ -1,5 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use MessagePack\MessagePack;
use openvk\Web\Models\Entities\Album;
use openvk\Web\Models\Repositories\Albums;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Nette\Utils\Image; use Nette\Utils\Image;
@ -8,21 +11,83 @@ class Photo extends Media
{ {
protected $tableName = "photos"; protected $tableName = "photos";
protected $fileExtension = "jpeg"; protected $fileExtension = "jpeg";
const ALLOWED_SIDE_MULTIPLIER = 7; const ALLOWED_SIDE_MULTIPLIER = 7;
private function resizeImage(string $filename, string $outputDir, \SimpleXMLElement $size): array
{
$res = [false];
$image = Image::fromFile($filename);
$requiresProportion = ((string) $size["requireProp"]) != "none";
if($requiresProportion) {
$props = explode(":", (string) $size["requireProp"]);
$px = (int) $props[0];
$py = (int) $props[1];
if(($image->getWidth() / $image->getHeight()) > ($px / $py)) {
# For some weird reason using resize with EXACT flag causes system to consume an unholy amount of RAM
$image->crop(0, 0, "100%", (int) ceil(($px * $image->getWidth()) / $py));
$res[0] = true;
}
}
if(isset($size["maxSize"])) {
$maxSize = (int) $size["maxSize"];
$image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT);
} else if(isset($size["maxResolution"])) {
$resolution = explode("x", (string) $size["maxResolution"]);
$image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT);
} else {
throw new \RuntimeException("Malformed size description: " . (string) $size["id"]);
}
$res[1] = $image->getWidth();
$res[2] = $image->getHeight();
if($res[1] <= 300 || $res[2] <= 300)
$image->save("$outputDir/" . (string) $size["id"] . ".gif");
else
$image->save("$outputDir/" . (string) $size["id"] . ".jpeg");
imagedestroy($image->getImageResource());
unset($image);
return $res;
}
private function saveImageResizedCopies(string $filename, string $hash): void
{
$dir = dirname($this->pathFromHash($hash));
$dir = "$dir/$hash" . "_cropped";
if(!is_dir($dir)) {
@unlink($dir); # Added to transparently bypass issues with dead pesudofolders summoned by buggy SWIFT impls (selectel)
mkdir($dir);
}
$sizes = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml");
if(!$sizes)
throw new \RuntimeException("Could not load photosizes.xml!");
$sizesMeta = [];
foreach($sizes->Size as $size)
$sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size);
$sizesMeta = MessagePack::pack($sizesMeta);
$this->stateChanges("sizes", $sizesMeta);
}
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
$image = Image::fromFile($filename); $image = Image::fromFile($filename);
if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER))) if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER)))
throw new ISE("Invalid layout: image is too wide/short"); throw new ISE("Invalid layout: image is too wide/short");
$image->resize(8192, 4320, Image::SHRINK_ONLY | Image::FIT);
$image->save($this->pathFromHash($hash), 92, Image::JPEG); $image->save($this->pathFromHash($hash), 92, Image::JPEG);
$this->saveImageResizedCopies($filename, $hash);
return true; return true;
} }
function crop(real $left, real $top, real $width, real $height): bool function crop(real $left, real $top, real $width, real $height): void
{ {
if(isset($this->changes["hash"])) if(isset($this->changes["hash"]))
$hash = $this->changes["hash"]; $hash = $this->changes["hash"];
@ -33,7 +98,7 @@ class Photo extends Media
$image = Image::fromFile($this->pathFromHash($hash)); $image = Image::fromFile($this->pathFromHash($hash));
$image->crop($left, $top, $width, $height); $image->crop($left, $top, $width, $height);
return $image->save($this->pathFromHash($hash)); $image->save($this->pathFromHash($hash));
} }
function isolate(): void function isolate(): void
@ -43,7 +108,131 @@ class Photo extends Media
DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete(); DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete();
} }
function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array
{
$sizes = $this->getRecord()->sizes;
if(!$sizes || $forceUpdate) {
if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) {
$hash = $this->getRecord()->hash;
$this->saveImageResizedCopies($this->pathFromHash($hash), $hash);
$this->save();
return $this->getSizes();
}
return NULL;
}
$res = [];
$sizes = MessagePack::unpack($sizes);
foreach($sizes as $id => $meta) {
$url = $this->getURL();
$url = str_replace(".$this->fileExtension", "_cropped/$id.", $url);
$url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg";
$res[$id] = (object) [
"url" => $url,
"width" => $meta[1],
"height" => $meta[2],
"crop" => $meta[0]
];
}
[$x, $y] = $this->getDimensions();
$res["UPLOADED_MAXRES"] = (object) [
"url" => $this->getURL(),
"width" => $x,
"height" => $y,
"crop" => false
];
return $res;
}
function getVkApiSizes(): ?array
{
$res = [];
$sizes = $this->getSizes();
if(!$sizes)
return NULL;
$manifest = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml");
if(!$manifest)
return NULL;
$mappings = [];
foreach($manifest->Size as $size)
$mappings[(string) $size["id"]] = (string) $size["vkId"];
foreach($sizes as $id => $meta) {
$type = $mappings[$id] ?? $id;
$meta->type = $type;
$res[$type] = $meta;
}
return $res;
}
function getURLBySizeId(string $size): string
{
$sizes = $this->getSizes();
if(!$sizes)
return $this->getURL();
$size = $sizes[$size];
if(!$size)
return $this->getURL();
return $size->url;
}
function getDimensions(): array
{
$x = $this->getRecord()->width;
$y = $this->getRecord()->height;
if(!$x) { # no sizes in database
$hash = $this->getRecord()->hash;
$image = Image::fromFile($this->pathFromHash($hash));
$x = $image->getWidth();
$y = $image->getHeight();
$this->stateChanges("width", $x);
$this->stateChanges("height", $y);
$this->save();
}
return [$x, $y];
}
function getAlbum(): ?Album
{
return (new Albums)->getAlbumByPhotoId($this);
}
function toVkApiStruct(): object
{
$res = (object) [];
$res->id = $res->pid = $this->getId();
$res->owner_id = $res->user_id = $this->getOwner()->getId()->getId();
$res->aid = $res->album_id = NULL;
$res->width = $this->getDimensions()[0];
$res->height = $this->getDimensions()[1];
$res->date = $res->created = $this->getPublicationTime()->timestamp();
$res->sizes = $this->getVkApiSizes();
$res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule");
$res->src = $res->photo_130 = $this->getURLBySizeId("tiny");
$res->src_big = $res->photo_604 = $this->getURLBySizeId("normal");
$res->src_xbig = $res->photo_807 = $this->getURLBySizeId("large");
$res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger");
$res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original");
$res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES");
return $res;
}
static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo
{ {
$photo = new static; $photo = new static;
@ -53,10 +242,10 @@ class Photo extends Media
$photo->setCreated(time()); $photo->setCreated(time());
$photo->setFile($file); $photo->setFile($file);
$photo->save(); $photo->save();
if(!is_null($album)) if(!is_null($album))
$album->addPhoto($photo); $album->addPhoto($photo);
return $photo; return $photo;
} }
} }

View file

@ -55,16 +55,21 @@ 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);
} }

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
{ {
@ -31,11 +33,11 @@ class User extends RowModel
const NSFW_TOLERANT = 1; const NSFW_TOLERANT = 1;
const NSFW_FULL_TOLERANT = 2; const NSFW_FULL_TOLERANT = 2;
protected function _abstractRelationGenerator(string $filename, int $page = 1): \Traversable protected function _abstractRelationGenerator(string $filename, int $page = 1, int $limit = 6): \Traversable
{ {
$id = $this->getId(); $id = $this->getId();
$query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); $query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql");
$query .= "\n LIMIT 6 OFFSET " . ( ($page - 1) * 6 ); $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit );
$rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id); $rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($rels as $rel) { foreach($rels as $rel) {
@ -102,7 +104,7 @@ class User extends RowModel
return "/id" . $this->getId(); return "/id" . $this->getId();
} }
function getAvatarUrl(): string function getAvatarUrl(string $size = "miniscule"): string
{ {
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
@ -115,7 +117,7 @@ class User extends RowModel
if(is_null($avPhoto)) if(is_null($avPhoto))
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
else else
return $avPhoto->getURL(); return $avPhoto->getURLBySizeId($size);
} }
function getAvatarLink(): string function getAvatarLink(): string
@ -166,153 +168,169 @@ 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->getRecord()->deleted)
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;
} }
function getBanInSupportReason(): ?string
{
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 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;
@ -327,7 +345,7 @@ class User extends RowModel
{ {
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"];
@ -342,7 +360,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, [
@ -359,7 +377,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, [
@ -378,7 +396,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);
@ -386,7 +404,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;
@ -397,12 +415,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;
@ -423,46 +441,46 @@ 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): \Traversable function getFriends(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-friends", $page); return $this->_abstractRelationGenerator("get-friends", $page, $limit);
} }
function getFriendsCount(): int function getFriendsCount(): int
{ {
return $this->_abstractRelationCount("get-friends"); return $this->_abstractRelationCount("get-friends");
} }
function getFollowers(int $page = 1): \Traversable function getFollowers(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-followers", $page); 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): \Traversable function getSubscriptions(int $page = 1, int $limit = 6): \Traversable
{ {
return $this->_abstractRelationGenerator("get-subscriptions-user", $page); 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");
@ -472,7 +490,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) {
@ -497,7 +515,7 @@ class User extends RowModel
} }
} }
} }
function getClubCount(bool $admin = false): int function getClubCount(bool $admin = false): int
{ {
if($admin) { if($admin) {
@ -512,7 +530,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) {
@ -553,16 +571,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);
@ -576,7 +594,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"));
@ -611,9 +629,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([
@ -624,71 +642,76 @@ 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());
} }
function isBannedInSupport(): bool
{
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([
@ -700,7 +723,7 @@ class User extends RowModel
"sent" => time(), "sent" => time(),
]); ]);
} }
function ban(string $reason, bool $deleteSubscriptions = true): void function ban(string $reason, bool $deleteSubscriptions = true): void
{ {
if($deleteSubscriptions) { if($deleteSubscriptions) {
@ -713,42 +736,42 @@ class User extends RowModel
); );
$subs->delete(); $subs->delete();
} }
$this->setBlock_Reason($reason); $this->setBlock_Reason($reason);
$this->save(); $this->save();
} }
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))
@ -757,15 +780,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, [
@ -784,7 +807,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, [
@ -800,10 +823,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)) {
@ -815,20 +838,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")
@ -839,10 +862,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!
@ -850,10 +873,10 @@ 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 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"];
@ -861,12 +884,12 @@ 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;
} }
@ -893,7 +916,7 @@ class User extends RowModel
case 2: case 2:
return 2; return 2;
break; break;
default: default:
return 0; return 0;
break; break;

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,11 +39,13 @@ 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();
Shell::bash(__DIR__ . "/../shell/processVideo.sh", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD $ext = Shell::isPowershell() ? "ps1" : "sh";
$cmd = Shell::isPowershell() ? "powershell" : "bash";
Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD
} catch(ShellUnavailableException $suex) { } catch(ShellUnavailableException $suex) {
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "Shell is unavailable" : VIDEOS_FRIENDLY_ERROR); exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "Shell is unavailable" : VIDEOS_FRIENDLY_ERROR);
} catch(UnknownCommandException $ucex) { } catch(UnknownCommandException $ucex) {
@ -51,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;
@ -81,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

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Album; use openvk\Web\Models\Entities\Album;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
@ -115,4 +116,11 @@ class Albums
return new Album($album); return new Album($album);
} }
function getAlbumByPhotoId(Photo $photo): ?Album
{
$dbalbum = $this->context->table("album_relations")->where(["media" => $photo->getId()])->fetch();
return $dbalbum->collection ? $this->get($dbalbum->collection) : null;
}
} }

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

@ -29,7 +29,7 @@ class Notes
function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL): \Traversable function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->page($page, $perPage) as $album) foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created DESC")->page($page, $perPage) as $album)
yield new Note($album); yield new Note($album);
} }

View file

@ -94,7 +94,6 @@ class Posts
{ {
$post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch();
if(!is_null($post)) if(!is_null($post))
return new Post($post); return new Post($post);
else else
return null; return null;

View file

@ -39,7 +39,7 @@ class Users
function find(string $query): Util\EntityStream function find(string $query): Util\EntityStream
{ {
$query = "%$query%"; $query = "%$query%";
$result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo) LIKE ?", $query)->where("deleted", 0); $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0);
return new Util\EntityStream("User", $result); return new Util\EntityStream("User", $result);
} }

View file

@ -0,0 +1,20 @@
$ovkRoot = $args[0]
$file = $args[1]
$dir = $args[2]
$hash = $args[3]
$hashT = $hash.substring(0, 2)
$temp = [System.IO.Path]::GetTempFileName()
$temp2 = [System.IO.Path]::GetTempFileName()
$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID"
$shell.SetPriority(16384)
Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once
ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
ffmpeg -i $temp -c:v 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"
Remove-Item $temp
Remove-Item $temp2

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

@ -126,4 +126,11 @@ final class AboutPresenter extends OpenVKPresenter
header("Location: https://github.com/openvk/openvk#readme"); header("Location: https://github.com/openvk/openvk#readme");
exit; exit;
} }
function renderDev(): void
{
header("HTTP/1.1 302 Found");
header("Location: https://docs.openvk.su/");
exit;
}
} }

View file

@ -23,7 +23,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)
@ -346,7 +346,20 @@ final class AdminPresenter extends OpenVKPresenter
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"));
exit(json_encode([ "reason" => $this->queryParam("reason") ])); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ]));
}
function renderQuickUnban(int $id): void
{
$this->assertNoCSRF();
$user = $this->users->get($id);
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->setBlock_Reason(null);
$user->save();
exit(json_encode([ "success" => true ]));
} }
function renderQuickWarn(int $id): void function renderQuickWarn(int $id): void

View file

@ -4,6 +4,7 @@ use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\PasswordReset; use openvk\Web\Models\Entities\PasswordReset;
use openvk\Web\Models\Entities\EmailVerification; use openvk\Web\Models\Entities\EmailVerification;
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Models\Repositories\IPs; use openvk\Web\Models\Repositories\IPs;
use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Restores; use openvk\Web\Models\Repositories\Restores;
@ -88,20 +89,25 @@ 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"));
try {
$user = new User;
$user->setFirst_Name($this->postParam("first_name"));
$user->setLast_Name($this->postParam("last_name"));
$user->setSex((int)($this->postParam("sex") === "female"));
$user->setEmail($this->postParam("email"));
$user->setSince(date("Y-m-d H:i:s"));
$user->setRegistering_Ip(CONNECTING_IP);
$user->setBirthday(strtotime($this->postParam("birthday")));
$user->setActivated((int)!OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']);
} catch(InvalidUserNameException $ex) {
$this->flashFail("err", tr("error"), tr("invalid_real_name"));
}
$chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password")); $chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password"));
if(!$chUser) if(!$chUser)
$this->flashFail("err", tr("failed_to_register"), tr("user_already_exists")); $this->flashFail("err", tr("failed_to_register"), tr("user_already_exists"));
$user = new User;
$user->setUser($chUser->getId()); $user->setUser($chUser->getId());
$user->setFirst_Name($this->postParam("first_name"));
$user->setLast_Name($this->postParam("last_name"));
$user->setSex((int) ($this->postParam("sex") === "female"));
$user->setEmail($this->postParam("email"));
$user->setSince(date("Y-m-d H:i:s"));
$user->setRegistering_Ip(CONNECTING_IP);
$user->setBirthday(strtotime($this->postParam("birthday")));
$user->setActivated((int) !OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']);
$user->save(); $user->save();
if(!is_null($referer)) { if(!is_null($referer)) {

View file

@ -17,20 +17,23 @@ final class BlobPresenter extends OpenVKPresenter
function renderFile(/*string*/ $dir, string $name, string $format) function renderFile(/*string*/ $dir, string $name, string $format)
{ {
$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();
if(isset($_SERVER["HTTP_IF_NONE_MATCH"]))
exit(header("HTTP/1.1 304 Not Modified")); exit(header("HTTP/1.1 304 Not Modified"));
header("Content-Type: " . mime_content_type($path)); header("Content-Type: " . mime_content_type($path));
header("Content-Size: " . filesize($path)); header("Content-Size: " . filesize($path));
header("Cache-Control: public, max-age=1210000");
header("X-Accel-Expires: 1210000");
header("ETag: W/\"" . hash_file("snefru", $path) . "\""); header("ETag: W/\"" . hash_file("snefru", $path) . "\"");
readfile($path); readfile($path);
exit; exit;
}
} }
} }

View file

@ -204,6 +204,8 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset"); $this->template->isTimezoned = Session::i()->get("_timezoneOffset");
$userValidated = 0;
$cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0;
if(!is_null($user)) { if(!is_null($user)) {
$this->user = (object) []; $this->user = (object) [];
$this->user->raw = $user; $this->user->raw = $user;
@ -261,6 +263,8 @@ abstract class OpenVKPresenter extends SimplePresenter
exit; exit;
} }
$userValidated = 1;
$cacheTime = 0; # Force no cache
if ($this->user->identity->onlineStatus() == 0) { if ($this->user->identity->onlineStatus() == 0) {
$this->user->identity->setOnline(time()); $this->user->identity->setOnline(time());
$this->user->identity->save(); $this->user->identity->save();
@ -271,6 +275,8 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
} }
header("X-OpenVK-User-Validated: $userValidated");
header("X-Accel-Expires: $cacheTime");
setlocale(LC_TIME, ...(explode(";", tr("__locale")))); setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
parent::onStartup(); parent::onStartup();

View file

@ -72,6 +72,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);
@ -100,6 +102,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());
@ -276,6 +281,8 @@ final class PhotosPresenter extends OpenVKPresenter
$photo->isolate(); $photo->isolate();
$photo->delete(); $photo->delete();
exit("Фотография успешно удалена!");
$this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
$this->redirect("/id0", static::REDIRECT_TEMPORARY);
} }
} }

View file

@ -25,6 +25,10 @@ final class SearchPresenter extends OpenVKPresenter
$type = $this->queryParam("type") ?? "users"; $type = $this->queryParam("type") ?? "users";
$page = (int) ($this->queryParam("p") ?? 1); $page = (int) ($this->queryParam("p") ?? 1);
$this->willExecuteWriteAction();
if($query != "")
$this->assertUserLoggedIn();
// https://youtu.be/pSAWM5YuXx8 // https://youtu.be/pSAWM5YuXx8
$repos = [ "groups" => "clubs", "users" => "users" ]; $repos = [ "groups" => "clubs", "users" => "users" ];

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Ticket; use openvk\Web\Models\Entities\Ticket;
use openvk\Web\Models\Repositories\Tickets; use openvk\Web\Models\Repositories\{Tickets, Users};
use openvk\Web\Models\Entities\TicketComment; use openvk\Web\Models\Entities\TicketComment;
use openvk\Web\Models\Repositories\TicketComments; use openvk\Web\Models\Repositories\TicketComments;
use openvk\Web\Util\Telegram; use openvk\Web\Util\Telegram;
@ -34,7 +34,13 @@ final class SupportPresenter extends OpenVKPresenter
$this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); $this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page);
} }
if($this->template->mode === "new")
$this->template->banReason = $this->user->identity->getBanInSupportReason();
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if($this->user->identity->isBannedInSupport())
$this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
if(!empty($this->postParam("name")) && !empty($this->postParam("text"))) { if(!empty($this->postParam("name")) && !empty($this->postParam("text"))) {
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -268,4 +274,32 @@ final class SupportPresenter extends OpenVKPresenter
exit(header("HTTP/1.1 200 OK")); exit(header("HTTP/1.1 200 OK"));
} }
}
function renderQuickBanInSupport(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$this->assertNoCSRF();
$user = (new Users)->get($id);
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->setBlock_In_Support_Reason($this->queryParam("reason"));
$user->save();
$this->returnJson([ "success" => true, "reason" => $this->queryParam("reason") ]);
}
function renderQuickUnbanInSupport(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$this->assertNoCSRF();
$user = (new Users)->get($id);
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->setBlock_In_Support_Reason(null);
$user->save();
$this->returnJson([ "success" => true ]);
}
}

View file

@ -31,7 +31,7 @@ final class UserPresenter extends OpenVKPresenter
{ {
$user = $this->users->get($id); $user = $this->users->get($id);
if(!$user || $user->isDeleted()) if(!$user || $user->isDeleted())
$this->notFound(); $this->template->_template = "User/deleted.xml";
else { else {
if($user->getShortCode()) if($user->getShortCode())
if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode()) if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode())
@ -285,7 +285,6 @@ final class UserPresenter extends OpenVKPresenter
$photo->setCreated(time()); $photo->setCreated(time());
$photo->save(); $photo->save();
} catch(ISE $ex) { } catch(ISE $ex) {
$name = $album->getName();
$this->flashFail("err", tr("error"), tr("error_upload_failed")); $this->flashFail("err", tr("error"), tr("error_upload_failed"));
} }
@ -482,6 +481,22 @@ 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("/", static::REDIRECT_TEMPORARY_PRESISTENT);
}
function renderCoinsTransfer(): void function renderCoinsTransfer(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -77,6 +77,92 @@ final class VKAPIPresenter extends OpenVKPresenter
exit; # Terminate request processing as this is definitely a CORS preflight request. exit; # Terminate request processing as this is definitely a CORS preflight request.
} }
} }
function renderPhotoUpload(string $signature): void
{
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
$computedSignature = hash_hmac("sha3-224", $_SERVER["QUERY_STRING"], $secret);
if(!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) {
header("HTTP/1.1 422 Unprocessable Entity");
exit("Try harder <3");
}
$data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($_SERVER["QUERY_STRING"]));
if((time() - $data["TIME"]) > 600) {
header("HTTP/1.1 422 Unprocessable Entity");
exit("Expired");
}
$folder = __DIR__ . "../../tmp/api-storage/photos";
$maxSize = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFileSize"];
$maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"];
$usrFiles = sizeof(glob("$folder/$data[USER]_*.oct"));
if($usrFiles >= $maxFiles) {
header("HTTP/1.1 507 Insufficient Storage");
exit("There are $maxFiles pending already. Please save them before uploading more :3");
}
# Not multifile
if($data["MF"] === 0) {
$file = $_FILES[$data["FIELD"]];
if(!$file) {
header("HTTP/1.0 400");
exit("No file");
} else if($file["error"] != UPLOAD_ERR_OK) {
header("HTTP/1.0 500");
exit("File could not be consumed");
} else if($file["size"] > $maxSize) {
header("HTTP/1.0 507 Insufficient Storage");
exit("File is too big");
}
move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_" . ($usrFiles + 1) . ".oct");
header("HTTP/1.0 202 Accepted");
$photo = $data["USER"] . "|" . ($usrFiles + 1) . "|" . $data["GROUP"];
exit(json_encode([
"server" => "ephemeral",
"photo" => $photo,
"hash" => hash_hmac("sha3-224", $photo, $secret),
]));
}
$files = [];
for($i = 1; $i <= 5; $i++) {
$file = $_FILES[$data["FIELD"] . $i] ?? NULL;
if (!$file || $file["error"] != UPLOAD_ERR_OK || $file["size"] > $maxSize) {
continue;
} else if((sizeof($files) + $usrFiles) > $maxFiles) {
# Clear uploaded files since they can't be saved anyway
foreach($files as $f)
unlink($f);
header("HTTP/1.1 507 Insufficient Storage");
exit("There are $maxFiles pending already. Please save them before uploading more :3");
}
$files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct");
}
if(sizeof($files) === 0) {
header("HTTP/1.0 400");
exit("No file");
}
$filesManifest = [];
foreach($files as $id => $file)
$filesManifest[] = ["keyholder" => $data["USER"], "resource" => $id, "club" => $data["GROUP"]];
$filesManifest = json_encode($filesManifest);
$manifestHash = hash_hmac("sha3-224", $filesManifest, $secret);
header("HTTP/1.0 202 Accepted");
exit(json_encode([
"server" => "ephemeral",
"photos_list" => $filesManifest,
"album_id" => "undefined",
"hash" => $manifestHash,
]));
}
function renderRoute(string $object, string $method): void function renderRoute(string $object, string $method): void
{ {

View file

@ -45,7 +45,7 @@ final class WallPresenter extends OpenVKPresenter
function renderWall(int $user, bool $embedded = false): void function renderWall(int $user, bool $embedded = false): void
{ {
if(false) if(false)
exit("Ошибка доступа: " . (string) random_int(0, 255)); exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
@ -54,7 +54,7 @@ final class WallPresenter extends OpenVKPresenter
if(!$owner->isBanned()) if(!$owner->isBanned())
$canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity);
else else
$this->flashFail("err", tr("error"), "Ошибка доступа"); $this->flashFail("err", tr("error"), tr("forbidden"));
} else if($user < 0) { } else if($user < 0) {
if($owner->canBeModifiedBy($this->user->identity)) if($owner->canBeModifiedBy($this->user->identity))
$canPost = true; $canPost = true;
@ -89,7 +89,7 @@ final class WallPresenter extends OpenVKPresenter
function renderRSS(int $user): void function renderRSS(int $user): void
{ {
if(false) if(false)
exit("Ошибка доступа: " . (string) random_int(0, 255)); exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
@ -98,7 +98,7 @@ final class WallPresenter extends OpenVKPresenter
if(!$owner->isBanned()) if(!$owner->isBanned())
$canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity);
else else
$this->flashFail("err", tr("error"), "Ошибка доступа"); $this->flashFail("err", tr("error"), tr("forbidden"));
} else if($user < 0) { } else if($user < 0) {
if($owner->canBeModifiedBy($this->user->identity)) if($owner->canBeModifiedBy($this->user->identity))
$canPost = true; $canPost = true;
@ -213,12 +213,12 @@ final class WallPresenter extends OpenVKPresenter
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует."); ?? $this->flashFail("err", tr("failed_to_publish_post"), tr("error_4"));
if($wall > 0) { if($wall > 0) {
if(!$wallOwner->isBanned()) if(!$wallOwner->isBanned())
$canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity);
else else
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену."); $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
} else if($wall < 0) { } else if($wall < 0) {
if($wallOwner->canBeModifiedBy($this->user->identity)) if($wallOwner->canBeModifiedBy($this->user->identity))
$canPost = true; $canPost = true;
@ -229,7 +229,7 @@ final class WallPresenter extends OpenVKPresenter
} }
if(!$canPost) if(!$canPost)
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену."); $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
@ -265,10 +265,18 @@ final class WallPresenter extends OpenVKPresenter
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик."); $this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик.");
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
} catch(ISE $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
} }
if(empty($this->postParam("text")) && empty($photos)) if(empty($this->postParam("text")) && !$photo && !$video)
$this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой."); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
$post = new Post; $post = new Post;
@ -281,7 +289,7 @@ final class WallPresenter extends OpenVKPresenter
$post->setNsfw($this->postParam("nsfw") === "on"); $post->setNsfw($this->postParam("nsfw") === "on");
$post->save(); $post->save();
} catch (\LengthException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Пост слишком большой."); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
} }
foreach($photos as $photo) { foreach($photos as $photo) {
@ -300,8 +308,6 @@ final class WallPresenter extends OpenVKPresenter
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();
@ -313,7 +319,7 @@ final class WallPresenter extends OpenVKPresenter
$this->template->wallOwner = (new Users)->get($post->getTargetWall()); $this->template->wallOwner = (new Users)->get($post->getTargetWall());
$this->template->isWallOfGroup = false; $this->template->isWallOfGroup = false;
if($this->template->wallOwner->isBanned()) if($this->template->wallOwner->isBanned())
$this->flashFail("err", tr("error"), "Ошибка доступа"); $this->flashFail("err", tr("error"), tr("forbidden"));
} else { } else {
$this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall()));
$this->template->isWallOfGroup = true; $this->template->isWallOfGroup = true;
@ -377,7 +383,7 @@ final class WallPresenter extends OpenVKPresenter
$user = $this->user->id; $user = $this->user->id;
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
?? $this->flashFail("err", "Не удалось удалить пост", "Такого пользователя не существует."); ?? $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4"));
if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity);
else $canBeDeletedByOtherUser = false; else $canBeDeletedByOtherUser = false;
@ -388,7 +394,7 @@ final class WallPresenter extends OpenVKPresenter
$post->delete(); $post->delete();
} }
} else { } else {
$this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт."); $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, static::REDIRECT_TEMPORARY);
@ -405,7 +411,7 @@ final class WallPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
if(!$post->canBePinnedBy($this->user->identity)) if(!$post->canBePinnedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Вам нельзя закреплять этот пост."); $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
if(($this->queryParam("act") ?? "pin") === "pin") { if(($this->queryParam("act") ?? "pin") === "pin") {
$post->pin(); $post->pin();
@ -414,6 +420,6 @@ final class WallPresenter extends OpenVKPresenter
} }
// TODO localize message based on language and ?act=(un)pin // TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", "Операция успешна", "Операция успешна."); $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">

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}
@ -98,12 +98,12 @@
<div class="layout"> <div class="layout">
<div id="xhead" class="dm"></div> <div id="xhead" class="dm"></div>
<div class="page_header {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}page_custom_header{/if}"> <div class="page_header{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME} page_custom_header{/if}">
<a href="/" class="home_button {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}home_button_custom{/if}" title="{$instance_name}">{$instance_name}</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"> <div class="link">
<a href="/">{_header_home}</a> <a href="/" title="[Alt+Shift+,]" accesskey=",">{_header_home}</a>
</div> </div>
<div class="link"> <div class="link">
<a href="/search?type=groups">{_header_groups}</a> <a href="/search?type=groups">{_header_groups}</a>
@ -122,14 +122,14 @@
</div> </div>
<div class="link"> <div class="link">
<form action="/search" method="get"> <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;" /> <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> </form>
</div> </div>
{else} {else}
<div class="link"> <div class="link">
<a href="/login">{_header_login}</a> <a href="/login">{_header_login}</a>
</div> </div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" class="link"> <div class="link">
<a href="/reg">{_header_registration}</a> <a href="/reg">{_header_registration}</a>
</div> </div>
<div class="link"> <div class="link">
@ -144,7 +144,7 @@
{ifset $thisUser} {ifset $thisUser}
{if !$thisUser->isBanned() XOR !$thisUser->isActivated()} {if !$thisUser->isBanned() XOR !$thisUser->isActivated()}
<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">{_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}
<object type="internal/link" n:if="$thisUser->getFollowersCount() > 0"> <object type="internal/link" n:if="$thisUser->getFollowersCount() > 0">
<a href="/friends{$thisUser->getId()}?act=incoming"> <a href="/friends{$thisUser->getId()}?act=incoming">
@ -161,19 +161,19 @@
</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">{_my_feed}</a> <a n:if="$thisUser->getLeftMenuItemStatus('news')" href="/feed" class="link" title="{_my_feed} [Alt+Shift+W]" accesskey="w">{_my_feed}</a>
<a href="/notifications" class="link">{_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="/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">Админ-панель</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,6 +186,14 @@
<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: 50px;" />
</a> </a>
@ -208,7 +216,7 @@
<input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" /> <input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" style="display: inline-block;" /> <input type="submit" value="{_log_in}" class="button" style="display: inline-block;" />
<a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br> <a href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br>
<a href="/restore">{_forgot_password}</a> <a href="/restore">{_forgot_password}</a>
</form> </form>
{/ifset} {/ifset}
@ -251,7 +259,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>
@ -288,12 +296,44 @@
{/if} {/if}
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script> <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']['enable']">
{var $piwik = (object) OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']}
//<![CDATA[
(function(window,document,dataLayerName,id){
window[dataLayerName]=window[dataLayerName]||[],window[dataLayerName].push({ start:(new Date).getTime(),event:"stg.start" });var scripts=document.getElementsByTagName('script')[0],tags=document.createElement('script');
function stgCreateCookie(a,b,c){ var d="";if(c){ var e=new Date;e.setTime(e.getTime()+24*c*60*60*1e3),d=";expires="+e.toUTCString() }document.cookie=a+"="+b+d+";path=/" }
var isStgDebug=(window.location.href.match("stg_debug")||document.cookie.match("stg_debug"))&&!window.location.href.match("stg_disable_debug");stgCreateCookie("stg_debug",isStgDebug?1:"",isStgDebug?14:-1);
var qP=[];dataLayerName!=="dataLayer"&&qP.push("data_layer_name="+dataLayerName),isStgDebug&&qP.push("stg_debug");var qPString=qP.length>0?("?"+qP.join("&")):"";
tags.async=!0,tags.src={$piwik->container . "/"}+id+".js"+qPString,scripts.parentNode.insertBefore(tags,scripts);
!function(a,n,i){ a[n]=a[n]||{ };for(var c=0;c<i.length;c++)!function(i){ a[n][i]=a[n][i]||{ },a[n][i].api=a[n][i].api||function(){ var a=[].slice.call(arguments,0);"string"==typeof a[0]&&window[dataLayerName].push({ event:n+"."+i+":"+a[0],parameters:[].slice.call(arguments,1) }) } }(i[c]) }(window,"ppms",["tm","cm"]);
})(window,document,{$piwik->layer}, {$piwik->site});
//]]>
</script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['matomo']['enable']">
{var $matomo = (object) OPENVK_ROOT_CONF['openvk']['telemetry']['matomo']}
//<![CDATA[
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//" + {$matomo->container} + "/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', {$matomo->site}]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
//]]>
</script>
{ifset bodyScripts} {ifset bodyScripts}
{include bodyScripts} {include bodyScripts}
{/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

@ -1,70 +1,74 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block wrap} {block wrap}
<div class="page_wrap"> <div class="wrap2">
<div n:ifset="tabs" n:ifcontent class="tabs"> <div class="wrap1">
{include tabs} <div class="page_wrap padding_top">
</div> <div n:ifset="tabs" class="tabs">
{include tabs}
</div>
{ifset size} {ifset size}
{include size, x => $dat} {include size, x => $dat}
{/ifset} {/ifset}
{ifset specpage}
{include specpage, x => $dat}
{else}
<div class="container_gray">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0} {ifset specpage}
<div class="content" n:foreach="$data as $dat"> {include specpage, x => $dat}
<table> {else}
<tbody> <div class="container_gray">
<tr> {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
<td valign="top">
<a href="{include link, x => $dat}"> {if sizeof($data) > 0}
{include preview, x => $dat} <div class="content" n:foreach="$data as $dat">
</a> <table>
</td> <tbody>
<td valign="top" style="width: 100%"> <tr>
{ifset infoTable} <td valign="top">
{include infoTable, x => $dat} <a href="{include link, x => $dat}">
{else} {include preview, x => $dat}
</a>
</td>
<td valign="top" style="width: 100%">
{ifset infotable}
{include infotable, x => $dat}
{else}
<a href="{include link, x => $dat}"> <a href="{include link, x => $dat}">
<b> <b>
{include name, x => $dat} {include name, x => $dat}
</b> </b>
</a> </a>
<br/> <br/>
{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; text-transform: lowercase;">
{include actions, x => $dat} {include actions, x => $dat}
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
{include "components/paginator.xml", conf => (object) [ {include "components/paginator.xml", conf => (object) [
"page" => $page, "page" => $page,
"count" => $count, "count" => $count,
"amount" => sizeof($data), "amount" => sizeof($data),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true, "atBottom" => true,
]} ]}
{else}
{ifset customErrorMessage}
{include customErrorMessage}
{else} {else}
{include "components/nothing.xml"} {ifset customErrorMessage}
{/ifset} {include customErrorMessage}
{/if} {else}
</div> {include "components/nothing.xml"}
{/ifset} {/ifset}
{/if}
</div>
{/ifset}
{ifset bottom} {ifset bottom}
{include bottom} {include bottom}
{/ifset} {/ifset}
</div>
</div> </div>
{/block} </div>
{/block}

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

@ -9,7 +9,7 @@
{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 n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" 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

@ -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">
@ -16,23 +17,15 @@
<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="/admin">
<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,64 @@
<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>
Группы
</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 +105,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 +113,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 +141,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

@ -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()}" 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()}" 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>
{/block} <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}

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>
@ -32,28 +34,28 @@
<td> <td>
<span class="aui-avatar aui-avatar-xsmall"> <span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner"> <span class="aui-avatar-inner">
<img src="{$club->getAvatarUrl()}" alt="{$club->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> <img src="{$club->getAvatarUrl('miniscule')}" alt="{$club->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span> </span>
</span> </span>
<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">
<img src="{$user->getAvatarUrl()}" alt="{$user->getCanonicalName()}" role="presentation" /> <img src="{$user->getAvatarUrl('miniscule')}" alt="{$user->getCanonicalName()}" role="presentation" />
</span> </span>
</span> </span>
<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>

View file

@ -1,7 +1,7 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block title} {block title}
Наборы подарков {_admin_giftsets}
{/block} {/block}
{block headingWrap} {block headingWrap}
@ -9,7 +9,7 @@
{_create} {_create}
</a> </a>
<h1>Наборы подарков</h1> <h1>{_admin_giftsets}</h1>
{/block} {/block}
{block content} {block content}
@ -27,12 +27,11 @@
</td> </td>
<td style="vertical-align: middle; text-align: right;"> <td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}.meta"> <a class="aui-button aui-button-primary" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}.meta">
<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>
<a class="aui-button" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}/"> <a class="aui-button" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}/">
<span class="aui-icon aui-icon-small aui-iconfont-gallery">Открыть</span> <span class="aui-icon aui-icon-small aui-iconfont-gallery">{_admin_open}</span>
Открыть
</a> </a>
</td> </td>
</tr> </tr>
@ -40,17 +39,13 @@
</table> </table>
{else} {else}
<center> <center>
<p>Наборов подарков нету. Чтобы создать подарок, создайте набор.</p> <p>{_admin_giftsets_none}</p>
</center> </center>
{/if} {/if}
<div align="right"> <div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count} {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}"> <a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
⭁ туда <a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</a>
<a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div> </div>
{/block} {/block}

View file

@ -2,7 +2,7 @@
{block title} {block title}
{if $form->id === 0} {if $form->id === 0}
Создать набор подарков {_admin_giftsets_create}
{else} {else}
{$form->languages["master"]->name} {$form->languages["master"]->name}
{/if} {/if}
@ -14,7 +14,7 @@
{block content} {block content}
<form class="aui" method="POST"> <form class="aui" method="POST">
<h3>Общие настройки</h3> <h3>{_admin_commonsettings}</h3>
<fieldset> <fieldset>
<div class="field-group"> <div class="field-group">
<label for="id"> <label for="id">
@ -24,37 +24,37 @@
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="name_master"> <label for="name_master">
Наименование {_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_master" name="name_master" value="{$form->languages['master']->name}" /> <input class="text long-field" type="text" id="name_master" name="name_master" value="{$form->languages['master']->name}" />
<div class="description">Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div> <div class="description">{_admin_giftsets_title}</div>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="description_master"> <label for="description_master">
Описание {_admin_description}
<span class="aui-icon icon-required"></span> <span class="aui-icon icon-required"></span>
</label> </label>
<input class="text long-field" type="text" id="description_master" name="description_master" value="{$form->languages['master']->description}" /> <input class="text long-field" type="text" id="description_master" name="description_master" value="{$form->languages['master']->description}" />
<div class="description">Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div> <div class="description">{_admin_giftsets_description}</div>
</div> </div>
</fieldset> </fieldset>
<h3>Языко-зависимые настройки</h3> <h3>{_admin_langsettings}</h3>
<fieldset> <fieldset>
{foreach $form->languages as $locale => $data} {foreach $form->languages as $locale => $data}
{continueIf $locale === "master"} {continueIf $locale === "master"}
<div class="field-group"> <div class="field-group">
<label for="name_{$locale}"> <label for="name_{$locale}">
Наименование {_admin_name}
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" /> <img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label> </label>
<input class="text long-field" type="text" id="name_{$locale}" name="name_{$locale}" value="{$data->name}" /> <input class="text long-field" type="text" id="name_{$locale}" name="name_{$locale}" value="{$data->name}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="description_{$locale}"> <label for="description_{$locale}">
Описание {_admin_description}
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" /> <img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label> </label>
<input class="text long-field" type="text" id="description_{$locale}" name="description_{$locale}" value="{$data->description}" /> <input class="text long-field" type="text" id="description_{$locale}" name="description_{$locale}" value="{$data->description}" />

View file

@ -9,7 +9,7 @@
{_create} {_create}
</a> </a>
<h1>Набор "{$cat->getName()}"</h1> <h1>{_admin_giftset} "{$cat->getName()}"</h1>
{/block} {/block}
{block content} {block content}
@ -32,11 +32,11 @@
<td style="vertical-align: middle;"> <td style="vertical-align: middle;">
{$gift->getName()} {$gift->getName()}
<span n:if="$gift->isFree()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-success"> <span n:if="$gift->isFree()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">
бесплатный {_admin_price_free}
</span> </span>
</td> </td>
<td style="vertical-align: middle;"> <td style="vertical-align: middle;">
{$gift->getPrice()} голосов {tr("points_amount", $gift->getPrice())}
</td> </td>
<td style="vertical-align: middle;"> <td style="vertical-align: middle;">
{$gift->getUsages()} раз {$gift->getUsages()} раз
@ -71,12 +71,9 @@
{/if} {/if}
<div align="right"> <div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count} {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $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> <a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div> </div>
{/block} {/block}

View file

@ -1,13 +1,13 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block title} {block title}
Сводка {_admin_overview_summary}
{/block} {/block}
{block heading} {block heading}
Сводка {_admin_overview_summary}
{/block} {/block}
{block content} {block content}
Да! ┬─┬︵/(.□.)╯
{/block} {/block}

View file

@ -1,7 +1,7 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block title} {block title}
Редактировать {$user->getCanonicalName()} {_edit} {$user->getCanonicalName()}
{/block} {/block}
{block heading} {block heading}
@ -10,89 +10,70 @@
{block content} {block content}
<div class="aui-tabs horizontal-tabs"> <div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST"> <form class="aui" method="POST">
<div class="field-group"> <div class="field-group">
<label for="avatar"> <label for="avatar">{_avatar}</label>
Аватарка <span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
</label> <span class="aui-avatar-inner">
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge"> <img src="{$user->getAvatarUrl('tiny')}" style="object-fit: cover;"></img>
<span class="aui-avatar-inner"> </span>
<img src="{$user->getAvatarUrl()}" style="object-fit: cover;"></img>
</span> </span>
</span>
</div>
<div class="field-group">
<label for="id">
ID
</label>
<input class="text medium-field" type="number" id="id" disabled value="{$user->getId()}" />
</div>
<div class="field-group">
<label for="guid">
GUID
</label>
<input class="text medium-field" id="guid" disabled value="{$user->getChandlerUser()->getId()}" />
</div>
<div class="field-group">
<label for="first_name">
Имя
</label>
<input class="text medium-field" type="text" id="first_name" name="first_name" value="{$user->getFirstName()}" />
</div>
<div class="field-group">
<label for="last_name">
Фамилия
</label>
<input class="text medium-field" type="text" id="last_name" name="last_name" value="{$user->getLastName()}" />
</div>
<div class="field-group">
<label for="nickname">
Никнейм
</label>
<input class="text medium-field" type="text" id="nickname" name="nickname" value="{$user->getPseudo()}" />
</div>
<div class="field-group">
<label for="status">
Статус
</label>
<input class="text medium-field" type="text" id="status" name="status" value="{$user->getStatus()}" />
</div>
<div class="field-group">
<label for="email">
E-Mail
</label>
<input class="text medium-field" type="email" id="email" name="email" value="{$user->getEmail()}" />
</div>
<div class="field-group">
<label for="shortcode">
Адрес страницы
</label>
<input class="text medium-field" type="text" id="shortcode" name="shortcode" value="{$user->getShortCode()}" />
</div>
<hr>
<div class="field-group">
<label for="city">
Верификация
</label>
<input class="toggle-large" type="checkbox" id="verify" name="verify" value="1" {if $user->isVerified()} checked {/if} />
</div>
<div class="field-group">
<label for="city">
Онлайн статус
</label>
<select name="online" class="select">
<option value="0" {if $user->onlineStatus() > 2}selected{/if}>По-умолчанию</option>
<option value="1" {if $user->onlineStatus() == 1}selected{/if}>Инкогнито</option>
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>Юзер умер</option>
</select>
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="Сохранить">
</div> </div>
</div> <div class="field-group">
</form> <label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$user->getId()}" />
</div>
<div class="field-group">
<label for="guid">GUID</label>
<input class="text medium-field" id="guid" disabled value="{$user->getChandlerUser()->getId()}" />
</div>
<div class="field-group">
<label for="registration_ip">{_admin_first_known_ip}</label>
<input class="text medium-field" id="guid" disabled value="{$user->getRegistrationIP()}" />
</div>
<div class="field-group">
<label for="first_name">{_name}</label>
<input class="text medium-field" type="text" id="first_name" name="first_name" value="{$user->getFirstName()}" />
</div>
<div class="field-group">
<label for="last_name">{_surname}</label>
<input class="text medium-field" type="text" id="last_name" name="last_name" value="{$user->getLastName()}" />
</div>
<div class="field-group">
<label for="nickname">{_nickname}</label>
<input class="text medium-field" type="text" id="nickname" name="nickname" value="{$user->getPseudo()}" />
</div>
<div class="field-group">
<label for="status">{_status}</label>
<input class="text medium-field" type="text" id="status" name="status" value="{$user->getStatus()}" />
</div>
<div class="field-group">
<label for="email">E-Mail</label>
<input class="text medium-field" type="email" id="email" name="email" value="{$user->getEmail()}" />
</div>
<div class="field-group">
<label for="shortcode">{_admin_shortcode}</label>
<input class="text medium-field" type="text" id="shortcode" name="shortcode" value="{$user->getShortCode()}" />
</div>
<hr>
<div class="field-group">
<label for="city">{_admin_verification}</label>
<input class="toggle-large" type="checkbox" id="verify" name="verify" value="1" {if $user->isVerified()} checked {/if} />
</div>
<div class="field-group">
<label for="city">{_admin_user_online}</label>
<select name="online" class="select">
<option value="0" {if $user->onlineStatus() > 2}selected{/if}>{_admin_user_online_default}</option>
<option value="1" {if $user->onlineStatus() == 1}selected{/if}>{_admin_user_online_incognite}</option>
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option>
</select>
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div> </div>
{/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_user_search}
{/block} {/block}
{block heading} {block heading}
Пиздюки {_users}
{/block} {/block}
{block searchTitle}Поиск пиздюков{/block} {block searchTitle}
{include title}
{/block}
{block content} {block content}
{var users = iterator_to_array($users)} {var $users = iterator_to_array($users)}
{var amount = sizeof($users)} {var $amount = sizeof($users)}
<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_name}</th>
<th>Пол</th> <th>{_gender}</th>
<th>Короткий адрес</th> <th>{_admin_shortcode}</th>
<th>Дата регистрации</th> <th>{_registration_date}</th>
<th>Действия</th> <th>{_admin_actions}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -32,26 +34,24 @@
<td> <td>
<span class="aui-avatar aui-avatar-xsmall"> <span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner"> <span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl()}" alt="{$user->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> <img src="{$user->getAvatarUrl('miniscule')}" alt="{$user->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span> </span>
</span> </span>
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> <a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed"> <span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
заблокирован
</span>
</td> </td>
<td>{$user->isFemale() ? "Женский" : "Мужской"}</td> <td>{$user->isFemale() ? tr("female") : tr("male")}</td>
<td>{$user->getShortCode() ?? "(отсутствует)"}</td> <td>{$user->getShortCode() ?? "(" . tr("none") . ")"}</td>
<td>{$user->getRegistrationTime()}</td> <td>{$user->getRegistrationTime()}</td>
<td> <td>
<a class="aui-button aui-button-primary" href="/admin/users/id{$user->getId()}"> <a class="aui-button aui-button-primary" href="/admin/users/id{$user->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>
{if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)} {if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)}
<a class="aui-button" href="/setSID/{$user->getChandlerUser()->getId()}?hash={rawurlencode($csrfToken)}"> <a class="aui-button" href="/setSID/{$user->getChandlerUser()->getId()}?hash={rawurlencode($csrfToken)}">
<span class="aui-icon aui-icon-small aui-iconfont-sign-in">Войти как</span> <span class="aui-icon aui-icon-small aui-iconfont-sign-in">{_admin_loginas}</span>
</a> </a>
{/if} {/if}
</td> </td>
@ -60,13 +60,9 @@
</table> </table>
<br/> <br/>
<div align="right"> <div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} {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 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

@ -5,45 +5,36 @@
{/block} {/block}
{block heading} {block heading}
{_edit} {$form->token ?? "undefined"} {_edit} #{$form->token ?? "undefined"}
{/block} {/block}
{block content} {block content}
<div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs"> <div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs">
<ul class="tabs-menu"> <ul class="tabs-menu">
<li class="menu-item active-tab"> <li class="menu-item active-tab">
<a href="#info">Информация</a> <a href="#info">{_admin_tab_main}</a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="#activators">{_voucher_activators}</a> <a href="#activators">{_voucher_activators}</a>
</li> </li>
</ul> </ul>
<div class="tabs-pane active-pane" id="info"> <div class="tabs-pane active-pane" id="info">
<form class="aui" method="POST"> <form class="aui" method="POST">
<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" name="id" disabled value="{$form->id}" /> <input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="token"> <label for="token">{_admin_voucher_serial}</label>
Серийный номер
</label>
<input class="text long-field" type="text" id="token" name="token" value="{$form->token}" /> <input class="text long-field" type="text" id="token" name="token" value="{$form->token}" />
<div class="description">Номер состоит из 24 символов, если формат неправильный или поле не заполнено, будет назначен автоматически.</div> <div class="description">{_admin_voucher_serial_desc}</div>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="coins"> <label for="coins">{_admin_voucher_coins}</label>
Количество голосов
</label>
<input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" /> <input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="rating"> <label for="rating">{_admin_voucher_rating}</label>
Количество рейтинга
</label>
<input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" /> <input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" />
</div> </div>
<div class="field-group"> <div class="field-group">
@ -55,9 +46,9 @@
{/if} {/if}
</label> </label>
<input class="text long-field" type="number" min="-1" id="usages" name="usages" value="{$form->usages}" /> <input class="text long-field" type="number" min="-1" id="usages" name="usages" value="{$form->usages}" />
<div class="description">Количество аккаунтов, которые могут использовать ваучер. Если написать -1, будет Infinity.</div> <div class="description">{_admin_voucher_usages_desc}</div>
</div> </div>
<div class="buttons-container"> <div class="buttons-container">
<div class="buttons"> <div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
@ -66,7 +57,6 @@
</div> </div>
</form> </form>
</div> </div>
<div class="tabs-pane" id="activators"> <div class="tabs-pane" id="activators">
<table rules="none" class="aui aui-table-list"> <table rules="none" class="aui aui-table-list">
<tbody> <tbody>
@ -74,15 +64,13 @@
<td> <td>
<span class="aui-avatar aui-avatar-xsmall"> <span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner"> <span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl()}" alt="{$user->getCanonicalName()}" role="presentation" /> <img src="{$user->getAvatarUrl('miniscule')}" alt="{$user->getCanonicalName()}" role="presentation" />
</span> </span>
</span> </span>
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> <a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed"> <span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
заблокирован
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -8,7 +8,7 @@
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/vouchers/id0"> <a style="float: right;" class="aui-button aui-button-primary" href="/admin/vouchers/id0">
{_create} {_create}
</a> </a>
<h1>{_vouchers}</h1> <h1>{_vouchers}</h1>
{/block} {/block}
@ -16,13 +16,13 @@
<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_voucher_serial}</th>
<th>Голоса</th> <th>{_coins}</th>
<th>Рейгтинг</th> <th>{_admin_voucher_rating}</th>
<th>Осталось использований</th> <th>{_usages_left}</th>
<th>Состояние</th> <th>{_admin_voucher_status}</th>
<th>Действия</th> <th>{_admin_actions}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -34,28 +34,25 @@
<td>{$voucher->getRemainingUsages() === INF ? "∞" : $voucher->getRemainingUsages()}</td> <td>{$voucher->getRemainingUsages() === INF ? "∞" : $voucher->getRemainingUsages()}</td>
<td> <td>
{if $voucher->isExpired()} {if $voucher->isExpired()}
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">закончился</span> <span class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_voucher_status_closed}</span>
{else} {else}
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">активен</span> <span class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">{_admin_voucher_status_opened}</span>
{/if} {/if}
</td> </td>
<td> <td>
<a class="aui-button aui-button-primary" href="/admin/vouchers/id{$voucher->getId()}"> <a class="aui-button aui-button-primary" href="/admin/vouchers/id{$voucher->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>
</tbody> </tbody>
</table> </table>
<br/> <br/>
<div align="right"> <div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count} {var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $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> <a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div> </div>
{/block} {/block}

View file

@ -18,7 +18,7 @@
<span>{_code}: </span> <span>{_code}: </span>
</td> </td>
<td> <td>
<input type="text" name="code" required /> <input type="text" name="code" autocomplete="off" required />
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -47,7 +47,7 @@
<span>{_"gender"}: </span> <span>{_"gender"}: </span>
</td> </td>
<td> <td>
{var femalePreferred = OPENVK_ROOT_CONF["openvk"]["preferences"]["femaleGenderPriority"]} {var $femalePreferred = OPENVK_ROOT_CONF["openvk"]["preferences"]["femaleGenderPriority"]}
<select name="sex" required> <select name="sex" required>
<option n:attr="selected => !$femalePreferred" value="male">{_"male"}</option> <option n:attr="selected => !$femalePreferred" value="male">{_"male"}</option>
<option n:attr="selected => $femalePreferred" value="female">{_"female"}</option> <option n:attr="selected => $femalePreferred" value="female">{_"female"}</option>

View file

@ -92,7 +92,7 @@
<span class="nobold">{_group_administrators_list}: </span> <span class="nobold">{_group_administrators_list}: </span>
</td> </td>
<td> <td>
{var areAllAdminsHidden = $club->getManagersCount(true) == 0} {var $areAllAdminsHidden = $club->getManagersCount(true) == 0}
<input type="radio" name="administrators_list_display" value="0" n:attr="checked => $club->getAdministratorsListDisplay() == 0, disabled => $areAllAdminsHidden" /> {_group_display_only_creator}<br> <input type="radio" name="administrators_list_display" value="0" n:attr="checked => $club->getAdministratorsListDisplay() == 0, disabled => $areAllAdminsHidden" /> {_group_display_only_creator}<br>
<input type="radio" name="administrators_list_display" value="1" n:attr="checked => $club->getAdministratorsListDisplay() == 1, disabled => $areAllAdminsHidden" /> {_group_display_all_administrators}<br> <input type="radio" name="administrators_list_display" value="1" n:attr="checked => $club->getAdministratorsListDisplay() == 1, disabled => $areAllAdminsHidden" /> {_group_display_all_administrators}<br>
<input type="radio" name="administrators_list_display" value="2" n:attr="checked => $club->getAdministratorsListDisplay() == 2" /> {_group_dont_display_administrators_list}<br> <input type="radio" name="administrators_list_display" value="2" n:attr="checked => $club->getAdministratorsListDisplay() == 2" /> {_group_dont_display_administrators_list}<br>

View file

@ -1,9 +1,9 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var $Manager = openvk\Web\Models\Entities\Manager::class} {var $Manager = openvk\Web\Models\Entities\Manager::class}
{var iterator = $onlyShowManagers ? $managers : $followers} {var $iterator = $onlyShowManagers ? $managers : $followers}
{var count = $paginatorConf->count} {var $count = $paginatorConf->count}
{var page = $paginatorConf->page} {var $page = $paginatorConf->page}
{var perPage = 6} {var $perPage = 6}
{block title}{_followers} {$club->getCanonicalName()}{/block} {block title}{_followers} {$club->getCanonicalName()}{/block}
@ -41,7 +41,7 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x instanceof $Manager ? $x->getUser()->getAvatarURL() : $x->getAvatarURL()}" alt="{$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()}" width=75 /> <img src="{$x instanceof $Manager ? $x->getUser()->getAvatarURL() : $x->getAvatarURL('miniscule')}" alt="{$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()}" width=75 />
{/block} {/block}
{block name} {block name}
@ -49,8 +49,8 @@
{/block} {/block}
{block description} {block description}
{var user = $x instanceof $Manager ? $x->getUser() : $x} {var $user = $x instanceof $Manager ? $x->getUser() : $x}
{var manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} {var $manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))}
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -106,8 +106,8 @@
{/block} {/block}
{block actions} {block actions}
{var user = $x instanceof $Manager ? $x->getUser() : $x} {var $user = $x instanceof $Manager ? $x->getUser() : $x}
{var manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))} {var $manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))}
{if $club->canBeModifiedBy($thisUser ?? NULL)} {if $club->canBeModifiedBy($thisUser ?? NULL)}
<a class="profile_link" href="/club{$club->getId()}/setAdmin?user={$user->getId()}&hash={rawurlencode($csrfToken)}" n:if="$club->getOwner()->getId() !== $user->getId()"> <a class="profile_link" href="/club{$club->getId()}/setAdmin?user={$user->getId()}&hash={rawurlencode($csrfToken)}" n:if="$club->getOwner()->getId() !== $user->getId()">
{if $manager} {if $manager}
@ -140,4 +140,4 @@
</a> </a>
{/if} {/if}
{/if} {/if}
{/block} {/block}

View file

@ -14,6 +14,8 @@
{block content} {block content}
<div class="left_big_block"> <div class="left_big_block">
<div n:if="!is_null($alert = $club->getAlert())" class="group-alert">{strpos($alert, "@") === 0 ? tr(substr($alert, 1)) : $alert}</div>
<div class="content_title_expanded" onclick="hidePanel(this);"> <div class="content_title_expanded" onclick="hidePanel(this);">
{_"information"} {_"information"}
</div> </div>
@ -41,7 +43,7 @@
</table> </table>
</div> </div>
<div n:if="$club->getFollowersCount() > 0"> <div n:if="$club->getFollowersCount() > 0">
{var followersCount = $club->getFollowersCount()} {var $followersCount = $club->getFollowersCount()}
<div class="content_title_expanded" onclick="hidePanel(this, {$followersCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$followersCount});">
{_participants} {_participants}
@ -57,7 +59,7 @@
<div class="cl_element" n:foreach="$club->getFollowers(1) as $follower"> <div class="cl_element" n:foreach="$club->getFollowers(1) as $follower">
<div class="cl_avatar"> <div class="cl_avatar">
<a href="{$follower->getURL()}"> <a href="{$follower->getURL()}">
<img class="ava" src="{$follower->getAvatarUrl()}" /> <img class="ava" src="{$follower->getAvatarUrl('miniscule')}" />
</a> </a>
</div> </div>
<a href="{$follower->getURL()}" class="cl_name"> <a href="{$follower->getURL()}" class="cl_name">
@ -91,10 +93,10 @@
{presenter "openvk!Wall->wallEmbedded", -$club->getId()} {presenter "openvk!Wall->wallEmbedded", -$club->getId()}
</div> </div>
<div class="right_small_block"> <div class="right_small_block">
{var avatarPhoto = $club->getAvatarPhoto()} {var $avatarPhoto = $club->getAvatarPhoto()}
{var avatarLink = ((is_null($avatarPhoto) ? FALSE : $avatarPhoto->isAnonymous()) ? "/photo" . ("s/" . base_convert((string) $avatarPhoto->getId(), 10, 32)) : $club->getAvatarLink())} {var $avatarLink = ((is_null($avatarPhoto) ? FALSE : $avatarPhoto->isAnonymous()) ? "/photo" . ("s/" . base_convert((string) $avatarPhoto->getId(), 10, 32)) : $club->getAvatarLink())}
<a href="{$avatarLink|nocheck}"> <a href="{$avatarLink|nocheck}">
<img src="{$club->getAvatarUrl()}" style="width: 100%; image-rendering: -webkit-optimize-contrast;" /> <img src="{$club->getAvatarUrl('normal')}" style="width: 100%; image-rendering: -webkit-optimize-contrast;" />
</a> </a>
<div n:ifset="$thisUser" id="profile_links"> <div n:ifset="$thisUser" id="profile_links">
{if $club->canBeModifiedBy($thisUser)} {if $club->canBeModifiedBy($thisUser)}
@ -132,7 +134,7 @@
{_"creator"} {_"creator"}
</div> </div>
<div class="avatar-list-item" style="padding: 8px;"> <div class="avatar-list-item" style="padding: 8px;">
{var author = $club->getOwner()} {var $author = $club->getOwner()}
<div class="avatar"> <div class="avatar">
<a href="{$author->getURL()}"> <a href="{$author->getURL()}">
<img class="ava" src="{$author->getAvatarUrl()}" /> <img class="ava" src="{$author->getAvatarUrl()}" />
@ -149,7 +151,7 @@
</div> </div>
</div> </div>
<div n:if="$club->getAdministratorsListDisplay() == 1"> <div n:if="$club->getAdministratorsListDisplay() == 1">
{var managersCount = $club->getManagersCount(true)} {var $managersCount = $club->getManagersCount(true)}
<div class="content_title_expanded" onclick="hidePanel(this, {$managersCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$managersCount});">
{_"administrators"} {_"administrators"}
@ -163,7 +165,7 @@
</div> </div>
<div class="avatar-list"> <div class="avatar-list">
<div class="avatar-list-item" n:if="!$club->isOwnerHidden()"> <div class="avatar-list-item" n:if="!$club->isOwnerHidden()">
{var author = $club->getOwner()} {var $author = $club->getOwner()}
<div class="avatar"> <div class="avatar">
<a href="{$author->getURL()}"> <a href="{$author->getURL()}">
<img class="ava" src="{$author->getAvatarUrl()}" /> <img class="ava" src="{$author->getAvatarUrl()}" />
@ -175,7 +177,7 @@
</div> </div>
</div> </div>
<div class="avatar-list-item" n:foreach="$club->getManagers(1, true) as $manager"> <div class="avatar-list-item" n:foreach="$club->getManagers(1, true) as $manager">
{var user = $manager->getUser()} {var $user = $manager->getUser()}
<div class="avatar"> <div class="avatar">
<a href="{$user->getURL()}"> <a href="{$user->getURL()}">
<img height="32" class="ava" src="{$user->getAvatarUrl()}" /> <img height="32" class="ava" src="{$user->getAvatarUrl()}" />
@ -203,7 +205,7 @@
<div style="padding: 5px;"> <div style="padding: 5px;">
<div class="ovk-album" style="display: inline-block;" n:foreach="$albums as $album"> <div class="ovk-album" style="display: inline-block;" n:foreach="$albums as $album">
<div style="text-align: center;float: left;height: 54pt;width: 100px;"> <div style="text-align: center;float: left;height: 54pt;width: 100px;">
{var cover = $album->getCoverPhoto()} {var $cover = $album->getCoverPhoto()}
<img <img
src="{is_null($cover)?'/assets/packages/static/openvk/img/camera_200.png':$cover->getURL()}" src="{is_null($cover)?'/assets/packages/static/openvk/img/camera_200.png':$cover->getURL()}"

View file

@ -6,7 +6,7 @@
<a href="/im">{_my_messages}</a> » <a href="/im">{_my_messages}</a> »
<a href="{$correspondent->getURL()}">{$correspondent->getCanonicalName()}</a> <a href="{$correspondent->getURL()}">{$correspondent->getCanonicalName()}</a>
<div n:if="($online = $correspondent->getOnline()->timestamp()) + 2505600 > time()" style="float: right;"> <div n:if="($online = $correspondent->getOnline()->timestamp()) + 2505600 > time()" style="float: right;">
{var diff = date_diff(date_create(), date_create('@' . $online))} {var $diff = date_diff(date_create(), date_create('@' . $online))}
{if 5 >= $diff->i} {if 5 >= $diff->i}
<span><b>{_online}</b></span> <span><b>{_online}</b></span>
{else} {else}
@ -44,7 +44,7 @@
</div> </div>
<div class="messenger-app--input"> <div class="messenger-app--input">
{if $correspondent->getId() === $thisUser->getId() || $correspondent->getPrivacyPermission('messages.write', $thisUser)} {if $correspondent->getId() === $thisUser->getId() || $correspondent->getPrivacyPermission('messages.write', $thisUser)}
<img class="ava" src="{$thisUser->getAvatarUrl()}" alt="{$thisUser->getCanonicalName()}" /> <img class="ava" src="{$thisUser->getAvatarUrl('miniscule')}" alt="{$thisUser->getCanonicalName()}" />
<div class="messenger-app--input---messagebox"> <div class="messenger-app--input---messagebox">
<textarea <textarea
data-bind="value: messageContent, event: { keydown: onTextareaKeyPress }" data-bind="value: messageContent, event: { keydown: onTextareaKeyPress }"
@ -52,7 +52,7 @@
placeholder="Введите сообщение"></textarea> placeholder="Введите сообщение"></textarea>
<button class="button" data-bind="click: sendMessage">Отправить</button> <button class="button" data-bind="click: sendMessage">Отправить</button>
</div> </div>
<img class="ava" src="{$correspondent->getAvatarUrl()}" alt="{$correspondent->getCanonicalName()}" /> <img class="ava" src="{$correspondent->getAvatarUrl('miniscule')}" alt="{$correspondent->getCanonicalName()}" />
{else} {else}
<div class="blocked" data-localized-text="Вы не можете писать сообщения {$correspondent->getCanonicalName()} из-за его настроек приватности."></div> <div class="blocked" data-localized-text="Вы не можете писать сообщения {$correspondent->getCanonicalName()} из-за его настроек приватности."></div>
{/if} {/if}

View file

@ -21,11 +21,11 @@
<div n:foreach="$corresps as $coresp" <div n:foreach="$corresps as $coresp"
class="crp-entry" class="crp-entry"
onmousedown="window.location.href = {$coresp->getURL()};" > onmousedown="window.location.href = {$coresp->getURL()};" >
{var recipient = $coresp->getCorrespondents()[1]} {var $recipient = $coresp->getCorrespondents()[1]}
{var lastMsg = $coresp->getPreviewMessage()} {var $lastMsg = $coresp->getPreviewMessage()}
<div class="crp-entry--image"> <div class="crp-entry--image">
<img src="{$recipient->getAvatarURL()}" <img src="{$recipient->getAvatarURL('miniscule')}"
alt="Фотография пользователя" /> alt="Фотография пользователя" />
</div> </div>
<div class="crp-entry--info"> <div class="crp-entry--info">
@ -33,10 +33,10 @@
<span>{$lastMsg->getSendTime()->format("%e %B %G" . tr("time_at_sp") . "%X")}</span> <span>{$lastMsg->getSendTime()->format("%e %B %G" . tr("time_at_sp") . "%X")}</span>
</div> </div>
<div n:class="crp-entry--message, $lastMsg->getUnreadState() ? unread"> <div n:class="crp-entry--message, $lastMsg->getUnreadState() ? unread">
{var _author = $lastMsg->getSender()} {var $_author = $lastMsg->getSender()}
<div class="crp-entry--message---av" n:if="$_author->getId() === $thisUser->getId()"> <div class="crp-entry--message---av" n:if="$_author->getId() === $thisUser->getId()">
<img src="{$_author->getAvatarURL()}" <img src="{$_author->getAvatarURL('miniscule')}"
alt="Фотография пользователя" /> alt="Фотография пользователя" />
</div> </div>
<div class="crp-entry--message---text"> <div class="crp-entry--message---text">

View file

@ -3,7 +3,7 @@
{block title}{_edit_note}{/block} {block title}{_edit_note}{/block}
{block header} {block header}
{var author = $note->getOwner()} {var $author = $note->getOwner()}
<a href="{$author->getURL()}">{$author->getCanonicalName()}</a> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a>
» »
<a href="/notes{$author->getId()}">{_notes}</a> <a href="/notes{$author->getId()}">{_notes}</a>

View file

@ -1,6 +1,6 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = iterator_to_array($notes)} {var $iterator = iterator_to_array($notes)}
{var page = $paginatorConf->page} {var $page = $paginatorConf->page}
{block title}{_notes}{/block} {block title}{_notes}{/block}
@ -34,15 +34,41 @@
{* BEGIN ELEMENTS DESCRIPTION *} {* BEGIN ELEMENTS DESCRIPTION *}
{block specpage} {block specpage}
<style>
#userContent img {
max-width: 245pt;
max-height: 200pt;
}
#userContent blockquote {
background-color: #f3f3f3;
border-bottom: 5px solid #969696;
padding: 1;
}
#userContent cite {
margin-top: 1em;
display: block;
}
#userContent cite::before {
content: "— ";
}
#userContent .underline {
text-decoration: underline;
}
</style>
<div class="container_gray" style="background: white; border-top: none;"> <div class="container_gray" style="background: white; border-top: none;">
{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 n:foreach="$data as $dat"> <div n:foreach="$data as $dat">
<div class="profile_thumb"> <div class="profile_thumb">
<a href="{$owner->getURL()}"> <a href="{$owner->getURL()}">
<img src="{$owner->getAvatarUrl()}" style="width: 50px;"> <img src="{$owner->getAvatarUrl('miniscule')}" style="width: 50px;">
</a> </a>
</div> </div>
<article class="note_body" id="userContent" style="width: 540px; display: inline-block; margin-bottom: 35px;"> <article class="note_body" id="userContent" style="width: 540px; display: inline-block; margin-bottom: 35px;">

View file

@ -3,7 +3,7 @@
{block title}{$note->getName()}{/block} {block title}{$note->getName()}{/block}
{block header} {block header}
{var author = $note->getOwner()} {var $author = $note->getOwner()}
<a href="{$author->getURL()}">{$author->getCanonicalName()}</a> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a>
» »
<a href="/notes{$author->getId()}">{_notes}</a> <a href="/notes{$author->getId()}">{_notes}</a>
@ -12,7 +12,7 @@
{/block} {/block}
{block content} {block content}
{var author = $note->getOwner()} {var $author = $note->getOwner()}
<style> <style>
#userContent img { #userContent img {
max-width: 245pt; max-width: 245pt;
@ -33,6 +33,10 @@
#userContent cite::before { #userContent cite::before {
content: "— "; content: "— ";
} }
#userContent .underline {
text-decoration: underline;
}
</style> </style>
<article id="userContent" style="margin: 10px 10px 0;"> <article id="userContent" style="margin: 10px 10px 0;">

View file

@ -1,5 +1,5 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var sorting = false} {var $sorting = false}
{block title} {block title}
{_feedback} {_feedback}
@ -26,7 +26,7 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getModel(1)->getAvatarUrl()}" width=64 /> <img src="{$x->getModel(1)->getAvatarUrl('miniscule')}" width=64 />
{/block} {/block}
{block name} {block name}

View file

@ -3,7 +3,7 @@
{block title}Альбом {$album->getName()}{/block} {block title}Альбом {$album->getName()}{/block}
{block header} {block header}
{var isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)} {var $isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)}
<a href="{$album->getOwner()->getURL()}"> <a href="{$album->getOwner()->getURL()}">
{$album->getOwner()->getCanonicalName()} {$album->getOwner()->getCanonicalName()}

View file

@ -1,16 +1,35 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = iterator_to_array($albums)} {var $iterator = iterator_to_array($albums)}
{var page = $paginatorConf->page} {var $page = $paginatorConf->page}
{block title}{_"albums"} {$owner->getCanonicalName()}{/block} {block title}{_"albums"} {$owner->getCanonicalName()}{/block}
{block header} {block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> {if isset($thisUser) && $thisUser->getId() == $owner->getId()}
» {_"albums"} {_my_photos}
{else}
<div n:if="$canEdit" style="float: right;"> <a href="{$owner->getURL()}">
{var isClub = ($owner instanceof openvk\Web\Models\Entities\Club)} {$owner->getCanonicalName()}</a>
<a href="/albums/create{$isClub ? '?gpid=' . $owner->getId() : ''}">{_"create_album"}</a> »
{_albums}
{/if}
{/block}
{block size}
<div style="padding-bottom: 0px; padding-top: 0;" class="summaryBar">
<div class="summary">
{if !is_null($thisUser) && $owner->getId() === $thisUser->getId()}
{tr("albums_list", $count)}
{else}
{tr("albums", $count)}
{/if}
<span n:if="$canEdit" style="float: right;">
&nbsp;|&nbsp;
{var $isClub = ($owner instanceof \openvk\Web\Models\Entities\Club)}
<a href="/albums/create{$isClub ? '?gpid=' . $owner->getId() : ''}">{_create_album}</a>
</span>
</div>
</div> </div>
{/block} {/block}
@ -25,8 +44,8 @@
{/block} {/block}
{block preview} {block preview}
{var cover = $x->getCoverPhoto()} {var $cover = $x->getCoverPhoto()}
{var preview = is_null($cover) ? "/assets/packages/static/openvk/img/camera_200.png" : $cover->getURL()} {var $preview = is_null($cover) ? "/assets/packages/static/openvk/img/camera_200.png" : $cover->getURLBySizeId("normal")}
<a href="/album{$x->getPrettyId()}"> <a href="/album{$x->getPrettyId()}">
<img src="{$preview}" alt="{$x->getName()}" style="height: 130px; width: 170px; object-fit: cover" /> <img src="{$preview}" alt="{$x->getName()}" style="height: 130px; width: 170px; object-fit: cover" />

View file

@ -21,7 +21,7 @@
{block content} {block content}
<center style="margin-bottom: 8pt;"> <center style="margin-bottom: 8pt;">
<img src="{$photo->getURL()}" style="max-width: 80%; max-height: 60vh;" /> <img src="{$photo->getURLBySizeId('large')}" style="max-width: 80%; max-height: 60vh;" />
</center> </center>
<hr/> <hr/>

View file

@ -50,7 +50,7 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getAvatarUrl()}" width="75" alt="{_"photo"}" /> <img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="{_"photo"}" />
{/block} {/block}
{block name} {block name}

View file

@ -47,7 +47,7 @@
<tr> <tr>
{if $comment->getUType() === 0} {if $comment->getUType() === 0}
<td width="54" valign="top"> <td width="54" valign="top">
<img src="{$comment->getUser()->getAvatarUrl()}" width="50" /> <img src="{$comment->getUser()->getAvatarUrl('miniscule')}" width="50" />
</td> </td>
{else} {else}
<td width="54" valign="top"> <td width="54" valign="top">
@ -70,7 +70,7 @@
{if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
<a href="{$comment->getUser()->getURL()}"> <a href="{$comment->getUser()->getURL()}">
<span class="nobold"> <span class="nobold">
{var lastName = $comment->getUser()->getLastName()} {var $lastName = $comment->getUser()->getLastName()}
{if empty(trim($lastName))} {if empty(trim($lastName))}
({$comment->getUser()->getFirstName()}) ({$comment->getUser()->getFirstName()})
{else} {else}

View file

@ -6,9 +6,9 @@
{/block} {/block}
{block content} {block content}
{var isMain = $mode === 'faq'} {var $isMain = $mode === 'faq'}
{var isNew = $mode === 'new'} {var $isNew = $mode === 'new'}
{var isList = $mode === 'list'} {var $isList = $mode === 'list'}
{if $thisUser} {if $thisUser}
<div class="tabs"> <div class="tabs">
@ -26,16 +26,26 @@
<br /> <br />
{if $isNew} {if $isNew}
<div class="new"> {if !is_null($banReason)}
<form action="/support" method="post" style="margin:0;"> <center>
<center> <img src="/assets/packages/static/openvk/img/oof.apng" alt="{_'banned_alt'}" style="width: 20%;" />
<input name="name" style="width: 80%; resize: vertical;" placeholder="{_support_new_title}" /><br /><br /> </center>
<textarea name="text" style="width: 80%; resize: vertical;" placeholder="{_support_new_content}"></textarea><br /><br /> <p>
<input type="hidden" name="hash" value="{$csrfToken}" /> {tr("banned_in_support_1", htmlentities($thisUser->getCanonicalName()))|noescape}<br/>
<input type="submit" value="{_write}" class="button" style="margin-left: 70%;" /><br /><br /> {tr("banned_in_support_2", htmlentities($banReason))|noescape}
</center> </p>
</form> {else}
</div> <div class="new">
<form action="/support" method="post" style="margin:0;">
<center>
<input name="name" style="width: 80%; resize: vertical;" placeholder="{_support_new_title}" /><br /><br />
<textarea name="text" style="width: 80%; resize: vertical;" placeholder="{_support_new_content}"></textarea><br /><br />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_write}" class="button" style="margin-left: 70%;" /><br /><br />
</center>
</form>
</div>
{/if}
{/if} {/if}
{/if} {/if}

View file

@ -37,7 +37,7 @@
{/block} {/block}
{block description} {block description}
{var author = $x->getUser()} {var $author = $x->getUser()}
{ovk_proc_strtr($x->getContext(), 50)}<br/> {ovk_proc_strtr($x->getContext(), 50)}<br/>
<span class="nobold">{_author}: </span> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a> <span class="nobold">{_author}: </span> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a>

View file

@ -60,7 +60,7 @@
<tr> <tr>
{if $comment->getUType() === 0} {if $comment->getUType() === 0}
<td width="54" valign="top"> <td width="54" valign="top">
<img src="{$comment->getUser()->getAvatarUrl()}" width="50" /> <img src="{$comment->getUser()->getAvatarUrl('miniscule')}" width="50" />
</td> </td>
{else} {else}
<td width="54" valign="top"> <td width="54" valign="top">
@ -109,7 +109,7 @@
{if $comment->getUType() === 1} {if $comment->getUType() === 1}
<div class="post-menu"> <div class="post-menu">
{var isLikedByUser = $comment->isLikedByUser()} {var $isLikedByUser = $comment->isLikedByUser()}
<strong id="markText-{$comment->getId()}"> <strong id="markText-{$comment->getId()}">
{if !is_null($isLikedByUser)} {if !is_null($isLikedByUser)}
{if $comment->isLikedByUser()} {if $comment->isLikedByUser()}

View file

@ -1,6 +1,6 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = iterator_to_array($topics)} {var $iterator = iterator_to_array($topics)}
{var page = $paginatorConf->page} {var $page = $paginatorConf->page}
{block title}{_discussions} {$club->getCanonicalName()}{/block} {block title}{_discussions} {$club->getCanonicalName()}{/block}
@ -46,7 +46,7 @@
<div style="float: left;"> <div style="float: left;">
{tr("messages", $x->getCommentsCount())} {tr("messages", $x->getCommentsCount())}
</div> </div>
{var lastComment = $x->getLastComment()} {var $lastComment = $x->getLastComment()}
<div n:if="$lastComment" class="avatar-list-item" style="float: right;"> <div n:if="$lastComment" class="avatar-list-item" style="float: right;">
<div class="avatar"> <div class="avatar">
<a href="{$lastComment->getOwner()->getURL()}"> <a href="{$lastComment->getOwner()->getURL()}">

View file

@ -7,10 +7,10 @@
{block content} {block content}
{var isMain = $mode === 'main'} {var $isMain = $mode === 'main'}
{var isContacts = $mode === 'contacts'} {var $isContacts = $mode === 'contacts'}
{var isInterests = $mode === 'interests'} {var $isInterests = $mode === 'interests'}
{var isAvatar = $mode === 'avatar'} {var $isAvatar = $mode === 'avatar'}
<div n:if="$user->hasPendingNumberChange()" class="msg"> <div n:if="$user->hasPendingNumberChange()" class="msg">
<b>Подтверждение номера телефона</b><br/> <b>Подтверждение номера телефона</b><br/>
Введите код для подтверждения смены номера: <a href="/edit/verify_phone">ввести код</a>. Введите код для подтверждения смены номера: <a href="/edit/verify_phone">ввести код</a>.

View file

@ -1,17 +1,17 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var perPage = 6} {* Why 6? Check User::_abstractRelationGenerator *} {var $perPage = 6} {* Why 6? Check User::_abstractRelationGenerator *}
{var act = $_GET["act"] ?? "friends"} {var $act = $_GET["act"] ?? "friends"}
{if $act == "incoming"} {if $act == "incoming"}
{var iterator = iterator_to_array($user->getFollowers($page))} {var $iterator = iterator_to_array($user->getFollowers($page))}
{var count = $user->getFollowersCount()} {var $count = $user->getFollowersCount()}
{elseif $act == "outcoming"} {elseif $act == "outcoming"}
{var iterator = iterator_to_array($user->getSubscriptions($page))} {var $iterator = iterator_to_array($user->getSubscriptions($page))}
{var count = $user->getSubscriptionsCount()} {var $count = $user->getSubscriptionsCount()}
{else} {else}
{var iterator = iterator_to_array($user->getFriends($page))} {var $iterator = iterator_to_array($user->getFriends($page))}
{var count = $user->getFriendsCount()} {var $count = $user->getFriendsCount()}
{/if} {/if}
{block title} {block title}
@ -25,13 +25,17 @@
{/block} {/block}
{block header} {block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> » {if isset($thisUser) && $thisUser->getId() == $user->getId()}
{if $act == "incoming"} {_my_friends}
{_"incoming_req"}
{elseif $act == "outcoming"}
{_"outcoming_req"}
{else} {else}
{_"friends"} <a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
{if $act == "incoming"}
{_"incoming_req"}
{elseif $act == "outcoming"}
{_"outcoming_req"}
{else}
{_"friends"}
{/if}
{/if} {/if}
{/block} {/block}
@ -39,11 +43,38 @@
<div n:attr="id => ($act === 'friends' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'friends' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'friends' ? 'act_tab_a' : 'ki')" href="?">{_friends}</a> <a n:attr="id => ($act === 'friends' ? 'act_tab_a' : 'ki')" href="?">{_friends}</a>
</div> </div>
<div n:attr="id => ($act === 'incoming' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'incoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_incoming_req}</a> <a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a>
</div> </div>
<div n:attr="id => ($act === 'outcoming' ? 'activetabs' : 'ki')" class="tab"> {/block}
<a n:attr="id => ($act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=outcoming">{_outcoming_req}</a>
{block size}
<div n:if="$act === 'incoming' || $act === 'outcoming'" class="mb_tabs">
<div n:attr="id => ($act === 'incoming' ? 'active' : 'ki')" class="mb_tab">
<div>
<a href="?act=incoming">{_incoming_req}</a>
</div>
</div>
<div n:attr="id => ($act === 'outcoming' ? 'active' : 'ki')" class="mb_tab">
<div>
<a href="?act=outcoming">{_outcoming_req}</a>
</div>
</div>
</div>
<div style="padding-bottom: 0px;" class="summaryBar">
<div class="summary">
{if !is_null($thisUser) && $user->getId() === $thisUser->getId()}
{if $act == "incoming"}
{tr("req", $count)}
{elseif $act == "outcoming"}
{tr("req", $count)}
{else}
{tr("friends_list", $count)}
{/if}
{else}
{tr("friends", $count)}
{/if}
</div>
</div> </div>
{/block} {/block}
@ -54,7 +85,7 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getAvatarUrl()}" width="75" alt="Фотография группы" /> <img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография пользователя" />
{/block} {/block}
{block name} {block name}
@ -82,7 +113,7 @@
{block actions} {block actions}
{if $x->getId() !== $thisUser->getId()} {if $x->getId() !== $thisUser->getId()}
{var subStatus = $x->getSubscriptionStatus($thisUser)} {var $subStatus = $x->getSubscriptionStatus($thisUser)}
{if $subStatus === 0} {if $subStatus === 0}
<form action="/setSub/user" method="post" class="profile_link_form"> <form action="/setSub/user" method="post" class="profile_link_form">
<input type="hidden" name="act" value="add" /> <input type="hidden" name="act" value="add" />

View file

@ -1,6 +1,6 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = $user->getClubs($page, $admin)} {var $iterator = $user->getClubs($page, $admin)}
{var count = $user->getClubCount($admin)} {var $count = $user->getClubCount($admin)}
{block title} {block title}
{_groups} {_groups}
@ -32,7 +32,7 @@
{/block} {/block}
{block size} {block size}
<div n:if="!is_null($thisUser) && $user->getId() === $thisUser->getId()" style="padding-bottom: 0px; border-bottom: 0;" class="summaryBar"> <div style="padding-bottom: 0px;" class="summaryBar">
<div class="summary"> <div class="summary">
{if !is_null($thisUser) && $user->getId() === $thisUser->getId()} {if !is_null($thisUser) && $user->getId() === $thisUser->getId()}
{tr("groups_list", $thisUser->getClubCount())} {tr("groups_list", $thisUser->getClubCount())}
@ -48,12 +48,12 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getAvatarUrl()}" width="75" alt="Фотография группы" /> <img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография группы" />
{/block} {/block}
{block name}{/block} {block name}{/block}
{block infoTable} {block infotable}
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0"> <table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0">
<tbody> <tbody>
<tr> <tr>
@ -73,26 +73,28 @@
{/block} {/block}
{block actions} {block actions}
{var clubPinned = $thisUser->isClubPinned($x)} {var $clubPinned = $thisUser->isClubPinned($x)}
{if $x->canBeModifiedBy($thisUser ?? NULL)} {if $x->canBeModifiedBy($thisUser ?? NULL)}
<a style="width: 140px;" class="profile_link" href="{$x->getURL()}"> <div class="navigation" style="width: 140px;">
{_check_community} <a class="link" href="{$x->getURL()}">
</a> {_check_community}
{if ($clubPinned || $thisUser->getPinnedClubCount() <= 10)}
<a style="width: 140px;" class="profile_link" href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
{if $clubPinned}
{_remove_from_left_menu}
{else}
{_add_to_left_menu}
{/if}
</a> </a>
{/if} {if ($clubPinned || $thisUser->getPinnedClubCount() <= 10)}
<form action="/setSub/club" method="post"> <a class="link" href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
<input type="hidden" name="act" value="rem" /> {if $clubPinned}
<input type="hidden" name="id" value="{$x->getId()}" /> {_remove_from_left_menu}
<input type="hidden" name="hash" value="{$csrfToken}" /> {else}
<input style="width: 140px; text-transform: lowercase;" type="submit" id="profile_link" value="{_leave_community}" /> {_add_to_left_menu}
</form> {/if}
</a>
{/if}
<form action="/setSub/club" method="post">
<input type="hidden" name="act" value="rem" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input style="text-transform: lowercase; width: 100%;" class="link" type="submit" value="{_"leave_community"}" />
</form>
</div>
{/if} {/if}
{/block} {/block}

View file

@ -7,11 +7,11 @@
{block content} {block content}
{var isMain = $mode === 'main'} {var $isMain = $mode === 'main'}
{var isPrivacy = $mode === 'privacy'} {var $isPrivacy = $mode === 'privacy'}
{var isFinance = $mode === 'finance'} {var $isFinance = $mode === 'finance'}
{var isFinanceTU = $mode === 'finance.top-up'} {var $isFinanceTU = $mode === 'finance.top-up'}
{var isInterface = $mode === 'interface'} {var $isInterface = $mode === 'interface'}
<div class="tabs"> <div class="tabs">
<div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab">
@ -347,16 +347,22 @@
<center>{tr("also_you_can_transfer_points", $thisUser->getCoins(), rawurlencode($csrfToken))|noescape}</center> <center>{tr("also_you_can_transfer_points", $thisUser->getCoins(), rawurlencode($csrfToken))|noescape}</center>
</div> </div>
<div style="width: 22%; float: right;"> <div style="width: 22%; float: right;">
<p style="margin: 0; font-size: medium; text-align: center;"> <div style="margin: 0; font-size: medium; text-align: center; font-weight: 900;">
<b> {_on_your_account}
{_on_your_account}<br/> <div style="width: 100%; height: 60px; font-weight: 100;" id="balance">{$thisUser->getCoins()}</div>
<span style="font-size: 50px;">{$thisUser->getCoins()}</span><br/> {_points_count}<br/>
{_points_count}<br/><br/> <small><a href="?act=finance.top-up">[{_have_voucher}?]</a></small>
<small><a href="?act=finance.top-up">[{_have_voucher}?]</a></small>
</b>
</p> </p>
</div> </div>
{script "js/node_modules/textfit/textFit.min.js"}
<script>
let balance = document.querySelector("#balance");
balance.style.width = Math.ceil(balance.parentNode.getBoundingClientRect().width) + "px";
textFit(balance, { alignVert: true });
</script>
{elseif $isFinanceTU} {elseif $isFinanceTU}
<p>{_voucher_explanation} {_voucher_explanation_ex}</p> <p>{_voucher_explanation} {_voucher_explanation_ex}</p>
@ -407,7 +413,7 @@
</td> </td>
<td> <td>
<select name="style_avatar"> <select name="style_avatar">
<option value="0" {if $user->getStyleAvatar() == 0}selected{/if}>{_"default"}</option> <option value="0" {if $user->getStyleAvatar() == 0}selected{/if}>{_"arbitrary_avatars"} ({_"default"})</option>
<option value="1" {if $user->getStyleAvatar() == 1}selected{/if}>{_"cut"}</option> <option value="1" {if $user->getStyleAvatar() == 1}selected{/if}>{_"cut"}</option>
<option value="2" {if $user->getStyleAvatar() == 2}selected{/if}>{_"round_avatars"}</option> <option value="2" {if $user->getStyleAvatar() == 2}selected{/if}>{_"round_avatars"}</option>
</select> </select>

View file

@ -7,7 +7,7 @@
<!-- openGraph --> <!-- openGraph -->
<meta property="og:title" content="{$user->getCanonicalName()}" /> <meta property="og:title" content="{$user->getCanonicalName()}" />
<meta property="og:url" content="http://{$_SERVER['HTTP_HOST']}{$user->getURL()}" /> <meta property="og:url" content="http://{$_SERVER['HTTP_HOST']}{$user->getURL()}" />
<meta property="og:image" content="{$user->getAvatarUrl()}" /> <meta property="og:image" content="{$user->getAvatarUrl('normal')}" />
<meta property="og:type" content="profile" /> <meta property="og:type" content="profile" />
<meta property="og:first_name" content="{$user->getFirstName()}" /> <meta property="og:first_name" content="{$user->getFirstName()}" />
<meta property="og:last_name" content="{$user->getLastName()}" /> <meta property="og:last_name" content="{$user->getLastName()}" />
@ -62,7 +62,7 @@
<div class="left_small_block"> <div class="left_small_block">
<div> <div>
<a href="{$user->getAvatarLink()|nocheck}"> <a href="{$user->getAvatarLink()|nocheck}">
<img src="{$user->getAvatarUrl()}" <img src="{$user->getAvatarUrl('normal')}"
alt="{$user->getCanonicalName()}" alt="{$user->getCanonicalName()}"
style="width: 100%; image-rendering: -webkit-optimize-contrast;" /> style="width: 100%; image-rendering: -webkit-optimize-contrast;" />
</a> </a>
@ -72,7 +72,7 @@
<div id="profile_link" style="width: 194px;"> <div id="profile_link" style="width: 194px;">
<a href="/edit" class="link">{_"edit_page"}</a> <a href="/edit" class="link">{_"edit_page"}</a>
</div> </div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce']" id="profile_link" style="width: 194px;"> <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && !$thisUser->prefersNotToSeeRating()" id="profile_link" style="width: 194px;">
<a onClick="showIncreaseRatingDialog({$thisUser->getCoins()}, {ltrim($thisUser->getUrl(), '/')}, {$csrfToken})" class="link">{_increase_rating}</a> <a onClick="showIncreaseRatingDialog({$thisUser->getCoins()}, {ltrim($thisUser->getUrl(), '/')}, {$csrfToken})" class="link">{_increase_rating}</a>
</div> </div>
{else} {else}
@ -93,11 +93,21 @@
{_warn_user_action} {_warn_user_action}
</a> </a>
{/if} {/if}
{if $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
<a href="javascript:toggleBanInSupport()" class="profile_link">
{if $user->isBannedInSupport()}
{_unban_in_support_user_action}
{else}
{_ban_in_support_user_action}
{/if}
</a>
{/if}
<a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $user->getGiftCount() == 0" href="/gifts?act=pick&user={$user->getId()}" class="profile_link">{_send_gift}</a> <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $user->getGiftCount() == 0" href="/gifts?act=pick&user={$user->getId()}" class="profile_link">{_send_gift}</a>
<a n:if="$user->getPrivacyPermission('messages.write', $thisUser)" href="/im?sel={$user->getId()}" class="profile_link">{_"send_message"}</a> <a n:if="$user->getPrivacyPermission('messages.write', $thisUser)" href="/im?sel={$user->getId()}" class="profile_link">{_"send_message"}</a>
{var subStatus = $user->getSubscriptionStatus($thisUser)} {var $subStatus = $user->getSubscriptionStatus($thisUser)}
{if $subStatus === 0} {if $subStatus === 0}
<form action="/setSub/user" method="post" class="profile_link_form"> <form action="/setSub/user" method="post" class="profile_link_form">
<input type="hidden" name="act" value="add" /> <input type="hidden" name="act" value="add" />
@ -131,7 +141,7 @@
<a n:if="$user->getFollowersCount() > 0" href="/friends{$user->getId()}?act=incoming" class="profile_link">{tr("followers", $user->getFollowersCount())}</a> <a n:if="$user->getFollowersCount() > 0" href="/friends{$user->getId()}?act=incoming" class="profile_link">{tr("followers", $user->getFollowersCount())}</a>
</div> </div>
<div n:if="isset($thisUser) && !$thisUser->prefersNotToSeeRating()" class="profile-hints"> <div n:if="isset($thisUser) && !$thisUser->prefersNotToSeeRating()" class="profile-hints">
{var completeness = $user->getProfileCompletenessReport()} {var $completeness = $user->getProfileCompletenessReport()}
<div n:class="completeness-gauge, $completeness->total >= 100 ? completeness-gauge-gold"> <div n:class="completeness-gauge, $completeness->total >= 100 ? completeness-gauge-gold">
<div style="width: {$completeness->percent}%"></div> <div style="width: {$completeness->percent}%"></div>
@ -164,7 +174,7 @@
</div> </div>
<br /> <br />
<div n:if="$user->getFriendsCount() > 0 && $user->getPrivacyPermission('friends.read', $thisUser ?? NULL)"> <div n:if="$user->getFriendsCount() > 0 && $user->getPrivacyPermission('friends.read', $thisUser ?? NULL)">
{var friendCount = $user->getFriendsCount()} {var $friendCount = $user->getFriendsCount()}
<div class="content_title_expanded" onclick="hidePanel(this, {$friendCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$friendCount});">
{_"friends"} {_"friends"}
@ -180,7 +190,7 @@
<div class="cl_element" n:foreach="$user->getFriends(1) as $friend"> <div class="cl_element" n:foreach="$user->getFriends(1) as $friend">
<div class="cl_avatar"> <div class="cl_avatar">
<a href="{$friend->getURL()}"> <a href="{$friend->getURL()}">
<img class="ava" src="{$friend->getAvatarUrl()}" /> <img class="ava" src="{$friend->getAvatarUrl('miniscule')}" />
</a> </a>
</div> </div>
<a href="{$friend->getURL()}" class="cl_name"> <a href="{$friend->getURL()}" class="cl_name">
@ -205,10 +215,10 @@
<div style="padding: 5px;"> <div style="padding: 5px;">
<div class="ovk-album" style="display: inline-block;" n:foreach="$albums as $album"> <div class="ovk-album" style="display: inline-block;" n:foreach="$albums as $album">
<div style="text-align: center;float: left;height: 54pt;width: 100px;"> <div style="text-align: center;float: left;height: 54pt;width: 100px;">
{var cover = $album->getCoverPhoto()} {var $cover = $album->getCoverPhoto()}
<img <img
src="{is_null($cover)?'/assets/packages/static/openvk/img/camera_200.png':$cover->getURL()}" src="{is_null($cover)?'/assets/packages/static/openvk/img/camera_200.png':$cover->getURLBySizeId('small')}"
style="max-width: 80px; max-height: 54pt;" /> style="max-width: 80px; max-height: 54pt;" />
</div> </div>
<div style="overflow: hidden; overflow-wrap: break-word;"> <div style="overflow: hidden; overflow-wrap: break-word;">
@ -274,7 +284,7 @@
</div> </div>
</div> </div>
<div n:if="$user->getClubCount() > 0 && $user->getPrivacyPermission('groups.read', $thisUser ?? NULL)"> <div n:if="$user->getClubCount() > 0 && $user->getPrivacyPermission('groups.read', $thisUser ?? NULL)">
{var clubsCount = $user->getClubCount()} {var $clubsCount = $user->getClubCount()}
<div class="content_title_expanded" onclick="hidePanel(this, {$clubsCount})"> <div class="content_title_expanded" onclick="hidePanel(this, {$clubsCount})">
{_"groups"} {_"groups"}
</div> </div>
@ -293,7 +303,7 @@
</div> </div>
</div> </div>
<div n:if="$user->getMeetingCount() > 0 && $user->getPrivacyPermission('groups.read', $thisUser ?? NULL)"> <div n:if="$user->getMeetingCount() > 0 && $user->getPrivacyPermission('groups.read', $thisUser ?? NULL)">
{var meetingCount = $user->getMeetingCount()} {var $meetingCount = $user->getMeetingCount()}
<div class="content_title_expanded" onclick="hidePanel(this, {$meetingCount})"> <div class="content_title_expanded" onclick="hidePanel(this, {$meetingCount})">
{_meetings} {_meetings}
</div> </div>
@ -317,7 +327,7 @@
<div class="right_big_block"> <div class="right_big_block">
<div class="page_info"> <div class="page_info">
<div n:if="!is_null($alert = $user->getAlert())" class="user-alert">{strpos($alert, "@") === 0 ? tr(substr($alert, 1)) : $alert}</div> <div n:if="!is_null($alert = $user->getAlert())" class="user-alert">{strpos($alert, "@") === 0 ? tr(substr($alert, 1)) : $alert}</div>
{var thatIsThisUser = isset($thisUser) && $user->getId() == $thisUser->getId()} {var $thatIsThisUser = isset($thisUser) && $user->getId() == $thisUser->getId()}
<div n:if="$thatIsThisUser" class="page_status_popup" id="status_editor" style="display: none;"> <div n:if="$thatIsThisUser" class="page_status_popup" id="status_editor" style="display: none;">
<form name="status_popup_form" onsubmit="changeStatus(); return false;"> <form name="status_popup_form" onsubmit="changeStatus(); return false;">
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px;">
@ -485,7 +495,7 @@
</div> </div>
<div class="content_list long"> <div class="content_list long">
<div class="cl_element" style="width: 25%;" n:foreach="$user->getGifts(1, 4) as $giftDescriptor"> <div class="cl_element" style="width: 25%;" n:foreach="$user->getGifts(1, 4) as $giftDescriptor">
{var hideInfo = !is_null($thisUser) ? ($giftDescriptor->anon ? $thisUser->getId() !== $user->getId() : false) : false} {var $hideInfo = !is_null($thisUser) ? ($giftDescriptor->anon ? $thisUser->getId() !== $user->getId() : false) : false}
<div class="cl_avatar"> <div class="cl_avatar">
<a href="{$hideInfo ? 'javascript:false' : $giftDescriptor->sender->getURL()}"> <a href="{$hideInfo ? 'javascript:false' : $giftDescriptor->sender->getURL()}">
<img style="width: 70px; max-height: 70px;" <img style="width: 70px; max-height: 70px;"
@ -512,7 +522,7 @@
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&hash=" + {rawurlencode($csrfToken)}, true); xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() { xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1) if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось забанить пользователя...", ["OK"], [Function.noop]); MessageBox("Ошибка", "Не удалось забанить пользователя...", ["OK"], [Function.noop]);
else else
MessageBox("Операция успешна", "Пользователь заблокирован", ["OK"], [Function.noop]); MessageBox("Операция успешна", "Пользователь заблокирован", ["OK"], [Function.noop]);
@ -526,7 +536,7 @@
function warnUser() { function warnUser() {
uBanMsgTxt = "Вы собираетесь предупредить пользователя " + {$user->getCanonicalName()} + "."; uBanMsgTxt = "Вы собираетесь предупредить пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/>Мы отправим уведомление пользователю в личные сообщения от имени аккаунта администратора."; uBanMsgTxt += "<br/>Мы отправим уведомление пользователю в личные сообщения от имени аккаунта администратора.";
uBanMsgTxt += "<br/><br/><b>Текст предупреждения</b>: <input type='text' id='uWarnMsgInput' placeholder='придумайте что-нибудь крутое' />" uBanMsgTxt += "<br/><br/><b>Текст предупреждения</b>: <input type='text' id='uWarnMsgInput' placeholder='придумайте что-нибудь крутое' />";
MessageBox("Выдать предупреждение " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [ MessageBox("Выдать предупреждение " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() { (function() {
@ -546,6 +556,51 @@
} }
</script> </script>
<script n:if="isset($thisUser) && $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)">
{if $user->isBannedInSupport()}
function toggleBanInSupport() {
uBanMsgTxt = "Вы собираетесь разблокировать в поддержке пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/>Сейчас он заблокирован по причине <strong>" + {$user->getBanInSupportReason()} + "</strong>.";
MessageBox("Разблокировать в поддержке " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/support/unban/" + {$user->getId()} + "?hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось разблокировать пользователя в поддержке...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Пользователь разблокирован в поддержке", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
{else}
function toggleBanInSupport() {
uBanMsgTxt = "Вы собираетесь заблокировать в поддержке пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/><br/><b>Причина бана</b>: <input type='text' id='uBanMsgInput' placeholder='придумайте что-нибудь крутое' />";
MessageBox("Заблокировать в поддержке " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uBanMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/support/ban/" + {$user->getId()} + "?reason=" + res + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось заблокировать пользователя в поддержке...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Пользователь заблокирован в поддержке", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
{/if}
</script>
<script n:if="isset($thisUser) && $user->getId() == $thisUser->getId()" n:syntax="off"> <script n:if="isset($thisUser) && $user->getId() == $thisUser->getId()" n:syntax="off">
function setStatusEditorShown(shown) { function setStatusEditorShown(shown) {
document.getElementById("status_editor").style.display = shown ? "block" : "none"; document.getElementById("status_editor").style.display = shown ? "block" : "none";

View file

@ -4,4 +4,86 @@
{tr("user_banned", htmlentities($user->getFirstName()))|noescape}<br/> {tr("user_banned", htmlentities($user->getFirstName()))|noescape}<br/>
{_"user_banned_comment"} <b>{$user->getBanReason()}</b>. {_"user_banned_comment"} <b>{$user->getBanReason()}</b>.
</p> </p>
{if isset($thisUser)}
<p n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) || $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)">
<br />
<a n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL)" href="javascript:unbanUser()" class="button">{_unban_user_action}</a>
<a n:if="$thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)" href="javascript:toggleBanInSupport()" class="button">
{if $user->isBannedInSupport()}
{_unban_in_support_user_action}
{else}
{_ban_in_support_user_action}
{/if}
</a>
</p>
{/if}
</center> </center>
{if isset($thisUser)}
<script n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL)">
function unbanUser() {
uUnbanMsgTxt = "Вы собираетесь разбанить пользователя " + {$user->getCanonicalName()} + ".";
uUnbanMsgTxt += "<br/>Сейчас он заблокирован по причине: <strong>" + {$user->getBanReason()} + "</strong>.";
MessageBox("Разбанить " + {$user->getFirstName()}, uUnbanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/unban/" + {$user->getId()} + "?hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось разблокировать пользователя...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Пользователь разблокирован", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
<script n:if="$thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)">
{if $user->isBannedInSupport()}
function toggleBanInSupport() {
uBanMsgTxt = "Вы собираетесь разблокировать в поддержке пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/>Сейчас он заблокирован по причине <strong>" + {$user->getBanInSupportReason()} + "</strong>.";
MessageBox("Разблокировать в поддержке " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/support/unban/" + {$user->getId()} + "?hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось разблокировать пользователя в поддержке...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Пользователь разблокирован в поддержке", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
{else}
function toggleBanInSupport() {
uBanMsgTxt = "Вы собираетесь заблокировать в поддержке пользователя " + {$user->getCanonicalName()} + ".";
uBanMsgTxt += "<br/><br/><b>Причина бана</b>: <input type='text' id='uBanMsgInput' placeholder='придумайте что-нибудь крутое' />";
MessageBox("Заблокировать в поддержке " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uBanMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/support/ban/" + {$user->getId()} + "?reason=" + res + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось заблокировать пользователя в поддержке...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Пользователь заблокирован в поддержке", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
{/if}
</script>
{/if}

View file

@ -0,0 +1,14 @@
{extends "../@layout.xml"}
{block title}{_information}{/block}
{block header}
{_information}
{/block}
{block content}
<center style="background: white;border: #DEDEDE solid 1px;">
<span style="color: #707070;margin: 60px 0;display: block;">
{_profile_not_found_text}
</span>
</center>
{/block}

View file

@ -1,7 +1,7 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = $videos} {var $iterator = $videos}
{var count = $paginatorConf->count} {var $count = $paginatorConf->count}
{var page = $paginatorConf->page} {var $page = $paginatorConf->page}
{block title}{_"videos"} {$user->getCanonicalName()}{/block} {block title}{_"videos"} {$user->getCanonicalName()}{/block}
@ -11,13 +11,13 @@
{/block} {/block}
{block size} {block size}
<div style="padding-bottom: 0px;border-bottom: 0; padding-top: 0;" class="summaryBar"> <div style="padding-bottom: 0px; padding-top: 0;" class="summaryBar">
<div class="summary"> <div class="summary">
{tr("videos", $count)} {tr("videos", $count)}
<span n:if="isset($thisUser) && $thisUser->getId() == $user->getId()"> <span n:if="isset($thisUser) && $thisUser->getId() == $user->getId()">
&nbsp;|&nbsp; &nbsp;|&nbsp;
<a href="/videos/upload">{_upload_video}</a> <a href="/videos/upload">{_upload_video}</a>
</span> </span>
</div> </div>
</div> </div>
{/block} {/block}
@ -33,9 +33,11 @@
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getThumbnailURL()}" <div class="video-preview">
alt="{$x->getName()}" <img src="{$x->getThumbnailURL()}"
style="max-height: 43pt; max-width: 140px; height: unset; width: unset; border-radius: unset;" /> alt="{$x->getName()}"
style="max-width: 170px; max-height: 127px; margin: auto;" />
</div>
{/block} {/block}
{block name} {block name}
@ -43,7 +45,13 @@
{/block} {/block}
{block description} {block description}
<span>{$x->getDescription() ?? ""}</span><br/> <p>
<span>{$x->getDescription() ?? ""}</span>
</p>
<span style="color: grey;">{_"video_uploaded"} {$x->getPublicationTime()}</span><br/> <span style="color: grey;">{_"video_uploaded"} {$x->getPublicationTime()}</span><br/>
<span style="color: grey;">{_"video_updated"} {$x->getEditTime() ?? $x->getPublicationTime()}</span> <span style="color: grey;">{_"video_updated"} {$x->getEditTime() ?? $x->getPublicationTime()}</span>
<p>
<a href="/video{$x->getPrettyId()}">{_view_video}</a>
{if $x->getCommentsCount() > 0}| <a href="/video{$x->getPrettyId()}#comments">{_"comments"} ({$x->getCommentsCount()})</a>{/if}
</p>
{/block} {/block}

View file

@ -15,7 +15,7 @@
{if $video->getType() === 0} {if $video->getType() === 0}
<video width="610" src="{$video->getURL()}" controls></video> <video width="610" src="{$video->getURL()}" controls></video>
{else} {else}
{var driver = $video->getVideoDriver()} {var $driver = $video->getVideoDriver()}
{if !$driver} {if !$driver}
Эта видеозапись не поддерживается в вашей версии OpenVK. Эта видеозапись не поддерживается в вашей версии OpenVK.
{else} {else}

View file

@ -27,10 +27,9 @@
<div style="float: left; min-height: 100px; width: 32%;"> <div style="float: left; min-height: 100px; width: 32%;">
<h4>{_actions}</h4> <h4>{_actions}</h4>
{if isset($thisUser)} {if isset($thisUser)}
{var canDelete = $post->canBeDeletedBy($thisUser)} {var $canDelete = $post->canBeDeletedBy($thisUser)}
{/if} {/if}
<a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a> <a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a>
<a class="profile_link" style="display:block;width:96%;" href="/report.pl/{$post->getId()}?type=post">{_report}</a>
</div> </div>
{/block} {/block}

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